@pushary/agent-hooks 0.89.12 → 0.90.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 (82) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +1 -0
  3. package/data/SKILL.md +1 -1
  4. package/data/cursor-plugin/skills/pushary/SKILL.md +1 -1
  5. package/data/vscode-plugin/skills/pushary/SKILL.md +1 -1
  6. package/dist/bin/pushary-bell-hook.js +4 -4
  7. package/dist/bin/pushary-bell.js +8 -8
  8. package/dist/bin/pushary-claude.js +194 -38
  9. package/dist/bin/pushary-clean.js +15 -32
  10. package/dist/bin/pushary-codex-bridge.js +37 -12
  11. package/dist/bin/pushary-codex-hook.js +45 -12
  12. package/dist/bin/pushary-codex.js +7 -7
  13. package/dist/bin/pushary-connect.js +11 -10
  14. package/dist/bin/pushary-daemon.js +186 -62
  15. package/dist/bin/pushary-disconnect.js +8 -8
  16. package/dist/bin/pushary-doctor.js +108 -25
  17. package/dist/bin/pushary-elicitation-hook.js +4 -4
  18. package/dist/bin/pushary-gemini-bridge.js +33 -11
  19. package/dist/bin/pushary-gemini-hook.js +21 -10
  20. package/dist/bin/pushary-hook.js +13 -13
  21. package/dist/bin/pushary-login.js +18 -7
  22. package/dist/bin/pushary-logout.js +6 -6
  23. package/dist/bin/pushary-mode.js +28 -21
  24. package/dist/bin/pushary-notification-hook.js +7 -7
  25. package/dist/bin/pushary-opencode-hook.js +8 -8
  26. package/dist/bin/pushary-permission-denied-hook.js +11 -11
  27. package/dist/bin/pushary-permission-hook.js +11 -11
  28. package/dist/bin/pushary-post-hook.js +14 -8
  29. package/dist/bin/pushary-prompt-hook.js +7 -7
  30. package/dist/bin/pushary-session-end-hook.js +7 -7
  31. package/dist/bin/pushary-session-start-hook.js +7 -7
  32. package/dist/bin/pushary-setup.js +168 -45
  33. package/dist/bin/pushary-stats.js +4 -4
  34. package/dist/bin/pushary-status.js +11 -10
  35. package/dist/bin/pushary-stop-hook.js +14 -8
  36. package/dist/bin/pushary-stopfailure-hook.js +7 -7
  37. package/dist/bin/pushary-suggestions.js +2 -2
  38. package/dist/bin/pushary-transcripts.d.ts +1 -0
  39. package/dist/bin/pushary-transcripts.js +79 -0
  40. package/dist/bin/pushary-upgrade.js +15 -15
  41. package/dist/bin/pushary-wait.js +5 -5
  42. package/dist/bin/pushary.js +3 -2
  43. package/dist/{chunk-KVHP5ZME.js → chunk-2LAHXHBR.js} +1 -1
  44. package/dist/{chunk-RLQWNEPM.js → chunk-43GMYSS4.js} +15 -0
  45. package/dist/{chunk-5NDB3YOE.js → chunk-54ELC7D7.js} +1 -1
  46. package/dist/{chunk-VPLQFXCH.js → chunk-5NAZ7SFS.js} +1 -1
  47. package/dist/chunk-7FE2KABY.js +156 -0
  48. package/dist/{chunk-DHR4F2CR.js → chunk-7TPQEWUZ.js} +1 -1
  49. package/dist/{chunk-Y4WVZIEI.js → chunk-AJYV3VGU.js} +1 -1
  50. package/dist/{chunk-QFJZHVVL.js → chunk-AZPIQG4G.js} +35 -1
  51. package/dist/{chunk-OCMOTJTW.js → chunk-B7UZHEBL.js} +29 -22
  52. package/dist/{chunk-7HYJPOGG.js → chunk-BQBEH6TV.js} +5 -5
  53. package/dist/{chunk-OTPJ4FUR.js → chunk-BVBEURWJ.js} +1 -1
  54. package/dist/{chunk-7DK3ZH3N.js → chunk-CELX75MW.js} +1 -1
  55. package/dist/{chunk-6VXNA5JB.js → chunk-CNPAT3HL.js} +3 -2
  56. package/dist/{chunk-327YMKP5.js → chunk-DYP6SMOR.js} +1 -1
  57. package/dist/chunk-DZDI62LY.js +145 -0
  58. package/dist/{chunk-VF3RTVWY.js → chunk-EZIEXHYM.js} +15 -7
  59. package/dist/{chunk-PS7DQ2SU.js → chunk-F2ICEWZL.js} +101 -19
  60. package/dist/{chunk-3XSP5SML.js → chunk-FQL5E6NG.js} +2 -2
  61. package/dist/{chunk-JHJHCMAF.js → chunk-FXEVRLHC.js} +20 -2
  62. package/dist/{chunk-6V7ZDH3A.js → chunk-GL3FFM7A.js} +7 -4
  63. package/dist/{chunk-NBRM3FMD.js → chunk-H2QZRQR2.js} +1 -1
  64. package/dist/{chunk-GKPURU7P.js → chunk-I463AEG3.js} +1 -1
  65. package/dist/{chunk-XREW7RQ3.js → chunk-JW4KO5AF.js} +1 -1
  66. package/dist/{chunk-OVEGEQSB.js → chunk-KUZWXPUS.js} +3 -2
  67. package/dist/{chunk-JVZX5YRJ.js → chunk-L4E2BIJ2.js} +80 -7
  68. package/dist/{chunk-RNKPLUWC.js → chunk-OSALSIUF.js} +1 -1
  69. package/dist/{chunk-MFNQ5IMW.js → chunk-PYA4FLKI.js} +32 -21
  70. package/dist/chunk-QCGNGEGE.js +15 -0
  71. package/dist/chunk-S6LYP7QA.js +127 -0
  72. package/dist/{chunk-OADGBOP7.js → chunk-TDZFPMSZ.js} +70 -11
  73. package/dist/{chunk-JJV5Q4KG.js → chunk-UZNKVRLE.js} +1 -1
  74. package/dist/{chunk-FCUUVHTS.js → chunk-VHWCVD42.js} +1 -1
  75. package/dist/{chunk-3MCEES6Z.js → chunk-VNMKIJZB.js} +15 -2
  76. package/dist/{chunk-6GU55AQQ.js → chunk-Y6BWPERV.js} +90 -7
  77. package/dist/chunk-YKLCWWEI.js +76 -0
  78. package/dist/src/index.d.ts +3 -0
  79. package/dist/src/index.js +11 -11
  80. package/package.json +3 -2
  81. package/dist/chunk-NESKLV6R.js +0 -72
  82. package/dist/chunk-WDVQ6O4C.js +0 -84
