@mnemonik/claude-code-hooks 0.9.48 → 0.9.51
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/dist/hook.js +96 -6
- package/dist/hook.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/install.js.map +1 -1
- package/dist/lib/actionCapture.d.ts +9 -0
- package/dist/lib/actionCapture.js +211 -0
- package/dist/lib/actionCapture.js.map +1 -0
- package/dist/lib/creditCapture.d.ts +32 -0
- package/dist/lib/creditCapture.js +417 -0
- package/dist/lib/creditCapture.js.map +1 -0
- package/dist/lib/http.d.ts +9 -1
- package/dist/lib/http.js +11 -0
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/types.d.ts +40 -0
- package/dist/lib/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
const MAX_FINGERPRINTS = 128;
|
|
5
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
6
|
+
function sha256(value) {
|
|
7
|
+
return createHash('sha256').update(value).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
function stableValue(value) {
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.map(stableValue);
|
|
12
|
+
if (value && typeof value === 'object') {
|
|
13
|
+
return Object.fromEntries(Object.entries(value)
|
|
14
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
15
|
+
.map(([key, item]) => [key, stableValue(item)]));
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function stableJson(value) {
|
|
20
|
+
return JSON.stringify(stableValue(value)) ?? 'null';
|
|
21
|
+
}
|
|
22
|
+
function stringLeaves(value, into) {
|
|
23
|
+
if (typeof value === 'string') {
|
|
24
|
+
into.push(value);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
for (const item of value)
|
|
29
|
+
stringLeaves(item, into);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (value && typeof value === 'object') {
|
|
33
|
+
for (const item of Object.values(value))
|
|
34
|
+
stringLeaves(item, into);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function fingerprintSurface(value, sessionSalt) {
|
|
38
|
+
if (!SHA256_RE.test(sessionSalt))
|
|
39
|
+
return [];
|
|
40
|
+
const strings = [];
|
|
41
|
+
stringLeaves(value, strings);
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const fingerprints = [];
|
|
44
|
+
for (const text of strings) {
|
|
45
|
+
const tokens = text
|
|
46
|
+
.normalize('NFC')
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.split(/[^\p{L}\p{N}]+/u);
|
|
49
|
+
for (const token of tokens) {
|
|
50
|
+
if (token.length < 4 || seen.has(token))
|
|
51
|
+
continue;
|
|
52
|
+
seen.add(token);
|
|
53
|
+
fingerprints.push({
|
|
54
|
+
hash: createHmac('sha256', Buffer.from(sessionSalt, 'hex'))
|
|
55
|
+
.update(token)
|
|
56
|
+
.digest('hex')
|
|
57
|
+
.slice(0, 32),
|
|
58
|
+
length: token.length,
|
|
59
|
+
});
|
|
60
|
+
if (fingerprints.length >= MAX_FINGERPRINTS)
|
|
61
|
+
return fingerprints;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return fingerprints;
|
|
65
|
+
}
|
|
66
|
+
function withoutPreImage(input) {
|
|
67
|
+
const result = { ...input };
|
|
68
|
+
delete result.old_string;
|
|
69
|
+
delete result.oldString;
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function targetValues(input) {
|
|
73
|
+
const args = input.tool_input ?? {};
|
|
74
|
+
const values = [];
|
|
75
|
+
for (const key of ['file_path', 'notebook_path', 'path', 'memoryId', 'memory_id', 'id']) {
|
|
76
|
+
const value = args[key];
|
|
77
|
+
if (typeof value === 'string' && value)
|
|
78
|
+
values.push(value);
|
|
79
|
+
}
|
|
80
|
+
for (const key of ['memoryIds', 'memory_ids', 'ids']) {
|
|
81
|
+
const value = args[key];
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
values.push(...value.filter((item) => typeof item === 'string' && !!item));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...new Set(values)].map((value) => value.includes('/') || value.includes('\\') ? resolve(input.cwd, value) : value);
|
|
87
|
+
}
|
|
88
|
+
function commandClass(command) {
|
|
89
|
+
const normalized = command.normalize('NFC').toLowerCase();
|
|
90
|
+
if (/\b(vitest|jest|pytest|rspec|cargo\s+test|go\s+test|npm\s+(run\s+)?test|make\s+test)\b/.test(normalized)) {
|
|
91
|
+
return 'validator_test';
|
|
92
|
+
}
|
|
93
|
+
if (/\b(tsc|typecheck|mypy|pyright|cargo\s+check)\b/.test(normalized)) {
|
|
94
|
+
return 'validator_typecheck';
|
|
95
|
+
}
|
|
96
|
+
if (/\b(eslint|prettier\s+--check|ruff|golangci-lint|npm\s+run\s+lint|make\s+lint)\b/.test(normalized)) {
|
|
97
|
+
return 'validator_lint';
|
|
98
|
+
}
|
|
99
|
+
if (/\b(npm\s+run\s+build|pnpm\s+build|yarn\s+build|make\s+build|cargo\s+build|go\s+build)\b/.test(normalized)) {
|
|
100
|
+
return 'validator_build';
|
|
101
|
+
}
|
|
102
|
+
return 'shell';
|
|
103
|
+
}
|
|
104
|
+
export function normalizeActionClass(toolName, toolInput) {
|
|
105
|
+
const lower = toolName.toLowerCase();
|
|
106
|
+
if (lower.includes('bash') || lower.includes('shell') || lower.includes('terminal')) {
|
|
107
|
+
return commandClass(typeof toolInput.command === 'string' ? toolInput.command : '');
|
|
108
|
+
}
|
|
109
|
+
if (lower.includes('memory_state') || lower.includes('reinforce')) {
|
|
110
|
+
const action = typeof toolInput.action === 'string' ? toolInput.action : '';
|
|
111
|
+
if (action === 'penalize')
|
|
112
|
+
return 'memory_penalize';
|
|
113
|
+
if (action === 'reinforce')
|
|
114
|
+
return 'memory_reinforce';
|
|
115
|
+
}
|
|
116
|
+
if (lower === 'edit' || lower.includes('strreplace') || lower.includes('replace'))
|
|
117
|
+
return 'edit';
|
|
118
|
+
if (lower === 'write' || lower.includes('write'))
|
|
119
|
+
return 'write';
|
|
120
|
+
if (lower.includes('notebook'))
|
|
121
|
+
return 'notebook_edit';
|
|
122
|
+
if (lower.includes('grep'))
|
|
123
|
+
return 'search';
|
|
124
|
+
return `tool:${lower || 'unknown'}`;
|
|
125
|
+
}
|
|
126
|
+
function resultExitStatus(result) {
|
|
127
|
+
const visit = (value, depth = 0) => {
|
|
128
|
+
if (depth > 5 || !value || typeof value !== 'object')
|
|
129
|
+
return undefined;
|
|
130
|
+
const row = value;
|
|
131
|
+
for (const key of ['exitCode', 'exit_code', 'statusCode', 'status_code']) {
|
|
132
|
+
const code = row[key];
|
|
133
|
+
if (typeof code === 'number' && Number.isFinite(code))
|
|
134
|
+
return `exit:${code}`;
|
|
135
|
+
}
|
|
136
|
+
if (row.is_error === true || row.isError === true || row.error)
|
|
137
|
+
return 'error';
|
|
138
|
+
if (row.ok === false || row.success === false)
|
|
139
|
+
return 'error';
|
|
140
|
+
for (const child of Object.values(row)) {
|
|
141
|
+
const found = visit(child, depth + 1);
|
|
142
|
+
if (found)
|
|
143
|
+
return found;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
return visit(result) ?? 'unknown';
|
|
148
|
+
}
|
|
149
|
+
export function transcriptSequence(input) {
|
|
150
|
+
if (!input.transcript_path)
|
|
151
|
+
return {};
|
|
152
|
+
try {
|
|
153
|
+
const seq = statSync(input.transcript_path).size;
|
|
154
|
+
return { seq, transcriptRef: sha256(input.transcript_path) };
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function buildActionObservation(input, sessionSalt) {
|
|
161
|
+
const event = input.hook_event_name === 'PreToolUse'
|
|
162
|
+
? 'pre_tool_use'
|
|
163
|
+
: input.hook_event_name === 'PostToolUse'
|
|
164
|
+
? 'post_tool_use'
|
|
165
|
+
: input.hook_event_name === 'UserPromptSubmit'
|
|
166
|
+
? 'user_prompt'
|
|
167
|
+
: input.hook_event_name === 'PreCompact'
|
|
168
|
+
? 'compaction'
|
|
169
|
+
: input.hook_event_name === 'SessionEnd'
|
|
170
|
+
? 'session_end'
|
|
171
|
+
: undefined;
|
|
172
|
+
if (!event)
|
|
173
|
+
return undefined;
|
|
174
|
+
const { seq, transcriptRef } = transcriptSequence(input);
|
|
175
|
+
if (event !== 'pre_tool_use' && event !== 'post_tool_use') {
|
|
176
|
+
return {
|
|
177
|
+
event,
|
|
178
|
+
nativeActionRef: `boundary:${event}:${seq ?? 'unknown'}`,
|
|
179
|
+
actionClass: 'boundary',
|
|
180
|
+
targetHashes: [],
|
|
181
|
+
fingerprintSet: { args: [], preImage: [] },
|
|
182
|
+
...(seq !== undefined ? { seq } : {}),
|
|
183
|
+
...(transcriptRef ? { transcriptRef } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!input.tool_use_id)
|
|
187
|
+
return undefined;
|
|
188
|
+
const args = input.tool_input ?? {};
|
|
189
|
+
const safeArgs = withoutPreImage(args);
|
|
190
|
+
const preImage = args.old_string ?? args.oldString;
|
|
191
|
+
return {
|
|
192
|
+
event,
|
|
193
|
+
nativeActionRef: input.tool_use_id,
|
|
194
|
+
actionClass: normalizeActionClass(input.tool_name ?? '', args),
|
|
195
|
+
targetHashes: targetValues(input).map(sha256),
|
|
196
|
+
argHash: sha256(stableJson(safeArgs)),
|
|
197
|
+
...(event === 'post_tool_use'
|
|
198
|
+
? {
|
|
199
|
+
resultHash: sha256(stableJson(input.tool_response)),
|
|
200
|
+
exitStatus: resultExitStatus(input.tool_response),
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
203
|
+
fingerprintSet: {
|
|
204
|
+
args: fingerprintSurface(safeArgs, sessionSalt),
|
|
205
|
+
preImage: fingerprintSurface(preImage, sessionSalt),
|
|
206
|
+
},
|
|
207
|
+
...(seq !== undefined ? { seq } : {}),
|
|
208
|
+
...(transcriptRef ? { transcriptRef } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=actionCapture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actionCapture.js","sourceRoot":"","sources":["../../src/lib/actionCapture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,SAAS,GAAG,gBAAgB,CAAC;AAEnC,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACxD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;aAC7C,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;aACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAClD,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC;AACtD,CAAC;AAED,SAAS,YAAY,CAAC,KAAc,EAAE,IAAc;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjB,OAAO;IACT,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnD,OAAO;IACT,CAAC;IACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAgC,CAAC;YAAE,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/F,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc,EAAE,WAAmB;IACpE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,YAAY,GAAuB,EAAE,CAAC;IAC5C,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,IAAI;aAChB,SAAS,CAAC,KAAK,CAAC;aAChB,WAAW,EAAE;aACb,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAC5B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,SAAS;YAClD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAChB,YAAY,CAAC,IAAI,CAAC;gBAChB,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;qBACxD,MAAM,CAAC,KAAK,CAAC;qBACb,MAAM,CAAC,KAAK,CAAC;qBACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;gBACf,MAAM,EAAE,KAAK,CAAC,MAAM;aACrB,CAAC,CAAC;YACH,IAAI,YAAY,CAAC,MAAM,IAAI,gBAAgB;gBAAE,OAAO,YAAY,CAAC;QACnE,CAAC;IACH,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAS,eAAe,CAAC,KAA8B;IACrD,MAAM,MAAM,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,UAAU,CAAC;IACzB,OAAO,MAAM,CAAC,SAAS,CAAC;IACxB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,KAAgB;IACpC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;IACpC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC;QACxF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK;YAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7D,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7F,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACxC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAChF,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;IAC1D,IACE,uFAAuF,CAAC,IAAI,CAC1F,UAAU,CACX,EACD,CAAC;QACD,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,IAAI,gDAAgD,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACtE,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IACD,IACE,iFAAiF,CAAC,IAAI,CACpF,UAAU,CACX,EACD,CAAC;QACD,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,IACE,yFAAyF,CAAC,IAAI,CAC5F,UAAU,CACX,EACD,CAAC;QACD,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,SAAkC;IACvF,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACpF,OAAO,YAAY,CAAC,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAClE,MAAM,MAAM,GAAG,OAAO,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,IAAI,MAAM,KAAK,UAAU;YAAE,OAAO,iBAAiB,CAAC;QACpD,IAAI,MAAM,KAAK,WAAW;YAAE,OAAO,kBAAkB,CAAC;IACxD,CAAC;IACD,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,MAAM,CAAC;IACjG,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACjE,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,eAAe,CAAC;IACvD,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC5C,OAAO,QAAQ,KAAK,IAAI,SAAS,EAAE,CAAC;AACtC,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAe;IACvC,MAAM,KAAK,GAAG,CAAC,KAAc,EAAE,KAAK,GAAG,CAAC,EAAsB,EAAE;QAC9D,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACvE,MAAM,GAAG,GAAG,KAAgC,CAAC;QAC7C,KAAK,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,CAAC,EAAE,CAAC;YACzE,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,OAAO,QAAQ,IAAI,EAAE,CAAC;QAC/E,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI,IAAI,GAAG,CAAC,KAAK;YAAE,OAAO,OAAO,CAAC;QAC/E,IAAI,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,CAAC,OAAO,KAAK,KAAK;YAAE,OAAO,OAAO,CAAC;QAC9D,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;YACtC,IAAI,KAAK;gBAAE,OAAO,KAAK,CAAC;QAC1B,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IACF,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAgB;IACjD,IAAI,CAAC,KAAK,CAAC,eAAe;QAAE,OAAO,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC;QACjD,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,KAAgB,EAChB,WAAmB;IAEnB,MAAM,KAAK,GACT,KAAK,CAAC,eAAe,KAAK,YAAY;QACpC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,KAAK,CAAC,eAAe,KAAK,aAAa;YACvC,CAAC,CAAC,eAAe;YACjB,CAAC,CAAC,KAAK,CAAC,eAAe,KAAK,kBAAkB;gBAC5C,CAAC,CAAC,aAAa;gBACf,CAAC,CAAC,KAAK,CAAC,eAAe,KAAK,YAAY;oBACtC,CAAC,CAAC,YAAY;oBACd,CAAC,CAAC,KAAK,CAAC,eAAe,KAAK,YAAY;wBACtC,CAAC,CAAC,aAAa;wBACf,CAAC,CAAC,SAAS,CAAC;IACxB,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACzD,IAAI,KAAK,KAAK,cAAc,IAAI,KAAK,KAAK,eAAe,EAAE,CAAC;QAC1D,OAAO;YACL,KAAK;YACL,eAAe,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI,SAAS,EAAE;YACxD,WAAW,EAAE,UAAU;YACvB,YAAY,EAAE,EAAE;YAChB,cAAc,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;YAC1C,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5C,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,WAAW;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;IACpC,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,SAAS,CAAC;IACnD,OAAO;QACL,KAAK;QACL,eAAe,EAAE,KAAK,CAAC,WAAW;QAClC,WAAW,EAAE,oBAAoB,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,EAAE,IAAI,CAAC;QAC9D,YAAY,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;QAC7C,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACrC,GAAG,CAAC,KAAK,KAAK,eAAe;YAC3B,CAAC,CAAC;gBACE,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;gBACnD,UAAU,EAAE,gBAAgB,CAAC,KAAK,CAAC,aAAa,CAAC;aAClD;YACH,CAAC,CAAC,EAAE,CAAC;QACP,cAAc,EAAE;YACd,IAAI,EAAE,kBAAkB,CAAC,QAAQ,EAAE,WAAW,CAAC;YAC/C,QAAQ,EAAE,kBAAkB,CAAC,QAAQ,EAAE,WAAW,CAAC;SACpD;QACD,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Private, fail-open Claude Code transcript confirmation. */
|
|
2
|
+
import type { DeliveryConfirmation, HookInput, InjectionsResponse } from './types.js';
|
|
3
|
+
export interface PendingDelivery {
|
|
4
|
+
deliveryRef: string;
|
|
5
|
+
emittedHashes: string[];
|
|
6
|
+
rendererVersion: string;
|
|
7
|
+
transcriptByteOffset: number;
|
|
8
|
+
sessionSalt: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function pendingDeliveryPath(sessionId: string): string;
|
|
11
|
+
/** Generate one random salt per host session without any awaited work. */
|
|
12
|
+
export declare function getOrCreateSessionSalt(sessionId: string): string;
|
|
13
|
+
/** One bounded O_APPEND write. Any failure is silent and never blocks emission. */
|
|
14
|
+
export declare function appendPendingDelivery(sessionId: string, record: PendingDelivery): boolean;
|
|
15
|
+
/** Parse only bytes appended after each stored transcript offset. */
|
|
16
|
+
export declare function collectDeliveryConfirmations(input: HookInput): DeliveryConfirmation[];
|
|
17
|
+
/** Remove accepted references after the existing request returns. */
|
|
18
|
+
export declare function acknowledgeDeliveryConfirmations(sessionId: string, accepted: string[]): void;
|
|
19
|
+
/** Sessions older than this can never confirm again (confirmation rides the SAME session's next hook event). */
|
|
20
|
+
export declare const STALE_SESSION_MS: number;
|
|
21
|
+
/**
|
|
22
|
+
* Remove abandoned session directories. Users close IDEs; SessionEnd rarely
|
|
23
|
+
* fires; the OS temp cleaner is not our contract. Runs once per SessionStart:
|
|
24
|
+
* one readdir plus one lstat per entry, synchronous, best-effort. Skips
|
|
25
|
+
* anything that is not a plain directory (symlinks, junctions, files) and
|
|
26
|
+
* never throws.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sweepStaleSessionDirs(now?: number): number;
|
|
29
|
+
/** Remove one session's private pending log on clean SessionEnd. */
|
|
30
|
+
export declare function clearPendingDeliveries(sessionId: string): void;
|
|
31
|
+
/** Record the reflex fragments that survived final additionalContext composition. */
|
|
32
|
+
export declare function captureReflexDeliveries(input: HookInput, additionalContext: string, response: InjectionsResponse | null): boolean;
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/** Private, fail-open Claude Code transcript confirmation. */
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { chmodSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync, } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
const MAX_PENDING_BYTES = 1024 * 1024;
|
|
7
|
+
const MAX_RECORD_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_DELTA_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
const DELTA_READ_CHUNK_BYTES = 64 * 1024;
|
|
10
|
+
const POINTER_RE = /— memory_get [0-9a-f]{8}/gi;
|
|
11
|
+
const ATTACHMENT_MARKER = Buffer.from('"hook_additional_context"');
|
|
12
|
+
function sha256(value) {
|
|
13
|
+
return createHash('sha256').update(value).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
function privateRoot() {
|
|
16
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
|
|
17
|
+
return join(tmpdir(), `mnemonik-credit-${uid}`);
|
|
18
|
+
}
|
|
19
|
+
export function pendingDeliveryPath(sessionId) {
|
|
20
|
+
return join(privateRoot(), sha256(sessionId), 'pending.jsonl');
|
|
21
|
+
}
|
|
22
|
+
function sessionSaltPath(sessionId) {
|
|
23
|
+
return join(privateRoot(), sha256(sessionId), 'session.salt');
|
|
24
|
+
}
|
|
25
|
+
/** Generate one random salt per host session without any awaited work. */
|
|
26
|
+
export function getOrCreateSessionSalt(sessionId) {
|
|
27
|
+
const path = sessionSaltPath(sessionId);
|
|
28
|
+
let fd = null;
|
|
29
|
+
try {
|
|
30
|
+
if (!ensurePrivateDirectory(path) || !secureRegularFile(path))
|
|
31
|
+
return '';
|
|
32
|
+
try {
|
|
33
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
34
|
+
const salt = randomBytes(32).toString('hex');
|
|
35
|
+
if (writeSync(fd, salt, 0, 'utf8') !== salt.length)
|
|
36
|
+
return '';
|
|
37
|
+
return salt;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
if (fd !== null) {
|
|
41
|
+
try {
|
|
42
|
+
closeSync(fd);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Fall through to the winner's file.
|
|
46
|
+
}
|
|
47
|
+
fd = null;
|
|
48
|
+
}
|
|
49
|
+
const existing = readFileSync(path, 'utf8').trim();
|
|
50
|
+
return /^[a-f0-9]{64}$/.test(existing) ? existing : '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (fd !== null) {
|
|
58
|
+
try {
|
|
59
|
+
closeSync(fd);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Fail-open.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function ensurePrivateDirectory(path) {
|
|
68
|
+
try {
|
|
69
|
+
const root = privateRoot();
|
|
70
|
+
if (existsSync(root) && lstatSync(root).isSymbolicLink())
|
|
71
|
+
return false;
|
|
72
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
73
|
+
chmodSync(root, 0o700);
|
|
74
|
+
if (existsSync(dirname(path)) && lstatSync(dirname(path)).isSymbolicLink())
|
|
75
|
+
return false;
|
|
76
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
77
|
+
chmodSync(dirname(path), 0o700);
|
|
78
|
+
return !lstatSync(root).isSymbolicLink() && !lstatSync(dirname(path)).isSymbolicLink();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function secureRegularFile(path) {
|
|
85
|
+
try {
|
|
86
|
+
if (!existsSync(path))
|
|
87
|
+
return true;
|
|
88
|
+
const stat = lstatSync(path);
|
|
89
|
+
return stat.isFile() && !stat.isSymbolicLink() && (stat.mode & 0o077) === 0;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** One bounded O_APPEND write. Any failure is silent and never blocks emission. */
|
|
96
|
+
export function appendPendingDelivery(sessionId, record) {
|
|
97
|
+
const path = pendingDeliveryPath(sessionId);
|
|
98
|
+
let fd = null;
|
|
99
|
+
try {
|
|
100
|
+
if (!ensurePrivateDirectory(path) || !secureRegularFile(path))
|
|
101
|
+
return false;
|
|
102
|
+
const existingBytes = existsSync(path) ? statSync(path).size : 0;
|
|
103
|
+
if (existingBytes >= MAX_PENDING_BYTES)
|
|
104
|
+
return false;
|
|
105
|
+
const line = Buffer.from(`${JSON.stringify(record)}\n`, 'utf8');
|
|
106
|
+
if (line.length > MAX_RECORD_BYTES || existingBytes + line.length > MAX_PENDING_BYTES) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
110
|
+
const stat = fstatSync(fd);
|
|
111
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0)
|
|
112
|
+
return false;
|
|
113
|
+
return writeSync(fd, line, 0, line.length) === line.length;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
if (fd !== null) {
|
|
120
|
+
try {
|
|
121
|
+
closeSync(fd);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Fail-open and log nothing.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function readPending(sessionId) {
|
|
130
|
+
const path = pendingDeliveryPath(sessionId);
|
|
131
|
+
try {
|
|
132
|
+
if (!secureRegularFile(path))
|
|
133
|
+
return [];
|
|
134
|
+
const text = readFileSync(path, 'utf8');
|
|
135
|
+
if (Buffer.byteLength(text) > MAX_PENDING_BYTES)
|
|
136
|
+
return [];
|
|
137
|
+
return text
|
|
138
|
+
.split('\n')
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.slice(-32)
|
|
141
|
+
.flatMap((line) => {
|
|
142
|
+
try {
|
|
143
|
+
const record = JSON.parse(line);
|
|
144
|
+
return typeof record.deliveryRef === 'string' &&
|
|
145
|
+
Array.isArray(record.emittedHashes) &&
|
|
146
|
+
typeof record.rendererVersion === 'string' &&
|
|
147
|
+
Number.isSafeInteger(record.transcriptByteOffset) &&
|
|
148
|
+
record.transcriptByteOffset >= 0 &&
|
|
149
|
+
typeof record.sessionSalt === 'string' &&
|
|
150
|
+
/^[a-f0-9]{64}$/.test(record.sessionSalt)
|
|
151
|
+
? [record]
|
|
152
|
+
: [];
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Match exact item hashes without assuming memory text has no blank lines.
|
|
165
|
+
* Every blank-line boundary before a pointer is tried; only a server-supplied
|
|
166
|
+
* SHA-256 match is admitted.
|
|
167
|
+
*/
|
|
168
|
+
function matchFragments(content, expected) {
|
|
169
|
+
const matches = [];
|
|
170
|
+
let cursor = 0;
|
|
171
|
+
let pointerIndex = 0;
|
|
172
|
+
POINTER_RE.lastIndex = 0;
|
|
173
|
+
let pointer;
|
|
174
|
+
while ((pointer = POINTER_RE.exec(content)) !== null && matches.length < expected.length) {
|
|
175
|
+
const end = pointer.index + pointer[0].length;
|
|
176
|
+
const starts = [cursor];
|
|
177
|
+
let boundary = content.indexOf('\n\n', cursor);
|
|
178
|
+
while (boundary >= 0 && boundary < pointer.index) {
|
|
179
|
+
starts.push(boundary + 2);
|
|
180
|
+
boundary = content.indexOf('\n\n', boundary + 2);
|
|
181
|
+
}
|
|
182
|
+
for (const start of starts) {
|
|
183
|
+
const candidate = content.slice(start, end).trim();
|
|
184
|
+
if (sha256(candidate) === expected[matches.length]) {
|
|
185
|
+
matches.push({ hash: expected[matches.length], fragmentIndex: pointerIndex });
|
|
186
|
+
cursor = end;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
pointerIndex += 1;
|
|
191
|
+
}
|
|
192
|
+
return matches;
|
|
193
|
+
}
|
|
194
|
+
function readTranscriptDelta(path, offset) {
|
|
195
|
+
let fd = null;
|
|
196
|
+
try {
|
|
197
|
+
const stat = lstatSync(path);
|
|
198
|
+
if (!stat.isFile() || stat.isSymbolicLink() || offset > stat.size)
|
|
199
|
+
return null;
|
|
200
|
+
const length = stat.size - offset;
|
|
201
|
+
if (length <= 0 || length > MAX_DELTA_BYTES)
|
|
202
|
+
return null;
|
|
203
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
204
|
+
let out = Buffer.allocUnsafe(Math.min(length, DELTA_READ_CHUNK_BYTES));
|
|
205
|
+
let total = 0;
|
|
206
|
+
while (total < length) {
|
|
207
|
+
if (total === out.length) {
|
|
208
|
+
const expanded = Buffer.allocUnsafe(Math.min(length, out.length + DELTA_READ_CHUNK_BYTES));
|
|
209
|
+
out.copy(expanded, 0, 0, total);
|
|
210
|
+
out = expanded;
|
|
211
|
+
}
|
|
212
|
+
const read = readSync(fd, out, total, out.length - total, offset + total);
|
|
213
|
+
if (read <= 0)
|
|
214
|
+
return null;
|
|
215
|
+
total += read;
|
|
216
|
+
const current = out.subarray(0, total);
|
|
217
|
+
const markerAt = current.indexOf(ATTACHMENT_MARKER);
|
|
218
|
+
if (markerAt >= 0) {
|
|
219
|
+
const newlineAt = current.indexOf(0x0a, markerAt);
|
|
220
|
+
if (newlineAt >= 0)
|
|
221
|
+
return current.subarray(0, newlineAt + 1);
|
|
222
|
+
if (total === length)
|
|
223
|
+
return current;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return out.subarray(0, total);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
if (fd !== null) {
|
|
233
|
+
try {
|
|
234
|
+
closeSync(fd);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Read failure is silent.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Parse only bytes appended after each stored transcript offset. */
|
|
243
|
+
export function collectDeliveryConfirmations(input) {
|
|
244
|
+
if (!input.transcript_path)
|
|
245
|
+
return [];
|
|
246
|
+
const pending = readPending(input.session_id);
|
|
247
|
+
const transcriptRef = sha256(input.transcript_path);
|
|
248
|
+
const confirmations = [];
|
|
249
|
+
for (const record of pending) {
|
|
250
|
+
const delta = readTranscriptDelta(input.transcript_path, record.transcriptByteOffset);
|
|
251
|
+
if (!delta)
|
|
252
|
+
continue;
|
|
253
|
+
let best;
|
|
254
|
+
// Multi-MB tool results dominate transcript deltas. Search for the exact
|
|
255
|
+
// attachment discriminator first, then JSON-decode only those JSONL lines.
|
|
256
|
+
// This preserves the reference parser's malformed-line behavior without
|
|
257
|
+
// allocating an array or parsing thousands of unrelated tool-result rows.
|
|
258
|
+
let searchFrom = 0;
|
|
259
|
+
const marker = ATTACHMENT_MARKER;
|
|
260
|
+
while (searchFrom < delta.length) {
|
|
261
|
+
const markerAt = delta.indexOf(marker, searchFrom);
|
|
262
|
+
if (markerAt < 0)
|
|
263
|
+
break;
|
|
264
|
+
const lineStart = delta.lastIndexOf(0x0a, markerAt) + 1;
|
|
265
|
+
const newlineAt = delta.indexOf(0x0a, markerAt);
|
|
266
|
+
const lineEnd = newlineAt < 0 ? delta.length : newlineAt;
|
|
267
|
+
const line = delta.subarray(lineStart, lineEnd).toString('utf8');
|
|
268
|
+
const currentOffset = record.transcriptByteOffset + lineStart;
|
|
269
|
+
searchFrom = lineEnd + 1;
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(line);
|
|
272
|
+
if (parsed.type !== 'attachment' ||
|
|
273
|
+
parsed.attachment?.type !== 'hook_additional_context' ||
|
|
274
|
+
!Array.isArray(parsed.attachment.content)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
parsed.attachment.content.forEach((content, contentIndex) => {
|
|
278
|
+
const found = matchFragments(content, record.emittedHashes);
|
|
279
|
+
if (found.length === 0)
|
|
280
|
+
return;
|
|
281
|
+
const candidate = {
|
|
282
|
+
hashes: found.map((item) => item.hash),
|
|
283
|
+
positions: found.map((item) => ({
|
|
284
|
+
transcriptRef,
|
|
285
|
+
byteOffset: currentOffset,
|
|
286
|
+
contentIndex,
|
|
287
|
+
fragmentIndex: item.fragmentIndex,
|
|
288
|
+
})),
|
|
289
|
+
};
|
|
290
|
+
if (!best || candidate.hashes.length > best.hashes.length)
|
|
291
|
+
best = candidate;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Reference parser contract: malformed JSONL lines are ignored.
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (best) {
|
|
299
|
+
confirmations.push({
|
|
300
|
+
deliveryRef: record.deliveryRef,
|
|
301
|
+
emittedHashes: record.emittedHashes,
|
|
302
|
+
transcriptHashes: best.hashes,
|
|
303
|
+
positions: best.positions,
|
|
304
|
+
rendererVersion: record.rendererVersion,
|
|
305
|
+
sessionSalt: record.sessionSalt,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return confirmations;
|
|
310
|
+
}
|
|
311
|
+
/** Remove accepted references after the existing request returns. */
|
|
312
|
+
export function acknowledgeDeliveryConfirmations(sessionId, accepted) {
|
|
313
|
+
if (accepted.length === 0)
|
|
314
|
+
return;
|
|
315
|
+
const path = pendingDeliveryPath(sessionId);
|
|
316
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
317
|
+
try {
|
|
318
|
+
if (!secureRegularFile(path))
|
|
319
|
+
return;
|
|
320
|
+
const keep = readPending(sessionId).filter((record) => !accepted.includes(record.deliveryRef));
|
|
321
|
+
writeFileSync(temp, keep.map((record) => JSON.stringify(record)).join('\n') + (keep.length ? '\n' : ''), {
|
|
322
|
+
mode: 0o600,
|
|
323
|
+
flag: 'wx',
|
|
324
|
+
});
|
|
325
|
+
renameSync(temp, path);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
try {
|
|
329
|
+
unlinkSync(temp);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Cleanup is best-effort.
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** Sessions older than this can never confirm again (confirmation rides the SAME session's next hook event). */
|
|
337
|
+
export const STALE_SESSION_MS = 24 * 60 * 60 * 1000;
|
|
338
|
+
/**
|
|
339
|
+
* Remove abandoned session directories. Users close IDEs; SessionEnd rarely
|
|
340
|
+
* fires; the OS temp cleaner is not our contract. Runs once per SessionStart:
|
|
341
|
+
* one readdir plus one lstat per entry, synchronous, best-effort. Skips
|
|
342
|
+
* anything that is not a plain directory (symlinks, junctions, files) and
|
|
343
|
+
* never throws.
|
|
344
|
+
*/
|
|
345
|
+
export function sweepStaleSessionDirs(now = Date.now()) {
|
|
346
|
+
const root = privateRoot();
|
|
347
|
+
let removed = 0;
|
|
348
|
+
try {
|
|
349
|
+
if (!existsSync(root) || lstatSync(root).isSymbolicLink())
|
|
350
|
+
return 0;
|
|
351
|
+
for (const entry of readdirSync(root)) {
|
|
352
|
+
const full = join(root, entry);
|
|
353
|
+
try {
|
|
354
|
+
const st = lstatSync(full);
|
|
355
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
356
|
+
continue;
|
|
357
|
+
if (now - st.mtimeMs < STALE_SESSION_MS)
|
|
358
|
+
continue;
|
|
359
|
+
rmSync(full, { recursive: true, force: true });
|
|
360
|
+
removed += 1;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Best-effort per entry.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Best-effort overall; never blocks a session from starting.
|
|
369
|
+
}
|
|
370
|
+
return removed;
|
|
371
|
+
}
|
|
372
|
+
/** Remove one session's private pending log on clean SessionEnd. */
|
|
373
|
+
export function clearPendingDeliveries(sessionId) {
|
|
374
|
+
const path = pendingDeliveryPath(sessionId);
|
|
375
|
+
try {
|
|
376
|
+
if (existsSync(path) && secureRegularFile(path))
|
|
377
|
+
unlinkSync(path);
|
|
378
|
+
const saltPath = sessionSaltPath(sessionId);
|
|
379
|
+
if (existsSync(saltPath) && secureRegularFile(saltPath))
|
|
380
|
+
unlinkSync(saltPath);
|
|
381
|
+
rmdirSync(dirname(path));
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Session cleanup is best-effort and never blocks shutdown.
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Record the reflex fragments that survived final additionalContext composition. */
|
|
388
|
+
export function captureReflexDeliveries(input, additionalContext, response) {
|
|
389
|
+
if (!input.transcript_path || !response?.ok)
|
|
390
|
+
return false;
|
|
391
|
+
let transcriptByteOffset;
|
|
392
|
+
try {
|
|
393
|
+
transcriptByteOffset = statSync(input.transcript_path).size;
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
let appended = false;
|
|
399
|
+
const sessionSalt = getOrCreateSessionSalt(input.session_id);
|
|
400
|
+
if (!sessionSalt)
|
|
401
|
+
return false;
|
|
402
|
+
for (const injection of response.injections) {
|
|
403
|
+
if (injection.signal !== 'reflex_recall' || !injection.creditCapture)
|
|
404
|
+
continue;
|
|
405
|
+
const emittedHashes = matchFragments(additionalContext, injection.creditCapture.expectedFragmentHashes).map((item) => item.hash);
|
|
406
|
+
appended =
|
|
407
|
+
appendPendingDelivery(input.session_id, {
|
|
408
|
+
deliveryRef: injection.creditCapture.deliveryRef,
|
|
409
|
+
emittedHashes,
|
|
410
|
+
rendererVersion: injection.creditCapture.rendererVersion,
|
|
411
|
+
transcriptByteOffset,
|
|
412
|
+
sessionSalt,
|
|
413
|
+
}) || appended;
|
|
414
|
+
}
|
|
415
|
+
return appended;
|
|
416
|
+
}
|
|
417
|
+
//# sourceMappingURL=creditCapture.js.map
|