@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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateKeypair,
|
|
3
|
+
openSealedKey,
|
|
4
|
+
publicKeyFingerprint
|
|
5
|
+
} from "./chunk-3EGEA4KH.js";
|
|
6
|
+
import {
|
|
7
|
+
getBaseUrl
|
|
8
|
+
} from "./chunk-2UMNXADU.js";
|
|
9
|
+
|
|
10
|
+
// src/cli/login-ui.ts
|
|
11
|
+
import { hostname } from "os";
|
|
12
|
+
|
|
13
|
+
// src/cli/browser.ts
|
|
14
|
+
import { spawn } from "child_process";
|
|
15
|
+
var command = () => {
|
|
16
|
+
if (process.platform === "darwin") return { bin: "open", args: [] };
|
|
17
|
+
if (process.platform === "win32") return { bin: "cmd", args: ["/c", "start", ""] };
|
|
18
|
+
if (process.platform === "linux") return { bin: "xdg-open", args: [] };
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
var canOpenBrowser = () => {
|
|
22
|
+
if (process.env.PUSHARY_NO_BROWSER === "1") return false;
|
|
23
|
+
if (process.env.CI) return false;
|
|
24
|
+
if (process.env.SSH_TTY) return false;
|
|
25
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
|
26
|
+
return command() !== null;
|
|
27
|
+
};
|
|
28
|
+
var openInBrowser = (url) => {
|
|
29
|
+
const resolved = command();
|
|
30
|
+
if (!resolved) return false;
|
|
31
|
+
try {
|
|
32
|
+
const child = spawn(resolved.bin, [...resolved.args, url], {
|
|
33
|
+
stdio: "ignore",
|
|
34
|
+
detached: true,
|
|
35
|
+
windowsHide: true
|
|
36
|
+
});
|
|
37
|
+
child.unref();
|
|
38
|
+
child.on("error", () => {
|
|
39
|
+
});
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/cli/login.ts
|
|
47
|
+
var START_PATH = "/api/cli/auth/start";
|
|
48
|
+
var CLAIM_PATH = "/api/cli/auth/claim";
|
|
49
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
50
|
+
var post = async (url, body, fetchImpl) => fetchImpl(url, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
55
|
+
});
|
|
56
|
+
var asString = (value) => typeof value === "string" && value ? value : null;
|
|
57
|
+
var asNumber = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
58
|
+
var startLogin = async (machineLabel, cliVersion, deps = {}) => {
|
|
59
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
60
|
+
const baseUrl = deps.baseUrl ?? getBaseUrl();
|
|
61
|
+
const keypair = generateKeypair();
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = await post(
|
|
65
|
+
`${baseUrl}${START_PATH}`,
|
|
66
|
+
{ cliPublicKey: keypair.publicKeyB64, machineLabel, cliVersion },
|
|
67
|
+
fetchImpl
|
|
68
|
+
);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return { kind: "unavailable", detail: err instanceof Error ? err.message : "network error" };
|
|
71
|
+
}
|
|
72
|
+
if (response.status === 429) {
|
|
73
|
+
const retry = Number(response.headers.get("retry-after") ?? "60");
|
|
74
|
+
return { kind: "rate-limited", retryAfterSeconds: Number.isFinite(retry) ? retry : 60 };
|
|
75
|
+
}
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
return { kind: "unavailable", detail: `HTTP ${response.status}` };
|
|
78
|
+
}
|
|
79
|
+
const body = await response.json().catch(() => null);
|
|
80
|
+
const loginId = body ? asString(body.loginId) : null;
|
|
81
|
+
const userCode = body ? asString(body.userCode) : null;
|
|
82
|
+
if (!body || !loginId || !userCode) {
|
|
83
|
+
return { kind: "unavailable", detail: "the server did not return a login code" };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
kind: "ok",
|
|
87
|
+
start: {
|
|
88
|
+
loginId,
|
|
89
|
+
userCode,
|
|
90
|
+
verificationUrl: asString(body.verificationUrl) ?? `${baseUrl}/cli/authorize`,
|
|
91
|
+
// The cadence comes from the server so a later change needs no CLI release.
|
|
92
|
+
intervalMs: Math.max(1, asNumber(body.interval, 2)) * 1e3,
|
|
93
|
+
expiresInSeconds: Math.max(1, asNumber(body.expiresInSeconds, 900)),
|
|
94
|
+
fingerprint: asString(body.fingerprint) ?? publicKeyFingerprint(keypair.publicKeyB64),
|
|
95
|
+
keypair
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
var pollOnce = async (start, deps = {}) => {
|
|
100
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
101
|
+
const baseUrl = deps.baseUrl ?? getBaseUrl();
|
|
102
|
+
let response;
|
|
103
|
+
try {
|
|
104
|
+
response = await fetchImpl(`${baseUrl}${CLAIM_PATH}?id=${encodeURIComponent(start.loginId)}`, {
|
|
105
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { kind: "unreachable", detail: err instanceof Error ? err.message : "network error" };
|
|
109
|
+
}
|
|
110
|
+
if (!response.ok) return { kind: "unreachable", detail: `HTTP ${response.status}` };
|
|
111
|
+
const body = await response.json().catch(() => null);
|
|
112
|
+
if (!body) return { kind: "unreachable", detail: "unreadable response" };
|
|
113
|
+
switch (body.status) {
|
|
114
|
+
case "authorized": {
|
|
115
|
+
const sealed = asString(body.sealed);
|
|
116
|
+
if (!sealed) return { kind: "unreachable", detail: "authorized without a sealed key" };
|
|
117
|
+
try {
|
|
118
|
+
return {
|
|
119
|
+
kind: "authorized",
|
|
120
|
+
apiKey: openSealedKey(sealed, start.keypair),
|
|
121
|
+
siteSlug: asString(body.siteSlug) ?? "",
|
|
122
|
+
keyName: asString(body.keyName) ?? "CLI"
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
return { kind: "unreachable", detail: "the key did not decrypt" };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
case "payment_required":
|
|
129
|
+
return {
|
|
130
|
+
kind: "payment-required",
|
|
131
|
+
checkoutUrl: asString(body.checkoutUrl) ?? `${baseUrl}/dashboard/settings?tab=billing`,
|
|
132
|
+
plan: asString(body.plan) ?? "agent"
|
|
133
|
+
};
|
|
134
|
+
case "rejected":
|
|
135
|
+
return { kind: "rejected" };
|
|
136
|
+
case "expired":
|
|
137
|
+
return { kind: "expired" };
|
|
138
|
+
case "needs_workspace":
|
|
139
|
+
return { kind: "needs-workspace" };
|
|
140
|
+
default:
|
|
141
|
+
return { kind: "pending", expiresInSeconds: asNumber(body.expiresInSeconds, 0) };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var waitForLogin = async (start, handlers = {}, deps = {}) => {
|
|
145
|
+
const now = deps.now ?? Date.now;
|
|
146
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
147
|
+
const deadline = now() + start.expiresInSeconds * 1e3;
|
|
148
|
+
let polls = 0;
|
|
149
|
+
let consecutiveFailures = 0;
|
|
150
|
+
const noticed = /* @__PURE__ */ new Set();
|
|
151
|
+
while (now() < deadline) {
|
|
152
|
+
const state = await pollOnce(start, deps);
|
|
153
|
+
polls += 1;
|
|
154
|
+
if (state.kind === "authorized" || state.kind === "rejected" || state.kind === "expired") {
|
|
155
|
+
return { state, polls };
|
|
156
|
+
}
|
|
157
|
+
if (state.kind === "unreachable") {
|
|
158
|
+
consecutiveFailures += 1;
|
|
159
|
+
if (consecutiveFailures === 3 && !noticed.has("unreachable")) {
|
|
160
|
+
noticed.add("unreachable");
|
|
161
|
+
handlers.onNotice?.(state);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
consecutiveFailures = 0;
|
|
165
|
+
if ((state.kind === "needs-workspace" || state.kind === "payment-required") && !noticed.has(state.kind)) {
|
|
166
|
+
noticed.add(state.kind);
|
|
167
|
+
handlers.onNotice?.(state);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
handlers.onTick?.(state, Math.max(0, Math.ceil((deadline - now()) / 1e3)));
|
|
171
|
+
await sleep(start.intervalMs);
|
|
172
|
+
}
|
|
173
|
+
return { state: { kind: "expired" }, polls };
|
|
174
|
+
};
|
|
175
|
+
var cancelLogin = async (start, deps = {}) => {
|
|
176
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
177
|
+
const baseUrl = deps.baseUrl ?? getBaseUrl();
|
|
178
|
+
try {
|
|
179
|
+
await fetchImpl(`${baseUrl}${START_PATH}`, {
|
|
180
|
+
method: "DELETE",
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify({ loginId: start.loginId, cliPublicKey: start.keypair.publicKeyB64 }),
|
|
183
|
+
signal: AbortSignal.timeout(5e3)
|
|
184
|
+
});
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// src/cli/login-ui.ts
|
|
190
|
+
var runBrowserLogin = async (ui, options, deps = {}) => {
|
|
191
|
+
const started = await startLogin(hostname(), options.cliVersion, deps);
|
|
192
|
+
if (started.kind === "rate-limited") {
|
|
193
|
+
return { kind: "unavailable", detail: `too many attempts, retry in ${started.retryAfterSeconds}s` };
|
|
194
|
+
}
|
|
195
|
+
if (started.kind === "unavailable") {
|
|
196
|
+
return { kind: "unavailable", detail: started.detail };
|
|
197
|
+
}
|
|
198
|
+
const start = started.start;
|
|
199
|
+
let cancelled = false;
|
|
200
|
+
const onSignal = () => {
|
|
201
|
+
if (cancelled) return;
|
|
202
|
+
cancelled = true;
|
|
203
|
+
void cancelLogin(start, deps).finally(() => {
|
|
204
|
+
ui.say("");
|
|
205
|
+
ui.say(" Cancelled. Nothing was written.");
|
|
206
|
+
process.exit(130);
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
process.on("SIGINT", onSignal);
|
|
210
|
+
process.on("SIGHUP", onSignal);
|
|
211
|
+
try {
|
|
212
|
+
ui.say("");
|
|
213
|
+
ui.say(` ${ui.bold("Sign in to Pushary")}`);
|
|
214
|
+
ui.say("");
|
|
215
|
+
ui.say(` 1. Open ${ui.cyan(start.verificationUrl)}`);
|
|
216
|
+
ui.say(` 2. Enter this code: ${ui.bold(start.userCode)}`);
|
|
217
|
+
ui.say("");
|
|
218
|
+
ui.say(` ${ui.dim(`This terminal shows fingerprint ${start.fingerprint}. The page shows the same one.`)}`);
|
|
219
|
+
ui.say(` ${ui.dim("The code is only on your screen. Nobody can send you a link that fills it in.")}`);
|
|
220
|
+
ui.say("");
|
|
221
|
+
if (!options.noBrowser && canOpenBrowser() && openInBrowser(start.verificationUrl)) {
|
|
222
|
+
ui.say(` ${ui.dim("Opened your browser.")}`);
|
|
223
|
+
}
|
|
224
|
+
const notice = (state2) => {
|
|
225
|
+
if (state2.kind === "needs-workspace") {
|
|
226
|
+
ui.say(` ${ui.yellow("!")} Waiting on a workspace. Finish creating one in the browser and this continues on its own.`);
|
|
227
|
+
} else if (state2.kind === "payment-required") {
|
|
228
|
+
ui.say(` ${ui.yellow("!")} A plan is needed before a key can be issued. Nothing was created yet.`);
|
|
229
|
+
ui.say(` ${ui.dim("Choose one at")} ${ui.cyan(state2.checkoutUrl)}`);
|
|
230
|
+
ui.say(` ${ui.dim("This terminal keeps the same code and finishes on its own once the plan is active.")}`);
|
|
231
|
+
if (!options.noBrowser && canOpenBrowser()) openInBrowser(state2.checkoutUrl);
|
|
232
|
+
} else if (state2.kind === "unreachable") {
|
|
233
|
+
ui.say(` ${ui.yellow("!")} Still trying. pushary.com is not answering ${ui.dim(`(${state2.detail})`)}`);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const { state } = await waitForLogin(start, { onNotice: notice }, deps);
|
|
237
|
+
if (state.kind === "authorized") {
|
|
238
|
+
return {
|
|
239
|
+
kind: "ok",
|
|
240
|
+
apiKey: state.apiKey,
|
|
241
|
+
siteSlug: state.siteSlug,
|
|
242
|
+
keyName: state.keyName
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (state.kind === "rejected") return { kind: "rejected" };
|
|
246
|
+
return { kind: "expired" };
|
|
247
|
+
} finally {
|
|
248
|
+
process.off("SIGINT", onSignal);
|
|
249
|
+
process.off("SIGHUP", onSignal);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export {
|
|
254
|
+
runBrowserLogin
|
|
255
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describeKeyCheck,
|
|
3
|
+
keyCheckFromResponse
|
|
4
|
+
} from "./chunk-UKNAAVEE.js";
|
|
5
|
+
import {
|
|
6
|
+
createIo
|
|
7
|
+
} from "./chunk-TX7KBKT7.js";
|
|
8
|
+
import {
|
|
9
|
+
EXIT
|
|
10
|
+
} from "./chunk-SML23YOT.js";
|
|
11
|
+
|
|
12
|
+
// src/cli/report-key.ts
|
|
13
|
+
var { dim, yellow } = createIo();
|
|
14
|
+
var printKeyCheck = (check, baseUrl) => {
|
|
15
|
+
const report = describeKeyCheck(check, baseUrl);
|
|
16
|
+
console.log(` ${yellow("!")} ${report.message}`);
|
|
17
|
+
for (const line of report.next) console.log(` ${dim(line)}`);
|
|
18
|
+
return report;
|
|
19
|
+
};
|
|
20
|
+
var exitIfKeyFailure = async (response, baseUrl) => {
|
|
21
|
+
const check = await keyCheckFromResponse(response, baseUrl);
|
|
22
|
+
if (!check) return;
|
|
23
|
+
const report = printKeyCheck(check, baseUrl);
|
|
24
|
+
process.exit(report.exitCode);
|
|
25
|
+
};
|
|
26
|
+
var responseDetail = async (response) => {
|
|
27
|
+
try {
|
|
28
|
+
const body = await response.json();
|
|
29
|
+
const text = [body.error, body.message].find((part) => typeof part === "string" && part);
|
|
30
|
+
if (typeof text === "string") return text;
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
return response.statusText || `HTTP ${response.status}`;
|
|
34
|
+
};
|
|
35
|
+
var exitOnFailedResponse = async (response, baseUrl) => {
|
|
36
|
+
await exitIfKeyFailure(response, baseUrl);
|
|
37
|
+
console.log(` ${yellow("!")} Failed: ${await responseDetail(response)}`);
|
|
38
|
+
process.exit(EXIT.FAILED);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
printKeyCheck,
|
|
43
|
+
exitOnFailedResponse
|
|
44
|
+
};
|
|
@@ -4,7 +4,12 @@ import {
|
|
|
4
4
|
|
|
5
5
|
// src/codex-config.ts
|
|
6
6
|
import { createHash } from "crypto";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
7
9
|
var CODEX_HOOK_BINARY = "pushary-codex-hook";
|
|
10
|
+
var codexHome = () => process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
|
|
11
|
+
var codexConfigToml = () => join(codexHome(), "config.toml");
|
|
12
|
+
var codexHooksJson = () => join(codexHome(), "hooks.json");
|
|
8
13
|
var CODEX_HOOK_EVENTS = [
|
|
9
14
|
{ event: "PermissionRequest", matcher: "Bash|apply_patch", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Waiting for your phone" },
|
|
10
15
|
{ event: "PreToolUse", matcher: "Bash|apply_patch", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Checking Pushary policy" },
|
|
@@ -310,6 +315,8 @@ var hasInstructionBlock = (filePath) => {
|
|
|
310
315
|
};
|
|
311
316
|
|
|
312
317
|
export {
|
|
318
|
+
codexConfigToml,
|
|
319
|
+
codexHooksJson,
|
|
313
320
|
addCodexMcpServer,
|
|
314
321
|
readCodexMcpAuth,
|
|
315
322
|
addCodexHooks,
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// src/cli/commands.ts
|
|
2
|
+
var COMMAND_LIST = [
|
|
3
|
+
{
|
|
4
|
+
name: "setup",
|
|
5
|
+
module: "pushary-setup",
|
|
6
|
+
summary: "Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: "claude",
|
|
10
|
+
module: "pushary-claude",
|
|
11
|
+
summary: "Run Claude Code through Pushary, reachable from your phone even when idle",
|
|
12
|
+
detail: [
|
|
13
|
+
"Send an instruction and it drives a fully idle agent. Ctrl-] takes the terminal back.",
|
|
14
|
+
"Add --remote to start headless."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "daemon",
|
|
19
|
+
module: "pushary-daemon",
|
|
20
|
+
summary: "Keep this machine reachable so your phone can START a new session",
|
|
21
|
+
detail: [
|
|
22
|
+
"Leave it running in a project dir; it launches a headless",
|
|
23
|
+
"'pushary claude --remote' when you send a prompt from the app."
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "status",
|
|
28
|
+
module: "pushary-status",
|
|
29
|
+
summary: "One screen: which key is in force, whether the server accepts it, what can receive an approval"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "login",
|
|
33
|
+
module: "pushary-login",
|
|
34
|
+
summary: "Sign in from this terminal, no key to copy (--with-token to pipe one in)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "logout",
|
|
38
|
+
module: "pushary-logout",
|
|
39
|
+
summary: "Remove the key this machine stores, and say what it does not remove"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "connect",
|
|
43
|
+
module: "pushary-connect",
|
|
44
|
+
summary: "Connect a phone without re-running setup (--app for the Pushary app)"
|
|
45
|
+
},
|
|
46
|
+
{ name: "doctor", module: "pushary-doctor", summary: "Verify your Pushary installation is working" },
|
|
47
|
+
{ name: "clean", module: "pushary-clean", summary: "Remove all Pushary configuration (--yes for non-interactive)" },
|
|
48
|
+
{ name: "mode", module: "pushary-mode", summary: "Switch approval mode (push_only, push_first, terminal_only)" },
|
|
49
|
+
{ name: "wait", module: "pushary-wait", summary: 'Show or set the "wait for your phone" ladder (pushary wait 45)' },
|
|
50
|
+
{ name: "stats", module: "pushary-stats", summary: "Show the approval moments your agents hit while not connected" },
|
|
51
|
+
{
|
|
52
|
+
name: "suggestions",
|
|
53
|
+
module: "pushary-suggestions",
|
|
54
|
+
summary: "List rules mined from your own approvals",
|
|
55
|
+
detail: ["Accept one with 'pushary suggestions accept <id>' to always-allow that pattern."]
|
|
56
|
+
},
|
|
57
|
+
{ name: "upgrade", module: "pushary-upgrade", summary: "Update the globally installed hooks to the latest version" },
|
|
58
|
+
{
|
|
59
|
+
name: "hook",
|
|
60
|
+
module: "pushary-hook",
|
|
61
|
+
summary: "Run as a PreToolUse hook (reads stdin, writes stdout)"
|
|
62
|
+
}
|
|
63
|
+
];
|
|
64
|
+
var COMMANDS = COMMAND_LIST;
|
|
65
|
+
var findCommand = (name) => COMMANDS.find((command) => command.name === name);
|
|
66
|
+
var isCommandName = (name) => COMMANDS.some((command) => command.name === name);
|
|
67
|
+
var COMMAND_OPTIONS = {
|
|
68
|
+
clean: [["--yes", "Remove everything without asking"]],
|
|
69
|
+
mode: [
|
|
70
|
+
["<mode>", "One of: push_only, push_first, terminal_only, notify_only"],
|
|
71
|
+
["status", "Show the current override"],
|
|
72
|
+
["clear", "Drop the override and go back to per-tool policies"],
|
|
73
|
+
["--for <30m|2h>", "Expire the override after this long"]
|
|
74
|
+
],
|
|
75
|
+
wait: [
|
|
76
|
+
["<seconds>", `Default push window, 0-300`],
|
|
77
|
+
["status", "Show the whole ladder"],
|
|
78
|
+
["clear", "Reset the default window"]
|
|
79
|
+
],
|
|
80
|
+
status: [["--json", "Emit the whole report as one object instead of the screen"]],
|
|
81
|
+
doctor: [
|
|
82
|
+
["--json", "Emit every check as one object instead of the screen"],
|
|
83
|
+
["--roundtrip", "Also send a real question and wait for you to answer it on your phone"],
|
|
84
|
+
["--no-push", "Run every check without waking a device"]
|
|
85
|
+
],
|
|
86
|
+
suggestions: [["accept <id>", "Turn a mined suggestion into an always-allow rule"]],
|
|
87
|
+
claude: [
|
|
88
|
+
["--remote", "Start headless and drive it entirely from your phone"],
|
|
89
|
+
["-p <prompt>", "Initial prompt"]
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
var SETUP_OPTIONS = [
|
|
93
|
+
["--key <pk_xxx.xxx>", "Use this API key instead of prompting. Visible in the process list; prefer --key-stdin in CI."],
|
|
94
|
+
["--key-stdin", "Read the API key from the first line of stdin, so it never appears in argv or shell history"],
|
|
95
|
+
["--agents <list>", 'Comma-separated agent ids, or "auto" for everything detected, or "none" to save the key only'],
|
|
96
|
+
["--yes, -y", "Accept every default and ask nothing. Implies --agents auto unless --agents is given"],
|
|
97
|
+
["--connect app", "Pair the native Pushary app, no key to paste"],
|
|
98
|
+
["--connect web", "Connect your phone via the browser subscribe page (default)"],
|
|
99
|
+
["--skip-phone", "Skip the phone connect step"],
|
|
100
|
+
["--dry-run", "Print what would be written and change nothing"],
|
|
101
|
+
["--json", "Emit one machine-readable object instead of the human report"]
|
|
102
|
+
];
|
|
103
|
+
var AGENT_IDS = "claude_code, codex, gemini_cli, hermes, cursor, custom";
|
|
104
|
+
var USAGE_EXAMPLES = [
|
|
105
|
+
"npx @pushary/agent-hooks@latest setup",
|
|
106
|
+
"npx @pushary/agent-hooks@latest setup --agents auto --yes --skip-phone",
|
|
107
|
+
"npx @pushary/agent-hooks@latest setup --key-stdin --agents claude_code --yes < key.txt",
|
|
108
|
+
"npx @pushary/agent-hooks@latest doctor",
|
|
109
|
+
"npx @pushary/agent-hooks@latest mode push_only --for 30m",
|
|
110
|
+
"pushary claude # native terminal, reachable from your phone",
|
|
111
|
+
'pushary claude --remote -p "start the refactor" # start headless, drive from your phone',
|
|
112
|
+
"pushary daemon # keep this machine ready to spawn a session"
|
|
113
|
+
];
|
|
114
|
+
var pad = (text, width) => text + " ".repeat(Math.max(0, width - text.length));
|
|
115
|
+
var renderHelp = (version) => {
|
|
116
|
+
const width = Math.max(...COMMANDS.map((command) => command.name.length)) + 2;
|
|
117
|
+
const commandLines = COMMANDS.flatMap((command) => [
|
|
118
|
+
` ${pad(command.name, width)}${command.summary}`,
|
|
119
|
+
...(command.detail ?? []).map((line) => ` ${" ".repeat(width)}${line}`)
|
|
120
|
+
]);
|
|
121
|
+
const optionWidth = Math.max(...SETUP_OPTIONS.map(([flag]) => flag.length)) + 2;
|
|
122
|
+
const optionLines = SETUP_OPTIONS.map(([flag, text]) => ` ${pad(flag, optionWidth)}${text}`);
|
|
123
|
+
return [
|
|
124
|
+
"",
|
|
125
|
+
`Pushary Agent Hooks v${version}`,
|
|
126
|
+
"",
|
|
127
|
+
"Usage: pushary <command> [options]",
|
|
128
|
+
"",
|
|
129
|
+
"Commands:",
|
|
130
|
+
...commandLines,
|
|
131
|
+
"",
|
|
132
|
+
"Setup options:",
|
|
133
|
+
...optionLines,
|
|
134
|
+
"",
|
|
135
|
+
`Agent ids: ${AGENT_IDS}`,
|
|
136
|
+
"",
|
|
137
|
+
"Examples:",
|
|
138
|
+
...USAGE_EXAMPLES.map((line) => ` ${line}`),
|
|
139
|
+
"",
|
|
140
|
+
"Run `pushary <command> --help` for a single command.",
|
|
141
|
+
""
|
|
142
|
+
].join("\n");
|
|
143
|
+
};
|
|
144
|
+
var renderCommandHelp = (name, version) => {
|
|
145
|
+
const spec = findCommand(name);
|
|
146
|
+
const options = name === "setup" ? SETUP_OPTIONS : COMMAND_OPTIONS[name] ?? [];
|
|
147
|
+
const width = options.length > 0 ? Math.max(...options.map(([flag]) => flag.length)) + 2 : 0;
|
|
148
|
+
return [
|
|
149
|
+
"",
|
|
150
|
+
`pushary ${name} ${version ? `(v${version})` : ""}`.trimEnd(),
|
|
151
|
+
"",
|
|
152
|
+
...spec ? [` ${spec.summary}`, ...(spec.detail ?? []).map((line) => ` ${line}`), ""] : [],
|
|
153
|
+
...options.length > 0 ? ["Options:", ...options.map(([flag, text]) => ` ${pad(flag, width)}${text}`), ""] : [],
|
|
154
|
+
...name === "setup" ? [
|
|
155
|
+
`Agent ids: ${AGENT_IDS}`,
|
|
156
|
+
"",
|
|
157
|
+
"Non-interactive runs need --agents or --yes. Without a terminal on stdin,",
|
|
158
|
+
"setup takes the defaults rather than waiting for a keypress that can never arrive.",
|
|
159
|
+
""
|
|
160
|
+
] : []
|
|
161
|
+
].join("\n");
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/cli/version.ts
|
|
165
|
+
import { readFileSync } from "fs";
|
|
166
|
+
import { dirname, join, parse } from "path";
|
|
167
|
+
import { fileURLToPath } from "url";
|
|
168
|
+
var PACKAGE_NAME = "@pushary/agent-hooks";
|
|
169
|
+
var findOwnPackageJson = (start) => {
|
|
170
|
+
const { root } = parse(start);
|
|
171
|
+
let dir = start;
|
|
172
|
+
while (true) {
|
|
173
|
+
const candidate = join(dir, "package.json");
|
|
174
|
+
try {
|
|
175
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
176
|
+
if (pkg.name === PACKAGE_NAME) return candidate;
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
if (dir === root) return void 0;
|
|
180
|
+
const parent = dirname(dir);
|
|
181
|
+
if (parent === dir) return void 0;
|
|
182
|
+
dir = parent;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var getPackageVersion = () => {
|
|
186
|
+
try {
|
|
187
|
+
const found = findOwnPackageJson(dirname(fileURLToPath(import.meta.url)));
|
|
188
|
+
if (!found) return "0.0.0";
|
|
189
|
+
const pkg = JSON.parse(readFileSync(found, "utf-8"));
|
|
190
|
+
return pkg.version ?? "0.0.0";
|
|
191
|
+
} catch {
|
|
192
|
+
return "0.0.0";
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/exit.ts
|
|
197
|
+
var EXIT = {
|
|
198
|
+
OK: 0,
|
|
199
|
+
FAILED: 1,
|
|
200
|
+
USAGE: 2,
|
|
201
|
+
NOT_CONFIGURED: 3,
|
|
202
|
+
UNAUTHENTICATED: 4,
|
|
203
|
+
PROBLEMS_FOUND: 5,
|
|
204
|
+
NO_DEVICE: 6,
|
|
205
|
+
INPUT_REQUIRED: 7,
|
|
206
|
+
UNREACHABLE: 8
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export {
|
|
210
|
+
COMMANDS,
|
|
211
|
+
isCommandName,
|
|
212
|
+
renderHelp,
|
|
213
|
+
renderCommandHelp,
|
|
214
|
+
getPackageVersion,
|
|
215
|
+
EXIT
|
|
216
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/cli/io.ts
|
|
2
|
+
var truthyLevel = (value) => {
|
|
3
|
+
if (value === void 0) return void 0;
|
|
4
|
+
if (value === "" || value === "false") return 0;
|
|
5
|
+
const parsed = Number(value);
|
|
6
|
+
return Number.isFinite(parsed) ? parsed : 1;
|
|
7
|
+
};
|
|
8
|
+
var useColor = (context = {}, stream = process.stdout) => {
|
|
9
|
+
const forced = truthyLevel(process.env.FORCE_COLOR);
|
|
10
|
+
if (forced !== void 0) return forced > 0;
|
|
11
|
+
if (context.color === false) return false;
|
|
12
|
+
if (process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "") return false;
|
|
13
|
+
if (process.env.TERM === "dumb") return false;
|
|
14
|
+
if (context.json) return false;
|
|
15
|
+
return stream.isTTY === true;
|
|
16
|
+
};
|
|
17
|
+
var SPINNER_FRAMES = [" ", ". ", ".. ", "..."];
|
|
18
|
+
var SPINNER_INTERVAL_MS = 200;
|
|
19
|
+
var describeError = (err) => {
|
|
20
|
+
if (err instanceof Error) {
|
|
21
|
+
const signalled = err;
|
|
22
|
+
if (signalled.killed && signalled.signal === "SIGTERM") {
|
|
23
|
+
return "timed out, check your network connection";
|
|
24
|
+
}
|
|
25
|
+
return err.message;
|
|
26
|
+
}
|
|
27
|
+
return String(err);
|
|
28
|
+
};
|
|
29
|
+
var createIo = (stream = process.stdout, context = {}) => {
|
|
30
|
+
const color = useColor(context, stream);
|
|
31
|
+
const wrap = (open) => (s) => color ? `${open}${s}\x1B[0m` : s;
|
|
32
|
+
const bold = wrap("\x1B[1m");
|
|
33
|
+
const dim = wrap("\x1B[2m");
|
|
34
|
+
const green = wrap("\x1B[32m");
|
|
35
|
+
const yellow = wrap("\x1B[33m");
|
|
36
|
+
const red = wrap("\x1B[31m");
|
|
37
|
+
const cyan = wrap("\x1B[36m");
|
|
38
|
+
const check = () => green("\u2713");
|
|
39
|
+
const line = (s = "") => {
|
|
40
|
+
stream.write(`${s}
|
|
41
|
+
`);
|
|
42
|
+
};
|
|
43
|
+
const warn = (s) => {
|
|
44
|
+
process.stderr.write(`${yellow("!")} ${s}
|
|
45
|
+
`);
|
|
46
|
+
};
|
|
47
|
+
const spinner = async (label, fn, options = {}) => {
|
|
48
|
+
if (!color) {
|
|
49
|
+
try {
|
|
50
|
+
await fn();
|
|
51
|
+
line(` ${check()} ${label}`);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
line(` ${yellow("!")} ${label} ${dim(`(${describeError(err)})`)}`);
|
|
54
|
+
if (!options.optional) throw err;
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let frame = 0;
|
|
59
|
+
const timer = setInterval(() => {
|
|
60
|
+
stream.write(`\r ${dim(SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length])} ${label}`);
|
|
61
|
+
}, SPINNER_INTERVAL_MS);
|
|
62
|
+
try {
|
|
63
|
+
await fn();
|
|
64
|
+
clearInterval(timer);
|
|
65
|
+
stream.write(`\r ${check()} ${label}\x1B[K
|
|
66
|
+
`);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
clearInterval(timer);
|
|
69
|
+
stream.write(`\r ${yellow("!")} ${label} ${dim(`(${describeError(err)})`)}\x1B[K
|
|
70
|
+
`);
|
|
71
|
+
if (!options.optional) throw err;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return { color, bold, dim, green, yellow, red, cyan, check, line, warn, spinner };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
createIo
|
|
79
|
+
};
|