@@ -91,7 +91,7 @@ var INSTALL_SOURCES = [
91
91
  var isInstallSource = (value) => typeof value === "string" && INSTALL_SOURCES.includes(value);
92
92
  var normalizeInstallSource = (value) => isInstallSource(value) ? value : "unknown";
93
93
  var HOOK_BUDGETS = {
94
- claude: { budgetSeconds: 120, guardSeconds: 10 },
94
+ claude: { budgetSeconds: 600, guardSeconds: 10 },
95
95
  codex: { budgetSeconds: 180, guardSeconds: 10 },
96
96
  gemini: { budgetSeconds: 180, guardSeconds: 10 },
97
97
  // The Cursor gate is a dependency-free .mjs that cannot import this module; it
@@ -1054,6 +1054,8 @@ var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repo
1054
1054
  };
1055
1055
  var isModeStateDegraded = (state) => state?.degraded === true;
1056
1056
  var KILL_REASON = "Stopped by user, this agent was halted from Pushary";
1057
+ var PUSHARY_TOOL_PREFIX = "mcp__pushary__";
1058
+ var isPusharyOwnTool = (toolName) => toolName.startsWith(PUSHARY_TOOL_PREFIX);
1057
1059
  var strictestScopeVerdict = (scope, paths) => {
1058
1060
  if (paths.length === 0) return evaluateScope(scope, void 0);
1059
1061
  let worst = { outcome: "in_scope" };
@@ -1085,6 +1087,13 @@ var ASKED_IN_EVERY_MODE = new Set(ASKED_IN_EVERY_MODE_TOOLS);
1085
1087
  var isAskedInEveryMode = (toolName) => ASKED_IN_EVERY_MODE.has(toolName);
1086
1088
  var resolveGate = (input) => {
1087
1089
  const { modeState, config, toolName, toolInputs, cwd, repoKey, sessionId } = input;
1090
+ if (isPusharyOwnTool(toolName)) {
1091
+ return {
1092
+ kind: "defer",
1093
+ policy: resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey, sessionId),
1094
+ reason: "own_tool"
1095
+ };
1096
+ }
1088
1097
  if (modeState.kill) return { kind: "kill", reason: KILL_REASON };
1089
1098
  const policy = resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey, sessionId);
1090
1099
  const verdict = verdictForPolicy(policy);
@@ -1128,9 +1137,11 @@ var modeStateFromResponse = (data) => {
1128
1137
  // Validated rather than cast: a malformed contract must read as no contract,
1129
1138
  // never as one that silently matches nothing.
1130
1139
  scope: isScopeContract(data.scope) ? data.scope : null,
1131
- degraded: false
1140
+ degraded: false,
1141
+ askHandoff: data.askHandoff === true
1132
1142
  };
1133
1143
  };
