@pushary/agent-hooks 0.78.0 → 0.80.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +52 -0
  3. package/data/SKILL.md +18 -5
  4. package/data/cursor-plugin/skills/pushary/SKILL.md +17 -4
  5. package/data/vscode-plugin/skills/pushary/SKILL.md +17 -4
  6. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  7. package/dist/bin/pushary-bell-hook.js +22 -0
  8. package/dist/bin/pushary-bell.d.ts +1 -0
  9. package/dist/bin/pushary-bell.js +203 -0
  10. package/dist/bin/pushary-claude.js +8 -7
  11. package/dist/bin/pushary-clean.js +14 -14
  12. package/dist/bin/pushary-codex-hook.js +8 -7
  13. package/dist/bin/pushary-codex.js +3 -2
  14. package/dist/bin/pushary-connect.js +15 -14
  15. package/dist/bin/pushary-daemon.js +5 -5
  16. package/dist/bin/pushary-doctor.js +34 -64
  17. package/dist/bin/pushary-gemini-hook.js +8 -7
  18. package/dist/bin/pushary-hook.js +11 -10
  19. package/dist/bin/pushary-login.js +15 -14
  20. package/dist/bin/pushary-logout.js +12 -11
  21. package/dist/bin/pushary-mode.js +7 -7
  22. package/dist/bin/pushary-notification-hook.js +3 -2
  23. package/dist/bin/pushary-permission-denied-hook.js +7 -6
  24. package/dist/bin/pushary-permission-hook.js +7 -6
  25. package/dist/bin/pushary-post-hook.js +3 -2
  26. package/dist/bin/pushary-prompt-hook.js +3 -2
  27. package/dist/bin/pushary-session-end-hook.js +3 -2
  28. package/dist/bin/pushary-session-start-hook.js +3 -2
  29. package/dist/bin/pushary-setup.js +131 -41
  30. package/dist/bin/pushary-stats.js +7 -7
  31. package/dist/bin/pushary-status.js +17 -16
  32. package/dist/bin/pushary-stop-hook.js +3 -2
  33. package/dist/bin/pushary-stopfailure-hook.js +3 -2
  34. package/dist/bin/pushary-suggestions.js +4 -4
  35. package/dist/bin/pushary-upgrade.js +10 -10
  36. package/dist/bin/pushary-wait.js +9 -9
  37. package/dist/bin/pushary.js +6 -5
  38. package/dist/chunk-22EQOB2T.js +212 -0
  39. package/dist/{chunk-CC2K2SST.js → chunk-7DYFAM32.js} +1 -1
  40. package/dist/{chunk-FWWBEB6L.js → chunk-A5DAEWBZ.js} +2 -2
  41. package/dist/{chunk-MGFZXUUR.js → chunk-BOMF4C2Q.js} +114 -14
  42. package/dist/{chunk-HPDLDQC5.js → chunk-DUYM5SHH.js} +10 -1
  43. package/dist/{chunk-NHKWU7MN.js → chunk-E6I3DBJW.js} +8 -1
  44. package/dist/{chunk-DLBW37U6.js → chunk-EBP4YG75.js} +1 -1
  45. package/dist/{chunk-NQTRA7H5.js → chunk-GQLB2GBJ.js} +3 -3
  46. package/dist/{chunk-IA5GBKNL.js → chunk-MHA2WB7S.js} +1 -1
  47. package/dist/chunk-PPD3HHNJ.js +44 -0
  48. package/dist/{chunk-QCYN5VXB.js → chunk-Q4QULGAV.js} +1 -1
  49. package/dist/{chunk-3CBPY2G5.js → chunk-R6AYBATA.js} +1 -1
  50. package/dist/{chunk-BBK3TTU2.js → chunk-RRZZ7TFJ.js} +1 -1
  51. package/dist/{chunk-TJJBF73A.js → chunk-T6BNEQ2A.js} +23 -0
  52. package/dist/{chunk-7BBUTWBD.js → chunk-U2PZKYZM.js} +1 -1
  53. package/dist/{chunk-46UEJXQO.js → chunk-UBUPETEQ.js} +2 -2
  54. package/dist/{chunk-NN4GTN6Q.js → chunk-ULITFWLG.js} +8 -8
  55. package/dist/{chunk-IZA7SWXX.js → chunk-WYVKPLVT.js} +3 -3
  56. package/dist/{chunk-7OYGFJYZ.js → chunk-ZUKH2NPJ.js} +18 -0
  57. package/dist/src/index.js +7 -6
  58. package/package.json +4 -2
