nexusmem 0.10.4 → 0.11.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 +768 -651
- package/LICENSE +21 -21
- package/README.md +605 -562
- package/dist/cli/agent-hook.js +450 -0
- package/dist/cli/agent-hook.js.map +1 -0
- package/dist/cli/index.js +1904 -291
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/recorder.js +225 -0
- package/dist/cli/recorder.js.map +1 -0
- package/package.json +71 -70
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/agent/capture-health.ts
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/config/paths.ts
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
function globalWorkspaceDir() {
|
|
11
|
+
return process.env.NEXUSMEM_HOME ?? join(homedir(), ".nexusmem");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/agent/paths.ts
|
|
15
|
+
import { join as join2 } from "path";
|
|
16
|
+
function agentEventLogPath() {
|
|
17
|
+
return join2(globalWorkspaceDir(), "agent-events.jsonl");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/agent/record.ts
|
|
21
|
+
import { appendFile, mkdir, readFile } from "fs/promises";
|
|
22
|
+
import { dirname } from "path";
|
|
23
|
+
async function appendAgentEvent(event, logPath) {
|
|
24
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
25
|
+
await appendFile(logPath, `${JSON.stringify(event)}
|
|
26
|
+
`, { encoding: "utf8", mode: 384 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/agent/capture-health.ts
|
|
30
|
+
var DROP_REASONS = [
|
|
31
|
+
"unparsable-json",
|
|
32
|
+
"unsupported-event",
|
|
33
|
+
"unsupported-tool",
|
|
34
|
+
"missing-fields",
|
|
35
|
+
"write-failed",
|
|
36
|
+
"payload-too-large"
|
|
37
|
+
];
|
|
38
|
+
var DROP_FAMILIES = ["post-tool-use", "post-tool-use-failure", "session-start", "other"];
|
|
39
|
+
var HEALTHY_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
40
|
+
function captureDropStatePath() {
|
|
41
|
+
return join3(globalWorkspaceDir(), "agent-capture-drops.json");
|
|
42
|
+
}
|
|
43
|
+
var isDropReason = (value) => DROP_REASONS.includes(value);
|
|
44
|
+
var isDropFamily = (value) => DROP_FAMILIES.includes(value);
|
|
45
|
+
function recordCaptureDrop(reason, family = "other", path = captureDropStatePath(), now = /* @__PURE__ */ new Date()) {
|
|
46
|
+
try {
|
|
47
|
+
const safeReason = isDropReason(reason) ? reason : "missing-fields";
|
|
48
|
+
const safeFamily = isDropFamily(family) ? family : "other";
|
|
49
|
+
const previous = readDropState(path);
|
|
50
|
+
const drops = typeof previous.drops === "number" && Number.isFinite(previous.drops) ? previous.drops : 0;
|
|
51
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
52
|
+
writeFileSync(
|
|
53
|
+
path,
|
|
54
|
+
JSON.stringify({ lastDropAt: now.toISOString(), lastDropReason: safeReason, lastDropFamily: safeFamily, drops: drops + 1 }),
|
|
55
|
+
{ encoding: "utf8", mode: 384 }
|
|
56
|
+
);
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function readDropState(path) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
63
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
64
|
+
} catch {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/conversation/redact.ts
|
|
70
|
+
var REDACTION_MARK = "[redacted]";
|
|
71
|
+
var MARK = REDACTION_MARK;
|
|
72
|
+
var NOT_MARK = String.raw`(?!\[redacted\])`;
|
|
73
|
+
var keepPrefix = (groups) => `${groups[0]}${MARK}`;
|
|
74
|
+
var SECRET_KEYWORD = String.raw`(?:(?:pass(?:word|wd|phrase)|secret|credential|api[_-]?key|(?:access|private|secret|client|signing|encryption|master)[_-]?key)s?|token)`;
|
|
75
|
+
var SECRET_KEY = String.raw`(?:[A-Za-z0-9_.-]{0,100}?${SECRET_KEYWORD}|(?:-{1,2}|(?:[A-Za-z0-9]{1,40}[_.-]){1,8})pass)(?:[_.-][A-Za-z0-9]{1,40}){0,8}`;
|
|
76
|
+
var TYPE_WORD = String.raw`(?:string|number|boolean|bool|int|str|null|undefined|none|nil|true|false|any|unknown|object)(?=[\s;,)|\]}>]|$)`;
|
|
77
|
+
var SECRET_VALUE = String.raw`(?:"(?:\\.|[^"\\\r\n]){1,500}"|'(?:\\.|[^'\\\r\n]){1,500}'|\x60(?:\\.|[^\x60\\\r\n]){1,500}\x60|"(?!")[^\r\n]+|'(?!')[^\r\n]+|\x60(?!\x60)[^\r\n]+|[^\s'"\x60]+)`;
|
|
78
|
+
var SAME_COMMAND = String.raw`[^\n|;&]{0,500}?`;
|
|
79
|
+
var NOT_FLAG_OR_REDIRECT = String.raw`(?![-<>|&;])`;
|
|
80
|
+
var AUTH_SCHEME = String.raw`(?:bearer|basic|token|digest|negotiate|ntlm)`;
|
|
81
|
+
var CURL_USER = String.raw`\bcurl(?=[ \t])${SAME_COMMAND}[ \t](?:-u|--user)(?:[ \t]+|=)?`;
|
|
82
|
+
var RULES = [
|
|
83
|
+
{
|
|
84
|
+
name: "private-key-block",
|
|
85
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
86
|
+
highConfidence: true
|
|
87
|
+
},
|
|
88
|
+
{ name: "aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/g, highConfidence: true },
|
|
89
|
+
{ name: "github-token", pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, highConfidence: true },
|
|
90
|
+
{ name: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, highConfidence: true },
|
|
91
|
+
{
|
|
92
|
+
name: "jwt",
|
|
93
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
94
|
+
highConfidence: true
|
|
95
|
+
},
|
|
96
|
+
// scheme://user:password@host -- the password may itself contain `@`, so the host starts after the last one.
|
|
97
|
+
{
|
|
98
|
+
name: "uri-credentials",
|
|
99
|
+
pattern: new RegExp(
|
|
100
|
+
String.raw`(\b[a-z][a-z0-9+.-]{1,30}://[^\s/?#'"@:]*:)${NOT_MARK}[^\s/?#'"]+(?=@[^\s/?#'"@]*(?:[/?#\s'"]|$))`,
|
|
101
|
+
"gi"
|
|
102
|
+
),
|
|
103
|
+
highConfidence: true,
|
|
104
|
+
render: keepPrefix
|
|
105
|
+
},
|
|
106
|
+
// An option whose name ends in `bearer` (`--oauth2-bearer x`) always takes a credential, whatever
|
|
107
|
+
// the token looks like. The prose-shaped rule below cannot cover it: it needs a digit, so an
|
|
108
|
+
// all-alphabetic token would survive.
|
|
109
|
+
{
|
|
110
|
+
name: "bearer-option-arg",
|
|
111
|
+
pattern: new RegExp(
|
|
112
|
+
String.raw`(?<=^|[\s'"(])(-{1,2}[A-Za-z0-9_.-]{0,40}bearer[ \t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,
|
|
113
|
+
"gi"
|
|
114
|
+
),
|
|
115
|
+
highConfidence: true,
|
|
116
|
+
render: keepPrefix
|
|
117
|
+
},
|
|
118
|
+
// A bearer token outside a header (`Bearer x`); it must contain a digit so "bearer authentication" is left alone.
|
|
119
|
+
{
|
|
120
|
+
name: "bearer-token",
|
|
121
|
+
pattern: new RegExp(String.raw`(\bbearer[ \t]+)${NOT_MARK}(?=[A-Za-z0-9._~+/-]*\d)[A-Za-z0-9._~+/-]{16,}=*`, "gi"),
|
|
122
|
+
highConfidence: true,
|
|
123
|
+
render: keepPrefix
|
|
124
|
+
},
|
|
125
|
+
// Tool-anchored password arguments: `-p` means port or parents almost everywhere else. Case-sensitive (`-P` is mysql's port).
|
|
126
|
+
// mysql takes only the compact `-psecret` form; `-p secret` means "prompt, then use database secret".
|
|
127
|
+
{
|
|
128
|
+
name: "mysql-password-arg",
|
|
129
|
+
pattern: new RegExp(
|
|
130
|
+
String.raw`(\b(?:mysql|mysqldump|mysqladmin|mysqlimport|mysqlsh|mariadb|mariadb-dump)(?=[ \t])${SAME_COMMAND}[ \t]-p)${NOT_MARK}(?=\S)${SECRET_VALUE}`,
|
|
131
|
+
"g"
|
|
132
|
+
),
|
|
133
|
+
highConfidence: true,
|
|
134
|
+
render: keepPrefix
|
|
135
|
+
},
|
|
136
|
+
// Only sshpass's own options may precede -p, so a wrapped `ssh -p 22` is never read as the password.
|
|
137
|
+
{
|
|
138
|
+
name: "sshpass-password-arg",
|
|
139
|
+
pattern: new RegExp(String.raw`(\bsshpass(?:[ \t]+-[^p\s]\S*)*[ \t]+-p[ \t]*)${NOT_MARK}(?=\S)${SECRET_VALUE}`, "g"),
|
|
140
|
+
highConfidence: true,
|
|
141
|
+
render: keepPrefix
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "mongo-password-arg",
|
|
145
|
+
pattern: new RegExp(
|
|
146
|
+
String.raw`(\bmongo(?:sh|dump|restore|export|import|stat|top|files)?(?=[ \t])${SAME_COMMAND}[ \t]-p[ \t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,
|
|
147
|
+
"g"
|
|
148
|
+
),
|
|
149
|
+
highConfidence: true,
|
|
150
|
+
render: keepPrefix
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "redis-cli-password-arg",
|
|
154
|
+
pattern: new RegExp(
|
|
155
|
+
String.raw`(\bredis-cli(?=[ \t])${SAME_COMMAND}[ \t]-a[ \t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,
|
|
156
|
+
"g"
|
|
157
|
+
),
|
|
158
|
+
highConfidence: true,
|
|
159
|
+
render: keepPrefix
|
|
160
|
+
},
|
|
161
|
+
// Quoted credentials get their own branches: `curl -u "user:pass word"` holds a space, which
|
|
162
|
+
// the unquoted branch would stop at, leaving the rest of the password in the text. Like
|
|
163
|
+
// SECRET_VALUE, they step over escaped characters so `"alice:pa\"ss"` is not cut at `\"`. Inside
|
|
164
|
+
// quotes the user name may hold a space too (`"alice smith:pw"`); only the quote ends it.
|
|
165
|
+
{
|
|
166
|
+
name: "curl-user-password",
|
|
167
|
+
pattern: new RegExp(
|
|
168
|
+
String.raw`(${CURL_USER}"(?:\\.|[^:"\\\r\n])*:)${NOT_MARK}(?:\\.|[^"\\\r\n])*|(${CURL_USER}'(?:\\.|[^:'\\\r\n])*:)${NOT_MARK}(?:\\.|[^'\\\r\n])*|(${CURL_USER}[^\s:'"]*:)${NOT_MARK}[^\s'"]+`,
|
|
169
|
+
"g"
|
|
170
|
+
),
|
|
171
|
+
highConfidence: true,
|
|
172
|
+
render: (groups) => `${groups.find((g) => g.length > 0) ?? ""}${MARK}`
|
|
173
|
+
},
|
|
174
|
+
// Authorization: Bearer x / Basic x / token x. The scheme word is kept; it must not be mistaken for the value.
|
|
175
|
+
{
|
|
176
|
+
name: "authorization-header",
|
|
177
|
+
pattern: new RegExp(
|
|
178
|
+
String.raw`(\b(?:proxy-)?authorization["']?[ \t]*[:=][ \t]*["'\x60]?(?:${AUTH_SCHEME}[ \t]+)?)(?!${AUTH_SCHEME}[ \t])${NOT_MARK}[^\s'"\x60]+`,
|
|
179
|
+
"gi"
|
|
180
|
+
),
|
|
181
|
+
highConfidence: false,
|
|
182
|
+
render: keepPrefix
|
|
183
|
+
},
|
|
184
|
+
// `--password secret`: a secret-named option whose value is the next argument. The flag must start a
|
|
185
|
+
// command-line word, so prose like "the `id`-token heuristic" is not read as a `-token` option.
|
|
186
|
+
{
|
|
187
|
+
name: "secret-option-arg",
|
|
188
|
+
pattern: new RegExp(
|
|
189
|
+
String.raw`(?<=^|[\s'"(])(-{1,2}${SECRET_KEY}[ \t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,
|
|
190
|
+
"gi"
|
|
191
|
+
),
|
|
192
|
+
highConfidence: false,
|
|
193
|
+
render: keepPrefix
|
|
194
|
+
},
|
|
195
|
+
// key/token/secret/password = "value" or : value, in code, JSON, env-file, shell or prose form.
|
|
196
|
+
// The key start is an explicit non-identifier lookbehind, not `\b`: `_` is a word character, so
|
|
197
|
+
// `\b` never fired inside DB_PASSWORD and the value leaked. Found live via `scan-shell`.
|
|
198
|
+
{
|
|
199
|
+
name: "key-value-secret",
|
|
200
|
+
pattern: new RegExp(
|
|
201
|
+
String.raw`(?<![A-Za-z0-9_.-])(${SECRET_KEY})["']?[ \t]*[:=](?![=>])[ \t]*(?!${TYPE_WORD})${NOT_MARK}${SECRET_VALUE}`,
|
|
202
|
+
"gi"
|
|
203
|
+
),
|
|
204
|
+
highConfidence: false,
|
|
205
|
+
// Keep the key name so the redaction is legible ("apiKey: [redacted]").
|
|
206
|
+
render: (groups) => `${groups[0]}: ${MARK}`
|
|
207
|
+
}
|
|
208
|
+
];
|
|
209
|
+
function redact(text, profile = "all") {
|
|
210
|
+
let redactedCount = 0;
|
|
211
|
+
let out = text;
|
|
212
|
+
for (const rule of RULES) {
|
|
213
|
+
if (profile === "high-confidence" && !rule.highConfidence) continue;
|
|
214
|
+
out = out.replace(rule.pattern, (...args) => {
|
|
215
|
+
redactedCount += 1;
|
|
216
|
+
const end = args.findIndex((arg2, i) => i > 0 && typeof arg2 === "number");
|
|
217
|
+
const groups = args.slice(1, end).map((g) => typeof g === "string" ? g : "");
|
|
218
|
+
return rule.render ? rule.render(groups) : MARK;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return { text: out, redactedCount };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/core/ids.ts
|
|
225
|
+
import { createHash } from "crypto";
|
|
226
|
+
function sha256Hex(input) {
|
|
227
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/agent/event.ts
|
|
231
|
+
var OBSERVATION_COMMANDS = /* @__PURE__ */ new Set(["ls", "pwd", "echo", "cat"]);
|
|
232
|
+
var OBSERVATION_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set(["status", "log", "diff", "show"]);
|
|
233
|
+
var UNSAFE_SEGMENT = /[|&<>`(){}]|\$\(/;
|
|
234
|
+
function splitSegments(command) {
|
|
235
|
+
const segments = [];
|
|
236
|
+
let current = "";
|
|
237
|
+
let quote = null;
|
|
238
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
239
|
+
const ch = command[i];
|
|
240
|
+
if (quote) {
|
|
241
|
+
current += ch;
|
|
242
|
+
if (ch === quote) quote = null;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (ch === '"' || ch === "'") {
|
|
246
|
+
quote = ch;
|
|
247
|
+
current += ch;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (ch === ";") {
|
|
251
|
+
segments.push(current);
|
|
252
|
+
current = "";
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (ch === "&" && command[i + 1] === "&") {
|
|
256
|
+
segments.push(current);
|
|
257
|
+
current = "";
|
|
258
|
+
i += 1;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
current += ch;
|
|
262
|
+
}
|
|
263
|
+
if (quote) return null;
|
|
264
|
+
segments.push(current);
|
|
265
|
+
return segments.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
266
|
+
}
|
|
267
|
+
var CD_SEGMENT = /^cd\s+(?:"([^"]*)"|'([^']*)'|(\S+))$/;
|
|
268
|
+
var DRIVE_SPELLING = /^\/(?:mnt\/)?([a-zA-Z])(?:\/|$)/;
|
|
269
|
+
var WINDOWS_DRIVE = /^[a-zA-Z]:(?:\/|$)/;
|
|
270
|
+
var WINDOWS_UNC = /^\/\/[^/]+\/[^/]+/;
|
|
271
|
+
function directoryForCompare(path, platform) {
|
|
272
|
+
if (platform !== "win32") return { text: path.replace(/\/+$/, ""), foldsCase: false };
|
|
273
|
+
const slashed = path.replace(/\\/g, "/");
|
|
274
|
+
const drive = DRIVE_SPELLING.exec(slashed);
|
|
275
|
+
const native = drive ? `${drive[1]}:/${slashed.slice(drive[0].length)}` : slashed;
|
|
276
|
+
return { text: native.replace(/\/+$/, ""), foldsCase: WINDOWS_DRIVE.test(native) || WINDOWS_UNC.test(native) };
|
|
277
|
+
}
|
|
278
|
+
function sameDirectory(a, b, platform = process.platform) {
|
|
279
|
+
const x = directoryForCompare(a, platform);
|
|
280
|
+
const y = directoryForCompare(b, platform);
|
|
281
|
+
return x.foldsCase && y.foldsCase ? x.text.toLowerCase() === y.text.toLowerCase() : x.text === y.text;
|
|
282
|
+
}
|
|
283
|
+
function classify(segment, cwd, platform) {
|
|
284
|
+
if (UNSAFE_SEGMENT.test(segment)) return "target";
|
|
285
|
+
const cd = CD_SEGMENT.exec(segment);
|
|
286
|
+
if (cd) {
|
|
287
|
+
if (!cwd) return "target";
|
|
288
|
+
const to = cd[1] ?? cd[2] ?? cd[3] ?? "";
|
|
289
|
+
if (to === ".") return "navigation";
|
|
290
|
+
return sameDirectory(to, cwd, platform) ? "navigation" : "target";
|
|
291
|
+
}
|
|
292
|
+
const [head, next] = segment.split(/\s+/);
|
|
293
|
+
if (!head) return "target";
|
|
294
|
+
if (OBSERVATION_COMMANDS.has(head)) return "observation";
|
|
295
|
+
if (head === "git" && next !== void 0 && OBSERVATION_GIT_SUBCOMMANDS.has(next)) return "observation";
|
|
296
|
+
return "target";
|
|
297
|
+
}
|
|
298
|
+
function canonicalizeCommand(command, cwd, platform = process.platform) {
|
|
299
|
+
const segments = splitSegments(command);
|
|
300
|
+
if (!segments) return command;
|
|
301
|
+
const targets = segments.filter((s) => classify(s, cwd, platform) === "target");
|
|
302
|
+
return targets.length === 1 ? targets[0] : command;
|
|
303
|
+
}
|
|
304
|
+
var MAX_ERROR_SIGNATURE_CHARS = 200;
|
|
305
|
+
function redactAgentEvent(raw) {
|
|
306
|
+
const { command, errorSignature, ...rest } = raw;
|
|
307
|
+
return {
|
|
308
|
+
...rest,
|
|
309
|
+
...command === void 0 ? {} : {
|
|
310
|
+
command: redact(command).text,
|
|
311
|
+
commandHash: sha256Hex(command).slice(0, 12),
|
|
312
|
+
execHash: sha256Hex(canonicalizeCommand(command, raw.cwd)).slice(0, 12)
|
|
313
|
+
},
|
|
314
|
+
...errorSignature === void 0 ? {} : { errorSignature: redact(errorSignature).text.slice(0, MAX_ERROR_SIGNATURE_CHARS) }
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/adapters/claude-code/payload.ts
|
|
319
|
+
var AGENT = "claude-code";
|
|
320
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
321
|
+
var EXIT_CODE = /^Exit code (\d+)/;
|
|
322
|
+
var str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
323
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
324
|
+
var EXIT_ECHO = /^exit\s*[:=]\s*(\d+)\s*$/i;
|
|
325
|
+
var EXIT_WRAPPER = /^([\s\S]*?);\s*echo\s+(?:"exit\s*[:=]\s*\$\?"|exit\s*[:=]\s*\$\?)\s*$/i;
|
|
326
|
+
function echoesOwnExitStatus(command) {
|
|
327
|
+
const body = EXIT_WRAPPER.exec(command.trim())?.[1];
|
|
328
|
+
return body !== void 0 && body.trim().length > 0 && !/[|;\r\n]/.test(body);
|
|
329
|
+
}
|
|
330
|
+
var OPAQUE_FILTER_TAIL = /\|\s*(?:head|tail|tee)(?![A-Za-z0-9._-])[^|]*$/i;
|
|
331
|
+
function pipesIntoOpaqueFilter(command) {
|
|
332
|
+
return OPAQUE_FILTER_TAIL.test(command.trim());
|
|
333
|
+
}
|
|
334
|
+
function classifyBashResult(hookOutcome, failed, error, recovered, command) {
|
|
335
|
+
if (failed) return { outcome: hookOutcome, exitCode: error?.exitCode ?? null };
|
|
336
|
+
if (recovered !== null) return { outcome: recovered === 0 ? "ok" : "fail", exitCode: recovered };
|
|
337
|
+
if (pipesIntoOpaqueFilter(command)) return { outcome: "unknown", exitCode: null };
|
|
338
|
+
return { outcome: hookOutcome, exitCode: 0 };
|
|
339
|
+
}
|
|
340
|
+
function recoverExitStatusFromOutput(command, stdout) {
|
|
341
|
+
if (!stdout || !echoesOwnExitStatus(command)) return null;
|
|
342
|
+
const lines = stdout.split(/\r?\n/).map((l) => l.trim());
|
|
343
|
+
let last = "";
|
|
344
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
345
|
+
if (lines[i]) {
|
|
346
|
+
last = lines[i];
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const match = EXIT_ECHO.exec(last);
|
|
351
|
+
return match ? Number(match[1]) : null;
|
|
352
|
+
}
|
|
353
|
+
function parseError(error) {
|
|
354
|
+
const [first = "", ...rest] = error.split(/\r?\n/);
|
|
355
|
+
const match = EXIT_CODE.exec(first);
|
|
356
|
+
if (!match) return { exitCode: null, signature: error.trim() };
|
|
357
|
+
return { exitCode: Number(match[1]), signature: rest.join(" ").trim() };
|
|
358
|
+
}
|
|
359
|
+
var FAMILY_BY_EVENT = {
|
|
360
|
+
PostToolUse: "post-tool-use",
|
|
361
|
+
PostToolUseFailure: "post-tool-use-failure",
|
|
362
|
+
SessionStart: "session-start"
|
|
363
|
+
};
|
|
364
|
+
var familyOf = (event) => event ? FAMILY_BY_EVENT[event] ?? "other" : "other";
|
|
365
|
+
function parseHookPayloadDetailed(rawJson, now) {
|
|
366
|
+
let payload;
|
|
367
|
+
try {
|
|
368
|
+
payload = JSON.parse(rawJson.trim());
|
|
369
|
+
} catch {
|
|
370
|
+
return { ok: false, reason: "unparsable-json", family: "other" };
|
|
371
|
+
}
|
|
372
|
+
if (typeof payload !== "object" || payload === null) return { ok: false, reason: "unparsable-json", family: "other" };
|
|
373
|
+
const p = payload;
|
|
374
|
+
const event = str(p.hook_event_name);
|
|
375
|
+
const family = familyOf(event);
|
|
376
|
+
const sessionId = str(p.session_id);
|
|
377
|
+
const eventId = str(p.tool_use_id);
|
|
378
|
+
const toolName = str(p.tool_name);
|
|
379
|
+
if (event !== "PostToolUse" && event !== "PostToolUseFailure") return { ok: false, reason: "unsupported-event", family };
|
|
380
|
+
if (!sessionId || !eventId || !toolName) return { ok: false, reason: "missing-fields", family };
|
|
381
|
+
const failed = event === "PostToolUseFailure";
|
|
382
|
+
const interrupted = failed && p.is_interrupt === true;
|
|
383
|
+
const outcome = interrupted ? "interrupted" : failed ? "fail" : "ok";
|
|
384
|
+
const base = {
|
|
385
|
+
agent: AGENT,
|
|
386
|
+
sessionId,
|
|
387
|
+
eventId,
|
|
388
|
+
ts: now,
|
|
389
|
+
cwd: str(p.cwd) ?? null,
|
|
390
|
+
outcome,
|
|
391
|
+
durationMs: num(p.duration_ms),
|
|
392
|
+
...str(p.agent_id) ? { agentId: str(p.agent_id) } : {}
|
|
393
|
+
};
|
|
394
|
+
if (toolName === "Bash") {
|
|
395
|
+
const command = str(p.tool_input?.command);
|
|
396
|
+
if (!command) return { ok: false, reason: "missing-fields", family };
|
|
397
|
+
const error = failed ? parseError(str(p.error) ?? "") : null;
|
|
398
|
+
const recovered = failed ? null : recoverExitStatusFromOutput(command, str(p.tool_response?.stdout));
|
|
399
|
+
const draft = {
|
|
400
|
+
...base,
|
|
401
|
+
kind: "command",
|
|
402
|
+
command,
|
|
403
|
+
...classifyBashResult(outcome, failed, error, recovered, command),
|
|
404
|
+
...error?.signature ? { errorSignature: error.signature } : {}
|
|
405
|
+
};
|
|
406
|
+
return { ok: true, event: redactAgentEvent(draft), family };
|
|
407
|
+
}
|
|
408
|
+
if (EDIT_TOOLS.has(toolName)) {
|
|
409
|
+
const filePath = str(p.tool_input?.file_path) ?? str(p.tool_input?.notebook_path);
|
|
410
|
+
if (!filePath) return { ok: false, reason: "missing-fields", family };
|
|
411
|
+
return { ok: true, event: redactAgentEvent({ ...base, kind: "edit", filePath, exitCode: null }), family };
|
|
412
|
+
}
|
|
413
|
+
return { ok: false, reason: "unsupported-tool", family };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/adapters/claude-code/hook-entry.ts
|
|
417
|
+
var MAX_EVENT_BYTES = 1e6;
|
|
418
|
+
var drop = () => process.exit(1);
|
|
419
|
+
process.on("uncaughtException", drop);
|
|
420
|
+
process.on("unhandledRejection", drop);
|
|
421
|
+
function arg(name) {
|
|
422
|
+
const i = process.argv.indexOf(name);
|
|
423
|
+
return i === -1 ? void 0 : process.argv[i + 1];
|
|
424
|
+
}
|
|
425
|
+
async function main() {
|
|
426
|
+
const chunks = [];
|
|
427
|
+
let size = 0;
|
|
428
|
+
for await (const chunk of process.stdin) {
|
|
429
|
+
size += chunk.length;
|
|
430
|
+
if (size > MAX_EVENT_BYTES) {
|
|
431
|
+
recordCaptureDrop("payload-too-large", "other", captureDropStatePath());
|
|
432
|
+
drop();
|
|
433
|
+
}
|
|
434
|
+
chunks.push(chunk);
|
|
435
|
+
}
|
|
436
|
+
const outcome = parseHookPayloadDetailed(Buffer.concat(chunks).toString("utf8"), (/* @__PURE__ */ new Date()).toISOString());
|
|
437
|
+
if (!outcome.ok) {
|
|
438
|
+
recordCaptureDrop(outcome.reason, outcome.family, captureDropStatePath());
|
|
439
|
+
return drop();
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
await appendAgentEvent(outcome.event, arg("--log") ?? agentEventLogPath());
|
|
443
|
+
} catch {
|
|
444
|
+
recordCaptureDrop("write-failed", outcome.family, captureDropStatePath());
|
|
445
|
+
return drop();
|
|
446
|
+
}
|
|
447
|
+
process.exit(0);
|
|
448
|
+
}
|
|
449
|
+
main().catch(drop);
|
|
450
|
+
//# sourceMappingURL=agent-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/capture-health.ts","../../src/config/paths.ts","../../src/agent/paths.ts","../../src/agent/record.ts","../../src/conversation/redact.ts","../../src/core/ids.ts","../../src/agent/event.ts","../../src/adapters/claude-code/payload.ts","../../src/adapters/claude-code/hook-entry.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { globalWorkspaceDir } from '../config/paths.js';\nimport { agentEventLogPath } from './paths.js';\nimport { parseAgentEventLine } from './record.js';\n\n/**\n * Whether capture is actually working, answered from evidence already on\n * disk rather than from the fact that hooks are configured.\n *\n * Two sources, both local, neither of which stores anything about the payload:\n *\n * - the agent event log's last line says when capture last succeeded. Reading\n * it costs nothing on the hook's path, because nothing extra is written.\n * - a drop marker file says when the hook last threw an event away, and why,\n * as a code from a fixed set. Without it, \"no events because the developer\n * was not coding\" and \"no events because the payload shape changed\" look\n * identical, which is exactly the silent failure this exists to catch.\n *\n * Deliberately no state a healthy run has to update: the common path writes\n * one line to the log and nothing else.\n */\n\nexport type CaptureHealth = 'healthy' | 'stale' | 'degraded' | 'never-observed' | 'unknown';\n\n/** Reasons the hook can record. A closed set, so no payload text can ever reach the file. */\nexport const DROP_REASONS = [\n 'unparsable-json',\n 'unsupported-event',\n 'unsupported-tool',\n 'missing-fields',\n 'write-failed',\n 'payload-too-large',\n] as const;\nexport type DropReason = (typeof DROP_REASONS)[number];\n\n/**\n * Which family of hook the dropped event came from, as a normalized code --\n * never the vendor's own string. Enough to tell \"the tool payload changed\"\n * from \"something is sending us an event we never asked for\" while debugging.\n */\nexport const DROP_FAMILIES = ['post-tool-use', 'post-tool-use-failure', 'session-start', 'other'] as const;\nexport type DropFamily = (typeof DROP_FAMILIES)[number];\n\n/** How recently capture must have worked to count as healthy. A day covers a normal working rhythm. */\nexport const HEALTHY_WINDOW_MS = 24 * 60 * 60 * 1000;\n\nexport interface CaptureStatus {\n health: CaptureHealth;\n lastEventAt: string | null;\n /** 'command' | 'edit' -- the kind of the last captured event, never its content. */\n lastEventKind: string | null;\n lastEventOutcome: string | null;\n lastDropAt: string | null;\n lastDropReason: DropReason | null;\n lastDropFamily: DropFamily | null;\n drops: number;\n}\n\ninterface DropState {\n lastDropAt?: unknown;\n lastDropReason?: unknown;\n lastDropFamily?: unknown;\n drops?: unknown;\n}\n\nexport function captureDropStatePath(): string {\n return join(globalWorkspaceDir(), 'agent-capture-drops.json');\n}\n\nconst isDropReason = (value: unknown): value is DropReason => DROP_REASONS.includes(value as DropReason);\nconst isDropFamily = (value: unknown): value is DropFamily => DROP_FAMILIES.includes(value as DropFamily);\n\n/**\n * Records that one event was thrown away. Called from the hook's failure\n * path, so it must never throw and never block the agent.\n *\n * The file is overwritten with the same four fields every time, so a hook\n * dropping every event for a week cannot grow it. Nothing derived from the\n * payload -- not a fragment of it, not a parser's exception message, which\n * would quote the input -- is written here.\n */\nexport function recordCaptureDrop(\n reason: DropReason,\n family: DropFamily = 'other',\n path = captureDropStatePath(),\n now = new Date(),\n): void {\n try {\n // Re-validated rather than trusted: these are the only fields a caller\n // could otherwise use to smuggle payload text into a file.\n const safeReason: DropReason = isDropReason(reason) ? reason : 'missing-fields';\n const safeFamily: DropFamily = isDropFamily(family) ? family : 'other';\n const previous = readDropState(path);\n const drops = typeof previous.drops === 'number' && Number.isFinite(previous.drops) ? previous.drops : 0;\n // A drop can be the first thing the hook ever writes, before anything has created the directory.\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(\n path,\n JSON.stringify({ lastDropAt: now.toISOString(), lastDropReason: safeReason, lastDropFamily: safeFamily, drops: drops + 1 }),\n { encoding: 'utf8', mode: 0o600 },\n );\n } catch {\n // Health reporting is never worth failing a capture over.\n }\n}\n\nfunction readDropState(path: string): DropState {\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));\n return typeof parsed === 'object' && parsed !== null ? (parsed as DropState) : {};\n } catch {\n return {};\n }\n}\n\n/** The last line the log holds that still parses; a torn final line is skipped, not fatal. */\nfunction lastEvent(logPath: string): { at: string; kind: string; outcome: string } | null | 'unreadable' {\n let raw: string;\n try {\n raw = readFileSync(logPath, 'utf8');\n } catch (err) {\n // Absent is a real answer (\"never observed\"); anything else means we cannot tell.\n return (err as NodeJS.ErrnoException).code === 'ENOENT' ? null : 'unreadable';\n }\n const lines = raw.split(/\\r?\\n/).filter((l) => l.trim().length > 0);\n for (let i = lines.length - 1; i >= 0; i -= 1) {\n const event = parseAgentEventLine(lines[i]!);\n if (event) return { at: event.ts, kind: event.kind, outcome: event.outcome };\n }\n return lines.length > 0 ? 'unreadable' : null;\n}\n\nexport interface CaptureStatusOptions {\n logPath?: string;\n dropStatePath?: string;\n now?: Date;\n}\n\nexport function readCaptureStatus(opts: CaptureStatusOptions = {}): CaptureStatus {\n const now = opts.now ?? new Date();\n const event = lastEvent(opts.logPath ?? agentEventLogPath());\n const drop = readDropState(opts.dropStatePath ?? captureDropStatePath());\n\n const lastDropAt = typeof drop.lastDropAt === 'string' ? drop.lastDropAt : null;\n const lastDropReason = isDropReason(drop.lastDropReason) ? drop.lastDropReason : null;\n const lastDropFamily = isDropFamily(drop.lastDropFamily) ? drop.lastDropFamily : null;\n const drops = typeof drop.drops === 'number' && Number.isFinite(drop.drops) ? drop.drops : 0;\n const base = { lastDropAt, lastDropReason, lastDropFamily, drops };\n\n if (event === 'unreadable') {\n return { ...base, health: 'unknown', lastEventAt: null, lastEventKind: null, lastEventOutcome: null };\n }\n // Only a recent drop is evidence of a problem now. An old one, with nothing\n // since, says the machine has been quiet -- not that capture is broken.\n const droppedRecently = lastDropAt !== null && now.getTime() - Date.parse(lastDropAt) <= HEALTHY_WINDOW_MS;\n\n if (event === null) {\n // Events arriving and being dropped, with none ever captured, is the\n // signature of a payload shape this adapter no longer understands.\n return {\n ...base,\n health: droppedRecently ? 'degraded' : 'never-observed',\n lastEventAt: null,\n lastEventKind: null,\n lastEventOutcome: null,\n };\n }\n\n const captured = { lastEventAt: event.at, lastEventKind: event.kind, lastEventOutcome: event.outcome };\n const eventAt = Date.parse(event.at);\n if (Number.isNaN(eventAt)) return { ...base, ...captured, health: 'unknown' };\n\n // A recent drop after the last success: capture worked, then stopped\n // working. A later successful capture outranks an earlier drop, which is\n // what lets health return to healthy once events flow again.\n if (droppedRecently && lastDropAt !== null && Date.parse(lastDropAt) > eventAt) {\n return { ...base, ...captured, health: 'degraded' };\n }\n\n return { ...base, ...captured, health: now.getTime() - eventAt <= HEALTHY_WINDOW_MS ? 'healthy' : 'stale' };\n}\n","import { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n/**\n * The user-scoped (not repo-scoped) NexusMem directory.\n *\n * Lived in `shell/paths.ts` while the hook log was the only thing in it. The\n * project registry is the second, and it has nothing to do with shells, so\n * the location moved somewhere neither feature owns.\n *\n * `NEXUSMEM_HOME` overrides it. That is not a convenience flag: without it a\n * test of anything user-scoped would read and write the developer's real home\n * directory, which is exactly the kind of test this project does not have.\n */\nexport function globalWorkspaceDir(): string {\n return process.env.NEXUSMEM_HOME ?? join(homedir(), '.nexusmem');\n}\n","import { join } from 'node:path';\nimport { globalWorkspaceDir } from '../config/paths.js';\n\n/**\n * One stream for every agent session on this machine, like the shell hook log\n * -- an agent session moves between repos, so the log is filtered by cwd at\n * read time rather than split per project.\n */\nexport function agentEventLogPath(): string {\n return join(globalWorkspaceDir(), 'agent-events.jsonl');\n}\n","import { appendFile, mkdir, readFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type { AgentEvent, AgentEventKind, AgentOutcome } from './event.js';\n\n/**\n * The only writer and the only reader of the agent event log.\n *\n * Same contract as the shell recorder: an adapter hands over an already\n * redacted event, and a single append persists it. No temp file, so a crash\n * can leave nothing half-written behind.\n */\n\nconst KINDS: ReadonlySet<string> = new Set<AgentEventKind>(['command', 'edit']);\nconst OUTCOMES: ReadonlySet<string> = new Set<AgentOutcome>(['ok', 'fail', 'interrupted', 'unknown']);\n\nexport async function appendAgentEvent(event: AgentEvent, logPath: string): Promise<void> {\n await mkdir(dirname(logPath), { recursive: true });\n await appendFile(logPath, `${JSON.stringify(event)}\\n`, { encoding: 'utf8', mode: 0o600 });\n}\n\n/** A torn or unrecognised line is skipped, never fatal: one bad line must not stop a sync. */\nexport function parseAgentEventLine(line: string): AgentEvent | null {\n const trimmed = line.trim();\n if (!trimmed) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return null;\n }\n if (typeof parsed !== 'object' || parsed === null) return null;\n const e = parsed as Record<string, unknown>;\n if (typeof e.agent !== 'string' || typeof e.sessionId !== 'string' || typeof e.eventId !== 'string') return null;\n if (typeof e.ts !== 'string' || typeof e.kind !== 'string' || !KINDS.has(e.kind)) return null;\n if (typeof e.outcome !== 'string' || !OUTCOMES.has(e.outcome)) return null;\n return parsed as AgentEvent;\n}\n\nexport interface ReadAgentEventsResult {\n events: AgentEvent[];\n /** Total lines in the file: the caller's next cursor, as with the shell hook log. */\n totalLines: number;\n}\n\n/** Reads lines appended since `fromLine`; a cursor past the end means the file was rotated, so re-read it all. */\nexport async function readAgentEvents(logPath: string, fromLine: number): Promise<ReadAgentEventsResult> {\n let raw: string;\n try {\n raw = await readFile(logPath, 'utf8');\n } catch {\n return { events: [], totalLines: fromLine };\n }\n const lines = raw.split(/\\r?\\n/).filter((l) => l.length > 0);\n const start = fromLine > 0 && fromLine <= lines.length ? fromLine : 0;\n const events = lines.slice(start).map(parseAgentEventLine).filter((e): e is AgentEvent => e !== null);\n return { events, totalLines: lines.length };\n}\n","/**\n * Best-effort secret redaction for collected text before it is ever written\n * to the FTS index.\n *\n * This is a safety net, not a guarantee -- pattern-based redaction cannot\n * catch every shape a secret can take. It exists because conversation text\n * is the collector most likely to contain something sensitive (a pasted\n * credential, a key a user asked for help debugging), but a committed `.env`\n * or a hard-coded key makes the code-diff collector a real second candidate,\n * which is what the `high-confidence` profile below is for.\n */\n\ninterface Rule {\n name: string;\n pattern: RegExp;\n /**\n * The match is a secret by its own shape, with no reliance on the\n * surrounding text. Only these rules are safe to run over source code:\n * the shape rules match strings nothing else produces, while the\n * key/value rule matches ordinary code such as\n * `const apiKey = process.env.API_KEY` and would corrupt the very lines\n * a diff is indexed for.\n */\n highConfidence: boolean;\n /** Replacement built from the capture groups; without it the whole match becomes the marker. */\n render?: (groups: readonly string[]) => string;\n}\n\nexport const REDACTION_MARK = '[redacted]';\nconst MARK = REDACTION_MARK;\n// Every rule refuses to re-match its own marker, so redacting twice changes nothing.\nconst NOT_MARK = String.raw`(?!\\[redacted\\])`;\nconst keepPrefix = (groups: readonly string[]): string => `${groups[0]}${MARK}`;\n\n// The keyword may sit anywhere inside an identifier (DB_PASSWORD, OPENAI_API_KEY, dbPassword, --api-key).\n// `token` is singular only: plural keys (max_tokens, rawTokens) are LLM token counts, not credentials.\nconst SECRET_KEYWORD = String.raw`(?:(?:pass(?:word|wd|phrase)|secret|credential|api[_-]?key|(?:access|private|secret|client|signing|encryption|master)[_-]?key)s?|token)`;\n// Bare `pass` needs a prefix component or dashes (DB_PASS, --pass), so prose like \"first pass: x\" and `bypass` never match.\n// Bounded so a long identifier run full of keywords stays linear, not quadratic.\nconst SECRET_KEY = String.raw`(?:[A-Za-z0-9_.-]{0,100}?${SECRET_KEYWORD}|(?:-{1,2}|(?:[A-Za-z0-9]{1,40}[_.-]){1,8})pass)(?:[_.-][A-Za-z0-9]{1,40}){0,8}`;\n// Type annotations (`password: string`) are not values; everything else is hidden, however short.\nconst TYPE_WORD = String.raw`(?:string|number|boolean|bool|int|str|null|undefined|none|nil|true|false|any|unknown|object)(?=[\\s;,)|\\]}>]|$)`;\n// A quoted value runs to its *closing* quote: an escaped quote inside it (--password \"pa\\\"ss\")\n// must not end the match, or the tail after it survives redaction. Bounded so an ordinary value\n// stops at its own quote; one the bound cannot close (over 500 chars, or never closed) fails\n// closed and takes the rest of the line rather than none of it.\nconst SECRET_VALUE = String.raw`(?:\"(?:\\\\.|[^\"\\\\\\r\\n]){1,500}\"|'(?:\\\\.|[^'\\\\\\r\\n]){1,500}'|\\x60(?:\\\\.|[^\\x60\\\\\\r\\n]){1,500}\\x60|\"(?!\")[^\\r\\n]+|'(?!')[^\\r\\n]+|\\x60(?!\\x60)[^\\r\\n]+|[^\\s'\"\\x60]+)`;\n// The rest of one shell command: stops at a pipe, `;`, `&` or newline so a tool name never reaches into the next command.\nconst SAME_COMMAND = String.raw`[^\\n|;&]{0,500}?`;\n// A next argument that is a flag or a redirection is not a value.\nconst NOT_FLAG_OR_REDIRECT = String.raw`(?![-<>|&;])`;\nconst AUTH_SCHEME = String.raw`(?:bearer|basic|token|digest|negotiate|ntlm)`;\n/** Everything up to and including curl's credential flag, shared by that rule's quoted and bare branches. */\nconst CURL_USER = String.raw`\\bcurl(?=[ \\t])${SAME_COMMAND}[ \\t](?:-u|--user)(?:[ \\t]+|=)?`;\n\nconst RULES: Rule[] = [\n {\n name: 'private-key-block',\n pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,\n highConfidence: true,\n },\n { name: 'aws-access-key', pattern: /\\bAKIA[0-9A-Z]{16}\\b/g, highConfidence: true },\n { name: 'github-token', pattern: /\\bgh[pousr]_[A-Za-z0-9]{20,}\\b/g, highConfidence: true },\n { name: 'slack-token', pattern: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g, highConfidence: true },\n {\n name: 'jwt',\n pattern: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g,\n highConfidence: true,\n },\n // scheme://user:password@host -- the password may itself contain `@`, so the host starts after the last one.\n {\n name: 'uri-credentials',\n pattern: new RegExp(\n String.raw`(\\b[a-z][a-z0-9+.-]{1,30}://[^\\s/?#'\"@:]*:)${NOT_MARK}[^\\s/?#'\"]+(?=@[^\\s/?#'\"@]*(?:[/?#\\s'\"]|$))`,\n 'gi',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n // An option whose name ends in `bearer` (`--oauth2-bearer x`) always takes a credential, whatever\n // the token looks like. The prose-shaped rule below cannot cover it: it needs a digit, so an\n // all-alphabetic token would survive.\n {\n name: 'bearer-option-arg',\n pattern: new RegExp(\n String.raw`(?<=^|[\\s'\"(])(-{1,2}[A-Za-z0-9_.-]{0,40}bearer[ \\t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,\n 'gi',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n // A bearer token outside a header (`Bearer x`); it must contain a digit so \"bearer authentication\" is left alone.\n {\n name: 'bearer-token',\n pattern: new RegExp(String.raw`(\\bbearer[ \\t]+)${NOT_MARK}(?=[A-Za-z0-9._~+/-]*\\d)[A-Za-z0-9._~+/-]{16,}=*`, 'gi'),\n highConfidence: true,\n render: keepPrefix,\n },\n // Tool-anchored password arguments: `-p` means port or parents almost everywhere else. Case-sensitive (`-P` is mysql's port).\n // mysql takes only the compact `-psecret` form; `-p secret` means \"prompt, then use database secret\".\n {\n name: 'mysql-password-arg',\n pattern: new RegExp(\n String.raw`(\\b(?:mysql|mysqldump|mysqladmin|mysqlimport|mysqlsh|mariadb|mariadb-dump)(?=[ \\t])${SAME_COMMAND}[ \\t]-p)${NOT_MARK}(?=\\S)${SECRET_VALUE}`,\n 'g',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n // Only sshpass's own options may precede -p, so a wrapped `ssh -p 22` is never read as the password.\n {\n name: 'sshpass-password-arg',\n pattern: new RegExp(String.raw`(\\bsshpass(?:[ \\t]+-[^p\\s]\\S*)*[ \\t]+-p[ \\t]*)${NOT_MARK}(?=\\S)${SECRET_VALUE}`, 'g'),\n highConfidence: true,\n render: keepPrefix,\n },\n {\n name: 'mongo-password-arg',\n pattern: new RegExp(\n String.raw`(\\bmongo(?:sh|dump|restore|export|import|stat|top|files)?(?=[ \\t])${SAME_COMMAND}[ \\t]-p[ \\t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,\n 'g',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n {\n name: 'redis-cli-password-arg',\n pattern: new RegExp(\n String.raw`(\\bredis-cli(?=[ \\t])${SAME_COMMAND}[ \\t]-a[ \\t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,\n 'g',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n // Quoted credentials get their own branches: `curl -u \"user:pass word\"` holds a space, which\n // the unquoted branch would stop at, leaving the rest of the password in the text. Like\n // SECRET_VALUE, they step over escaped characters so `\"alice:pa\\\"ss\"` is not cut at `\\\"`. Inside\n // quotes the user name may hold a space too (`\"alice smith:pw\"`); only the quote ends it.\n {\n name: 'curl-user-password',\n pattern: new RegExp(\n String.raw`(${CURL_USER}\"(?:\\\\.|[^:\"\\\\\\r\\n])*:)${NOT_MARK}(?:\\\\.|[^\"\\\\\\r\\n])*|(${CURL_USER}'(?:\\\\.|[^:'\\\\\\r\\n])*:)${NOT_MARK}(?:\\\\.|[^'\\\\\\r\\n])*|(${CURL_USER}[^\\s:'\"]*:)${NOT_MARK}[^\\s'\"]+`,\n 'g',\n ),\n highConfidence: true,\n render: (groups) => `${groups.find((g) => g.length > 0) ?? ''}${MARK}`,\n },\n // Authorization: Bearer x / Basic x / token x. The scheme word is kept; it must not be mistaken for the value.\n {\n name: 'authorization-header',\n pattern: new RegExp(\n String.raw`(\\b(?:proxy-)?authorization[\"']?[ \\t]*[:=][ \\t]*[\"'\\x60]?(?:${AUTH_SCHEME}[ \\t]+)?)(?!${AUTH_SCHEME}[ \\t])${NOT_MARK}[^\\s'\"\\x60]+`,\n 'gi',\n ),\n highConfidence: false,\n render: keepPrefix,\n },\n // `--password secret`: a secret-named option whose value is the next argument. The flag must start a\n // command-line word, so prose like \"the `id`-token heuristic\" is not read as a `-token` option.\n {\n name: 'secret-option-arg',\n pattern: new RegExp(\n String.raw`(?<=^|[\\s'\"(])(-{1,2}${SECRET_KEY}[ \\t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,\n 'gi',\n ),\n highConfidence: false,\n render: keepPrefix,\n },\n // key/token/secret/password = \"value\" or : value, in code, JSON, env-file, shell or prose form.\n // The key start is an explicit non-identifier lookbehind, not `\\b`: `_` is a word character, so\n // `\\b` never fired inside DB_PASSWORD and the value leaked. Found live via `scan-shell`.\n {\n name: 'key-value-secret',\n pattern: new RegExp(\n String.raw`(?<![A-Za-z0-9_.-])(${SECRET_KEY})[\"']?[ \\t]*[:=](?![=>])[ \\t]*(?!${TYPE_WORD})${NOT_MARK}${SECRET_VALUE}`,\n 'gi',\n ),\n highConfidence: false,\n // Keep the key name so the redaction is legible (\"apiKey: [redacted]\").\n render: (groups) => `${groups[0]}: ${MARK}`,\n },\n];\n\nexport interface RedactResult {\n text: string;\n redactedCount: number;\n}\n\n/**\n * `all` runs every rule -- the right trade for prose, where a false positive\n * costs a mangled sentence. `high-confidence` runs only the shape rules, for\n * text that *is* code and must survive redaction intact.\n */\nexport type RedactProfile = 'all' | 'high-confidence';\n\nexport function redact(text: string, profile: RedactProfile = 'all'): RedactResult {\n let redactedCount = 0;\n let out = text;\n\n for (const rule of RULES) {\n if (profile === 'high-confidence' && !rule.highConfidence) continue;\n out = out.replace(rule.pattern, (...args: unknown[]) => {\n redactedCount += 1;\n // `replace` passes (match, ...groups, offset, wholeString): the groups end at the first\n // number. Slicing by type keeps a match offset from ever being mistaken for a group.\n const end = args.findIndex((arg, i) => i > 0 && typeof arg === 'number');\n const groups = args.slice(1, end).map((g) => (typeof g === 'string' ? g : ''));\n return rule.render ? rule.render(groups) : MARK;\n });\n }\n\n return { text: out, redactedCount };\n}\n","import { createHash } from 'node:crypto';\nimport type { NodeKind } from './types.js';\n\n/** NUL cannot appear in any of our key components, so it is a safe joiner. */\nconst KEY_SEP = '\\u0000';\n\nexport function sha256Hex(input: string): string {\n return createHash('sha256').update(input, 'utf8').digest('hex');\n}\n\n/**\n * Content-addressed node id.\n *\n * `naturalKey` must be whatever uniquely identifies the event at its source\n * (a commit sha, a `timestamp:command` pair, ...). Re-running `sync` then\n * re-derives the exact same id, which makes ingestion idempotent without\n * relying on a cursor being correct.\n */\nexport function makeNodeId(projectId: string, kind: NodeKind, naturalKey: string): string {\n return sha256Hex([projectId, kind, naturalKey].join(KEY_SEP)).slice(0, 24);\n}\n","import { redact } from '../conversation/redact.js';\nimport { sha256Hex } from '../core/ids.js';\n\n/**\n * One attempt a coding agent made: a command it ran, or a file it edited.\n *\n * Vendor-neutral on purpose. Nothing here names Claude Code, a hook event or\n * a tool_use id -- an adapter maps its own payload onto this shape, so a\n * second agent needs no change in the core. The raw command and error text\n * exist only in `RawAgentEvent`, in memory; `redactAgentEvent` is the only\n * way to get an `AgentEvent`, and what it returns is safe to persist.\n */\n\nexport type AgentEventKind = 'command' | 'edit';\n\n/** `interrupted` is a user abort, not a failure: it says nothing about the attempt. */\nexport type AgentOutcome = 'ok' | 'fail' | 'interrupted' | 'unknown';\n\nexport interface RawAgentEvent {\n agent: string;\n sessionId: string;\n /** The agent's own id for this action; makes the event idempotent across delivery paths. */\n eventId: string;\n ts: string;\n cwd: string | null;\n kind: AgentEventKind;\n /** Raw, for a command event. Redacted before it leaves this module. */\n command?: string;\n /** Repo-relative or absolute path, for an edit event. Never file content. */\n filePath?: string;\n outcome: AgentOutcome;\n exitCode: number | null;\n /** Raw first lines of the failure output. Redacted and truncated before persisting. */\n errorSignature?: string;\n durationMs: number | null;\n /** Set when a subagent produced the event. */\n agentId?: string;\n}\n\nexport interface AgentEvent extends Omit<RawAgentEvent, 'command' | 'errorSignature'> {\n command?: string;\n /** sha256 prefix of the RAW command: correlation matches on this, never on redacted text. */\n commandHash?: string;\n /**\n * sha256 prefix of the raw command reduced to the one execution that\n * identifies it -- see `canonicalizeCommand`. This is what `agent recall`\n * matches on: a live `cd \"<cwd>\" && ls && npm test; echo \"exit: $?\"` has to\n * find a historical bare `npm test` in the same project. Equal to\n * `commandHash` whenever the command is already a single execution, which is\n * every command recorded before this field existed.\n */\n execHash?: string;\n errorSignature?: string;\n}\n\n/**\n * Reduces a compound shell command to the one execution that identifies it.\n *\n * Claude Code rarely runs a command bare. Measured over the 69 task-execution\n * Bash calls in the Phase-5 eval transcripts, it wrapped the same `node\n * check.js` in fifteen distinct shapes: `cd \"<repo>\" && node check.js`,\n * `cd \"<repo>\" && node check.js; echo \"exit: $?\"`, `cd \"<repo>\" && ls -la &&\n * echo --- && node check.js`, and so on. An exact-text hash finds none of\n * them in a history that recorded the bare command.\n *\n * The rewrite is a semantic allowlist, never a string-stripper: the command is\n * split into `&&`/`;` segments (quote-aware), each is classified, and the\n * result is the single segment that is neither navigation nor observation.\n * Anything else -- two real commands, an env-var assignment, `sudo`, a\n * pipeline, a `cd` elsewhere, an unrecognised word -- leaves the command\n * untouched, because none of those can be proven not to have changed what\n * ran. A missed match costs a silent recall; a wrong one teaches the model a\n * false fact, so the asymmetry is deliberate.\n *\n * Dropping a *trailing* observation segment matters as much as a leading one:\n * 34 of those 69 calls end in `; echo \"exit: $?\"` or a case variant, which is\n * how the agent reads an exit status the tool call itself swallowed.\n */\n\n/** Read-only in every flag form they accept, given the metacharacter check below. */\nconst OBSERVATION_COMMANDS = new Set(['ls', 'pwd', 'echo', 'cat']);\n/** `git` is read-only only per subcommand: `branch -d` and `remote add` are not. */\nconst OBSERVATION_GIT_SUBCOMMANDS = new Set(['status', 'log', 'diff', 'show']);\n\n/**\n * Redirection, pipes, command substitution, subshells and background `&` all\n * mean a dropped segment could have had an effect. A bare `$` is fine and\n * common (`echo \"exit: $?\"`); only `$(` executes.\n */\nconst UNSAFE_SEGMENT = /[|&<>`(){}]|\\$\\(/;\n\ntype SegmentKind = 'navigation' | 'observation' | 'target';\n\n/** Splits on top-level `&&` and `;` only. Returns null on unbalanced quotes. */\nfunction splitSegments(command: string): string[] | null {\n const segments: string[] = [];\n let current = '';\n let quote: '\"' | \"'\" | null = null;\n\n for (let i = 0; i < command.length; i += 1) {\n const ch = command[i]!;\n if (quote) {\n current += ch;\n if (ch === quote) quote = null;\n continue;\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch;\n current += ch;\n continue;\n }\n if (ch === ';') {\n segments.push(current);\n current = '';\n continue;\n }\n if (ch === '&' && command[i + 1] === '&') {\n segments.push(current);\n current = '';\n i += 1;\n continue;\n }\n current += ch;\n }\n\n if (quote) return null;\n segments.push(current);\n return segments.map((s) => s.trim()).filter((s) => s.length > 0);\n}\n\n/** Quote-aware so a repository path containing spaces still parses as one argument. */\nconst CD_SEGMENT = /^cd\\s+(?:\"([^\"]*)\"|'([^']*)'|(\\S+))$/;\n\n/**\n * `/c/Users/x` (Git Bash) and `/mnt/c/Users/x` (WSL) name the same directory\n * as `C:\\Users\\x`, and the agent writes whichever spelling its shell handed\n * it -- in one measured Phase-5 run it `cd`-ed to the Git Bash form while the\n * hook reported the native one, and the two could not be related. Windows-only,\n * like `eval/ambient/paths.ts`: on a POSIX host `/c/tools` and `/mnt/c/data`\n * are ordinary directories.\n */\nconst DRIVE_SPELLING = /^\\/(?:mnt\\/)?([a-zA-Z])(?:\\/|$)/;\n/** A drive-letter path, and a UNC share, once separators read as `/`. */\nconst WINDOWS_DRIVE = /^[a-zA-Z]:(?:\\/|$)/;\nconst WINDOWS_UNC = /^\\/\\/[^/]+\\/[^/]+/;\n\n/**\n * One directory, in the form this comparison can decide on, with whether its\n * case can be folded. Only a Windows volume is folded -- Windows is\n * case-insensitive, and `cd /Repo` under a cwd of `/repo` is a different\n * directory on a case-sensitive filesystem, so folding it would strip a `cd`\n * that changed where the command ran and hand the run another directory's\n * history. Nothing here reads the filesystem: the path may be historical,\n * remote, or since deleted.\n */\nfunction directoryForCompare(path: string, platform: NodeJS.Platform): { text: string; foldsCase: boolean } {\n // A backslash is an ordinary character in a POSIX filename, so it is only a separator on Windows.\n if (platform !== 'win32') return { text: path.replace(/\\/+$/, ''), foldsCase: false };\n const slashed = path.replace(/\\\\/g, '/');\n const drive = DRIVE_SPELLING.exec(slashed);\n const native = drive ? `${drive[1]}:/${slashed.slice(drive[0].length)}` : slashed;\n return { text: native.replace(/\\/+$/, ''), foldsCase: WINDOWS_DRIVE.test(native) || WINDOWS_UNC.test(native) };\n}\n\n/**\n * `platform` is a parameter rather than read from the environment so both\n * rule sets are exercised wherever the suite runs. In the product it is the\n * host the hook fired on, which is the host that ran the command.\n */\nexport function sameDirectory(a: string, b: string, platform: NodeJS.Platform = process.platform): boolean {\n const x = directoryForCompare(a, platform);\n const y = directoryForCompare(b, platform);\n // Case is folded only when both sides are provably the same kind of Windows\n // volume; anything else compares exactly, which can only cost a recall.\n return x.foldsCase && y.foldsCase ? x.text.toLowerCase() === y.text.toLowerCase() : x.text === y.text;\n}\n\nfunction classify(segment: string, cwd: string | null, platform: NodeJS.Platform): SegmentKind {\n if (UNSAFE_SEGMENT.test(segment)) return 'target';\n\n const cd = CD_SEGMENT.exec(segment);\n if (cd) {\n // Navigation only when it names the directory the event already carries:\n // a `cd` anywhere else genuinely changes what the next segment runs against.\n if (!cwd) return 'target';\n const to = cd[1] ?? cd[2] ?? cd[3] ?? '';\n if (to === '.') return 'navigation';\n return sameDirectory(to, cwd, platform) ? 'navigation' : 'target';\n }\n\n const [head, next] = segment.split(/\\s+/);\n if (!head) return 'target';\n if (OBSERVATION_COMMANDS.has(head)) return 'observation';\n if (head === 'git' && next !== undefined && OBSERVATION_GIT_SUBCOMMANDS.has(next)) return 'observation';\n return 'target';\n}\n\nexport function canonicalizeCommand(command: string, cwd: string | null, platform: NodeJS.Platform = process.platform): string {\n const segments = splitSegments(command);\n if (!segments) return command;\n\n const targets = segments.filter((s) => classify(s, cwd, platform) === 'target');\n // Exactly one real execution, with everything around it proven inert. Zero\n // targets (a pure `ls && pwd`) has no execution to name; two or more cannot\n // be reduced to one without guessing which mattered.\n return targets.length === 1 ? targets[0]! : command;\n}\n\n/** Long enough to identify a failure, short enough that a stack trace never lands in the DB. */\nexport const MAX_ERROR_SIGNATURE_CHARS = 200;\n\nexport function agentEventNaturalKey(event: Pick<AgentEvent, 'agent' | 'sessionId' | 'eventId'>): string {\n return `agent:${event.agent}:${event.sessionId}:${event.eventId}`;\n}\n\n/** The one gate between raw agent output and anything durable. */\nexport function redactAgentEvent(raw: RawAgentEvent): AgentEvent {\n const { command, errorSignature, ...rest } = raw;\n return {\n ...rest,\n ...(command === undefined\n ? {}\n : {\n command: redact(command).text,\n commandHash: sha256Hex(command).slice(0, 12),\n execHash: sha256Hex(canonicalizeCommand(command, raw.cwd)).slice(0, 12),\n }),\n ...(errorSignature === undefined ? {} : { errorSignature: redact(errorSignature).text.slice(0, MAX_ERROR_SIGNATURE_CHARS) }),\n };\n}\n","import type { DropFamily, DropReason } from '../../agent/capture-health.js';\nimport { type AgentEvent, type AgentOutcome, type RawAgentEvent, redactAgentEvent } from '../../agent/event.js';\n\n/**\n * Maps one Claude Code hook payload onto an `AgentEvent`.\n *\n * Every field below was read off a live probe against Claude Code 2.1.226\n * (see tests/agent-payload.test.ts for the captured payloads), not from the\n * docs -- doc summaries claimed a `tool_response.exit_code` that does not\n * exist. What the probe established:\n *\n * - a Bash call that exits non-zero fires `PostToolUseFailure` ONLY, and its\n * `error` is the text \"Exit code N\\n<stderr>\"; there is no exit-code field\n * - a Bash call that succeeds fires `PostToolUse`, carrying `tool_response`\n * - both carry `tool_use_id`, `cwd`, `session_id` and `duration_ms`\n *\n * Anything that does not match is dropped, never guessed at.\n */\n\nexport const AGENT = 'claude-code';\n\nconst EDIT_TOOLS: ReadonlySet<string> = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);\nconst EXIT_CODE = /^Exit code (\\d+)/;\n\ninterface HookPayload {\n hook_event_name?: unknown;\n session_id?: unknown;\n cwd?: unknown;\n tool_name?: unknown;\n tool_use_id?: unknown;\n tool_input?: { command?: unknown; file_path?: unknown; notebook_path?: unknown };\n tool_response?: { stdout?: unknown };\n error?: unknown;\n is_interrupt?: unknown;\n duration_ms?: unknown;\n agent_id?: unknown;\n}\n\nconst str = (v: unknown): string | undefined => (typeof v === 'string' && v.length > 0 ? v : undefined);\nconst num = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null);\n\n/**\n * Measured in the Phase-5 eval: 15 of 17 real Bash calls hid the exit status\n * of the command they actually cared about behind one of these -- `cmd;\n * echo \"EXIT:$?\"`, `cmd; echo \"exit: $?\"`, `cmd; echo \"exit=$?\"` -- so the\n * *outer* shell invocation Claude Code's hook sees is `echo`'s own exit code\n * (0), and `PostToolUseFailure` never fires even though `cmd` failed.\n *\n * `tool_response.stdout` is the only place that status can still be found,\n * and only in this literal form: only the LAST non-blank line is examined,\n * and only if it is *exactly* `exit[:=]<digits>` (case-insensitive) with\n * nothing else on it. This is a closed set of known echo spellings, not\n * free-text parsing -- a program whose own output happens to mention \"exit\n * code\" (e.g. \"Process finished with exit code 1\") does not match, because\n * that text is not alone on its line and uses neither `:` nor `=`. A\n * malformed or missing echo (`EXIT:`, `EXIT:abc`, no such line at all)\n * yields `null`: no evidence, not a guessed zero and not a guessed failure.\n *\n * Output alone is never evidence: a program can print `exit: 1` itself. The\n * command must end with that echo of `$?`, and nothing before it may make\n * `$?` another command's status -- a pipe (the last stage's), `||` (the\n * fallback's), or another `;` or line (whatever ran last). `cd x && cmd` is\n * fine: `$?` is then cmd's status, or cd's own failure.\n */\nconst EXIT_ECHO = /^exit\\s*[:=]\\s*(\\d+)\\s*$/i;\nconst EXIT_WRAPPER = /^([\\s\\S]*?);\\s*echo\\s+(?:\"exit\\s*[:=]\\s*\\$\\?\"|exit\\s*[:=]\\s*\\$\\?)\\s*$/i;\n\nfunction echoesOwnExitStatus(command: string): boolean {\n const body = EXIT_WRAPPER.exec(command.trim())?.[1];\n return body !== undefined && body.trim().length > 0 && !/[|;\\r\\n]/.test(body);\n}\n\n/**\n * `head`, `tail` and `tee`, as the last stage of a pipeline: three utilities\n * that pass their input through (or duplicate it) and exit 0 regardless of\n * whether what fed them did. `cmd 2>&1 | head -100` is the shape the Phase-5\n * eval actually observed (12 of 69 calls): the outer command Claude Code's\n * hook sees is the pipeline's, so it reports PostToolUse/exit 0 even when\n * `cmd` itself failed loudly.\n *\n * Deliberately just these three, matched only as the pipeline's final stage.\n * A pipe into anything else (`| grep`, `| jq`, `| sort`, a second real\n * program) is left alone: nothing about an arbitrary filter's own exit code\n * is known here, and guessing would be exactly the overclaim this module\n * exists to avoid. See `recallUncertain` for how this is surfaced without\n * ever being worded as a proven failure.\n *\n * The name must not be followed by another filename character, not just a\n * `\\b` word boundary: `\\b` alone also fires before `-` or `.`, which matched\n * a script called `head-check.sh` or `tail.sh` as if it were the real\n * `head`/`tail`. Found by testing the regex against exactly that shape.\n */\nconst OPAQUE_FILTER_TAIL = /\\|\\s*(?:head|tail|tee)(?![A-Za-z0-9._-])[^|]*$/i;\n\nfunction pipesIntoOpaqueFilter(command: string): boolean {\n return OPAQUE_FILTER_TAIL.test(command.trim());\n}\n\n/**\n * What actually happened to a Bash call, as one decision instead of two\n * `outcome`/`exitCode` fields computed by separate, parallel ternaries that\n * have to be kept in sync by hand. Checked in order of how much the evidence\n * can be trusted:\n *\n * 1. A real `PostToolUseFailure` already carries its own exit code in `error`\n * -- nothing here second-guesses it.\n * 2. An explicit `; echo \"exit:$?\"` the command itself printed is the next\n * most trustworthy thing, and can flip a hook-reported \"ok\" to a \"fail\".\n * 3. With neither, a pipeline into `head`/`tail`/`tee` means the hook's own\n * \"ok\" is unproven, not disproven: `unknown`, never a guessed pass or fail.\n * 4. Otherwise the hook's own verdict stands.\n */\nfunction classifyBashResult(\n hookOutcome: AgentOutcome,\n failed: boolean,\n error: { exitCode: number | null; signature: string } | null,\n recovered: number | null,\n command: string,\n): { outcome: AgentOutcome; exitCode: number | null } {\n if (failed) return { outcome: hookOutcome, exitCode: error?.exitCode ?? null };\n if (recovered !== null) return { outcome: recovered === 0 ? 'ok' : 'fail', exitCode: recovered };\n if (pipesIntoOpaqueFilter(command)) return { outcome: 'unknown', exitCode: null };\n return { outcome: hookOutcome, exitCode: 0 };\n}\n\nexport function recoverExitStatusFromOutput(command: string, stdout: string | undefined): number | null {\n if (!stdout || !echoesOwnExitStatus(command)) return null;\n const lines = stdout.split(/\\r?\\n/).map((l) => l.trim());\n let last = '';\n for (let i = lines.length - 1; i >= 0; i -= 1) {\n if (lines[i]) {\n last = lines[i]!;\n break;\n }\n }\n const match = EXIT_ECHO.exec(last);\n return match ? Number(match[1]) : null;\n}\n\n/** Splits \"Exit code 2\\nls: cannot access ...\" into its code and the rest. */\nfunction parseError(error: string): { exitCode: number | null; signature: string } {\n const [first = '', ...rest] = error.split(/\\r?\\n/);\n const match = EXIT_CODE.exec(first);\n if (!match) return { exitCode: null, signature: error.trim() };\n return { exitCode: Number(match[1]), signature: rest.join(' ').trim() };\n}\n\n/**\n * `now` is passed in because no hook payload carries a timestamp -- the event\n * is stamped when it is received, which is within milliseconds of the action.\n */\nexport interface SessionStartPayload {\n sessionId: string;\n cwd: string;\n /** startup | resume | clear | compact, per the live probe. */\n source: string | null;\n}\n\n/** SessionStart carries no tool fields, so it gets its own tiny parser rather than bending the event one. */\nexport function parseSessionStart(rawJson: string): SessionStartPayload | null {\n let payload: unknown;\n try {\n payload = JSON.parse(rawJson.trim());\n } catch {\n return null;\n }\n if (typeof payload !== 'object' || payload === null) return null;\n const p = payload as HookPayload & { source?: unknown };\n if (str(p.hook_event_name) !== 'SessionStart') return null;\n\n const sessionId = str(p.session_id);\n const cwd = str(p.cwd);\n if (!sessionId || !cwd) return null;\n return { sessionId, cwd, source: str(p.source) ?? null };\n}\n\n/**\n * Why an event was thrown away, so `agent status` can tell a quiet week from\n * a payload shape this adapter no longer understands. The reason is a code\n * from the core's closed set; no payload text ever travels with it.\n */\nexport type ParseOutcome = { ok: true; event: AgentEvent; family: DropFamily } | { ok: false; reason: DropReason; family: DropFamily };\n\n/** Vendor event name to a normalized family code; the vendor's own string is never stored. */\nconst FAMILY_BY_EVENT: Record<string, DropFamily> = {\n PostToolUse: 'post-tool-use',\n PostToolUseFailure: 'post-tool-use-failure',\n SessionStart: 'session-start',\n};\n\nconst familyOf = (event: string | undefined): DropFamily => (event ? (FAMILY_BY_EVENT[event] ?? 'other') : 'other');\n\n/** Thin wrapper: the common callers only care whether there is an event. */\nexport function parseHookPayload(rawJson: string, now: string): AgentEvent | null {\n const outcome = parseHookPayloadDetailed(rawJson, now);\n return outcome.ok ? outcome.event : null;\n}\n\nexport function parseHookPayloadDetailed(rawJson: string, now: string): ParseOutcome {\n let payload: unknown;\n try {\n // A JSON.parse error message quotes its input, i.e. the raw command; never let it escape.\n payload = JSON.parse(rawJson.trim());\n } catch {\n // Nothing about the input is known here, and a parser's own message would quote it.\n return { ok: false, reason: 'unparsable-json', family: 'other' };\n }\n if (typeof payload !== 'object' || payload === null) return { ok: false, reason: 'unparsable-json', family: 'other' };\n const p = payload as HookPayload;\n\n const event = str(p.hook_event_name);\n const family = familyOf(event);\n const sessionId = str(p.session_id);\n const eventId = str(p.tool_use_id);\n const toolName = str(p.tool_name);\n if (event !== 'PostToolUse' && event !== 'PostToolUseFailure') return { ok: false, reason: 'unsupported-event', family };\n if (!sessionId || !eventId || !toolName) return { ok: false, reason: 'missing-fields', family };\n\n const failed = event === 'PostToolUseFailure';\n const interrupted = failed && p.is_interrupt === true;\n const outcome: AgentOutcome = interrupted ? 'interrupted' : failed ? 'fail' : 'ok';\n\n const base = {\n agent: AGENT,\n sessionId,\n eventId,\n ts: now,\n cwd: str(p.cwd) ?? null,\n outcome,\n durationMs: num(p.duration_ms),\n ...(str(p.agent_id) ? { agentId: str(p.agent_id) as string } : {}),\n };\n\n if (toolName === 'Bash') {\n const command = str(p.tool_input?.command);\n if (!command) return { ok: false, reason: 'missing-fields', family };\n const error = failed ? parseError(str(p.error) ?? '') : null;\n // Only checked on the hook's own success path: a failure already carries a\n // real exit code from `error`, and second-guessing a genuine failure would\n // be guessing in the more dangerous direction.\n const recovered = failed ? null : recoverExitStatusFromOutput(command, str(p.tool_response?.stdout));\n const draft: RawAgentEvent = {\n ...base,\n kind: 'command',\n command,\n ...classifyBashResult(outcome, failed, error, recovered, command),\n ...(error?.signature ? { errorSignature: error.signature } : {}),\n };\n return { ok: true, event: redactAgentEvent(draft), family };\n }\n\n if (EDIT_TOOLS.has(toolName)) {\n // Only the path: an edit's payload also holds the file's old and new content, which is never recorded.\n const filePath = str(p.tool_input?.file_path) ?? str(p.tool_input?.notebook_path);\n if (!filePath) return { ok: false, reason: 'missing-fields', family };\n return { ok: true, event: redactAgentEvent({ ...base, kind: 'edit', filePath, exitCode: null }), family };\n }\n\n return { ok: false, reason: 'unsupported-tool', family };\n}\n","/**\n * Entry point Claude Code's PostToolUse / PostToolUseFailure hooks run:\n *\n * <hook payload on stdin> | node agent-hook.js [--log <path>]\n *\n * Its own small bundle, like the shell recorder, so it starts fast and loads\n * no native dependency. Silent by design: nothing on stdout or stderr, no\n * temp file, so no failure path can echo the payload it received. Any event\n * it cannot handle is dropped. It never exits 2, which is the only code that\n * would block the agent.\n */\nimport { captureDropStatePath as dropStatePath, recordCaptureDrop } from '../../agent/capture-health.js';\nimport { appendAgentEvent } from '../../agent/record.js';\nimport { agentEventLogPath } from '../../agent/paths.js';\nimport { parseHookPayloadDetailed } from './payload.js';\n\nconst MAX_EVENT_BYTES = 1_000_000;\n\nconst drop = (): never => process.exit(1);\nprocess.on('uncaughtException', drop);\nprocess.on('unhandledRejection', drop);\n\nfunction arg(name: string): string | undefined {\n const i = process.argv.indexOf(name);\n return i === -1 ? undefined : process.argv[i + 1];\n}\n\nasync function main(): Promise<void> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of process.stdin) {\n size += (chunk as Buffer).length;\n if (size > MAX_EVENT_BYTES) {\n // Never parsed, so which hook sent it is unknown; an edit carrying a large file can land here.\n recordCaptureDrop('payload-too-large', 'other', dropStatePath());\n drop();\n }\n chunks.push(chunk as Buffer);\n }\n // Windows PowerShell can prepend a BOM to a redirected stdin; the parser trims it.\n const outcome = parseHookPayloadDetailed(Buffer.concat(chunks).toString('utf8'), new Date().toISOString());\n if (!outcome.ok) {\n // A dropped event leaves no other trace, so record why -- two normalized\n // codes and a timestamp, never the payload or a parser's message.\n recordCaptureDrop(outcome.reason, outcome.family, dropStatePath());\n return drop();\n }\n try {\n await appendAgentEvent(outcome.event, arg('--log') ?? agentEventLogPath());\n } catch {\n recordCaptureDrop('write-failed', outcome.family, dropStatePath());\n return drop();\n }\n process.exit(0);\n}\n\nmain().catch(drop);\n"],"mappings":";;;AAAA,SAAS,WAAW,cAAc,qBAAqB;AACvD,SAAS,WAAAA,UAAS,QAAAC,aAAY;;;ACD9B,SAAS,eAAe;AACxB,SAAS,YAAY;AAad,SAAS,qBAA6B;AAC3C,SAAO,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,GAAG,WAAW;AACjE;;;AChBA,SAAS,QAAAC,aAAY;AAQd,SAAS,oBAA4B;AAC1C,SAAOC,MAAK,mBAAmB,GAAG,oBAAoB;AACxD;;;ACVA,SAAS,YAAY,OAAO,gBAAgB;AAC5C,SAAS,eAAe;AAcxB,eAAsB,iBAAiB,OAAmB,SAAgC;AACxF,QAAM,MAAM,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,QAAM,WAAW,SAAS,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,GAAM,EAAE,UAAU,QAAQ,MAAM,IAAM,CAAC;AAC3F;;;AHQO,IAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQO,IAAM,gBAAgB,CAAC,iBAAiB,yBAAyB,iBAAiB,OAAO;AAIzF,IAAM,oBAAoB,KAAK,KAAK,KAAK;AAqBzC,SAAS,uBAA+B;AAC7C,SAAOC,MAAK,mBAAmB,GAAG,0BAA0B;AAC9D;AAEA,IAAM,eAAe,CAAC,UAAwC,aAAa,SAAS,KAAmB;AACvG,IAAM,eAAe,CAAC,UAAwC,cAAc,SAAS,KAAmB;AAWjG,SAAS,kBACd,QACA,SAAqB,SACrB,OAAO,qBAAqB,GAC5B,MAAM,oBAAI,KAAK,GACT;AACN,MAAI;AAGF,UAAM,aAAyB,aAAa,MAAM,IAAI,SAAS;AAC/D,UAAM,aAAyB,aAAa,MAAM,IAAI,SAAS;AAC/D,UAAM,WAAW,cAAc,IAAI;AACnC,UAAM,QAAQ,OAAO,SAAS,UAAU,YAAY,OAAO,SAAS,SAAS,KAAK,IAAI,SAAS,QAAQ;AAEvG,cAAUC,SAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C;AAAA,MACE;AAAA,MACA,KAAK,UAAU,EAAE,YAAY,IAAI,YAAY,GAAG,gBAAgB,YAAY,gBAAgB,YAAY,OAAO,QAAQ,EAAE,CAAC;AAAA,MAC1H,EAAE,UAAU,QAAQ,MAAM,IAAM;AAAA,IAClC;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,cAAc,MAAyB;AAC9C,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAC7D,WAAO,OAAO,WAAW,YAAY,WAAW,OAAQ,SAAuB,CAAC;AAAA,EAClF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;AItFO,IAAM,iBAAiB;AAC9B,IAAM,OAAO;AAEb,IAAM,WAAW,OAAO;AACxB,IAAM,aAAa,CAAC,WAAsC,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI;AAI7E,IAAM,iBAAiB,OAAO;AAG9B,IAAM,aAAa,OAAO,+BAA+B,cAAc;AAEvE,IAAM,YAAY,OAAO;AAKzB,IAAM,eAAe,OAAO;AAE5B,IAAM,eAAe,OAAO;AAE5B,IAAM,uBAAuB,OAAO;AACpC,IAAM,cAAc,OAAO;AAE3B,IAAM,YAAY,OAAO,qBAAqB,YAAY;AAE1D,IAAM,QAAgB;AAAA,EACpB;AAAA,IACE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,gBAAgB;AAAA,EAClB;AAAA,EACA,EAAE,MAAM,kBAAkB,SAAS,yBAAyB,gBAAgB,KAAK;AAAA,EACjF,EAAE,MAAM,gBAAgB,SAAS,mCAAmC,gBAAgB,KAAK;AAAA,EACzF,EAAE,MAAM,eAAe,SAAS,qCAAqC,gBAAgB,KAAK;AAAA,EAC1F;AAAA,IACE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,gBAAgB;AAAA,EAClB;AAAA;AAAA,EAEA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,iDAAiD,QAAQ;AAAA,MAChE;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,4DAA4D,oBAAoB,GAAG,QAAQ,GAAG,YAAY;AAAA,MACjH;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA;AAAA,EAEA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI,OAAO,OAAO,sBAAsB,QAAQ,oDAAoD,IAAI;AAAA,IACjH,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,yFAAyF,YAAY,WAAW,QAAQ,SAAS,YAAY;AAAA,MACpJ;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA;AAAA,EAEA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI,OAAO,OAAO,oDAAoD,QAAQ,SAAS,YAAY,IAAI,GAAG;AAAA,IACnH,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,wEAAwE,YAAY,iBAAiB,oBAAoB,GAAG,QAAQ,GAAG,YAAY;AAAA,MAC1J;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,2BAA2B,YAAY,iBAAiB,oBAAoB,GAAG,QAAQ,GAAG,YAAY;AAAA,MAC7G;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,OAAO,SAAS,0BAA0B,QAAQ,wBAAwB,SAAS,0BAA0B,QAAQ,wBAAwB,SAAS,cAAc,QAAQ;AAAA,MACnL;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ,CAAC,WAAW,GAAG,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,EAAE,GAAG,IAAI;AAAA,EACtE;AAAA;AAAA,EAEA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,kEAAkE,WAAW,eAAe,WAAW,SAAS,QAAQ;AAAA,MAC/H;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,2BAA2B,UAAU,UAAU,oBAAoB,GAAG,QAAQ,GAAG,YAAY;AAAA,MACpG;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,IAAI;AAAA,MACX,OAAO,0BAA0B,UAAU,oCAAoC,SAAS,IAAI,QAAQ,GAAG,YAAY;AAAA,MACnH;AAAA,IACF;AAAA,IACA,gBAAgB;AAAA;AAAA,IAEhB,QAAQ,CAAC,WAAW,GAAG,OAAO,CAAC,CAAC,KAAK,IAAI;AAAA,EAC3C;AACF;AAcO,SAAS,OAAO,MAAc,UAAyB,OAAqB;AACjF,MAAI,gBAAgB;AACpB,MAAI,MAAM;AAEV,aAAW,QAAQ,OAAO;AACxB,QAAI,YAAY,qBAAqB,CAAC,KAAK,eAAgB;AAC3D,UAAM,IAAI,QAAQ,KAAK,SAAS,IAAI,SAAoB;AACtD,uBAAiB;AAGjB,YAAM,MAAM,KAAK,UAAU,CAACC,MAAK,MAAM,IAAI,KAAK,OAAOA,SAAQ,QAAQ;AACvE,YAAM,SAAS,KAAK,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,MAAO,OAAO,MAAM,WAAW,IAAI,EAAG;AAC7E,aAAO,KAAK,SAAS,KAAK,OAAO,MAAM,IAAI;AAAA,IAC7C,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,MAAM,KAAK,cAAc;AACpC;;;ACpNA,SAAS,kBAAkB;AAMpB,SAAS,UAAU,OAAuB;AAC/C,SAAO,WAAW,QAAQ,EAAE,OAAO,OAAO,MAAM,EAAE,OAAO,KAAK;AAChE;;;ACwEA,IAAM,uBAAuB,oBAAI,IAAI,CAAC,MAAM,OAAO,QAAQ,KAAK,CAAC;AAEjE,IAAM,8BAA8B,oBAAI,IAAI,CAAC,UAAU,OAAO,QAAQ,MAAM,CAAC;AAO7E,IAAM,iBAAiB;AAKvB,SAAS,cAAc,SAAkC;AACvD,QAAM,WAAqB,CAAC;AAC5B,MAAI,UAAU;AACd,MAAI,QAA0B;AAE9B,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;AAC1C,UAAM,KAAK,QAAQ,CAAC;AACpB,QAAI,OAAO;AACT,iBAAW;AACX,UAAI,OAAO,MAAO,SAAQ;AAC1B;AAAA,IACF;AACA,QAAI,OAAO,OAAO,OAAO,KAAK;AAC5B,cAAQ;AACR,iBAAW;AACX;AAAA,IACF;AACA,QAAI,OAAO,KAAK;AACd,eAAS,KAAK,OAAO;AACrB,gBAAU;AACV;AAAA,IACF;AACA,QAAI,OAAO,OAAO,QAAQ,IAAI,CAAC,MAAM,KAAK;AACxC,eAAS,KAAK,OAAO;AACrB,gBAAU;AACV,WAAK;AACL;AAAA,IACF;AACA,eAAW;AAAA,EACb;AAEA,MAAI,MAAO,QAAO;AAClB,WAAS,KAAK,OAAO;AACrB,SAAO,SAAS,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AACjE;AAGA,IAAM,aAAa;AAUnB,IAAM,iBAAiB;AAEvB,IAAM,gBAAgB;AACtB,IAAM,cAAc;AAWpB,SAAS,oBAAoB,MAAc,UAAiE;AAE1G,MAAI,aAAa,QAAS,QAAO,EAAE,MAAM,KAAK,QAAQ,QAAQ,EAAE,GAAG,WAAW,MAAM;AACpF,QAAM,UAAU,KAAK,QAAQ,OAAO,GAAG;AACvC,QAAM,QAAQ,eAAe,KAAK,OAAO;AACzC,QAAM,SAAS,QAAQ,GAAG,MAAM,CAAC,CAAC,KAAK,QAAQ,MAAM,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK;AAC1E,SAAO,EAAE,MAAM,OAAO,QAAQ,QAAQ,EAAE,GAAG,WAAW,cAAc,KAAK,MAAM,KAAK,YAAY,KAAK,MAAM,EAAE;AAC/G;AAOO,SAAS,cAAc,GAAW,GAAW,WAA4B,QAAQ,UAAmB;AACzG,QAAM,IAAI,oBAAoB,GAAG,QAAQ;AACzC,QAAM,IAAI,oBAAoB,GAAG,QAAQ;AAGzC,SAAO,EAAE,aAAa,EAAE,YAAY,EAAE,KAAK,YAAY,MAAM,EAAE,KAAK,YAAY,IAAI,EAAE,SAAS,EAAE;AACnG;AAEA,SAAS,SAAS,SAAiB,KAAoB,UAAwC;AAC7F,MAAI,eAAe,KAAK,OAAO,EAAG,QAAO;AAEzC,QAAM,KAAK,WAAW,KAAK,OAAO;AAClC,MAAI,IAAI;AAGN,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,KAAK;AACtC,QAAI,OAAO,IAAK,QAAO;AACvB,WAAO,cAAc,IAAI,KAAK,QAAQ,IAAI,eAAe;AAAA,EAC3D;AAEA,QAAM,CAAC,MAAM,IAAI,IAAI,QAAQ,MAAM,KAAK;AACxC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,qBAAqB,IAAI,IAAI,EAAG,QAAO;AAC3C,MAAI,SAAS,SAAS,SAAS,UAAa,4BAA4B,IAAI,IAAI,EAAG,QAAO;AAC1F,SAAO;AACT;AAEO,SAAS,oBAAoB,SAAiB,KAAoB,WAA4B,QAAQ,UAAkB;AAC7H,QAAM,WAAW,cAAc,OAAO;AACtC,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,UAAU,SAAS,OAAO,CAAC,MAAM,SAAS,GAAG,KAAK,QAAQ,MAAM,QAAQ;AAI9E,SAAO,QAAQ,WAAW,IAAI,QAAQ,CAAC,IAAK;AAC9C;AAGO,IAAM,4BAA4B;AAOlC,SAAS,iBAAiB,KAAgC;AAC/D,QAAM,EAAE,SAAS,gBAAgB,GAAG,KAAK,IAAI;AAC7C,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAI,YAAY,SACZ,CAAC,IACD;AAAA,MACE,SAAS,OAAO,OAAO,EAAE;AAAA,MACzB,aAAa,UAAU,OAAO,EAAE,MAAM,GAAG,EAAE;AAAA,MAC3C,UAAU,UAAU,oBAAoB,SAAS,IAAI,GAAG,CAAC,EAAE,MAAM,GAAG,EAAE;AAAA,IACxE;AAAA,IACJ,GAAI,mBAAmB,SAAY,CAAC,IAAI,EAAE,gBAAgB,OAAO,cAAc,EAAE,KAAK,MAAM,GAAG,yBAAyB,EAAE;AAAA,EAC5H;AACF;;;AClNO,IAAM,QAAQ;AAErB,IAAM,aAAkC,oBAAI,IAAI,CAAC,QAAQ,SAAS,aAAa,cAAc,CAAC;AAC9F,IAAM,YAAY;AAgBlB,IAAM,MAAM,CAAC,MAAoC,OAAO,MAAM,YAAY,EAAE,SAAS,IAAI,IAAI;AAC7F,IAAM,MAAM,CAAC,MAA+B,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI;AAyB9F,IAAM,YAAY;AAClB,IAAM,eAAe;AAErB,SAAS,oBAAoB,SAA0B;AACrD,QAAM,OAAO,aAAa,KAAK,QAAQ,KAAK,CAAC,IAAI,CAAC;AAClD,SAAO,SAAS,UAAa,KAAK,KAAK,EAAE,SAAS,KAAK,CAAC,WAAW,KAAK,IAAI;AAC9E;AAsBA,IAAM,qBAAqB;AAE3B,SAAS,sBAAsB,SAA0B;AACvD,SAAO,mBAAmB,KAAK,QAAQ,KAAK,CAAC;AAC/C;AAgBA,SAAS,mBACP,aACA,QACA,OACA,WACA,SACoD;AACpD,MAAI,OAAQ,QAAO,EAAE,SAAS,aAAa,UAAU,OAAO,YAAY,KAAK;AAC7E,MAAI,cAAc,KAAM,QAAO,EAAE,SAAS,cAAc,IAAI,OAAO,QAAQ,UAAU,UAAU;AAC/F,MAAI,sBAAsB,OAAO,EAAG,QAAO,EAAE,SAAS,WAAW,UAAU,KAAK;AAChF,SAAO,EAAE,SAAS,aAAa,UAAU,EAAE;AAC7C;AAEO,SAAS,4BAA4B,SAAiB,QAA2C;AACtG,MAAI,CAAC,UAAU,CAAC,oBAAoB,OAAO,EAAG,QAAO;AACrD,QAAM,QAAQ,OAAO,MAAM,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACvD,MAAI,OAAO;AACX,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAC7C,QAAI,MAAM,CAAC,GAAG;AACZ,aAAO,MAAM,CAAC;AACd;AAAA,IACF;AAAA,EACF;AACA,QAAM,QAAQ,UAAU,KAAK,IAAI;AACjC,SAAO,QAAQ,OAAO,MAAM,CAAC,CAAC,IAAI;AACpC;AAGA,SAAS,WAAW,OAA+D;AACjF,QAAM,CAAC,QAAQ,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,OAAO;AACjD,QAAM,QAAQ,UAAU,KAAK,KAAK;AAClC,MAAI,CAAC,MAAO,QAAO,EAAE,UAAU,MAAM,WAAW,MAAM,KAAK,EAAE;AAC7D,SAAO,EAAE,UAAU,OAAO,MAAM,CAAC,CAAC,GAAG,WAAW,KAAK,KAAK,GAAG,EAAE,KAAK,EAAE;AACxE;AAuCA,IAAM,kBAA8C;AAAA,EAClD,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,cAAc;AAChB;AAEA,IAAM,WAAW,CAAC,UAA2C,QAAS,gBAAgB,KAAK,KAAK,UAAW;AAQpG,SAAS,yBAAyB,SAAiB,KAA2B;AACnF,MAAI;AACJ,MAAI;AAEF,cAAU,KAAK,MAAM,QAAQ,KAAK,CAAC;AAAA,EACrC,QAAQ;AAEN,WAAO,EAAE,IAAI,OAAO,QAAQ,mBAAmB,QAAQ,QAAQ;AAAA,EACjE;AACA,MAAI,OAAO,YAAY,YAAY,YAAY,KAAM,QAAO,EAAE,IAAI,OAAO,QAAQ,mBAAmB,QAAQ,QAAQ;AACpH,QAAM,IAAI;AAEV,QAAM,QAAQ,IAAI,EAAE,eAAe;AACnC,QAAM,SAAS,SAAS,KAAK;AAC7B,QAAM,YAAY,IAAI,EAAE,UAAU;AAClC,QAAM,UAAU,IAAI,EAAE,WAAW;AACjC,QAAM,WAAW,IAAI,EAAE,SAAS;AAChC,MAAI,UAAU,iBAAiB,UAAU,qBAAsB,QAAO,EAAE,IAAI,OAAO,QAAQ,qBAAqB,OAAO;AACvH,MAAI,CAAC,aAAa,CAAC,WAAW,CAAC,SAAU,QAAO,EAAE,IAAI,OAAO,QAAQ,kBAAkB,OAAO;AAE9F,QAAM,SAAS,UAAU;AACzB,QAAM,cAAc,UAAU,EAAE,iBAAiB;AACjD,QAAM,UAAwB,cAAc,gBAAgB,SAAS,SAAS;AAE9E,QAAM,OAAO;AAAA,IACX,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,IAAI;AAAA,IACJ,KAAK,IAAI,EAAE,GAAG,KAAK;AAAA,IACnB;AAAA,IACA,YAAY,IAAI,EAAE,WAAW;AAAA,IAC7B,GAAI,IAAI,EAAE,QAAQ,IAAI,EAAE,SAAS,IAAI,EAAE,QAAQ,EAAY,IAAI,CAAC;AAAA,EAClE;AAEA,MAAI,aAAa,QAAQ;AACvB,UAAM,UAAU,IAAI,EAAE,YAAY,OAAO;AACzC,QAAI,CAAC,QAAS,QAAO,EAAE,IAAI,OAAO,QAAQ,kBAAkB,OAAO;AACnE,UAAM,QAAQ,SAAS,WAAW,IAAI,EAAE,KAAK,KAAK,EAAE,IAAI;AAIxD,UAAM,YAAY,SAAS,OAAO,4BAA4B,SAAS,IAAI,EAAE,eAAe,MAAM,CAAC;AACnG,UAAM,QAAuB;AAAA,MAC3B,GAAG;AAAA,MACH,MAAM;AAAA,MACN;AAAA,MACA,GAAG,mBAAmB,SAAS,QAAQ,OAAO,WAAW,OAAO;AAAA,MAChE,GAAI,OAAO,YAAY,EAAE,gBAAgB,MAAM,UAAU,IAAI,CAAC;AAAA,IAChE;AACA,WAAO,EAAE,IAAI,MAAM,OAAO,iBAAiB,KAAK,GAAG,OAAO;AAAA,EAC5D;AAEA,MAAI,WAAW,IAAI,QAAQ,GAAG;AAE5B,UAAM,WAAW,IAAI,EAAE,YAAY,SAAS,KAAK,IAAI,EAAE,YAAY,aAAa;AAChF,QAAI,CAAC,SAAU,QAAO,EAAE,IAAI,OAAO,QAAQ,kBAAkB,OAAO;AACpE,WAAO,EAAE,IAAI,MAAM,OAAO,iBAAiB,EAAE,GAAG,MAAM,MAAM,QAAQ,UAAU,UAAU,KAAK,CAAC,GAAG,OAAO;AAAA,EAC1G;AAEA,SAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB,OAAO;AACzD;;;ACnPA,IAAM,kBAAkB;AAExB,IAAM,OAAO,MAAa,QAAQ,KAAK,CAAC;AACxC,QAAQ,GAAG,qBAAqB,IAAI;AACpC,QAAQ,GAAG,sBAAsB,IAAI;AAErC,SAAS,IAAI,MAAkC;AAC7C,QAAM,IAAI,QAAQ,KAAK,QAAQ,IAAI;AACnC,SAAO,MAAM,KAAK,SAAY,QAAQ,KAAK,IAAI,CAAC;AAClD;AAEA,eAAe,OAAsB;AACnC,QAAM,SAAmB,CAAC;AAC1B,MAAI,OAAO;AACX,mBAAiB,SAAS,QAAQ,OAAO;AACvC,YAAS,MAAiB;AAC1B,QAAI,OAAO,iBAAiB;AAE1B,wBAAkB,qBAAqB,SAAS,qBAAc,CAAC;AAC/D,WAAK;AAAA,IACP;AACA,WAAO,KAAK,KAAe;AAAA,EAC7B;AAEA,QAAM,UAAU,yBAAyB,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,IAAG,oBAAI,KAAK,GAAE,YAAY,CAAC;AACzG,MAAI,CAAC,QAAQ,IAAI;AAGf,sBAAkB,QAAQ,QAAQ,QAAQ,QAAQ,qBAAc,CAAC;AACjE,WAAO,KAAK;AAAA,EACd;AACA,MAAI;AACF,UAAM,iBAAiB,QAAQ,OAAO,IAAI,OAAO,KAAK,kBAAkB,CAAC;AAAA,EAC3E,QAAQ;AACN,sBAAkB,gBAAgB,QAAQ,QAAQ,qBAAc,CAAC;AACjE,WAAO,KAAK;AAAA,EACd;AACA,UAAQ,KAAK,CAAC;AAChB;AAEA,KAAK,EAAE,MAAM,IAAI;","names":["dirname","join","join","join","join","dirname","arg"]}
|