1144
+ var stoppedOutput = (output, stopReason) => ({ ...output, continue: false, stopReason });
1134
1145
  var claudePreToolUseOutput = (decision, reason, updatedInput) => ({
1135
1146
  hookSpecificOutput: {
1136
1147
  hookEventName: "PreToolUse",
@@ -1362,10 +1373,12 @@ export {
1362
1373
  resolveAutoResolveOrigin,
1363
1374
  resolvePolicy,
1364
1375
  KILL_REASON,
1376
+ isPusharyOwnTool,
1365
1377
  shouldDeferToNativeMode,
1366
1378
  ASKED_IN_EVERY_MODE_TOOLS,
1367
1379
  resolveGate,
1368
1380
  modeStateFromResponse,
1381
+ stoppedOutput,
1369
1382
  claudePreToolUseOutput,
1370
1383
  PENDING_COMMAND_PREFIX,
1371
1384
  pendingCommandEvents,
@@ -1,22 +1,23 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-OVEGEQSB.js";
3
+ } from "./chunk-KUZWXPUS.js";
4
4
  import {
5
5
  readJsonSafe
6
6
  } from "./chunk-4LL6LG5X.js";
7
7
 
8
8
  // src/process-ownership.ts
9
9
  import { createHash, randomUUID } from "crypto";
10
- import { mkdirSync, readdirSync, unlinkSync, writeFileSync } from "fs";
10
+ import { mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
11
11
  import { createConnection, createServer } from "net";
12
12
  import { join } from "path";
13
13
  var MAX_PROOF_BYTES = 1024;
14
14
  var ownershipDir = (options) => options.dir ?? join(pusharyDir(), "processes");
15
15
  var proofPath = (dir, sessionId) => join(dir, `${createHash("sha256").update(sessionId).digest("hex")}.json`);
16
+ var isOptionalTimestamp = (value) => value === void 0 || typeof value === "string" && Number.isFinite(Date.parse(value));
16
17
  var isProof = (value) => {
17
18
  if (!value || typeof value !== "object") return false;
18
19
  const proof = value;
19
- return typeof proof.sessionId === "string" && Number.isInteger(proof.pid) && proof.pid > 1 && Number.isInteger(proof.port) && proof.port > 0 && proof.port <= 65535 && typeof proof.token === "string" && proof.token.length > 0 && (proof.spawnId === void 0 || typeof proof.spawnId === "string");
20
+ return typeof proof.sessionId === "string" && Number.isInteger(proof.pid) && proof.pid > 1 && Number.isInteger(proof.port) && proof.port > 0 && proof.port <= 65535 && typeof proof.token === "string" && proof.token.length > 0 && (proof.spawnId === void 0 || typeof proof.spawnId === "string") && isOptionalTimestamp(proof.startedAt) && isOptionalTimestamp(proof.heartbeatAt);
20
21
  };
21
22
  var removeProof = (path, token) => {
22
23
  const current = readJsonSafe(path);
@@ -34,16 +35,74 @@ var processIsAlive = (pid) => {
34
35
  return error.code === "EPERM";
35
36
  }
36
37
  };
37
- var pruneProofs = (dir) => {
38
+ var fileTimes = (path) => {
39
+ try {
40
+ const stat = statSync(path);
41
+ return { createdAt: stat.birthtimeMs || stat.mtimeMs, modifiedAt: stat.mtimeMs };
42
+ } catch {
43
+ return { createdAt: 0, modifiedAt: 0 };
44
+ }
45
+ };
46
+ var parseTimestamp = (value) => {
47
+ if (value === void 0) return null;
48
+ const parsed = Date.parse(value);
49
+ return Number.isFinite(parsed) ? parsed : null;
50
+ };
51
+ var proofEntry = (proof, path) => {
52
+ const times = fileTimes(path);
53
+ const recordedStart = parseTimestamp(proof.startedAt);
54
+ const startedAt = recordedStart ?? times.createdAt;
55
+ return {
56
+ sessionId: proof.sessionId,
57
+ pid: proof.pid,
58
+ spawnId: proof.spawnId ?? null,
59
+ startedAt,
60
+ lastSeenAt: parseTimestamp(proof.heartbeatAt) ?? recordedStart ?? times.modifiedAt,
61
+ alive: processIsAlive(proof.pid)
62
+ };
63
+ };
64
+ var safeUnlink = (path) => {
65
+ try {
66
+ unlinkSync(path);
67
+ } catch {
68
+ }
69
+ };
70
+ var listProcessProofs = (options = {}) => {
71
+ const dir = ownershipDir(options);
72
+ const entries = [];
38
73
  try {
39
74
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
40
75
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
41
76
  const path = join(dir, entry.name);
42
77
  const stored = readJsonSafe(path);
43
- if (stored.kind !== "ok" || !isProof(stored.value) || !processIsAlive(stored.value.pid)) unlinkSync(path);
78
+ if (stored.kind !== "ok" || !isProof(stored.value)) {
79
+ if (options.reapDead) safeUnlink(path);
80
+ continue;
81
+ }
82
+ const listed = proofEntry(stored.value, path);
83
+ if (!listed.alive && options.reapDead) safeUnlink(path);
84
+ entries.push(listed);
44
85
  }
45
86
  } catch {
46
87
  }
88
+ return entries;
89
+ };
90
+ var pruneProofs = (dir) => {
91
+ listProcessProofs({ dir, reapDead: true });
92
+ };
93
+ var wasSpawned = (entry) => entry.spawnId !== null;
94
+ var silentSpawnedProofs = (entries, now, silenceMs) => {
95
+ const freshestByPid = /* @__PURE__ */ new Map();
96
+ for (const entry of entries) {
97
+ freshestByPid.set(entry.pid, Math.max(freshestByPid.get(entry.pid) ?? 0, entry.lastSeenAt));
98
+ }
99
+ const reported = /* @__PURE__ */ new Set();
100
+ return entries.filter((entry) => {
101
+ if (!entry.alive || !wasSpawned(entry) || reported.has(entry.pid)) return false;
102
+ if (now - (freshestByPid.get(entry.pid) ?? 0) < silenceMs) return false;
103
+ reported.add(entry.pid);
104
+ return true;
105
+ });
47
106
  };
48
107
  var startProcessOwnership = (identity, options = {}) => {
49
108
  const dir = ownershipDir(options);
@@ -88,7 +147,7 @@ var startProcessOwnership = (identity, options = {}) => {
88
147
  return;
89
148
  }
90
149
  try {
91
- proof = { ...identity, port: address.port, token };
150
+ proof = { ...identity, port: address.port, token, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
92
151
  mkdirSync(dir, { recursive: true, mode: 448 });
93
152
  pruneProofs(dir);
94
153
  writeFileSync(path, JSON.stringify(proof), {
@@ -107,6 +166,14 @@ var startProcessOwnership = (identity, options = {}) => {
107
166
  process.once("exit", cleanup);
108
167
  return {
109
168
  ready,
169
+ touch(at = Date.now()) {
170
+ if (stopped || !proof) return;
171
+ proof = { ...proof, heartbeatAt: new Date(at).toISOString() };
172
+ try {
173
+ writeFileSync(path, JSON.stringify(proof), { encoding: "utf8", mode: 384 });
174
+ } catch {
175
+ }
176
+ },
110
177
  stop() {
111
178
  if (stopped) return;
112
179
  stopped = true;
@@ -184,9 +251,25 @@ var verifyProof = (proof, options) => new Promise((resolve) => {
184
251
  });
185
252
  });
186
253
 
254
+ // src/session-idle.ts
255
+ var SESSION_IDLE_TIMEOUT_MS = 10 * 6e4;
256
+ var SESSION_REAP_GRACE_MS = 5 * 6e4;
257
+ var SESSION_REAP_SILENCE_MS = SESSION_IDLE_TIMEOUT_MS + SESSION_REAP_GRACE_MS;
258
+ var idleDeadlinePassed = (idleSince, now, timeoutMs = SESSION_IDLE_TIMEOUT_MS) => idleSince !== null && now - idleSince >= timeoutMs;
259
+ var describeIdleWindow = (timeoutMs) => {
260
+ const minutes = Math.round(timeoutMs / 6e4);
261
+ return minutes >= 1 ? `${minutes} minute${minutes === 1 ? "" : "s"}` : `${Math.round(timeoutMs / 1e3)}s`;
262
+ };
263
+
187
264
  export {
265
+ listProcessProofs,
266
+ silentSpawnedProofs,
188
267
  startProcessOwnership,
189
268
  withSpawnProcessOwnership,
190
269
  ownsLiveProcess,
191
- findLiveProcessBySpawnId
270
+ findLiveProcessBySpawnId,
271
+ SESSION_IDLE_TIMEOUT_MS,
272
+ SESSION_REAP_SILENCE_MS,
273
+ idleDeadlinePassed,
274
+ describeIdleWindow
192
275
  };
@@ -0,0 +1,76 @@
1
+ import {
2
+ writeJsonAtomicDurable
3
+ } from "./chunk-4LL6LG5X.js";
4
+
5
+ // src/transcript-recipient-pin.ts
6
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { dirname, join } from "path";
9
+ var pinFile = () => process.env.PUSHARY_RECIPIENT_PIN_FILE?.trim() || join(homedir(), ".pushary", "recipients.json");
10
+ var asPin = (offer) => ({
11
+ phonePublicKey: offer.phonePublicKey,
12
+ providerKeyId: offer.provider?.keyId ?? null,
13
+ providerPublicKey: offer.provider?.publicKey ?? null
14
+ });
15
+ var loadPin = () => {
16
+ const path = pinFile();
17
+ if (!existsSync(path)) return null;
18
+ try {
19
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
20
+ if (typeof raw.phonePublicKey !== "string" || raw.phonePublicKey.length === 0) return null;
21
+ return {
22
+ phonePublicKey: raw.phonePublicKey,
23
+ providerKeyId: typeof raw.providerKeyId === "string" ? raw.providerKeyId : null,
24
+ providerPublicKey: typeof raw.providerPublicKey === "string" ? raw.providerPublicKey : null
25
+ };
26
+ } catch {
27
+ return null;
28
+ }
29
+ };
30
+ var savePin = (pin) => {
31
+ const path = pinFile();
32
+ try {
33
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
34
+ writeJsonAtomicDurable(path, pin, 384);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ };
40
+ var clearPin = () => {
41
+ try {
42
+ rmSync(pinFile(), { force: true });
43
+ } catch {
44
+ }
45
+ };
46
+ var verifyRecipients = (offer, pinned) => {
47
+ if (!pinned) return { kind: "first-use", pin: asPin(offer) };
48
+ const next = asPin(offer);
49
+ const changed = [];
50
+ if (next.phonePublicKey !== pinned.phonePublicKey) changed.push("the phone key this machine seals to");
51
+ if (next.providerKeyId !== pinned.providerKeyId) changed.push("the compliance recovery key id");
52
+ if (next.providerPublicKey !== pinned.providerPublicKey) changed.push("the compliance recovery key");
53
+ return changed.length === 0 ? { kind: "match" } : { kind: "changed", changed };
54
+ };
55
+ var fingerprint = (publicKey) => {
56
+ const hex = Buffer.from(publicKey, "base64").toString("hex").toUpperCase();
57
+ return (hex.slice(0, 8).match(/.{2}/g) ?? []).join(" ");
58
+ };
59
+ var changedRecipientsMessage = (verdict, offer) => [
60
+ "[pushary] Transcript sync stopped: the keys this machine seals transcripts to have changed.",
61
+ ...verdict.changed.map((what) => `[pushary] changed: ${what}`),
62
+ `[pushary] now offered: phone ${fingerprint(offer.phonePublicKey)}${offer.provider ? `, recovery ${offer.provider.keyId.slice(0, 8)}` : ", no recovery key"}`,
63
+ "[pushary] If you reinstalled the app or rotated a key, this is expected. Compare the",
64
+ "[pushary] fingerprint above with the one in the app under Settings, then run:",
65
+ "[pushary] pushary transcripts trust --rotate",
66
+ "[pushary] Until then nothing is uploaded, so no turn is sealed to a key you have not seen."
67
+ ].join("\n");
68
+
69
+ export {
70
+ loadPin,
71
+ savePin,
72
+ clearPin,
73
+ verifyRecipients,
74
+ fingerprint,
75
+ changedRecipientsMessage
76
+ };
@@ -39,6 +39,7 @@ interface AgentEvent {
39
39
  machineId?: string;
40
40
  repoKey?: string;
41
41
  sessionId?: string;
42
+ parentSessionId?: string;
42
43
  error?: string;
43
44
  taskTitle?: string;
44
45
  decisionSource?: DecisionSource;
@@ -80,6 +81,7 @@ declare const handlePostToolUse: (input: {
80
81
  cwd?: string;
81
82
  session_id?: string;
82
83
  transcript_path?: string;
84
+ tool_use_id?: string;
83
85
  }, agent?: AgentIdentity) => Promise<string | undefined>;
84
86
  interface StopHookOutput {
85
87
  readonly decision: 'block';
@@ -88,6 +90,7 @@ interface StopHookOutput {
88
90
  declare const handleStop: (input: {
89
91
  cwd?: string;
90
92
  session_id?: string;
93
+ agent_id?: string;
91
94
  stop_hook_active?: boolean;
92
95
  transcript_path?: string;
93
96
  last_assistant_message?: string;
package/dist/src/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-OCMOTJTW.js";
4
- import "../chunk-KVHP5ZME.js";
3
+ } from "../chunk-B7UZHEBL.js";
4
+ import "../chunk-2LAHXHBR.js";
5
5
  import {
6
6
  fetchAccountPublicKey
7
7
  } from "../chunk-QZCFVWA7.js";
8
- import "../chunk-GKPURU7P.js";
9
- import "../chunk-FCUUVHTS.js";
8
+ import "../chunk-I463AEG3.js";
9
+ import "../chunk-VHWCVD42.js";
10
10
  import "../chunk-YHG74UFF.js";
11
11
  import {
12
12
  fetchModeOverride,
@@ -16,20 +16,20 @@ import {
16
16
  handlePostToolUse,
17
17
  handleStop,
18
18
  reportEvent
19
- } from "../chunk-OADGBOP7.js";
19
+ } from "../chunk-TDZFPMSZ.js";
20
20
  import {
21
21
  askUser,
22
22
  cancelQuestion,
23
23
  waitForAnswer
24
- } from "../chunk-JHJHCMAF.js";
25
- import "../chunk-JVZX5YRJ.js";
26
- import "../chunk-5NDB3YOE.js";
27
- import "../chunk-OVEGEQSB.js";
24
+ } from "../chunk-FXEVRLHC.js";
25
+ import "../chunk-L4E2BIJ2.js";
26
+ import "../chunk-54ELC7D7.js";
27
+ import "../chunk-KUZWXPUS.js";
28
28
  import "../chunk-BSZYIAZL.js";
29
29
  import "../chunk-SAF6HGAA.js";
30
30
  import "../chunk-7QLSKOSU.js";
31
31
  import "../chunk-KZERVKTD.js";
32
- import "../chunk-XREW7RQ3.js";
32
+ import "../chunk-JW4KO5AF.js";
33
33
  import "../chunk-4LL6LG5X.js";
34
34
  import {
35
35
  getApiKey,
@@ -37,7 +37,7 @@ import {
37
37
  } from "../chunk-2UMNXADU.js";
38
38
  import {
39
39
  resolvePolicy
40
- } from "../chunk-3MCEES6Z.js";
40
+ } from "../chunk-VNMKIJZB.js";
41
41
  export {
42
42
  askUser,
43
43
  cancelQuestion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.89.12",
3
+ "version": "0.90.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -72,7 +72,8 @@
72
72
  "pushary-upgrade": "./dist/bin/pushary-upgrade.js",
73
73
  "pushary-bell": "./dist/bin/pushary-bell.js",
74
74
  "pushary-bell-hook": "./dist/bin/pushary-bell-hook.js",
75
- "pushary-disconnect": "./dist/bin/pushary-disconnect.js"
75
+ "pushary-disconnect": "./dist/bin/pushary-disconnect.js",
76
+ "pushary-transcripts": "./dist/bin/pushary-transcripts.js"
76
77
  },
77
78
  "files": [
78
79
  "dist",
@@ -1,72 +0,0 @@
1
- import {
2
- hasGeminiHooks
3
- } from "./chunk-6V7ZDH3A.js";
4
- import {
5
- hasCodexHooks
6
- } from "./chunk-OVEGEQSB.js";
7
- import {
8
- HOOK_LOCATOR_BINARY
9
- } from "./chunk-3MCEES6Z.js";
10
-
11
- // src/diagnostics/wiring.ts
12
- var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
13
- var CLAUDE_HOOK_BINARY = /pushary-[a-z-]*hook/;
14
- var EXACT_HOOK_BINARY = /^pushary-[a-z-]*(hook|gate)$/;
15
- var pusharyHookExecutable = (command) => {
16
- const tokens = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
17
- for (const raw of tokens.reverse()) {
18
- const token = raw[0] === '"' && raw.at(-1) === '"' || raw[0] === "'" && raw.at(-1) === "'" ? raw.slice(1, -1) : raw;
19
- const name = token.split(/[/\\]/).pop()?.replace(/\.(cmd|exe|mjs)$/i, "") ?? "";
20
- if (name === HOOK_LOCATOR_BINARY || EXACT_HOOK_BINARY.test(name)) return token;
21
- }
22
- return null;
23
- };
24
- var claudeWired = ({ claudeJson, settings, skillExists }) => {
25
- if (record(record(claudeJson)?.mcpServers)?.pushary) return true;
26
- const s = record(settings);
27
- if (s) {
28
- const hooks = JSON.stringify(s.hooks ?? {});
29
- if (CLAUDE_HOOK_BINARY.test(hooks) || hooks.includes(HOOK_LOCATOR_BINARY)) return true;
30
- const allow = record(s.permissions)?.allow;
31
- if (Array.isArray(allow) && allow.some((rule) => String(rule).includes("mcp__pushary__"))) return true;
32
- }
33
- return skillExists;
34
- };
35
- var codexWired = ({ config, hooks, skillExists }) => {
36
- if (record(record(config)?.mcp_servers)?.pushary) return true;
37
- const h = record(hooks);
38
- if (h && hasCodexHooks(h)) return true;
39
- return skillExists;
40
- };
41
- var geminiWired = ({ settings }) => {
42
- const s = record(settings);
43
- if (!s) return false;
44
- if (record(s.mcpServers)?.pushary) return true;
45
- return hasGeminiHooks(s);
46
- };
47
- var cursorWired = ({ userHooks, mcp, pluginDirExists }) => {
48
- if (pluginDirExists) return true;
49
- if (record(record(mcp)?.mcpServers)?.pushary) return true;
50
- const hooks = record(record(userHooks)?.hooks);
51
- if (!hooks) return false;
52
- return Object.values(hooks).some((entries) => Array.isArray(entries) && entries.some((entry) => pusharyHookExecutable(String(record(entry)?.command ?? "")) !== null));
53
- };
54
- var cursorMcpAuthorization = (pluginMcp, userMcp) => {
55
- const pluginServers = record(record(pluginMcp)?.mcpServers);
56
- const usePlugin = pluginServers !== null && Object.hasOwn(pluginServers, "pushary");
57
- const servers = usePlugin ? pluginServers : record(record(userMcp)?.mcpServers);
58
- const headers = record(record(servers?.pushary)?.headers);
59
- return {
60
- authorization: typeof headers?.Authorization === "string" ? headers.Authorization : null,
61
- source: usePlugin ? "plugin mcp.json" : "~/.cursor/mcp.json"
62
- };
63
- };
64
-
65
- export {
66
- pusharyHookExecutable,
67
- claudeWired,
68
- codexWired,
69
- geminiWired,
70
- cursorWired,
71
- cursorMcpAuthorization
72
- };
@@ -1,84 +0,0 @@
1
- import {
2
- redactSecrets
3
- } from "./chunk-3MCEES6Z.js";
4
-
5
- // src/crypto.ts
6
- import nacl from "tweetnacl";
7
- var EPH_PUB_LEN = nacl.box.publicKeyLength;
8
- var BOX_NONCE_LEN = nacl.box.nonceLength;
9
- var SECRETBOX_NONCE_LEN = nacl.secretbox.nonceLength;
10
- var SESSION_KEY_LEN = nacl.secretbox.keyLength;
11
- var toBase64 = (bytes) => Buffer.from(bytes).toString("base64");
12
- var fromBase64 = (value) => new Uint8Array(Buffer.from(value, "base64"));
13
- var utf8Encode = (text) => new Uint8Array(Buffer.from(text, "utf-8"));
14
- var decodePublicKey = (value) => fromBase64(value);
15
- var generateSessionKey = () => nacl.randomBytes(SESSION_KEY_LEN);
16
- var wrapSessionKey = (sessionKey, accountPublicKey) => {
17
- const ephemeral = nacl.box.keyPair();
18
- const nonce = nacl.randomBytes(BOX_NONCE_LEN);
19
- const box = nacl.box(sessionKey, nonce, accountPublicKey, ephemeral.secretKey);
20
- const bundle = new Uint8Array(EPH_PUB_LEN + BOX_NONCE_LEN + box.length);
21
- bundle.set(ephemeral.publicKey, 0);
22
- bundle.set(nonce, EPH_PUB_LEN);
23
- bundle.set(box, EPH_PUB_LEN + BOX_NONCE_LEN);
24
- return toBase64(bundle);
25
- };
26
- var encryptRecord = (plaintext, sessionKey) => {
27
- const nonce = nacl.randomBytes(SECRETBOX_NONCE_LEN);
28
- const box = nacl.secretbox(utf8Encode(plaintext), nonce, sessionKey);
29
- const bundle = new Uint8Array(SECRETBOX_NONCE_LEN + box.length);
30
- bundle.set(nonce, 0);
31
- bundle.set(box, SECRETBOX_NONCE_LEN);
32
- return toBase64(bundle);
33
- };
34
-
35
- // src/transcript-capture.ts
36
- import { createHash } from "crypto";
37
- var INTERNAL_TYPES = /* @__PURE__ */ new Set([
38
- "file-history-snapshot",
39
- "change",
40
- "queue-operation",
41
- "summary"
42
- ]);
43
- var redactDeep = (value) => {
44
- if (typeof value === "string") return redactSecrets(value);
45
- if (Array.isArray(value)) return value.map(redactDeep);
46
- if (value && typeof value === "object") {
47
- const out = {};
48
- for (const [key, inner] of Object.entries(value)) {
49
- out[key] = redactDeep(inner);
50
- }
51
- return out;
52
- }
53
- return value;
54
- };
55
- var localIdFor = (parsed, rawLine) => typeof parsed.uuid === "string" && parsed.uuid.length > 0 ? parsed.uuid : createHash("sha256").update(rawLine).digest("hex").slice(0, 32);
56
- var buildTranscriptRecords = (lines, sessionKey, seen = /* @__PURE__ */ new Set()) => {
57
- const records = [];
58
- const nextSeen = new Set(seen);
59
- for (const line of lines) {
60
- const trimmed = line.trim();
61
- if (!trimmed) continue;
62
- let parsed;
63
- try {
64
- parsed = JSON.parse(trimmed);
65
- } catch {
66
- continue;
67
- }
68
- const type = typeof parsed.type === "string" ? parsed.type : "";
69
- if (INTERNAL_TYPES.has(type)) continue;
70
- const localId = localIdFor(parsed, trimmed);
71
- if (nextSeen.has(localId)) continue;
72
- nextSeen.add(localId);
73
- const content = encryptRecord(JSON.stringify(redactDeep(parsed)), sessionKey);
74
- records.push({ localId, content });
75
- }
76
- return { records, seen: nextSeen };
77
- };
78
-
79
- export {
80
- decodePublicKey,
81
- generateSessionKey,
82
- wrapSessionKey,
83
- buildTranscriptRecords
84
- };