@@ -0,0 +1,212 @@
1
+ import {
2
+ readConfigFileKey
3
+ } from "./chunk-E6I3DBJW.js";
4
+
5
+ // src/bell/fleet.ts
6
+ import { mkdirSync, readdirSync, statSync, utimesSync, writeFileSync, unlinkSync } from "fs";
7
+ import { createHash } from "crypto";
8
+ import { tmpdir } from "os";
9
+ import { join } from "path";
10
+ var BELL_DIR = join(tmpdir(), "pushary-bell");
11
+ var SESSION_TTL_MS = 10 * 60 * 1e3;
12
+ var sessionFile = (sessionId) => join(BELL_DIR, createHash("sha256").update(sessionId).digest("hex").slice(0, 16));
13
+ var heartbeat = (sessionId, now = Date.now()) => {
14
+ try {
15
+ mkdirSync(BELL_DIR, { recursive: true });
16
+ const path = sessionFile(sessionId);
17
+ writeFileSync(path, "", "utf-8");
18
+ const seconds = now / 1e3;
19
+ utimesSync(path, seconds, seconds);
20
+ } catch {
21
+ }
22
+ };
23
+ var liveAgentCount = (now = Date.now()) => {
24
+ try {
25
+ let live = 0;
26
+ for (const name of readdirSync(BELL_DIR)) {
27
+ const path = join(BELL_DIR, name);
28
+ try {
29
+ if (now - statSync(path).mtimeMs < SESSION_TTL_MS) live += 1;
30
+ else unlinkSync(path);
31
+ } catch {
32
+ }
33
+ }
34
+ return live;
35
+ } catch {
36
+ return 0;
37
+ }
38
+ };
39
+ var recordAndCount = (sessionId, now = Date.now()) => {
40
+ heartbeat(sessionId, now);
41
+ return Math.max(1, liveAgentCount(now));
42
+ };
43
+
44
+ // src/bell/ring.ts
45
+ import { execFile } from "child_process";
46
+ import { openSync, writeSync, closeSync } from "fs";
47
+ var platformOf = (value = process.platform) => value === "darwin" || value === "linux" || value === "win32" ? value : "other";
48
+ var writeBell = () => {
49
+ try {
50
+ const fd = openSync("/dev/tty", "w");
51
+ try {
52
+ writeSync(fd, "\x07");
53
+ return true;
54
+ } finally {
55
+ closeSync(fd);
56
+ }
57
+ } catch {
58
+ try {
59
+ process.stderr.write("\x07");
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ };
66
+ var noticeCommand = (notice, platform = platformOf()) => {
67
+ const clean = (text) => text.replace(/["\\]/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
68
+ const title = clean(notice.title);
69
+ const body = clean(notice.body);
70
+ switch (platform) {
71
+ case "darwin":
72
+ return ["osascript", "-e", `display notification "${body}" with title "${title}"`];
73
+ case "linux":
74
+ return ["notify-send", title, body];
75
+ default:
76
+ return null;
77
+ }
78
+ };
79
+ var spawnNotifier = (argv) => {
80
+ const child = execFile(argv[0], argv.slice(1), { timeout: 5e3 }, () => {
81
+ });
82
+ child.unref?.();
83
+ };
84
+ var ring = (notice, deps = {}) => {
85
+ const bell = deps.bell ?? writeBell;
86
+ const notify = deps.notify ?? spawnNotifier;
87
+ const rang = bell();
88
+ const argv = noticeCommand(notice, deps.platform ?? platformOf());
89
+ if (!argv) return { rang, notified: false };
90
+ try {
91
+ notify(argv);
92
+ return { rang, notified: true };
93
+ } catch {
94
+ return { rang, notified: false };
95
+ }
96
+ };
97
+
98
+ // src/bell/upgrade.ts
99
+ import { existsSync, mkdirSync as mkdirSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
100
+ import { tmpdir as tmpdir2 } from "os";
101
+ import { join as join2 } from "path";
102
+ var DEFAULT_UPGRADE_THRESHOLD = 3;
103
+ var UPGRADE_MARKER = join2(tmpdir2(), "pushary-bell-upgrade-shown");
104
+ var ONCE_PER = 24 * 60 * 60 * 1e3;
105
+ var upgradeThreshold = (env = process.env) => {
106
+ const raw = Number(env.PUSHARY_BELL_UPGRADE_AT);
107
+ if (!Number.isFinite(raw) || raw < 1) return DEFAULT_UPGRADE_THRESHOLD;
108
+ return Math.floor(raw);
109
+ };
110
+ var readLastShown = () => {
111
+ try {
112
+ return existsSync(UPGRADE_MARKER) ? statSync2(UPGRADE_MARKER).mtimeMs : null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ };
117
+ var writeLastShown = (at) => {
118
+ try {
119
+ mkdirSync2(tmpdir2(), { recursive: true });
120
+ writeFileSync2(UPGRADE_MARKER, String(at), "utf-8");
121
+ } catch {
122
+ }
123
+ };
124
+ var upgradeLine = (context) => ` ${context.liveAgents} agents running. A bell can't tell you which one needs you, and it can't reach your pocket.
125
+ pushary connect`;
126
+ var upgradeNotice = (context, deps = {}) => {
127
+ const env = deps.env ?? process.env;
128
+ if (env.PUSHARY_BELL_QUIET === "1" || env.PUSHARY_BELL_QUIET === "true") return null;
129
+ if (context.connected) return null;
130
+ if (context.liveAgents < upgradeThreshold(env)) return null;
131
+ const now = (deps.now ?? Date.now)();
132
+ const last = (deps.lastShown ?? readLastShown)();
133
+ if (last !== null && now - last < ONCE_PER) return null;
134
+ (deps.markShown ?? writeLastShown)(now);
135
+ return upgradeLine(context);
136
+ };
137
+
138
+ // src/bell/handler.ts
139
+ import { basename } from "path";
140
+ var FINISHED_NOTIFICATIONS = /* @__PURE__ */ new Set(["agent_completed"]);
141
+ var BLOCKED_NOTIFICATIONS = /* @__PURE__ */ new Set(["idle_prompt", "agent_needs_input"]);
142
+ var SUMMARY_MAX = 100;
143
+ var summarize = (message, fallback) => {
144
+ if (typeof message !== "string") return fallback;
145
+ const collapsed = message.replace(/\s+/g, " ").trim();
146
+ return collapsed ? collapsed.slice(0, SUMMARY_MAX) : fallback;
147
+ };
148
+ var decideBell = (input) => {
149
+ const project = basename(input.cwd ?? process.cwd());
150
+ const notifType = input.notification_type ?? input.type ?? "";
151
+ if (FINISHED_NOTIFICATIONS.has(notifType)) {
152
+ return {
153
+ reason: "finished",
154
+ title: `${project}: done`,
155
+ body: summarize(input.message ?? input.last_assistant_message, "Your agent finished.")
156
+ };
157
+ }
158
+ if (BLOCKED_NOTIFICATIONS.has(notifType)) {
159
+ return {
160
+ reason: "needs-input",
161
+ title: `${project}: your agent needs you`,
162
+ body: summarize(input.message, "Waiting for your input.")
163
+ };
164
+ }
165
+ return null;
166
+ };
167
+ var handleBell = (input, deps = {}) => {
168
+ const idle = { rang: false, reason: null, liveAgents: 0, upgrade: null };
169
+ try {
170
+ const decision = decideBell(input);
171
+ if (!decision) return idle;
172
+ const count = deps.count ?? ((sessionId) => recordAndCount(sessionId));
173
+ const liveAgents = count(input.session_id || "default");
174
+ const result = (deps.ring ?? ring)({ title: decision.title, body: decision.body }, deps.ringDeps);
175
+ const notice = upgradeNotice(
176
+ {
177
+ agentName: decision.title,
178
+ liveAgents,
179
+ connected: (deps.connected ?? hasKey)()
180
+ },
181
+ deps.upgradeDeps
182
+ );
183
+ if (notice) (deps.write ?? writeStderr)(`
184
+ ${notice}
185
+ `);
186
+ return { rang: result.rang, reason: decision.reason, liveAgents, upgrade: notice };
187
+ } catch {
188
+ return idle;
189
+ }
190
+ };
191
+ var writeStderr = (line) => {
192
+ try {
193
+ process.stderr.write(line);
194
+ } catch {
195
+ }
196
+ };
197
+ var hasKey = () => {
198
+ try {
199
+ if (process.env.PUSHARY_API_KEY?.trim()) return true;
200
+ return Boolean(readConfigFileKey());
201
+ } catch {
202
+ return false;
203
+ }
204
+ };
205
+
206
+ export {
207
+ liveAgentCount,
208
+ ring,
209
+ DEFAULT_UPGRADE_THRESHOLD,
210
+ upgradeThreshold,
211
+ handleBell
212
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-TJJBF73A.js";
3
+ } from "./chunk-T6BNEQ2A.js";
4
4
 
5
5
  // src/gemini-config.ts
6
6
  var GEMINI_HOOK_BINARY = "pushary-gemini-hook";
@@ -4,12 +4,12 @@ import {
4
4
  isModeStateDegraded,
5
5
  resolveAutoResolveOrigin,
6
6
  resolvePolicyAcross
7
- } from "./chunk-HPDLDQC5.js";
7
+ } from "./chunk-DUYM5SHH.js";
8
8
  import {
9
9
  evaluateScope,
10
10
  scopeChangeReason,
11
11
  scopeRequiresHuman
12
- } from "./chunk-TJJBF73A.js";
12
+ } from "./chunk-T6BNEQ2A.js";
13
13
 
14
14
  // src/answer.ts
15
15
  var denyReasonFrom = (value, note) => {
@@ -6,6 +6,9 @@ import {
6
6
  import {
7
7
  callMcpTool
8
8
  } from "./chunk-BSZYIAZL.js";
9
+ import {
10
+ reportedInstallSource
11
+ } from "./chunk-PPD3HHNJ.js";
9
12
 
10
13
  // src/reach.ts
11
14
  var reachVerdict = (channels) => {
@@ -67,19 +70,6 @@ var describeAppRepair = (verdict) => {
67
70
  }
68
71
  };
69
72
 
70
- // src/cli/human.ts
71
- var stream = process.stdout;
72
- var setHumanStream = (target) => {
73
- stream = target;
74
- };
75
- var writeHuman = (text) => {
76
- stream.write(text);
77
- };
78
- var humanIsTty = () => stream === process.stdout && process.stdout.isTTY === true;
79
-
80
- // src/onboarding.ts
81
- import qrcodeTerminal from "qrcode-terminal";
82
-
83
73
  // src/setup/verify.ts
84
74
  var plural2 = (count, one) => `${count} ${one}${count === 1 ? "" : "s"}`;
85
75
  var describeRecipients = (push, channels) => {
@@ -168,8 +158,111 @@ var describeVerifyOutcome = (outcome) => {
168
158
  };
169
159
  }
170
160
  };
161
+ var ROUND_TRIP_QUESTION = "Setup is nearly done. Tap Yes and this machine is connected.";
162
+ var ROUND_TRIP_TIMEOUT_MS = 55e3;
163
+ var defaultAsk = (apiKey, agentName) => (
164
+ // wait:false so the poll is ours: the blocking form returns on the SITE's
165
+ // policy timeout, which a user may have set to seconds, and a setup check
166
+ // reporting "unanswered" after three seconds would be a lie about the product.
167
+ callMcpTool(apiKey, "ask_user", {
168
+ question: ROUND_TRIP_QUESTION,
169
+ type: "confirm",
170
+ agentName,
171
+ wait: false
172
+ }, { timeoutMs: 15e3 })
173
+ );
174
+ var defaultWait = (apiKey, correlationId) => callMcpTool(apiKey, "wait_for_answer", {
175
+ correlationId,
176
+ timeoutMs: ROUND_TRIP_TIMEOUT_MS
177
+ }, { timeoutMs: ROUND_TRIP_TIMEOUT_MS + 5e3 });
178
+ var defaultCancel = async (apiKey, correlationId) => {
179
+ await callMcpTool(apiKey, "cancel_question", { correlationId }, { timeoutMs: 1e4 }).catch(() => {
180
+ });
181
+ };
182
+ var verifyRoundTrip = async (apiKey, agentName = "Pushary setup", deps = {}) => {
183
+ const ask = deps.ask ?? defaultAsk;
184
+ const wait = deps.wait ?? defaultWait;
185
+ const cancel = deps.cancel ?? defaultCancel;
186
+ const now = deps.now ?? Date.now;
187
+ const started = now();
188
+ let asked;
189
+ try {
190
+ asked = await ask(apiKey, agentName);
191
+ } catch (err) {
192
+ return { kind: "error", detail: err instanceof Error ? err.message : "ask failed" };
193
+ }
194
+ if (asked.noDevices || asked.warning) return { kind: "no-recipients" };
195
+ if (asked.mode === "notify_only" || asked.status === "notified") return { kind: "notify-only" };
196
+ if (!asked.correlationId) return { kind: "error", detail: "no correlationId returned" };
197
+ if (asked.answered === true) {
198
+ return { kind: "answered", value: asked.value ?? "yes", latencyMs: now() - started };
199
+ }
200
+ let answer;
201
+ try {
202
+ answer = await wait(apiKey, asked.correlationId);
203
+ } catch (err) {
204
+ await cancel(apiKey, asked.correlationId);
205
+ return { kind: "error", detail: err instanceof Error ? err.message : "wait failed" };
206
+ }
207
+ if (answer.answered === true) {
208
+ return { kind: "answered", value: answer.value ?? "yes", latencyMs: now() - started };
209
+ }
210
+ await cancel(apiKey, asked.correlationId);
211
+ return { kind: "unanswered" };
212
+ };
213
+ var describeRoundTrip = (outcome) => {
214
+ switch (outcome.kind) {
215
+ case "answered":
216
+ return {
217
+ ok: true,
218
+ message: `Answered "${outcome.value}" in ${(outcome.latencyMs / 1e3).toFixed(1)}s. Your agent can reach you and you can answer it.`,
219
+ next: []
220
+ };
221
+ case "unanswered":
222
+ return {
223
+ ok: false,
224
+ message: "The question was sent and nothing came back.",
225
+ next: [
226
+ "A push that arrives and cannot be answered is the most common way this breaks, so this is worth resolving now.",
227
+ "Check the notification actually opened something you could tap, then run pushary doctor --roundtrip."
228
+ ]
229
+ };
230
+ case "notify-only":
231
+ return {
232
+ ok: true,
233
+ message: "Sent. This site is in notify-only mode, so questions never wait for an answer.",
234
+ next: ["Switch with pushary mode push_first if you want approvals to hold for your phone."]
235
+ };
236
+ case "no-recipients":
237
+ return {
238
+ ok: false,
239
+ message: "Nothing to ask. No phone or browser is connected to this site.",
240
+ next: [
241
+ "Run pushary connect to add your phone.",
242
+ "Or pushary connect --app if you already have the Pushary app."
243
+ ]
244
+ };
245
+ case "error":
246
+ return {
247
+ ok: false,
248
+ message: `Could not complete the round trip (${outcome.detail}).`,
249
+ next: ["Agents are configured. Run pushary doctor --roundtrip once the connection is back."]
250
+ };
251
+ }
252
+ };
253
+
254
+ // src/cli/human.ts
255
+ var stream = process.stdout;
256
+ var setHumanStream = (target) => {
257
+ stream = target;
258
+ };
259
+ var writeHuman = (text) => {
260
+ stream.write(text);
261
+ };
262
+ var humanIsTty = () => stream === process.stdout && process.stdout.isTTY === true;
171
263
 
172
264
  // src/onboarding.ts
265
+ import qrcodeTerminal from "qrcode-terminal";
173
266
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
174
267
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
175
268
  var green = (s) => `\x1B[32m${s}\x1B[0m`;
@@ -528,7 +621,12 @@ var startPairing = async (cliPublicKey) => {
528
621
  const res = await fetch(`${apiBase()}/api/mobile/pair/start`, {
529
622
  method: "POST",
530
623
  headers: { "Content-Type": "application/json" },
531
- body: JSON.stringify({ cliPublicKey }),
624
+ // The only moment this fact can be carried across the handshake. This
625
+ // terminal knows which front door it came through and has no session; the
626
+ // phone that authorizes has the session and mints the key but never saw
627
+ // this terminal. Sending it here is what lets the minted key name its
628
+ // origin. An older server ignores the extra field.
629
+ body: JSON.stringify({ cliPublicKey, installSource: reportedInstallSource() }),
532
630
  signal: AbortSignal.timeout(1e4)
533
631
  });
534
632
  if (!res.ok) return null;
@@ -687,6 +785,8 @@ var connectViaAppPairing = async (options = {}) => {
687
785
  export {
688
786
  reachVerdict,
689
787
  describeReach,
788
+ verifyRoundTrip,
789
+ describeRoundTrip,
690
790
  setHumanStream,
691
791
  fetchChannels,
692
792
  connectDevice,
@@ -5,6 +5,9 @@ import {
5
5
  import {
6
6
  installProxyDispatcher
7
7
  } from "./chunk-SAF6HGAA.js";
8
+ import {
9
+ reportedInstallSource
10
+ } from "./chunk-PPD3HHNJ.js";
8
11
  import {
9
12
  ACTION_BODY_MAX,
10
13
  DECISION_LINE_MAX,
@@ -20,7 +23,7 @@ import {
20
23
  policyArgForms,
21
24
  redactSecrets,
22
25
  redactSecretsDeep
23
- } from "./chunk-TJJBF73A.js";
26
+ } from "./chunk-T6BNEQ2A.js";
24
27
  import {
25
28
  getMachineId
26
29
  } from "./chunk-RN3NOEJF.js";
@@ -1033,6 +1036,12 @@ var reportEvent = async (event, options = {}) => {
1033
1036
  pid: event.pid ?? process.pid,
1034
1037
  repoKey: event.repoKey ?? repoKeyFor(),
1035
1038
  installMode: detectInstallMode(),
1039
+ // How the binary was INVOKED this time and which front door produced the
1040
+ // install are different questions, and only the second is attribution.
1041
+ // A machine installed from the plugin marketplace that later runs
1042
+ // `npx ... doctor` reports npx for that invocation and claude_plugin as
1043
+ // its origin, permanently.
1044
+ installSource: reportedInstallSource(),
1036
1045
  // Link-back for a phone-spawned session: forward the spawn id the daemon
1037
1046
  // set in the environment so the server can map spawnId -> sessionId.
1038
1047
  ...process.env.PUSHARY_SPAWN_ID ? { spawnId: process.env.PUSHARY_SPAWN_ID } : {},
@@ -7,6 +7,9 @@ import {
7
7
  import {
8
8
  resolveShellRc
9
9
  } from "./chunk-BC3VCZ3E.js";
10
+ import {
11
+ resolveInstallSourceFor
12
+ } from "./chunk-PPD3HHNJ.js";
10
13
  import {
11
14
  configFilePath
12
15
  } from "./chunk-2UMNXADU.js";
@@ -116,7 +119,11 @@ var writeKey = (apiKey, options = {}) => {
116
119
  } else if (existing.kind === "unparseable") {
117
120
  configBackupPath = backupFile(configPath);
118
121
  }
119
- writeJsonAtomic(configPath, { ...current, apiKey }, KEY_FILE_MODE);
122
+ writeJsonAtomic(
123
+ configPath,
124
+ { ...current, apiKey, installSource: resolveInstallSourceFor(current) },
125
+ KEY_FILE_MODE
126
+ );
120
127
  restrictFile(configPath);
121
128
  const target = (options.resolveRc ?? resolveRcTarget)();
122
129
  const rc = target ? rewriteRcExport(target.path, apiKey, target.shell) : rcResult("no-rc");
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-7QLSKOSU.js";
7
7
  import {
8
8
  isValidApiKey
9
- } from "./chunk-TJJBF73A.js";
9
+ } from "./chunk-T6BNEQ2A.js";
10
10
  import {
11
11
  getBaseUrl
12
12
  } from "./chunk-2UMNXADU.js";
@@ -1,9 +1,9 @@
1
- import {
2
- getPackageVersion
3
- } from "./chunk-7OYGFJYZ.js";
4
1
  import {
5
2
  getMachineId
6
3
  } from "./chunk-RN3NOEJF.js";
4
+ import {
5
+ getPackageVersion
6
+ } from "./chunk-ZUKH2NPJ.js";
7
7
  import {
8
8
  getBaseUrl
9
9
  } from "./chunk-2UMNXADU.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-TJJBF73A.js";
3
+ } from "./chunk-T6BNEQ2A.js";
4
4
 
5
5
  // src/ledger.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
@@ -0,0 +1,44 @@
1
+ import {
2
+ normalizeInstallSource
3
+ } from "./chunk-T6BNEQ2A.js";
4
+ import {
5
+ configFilePath
6
+ } from "./chunk-2UMNXADU.js";
7
+
8
+ // src/install-source.ts
9
+ import { readFileSync } from "fs";
10
+ var ENV_OVERRIDE = "PUSHARY_INSTALL_SOURCE";
11
+ var PLUGIN_ROOT = "CLAUDE_PLUGIN_ROOT";
12
+ var EDITOR_PLUGIN_MARKERS = ["cursor-plugin", "vscode-plugin"];
13
+ var detectInstallSource = (env = process.env, argv = process.argv) => {
14
+ const declared = env[ENV_OVERRIDE]?.trim();
15
+ if (declared) return normalizeInstallSource(declared);
16
+ const pluginRoot = env[PLUGIN_ROOT]?.trim();
17
+ if (pluginRoot) {
18
+ return EDITOR_PLUGIN_MARKERS.some((marker) => pluginRoot.includes(marker)) ? "editor_plugin" : "claude_plugin";
19
+ }
20
+ const entry = argv[1] ?? "";
21
+ if (entry.includes("_npx")) return "npx";
22
+ if (entry) return "npm";
23
+ return "unknown";
24
+ };
25
+ var storedInstallSource = () => {
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(configFilePath(), "utf-8"));
28
+ const stored = parsed.installSource;
29
+ return typeof stored === "string" && stored ? normalizeInstallSource(stored) : void 0;
30
+ } catch {
31
+ return void 0;
32
+ }
33
+ };
34
+ var resolveInstallSourceFor = (config) => {
35
+ const stored = config.installSource;
36
+ if (typeof stored === "string" && stored) return normalizeInstallSource(stored);
37
+ return detectInstallSource();
38
+ };
39
+ var reportedInstallSource = () => storedInstallSource() ?? detectInstallSource();
40
+
41
+ export {
42
+ resolveInstallSourceFor,
43
+ reportedInstallSource
44
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-TJJBF73A.js";
3
+ } from "./chunk-T6BNEQ2A.js";
4
4
 
5
5
  // src/codex-config.ts
6
6
  import { createHash } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-TJJBF73A.js";
3
+ } from "./chunk-T6BNEQ2A.js";
4
4
 
5
5
  // src/claude-config.ts
6
6
  import { join } from "path";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getPackageVersion,
3
3
  renderCommandHelp
4
- } from "./chunk-7OYGFJYZ.js";
4
+ } from "./chunk-ZUKH2NPJ.js";
5
5
  import {
6
6
  EXIT
7
7
  } from "./chunk-KZERVKTD.js";
@@ -1,6 +1,28 @@
1
1
  // ../contracts/src/index.ts
2
2
  var APPROVAL_MODES = ["push_only", "terminal_only", "push_first", "notify_only"];
3
3
  var isApprovalMode = (value) => typeof value === "string" && APPROVAL_MODES.includes(value);
4
+ var INSTALL_SOURCES = [
5
+ /** `npx @pushary/agent-hooks@latest ...`, the documented one-liner. */
6
+ "npx",
7
+ /** A global or project npm install, invoked as `pushary`. */
8
+ "npm",
9
+ /** Claude Code plugin, from the marketplace. Detected via CLAUDE_PLUGIN_ROOT. */
10
+ "claude_plugin",
11
+ /** Cursor or VS Code plugin bundle. */
12
+ "editor_plugin",
13
+ /** Installed by the skills.sh wizard. */
14
+ "skills_sh",
15
+ /** An MCP client configured straight from the registry entry, no CLI involved. */
16
+ "mcp_registry",
17
+ /** One of the published framework adapters (ai-sdk, mastra, langgraph, ...). */
18
+ "framework_adapter",
19
+ /** The browser onboarding wizard. */
20
+ "dashboard",
21
+ /** Reported nothing, or reported something we do not recognise. */
22
+ "unknown"
23
+ ];
24
+ var isInstallSource = (value) => typeof value === "string" && INSTALL_SOURCES.includes(value);
25
+ var normalizeInstallSource = (value) => isInstallSource(value) ? value : "unknown";
4
26
  var HOOK_BUDGETS = {
5
27
  claude: { budgetSeconds: 120, guardSeconds: 10 },
6
28
  codex: { budgetSeconds: 180, guardSeconds: 10 },
@@ -760,6 +782,7 @@ var isScopeContract = (value) => {
760
782
 
761
783
  export {
762
784
  isApprovalMode,
785
+ normalizeInstallSource,
763
786
  HOOK_BUDGETS,
764
787
  hookWaitDeadline,
765
788
  hookWaitClamped,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  describeKeyCheck,
3
3
  keyCheckFromResponse
4
- } from "./chunk-DLBW37U6.js";
4
+ } from "./chunk-EBP4YG75.js";
5
5
  import {
6
6
  createIo
7
7
  } from "./chunk-TX7KBKT7.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  hasGeminiHooks
3
- } from "./chunk-CC2K2SST.js";
3
+ } from "./chunk-7DYFAM32.js";
4
4
  import {
5
5
  hasCodexHooks
6
- } from "./chunk-QCYN5VXB.js";
6
+ } from "./chunk-Q4QULGAV.js";
7
7
 
8
8
  // src/diagnostics/wiring.ts
9
9
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-3CBPY2G5.js";
3
+ } from "./chunk-R6AYBATA.js";
4
+ import {
5
+ isGatingMoment,
6
+ recordKeylessMoment
7
+ } from "./chunk-MHA2WB7S.js";
4
8
  import {
5
9
  denyReasonFrom,
6
10
  isDeferAnswer,
7
11
  resolveGate
8
- } from "./chunk-FWWBEB6L.js";
12
+ } from "./chunk-A5DAEWBZ.js";
9
13
  import {
10
14
  DEFAULT_SESSION,
11
15
  askUser,
@@ -25,17 +29,13 @@ import {
25
29
  sendNotification,
26
30
  throttlePass,
27
31
  waitForAnswer
28
- } from "./chunk-HPDLDQC5.js";
29
- import {
30
- isGatingMoment,
31
- recordKeylessMoment
32
- } from "./chunk-IA5GBKNL.js";
32
+ } from "./chunk-DUYM5SHH.js";
33
33
  import {
34
34
  buildDecisionEpisodeFeatures,
35
35
  effectiveWaitSeconds,
36
36
  hookWaitClamped,
37
37
  hookWaitDeadline
38
- } from "./chunk-TJJBF73A.js";
38
+ } from "./chunk-T6BNEQ2A.js";
39
39
  import {
40
40
  getMachineId
41
41
  } from "./chunk-RN3NOEJF.js";