@pushary/agent-hooks 0.94.0 → 0.95.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 +9 -0
- package/data/cursor-plugin/CONTRIBUTING.md +31 -19
- package/data/vscode-plugin/CONTRIBUTING.md +31 -19
- package/dist/bin/pushary-bell-hook.js +22 -1
- package/dist/bin/pushary-bell.js +3 -3
- package/dist/bin/pushary-claude.js +335 -357
- package/dist/bin/pushary-clean.js +21 -16
- package/dist/bin/pushary-codex-bridge.js +6 -6
- package/dist/bin/pushary-codex-hook.js +20 -2
- package/dist/bin/pushary-codex.js +1 -1
- package/dist/bin/pushary-connect.js +3 -3
- package/dist/bin/pushary-daemon.js +33 -18
- package/dist/bin/pushary-disconnect.js +4 -2
- package/dist/bin/pushary-doctor.js +31 -28
- package/dist/bin/pushary-elicitation-hook.js +21 -1
- package/dist/bin/pushary-gemini-bridge.js +6 -6
- package/dist/bin/pushary-gemini-hook.js +20 -2
- package/dist/bin/pushary-hook.js +20 -3
- package/dist/bin/pushary-login.js +3 -3
- package/dist/bin/pushary-logout.js +3 -3
- package/dist/bin/pushary-mode.js +3 -3
- package/dist/bin/pushary-notification-hook.js +20 -2
- package/dist/bin/pushary-opencode-hook.js +20 -2
- package/dist/bin/pushary-permission-denied-hook.js +21 -3
- package/dist/bin/pushary-permission-hook.js +21 -3
- package/dist/bin/pushary-post-hook.js +20 -2
- package/dist/bin/pushary-prompt-hook.js +20 -2
- package/dist/bin/pushary-session-end-hook.js +24 -3
- package/dist/bin/pushary-session-start-hook.js +23 -8
- package/dist/bin/pushary-setup.js +47 -41
- package/dist/bin/pushary-status.js +3 -3
- package/dist/bin/pushary-stop-hook.js +20 -2
- package/dist/bin/pushary-stopfailure-hook.js +20 -2
- package/dist/bin/pushary-suggestions.js +3 -3
- package/dist/bin/pushary-transcript-register.d.ts +1 -0
- package/dist/bin/pushary-transcript-register.js +42 -0
- package/dist/bin/pushary-transcripts.js +1 -1
- package/dist/bin/pushary-upgrade.js +11 -10
- package/dist/bin/pushary-wait.js +3 -3
- package/dist/{chunk-6WYL3XIP.js → chunk-2WVDQVI5.js} +3 -2
- package/dist/{chunk-HSAVPZ46.js → chunk-5SJ54JJO.js} +1 -1
- package/dist/{chunk-QKNLW7H6.js → chunk-6LQAGVG2.js} +1 -1
- package/dist/{chunk-OB4RF2TF.js → chunk-AYJ7NH5S.js} +1 -1
- package/dist/chunk-BMOFXRGS.js +185 -0
- package/dist/chunk-FH3YLP5Z.js +194 -0
- package/dist/chunk-GHCGSEPE.js +86 -0
- package/dist/chunk-GI5UQSG7.js +266 -0
- package/dist/{chunk-ZO2XG3CX.js → chunk-LSXJH2HB.js} +1 -1
- package/dist/{chunk-YANV5TGK.js → chunk-MDQOBG5J.js} +1 -84
- package/dist/chunk-N6FAXQDD.js +276 -0
- package/dist/{chunk-5HGLZGI4.js → chunk-OXTRFRMD.js} +1 -0
- package/dist/chunk-PQ4K74WL.js +30 -0
- package/dist/{chunk-46P3VES5.js → chunk-Q5UFC2QK.js} +1 -0
- package/dist/{chunk-36LR2LOT.js → chunk-SFCGKE6U.js} +8 -6
- package/dist/{chunk-6CUUA7HO.js → chunk-XY4GSZ3K.js} +7 -1
- package/dist/chunk-YGZXYEVY.js +503 -0
- package/dist/src/index.js +5 -5
- package/package.json +3 -2
- package/dist/chunk-FYOIW5RV.js +0 -539
- package/dist/chunk-RX75MRGC.js +0 -368
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/wrapper/claudeBinary.ts
|
|
2
|
+
import { statSync } from "fs";
|
|
3
|
+
import { join, delimiter } from "path";
|
|
4
|
+
var WRAPPER_ACTIVE_ENV = "PUSHARY_WRAPPER_ACTIVE";
|
|
5
|
+
var isFile = (path) => {
|
|
6
|
+
try {
|
|
7
|
+
return statSync(path).isFile();
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var findClaudeBinary = () => {
|
|
13
|
+
const override = process.env.PUSHARY_CLAUDE_BIN?.trim();
|
|
14
|
+
if (override) return isFile(override) ? override : null;
|
|
15
|
+
const pathVar = process.env.PATH ?? "";
|
|
16
|
+
const exts = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
|
|
17
|
+
for (const dir of pathVar.split(delimiter)) {
|
|
18
|
+
if (!dir) continue;
|
|
19
|
+
for (const ext of exts) {
|
|
20
|
+
const candidate = join(dir, `claude${ext}`);
|
|
21
|
+
if (isFile(candidate)) return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
WRAPPER_ACTIVE_ENV,
|
|
29
|
+
findClaudeBinary
|
|
30
|
+
};
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
detectClaudeVersion
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LSXJH2HB.js";
|
|
4
|
+
import {
|
|
5
|
+
addClaudeMcpServer,
|
|
6
|
+
addPusharyHooks,
|
|
7
|
+
addPusharyToolPermissions
|
|
8
|
+
} from "./chunk-FH3YLP5Z.js";
|
|
4
9
|
import {
|
|
5
10
|
claudeWired,
|
|
6
11
|
codexWired,
|
|
7
12
|
geminiWired
|
|
8
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-5SJ54JJO.js";
|
|
9
14
|
import {
|
|
10
15
|
CLAUDE_HOOK_EVENTS,
|
|
11
16
|
GEMINI_HOOK_BINARY,
|
|
12
|
-
addClaudeMcpServer,
|
|
13
17
|
addGeminiHooks,
|
|
14
18
|
addGeminiMcpServer,
|
|
15
|
-
addPusharyHooks,
|
|
16
|
-
addPusharyToolPermissions,
|
|
17
19
|
formatVersion,
|
|
18
20
|
hasGeminiHooks,
|
|
19
21
|
needsReregistration,
|
|
20
22
|
registrationSignatures,
|
|
21
23
|
supportedClaudeEvents
|
|
22
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-BMOFXRGS.js";
|
|
23
25
|
import {
|
|
24
26
|
guardBinary
|
|
25
27
|
} from "./chunk-EZIEXHYM.js";
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerTranscript
|
|
3
|
+
} from "./chunk-YGZXYEVY.js";
|
|
4
|
+
|
|
1
5
|
// src/admission.ts
|
|
2
6
|
import { readFileSync } from "fs";
|
|
3
7
|
import { homedir } from "os";
|
|
@@ -46,7 +50,9 @@ var readHookInput = async (source, fallbackEvent = "", stdin = process.stdin) =>
|
|
|
46
50
|
}
|
|
47
51
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
48
52
|
const event = eventNameOf(parsed) ?? fallbackEvent;
|
|
49
|
-
|
|
53
|
+
if (!isAdmitted(readAdmissionRules(), source, event)) return null;
|
|
54
|
+
if (stdin === process.stdin) registerTranscript(source, parsed);
|
|
55
|
+
return parsed;
|
|
50
56
|
};
|
|
51
57
|
var eventNameOf = (input) => {
|
|
52
58
|
if (!("hook_event_name" in input)) return void 0;
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import {
|
|
2
|
+
changedRecipientsMessage,
|
|
3
|
+
loadPin,
|
|
4
|
+
savePin,
|
|
5
|
+
verifyRecipients
|
|
6
|
+
} from "./chunk-YKLCWWEI.js";
|
|
7
|
+
import {
|
|
8
|
+
hookOwnerOf
|
|
9
|
+
} from "./chunk-GI5UQSG7.js";
|
|
10
|
+
import {
|
|
11
|
+
ensureDaemonRunning
|
|
12
|
+
} from "./chunk-ZTJF6ANY.js";
|
|
13
|
+
import {
|
|
14
|
+
buildTranscriptRecords,
|
|
15
|
+
decodePublicKey,
|
|
16
|
+
generateSessionKey,
|
|
17
|
+
wrapSessionKey
|
|
18
|
+
} from "./chunk-Q5UFC2QK.js";
|
|
19
|
+
import {
|
|
20
|
+
fetchTranscriptRecipients,
|
|
21
|
+
uploadTranscriptRecords
|
|
22
|
+
} from "./chunk-EQRS3SGM.js";
|
|
23
|
+
import {
|
|
24
|
+
transcriptEnvironmentPreference,
|
|
25
|
+
transcriptsEnabled
|
|
26
|
+
} from "./chunk-2WVDQVI5.js";
|
|
27
|
+
import {
|
|
28
|
+
codexHome,
|
|
29
|
+
pusharyDir
|
|
30
|
+
} from "./chunk-Y6XXFXVB.js";
|
|
31
|
+
import {
|
|
32
|
+
readTranscriptPreference
|
|
33
|
+
} from "./chunk-6BIMJIRG.js";
|
|
34
|
+
import {
|
|
35
|
+
writeJsonAtomic
|
|
36
|
+
} from "./chunk-4LL6LG5X.js";
|
|
37
|
+
import {
|
|
38
|
+
getApiKey,
|
|
39
|
+
getBaseUrl
|
|
40
|
+
} from "./chunk-2UMNXADU.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
|
+
};
|
package/dist/src/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handlePreToolUse
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import {
|
|
5
|
-
fetchAccountPublicKey
|
|
6
|
-
} from "../chunk-EQRS3SGM.js";
|
|
3
|
+
} from "../chunk-6LQAGVG2.js";
|
|
7
4
|
import "../chunk-OYNPLEXP.js";
|
|
8
5
|
import "../chunk-2ORRI66J.js";
|
|
9
6
|
import "../chunk-YHG74UFF.js";
|
|
7
|
+
import {
|
|
8
|
+
fetchAccountPublicKey
|
|
9
|
+
} from "../chunk-EQRS3SGM.js";
|
|
10
10
|
import {
|
|
11
11
|
fetchModeOverride,
|
|
12
12
|
fetchModeState,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
handlePostToolUse,
|
|
16
16
|
handleStop,
|
|
17
17
|
reportEvent
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-OXTRFRMD.js";
|
|
19
19
|
import {
|
|
20
20
|
askUser,
|
|
21
21
|
cancelQuestion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.95.0",
|
|
4
4
|
"description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pushary",
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
"pushary-bell": "./dist/bin/pushary-bell.js",
|
|
74
74
|
"pushary-bell-hook": "./dist/bin/pushary-bell-hook.js",
|
|
75
75
|
"pushary-disconnect": "./dist/bin/pushary-disconnect.js",
|
|
76
|
-
"pushary-transcripts": "./dist/bin/pushary-transcripts.js"
|
|
76
|
+
"pushary-transcripts": "./dist/bin/pushary-transcripts.js",
|
|
77
|
+
"pushary-transcript-register": "./dist/bin/pushary-transcript-register.js"
|
|
77
78
|
},
|
|
78
79
|
"files": [
|
|
79
80
|
"dist",
|