pushary 1.8.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 (186) hide show
  1. package/CHANGELOG.md +1943 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/data/SKILL.md +591 -0
  5. package/data/cowork/SKILL.md +77 -0
  6. package/data/cursor-plugin/.cursor-plugin/plugin.json +25 -0
  7. package/data/cursor-plugin/CHANGELOG.md +70 -0
  8. package/data/cursor-plugin/CONTRIBUTING.md +40 -0
  9. package/data/cursor-plugin/LICENSE +21 -0
  10. package/data/cursor-plugin/README.md +113 -0
  11. package/data/cursor-plugin/SECURITY.md +33 -0
  12. package/data/cursor-plugin/assets/logo.png +0 -0
  13. package/data/cursor-plugin/commands/notify-when-done.md +14 -0
  14. package/data/cursor-plugin/commands/pushary-test.md +13 -0
  15. package/data/cursor-plugin/hooks/hooks.json +75 -0
  16. package/data/cursor-plugin/mcp.json +11 -0
  17. package/data/cursor-plugin/rules/pushary.mdc +40 -0
  18. package/data/cursor-plugin/scripts/pushary-gate.mjs +724 -0
  19. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +137 -0
  20. package/data/cursor-plugin/scripts/redaction.mjs +51 -0
  21. package/data/cursor-plugin/skills/pushary/SKILL.md +584 -0
  22. package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
  23. package/data/vscode-plugin/.mcp.json +11 -0
  24. package/data/vscode-plugin/CHANGELOG.md +49 -0
  25. package/data/vscode-plugin/CONTRIBUTING.md +40 -0
  26. package/data/vscode-plugin/LICENSE +21 -0
  27. package/data/vscode-plugin/README.md +128 -0
  28. package/data/vscode-plugin/SECURITY.md +56 -0
  29. package/data/vscode-plugin/assets/logo.png +0 -0
  30. package/data/vscode-plugin/commands/notify-when-done.md +14 -0
  31. package/data/vscode-plugin/commands/pushary-test.md +13 -0
  32. package/data/vscode-plugin/hooks/hooks.json +60 -0
  33. package/data/vscode-plugin/scripts/pushary-gate.mjs +762 -0
  34. package/data/vscode-plugin/scripts/redaction.mjs +51 -0
  35. package/data/vscode-plugin/skills/pushary/SKILL.md +584 -0
  36. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  37. package/dist/bin/pushary-bell-hook.js +46 -0
  38. package/dist/bin/pushary-bell.d.ts +1 -0
  39. package/dist/bin/pushary-bell.js +146 -0
  40. package/dist/bin/pushary-claude.d.ts +1 -0
  41. package/dist/bin/pushary-claude.js +1916 -0
  42. package/dist/bin/pushary-clean.d.ts +1 -0
  43. package/dist/bin/pushary-clean.js +1300 -0
  44. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  45. package/dist/bin/pushary-codex-bridge.js +599 -0
  46. package/dist/bin/pushary-codex-hook.d.ts +1 -0
  47. package/dist/bin/pushary-codex-hook.js +886 -0
  48. package/dist/bin/pushary-codex.d.ts +1 -0
  49. package/dist/bin/pushary-codex.js +125 -0
  50. package/dist/bin/pushary-connect.d.ts +1 -0
  51. package/dist/bin/pushary-connect.js +100 -0
  52. package/dist/bin/pushary-cowork.d.ts +1 -0
  53. package/dist/bin/pushary-cowork.js +36 -0
  54. package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
  55. package/dist/bin/pushary-daemon-supervisor.js +27 -0
  56. package/dist/bin/pushary-daemon.d.ts +1 -0
  57. package/dist/bin/pushary-daemon.js +878 -0
  58. package/dist/bin/pushary-disconnect.d.ts +1 -0
  59. package/dist/bin/pushary-disconnect.js +264 -0
  60. package/dist/bin/pushary-doctor.d.ts +1 -0
  61. package/dist/bin/pushary-doctor.js +1433 -0
  62. package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
  63. package/dist/bin/pushary-elicitation-hook.js +149 -0
  64. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  65. package/dist/bin/pushary-gemini-bridge.js +339 -0
  66. package/dist/bin/pushary-gemini-hook.d.ts +1 -0
  67. package/dist/bin/pushary-gemini-hook.js +381 -0
  68. package/dist/bin/pushary-hook.d.ts +1 -0
  69. package/dist/bin/pushary-hook.js +95 -0
  70. package/dist/bin/pushary-login.d.ts +1 -0
  71. package/dist/bin/pushary-login.js +198 -0
  72. package/dist/bin/pushary-logout.d.ts +1 -0
  73. package/dist/bin/pushary-logout.js +147 -0
  74. package/dist/bin/pushary-mcp.d.ts +1 -0
  75. package/dist/bin/pushary-mcp.js +114 -0
  76. package/dist/bin/pushary-mode.d.ts +1 -0
  77. package/dist/bin/pushary-mode.js +130 -0
  78. package/dist/bin/pushary-notification-hook.d.ts +1 -0
  79. package/dist/bin/pushary-notification-hook.js +56 -0
  80. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  81. package/dist/bin/pushary-opencode-hook.js +338 -0
  82. package/dist/bin/pushary-permission-denied-hook.d.ts +1 -0
  83. package/dist/bin/pushary-permission-denied-hook.js +60 -0
  84. package/dist/bin/pushary-permission-hook.d.ts +1 -0
  85. package/dist/bin/pushary-permission-hook.js +65 -0
  86. package/dist/bin/pushary-post-hook.d.ts +1 -0
  87. package/dist/bin/pushary-post-hook.js +70 -0
  88. package/dist/bin/pushary-prompt-hook.d.ts +1 -0
  89. package/dist/bin/pushary-prompt-hook.js +61 -0
  90. package/dist/bin/pushary-session-end-hook.d.ts +1 -0
  91. package/dist/bin/pushary-session-end-hook.js +59 -0
  92. package/dist/bin/pushary-session-start-hook.d.ts +1 -0
  93. package/dist/bin/pushary-session-start-hook.js +72 -0
  94. package/dist/bin/pushary-setup.d.ts +1 -0
  95. package/dist/bin/pushary-setup.js +2660 -0
  96. package/dist/bin/pushary-stats.d.ts +1 -0
  97. package/dist/bin/pushary-stats.js +44 -0
  98. package/dist/bin/pushary-status.d.ts +1 -0
  99. package/dist/bin/pushary-status.js +262 -0
  100. package/dist/bin/pushary-stop-hook.d.ts +1 -0
  101. package/dist/bin/pushary-stop-hook.js +63 -0
  102. package/dist/bin/pushary-stopfailure-hook.d.ts +1 -0
  103. package/dist/bin/pushary-stopfailure-hook.js +56 -0
  104. package/dist/bin/pushary-suggestions.d.ts +1 -0
  105. package/dist/bin/pushary-suggestions.js +100 -0
  106. package/dist/bin/pushary-transcript-register.d.ts +1 -0
  107. package/dist/bin/pushary-transcript-register.js +40 -0
  108. package/dist/bin/pushary-transcripts.d.ts +1 -0
  109. package/dist/bin/pushary-transcripts.js +77 -0
  110. package/dist/bin/pushary-upgrade.d.ts +1 -0
  111. package/dist/bin/pushary-upgrade.js +599 -0
  112. package/dist/bin/pushary-wait.d.ts +1 -0
  113. package/dist/bin/pushary-wait.js +122 -0
  114. package/dist/bin/pushary.d.ts +1 -0
  115. package/dist/bin/pushary.js +74 -0
  116. package/dist/chunk-2ABTSGFT.js +173 -0
  117. package/dist/chunk-2OY3ZZ5N.js +124 -0
  118. package/dist/chunk-3EGEA4KH.js +44 -0
  119. package/dist/chunk-3EVPNIBE.js +185 -0
  120. package/dist/chunk-5OP5MQI7.js +45 -0
  121. package/dist/chunk-6J2JC2RT.js +149 -0
  122. package/dist/chunk-7QLSKOSU.js +19 -0
  123. package/dist/chunk-7Z7Q4GGS.js +88 -0
  124. package/dist/chunk-A2A2YTMG.js +835 -0
  125. package/dist/chunk-AHV4LNB5.js +377 -0
  126. package/dist/chunk-AXPCESYO.js +197 -0
  127. package/dist/chunk-C2WCPBF7.js +86 -0
  128. package/dist/chunk-CJVVKOSR.js +73 -0
  129. package/dist/chunk-CULUZTWQ.js +2117 -0
  130. package/dist/chunk-CVQYGMSR.js +142 -0
  131. package/dist/chunk-CVWG2N3Y.js +16 -0
  132. package/dist/chunk-D2FX3EPW.js +15 -0
  133. package/dist/chunk-DB3ONZB4.js +44 -0
  134. package/dist/chunk-DE4Y6TCC.js +160 -0
  135. package/dist/chunk-E4FJTUH4.js +249 -0
  136. package/dist/chunk-EPCXUP3P.js +41 -0
  137. package/dist/chunk-EX3GAPA4.js +44 -0
  138. package/dist/chunk-EYK3GSRH.js +143 -0
  139. package/dist/chunk-GMXKITVA.js +70 -0
  140. package/dist/chunk-GPEGOGRJ.js +49 -0
  141. package/dist/chunk-GU3EJCVV.js +43 -0
  142. package/dist/chunk-HQ6G3B5R.js +59 -0
  143. package/dist/chunk-HZVVNGEW.js +22 -0
  144. package/dist/chunk-IPVKH2ST.js +231 -0
  145. package/dist/chunk-J5TIV3J6.js +238 -0
  146. package/dist/chunk-JU6W3XAU.js +64 -0
  147. package/dist/chunk-JYG4B33G.js +227 -0
  148. package/dist/chunk-JZTHGAPA.js +503 -0
  149. package/dist/chunk-KGDWDSNL.js +102 -0
  150. package/dist/chunk-KVQBWI5T.js +99 -0
  151. package/dist/chunk-KZERVKTD.js +16 -0
  152. package/dist/chunk-LXQ5FBBD.js +47 -0
  153. package/dist/chunk-M5ICYGHT.js +138 -0
  154. package/dist/chunk-M5JDZ45W.js +943 -0
  155. package/dist/chunk-MDEPU45E.js +1226 -0
  156. package/dist/chunk-MPWXGONV.js +157 -0
  157. package/dist/chunk-N6IOSZOE.js +185 -0
  158. package/dist/chunk-NGDD6TXY.js +192 -0
  159. package/dist/chunk-NQTHFF4W.js +2830 -0
  160. package/dist/chunk-OKXS7WDZ.js +68 -0
  161. package/dist/chunk-OLQRRFBX.js +21 -0
  162. package/dist/chunk-P7WQEVWA.js +307 -0
  163. package/dist/chunk-POFBE6ZY.js +12 -0
  164. package/dist/chunk-PQ4K74WL.js +30 -0
  165. package/dist/chunk-PUZZELE6.js +37 -0
  166. package/dist/chunk-Q7E4I6UA.js +767 -0
  167. package/dist/chunk-QCGNGEGE.js +15 -0
  168. package/dist/chunk-R7437YI3.js +151 -0
  169. package/dist/chunk-RHR6NARM.js +305 -0
  170. package/dist/chunk-SAXBC4AZ.js +60 -0
  171. package/dist/chunk-SGZJ5LYH.js +76 -0
  172. package/dist/chunk-SIFVFP6T.js +70 -0
  173. package/dist/chunk-SLFABHW3.js +150 -0
  174. package/dist/chunk-T7E75EKA.js +67 -0
  175. package/dist/chunk-TY4JT7W3.js +38 -0
  176. package/dist/chunk-U4TMAANR.js +111 -0
  177. package/dist/chunk-UI36QSBI.js +177 -0
  178. package/dist/chunk-VQKRG3AC.js +339 -0
  179. package/dist/chunk-WE2J62NC.js +1131 -0
  180. package/dist/chunk-XHKBHWLX.js +14 -0
  181. package/dist/chunk-ZSZSSJCV.js +275 -0
  182. package/dist/chunk-ZVSKMPBZ.js +135 -0
  183. package/dist/reapply-CW3MA66V.js +34 -0
  184. package/dist/src/index.d.ts +204 -0
  185. package/dist/src/index.js +64 -0
  186. package/package.json +108 -0
