memroot 0.1.0-alpha.0 → 0.1.0-alpha.3
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/README.md +50 -3
- package/dist/index.js +2831 -250
- package/dist/plugins/claude/.claude-plugin/plugin.json +2 -2
- package/dist/plugins/claude/README.md +8 -4
- package/dist/plugins/claude/hooks/hooks.json +10 -0
- package/dist/plugins/claude/scripts/capture-worker.mjs +19294 -0
- package/dist/plugins/claude/scripts/session-end.mjs +469 -0
- package/dist/plugins/claude/scripts/session-start.mjs +372 -53
- package/dist/plugins/claude/scripts/status.mjs +152 -12
- package/dist/plugins/claude/skills/memroot-memory/SKILL.md +48 -0
- package/dist/plugins/claude/skills/memroot-memory/references/save.md +76 -0
- package/dist/plugins/claude/skills/memroot-status/SKILL.md +19 -0
- package/dist/plugins/codex/.codex-plugin/plugin.json +5 -5
- package/dist/plugins/codex/README.md +13 -7
- package/dist/plugins/codex/hooks/hooks.json +11 -0
- package/dist/plugins/codex/scripts/capture-worker.mjs +19294 -0
- package/dist/plugins/codex/scripts/session-end.mjs +469 -0
- package/dist/plugins/codex/scripts/session-start.mjs +372 -53
- package/dist/plugins/codex/scripts/status.mjs +152 -12
- package/dist/plugins/codex/skills/memroot-memory/SKILL.md +48 -0
- package/dist/plugins/codex/skills/memroot-memory/references/save.md +76 -0
- package/dist/plugins/codex/skills/memroot-status/SKILL.md +20 -0
- package/dist/plugins/grok/.claude-plugin/plugin.json +2 -2
- package/dist/plugins/grok/README.md +10 -4
- package/dist/plugins/grok/hooks/hooks.json +24 -1
- package/dist/plugins/grok/scripts/capture-worker.mjs +19294 -0
- package/dist/plugins/grok/scripts/session-end.mjs +469 -0
- package/dist/plugins/grok/scripts/session-reminder.mjs +275 -0
- package/dist/plugins/grok/scripts/status.mjs +152 -12
- package/dist/plugins/grok/skills/memroot-memory/SKILL.md +48 -0
- package/dist/plugins/grok/skills/memroot-memory/references/save.md +76 -0
- package/dist/plugins/grok/skills/memroot-status/SKILL.md +20 -0
- package/package.json +13 -4
- package/dist/plugins/claude/skills/status/SKILL.md +0 -18
- package/dist/plugins/codex/skills/status/SKILL.md +0 -18
- package/dist/plugins/grok/skills/status/SKILL.md +0 -18
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/api-base.ts
|
|
4
|
+
function isLoopbackHostname(hostname) {
|
|
5
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
|
|
6
|
+
}
|
|
7
|
+
function isAllowedApiBase(value) {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
if (url.username || url.password || url.search || url.hash) return false;
|
|
11
|
+
if (url.protocol === "https:") return true;
|
|
12
|
+
return url.protocol === "http:" && isLoopbackHostname(url.hostname);
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/capture/paths.ts
|
|
19
|
+
import { randomBytes } from "node:crypto";
|
|
20
|
+
import {
|
|
21
|
+
chmodSync,
|
|
22
|
+
closeSync,
|
|
23
|
+
constants,
|
|
24
|
+
fstatSync,
|
|
25
|
+
lstatSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
openSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
readFileSync,
|
|
30
|
+
renameSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
writeFileSync
|
|
33
|
+
} from "node:fs";
|
|
34
|
+
import { homedir } from "node:os";
|
|
35
|
+
import { dirname, isAbsolute, join, normalize } from "node:path";
|
|
36
|
+
var UnsafePathError = class extends Error {
|
|
37
|
+
constructor(code = "UNSAFE_PATH") {
|
|
38
|
+
super(`${code}: refusing an unexpected file type or symbolic link.`);
|
|
39
|
+
this.code = code;
|
|
40
|
+
}
|
|
41
|
+
code;
|
|
42
|
+
};
|
|
43
|
+
function absoluteEnv(name) {
|
|
44
|
+
const value = process.env[name];
|
|
45
|
+
if (!value) return void 0;
|
|
46
|
+
if (!isAbsolute(value))
|
|
47
|
+
throw new Error(`INVALID_STATE_DIRECTORY: ${name} must be absolute.`);
|
|
48
|
+
return normalize(value);
|
|
49
|
+
}
|
|
50
|
+
function stateBoundary() {
|
|
51
|
+
const explicit = absoluteEnv("MEMROOT_STATE_DIR");
|
|
52
|
+
if (explicit) return { boundary: explicit, dir: explicit };
|
|
53
|
+
const base = absoluteEnv("XDG_STATE_HOME") ?? join(homedir(), ".local", "state");
|
|
54
|
+
return { boundary: base, dir: join(base, "memroot", "capture") };
|
|
55
|
+
}
|
|
56
|
+
function memrootConfigDir() {
|
|
57
|
+
const override = process.env.MEMROOT_TEST_CONFIG_DIR;
|
|
58
|
+
if (process.env.NODE_ENV === "test" && override && isAbsolute(override))
|
|
59
|
+
return normalize(override);
|
|
60
|
+
return join(homedir(), ".memroot");
|
|
61
|
+
}
|
|
62
|
+
function consentPath() {
|
|
63
|
+
return join(memrootConfigDir(), "capture.json");
|
|
64
|
+
}
|
|
65
|
+
function assertRealDirectory(path) {
|
|
66
|
+
const info = lstatSync(path);
|
|
67
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new UnsafePathError();
|
|
68
|
+
}
|
|
69
|
+
function ensurePrivateDir(path, boundary, checkRoot = false) {
|
|
70
|
+
const target = normalize(path);
|
|
71
|
+
const root = normalize(boundary);
|
|
72
|
+
if (target !== root && !target.startsWith(`${root}/`))
|
|
73
|
+
throw new UnsafePathError();
|
|
74
|
+
mkdirSync(target, { recursive: true, mode: 448 });
|
|
75
|
+
let current = target;
|
|
76
|
+
for (; ; ) {
|
|
77
|
+
if (current === root) {
|
|
78
|
+
if (checkRoot) assertRealDirectory(current);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
assertRealDirectory(current);
|
|
82
|
+
current = dirname(current);
|
|
83
|
+
}
|
|
84
|
+
const info = lstatSync(target);
|
|
85
|
+
if ((info.mode & 511) !== 448) chmodSync(target, 448);
|
|
86
|
+
}
|
|
87
|
+
function stateSubdir(...parts) {
|
|
88
|
+
const { boundary, dir } = stateBoundary();
|
|
89
|
+
const path = join(dir, ...parts);
|
|
90
|
+
ensurePrivateDir(path, boundary, boundary === dir);
|
|
91
|
+
return path;
|
|
92
|
+
}
|
|
93
|
+
function readPrivateFile(path, maxBytes = 16 * 1024 * 1024) {
|
|
94
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
95
|
+
try {
|
|
96
|
+
const info = fstatSync(fd);
|
|
97
|
+
if (!info.isFile()) throw new UnsafePathError();
|
|
98
|
+
if (info.size > maxBytes) throw new UnsafePathError("FILE_TOO_LARGE");
|
|
99
|
+
return readFileSync(fd, "utf8");
|
|
100
|
+
} finally {
|
|
101
|
+
closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function readPrivateFileIfExists(path, maxBytes) {
|
|
105
|
+
try {
|
|
106
|
+
return readPrivateFile(path, maxBytes);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.code === "ENOENT") return void 0;
|
|
109
|
+
if (error.code === "ELOOP")
|
|
110
|
+
throw new UnsafePathError();
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function writePrivateFileAtomic(path, content) {
|
|
115
|
+
const temporary = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
116
|
+
try {
|
|
117
|
+
writeFileSync(temporary, content, { flag: "wx", mode: 384 });
|
|
118
|
+
renameSync(temporary, path);
|
|
119
|
+
} finally {
|
|
120
|
+
rmSync(temporary, { force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function createPrivateFileExclusive(path, content) {
|
|
124
|
+
try {
|
|
125
|
+
writeFileSync(path, content, { flag: "wx", mode: 384 });
|
|
126
|
+
return true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code === "EEXIST") return false;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/capture/consent.ts
|
|
134
|
+
var CAPTURE_AGENTS = ["claude", "codex", "grok"];
|
|
135
|
+
var CONSENT_TEXT_VERSION = "capture-consent-v2";
|
|
136
|
+
var KEYS = [
|
|
137
|
+
"schemaVersion",
|
|
138
|
+
"enabled",
|
|
139
|
+
"enabledAt",
|
|
140
|
+
"consentTextVersion",
|
|
141
|
+
"apiBase",
|
|
142
|
+
"agents",
|
|
143
|
+
"crossAgentExtraction",
|
|
144
|
+
"includeHeadlessSessions",
|
|
145
|
+
"maxItemsPerSession",
|
|
146
|
+
"maxItemsPerDay",
|
|
147
|
+
"models"
|
|
148
|
+
];
|
|
149
|
+
var MODEL = /^[A-Za-z0-9._:/@+-]{1,128}$/;
|
|
150
|
+
function isInt(value, min, max) {
|
|
151
|
+
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
152
|
+
}
|
|
153
|
+
function parseConsent(value) {
|
|
154
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
155
|
+
return void 0;
|
|
156
|
+
const record = value;
|
|
157
|
+
const keys = Object.keys(record);
|
|
158
|
+
if (keys.some((key) => !KEYS.includes(key)) || keys.length !== KEYS.length)
|
|
159
|
+
return void 0;
|
|
160
|
+
const agents = record.agents;
|
|
161
|
+
const models = record.models;
|
|
162
|
+
if (record.schemaVersion !== 1 || typeof record.enabled !== "boolean" || typeof record.enabledAt !== "string" || Number.isNaN(Date.parse(record.enabledAt)) || record.consentTextVersion !== CONSENT_TEXT_VERSION || typeof record.apiBase !== "string" || !isAllowedApiBase(record.apiBase) || record.apiBase.endsWith("/") || !Array.isArray(agents) || agents.length === 0 || agents.length > CAPTURE_AGENTS.length || new Set(agents).size !== agents.length || agents.some((agent2) => !CAPTURE_AGENTS.includes(agent2)) || typeof record.crossAgentExtraction !== "boolean" || typeof record.includeHeadlessSessions !== "boolean" || !isInt(record.maxItemsPerSession, 1, 5) || !isInt(record.maxItemsPerDay, 1, 20) || !models || typeof models !== "object" || Array.isArray(models))
|
|
163
|
+
return void 0;
|
|
164
|
+
const modelRecord = models;
|
|
165
|
+
const modelKeys = Object.keys(modelRecord);
|
|
166
|
+
if (modelKeys.length !== CAPTURE_AGENTS.length || modelKeys.some((key) => !CAPTURE_AGENTS.includes(key)) || Object.values(modelRecord).some(
|
|
167
|
+
(model) => model !== null && (typeof model !== "string" || !MODEL.test(model))
|
|
168
|
+
))
|
|
169
|
+
return void 0;
|
|
170
|
+
return record;
|
|
171
|
+
}
|
|
172
|
+
function readConsent() {
|
|
173
|
+
let raw;
|
|
174
|
+
try {
|
|
175
|
+
raw = readPrivateFileIfExists(consentPath(), 64 * 1024);
|
|
176
|
+
} catch {
|
|
177
|
+
return { kind: "invalid" };
|
|
178
|
+
}
|
|
179
|
+
if (raw === void 0) return { kind: "absent" };
|
|
180
|
+
try {
|
|
181
|
+
const consent = parseConsent(JSON.parse(raw));
|
|
182
|
+
return consent ? { kind: "ok", consent } : { kind: "invalid" };
|
|
183
|
+
} catch {
|
|
184
|
+
return { kind: "invalid" };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function activeConsentFor(agent2) {
|
|
188
|
+
const read = readConsent();
|
|
189
|
+
if (read.kind !== "ok") return void 0;
|
|
190
|
+
return read.consent.enabled && read.consent.agents.includes(agent2) ? read.consent : void 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/capture/queue/job.ts
|
|
194
|
+
import { createHash, randomBytes as randomBytes2 } from "node:crypto";
|
|
195
|
+
import { join as join2 } from "node:path";
|
|
196
|
+
var JOB_STATES = [
|
|
197
|
+
"pending",
|
|
198
|
+
"extracted",
|
|
199
|
+
"quota_blocked",
|
|
200
|
+
"auth_required",
|
|
201
|
+
"server_unsupported",
|
|
202
|
+
"runner_unavailable"
|
|
203
|
+
];
|
|
204
|
+
var JOB_ID = /^(claude|codex|grok)-[a-f0-9]{32}$/;
|
|
205
|
+
var HEX64 = /^[a-f0-9]{64}$/;
|
|
206
|
+
function sha256Hex(value) {
|
|
207
|
+
return createHash("sha256").update(value).digest("hex");
|
|
208
|
+
}
|
|
209
|
+
function readOrCreateSalt() {
|
|
210
|
+
const path = join2(stateSubdir(), "salt");
|
|
211
|
+
const existing = readPrivateFileIfExists(path, 1024);
|
|
212
|
+
if (existing !== void 0) {
|
|
213
|
+
const value = existing.trim();
|
|
214
|
+
if (!HEX64.test(value)) throw new Error("INVALID_SALT");
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
const salt = randomBytes2(32).toString("hex");
|
|
218
|
+
if (createPrivateFileExclusive(path, `${salt}
|
|
219
|
+
`)) return salt;
|
|
220
|
+
return readOrCreateSalt();
|
|
221
|
+
}
|
|
222
|
+
function sessionRefFor(salt, agent2, sessionId) {
|
|
223
|
+
return sha256Hex(`${salt}\0${agent2}\0${sessionId}`);
|
|
224
|
+
}
|
|
225
|
+
function queueDir() {
|
|
226
|
+
return stateSubdir("queue");
|
|
227
|
+
}
|
|
228
|
+
function jobPath(jobId) {
|
|
229
|
+
return join2(queueDir(), `${jobId}.json`);
|
|
230
|
+
}
|
|
231
|
+
function parseJob(value) {
|
|
232
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
233
|
+
return void 0;
|
|
234
|
+
const job = value;
|
|
235
|
+
const optionalTime = (field) => field === null || typeof field === "string" && !Number.isNaN(Date.parse(field));
|
|
236
|
+
if (job.schemaVersion !== 1 || typeof job.jobId !== "string" || !JOB_ID.test(job.jobId) || !CAPTURE_AGENTS.includes(job.agent) || !job.jobId.startsWith(`${job.agent}-`) || typeof job.sessionId !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(job.sessionId) || typeof job.sessionRef !== "string" || !HEX64.test(job.sessionRef) || job.jobId !== `${job.agent}-${job.sessionRef.slice(0, 32)}` || !(job.transcriptPath === null || typeof job.transcriptPath === "string") || typeof job.cwd !== "string" || !job.cwd.startsWith("/") || typeof job.reason !== "string" || !optionalTime(job.endedAt) || job.endedAt === null || !optionalTime(job.createdAt) || job.createdAt === null || !JOB_STATES.includes(job.state) || typeof job.attempts !== "number" || !Number.isInteger(job.attempts) || job.attempts < 0 || !optionalTime(job.nextAttemptAt) || !(job.lastError === null || typeof job.lastError === "string"))
|
|
237
|
+
return void 0;
|
|
238
|
+
return job;
|
|
239
|
+
}
|
|
240
|
+
function readJob(jobId) {
|
|
241
|
+
const raw = readPrivateFileIfExists(jobPath(jobId), 64 * 1024);
|
|
242
|
+
if (raw === void 0) return void 0;
|
|
243
|
+
try {
|
|
244
|
+
return parseJob(JSON.parse(raw));
|
|
245
|
+
} catch {
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function writeJob(job) {
|
|
250
|
+
writePrivateFileAtomic(jobPath(job.jobId), `${JSON.stringify(job)}
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
function enqueueFromHook(input) {
|
|
254
|
+
const salt = readOrCreateSalt();
|
|
255
|
+
const sessionRef = sessionRefFor(salt, input.agent, input.sessionId);
|
|
256
|
+
const jobId = `${input.agent}-${sessionRef.slice(0, 32)}`;
|
|
257
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
258
|
+
const job = {
|
|
259
|
+
schemaVersion: 1,
|
|
260
|
+
jobId,
|
|
261
|
+
agent: input.agent,
|
|
262
|
+
sessionId: input.sessionId,
|
|
263
|
+
sessionRef,
|
|
264
|
+
transcriptPath: input.transcriptPath,
|
|
265
|
+
cwd: input.cwd,
|
|
266
|
+
reason: input.reason,
|
|
267
|
+
endedAt: now,
|
|
268
|
+
createdAt: now,
|
|
269
|
+
state: "pending",
|
|
270
|
+
attempts: 0,
|
|
271
|
+
nextAttemptAt: null,
|
|
272
|
+
lastError: null
|
|
273
|
+
};
|
|
274
|
+
if (createPrivateFileExclusive(jobPath(jobId), `${JSON.stringify(job)}
|
|
275
|
+
`))
|
|
276
|
+
return job;
|
|
277
|
+
const existing = readJob(jobId);
|
|
278
|
+
if (existing) {
|
|
279
|
+
const updated = {
|
|
280
|
+
...existing,
|
|
281
|
+
endedAt: now,
|
|
282
|
+
reason: input.reason,
|
|
283
|
+
transcriptPath: existing.transcriptPath ?? input.transcriptPath
|
|
284
|
+
};
|
|
285
|
+
writeJob(updated);
|
|
286
|
+
return updated;
|
|
287
|
+
}
|
|
288
|
+
return existing ?? job;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/capture/hooks/envelope.ts
|
|
292
|
+
var MAX_ENVELOPE_BYTES = 64 * 1024;
|
|
293
|
+
var STDIN_DEADLINE_MS = 500;
|
|
294
|
+
function readEnvelope() {
|
|
295
|
+
return new Promise((resolve) => {
|
|
296
|
+
let bytes = 0;
|
|
297
|
+
let chunks = [];
|
|
298
|
+
let settled = false;
|
|
299
|
+
const finish = (value) => {
|
|
300
|
+
if (settled) return;
|
|
301
|
+
settled = true;
|
|
302
|
+
clearTimeout(deadline);
|
|
303
|
+
chunks = [];
|
|
304
|
+
process.stdin.removeAllListeners("data");
|
|
305
|
+
process.stdin.destroy();
|
|
306
|
+
resolve(value);
|
|
307
|
+
};
|
|
308
|
+
const deadline = setTimeout(() => finish(void 0), STDIN_DEADLINE_MS);
|
|
309
|
+
process.stdin.on("error", () => finish(void 0));
|
|
310
|
+
process.stdin.on("data", (chunk) => {
|
|
311
|
+
bytes += chunk.length;
|
|
312
|
+
if (bytes > MAX_ENVELOPE_BYTES) {
|
|
313
|
+
finish(void 0);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
chunks.push(chunk);
|
|
317
|
+
});
|
|
318
|
+
process.stdin.on("end", () => {
|
|
319
|
+
try {
|
|
320
|
+
const value = JSON.parse(
|
|
321
|
+
Buffer.concat(chunks).toString("utf8")
|
|
322
|
+
);
|
|
323
|
+
finish(
|
|
324
|
+
value && typeof value === "object" && !Array.isArray(value) ? value : void 0
|
|
325
|
+
);
|
|
326
|
+
} catch {
|
|
327
|
+
finish(void 0);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
333
|
+
var REASON = /^[a-z_]{1,40}$/;
|
|
334
|
+
function string(record, ...keys) {
|
|
335
|
+
for (const key of keys) {
|
|
336
|
+
const value = record[key];
|
|
337
|
+
if (typeof value === "string") return value;
|
|
338
|
+
}
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
function isSessionEndEvent(agent2, record) {
|
|
342
|
+
if (agent2 === "grok")
|
|
343
|
+
return record.hook_event_name === "SessionEnd" || record.hookEventName === "session_end" || record.hookEventName === "SessionEnd";
|
|
344
|
+
return record.hook_event_name === "SessionEnd";
|
|
345
|
+
}
|
|
346
|
+
function isSubagentEvent(record) {
|
|
347
|
+
return record.subagentType !== void 0 || record.subagent_type !== void 0 || record.agent_id !== void 0;
|
|
348
|
+
}
|
|
349
|
+
function normalizeSessionEnd(agent2, record) {
|
|
350
|
+
const sessionId = string(record, "session_id", "sessionId");
|
|
351
|
+
const cwd = string(record, "cwd", "workspaceRoot");
|
|
352
|
+
const rawTranscript = "transcript_path" in record ? record.transcript_path : record.transcriptPath;
|
|
353
|
+
if (!sessionId || !SESSION_ID.test(sessionId)) return void 0;
|
|
354
|
+
if (!cwd?.startsWith("/") || cwd.length > 4096 || cwd.includes("\0"))
|
|
355
|
+
return void 0;
|
|
356
|
+
let transcriptPath;
|
|
357
|
+
if (typeof rawTranscript === "string" && rawTranscript.length > 0)
|
|
358
|
+
transcriptPath = rawTranscript;
|
|
359
|
+
else if (agent2 === "grok" && rawTranscript === void 0)
|
|
360
|
+
transcriptPath = null;
|
|
361
|
+
else return void 0;
|
|
362
|
+
const reasonValue = string(record, "reason");
|
|
363
|
+
const reason = reasonValue && REASON.test(reasonValue) ? reasonValue : "other";
|
|
364
|
+
return {
|
|
365
|
+
sessionId,
|
|
366
|
+
transcriptPath,
|
|
367
|
+
cwd,
|
|
368
|
+
reason,
|
|
369
|
+
subagent: isSubagentEvent(record)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/capture/hooks/guards.ts
|
|
374
|
+
import { realpathSync } from "node:fs";
|
|
375
|
+
import { homedir as homedir2 } from "node:os";
|
|
376
|
+
import { isAbsolute as isAbsolute2, join as join3, normalize as normalize2 } from "node:path";
|
|
377
|
+
function processGuard() {
|
|
378
|
+
if (process.env.MEMROOT_CAPTURE_WORKER === "1") return true;
|
|
379
|
+
if (process.env.CLAUDE_CODE_REMOTE === "true") return true;
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
function hostAgent(bundled) {
|
|
383
|
+
return process.env.GROK_HOOK_EVENT ? "grok" : bundled;
|
|
384
|
+
}
|
|
385
|
+
function absoluteEnv2(name) {
|
|
386
|
+
const value = process.env[name];
|
|
387
|
+
return value && isAbsolute2(value) ? normalize2(value) : void 0;
|
|
388
|
+
}
|
|
389
|
+
function transcriptRoots(agent2) {
|
|
390
|
+
if (agent2 === "claude")
|
|
391
|
+
return [
|
|
392
|
+
join3(
|
|
393
|
+
absoluteEnv2("CLAUDE_CONFIG_DIR") ?? join3(homedir2(), ".claude"),
|
|
394
|
+
"projects"
|
|
395
|
+
)
|
|
396
|
+
];
|
|
397
|
+
if (agent2 === "codex") {
|
|
398
|
+
const home = absoluteEnv2("CODEX_HOME") ?? join3(homedir2(), ".codex");
|
|
399
|
+
return [join3(home, "sessions"), join3(home, "archived_sessions")];
|
|
400
|
+
}
|
|
401
|
+
return [
|
|
402
|
+
join3(absoluteEnv2("GROK_HOME") ?? join3(homedir2(), ".grok"), "sessions")
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
function transcriptPathAllowed(agent2, path) {
|
|
406
|
+
if (!isAbsolute2(path) || path.length > 4096 || path.includes("\0") || path.split("/").includes("..") || normalize2(path) !== path)
|
|
407
|
+
return false;
|
|
408
|
+
if (agent2 !== "grok" && !path.endsWith(".jsonl")) return false;
|
|
409
|
+
return transcriptRoots(agent2).some((root) => {
|
|
410
|
+
if (path.startsWith(`${root}/`)) return true;
|
|
411
|
+
try {
|
|
412
|
+
return path.startsWith(`${realpathSync(root)}/`);
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
function isHeadlessSession(agent2) {
|
|
419
|
+
if (agent2 === "claude")
|
|
420
|
+
return (process.env.CLAUDE_CODE_ENTRYPOINT ?? "").startsWith("sdk");
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/capture/hooks/spawn.ts
|
|
425
|
+
import { spawn } from "node:child_process";
|
|
426
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
427
|
+
import { fileURLToPath } from "node:url";
|
|
428
|
+
function spawnWorker() {
|
|
429
|
+
try {
|
|
430
|
+
const worker = join4(
|
|
431
|
+
dirname2(fileURLToPath(import.meta.url)),
|
|
432
|
+
"capture-worker.mjs"
|
|
433
|
+
);
|
|
434
|
+
const child = spawn(process.execPath, [worker], {
|
|
435
|
+
detached: true,
|
|
436
|
+
stdio: "ignore",
|
|
437
|
+
env: { ...process.env, MEMROOT_CAPTURE_WORKER: "1" }
|
|
438
|
+
});
|
|
439
|
+
child.on("error", () => {
|
|
440
|
+
});
|
|
441
|
+
child.unref();
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/capture/hooks/session-end.ts
|
|
447
|
+
var agent = hostAgent("codex");
|
|
448
|
+
async function main() {
|
|
449
|
+
const record = await readEnvelope();
|
|
450
|
+
if (processGuard()) return;
|
|
451
|
+
const consent = activeConsentFor(agent);
|
|
452
|
+
if (!consent) return;
|
|
453
|
+
if (!record || !isSessionEndEvent(agent, record)) return;
|
|
454
|
+
const envelope = normalizeSessionEnd(agent, record);
|
|
455
|
+
if (!envelope || envelope.subagent) return;
|
|
456
|
+
if (envelope.transcriptPath !== null && !transcriptPathAllowed(agent, envelope.transcriptPath))
|
|
457
|
+
return;
|
|
458
|
+
if (!consent.includeHeadlessSessions && isHeadlessSession(agent)) return;
|
|
459
|
+
enqueueFromHook({
|
|
460
|
+
agent,
|
|
461
|
+
sessionId: envelope.sessionId,
|
|
462
|
+
transcriptPath: envelope.transcriptPath,
|
|
463
|
+
cwd: envelope.cwd,
|
|
464
|
+
reason: envelope.reason
|
|
465
|
+
});
|
|
466
|
+
spawnWorker();
|
|
467
|
+
}
|
|
468
|
+
main().catch(() => {
|
|
469
|
+
}).finally(() => process.exit(0));
|