@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
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateKeypair,
|
|
3
|
+
openSealedKey,
|
|
4
|
+
publicKeyFingerprint
|
|
5
|
+
} from "./chunk-3EGEA4KH.js";
|
|
6
|
+
import {
|
|
7
|
+
callMcpTool
|
|
8
|
+
} from "./chunk-HNTKTK5B.js";
|
|
9
|
+
|
|
10
|
+
// src/reach.ts
|
|
11
|
+
var reachVerdict = (channels) => {
|
|
12
|
+
const { yours, site } = channels;
|
|
13
|
+
if (yours === null || site === null) {
|
|
14
|
+
return channels.phoneReachable || channels.webActive > 0 ? { kind: "yours" } : { kind: "nobody" };
|
|
15
|
+
}
|
|
16
|
+
if (yours.phoneReachable) return { kind: "yours" };
|
|
17
|
+
const solo = site.otherMembers === 0;
|
|
18
|
+
if (solo) {
|
|
19
|
+
return site.phoneReachable || site.webActive > 0 ? { kind: "yours" } : { kind: "nobody" };
|
|
20
|
+
}
|
|
21
|
+
if (site.appDevices === 0 && site.webActive === 0) return { kind: "nobody" };
|
|
22
|
+
if (site.webActive > 0 && !yours.webAttributable) {
|
|
23
|
+
return { kind: "unattributable", otherMembers: site.otherMembers };
|
|
24
|
+
}
|
|
25
|
+
return { kind: "others", otherMembers: site.otherMembers };
|
|
26
|
+
};
|
|
27
|
+
var reachesYou = (channels) => reachVerdict(channels).kind === "yours";
|
|
28
|
+
var plural = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
|
|
29
|
+
var describeReach = (channels) => {
|
|
30
|
+
const verdict = reachVerdict(channels);
|
|
31
|
+
if (verdict.kind === "nobody") return "nothing can receive an approval";
|
|
32
|
+
const parts = [];
|
|
33
|
+
const own = channels.yours;
|
|
34
|
+
if (verdict.kind === "yours" && own !== null && own.appDevices > 0) {
|
|
35
|
+
const platforms = own.appPlatforms.length > 0 ? ` (${own.appPlatforms.join(", ")})` : "";
|
|
36
|
+
parts.push(`${plural(own.appDevices, "app device")}${platforms}`);
|
|
37
|
+
} else if (channels.appDevices > 0) {
|
|
38
|
+
const platforms = channels.appPlatforms.length > 0 ? ` (${channels.appPlatforms.join(", ")})` : "";
|
|
39
|
+
parts.push(`${plural(channels.appDevices, "app device")}${platforms}`);
|
|
40
|
+
}
|
|
41
|
+
if (channels.webPhone > 0) parts.push(`${plural(channels.webPhone, "browser")} on a phone`);
|
|
42
|
+
if (channels.webDesktop > 0) parts.push(`${plural(channels.webDesktop, "browser")} on a desktop`);
|
|
43
|
+
const summary = parts.length > 0 ? parts.join(", ") : "a device";
|
|
44
|
+
if (verdict.kind === "others") return `${summary}, none of them yours`;
|
|
45
|
+
if (verdict.kind === "unattributable") return `${summary}, and no way to tell whose`;
|
|
46
|
+
return summary;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/cli/human.ts
|
|
50
|
+
var stream = process.stdout;
|
|
51
|
+
var setHumanStream = (target) => {
|
|
52
|
+
stream = target;
|
|
53
|
+
};
|
|
54
|
+
var writeHuman = (text) => {
|
|
55
|
+
stream.write(text);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/onboarding.ts
|
|
59
|
+
import qrcodeTerminal from "qrcode-terminal";
|
|
60
|
+
|
|
61
|
+
// src/setup/verify.ts
|
|
62
|
+
var plural2 = (count, one) => `${count} ${one}${count === 1 ? "" : "s"}`;
|
|
63
|
+
var describeRecipients = (push, channels) => {
|
|
64
|
+
const parts = [];
|
|
65
|
+
if (push.app > 0) {
|
|
66
|
+
const platforms = channels.yours?.appPlatforms ?? channels.appPlatforms;
|
|
67
|
+
const suffix = platforms.length > 0 ? ` (${platforms.join(", ")})` : "";
|
|
68
|
+
parts.push(`${plural2(push.app, "app device")}${suffix}`);
|
|
69
|
+
}
|
|
70
|
+
if (push.web > 0) parts.push(plural2(push.web, "browser"));
|
|
71
|
+
return parts.length > 0 ? parts.join(" and ") : plural2(push.total, "device");
|
|
72
|
+
};
|
|
73
|
+
var classifyDelivery = (channels, push) => {
|
|
74
|
+
if (push.total <= 0) return { kind: "no-recipients" };
|
|
75
|
+
const verdict = reachVerdict(channels);
|
|
76
|
+
if (verdict.kind === "yours" && (push.app > 0 || channels.yours === null || channels.site?.otherMembers === 0)) {
|
|
77
|
+
return { kind: "delivered", recipients: push.total, deviceLabel: describeRecipients(push, channels) };
|
|
78
|
+
}
|
|
79
|
+
if (verdict.kind === "unattributable") {
|
|
80
|
+
return { kind: "unattributable", recipients: push.total, otherMembers: verdict.otherMembers };
|
|
81
|
+
}
|
|
82
|
+
if (verdict.kind === "others") {
|
|
83
|
+
return { kind: "others-only", recipients: push.total, otherMembers: verdict.otherMembers };
|
|
84
|
+
}
|
|
85
|
+
if (verdict.kind === "yours") {
|
|
86
|
+
const otherMembers = channels.site?.otherMembers ?? 0;
|
|
87
|
+
return { kind: "unattributable", recipients: push.total, otherMembers };
|
|
88
|
+
}
|
|
89
|
+
return { kind: "delivered", recipients: push.total, deviceLabel: describeRecipients(push, channels) };
|
|
90
|
+
};
|
|
91
|
+
var sendTestNotification = async (apiKey) => {
|
|
92
|
+
const result = await callMcpTool(apiKey, "send_notification", {
|
|
93
|
+
title: "Pushary is connected",
|
|
94
|
+
body: "Your AI agent can now reach you right here.",
|
|
95
|
+
agentName: "Pushary setup"
|
|
96
|
+
}, { timeoutMs: 2e4 });
|
|
97
|
+
const toCount = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
98
|
+
const app = toCount(result.delivery?.mobile?.recipients);
|
|
99
|
+
const web = toCount(result.delivery?.web?.recipients);
|
|
100
|
+
return { app, web, total: typeof result.sent === "number" ? result.sent : app + web };
|
|
101
|
+
};
|
|
102
|
+
var verifyDelivery = async (apiKey, channels, deps = {}) => {
|
|
103
|
+
const send = deps.send ?? sendTestNotification;
|
|
104
|
+
try {
|
|
105
|
+
return classifyDelivery(channels, await send(apiKey));
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return { kind: "error", detail: err instanceof Error ? err.message : "send failed" };
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var describeVerifyOutcome = (outcome) => {
|
|
111
|
+
switch (outcome.kind) {
|
|
112
|
+
case "delivered":
|
|
113
|
+
return { ok: true, message: `Delivered to ${outcome.deviceLabel}. Push works.`, next: [] };
|
|
114
|
+
case "no-recipients":
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
message: "Nothing to deliver to. Agents are configured; approvals will fall back to the terminal.",
|
|
118
|
+
next: [
|
|
119
|
+
"Run pushary connect to add your phone.",
|
|
120
|
+
"Then pushary doctor --roundtrip to test a real approval."
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
case "others-only":
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
message: `Delivered to ${plural2(outcome.recipients, "device")} on this site, none of them yours.`,
|
|
127
|
+
next: [
|
|
128
|
+
`Approvals for your agent would wake one of your ${plural2(outcome.otherMembers, "teammate")}.`,
|
|
129
|
+
"Run pushary connect to add your own phone."
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
case "unattributable":
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
message: `Delivered to ${plural2(outcome.recipients, "device")} on this site, and no way to tell whose.`,
|
|
136
|
+
next: [
|
|
137
|
+
"Browser subscriptions carry no owner, and this site has other members.",
|
|
138
|
+
"Install the Pushary app with pushary connect, or run pushary doctor --roundtrip to prove it reaches you."
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
case "error":
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: `Could not send the test notification (${outcome.detail}).`,
|
|
145
|
+
next: ["Agents are configured. Run pushary doctor once the connection is back."]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/onboarding.ts
|
|
151
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
152
|
+
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
153
|
+
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
154
|
+
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
155
|
+
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
156
|
+
var check = green("\u2713");
|
|
157
|
+
var apiBase = () => process.env.PUSHARY_API_URL?.trim() || process.env.PUSHARY_BASE_URL?.trim() || "https://pushary.com";
|
|
158
|
+
var EXIT_ABORTED = 130;
|
|
159
|
+
var CONNECT_POLL_INTERVAL_MS = 2500;
|
|
160
|
+
var CONNECT_TIMEOUT_MS = 18e4;
|
|
161
|
+
var EMPTY_CHANNELS = {
|
|
162
|
+
appDevices: 0,
|
|
163
|
+
appPlatforms: [],
|
|
164
|
+
webActive: 0,
|
|
165
|
+
webPhone: 0,
|
|
166
|
+
webDesktop: 0,
|
|
167
|
+
phoneReachable: false,
|
|
168
|
+
precise: true,
|
|
169
|
+
yours: null,
|
|
170
|
+
site: null
|
|
171
|
+
};
|
|
172
|
+
var authHeaders = (apiKey) => ({
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
Authorization: `Bearer ${apiKey}`
|
|
175
|
+
});
|
|
176
|
+
var getJson = async (url, apiKey) => {
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch(url, { headers: authHeaders(apiKey), signal: AbortSignal.timeout(1e4) });
|
|
179
|
+
const body = await res.json().catch(() => null);
|
|
180
|
+
if (!res.ok) return { kind: "http", status: res.status, body };
|
|
181
|
+
if (!body) return { kind: "http", status: res.status, body: null };
|
|
182
|
+
return { kind: "ok", body };
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return { kind: "network", detail: err instanceof Error ? err.message : "network error" };
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var fetchSite = async (apiKey) => {
|
|
188
|
+
const result = await getJson(`${apiBase()}/api/v1/server/site`, apiKey);
|
|
189
|
+
if (result.kind !== "ok") return null;
|
|
190
|
+
const data = result.body;
|
|
191
|
+
if (typeof data.slug !== "string" || typeof data.subscribeUrl !== "string") return null;
|
|
192
|
+
return { slug: data.slug, name: typeof data.name === "string" ? data.name : data.slug, subscribeUrl: data.subscribeUrl };
|
|
193
|
+
};
|
|
194
|
+
var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
195
|
+
var strings = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
196
|
+
var record = (value) => typeof value === "object" && value !== null ? value : {};
|
|
197
|
+
var parseOwnerCounts = (value) => {
|
|
198
|
+
if (typeof value !== "object" || value === null) return null;
|
|
199
|
+
const raw = value;
|
|
200
|
+
const app = record(raw.app);
|
|
201
|
+
return {
|
|
202
|
+
appDevices: num(app.devices),
|
|
203
|
+
appPlatforms: strings(app.platforms),
|
|
204
|
+
phoneReachable: raw.phoneReachable === true,
|
|
205
|
+
webAttributable: raw.webAttributable === true
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
var parseSiteCounts = (value) => {
|
|
209
|
+
if (typeof value !== "object" || value === null) return null;
|
|
210
|
+
const raw = value;
|
|
211
|
+
const app = record(raw.app);
|
|
212
|
+
const web = record(raw.web);
|
|
213
|
+
return {
|
|
214
|
+
appDevices: num(app.devices),
|
|
215
|
+
appPlatforms: strings(app.platforms),
|
|
216
|
+
webActive: num(web.active),
|
|
217
|
+
webPhone: num(web.phone),
|
|
218
|
+
webDesktop: num(web.desktop),
|
|
219
|
+
phoneReachable: raw.phoneReachable === true,
|
|
220
|
+
otherMembers: num(raw.otherMembers)
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
var parseChannels = (data) => {
|
|
224
|
+
const app = record(data.app);
|
|
225
|
+
const web = record(data.web);
|
|
226
|
+
return {
|
|
227
|
+
appDevices: num(app.devices),
|
|
228
|
+
appPlatforms: strings(app.platforms),
|
|
229
|
+
webActive: num(web.active),
|
|
230
|
+
webPhone: num(web.phone),
|
|
231
|
+
webDesktop: num(web.desktop),
|
|
232
|
+
phoneReachable: data.phoneReachable === true,
|
|
233
|
+
precise: true,
|
|
234
|
+
yours: parseOwnerCounts(data.yours),
|
|
235
|
+
site: parseSiteCounts(data.site)
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
var SUBSCRIPTION_REQUIRED = "subscription_required";
|
|
239
|
+
var fetchChannels = async (apiKey) => {
|
|
240
|
+
const result = await getJson(`${apiBase()}/api/v1/server/channels`, apiKey);
|
|
241
|
+
if (result.kind === "ok") return { kind: "ok", status: parseChannels(result.body) };
|
|
242
|
+
if (result.kind === "network") return { kind: "unreachable", detail: result.detail };
|
|
243
|
+
if (result.status === 401) return { kind: "unauthorized" };
|
|
244
|
+
if (result.status === 403) {
|
|
245
|
+
return result.body?.code === SUBSCRIPTION_REQUIRED ? { kind: "locked" } : { kind: "unreachable", detail: "HTTP 403" };
|
|
246
|
+
}
|
|
247
|
+
if (result.status !== 404) return { kind: "unreachable", detail: `HTTP ${result.status}` };
|
|
248
|
+
const legacy = await getJson(`${apiBase()}/api/v1/server/subscribers/count`, apiKey);
|
|
249
|
+
if (legacy.kind === "network") return { kind: "unreachable", detail: legacy.detail };
|
|
250
|
+
if (legacy.kind === "http") {
|
|
251
|
+
if (legacy.status === 401) return { kind: "unauthorized" };
|
|
252
|
+
return { kind: "unreachable", detail: `HTTP ${legacy.status}` };
|
|
253
|
+
}
|
|
254
|
+
const active = num(legacy.body.active);
|
|
255
|
+
return {
|
|
256
|
+
kind: "ok",
|
|
257
|
+
status: { ...EMPTY_CHANNELS, webActive: active, phoneReachable: active > 0, precise: false }
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
261
|
+
var startSpinner = (getLabel) => {
|
|
262
|
+
const frames = [" ", ". ", ".. ", "..."];
|
|
263
|
+
let i = 0;
|
|
264
|
+
const interval = setInterval(() => {
|
|
265
|
+
writeHuman(`\r ${dim(frames[i++ % frames.length])} ${getLabel()}`);
|
|
266
|
+
}, 200);
|
|
267
|
+
return (finalGlyph, finalLabel) => {
|
|
268
|
+
clearInterval(interval);
|
|
269
|
+
writeHuman(`\r ${finalGlyph} ${finalLabel}\x1B[K
|
|
270
|
+
`);
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
var printQr = (url) => new Promise((resolve, reject) => {
|
|
274
|
+
try {
|
|
275
|
+
qrcodeTerminal.generate(url, { small: true }, (qr) => {
|
|
276
|
+
writeHuman("\n" + qr.split("\n").map((line) => " " + line).join("\n") + "\n");
|
|
277
|
+
resolve();
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
reject(err);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
var phoneFailure = (detail) => ({ reachesYou: false, outcome: null, detail });
|
|
284
|
+
var waitForDevice = async (apiKey, baseline) => {
|
|
285
|
+
const deadline = Date.now() + CONNECT_TIMEOUT_MS;
|
|
286
|
+
let consecutiveFailures = 0;
|
|
287
|
+
let warned = false;
|
|
288
|
+
const stop = startSpinner(() => {
|
|
289
|
+
const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
|
|
290
|
+
return `Waiting for your phone ${dim(`(${left}s)`)}`;
|
|
291
|
+
});
|
|
292
|
+
try {
|
|
293
|
+
while (Date.now() < deadline) {
|
|
294
|
+
const probe = await fetchChannels(apiKey);
|
|
295
|
+
if (probe.kind === "unauthorized") {
|
|
296
|
+
stop(yellow("!"), "The server stopped accepting this key");
|
|
297
|
+
return { kind: "unauthorized" };
|
|
298
|
+
}
|
|
299
|
+
if (probe.kind === "locked") {
|
|
300
|
+
stop(yellow("!"), "This workspace has no active plan");
|
|
301
|
+
return { kind: "locked" };
|
|
302
|
+
}
|
|
303
|
+
if (probe.kind === "unreachable") {
|
|
304
|
+
consecutiveFailures += 1;
|
|
305
|
+
if (consecutiveFailures >= 3 && !warned) {
|
|
306
|
+
warned = true;
|
|
307
|
+
writeHuman(`\r ${yellow("!")} Still trying. pushary.com is not answering ${dim(`(${probe.detail})`)}\x1B[K
|
|
308
|
+
`);
|
|
309
|
+
}
|
|
310
|
+
await sleep(CONNECT_POLL_INTERVAL_MS);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
consecutiveFailures = 0;
|
|
314
|
+
const channels = probe.status;
|
|
315
|
+
if (reachesYou(channels) || channels.webActive > baseline.webActive) {
|
|
316
|
+
stop(check, "Phone connected");
|
|
317
|
+
return { kind: "connected", channels };
|
|
318
|
+
}
|
|
319
|
+
await sleep(CONNECT_POLL_INTERVAL_MS);
|
|
320
|
+
}
|
|
321
|
+
stop(yellow("!"), "Didn't detect a connection yet");
|
|
322
|
+
return { kind: "timeout" };
|
|
323
|
+
} catch (err) {
|
|
324
|
+
stop(yellow("!"), `Couldn't check connection ${dim(`(${err instanceof Error ? err.message : "network error"})`)}`);
|
|
325
|
+
return { kind: "timeout" };
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var sendAndConfirm = async (apiKey, channels) => {
|
|
329
|
+
const stopSend = startSpinner(() => "Sending a test notification");
|
|
330
|
+
const outcome = await verifyDelivery(apiKey, channels);
|
|
331
|
+
const report = describeVerifyOutcome(outcome);
|
|
332
|
+
stopSend(report.ok ? check : yellow("!"), report.message);
|
|
333
|
+
for (const line of report.next) console.log(` ${dim(line)}`);
|
|
334
|
+
return { reachesYou: report.ok, outcome, detail: null };
|
|
335
|
+
};
|
|
336
|
+
var printCapabilityDelta = () => {
|
|
337
|
+
console.log(` ${dim("Two ways to get approvals on your phone:")}`);
|
|
338
|
+
console.log();
|
|
339
|
+
console.log(` ${bold("Pushary app")} ${dim("(iOS / Android)")} ${cyan("pushary setup --connect app")}`);
|
|
340
|
+
console.log(` ${dim("\u2022 Answers from the lock screen, and while the app is closed")}`);
|
|
341
|
+
console.log(` ${dim("\u2022 Can start and steer agents remotely")}`);
|
|
342
|
+
console.log();
|
|
343
|
+
console.log(` ${bold("Browser")} ${dim("(add to home screen)")} ${dim("scan below")}`);
|
|
344
|
+
console.log(` ${dim("\u2022 Works without installing an app")}`);
|
|
345
|
+
console.log(` ${dim("\u2022 Answers require opening the page")}`);
|
|
346
|
+
console.log(` ${dim("\u2022 iOS needs the page added to the home screen first")}`);
|
|
347
|
+
};
|
|
348
|
+
var printSiteOnlyWarning = (channels) => {
|
|
349
|
+
const others = channels.site?.otherMembers ?? 0;
|
|
350
|
+
console.log(` ${yellow("!")} This site has ${channels.appDevices + channels.webActive} device(s), ${describeReach(channels)}.`);
|
|
351
|
+
console.log(` ${dim(`Approvals for your agent would wake one of your ${others === 1 ? "teammate" : `${others} teammates`}.`)}`);
|
|
352
|
+
console.log(` ${dim("Connect your own phone:")}`);
|
|
353
|
+
};
|
|
354
|
+
var connectDevice = async (apiKey, options = {}) => {
|
|
355
|
+
const probe = await fetchChannels(apiKey);
|
|
356
|
+
if (probe.kind !== "ok") return describeProbeFailure(probe);
|
|
357
|
+
const channels = probe.status;
|
|
358
|
+
if (reachesYou(channels)) {
|
|
359
|
+
console.log(` ${check} ${describeExistingConnection(channels)}`);
|
|
360
|
+
return sendAndConfirm(apiKey, channels);
|
|
361
|
+
}
|
|
362
|
+
const site = await fetchSite(apiKey);
|
|
363
|
+
if (!site) {
|
|
364
|
+
console.log(` ${yellow("!")} Couldn't load your subscribe link. Connect your phone later at ${cyan("pushary.com/download")}.`);
|
|
365
|
+
return phoneFailure("no_subscribe_link");
|
|
366
|
+
}
|
|
367
|
+
console.log();
|
|
368
|
+
if (channels.site !== null && channels.site.otherMembers > 0 && channels.phoneReachable) {
|
|
369
|
+
printSiteOnlyWarning(channels);
|
|
370
|
+
} else {
|
|
371
|
+
console.log(` ${bold("Connect your phone")}`);
|
|
372
|
+
if (channels.webDesktop > 0) {
|
|
373
|
+
console.log(` ${dim("A browser on this computer is subscribed, but no phone is connected yet.")}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
console.log();
|
|
377
|
+
if (options.showCapabilities) printCapabilityDelta();
|
|
378
|
+
console.log();
|
|
379
|
+
console.log(` ${dim("Scan this with your phone camera:")}`);
|
|
380
|
+
await printQr(site.subscribeUrl);
|
|
381
|
+
console.log(` ${dim("or open")} ${cyan(site.subscribeUrl)}`);
|
|
382
|
+
console.log();
|
|
383
|
+
const waited = await waitForDevice(apiKey, channels);
|
|
384
|
+
if (waited.kind === "unauthorized") return phoneFailure("unauthorized");
|
|
385
|
+
if (waited.kind === "locked") return phoneFailure("locked");
|
|
386
|
+
if (waited.kind === "timeout") {
|
|
387
|
+
console.log(` ${dim("No rush, open")} ${cyan(site.subscribeUrl)} ${dim("on your phone whenever you're ready.")}`);
|
|
388
|
+
return phoneFailure("timeout");
|
|
389
|
+
}
|
|
390
|
+
return sendAndConfirm(apiKey, waited.channels);
|
|
391
|
+
};
|
|
392
|
+
var describeProbeFailure = (probe) => {
|
|
393
|
+
if (probe.kind === "unauthorized") {
|
|
394
|
+
console.log(` ${yellow("!")} The server rejected this key when checking for devices.`);
|
|
395
|
+
return phoneFailure("unauthorized");
|
|
396
|
+
}
|
|
397
|
+
if (probe.kind === "locked") {
|
|
398
|
+
console.log(` ${yellow("!")} No approval can be delivered until the plan is active.`);
|
|
399
|
+
return phoneFailure("locked");
|
|
400
|
+
}
|
|
401
|
+
if (probe.kind === "unreachable") {
|
|
402
|
+
console.log(` ${yellow("!")} Couldn't check your devices ${dim(`(${probe.detail})`)}`);
|
|
403
|
+
console.log(` ${dim("Agents are configured. Run")} ${cyan("pushary doctor")} ${dim("when the connection is back.")}`);
|
|
404
|
+
return phoneFailure(probe.detail);
|
|
405
|
+
}
|
|
406
|
+
return phoneFailure("unknown");
|
|
407
|
+
};
|
|
408
|
+
var describeExistingConnection = (channels) => {
|
|
409
|
+
if (!channels.precise) return "A device is already connected";
|
|
410
|
+
const own = channels.yours;
|
|
411
|
+
if (own !== null && own.appDevices > 0) {
|
|
412
|
+
const platforms = own.appPlatforms.length > 0 ? ` ${dim(`(${own.appPlatforms.join(", ")})`)}` : "";
|
|
413
|
+
return `Pushary app connected${platforms}`;
|
|
414
|
+
}
|
|
415
|
+
if (channels.appDevices > 0) {
|
|
416
|
+
const platforms = channels.appPlatforms.length > 0 ? ` ${dim(`(${channels.appPlatforms.join(", ")})`)}` : "";
|
|
417
|
+
return `Pushary app connected${platforms}`;
|
|
418
|
+
}
|
|
419
|
+
return "Phone already connected";
|
|
420
|
+
};
|
|
421
|
+
var confirmAppConnection = async (apiKey) => {
|
|
422
|
+
const probe = await fetchChannels(apiKey);
|
|
423
|
+
if (probe.kind !== "ok") return describeProbeFailure(probe);
|
|
424
|
+
const channels = probe.status;
|
|
425
|
+
const ownedApp = channels.yours?.appDevices ?? channels.appDevices;
|
|
426
|
+
if (channels.precise && ownedApp === 0) {
|
|
427
|
+
console.log(` ${yellow("!")} Paired, but the app hasn't registered for notifications yet.`);
|
|
428
|
+
console.log(` ${dim("Open the Pushary app and allow notifications, then run")} ${cyan("pushary doctor")}${dim(".")}`);
|
|
429
|
+
return phoneFailure("app_not_registered");
|
|
430
|
+
}
|
|
431
|
+
return sendAndConfirm(apiKey, channels);
|
|
432
|
+
};
|
|
433
|
+
var printConnectInstructions = async (apiKey) => {
|
|
434
|
+
const probe = await fetchChannels(apiKey);
|
|
435
|
+
if (probe.kind !== "ok") return describeProbeFailure(probe);
|
|
436
|
+
const channels = probe.status;
|
|
437
|
+
if (reachesYou(channels)) {
|
|
438
|
+
console.log(` ${check} ${describeExistingConnection(channels)}`);
|
|
439
|
+
return sendAndConfirm(apiKey, channels);
|
|
440
|
+
}
|
|
441
|
+
if (channels.site !== null && channels.site.otherMembers > 0 && channels.phoneReachable) {
|
|
442
|
+
printSiteOnlyWarning(channels);
|
|
443
|
+
}
|
|
444
|
+
const site = await fetchSite(apiKey);
|
|
445
|
+
const target = site?.subscribeUrl ?? "https://pushary.com/download";
|
|
446
|
+
console.log(` ${dim("Connect your phone for push notifications at")} ${cyan(target)}`);
|
|
447
|
+
console.log(` ${dim("or get the Pushary app at")} ${cyan("https://pushary.com/download")}`);
|
|
448
|
+
return phoneFailure("no_device");
|
|
449
|
+
};
|
|
450
|
+
var PAIR_POLL_INTERVAL_MS = 2e3;
|
|
451
|
+
var PAIR_TIMEOUT_MS = 18e4;
|
|
452
|
+
var startPairing = async (cliPublicKey) => {
|
|
453
|
+
try {
|
|
454
|
+
const res = await fetch(`${apiBase()}/api/mobile/pair/start`, {
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers: { "Content-Type": "application/json" },
|
|
457
|
+
body: JSON.stringify({ cliPublicKey }),
|
|
458
|
+
signal: AbortSignal.timeout(1e4)
|
|
459
|
+
});
|
|
460
|
+
if (!res.ok) return null;
|
|
461
|
+
const data = await res.json().catch(() => null);
|
|
462
|
+
return data && typeof data.pairId === "string" ? data.pairId : null;
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
var cancelPairing = async (pairId) => {
|
|
468
|
+
try {
|
|
469
|
+
await fetch(`${apiBase()}/api/mobile/pair/start`, {
|
|
470
|
+
method: "DELETE",
|
|
471
|
+
headers: { "Content-Type": "application/json" },
|
|
472
|
+
body: JSON.stringify({ pairId }),
|
|
473
|
+
signal: AbortSignal.timeout(5e3)
|
|
474
|
+
});
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
var claimPairing = async (pairId) => {
|
|
479
|
+
try {
|
|
480
|
+
const res = await fetch(`${apiBase()}/api/mobile/pair/claim?id=${encodeURIComponent(pairId)}`, {
|
|
481
|
+
signal: AbortSignal.timeout(1e4)
|
|
482
|
+
});
|
|
483
|
+
if (!res.ok) return null;
|
|
484
|
+
return await res.json().catch(() => null);
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
var buildPairLinks = (pairId, publicKeyB64, baseUrl = apiBase()) => {
|
|
490
|
+
const id = encodeURIComponent(pairId);
|
|
491
|
+
const pk = encodeURIComponent(publicKeyB64);
|
|
492
|
+
return {
|
|
493
|
+
universalLink: `${baseUrl.replace(/\/+$/, "")}/app/pair?id=${id}&pk=${pk}`,
|
|
494
|
+
deepLink: `pushary://pair?pk=${pk}&id=${id}`
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
var connectViaAppPairing = async () => {
|
|
498
|
+
const keypair = generateKeypair();
|
|
499
|
+
const pairId = await startPairing(keypair.publicKeyB64);
|
|
500
|
+
if (!pairId) {
|
|
501
|
+
console.log(` ${yellow("!")} Couldn't start pairing. Check your connection and try again.`);
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const { universalLink, deepLink } = buildPairLinks(pairId, keypair.publicKeyB64);
|
|
505
|
+
console.log();
|
|
506
|
+
console.log(` ${bold("Connect your Pushary app")}`);
|
|
507
|
+
console.log(` ${dim("Scan this with your phone camera:")}`);
|
|
508
|
+
await printQr(universalLink);
|
|
509
|
+
console.log(` ${dim("No camera handy? Open this on the phone instead:")}`);
|
|
510
|
+
console.log(` ${cyan(universalLink)}`);
|
|
511
|
+
console.log(` ${dim("Confirm the app shows fingerprint")} ${cyan(publicKeyFingerprint(keypair.publicKeyB64))}`);
|
|
512
|
+
console.log(` ${dim("Already have the app and it opened a browser? Use")} ${cyan(deepLink)}`);
|
|
513
|
+
console.log();
|
|
514
|
+
const deadline = Date.now() + PAIR_TIMEOUT_MS;
|
|
515
|
+
const stop = startSpinner(() => {
|
|
516
|
+
const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
|
|
517
|
+
return `Waiting for the app to authorize ${dim(`(${left}s)`)}`;
|
|
518
|
+
});
|
|
519
|
+
let cancelled = false;
|
|
520
|
+
const onInterrupt = () => {
|
|
521
|
+
cancelled = true;
|
|
522
|
+
stop(yellow("!"), "Cancelled");
|
|
523
|
+
void cancelPairing(pairId).finally(() => process.exit(EXIT_ABORTED));
|
|
524
|
+
};
|
|
525
|
+
process.once("SIGINT", onInterrupt);
|
|
526
|
+
try {
|
|
527
|
+
while (Date.now() < deadline) {
|
|
528
|
+
if (cancelled) return null;
|
|
529
|
+
const claim = await claimPairing(pairId);
|
|
530
|
+
if (claim?.status === "authorized") {
|
|
531
|
+
let apiKey;
|
|
532
|
+
try {
|
|
533
|
+
apiKey = openSealedKey(claim.sealed, keypair);
|
|
534
|
+
} catch {
|
|
535
|
+
stop(yellow("!"), "Pairing failed to decrypt \u2014 re-run setup to try again");
|
|
536
|
+
await cancelPairing(pairId);
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
stop(check, "App connected");
|
|
540
|
+
return { apiKey, handle: claim.siteSlug };
|
|
541
|
+
}
|
|
542
|
+
if (claim?.status === "expired") {
|
|
543
|
+
stop(yellow("!"), "Pairing expired \u2014 re-run setup to try again");
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
await sleep(PAIR_POLL_INTERVAL_MS);
|
|
547
|
+
}
|
|
548
|
+
stop(yellow("!"), "Timed out waiting for the app");
|
|
549
|
+
await cancelPairing(pairId);
|
|
550
|
+
return null;
|
|
551
|
+
} catch (err) {
|
|
552
|
+
stop(yellow("!"), `Pairing error ${dim(`(${err instanceof Error ? err.message : "network"})`)}`);
|
|
553
|
+
await cancelPairing(pairId);
|
|
554
|
+
return null;
|
|
555
|
+
} finally {
|
|
556
|
+
process.off("SIGINT", onInterrupt);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
export {
|
|
561
|
+
reachVerdict,
|
|
562
|
+
describeReach,
|
|
563
|
+
setHumanStream,
|
|
564
|
+
fetchChannels,
|
|
565
|
+
connectDevice,
|
|
566
|
+
confirmAppConnection,
|
|
567
|
+
printConnectInstructions,
|
|
568
|
+
connectViaAppPairing
|
|
569
|
+
};
|
|
@@ -25,10 +25,7 @@ import {
|
|
|
25
25
|
sendNotification,
|
|
26
26
|
throttlePass,
|
|
27
27
|
waitForAnswer
|
|
28
|
-
} from "./chunk-
|
|
29
|
-
import {
|
|
30
|
-
getMachineId
|
|
31
|
-
} from "./chunk-RN3NOEJF.js";
|
|
28
|
+
} from "./chunk-HIIBSYFJ.js";
|
|
32
29
|
import {
|
|
33
30
|
isGatingMoment,
|
|
34
31
|
recordKeylessMoment
|
|
@@ -42,10 +39,13 @@ import {
|
|
|
42
39
|
scopeChangeReason,
|
|
43
40
|
scopeRequiresHuman
|
|
44
41
|
} from "./chunk-DQAN3JQP.js";
|
|
42
|
+
import {
|
|
43
|
+
getMachineId
|
|
44
|
+
} from "./chunk-RN3NOEJF.js";
|
|
45
45
|
import {
|
|
46
46
|
getApiKey,
|
|
47
47
|
getBaseUrl
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-2UMNXADU.js";
|
|
49
49
|
|
|
50
50
|
// src/decision-episode.ts
|
|
51
51
|
var EPISODE_PATH = "/api/agent/decision-episode";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/cli/errors.ts
|
|
2
|
+
var UsageError = class extends Error {
|
|
3
|
+
constructor(hint) {
|
|
4
|
+
super(hint);
|
|
5
|
+
this.hint = hint;
|
|
6
|
+
this.name = "UsageError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var NetworkError = class extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "NetworkError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
UsageError,
|
|
18
|
+
NetworkError
|
|
19
|
+
};
|