@@ -0,0 +1,503 @@
1
+ import {
2
+ changedRecipientsMessage,
3
+ loadPin,
4
+ savePin,
5
+ verifyRecipients
6
+ } from "./chunk-SGZJ5LYH.js";
7
+ import {
8
+ hookOwnerOf
9
+ } from "./chunk-EYK3GSRH.js";
10
+ import {
11
+ ensureDaemonRunning
12
+ } from "./chunk-WE2J62NC.js";
13
+ import {
14
+ buildTranscriptRecords,
15
+ decodePublicKey,
16
+ generateSessionKey,
17
+ wrapSessionKey
18
+ } from "./chunk-MPWXGONV.js";
19
+ import {
20
+ fetchTranscriptRecipients,
21
+ uploadTranscriptRecords
22
+ } from "./chunk-SAXBC4AZ.js";
23
+ import {
24
+ transcriptEnvironmentPreference,
25
+ transcriptsEnabled
26
+ } from "./chunk-OLQRRFBX.js";
27
+ import {
28
+ readTranscriptPreference
29
+ } from "./chunk-P7WQEVWA.js";
30
+ import {
31
+ codexHome,
32
+ pusharyDir
33
+ } from "./chunk-A2A2YTMG.js";
34
+ import {
35
+ writeJsonAtomic
36
+ } from "./chunk-T7E75EKA.js";
37
+ import {
38
+ getApiKey,
39
+ getBaseUrl
40
+ } from "./chunk-GPEGOGRJ.js";
41
+
42
+ // src/transcript-registration.ts
43
+ import { createHash } from "crypto";
44
+ import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync3, unlinkSync } from "fs";
45
+ import { isAbsolute, join as join3, resolve as resolve2, sep } from "path";
46
+
47
+ // src/transcript-source.ts
48
+ var normalizeTranscriptLines = (lines, source) => {
49
+ if (source === "claude") return [...lines];
50
+ return lines.flatMap((line) => {
51
+ try {
52
+ const record = JSON.parse(line);
53
+ const item = record.payload;
54
+ if (record.type !== "response_item" || item?.type !== "message" || !["user", "assistant"].includes(item.role)) return [];
55
+ const content = (Array.isArray(item.content) ? item.content : []).flatMap((block) => ["input_text", "output_text"].includes(block.type ?? "") && typeof block.text === "string" ? [{ type: "text", text: block.text }] : []);
56
+ return content.length ? [JSON.stringify({ timestamp: record.timestamp, message: { role: item.role, content } })] : [];
57
+ } catch {
58
+ return [];
59
+ }
60
+ });
61
+ };
62
+
63
+ // src/transcript-sync.ts
64
+ import { homedir as homedir2 } from "os";
65
+ import { join as join2, resolve } from "path";
66
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
67
+
68
+ // src/transcript-session-key.ts
69
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
70
+ import { homedir } from "os";
71
+ import { join } from "path";
72
+ var sessionDir = () => process.env.PUSHARY_SESSION_KEY_DIR?.trim() || join(homedir(), ".pushary", "sessions");
73
+ var SESSION_KEY_TTL_MS = 48 * 60 * 60 * 1e3;
74
+ var SAFE_SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
75
+ var fileFor = (sessionId) => SAFE_SESSION_ID.test(sessionId) ? join(sessionDir(), `${sessionId}.json`) : null;
76
+ var loadSessionKey = (sessionId) => {
77
+ const path = fileFor(sessionId);
78
+ if (!path || !existsSync(path)) return null;
79
+ try {
80
+ if (Date.now() - statSync(path).mtimeMs > SESSION_KEY_TTL_MS) return null;
81
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
82
+ const key = typeof raw.sessionKey === "string" ? Buffer.from(raw.sessionKey, "base64") : null;
83
+ if (!key || key.length !== 32) return null;
84
+ return {
85
+ sessionKey: new Uint8Array(key),
86
+ wrappedKey: typeof raw.wrappedKey === "string" ? raw.wrappedKey : null,
87
+ providerWrappedKey: typeof raw.providerWrappedKey === "string" ? raw.providerWrappedKey : null,
88
+ providerKeyId: typeof raw.providerKeyId === "string" ? raw.providerKeyId : null,
89
+ keyUploaded: raw.keyUploaded === true
90
+ };
91
+ } catch {
92
+ return null;
93
+ }
94
+ };
95
+ var saveSessionKey = (sessionId, value) => {
96
+ const path = fileFor(sessionId);
97
+ if (!path) return;
98
+ try {
99
+ mkdirSync(sessionDir(), { recursive: true, mode: 448 });
100
+ writeFileSync(
101
+ path,
102
+ JSON.stringify({
103
+ sessionKey: Buffer.from(value.sessionKey).toString("base64"),
104
+ wrappedKey: value.wrappedKey,
105
+ providerWrappedKey: value.providerWrappedKey,
106
+ providerKeyId: value.providerKeyId,
107
+ keyUploaded: value.keyUploaded
108
+ }),
109
+ { mode: 384 }
110
+ );
111
+ } catch {
112
+ }
113
+ };
114
+ var sweepStaleSessionKeys = (now = Date.now()) => {
115
+ const dir = sessionDir();
116
+ if (!existsSync(dir)) return 0;
117
+ let removed = 0;
118
+ try {
119
+ for (const name of readdirSync(dir)) {
120
+ if (!name.endsWith(".json")) continue;
121
+ const path = join(dir, name);
122
+ try {
123
+ if (now - statSync(path).mtimeMs <= SESSION_KEY_TTL_MS) continue;
124
+ rmSync(path, { force: true });
125
+ removed += 1;
126
+ } catch {
127
+ }
128
+ }
129
+ } catch {
130
+ return removed;
131
+ }
132
+ return removed;
133
+ };
134
+
135
+ // src/transcript-sync.ts
136
+ var claudeTranscriptPath = (cwd, sessionId) => {
137
+ const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
138
+ const configDir = process.env.CLAUDE_CONFIG_DIR || join2(homedir2(), ".claude");
139
+ return join2(configDir, "projects", projectId, `${sessionId}.jsonl`);
140
+ };
141
+ var createSyncState = () => ({
142
+ sessionKey: null,
143
+ wrappedKey: null,
144
+ providerWrappedKey: null,
145
+ providerKeyId: null,
146
+ seen: /* @__PURE__ */ new Set(),
147
+ keyUploaded: false,
148
+ keyRejected: false,
149
+ keyUnavailable: false
150
+ });
151
+ var syncTick = async (lines, state, uploader, sessionId, machineId) => {
152
+ if (state.keyRejected) return state;
153
+ let sessionKey = state.sessionKey;
154
+ let wrappedKey = state.wrappedKey;
155
+ let providerWrappedKey = state.providerWrappedKey;
156
+ let providerKeyId = state.providerKeyId;
157
+ if (!sessionKey) {
158
+ const recipients = await uploader.fetchTranscriptRecipients();
159
+ if (!recipients) return state;
160
+ sessionKey = generateSessionKey();
161
+ wrappedKey = wrapSessionKey(sessionKey, decodePublicKey(recipients.phonePublicKey));
162
+ if (recipients.provider) {
163
+ providerWrappedKey = wrapSessionKey(sessionKey, decodePublicKey(recipients.provider.publicKey));
164
+ providerKeyId = recipients.provider.keyId;
165
+ }
166
+ }
167
+ const built = buildTranscriptRecords(lines, sessionKey, state.seen);
168
+ if (built.records.length === 0) {
169
+ return { ...state, sessionKey, wrappedKey, providerWrappedKey, providerKeyId, seen: built.seen };
170
+ }
171
+ const seen = new Set(state.seen);
172
+ let keyUploaded = state.keyUploaded;
173
+ let keyRejected = false;
174
+ let keyUnavailable = false;
175
+ let rejected = 0;
176
+ for (const chunk of chunkRecords(built.records)) {
177
+ const result = await uploader.uploadRecords({
178
+ sessionId,
179
+ machineId,
180
+ dataEncryptionKey: keyUploaded ? void 0 : wrappedKey ?? void 0,
181
+ providerWrappedSessionKey: keyUploaded ? void 0 : providerWrappedKey ?? void 0,
182
+ providerKeyId: keyUploaded ? void 0 : providerKeyId ?? void 0,
183
+ records: chunk
184
+ });
185
+ keyUploaded = keyUploaded || result.keyRegistered;
186
+ if (result.keyConflict) {
187
+ keyRejected = true;
188
+ break;
189
+ }
190
+ if (result.keyUnavailable) {
191
+ keyUnavailable = true;
192
+ break;
193
+ }
194
+ if (!result.ok) break;
195
+ for (const id of result.settledIds ?? chunk.map((r) => r.localId)) seen.add(id);
196
+ rejected += result.rejectedIds.length;
197
+ }
198
+ if (rejected > 0) {
199
+ process.stderr.write(`[pushary] transcript: ${rejected} record${rejected === 1 ? "" : "s"} could not be stored and will not be retried.
200
+ `);
201
+ }
202
+ return {
203
+ sessionKey,
204
+ wrappedKey,
205
+ providerWrappedKey,
206
+ providerKeyId,
207
+ seen,
208
+ keyUploaded,
209
+ keyRejected,
210
+ keyUnavailable
211
+ };
212
+ };
213
+ var MAX_RECORDS_PER_BATCH = 200;
214
+ var MAX_BATCH_BYTES = 3e6;
215
+ var chunkRecords = (records) => {
216
+ const chunks = [];
217
+ let current = [];
218
+ let bytes = 0;
219
+ for (const record of records) {
220
+ const size = record.content.length;
221
+ if (current.length > 0 && (current.length >= MAX_RECORDS_PER_BATCH || bytes + size > MAX_BATCH_BYTES)) {
222
+ chunks.push(current);
223
+ current = [];
224
+ bytes = 0;
225
+ }
226
+ current.push(record);
227
+ bytes += size;
228
+ }
229
+ if (current.length > 0) chunks.push(current);
230
+ return chunks;
231
+ };
232
+ var readLines = (path) => {
233
+ try {
234
+ return existsSync2(path) ? readFileSync2(path, "utf-8").split("\n") : [];
235
+ } catch {
236
+ return [];
237
+ }
238
+ };
239
+ var IDLE_CEILING_MS = 6e4;
240
+ var nextInterval = (previousMs, idle, baseMs, maxMs) => idle ? Math.min(previousMs * 2, maxMs) : baseMs;
241
+ var startTranscriptSync = (opts) => {
242
+ let warnedAboutRecipients = false;
243
+ const fetchPinnedRecipients = async () => {
244
+ const offer = await fetchTranscriptRecipients(opts.apiKey);
245
+ if (!offer) return null;
246
+ const verdict = verifyRecipients(offer, loadPin());
247
+ if (verdict.kind === "first-use" && !savePin(verdict.pin)) return null;
248
+ if (verdict.kind === "changed") {
249
+ if (!warnedAboutRecipients) {
250
+ warnedAboutRecipients = true;
251
+ process.stderr.write(`${changedRecipientsMessage(verdict, offer)}
252
+ `);
253
+ }
254
+ return null;
255
+ }
256
+ return offer;
257
+ };
258
+ const uploader = {
259
+ fetchTranscriptRecipients: fetchPinnedRecipients,
260
+ uploadRecords: (params) => (opts.enabled ? opts.enabled() : transcriptsEnabled()) ? uploadTranscriptRecords(opts.apiKey, params) : Promise.resolve({ ok: false, keyRegistered: false, keyConflict: false, keyUnavailable: false, settledIds: null, rejectedIds: [] })
261
+ };
262
+ const path = opts.path ?? claudeTranscriptPath(opts.cwd, opts.sessionId);
263
+ const persisted = loadSessionKey(opts.sessionId);
264
+ let state = persisted ? { ...createSyncState(), ...persisted } : createSyncState();
265
+ sweepStaleSessionKeys();
266
+ let persistedUploaded = persisted?.keyUploaded ?? false;
267
+ let running = true;
268
+ let inFlight = false;
269
+ const tick = async () => {
270
+ if (!running || inFlight || !(opts.enabled ? opts.enabled() : transcriptsEnabled())) return;
271
+ inFlight = true;
272
+ try {
273
+ const before = state.sessionKey;
274
+ const ending = opts.ending?.() === true;
275
+ const modified = existsSync2(path) ? statSync2(path).mtimeMs : null;
276
+ const lines = normalizeTranscriptLines(readLines(path), opts.source ?? "claude");
277
+ state = await syncTick(lines, state, uploader, opts.sessionId, opts.machineId);
278
+ if (state.sessionKey && (before !== state.sessionKey || state.keyUploaded !== persistedUploaded)) {
279
+ persistedUploaded = state.keyUploaded;
280
+ saveSessionKey(opts.sessionId, {
281
+ sessionKey: state.sessionKey,
282
+ wrappedKey: state.wrappedKey,
283
+ providerWrappedKey: state.providerWrappedKey,
284
+ providerKeyId: state.providerKeyId,
285
+ keyUploaded: state.keyUploaded
286
+ });
287
+ }
288
+ if (ending && opts.ending?.() && existsSync2(path) && statSync2(path).mtimeMs === modified && state.keyUploaded && !state.keyRejected && buildTranscriptRecords(lines, state.sessionKey, state.seen).records.length === 0) opts.onDrained?.();
289
+ } catch {
290
+ } finally {
291
+ inFlight = false;
292
+ }
293
+ };
294
+ const baseIntervalMs = opts.intervalMs ?? 3e3;
295
+ const maxIntervalMs = Math.max(baseIntervalMs, IDLE_CEILING_MS);
296
+ let timer;
297
+ const schedule = (delayMs) => {
298
+ if (!running) return;
299
+ timer = setTimeout(() => {
300
+ void (async () => {
301
+ const before = state.sessionKey;
302
+ await tick();
303
+ const idle = state.sessionKey === null && before === null || state.keyUnavailable;
304
+ schedule(nextInterval(delayMs, idle, baseIntervalMs, maxIntervalMs));
305
+ })();
306
+ }, delayMs);
307
+ if (typeof timer.unref === "function") timer.unref();
308
+ };
309
+ void tick();
310
+ schedule(baseIntervalMs);
311
+ return () => {
312
+ running = false;
313
+ if (timer) clearTimeout(timer);
314
+ };
315
+ };
316
+
317
+ // src/transcript-registration.ts
318
+ import { homedir as homedir3 } from "os";
319
+ var directory = () => process.env.PUSHARY_TRANSCRIPT_REGISTRY_DIR || join3(pusharyDir(), "transcript-capture");
320
+ var accountFor = (apiKey) => createHash("sha256").update(`${getBaseUrl()}\0${apiKey}`).digest("hex");
321
+ var TTL = 48 * 60 * 60 * 1e3;
322
+ var read = (path) => JSON.parse(readFileSync3(path, "utf8"));
323
+ var consent = (record) => record.consentOverride ?? readTranscriptPreference() ?? false;
324
+ var sameServer = (url) => url.replace(/\/+$/, "") === getBaseUrl().replace(/\/+$/, "");
325
+ var fileFor2 = (apiKey, sessionId) => join3(directory(), `${createHash("sha256").update(accountFor(apiKey) + sessionId).digest("hex")}.json`);
326
+ var permitted = (apiKey, raw) => raw.account === accountFor(apiKey) && consent(raw) && !raw.drained && (raw.source !== "claude" || !!raw.expectedSiteId || !macOwnsClaudeHooks(raw.cwd)) && (!raw.expectedSiteId || raw.expectedSiteId === raw.verifiedSiteId && !!raw.expectedBaseUrl && sameServer(raw.expectedBaseUrl));
327
+ var registeredTranscriptEnabled = (apiKey, sessionId) => {
328
+ try {
329
+ return permitted(apiKey, read(fileFor2(apiKey, sessionId)));
330
+ } catch {
331
+ return false;
332
+ }
333
+ };
334
+ var macOwnsClaudeHooks = (cwd) => [
335
+ join3(process.env.CLAUDE_CONFIG_DIR || join3(homedir3(), ".claude"), "settings.json"),
336
+ join3(cwd, ".claude", "settings.json"),
337
+ join3(cwd, ".claude", "settings.local.json")
338
+ ].some((path) => {
339
+ try {
340
+ return hookOwnerOf(JSON.parse(readFileSync3(path, "utf8"))) === "app";
341
+ } catch {
342
+ return false;
343
+ }
344
+ });
345
+ var transcriptRoots = (source) => {
346
+ const home = homedir3();
347
+ if (source === "codex") return [join3(codexHome(), "sessions")];
348
+ const configured = process.env.CLAUDE_CONFIG_DIR;
349
+ return [join3(configured || join3(home, ".claude"), "projects"), join3(home, ".claude", "projects")];
350
+ };
351
+ var withinTranscriptRoot = (candidate, source) => {
352
+ const target = resolve2(candidate);
353
+ return transcriptRoots(source).some((root) => target.startsWith(resolve2(root) + sep));
354
+ };
355
+ var captureRegistration = (source, value) => {
356
+ const input = value;
357
+ if (source !== "claude" && source !== "codex") return null;
358
+ if (input.agent_id || typeof input.session_id !== "string" || !/^[\w-]{1,128}$/.test(input.session_id)) return null;
359
+ if (typeof input.cwd !== "string" || !isAbsolute(input.cwd)) return null;
360
+ const path = input.transcript_path ?? input.agent_transcript_path ?? (source === "claude" ? claudeTranscriptPath(input.cwd, input.session_id) : null);
361
+ if (typeof path !== "string" || !isAbsolute(path) || !path.endsWith(".jsonl")) return null;
362
+ if (!withinTranscriptRoot(path, source)) return null;
363
+ return { sessionId: input.session_id, cwd: input.cwd, path, source };
364
+ };
365
+ var registerTranscript = (source, input, startDaemon = () => {
366
+ void ensureDaemonRunning({ waitTimeoutMs: 0 }).catch(() => {
367
+ });
368
+ }, options = {}) => {
369
+ const registration = captureRegistration(source, input);
370
+ if (!registration) return;
371
+ try {
372
+ const account = accountFor(getApiKey());
373
+ const path = fileFor2(getApiKey(), registration.sessionId);
374
+ let previous;
375
+ try {
376
+ previous = read(path);
377
+ } catch {
378
+ }
379
+ if (source === "claude" && !options.siteId && !previous?.expectedSiteId && macOwnsClaudeHooks(registration.cwd)) return;
380
+ const ending = options.ended || input.hook_event_name === "SessionEnd" && process.env.PUSHARY_WRAPPER_ACTIVE !== "1";
381
+ const stored = {
382
+ ...registration,
383
+ account,
384
+ consentOverride: transcriptEnvironmentPreference(),
385
+ // A wrapper refresh must not erase the Mac's account binding.
386
+ expectedSiteId: options.siteId ?? previous?.expectedSiteId,
387
+ expectedBaseUrl: options.baseUrl ?? previous?.expectedBaseUrl,
388
+ verifiedSiteId: (!options.siteId || options.siteId === previous?.expectedSiteId) && (!options.baseUrl || options.baseUrl === previous?.expectedBaseUrl) ? previous?.verifiedSiteId : void 0,
389
+ ...ending ? { endedAt: Date.now() } : {}
390
+ };
391
+ if (previous && Date.now() - statSync3(path).mtimeMs < 6e4 && JSON.stringify(previous) === JSON.stringify(stored)) return;
392
+ writeJsonAtomic(path, stored, 384);
393
+ if (consent(stored)) startDaemon();
394
+ } catch {
395
+ }
396
+ };
397
+ var startRegisteredTranscripts = (apiKey, machineId, intervalMs = 3e3) => {
398
+ const active = /* @__PURE__ */ new Map();
399
+ let stopped = false;
400
+ let scanning = false;
401
+ let verifiedSiteId;
402
+ let nextVerification = 0;
403
+ const tick = async () => {
404
+ if (stopped || scanning) return;
405
+ scanning = true;
406
+ const wanted = /* @__PURE__ */ new Map();
407
+ try {
408
+ const files = readdirSync2(directory()).filter((name) => name.endsWith(".json")).flatMap((name) => {
409
+ const path = join3(directory(), name);
410
+ try {
411
+ const time = statSync3(path).mtimeMs;
412
+ if (Date.now() - time > TTL) {
413
+ unlinkSync(path);
414
+ return [];
415
+ }
416
+ return [{ path, time, raw: read(path) }];
417
+ } catch {
418
+ return [];
419
+ }
420
+ }).sort((a, b) => Number(!!a.raw.endedAt) - Number(!!b.raw.endedAt) || b.time - a.time);
421
+ for (const file of files) {
422
+ const raw = file.raw;
423
+ if (raw.account !== accountFor(apiKey) || !consent(raw) || raw.drained) continue;
424
+ if (raw.expectedSiteId && !verifiedSiteId && Date.now() >= nextVerification && raw.expectedBaseUrl && sameServer(raw.expectedBaseUrl)) {
425
+ nextVerification = Date.now() + 3e4;
426
+ try {
427
+ const response = await fetch(`${getBaseUrl()}/api/agent/encryption-account`, {
428
+ headers: { Authorization: `Bearer ${apiKey}` },
429
+ signal: AbortSignal.timeout(5e3)
430
+ });
431
+ const proof = response.ok ? await response.json() : null;
432
+ if (typeof proof?.siteId === "string") verifiedSiteId = proof.siteId;
433
+ } catch {
434
+ }
435
+ }
436
+ if (raw.expectedSiteId && raw.expectedSiteId === verifiedSiteId && raw.expectedBaseUrl && sameServer(raw.expectedBaseUrl) && raw.verifiedSiteId !== verifiedSiteId) {
437
+ const latest = read(file.path);
438
+ if (JSON.stringify(latest) !== JSON.stringify(raw)) continue;
439
+ raw.verifiedSiteId = verifiedSiteId;
440
+ writeJsonAtomic(file.path, raw, 384);
441
+ }
442
+ if (!permitted(apiKey, raw)) continue;
443
+ if (wanted.size >= (raw.endedAt ? 33 : 32)) continue;
444
+ const registration = captureRegistration(raw.source, { session_id: raw.sessionId, cwd: raw.cwd, transcript_path: raw.path });
445
+ if (registration) wanted.set(registration.sessionId, { ...registration, registrationPath: file.path });
446
+ }
447
+ if (stopped) return;
448
+ for (const [id, reader] of active) {
449
+ if (wanted.get(id)?.path === reader.path) continue;
450
+ reader.stop();
451
+ active.delete(id);
452
+ }
453
+ for (const [id, registration] of wanted) {
454
+ if (active.has(id)) continue;
455
+ const current = () => read(registration.registrationPath);
456
+ active.set(id, { path: registration.path, stop: startTranscriptSync({
457
+ apiKey,
458
+ machineId,
459
+ ...registration,
460
+ intervalMs,
461
+ enabled: () => {
462
+ try {
463
+ return permitted(apiKey, current());
464
+ } catch {
465
+ return false;
466
+ }
467
+ },
468
+ ending: () => {
469
+ try {
470
+ return !!current().endedAt;
471
+ } catch {
472
+ return false;
473
+ }
474
+ },
475
+ onDrained: () => {
476
+ const raw = current();
477
+ if (raw.endedAt) writeJsonAtomic(registration.registrationPath, { ...raw, drained: true }, 384);
478
+ }
479
+ }) });
480
+ }
481
+ } catch {
482
+ } finally {
483
+ scanning = false;
484
+ }
485
+ };
486
+ void tick();
487
+ const timer = setInterval(() => void tick(), intervalMs);
488
+ timer.unref();
489
+ return () => {
490
+ stopped = true;
491
+ clearInterval(timer);
492
+ for (const reader of active.values()) reader.stop();
493
+ active.clear();
494
+ };
495
+ };
496
+
497
+ export {
498
+ loadSessionKey,
499
+ claudeTranscriptPath,
500
+ registeredTranscriptEnabled,
501
+ registerTranscript,
502
+ startRegisteredTranscripts
503
+ };
@@ -0,0 +1,102 @@
1
+ import {
2
+ registerTranscript
3
+ } from "./chunk-JZTHGAPA.js";
4
+ import {
5
+ writeDegraded
6
+ } from "./chunk-CVQYGMSR.js";
7
+
8
+ // src/tool-hook-input.ts
9
+ var OPTIONAL_TEXT_FIELDS = ["hook_event_name", "session_id", "cwd", "transcript_path", "tool_use_id", "permission_mode"];
10
+ function isInputRecord(value) {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ function isToolHookInput(value) {
14
+ if (!isInputRecord(value)) return false;
15
+ if (typeof value.tool_name !== "string" || value.tool_name.length === 0) return false;
16
+ if (!isInputRecord(value.tool_input)) return false;
17
+ for (const field of OPTIONAL_TEXT_FIELDS) {
18
+ if (value[field] !== void 0 && typeof value[field] !== "string") return false;
19
+ }
20
+ return true;
21
+ }
22
+ function parseToolHookInput(value) {
23
+ if (!isInputRecord(value)) return null;
24
+ const input = { ...value, tool_input: value.tool_input === void 0 ? {} : value.tool_input };
25
+ return isToolHookInput(input) ? input : null;
26
+ }
27
+
28
+ // src/admission.ts
29
+ import { readFileSync } from "fs";
30
+ import { homedir } from "os";
31
+ import { join } from "path";
32
+ var NO_RULES = { version: 1, disabled: [] };
33
+ var SUPPORTED_VERSION = 1;
34
+ var parseAdmissionRules = (raw) => {
35
+ if (!raw) return NO_RULES;
36
+ try {
37
+ const parsed = JSON.parse(raw);
38
+ if (parsed.version !== SUPPORTED_VERSION) return NO_RULES;
39
+ if (!Array.isArray(parsed.disabled)) return NO_RULES;
40
+ return {
41
+ version: SUPPORTED_VERSION,
42
+ disabled: parsed.disabled.filter((rule) => typeof rule === "string")
43
+ };
44
+ } catch {
45
+ return NO_RULES;
46
+ }
47
+ };
48
+ var admissionRulesFile = () => process.env.PUSHARY_ADMISSION_FILE?.trim() || join(homedir(), ".pushary", "admission-rules.json");
49
+ var readAdmissionRules = () => {
50
+ try {
51
+ return parseAdmissionRules(readFileSync(admissionRulesFile(), "utf-8"));
52
+ } catch {
53
+ return NO_RULES;
54
+ }
55
+ };
56
+ var matches = (rule, source, event) => {
57
+ const colon = rule.indexOf(":");
58
+ if (colon <= 0 || colon === rule.length - 1) return false;
59
+ const ruleSource = rule.slice(0, colon);
60
+ const ruleEvent = rule.slice(colon + 1);
61
+ return (ruleSource === "*" || ruleSource === source) && (ruleEvent === "*" || ruleEvent === event);
62
+ };
63
+ var isAdmitted = (rules, source, event) => !rules.disabled.some((rule) => matches(rule, source, event));
64
+ var readHookInput = async (source, fallbackEvent = "", stdin = process.stdin, parseInput) => {
65
+ if (source === null) {
66
+ writeDegraded("hook", {
67
+ reason: "unknown_agent",
68
+ fallback: "stood aside, so nothing is gated or reported for this call",
69
+ event: fallbackEvent
70
+ });
71
+ return null;
72
+ }
73
+ let raw = "";
74
+ for await (const chunk of stdin) raw += chunk;
75
+ if (!raw.trim()) return null;
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(raw);
79
+ } catch {
80
+ return null;
81
+ }
82
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
83
+ const input = parseInput ? parseInput(parsed) : parsed;
84
+ if (input === null) return null;
85
+ const event = eventNameOf(input) ?? fallbackEvent;
86
+ if (!isAdmitted(readAdmissionRules(), source, event)) return null;
87
+ if (stdin === process.stdin) registerTranscript(source, input);
88
+ return input;
89
+ };
90
+ var eventNameOf = (input) => {
91
+ if (!("hook_event_name" in input)) return void 0;
92
+ const name = input.hook_event_name;
93
+ return typeof name === "string" && name.length > 0 ? name : void 0;
94
+ };
95
+ function readToolHookInput(source, event, stdin = process.stdin) {
96
+ return readHookInput(source, event, stdin, parseToolHookInput);
97
+ }
98
+
99
+ export {
100
+ readHookInput,
101
+ readToolHookInput
102
+ };