@letta-ai/dreams 0.0.1 → 0.0.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 +74 -20
- package/dist/auth/commands.js +46 -4
- package/dist/auth/credentials.js +28 -8
- package/dist/cli.js +198 -8
- package/dist/cloud/client.js +89 -5
- package/dist/cloud/upload.js +150 -70
- package/dist/development-reset.js +359 -0
- package/dist/launcher.js +311 -0
- package/dist/local/backfill.js +608 -0
- package/dist/local/bindings.js +30 -0
- package/dist/local/claude.js +61 -163
- package/dist/local/codex.js +313 -0
- package/dist/local/commands.js +129 -10
- package/dist/local/db.js +64 -0
- package/dist/local/detect.js +144 -0
- package/dist/local/discovered.js +6 -0
- package/dist/local/leases.js +3 -1
- package/dist/local/scan.js +99 -17
- package/dist/local/source-adapter.js +8 -0
- package/dist/local/source-adapters.js +58 -0
- package/dist/local/state-lock.js +88 -0
- package/dist/local/sync-control.js +381 -0
- package/dist/local/transcript-bytes.js +204 -0
- package/dist/managed-install.js +74 -86
- package/dist/onboard.js +26 -72
- package/dist/skill.js +281 -16
- package/dist/snapshots.js +210 -0
- package/dist/sync.js +790 -0
- package/package.json +1 -1
package/dist/local/claude.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Claude Code source adapter — discovery
|
|
3
|
+
* Claude Code source adapter — discovery and harness detection.
|
|
4
4
|
*
|
|
5
5
|
* Discovery rule (filesystem shape is authoritative for the pilot):
|
|
6
6
|
* - only DIRECT children of ~/.claude/projects/<encoded-project>/
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* `parentUuid` is intentionally NOT used — ordinary main-session records also
|
|
12
12
|
* carry parent relationships.
|
|
13
13
|
*
|
|
14
|
-
* This module never normalizes transcripts; it only reads raw bytes
|
|
14
|
+
* This module never normalizes transcripts; it only reads raw bytes for
|
|
15
|
+
* discovery. Generic scan byte primitives live in `transcript-bytes.ts`.
|
|
16
|
+
* Registration lives in `source-adapters.ts`.
|
|
15
17
|
*/
|
|
16
18
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
19
|
if (k2 === undefined) k2 = k;
|
|
@@ -47,35 +49,63 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
47
49
|
};
|
|
48
50
|
})();
|
|
49
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
-
exports.
|
|
52
|
+
exports.claudeSourceAdapter = exports.CLAUDE_DISPLAY_NAME = exports.CLAUDE_SOURCE_ID = exports.CLAUDE_ADAPTER_VERSION = exports.CLAUDE_SOURCE_TYPE = void 0;
|
|
51
53
|
exports.claudeProjectsDir = claudeProjectsDir;
|
|
52
|
-
exports.
|
|
53
|
-
exports.
|
|
54
|
-
exports.scanCompleteLinesVerified = scanCompleteLinesVerified;
|
|
54
|
+
exports.validateClaudeSessionId = validateClaudeSessionId;
|
|
55
|
+
exports.detectClaudeCodeHarness = detectClaudeCodeHarness;
|
|
55
56
|
exports.discoverSessions = discoverSessions;
|
|
56
|
-
const crypto = __importStar(require("node:crypto"));
|
|
57
57
|
const fs = __importStar(require("node:fs"));
|
|
58
58
|
const os = __importStar(require("node:os"));
|
|
59
59
|
const path = __importStar(require("node:path"));
|
|
60
60
|
const config_1 = require("./config");
|
|
61
|
+
const transcript_bytes_1 = require("./transcript-bytes");
|
|
61
62
|
exports.CLAUDE_SOURCE_TYPE = "claude-code";
|
|
62
63
|
exports.CLAUDE_ADAPTER_VERSION = "claude-code-1";
|
|
64
|
+
exports.CLAUDE_SOURCE_ID = `src_${exports.CLAUDE_SOURCE_TYPE}`;
|
|
65
|
+
exports.CLAUDE_DISPLAY_NAME = "Claude Code";
|
|
63
66
|
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
64
67
|
const HEAD_READ_BYTES = 256 * 1024;
|
|
65
|
-
const FIRST_CHUNK_BYTES = 4096;
|
|
66
68
|
function claudeProjectsDir() {
|
|
67
69
|
return path.join(os.homedir(), ".claude", "projects");
|
|
68
70
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
constructor(filePath) {
|
|
72
|
-
super(`file identity changed under read: ${filePath}`);
|
|
73
|
-
this.name = "IdentityChangedError";
|
|
74
|
-
}
|
|
71
|
+
function validateClaudeSessionId(value) {
|
|
72
|
+
return UUID_RE.test(value);
|
|
75
73
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Deterministic harness-owned filesystem facts only. Never opens transcript
|
|
76
|
+
* files or walks session contents. Paths stay private to this adapter.
|
|
77
|
+
*/
|
|
78
|
+
function detectClaudeCodeHarness(home = os.homedir()) {
|
|
79
|
+
const claudeDir = path.join(home, ".claude");
|
|
80
|
+
const evidence = [];
|
|
81
|
+
try {
|
|
82
|
+
if (!fs.statSync(claudeDir).isDirectory()) {
|
|
83
|
+
return { present: false, evidence };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { present: false, evidence };
|
|
88
|
+
}
|
|
89
|
+
evidence.push(claudeDir);
|
|
90
|
+
const projectsDir = path.join(home, ".claude", "projects");
|
|
91
|
+
try {
|
|
92
|
+
if (fs.statSync(projectsDir).isDirectory())
|
|
93
|
+
evidence.push(projectsDir);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// projects/ is optional evidence; ~/.claude alone is enough.
|
|
97
|
+
}
|
|
98
|
+
for (const name of ["settings.json", "settings.local.json", "CLAUDE.md"]) {
|
|
99
|
+
const candidate = path.join(claudeDir, name);
|
|
100
|
+
try {
|
|
101
|
+
if (fs.statSync(candidate).isFile())
|
|
102
|
+
evidence.push(candidate);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// ignore
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { present: true, evidence };
|
|
79
109
|
}
|
|
80
110
|
/** Read the file head and return the first COMPLETE JSONL record, or null. */
|
|
81
111
|
function readFirstRecord(filePath) {
|
|
@@ -101,151 +131,6 @@ function readFirstRecord(filePath) {
|
|
|
101
131
|
fs.closeSync(fd);
|
|
102
132
|
}
|
|
103
133
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Fingerprint the transcript's FIRST LINE (which carries the stable sessionId).
|
|
106
|
-
* This is invariant under appends — new bytes go after the first line — but
|
|
107
|
-
* changes when the file is replaced by a different session, so it is a reliable
|
|
108
|
-
* replacement signal. (Hashing a fixed-size prefix would wrongly change on every
|
|
109
|
-
* append for files shorter than that prefix.)
|
|
110
|
-
*/
|
|
111
|
-
function firstChunkFingerprint(filePath) {
|
|
112
|
-
let fd;
|
|
113
|
-
try {
|
|
114
|
-
fd = fs.openSync(filePath, "r");
|
|
115
|
-
const buf = Buffer.alloc(FIRST_CHUNK_BYTES);
|
|
116
|
-
const read = fs.readSync(fd, buf, 0, FIRST_CHUNK_BYTES, 0);
|
|
117
|
-
const head = buf.subarray(0, read);
|
|
118
|
-
const nl = head.indexOf(0x0a);
|
|
119
|
-
const firstLine = nl === -1 ? head : head.subarray(0, nl + 1);
|
|
120
|
-
return fingerprint(firstLine);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
return "";
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
if (fd !== undefined)
|
|
127
|
-
fs.closeSync(fd);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const IO_CHUNK_BYTES = 64 * 1024;
|
|
131
|
-
function normalizedMtime(stat) {
|
|
132
|
-
return Number.isFinite(stat.mtimeMs) ? Math.floor(stat.mtimeMs) : null;
|
|
133
|
-
}
|
|
134
|
-
function inodeOf(stat) {
|
|
135
|
-
return stat.ino ? String(stat.ino) : null;
|
|
136
|
-
}
|
|
137
|
-
function hashRange(fd, end, digest, onProgress) {
|
|
138
|
-
let position = 0;
|
|
139
|
-
while (position < end) {
|
|
140
|
-
const buf = Buffer.allocUnsafe(Math.min(IO_CHUNK_BYTES, end - position));
|
|
141
|
-
const read = fs.readSync(fd, buf, 0, buf.length, position);
|
|
142
|
-
if (read === 0)
|
|
143
|
-
throw new IdentityChangedError("opened transcript");
|
|
144
|
-
digest.update(buf.subarray(0, read));
|
|
145
|
-
position += read;
|
|
146
|
-
onProgress?.();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/** SHA-256 of [0, length), read with bounded memory. */
|
|
150
|
-
function prefixFingerprint(filePath, length, onProgress) {
|
|
151
|
-
if (length <= 0)
|
|
152
|
-
return "";
|
|
153
|
-
const fd = fs.openSync(filePath, "r");
|
|
154
|
-
try {
|
|
155
|
-
const digest = crypto.createHash("sha256");
|
|
156
|
-
hashRange(fd, length, digest, onProgress);
|
|
157
|
-
return digest.digest("hex");
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
fs.closeSync(fd);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
class OversizedLineError extends Error {
|
|
164
|
-
filePath;
|
|
165
|
-
startOffset;
|
|
166
|
-
byteLength;
|
|
167
|
-
maxBytes;
|
|
168
|
-
constructor(filePath, startOffset, byteLength, maxBytes) {
|
|
169
|
-
super(`JSONL record at byte ${startOffset} in ${filePath} is ${byteLength} bytes; maximum is ${maxBytes}`);
|
|
170
|
-
this.filePath = filePath;
|
|
171
|
-
this.startOffset = startOffset;
|
|
172
|
-
this.byteLength = byteLength;
|
|
173
|
-
this.maxBytes = maxBytes;
|
|
174
|
-
this.name = "OversizedLineError";
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
exports.OversizedLineError = OversizedLineError;
|
|
178
|
-
/**
|
|
179
|
-
* Read complete JSONL records from one opened handle using bounded memory.
|
|
180
|
-
* The covered-prefix hash is computed from that same handle, and a final fstat
|
|
181
|
-
* rejects files that changed while being scanned.
|
|
182
|
-
*/
|
|
183
|
-
function scanCompleteLinesVerified(realPath, start, expectedInode, maxLineBytes, onLine, onProgress) {
|
|
184
|
-
let fd;
|
|
185
|
-
try {
|
|
186
|
-
fd = fs.openSync(realPath, "r");
|
|
187
|
-
const initial = fs.fstatSync(fd);
|
|
188
|
-
const inode = inodeOf(initial);
|
|
189
|
-
if (expectedInode !== null && inode !== null && inode !== expectedInode)
|
|
190
|
-
throw new IdentityChangedError(realPath);
|
|
191
|
-
if (start > initial.size)
|
|
192
|
-
throw new IdentityChangedError(realPath);
|
|
193
|
-
const digest = crypto.createHash("sha256");
|
|
194
|
-
hashRange(fd, start, digest, onProgress);
|
|
195
|
-
let readPosition = start;
|
|
196
|
-
let carry = Buffer.alloc(0);
|
|
197
|
-
let carryStart = start;
|
|
198
|
-
let completeThrough = start;
|
|
199
|
-
while (readPosition < initial.size) {
|
|
200
|
-
const chunk = Buffer.allocUnsafe(Math.min(IO_CHUNK_BYTES, initial.size - readPosition));
|
|
201
|
-
const read = fs.readSync(fd, chunk, 0, chunk.length, readPosition);
|
|
202
|
-
if (read === 0)
|
|
203
|
-
break;
|
|
204
|
-
readPosition += read;
|
|
205
|
-
onProgress?.();
|
|
206
|
-
const combined = carry.length === 0 ? chunk.subarray(0, read) : Buffer.concat([carry, chunk.subarray(0, read)]);
|
|
207
|
-
const combinedStart = carryStart;
|
|
208
|
-
let consumed = 0;
|
|
209
|
-
while (consumed < combined.length) {
|
|
210
|
-
const newline = combined.indexOf(0x0a, consumed);
|
|
211
|
-
if (newline === -1)
|
|
212
|
-
break;
|
|
213
|
-
const end = newline + 1;
|
|
214
|
-
const byteLength = end - consumed;
|
|
215
|
-
const lineStart = combinedStart + consumed;
|
|
216
|
-
if (byteLength > maxLineBytes)
|
|
217
|
-
throw new OversizedLineError(realPath, lineStart, byteLength, maxLineBytes);
|
|
218
|
-
const bytes = combined.subarray(consumed, end);
|
|
219
|
-
digest.update(bytes);
|
|
220
|
-
completeThrough = combinedStart + end;
|
|
221
|
-
onLine({ bytes, start_offset: lineStart, end_offset: completeThrough });
|
|
222
|
-
consumed = end;
|
|
223
|
-
}
|
|
224
|
-
carry = Buffer.from(combined.subarray(consumed));
|
|
225
|
-
carryStart = combinedStart + consumed;
|
|
226
|
-
if (carry.length > maxLineBytes) {
|
|
227
|
-
throw new OversizedLineError(realPath, carryStart, carry.length, maxLineBytes);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
const final = fs.fstatSync(fd);
|
|
231
|
-
if (inodeOf(final) !== inode ||
|
|
232
|
-
final.size !== initial.size ||
|
|
233
|
-
normalizedMtime(final) !== normalizedMtime(initial)) {
|
|
234
|
-
throw new IdentityChangedError(realPath);
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
size: initial.size,
|
|
238
|
-
inode,
|
|
239
|
-
mtime_ms: normalizedMtime(initial),
|
|
240
|
-
complete_through_offset: completeThrough,
|
|
241
|
-
prefix_fp: completeThrough > 0 ? digest.digest("hex") : "",
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
finally {
|
|
245
|
-
if (fd !== undefined)
|
|
246
|
-
fs.closeSync(fd);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
134
|
function statSignals(filePath) {
|
|
250
135
|
const stat = fs.statSync(filePath);
|
|
251
136
|
const inode = stat.ino ? String(stat.ino) : null;
|
|
@@ -256,6 +141,9 @@ function statSignals(filePath) {
|
|
|
256
141
|
/**
|
|
257
142
|
* Enumerate valid top-level Claude Code session files. Malformed, sidechain,
|
|
258
143
|
* name-mismatched, and not-yet-complete files are skipped rather than failing.
|
|
144
|
+
*
|
|
145
|
+
* Optional `projectsDir` is Claude-private (tests / direct callers). The
|
|
146
|
+
* `SourceAdapter.discoverSessions` method does not expose this path override.
|
|
259
147
|
*/
|
|
260
148
|
function discoverSessions(projectsDir = claudeProjectsDir()) {
|
|
261
149
|
const sessions = [];
|
|
@@ -306,10 +194,20 @@ function discoverSessions(projectsDir = claudeProjectsDir()) {
|
|
|
306
194
|
inode: signals.inode,
|
|
307
195
|
birthtime_ms: signals.birthtime_ms,
|
|
308
196
|
mtime_ms: signals.mtime_ms,
|
|
309
|
-
first_chunk_fp: firstChunkFingerprint(filePath),
|
|
197
|
+
first_chunk_fp: (0, transcript_bytes_1.firstChunkFingerprint)(filePath),
|
|
310
198
|
});
|
|
311
199
|
}
|
|
312
200
|
}
|
|
313
201
|
sessions.sort((a, b) => a.path.localeCompare(b.path));
|
|
314
202
|
return sessions;
|
|
315
203
|
}
|
|
204
|
+
exports.claudeSourceAdapter = {
|
|
205
|
+
sourceType: exports.CLAUDE_SOURCE_TYPE,
|
|
206
|
+
sourceKey: exports.CLAUDE_SOURCE_TYPE,
|
|
207
|
+
sourceId: exports.CLAUDE_SOURCE_ID,
|
|
208
|
+
adapterVersion: exports.CLAUDE_ADAPTER_VERSION,
|
|
209
|
+
displayName: exports.CLAUDE_DISPLAY_NAME,
|
|
210
|
+
validateSessionId: validateClaudeSessionId,
|
|
211
|
+
discoverSessions: () => discoverSessions(),
|
|
212
|
+
detect: () => detectClaudeCodeHarness(),
|
|
213
|
+
};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Codex source adapter — discovery and harness detection ([LET-10314]).
|
|
4
|
+
*
|
|
5
|
+
* On-disk layout (fail closed when unrecognized):
|
|
6
|
+
* ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<sessionId>.jsonl
|
|
7
|
+
*
|
|
8
|
+
* Identity chain (upstream openai/codex@fb6aad9ae341; ThreadId is UUIDv7 Display string):
|
|
9
|
+
* - Filename suffix = `conversation_id` / ThreadId
|
|
10
|
+
* (`codex-rs/rollout/src/recorder.rs` `precompute_log_file_info`)
|
|
11
|
+
* - First JSONL item is RolloutLine{type:session_meta,payload:SessionMetaLine}
|
|
12
|
+
* (`codex-rs/protocol/src/protocol.rs` RolloutItem + RolloutLine)
|
|
13
|
+
* - `payload.id` and `payload.session_id` are the same ThreadId uuid
|
|
14
|
+
* (`SessionMeta { session_id: id.into(), id: thread_id, ... }`)
|
|
15
|
+
* - Hook stdin `session_id` is `ThreadId.to_string()` from `sess.session_id()`
|
|
16
|
+
* (`codex-rs/hooks/src/events/session_start.rs`,
|
|
17
|
+
* `codex-rs/core/src/hook_runtime.rs`,
|
|
18
|
+
* `codex-rs/hooks/schema/generated/session-start.command.input.schema.json`)
|
|
19
|
+
* Pin: https://github.com/openai/codex/tree/fb6aad9ae34116128e537696c24e35fe6548e1c2
|
|
20
|
+
*
|
|
21
|
+
* Subagent rollouts are skipped. Paths stay private to this module.
|
|
22
|
+
* Registration lives in `source-adapters.ts` (not the default).
|
|
23
|
+
*/
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.codexSourceAdapter = exports.CODEX_DISPLAY_NAME = exports.CODEX_SOURCE_ID = exports.CODEX_ADAPTER_VERSION = exports.CODEX_SOURCE_TYPE = void 0;
|
|
59
|
+
exports.codexSessionsDir = codexSessionsDir;
|
|
60
|
+
exports.validateCodexSessionId = validateCodexSessionId;
|
|
61
|
+
exports.detectCodexHarness = detectCodexHarness;
|
|
62
|
+
exports.discoverSessions = discoverSessions;
|
|
63
|
+
const fs = __importStar(require("node:fs"));
|
|
64
|
+
const os = __importStar(require("node:os"));
|
|
65
|
+
const path = __importStar(require("node:path"));
|
|
66
|
+
const config_1 = require("./config");
|
|
67
|
+
const transcript_bytes_1 = require("./transcript-bytes");
|
|
68
|
+
exports.CODEX_SOURCE_TYPE = "codex";
|
|
69
|
+
exports.CODEX_ADAPTER_VERSION = "codex-1";
|
|
70
|
+
exports.CODEX_SOURCE_ID = `src_${exports.CODEX_SOURCE_TYPE}`;
|
|
71
|
+
exports.CODEX_DISPLAY_NAME = "Codex";
|
|
72
|
+
/** Opaque safe identifiers (not UUID-bound; not permanently tied to thr_). */
|
|
73
|
+
const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
74
|
+
const HEAD_READ_BYTES = 256 * 1024;
|
|
75
|
+
/** Timestamp uses hyphens instead of colons: YYYY-MM-DDTHH-MM-SS. */
|
|
76
|
+
const ROLLOUT_RE = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([A-Za-z0-9][A-Za-z0-9_-]{0,127})\.jsonl$/;
|
|
77
|
+
function codexSessionsDir() {
|
|
78
|
+
return path.join(os.homedir(), ".codex", "sessions");
|
|
79
|
+
}
|
|
80
|
+
function validateCodexSessionId(value) {
|
|
81
|
+
return SESSION_ID_RE.test(value);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Deterministic harness-owned filesystem facts only. Never opens transcript
|
|
85
|
+
* files or walks session contents.
|
|
86
|
+
*/
|
|
87
|
+
function detectCodexHarness(home = os.homedir()) {
|
|
88
|
+
const codexDir = path.join(home, ".codex");
|
|
89
|
+
const evidence = [];
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.statSync(codexDir).isDirectory()) {
|
|
92
|
+
return { present: false, evidence };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { present: false, evidence };
|
|
97
|
+
}
|
|
98
|
+
evidence.push(codexDir);
|
|
99
|
+
const sessionsDir = path.join(codexDir, "sessions");
|
|
100
|
+
try {
|
|
101
|
+
if (fs.statSync(sessionsDir).isDirectory())
|
|
102
|
+
evidence.push(sessionsDir);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// optional
|
|
106
|
+
}
|
|
107
|
+
for (const name of ["config.toml", "hooks.json"]) {
|
|
108
|
+
const candidate = path.join(codexDir, name);
|
|
109
|
+
try {
|
|
110
|
+
if (fs.statSync(candidate).isFile())
|
|
111
|
+
evidence.push(candidate);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// ignore
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { present: true, evidence };
|
|
118
|
+
}
|
|
119
|
+
function readFirstRecord(filePath) {
|
|
120
|
+
let fd;
|
|
121
|
+
try {
|
|
122
|
+
fd = fs.openSync(filePath, "r");
|
|
123
|
+
const buf = Buffer.alloc(HEAD_READ_BYTES);
|
|
124
|
+
const read = fs.readSync(fd, buf, 0, HEAD_READ_BYTES, 0);
|
|
125
|
+
const head = buf.subarray(0, read);
|
|
126
|
+
const nl = head.indexOf(0x0a);
|
|
127
|
+
if (nl === -1)
|
|
128
|
+
return null;
|
|
129
|
+
const line = head.subarray(0, nl).toString("utf8").trim();
|
|
130
|
+
if (line === "")
|
|
131
|
+
return null;
|
|
132
|
+
return JSON.parse(line);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
if (fd !== undefined)
|
|
139
|
+
fs.closeSync(fd);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function isSubagentMeta(payload) {
|
|
143
|
+
if (typeof payload.parent_thread_id === "string" && payload.parent_thread_id.length > 0)
|
|
144
|
+
return true;
|
|
145
|
+
const source = payload.source;
|
|
146
|
+
if (source && typeof source === "object" && !Array.isArray(source) && "subagent" in source)
|
|
147
|
+
return true;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function underRoot(rootReal, candidateReal) {
|
|
151
|
+
const prefix = rootReal.endsWith(path.sep) ? rootReal : rootReal + path.sep;
|
|
152
|
+
return candidateReal === rootReal || candidateReal.startsWith(prefix);
|
|
153
|
+
}
|
|
154
|
+
function statSignals(filePath) {
|
|
155
|
+
const stat = fs.statSync(filePath);
|
|
156
|
+
const inode = stat.ino ? String(stat.ino) : null;
|
|
157
|
+
const birthtime = Number.isFinite(stat.birthtimeMs) && stat.birthtimeMs > 0 ? Math.floor(stat.birthtimeMs) : null;
|
|
158
|
+
const mtime = Number.isFinite(stat.mtimeMs) ? Math.floor(stat.mtimeMs) : null;
|
|
159
|
+
return { size: stat.size, inode, birthtime_ms: birthtime, mtime_ms: mtime };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Enumerate top-level Codex rollout files. Malformed, truncated, escaped,
|
|
163
|
+
* identity-mismatched, and subagent files are skipped (fail closed).
|
|
164
|
+
*
|
|
165
|
+
* Optional `sessionsDir` is Codex-private (tests / direct callers).
|
|
166
|
+
*/
|
|
167
|
+
function discoverSessions(sessionsDir = codexSessionsDir()) {
|
|
168
|
+
const sessions = [];
|
|
169
|
+
let rootReal;
|
|
170
|
+
try {
|
|
171
|
+
rootReal = (0, config_1.canonicalize)(sessionsDir);
|
|
172
|
+
if (!fs.statSync(rootReal).isDirectory())
|
|
173
|
+
return sessions;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return sessions;
|
|
177
|
+
}
|
|
178
|
+
let yearEntries;
|
|
179
|
+
try {
|
|
180
|
+
yearEntries = fs.readdirSync(rootReal, { withFileTypes: true });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return sessions;
|
|
184
|
+
}
|
|
185
|
+
for (const yearEntry of yearEntries) {
|
|
186
|
+
if (!yearEntry.isDirectory())
|
|
187
|
+
continue;
|
|
188
|
+
if (!/^\d{4}$/.test(yearEntry.name))
|
|
189
|
+
continue;
|
|
190
|
+
const yearDir = path.join(rootReal, yearEntry.name);
|
|
191
|
+
let monthEntries;
|
|
192
|
+
try {
|
|
193
|
+
monthEntries = fs.readdirSync(yearDir, { withFileTypes: true });
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
for (const monthEntry of monthEntries) {
|
|
199
|
+
if (!monthEntry.isDirectory())
|
|
200
|
+
continue;
|
|
201
|
+
if (!/^\d{2}$/.test(monthEntry.name))
|
|
202
|
+
continue;
|
|
203
|
+
const monthDir = path.join(yearDir, monthEntry.name);
|
|
204
|
+
let dayEntries;
|
|
205
|
+
try {
|
|
206
|
+
dayEntries = fs.readdirSync(monthDir, { withFileTypes: true });
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
for (const dayEntry of dayEntries) {
|
|
212
|
+
if (!dayEntry.isDirectory())
|
|
213
|
+
continue;
|
|
214
|
+
if (!/^\d{2}$/.test(dayEntry.name))
|
|
215
|
+
continue;
|
|
216
|
+
const dayDir = path.join(monthDir, dayEntry.name);
|
|
217
|
+
let files;
|
|
218
|
+
try {
|
|
219
|
+
files = fs.readdirSync(dayDir, { withFileTypes: true });
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
if (!file.isFile())
|
|
226
|
+
continue;
|
|
227
|
+
const match = ROLLOUT_RE.exec(file.name);
|
|
228
|
+
if (!match)
|
|
229
|
+
continue;
|
|
230
|
+
const filenameId = match[1];
|
|
231
|
+
if (!validateCodexSessionId(filenameId))
|
|
232
|
+
continue;
|
|
233
|
+
const filePath = path.join(dayDir, file.name);
|
|
234
|
+
try {
|
|
235
|
+
let realPath;
|
|
236
|
+
try {
|
|
237
|
+
realPath = (0, config_1.canonicalize)(filePath);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!underRoot(rootReal, realPath))
|
|
243
|
+
continue;
|
|
244
|
+
const record = readFirstRecord(filePath);
|
|
245
|
+
if (!record)
|
|
246
|
+
continue;
|
|
247
|
+
if (record.type !== "session_meta")
|
|
248
|
+
continue;
|
|
249
|
+
const payload = record.payload;
|
|
250
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
251
|
+
continue;
|
|
252
|
+
const meta = payload;
|
|
253
|
+
if (isSubagentMeta(meta))
|
|
254
|
+
continue;
|
|
255
|
+
// Prefer payload.id; accept payload.session_id when present (same ThreadId).
|
|
256
|
+
const sessionId = typeof meta.id === "string"
|
|
257
|
+
? meta.id
|
|
258
|
+
: typeof meta.session_id === "string"
|
|
259
|
+
? meta.session_id
|
|
260
|
+
: null;
|
|
261
|
+
if (!sessionId || sessionId !== filenameId)
|
|
262
|
+
continue;
|
|
263
|
+
if (typeof meta.session_id === "string" &&
|
|
264
|
+
typeof meta.id === "string" &&
|
|
265
|
+
meta.session_id !== meta.id) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (!validateCodexSessionId(sessionId))
|
|
269
|
+
continue;
|
|
270
|
+
let cwd = null;
|
|
271
|
+
if (typeof meta.cwd === "string") {
|
|
272
|
+
try {
|
|
273
|
+
cwd = (0, config_1.canonicalize)(meta.cwd);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Unresolvable cwd → discover but deny-by-default at policy gate.
|
|
277
|
+
cwd = null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const signals = statSignals(filePath);
|
|
281
|
+
sessions.push({
|
|
282
|
+
path: filePath,
|
|
283
|
+
real_path: realPath,
|
|
284
|
+
session_id: sessionId,
|
|
285
|
+
cwd,
|
|
286
|
+
size: signals.size,
|
|
287
|
+
inode: signals.inode,
|
|
288
|
+
birthtime_ms: signals.birthtime_ms,
|
|
289
|
+
mtime_ms: signals.mtime_ms,
|
|
290
|
+
first_chunk_fp: (0, transcript_bytes_1.firstChunkFingerprint)(filePath),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Fail closed for this candidate only — never abort the whole pass.
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
sessions.sort((a, b) => a.path.localeCompare(b.path));
|
|
302
|
+
return sessions;
|
|
303
|
+
}
|
|
304
|
+
exports.codexSourceAdapter = {
|
|
305
|
+
sourceType: exports.CODEX_SOURCE_TYPE,
|
|
306
|
+
sourceKey: exports.CODEX_SOURCE_TYPE,
|
|
307
|
+
sourceId: exports.CODEX_SOURCE_ID,
|
|
308
|
+
adapterVersion: exports.CODEX_ADAPTER_VERSION,
|
|
309
|
+
displayName: exports.CODEX_DISPLAY_NAME,
|
|
310
|
+
validateSessionId: validateCodexSessionId,
|
|
311
|
+
discoverSessions: () => discoverSessions(),
|
|
312
|
+
detect: () => detectCodexHarness(),
|
|
313
|
+
};
|