@letta-ai/dreams 0.0.1 → 0.0.4

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.
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * Claude Code live-sync hook helpers — merge Dreams SessionStart/Stop into
4
+ * ~/.claude/settings.json without clobbering unrelated handlers.
5
+ *
6
+ * SessionStart → `dreams wake` (housekeeping + pending drain)
7
+ * Stop → `dreams sync` (live turn, stdin session hint)
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.CLAUDE_HOOK_MARKER = void 0;
44
+ exports.claudeSettingsPath = claudeSettingsPath;
45
+ exports.buildClaudeHookCommandPosix = buildClaudeHookCommandPosix;
46
+ exports.buildClaudeHookCommandPowerShell = buildClaudeHookCommandPowerShell;
47
+ exports.buildClaudeHookCommand = buildClaudeHookCommand;
48
+ exports.mergeClaudeSettingsJson = mergeClaudeSettingsJson;
49
+ exports.planClaudeHooksInstall = planClaudeHooksInstall;
50
+ exports.installClaudeHooks = installClaudeHooks;
51
+ const fs = __importStar(require("node:fs"));
52
+ const os = __importStar(require("node:os"));
53
+ const path = __importStar(require("node:path"));
54
+ exports.CLAUDE_HOOK_MARKER = "dreams-sync-hook";
55
+ function claudeSettingsPath(home = os.homedir()) {
56
+ return path.join(home, ".claude", "settings.json");
57
+ }
58
+ /** POSIX bash command for Claude Code command hooks. */
59
+ function buildClaudeHookCommandPosix(cliPath, mode) {
60
+ return `"${cliPath}" ${mode} >/dev/null 2>&1 || true # ${exports.CLAUDE_HOOK_MARKER}`;
61
+ }
62
+ /** PowerShell command for Claude Code command hooks on Windows. */
63
+ function buildClaudeHookCommandPowerShell(cliPath, mode) {
64
+ const quoted = cliPath.replace(/'/g, "''");
65
+ return `& '${quoted}' ${mode} *> $null; exit 0 # ${exports.CLAUDE_HOOK_MARKER}`;
66
+ }
67
+ function buildClaudeHookCommand(cliPath, mode, platform = process.platform) {
68
+ return platform === "win32"
69
+ ? buildClaudeHookCommandPowerShell(cliPath, mode)
70
+ : buildClaudeHookCommandPosix(cliPath, mode);
71
+ }
72
+ function isDreamsClaudeHandler(command) {
73
+ if (typeof command !== "string")
74
+ return false;
75
+ // Prefer the marker; also recognize older Dreams sync-only handlers by cli+sync.
76
+ return (command.includes(exports.CLAUDE_HOOK_MARKER) ||
77
+ (/\bsync\b/.test(command) && (command.includes("dreams") || command.includes(">/dev/null") || command.includes("*> $null"))));
78
+ }
79
+ function upsertEventHandler(matchers, command, platform) {
80
+ const next = matchers.map((matcher) => ({ ...matcher, hooks: [...(matcher.hooks ?? [])] }));
81
+ let updated = false;
82
+ for (const matcher of next) {
83
+ const hooks = matcher.hooks ?? [];
84
+ for (let i = 0; i < hooks.length; i++) {
85
+ if (isDreamsClaudeHandler(hooks[i]?.command)) {
86
+ const handler = { ...hooks[i], type: "command", command };
87
+ if (platform === "win32")
88
+ handler.shell = "powershell";
89
+ else
90
+ delete handler.shell;
91
+ hooks[i] = handler;
92
+ updated = true;
93
+ }
94
+ }
95
+ matcher.hooks = hooks;
96
+ }
97
+ if (updated)
98
+ return next;
99
+ const handler = { type: "command", command };
100
+ if (platform === "win32")
101
+ handler.shell = "powershell";
102
+ return [...next, { hooks: [handler] }];
103
+ }
104
+ /**
105
+ * Merge Dreams Claude handlers into settings.json.
106
+ * SessionStart uses wake; Stop uses sync.
107
+ */
108
+ function mergeClaudeSettingsJson(existingRaw, cliPath, platform = process.platform) {
109
+ const wakeCmd = buildClaudeHookCommand(cliPath, "wake", platform);
110
+ const syncCmd = buildClaudeHookCommand(cliPath, "sync", platform);
111
+ let doc = {};
112
+ if (existingRaw !== null && existingRaw.trim() !== "") {
113
+ const parsed = JSON.parse(existingRaw);
114
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
115
+ throw new Error("claude settings.json must be a JSON object");
116
+ }
117
+ doc = { ...parsed };
118
+ }
119
+ const hooksValue = doc.hooks;
120
+ const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue)
121
+ ? { ...hooksValue }
122
+ : {};
123
+ const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
124
+ const stop = Array.isArray(hooks.Stop) ? hooks.Stop : [];
125
+ hooks.SessionStart = upsertEventHandler(sessionStart, wakeCmd, platform);
126
+ hooks.Stop = upsertEventHandler(stop, syncCmd, platform);
127
+ doc.hooks = hooks;
128
+ return `${JSON.stringify(doc, null, 2)}\n`;
129
+ }
130
+ function planClaudeHooksInstall(options) {
131
+ const home = options.home ?? os.homedir();
132
+ const platform = options.platform ?? process.platform;
133
+ const settingsPath = claudeSettingsPath(home);
134
+ let existing = null;
135
+ try {
136
+ existing = fs.readFileSync(settingsPath, "utf8");
137
+ }
138
+ catch {
139
+ existing = null;
140
+ }
141
+ const next = mergeClaudeSettingsJson(existing, options.cliPath, platform);
142
+ return {
143
+ settings_path: settingsPath,
144
+ changed: existing !== next,
145
+ session_start_command: buildClaudeHookCommand(options.cliPath, "wake", platform),
146
+ stop_command: buildClaudeHookCommand(options.cliPath, "sync", platform),
147
+ next_contents: next,
148
+ };
149
+ }
150
+ function installClaudeHooks(options) {
151
+ const plan = planClaudeHooksInstall(options);
152
+ if (plan.changed) {
153
+ fs.mkdirSync(path.dirname(plan.settings_path), { recursive: true });
154
+ const temporary = `${plan.settings_path}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
155
+ try {
156
+ fs.writeFileSync(temporary, plan.next_contents, { encoding: "utf8" });
157
+ fs.renameSync(temporary, plan.settings_path);
158
+ }
159
+ finally {
160
+ try {
161
+ fs.rmSync(temporary, { force: true });
162
+ }
163
+ catch {
164
+ // ignore
165
+ }
166
+ }
167
+ }
168
+ return plan;
169
+ }
@@ -1,17 +1,22 @@
1
1
  "use strict";
2
2
  /**
3
- * Claude Code source adapter — discovery, file identity, and byte reading.
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>/
7
7
  * - filename must be a UUID-like `<sessionId>.jsonl`
8
- * - the transcript's own `sessionId` must agree with the filename
8
+ * - a complete head record's `sessionId` must agree with the filename
9
+ * - `cwd` is taken from the first head record that carries a string `cwd`
10
+ * for that session (modern Claude transcripts often open with mode /
11
+ * ai-title / etc. before the first cwd-bearing record)
9
12
  * - never recurse; the `subagents/` directory is excluded outright
10
- * - `isSidechain: true` records are a secondary signal to exclude sidechains
13
+ * - `isSidechain: true` on any matching head record excludes the file
11
14
  * `parentUuid` is intentionally NOT used — ordinary main-session records also
12
15
  * carry parent relationships.
13
16
  *
14
- * This module never normalizes transcripts; it only reads raw bytes.
17
+ * This module never normalizes transcripts; it only reads raw bytes for
18
+ * discovery. Generic scan byte primitives live in `transcript-bytes.ts`.
19
+ * Registration lives in `source-adapters.ts`.
15
20
  */
16
21
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
22
  if (k2 === undefined) k2 = k;
@@ -47,203 +52,142 @@ var __importStar = (this && this.__importStar) || (function () {
47
52
  };
48
53
  })();
49
54
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.OversizedLineError = exports.IdentityChangedError = exports.CLAUDE_ADAPTER_VERSION = exports.CLAUDE_SOURCE_TYPE = void 0;
55
+ exports.claudeSourceAdapter = exports.CLAUDE_DISPLAY_NAME = exports.CLAUDE_SOURCE_ID = exports.CLAUDE_ADAPTER_VERSION = exports.CLAUDE_SOURCE_TYPE = void 0;
51
56
  exports.claudeProjectsDir = claudeProjectsDir;
52
- exports.firstChunkFingerprint = firstChunkFingerprint;
53
- exports.prefixFingerprint = prefixFingerprint;
54
- exports.scanCompleteLinesVerified = scanCompleteLinesVerified;
57
+ exports.validateClaudeSessionId = validateClaudeSessionId;
58
+ exports.detectClaudeCodeHarness = detectClaudeCodeHarness;
55
59
  exports.discoverSessions = discoverSessions;
56
- const crypto = __importStar(require("node:crypto"));
57
60
  const fs = __importStar(require("node:fs"));
58
61
  const os = __importStar(require("node:os"));
59
62
  const path = __importStar(require("node:path"));
60
63
  const config_1 = require("./config");
64
+ const transcript_bytes_1 = require("./transcript-bytes");
61
65
  exports.CLAUDE_SOURCE_TYPE = "claude-code";
62
66
  exports.CLAUDE_ADAPTER_VERSION = "claude-code-1";
67
+ exports.CLAUDE_SOURCE_ID = `src_${exports.CLAUDE_SOURCE_TYPE}`;
68
+ exports.CLAUDE_DISPLAY_NAME = "Claude Code";
63
69
  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
70
  const HEAD_READ_BYTES = 256 * 1024;
65
- const FIRST_CHUNK_BYTES = 4096;
66
71
  function claudeProjectsDir() {
67
72
  return path.join(os.homedir(), ".claude", "projects");
68
73
  }
69
- /** Thrown when a file's identity changes between discovery and read (TOCTOU / swap). */
70
- class IdentityChangedError extends Error {
71
- constructor(filePath) {
72
- super(`file identity changed under read: ${filePath}`);
73
- this.name = "IdentityChangedError";
74
- }
75
- }
76
- exports.IdentityChangedError = IdentityChangedError;
77
- function fingerprint(bytes) {
78
- return crypto.createHash("sha256").update(bytes).digest("hex");
79
- }
80
- /** Read the file head and return the first COMPLETE JSONL record, or null. */
81
- function readFirstRecord(filePath) {
82
- let fd;
83
- try {
84
- fd = fs.openSync(filePath, "r");
85
- const buf = Buffer.alloc(HEAD_READ_BYTES);
86
- const read = fs.readSync(fd, buf, 0, HEAD_READ_BYTES, 0);
87
- const head = buf.subarray(0, read);
88
- const nl = head.indexOf(0x0a);
89
- if (nl === -1)
90
- return null; // no complete first line yet
91
- const line = head.subarray(0, nl).toString("utf8").trim();
92
- if (line === "")
93
- return null;
94
- return JSON.parse(line);
95
- }
96
- catch {
97
- return null;
98
- }
99
- finally {
100
- if (fd !== undefined)
101
- fs.closeSync(fd);
102
- }
74
+ function validateClaudeSessionId(value) {
75
+ return UUID_RE.test(value);
103
76
  }
104
77
  /**
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.)
78
+ * Deterministic harness-owned filesystem facts only. Never opens transcript
79
+ * files or walks session contents. Paths stay private to this adapter.
110
80
  */
111
- function firstChunkFingerprint(filePath) {
112
- let fd;
81
+ function detectClaudeCodeHarness(home = os.homedir()) {
82
+ const claudeDir = path.join(home, ".claude");
83
+ const evidence = [];
113
84
  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);
85
+ if (!fs.statSync(claudeDir).isDirectory()) {
86
+ return { present: false, evidence };
87
+ }
121
88
  }
122
89
  catch {
123
- return "";
124
- }
125
- finally {
126
- if (fd !== undefined)
127
- fs.closeSync(fd);
90
+ return { present: false, evidence };
128
91
  }
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");
92
+ evidence.push(claudeDir);
93
+ const projectsDir = path.join(home, ".claude", "projects");
154
94
  try {
155
- const digest = crypto.createHash("sha256");
156
- hashRange(fd, length, digest, onProgress);
157
- return digest.digest("hex");
95
+ if (fs.statSync(projectsDir).isDirectory())
96
+ evidence.push(projectsDir);
158
97
  }
159
- finally {
160
- fs.closeSync(fd);
98
+ catch {
99
+ // projects/ is optional evidence; ~/.claude alone is enough.
161
100
  }
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";
101
+ for (const name of ["settings.json", "settings.local.json", "CLAUDE.md"]) {
102
+ const candidate = path.join(claudeDir, name);
103
+ try {
104
+ if (fs.statSync(candidate).isFile())
105
+ evidence.push(candidate);
106
+ }
107
+ catch {
108
+ // ignore
109
+ }
175
110
  }
111
+ return { present: true, evidence };
176
112
  }
177
- exports.OversizedLineError = OversizedLineError;
178
113
  /**
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.
114
+ * Read up to HEAD_READ_BYTES and derive session identity + cwd from complete
115
+ * JSONL records in that window. Modern Claude Code transcripts often open with
116
+ * metadata lines (mode, ai-title, …) that omit `cwd`; the first cwd-bearing
117
+ * record may appear several lines later (still well within the head window).
118
+ *
119
+ * Returns null when the head has no complete line, no matching sessionId, a
120
+ * mismatched sessionId, or is unreadable / malformed beyond recovery.
182
121
  */
183
- function scanCompleteLinesVerified(realPath, start, expectedInode, maxLineBytes, onLine, onProgress) {
122
+ function readHeadSessionFacts(filePath, expectedSessionId) {
184
123
  let fd;
185
124
  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;
125
+ fd = fs.openSync(filePath, "r");
126
+ const buf = Buffer.alloc(HEAD_READ_BYTES);
127
+ const read = fs.readSync(fd, buf, 0, HEAD_READ_BYTES, 0);
128
+ if (read === 0)
129
+ return null;
130
+ const head = buf.subarray(0, read);
131
+ // Only complete lines — a trailing partial record in the head is ignored.
132
+ const text = head.toString("utf8");
133
+ const lines = text.split("\n");
134
+ // If the buffer did not end on a newline, the final split piece is partial.
135
+ const completeLines = head[head.length - 1] === 0x0a ? lines : lines.slice(0, -1);
136
+ if (completeLines.length === 0)
137
+ return null;
138
+ let sawMatchingSession = false;
139
+ let isSidechain = false;
140
+ let cwd = null;
141
+ for (const raw of completeLines) {
142
+ const line = raw.trim();
143
+ if (line === "")
144
+ continue;
145
+ let record;
146
+ try {
147
+ const parsed = JSON.parse(line);
148
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
149
+ continue;
150
+ record = parsed;
223
151
  }
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);
152
+ catch {
153
+ continue;
154
+ }
155
+ const sessionId = typeof record.sessionId === "string" ? record.sessionId : null;
156
+ if (sessionId !== null) {
157
+ if (sessionId !== expectedSessionId) {
158
+ return null; // filename must agree with every sessionId in the head
159
+ }
160
+ sawMatchingSession = true;
161
+ if (record.isSidechain === true)
162
+ isSidechain = true;
163
+ }
164
+ // First cwd-bearing record wins. Modern transcripts put cwd on a later
165
+ // user/assistant line; some preamble lines omit sessionId entirely.
166
+ if (cwd === null && typeof record.cwd === "string" && record.cwd.length > 0) {
167
+ try {
168
+ cwd = (0, config_1.canonicalize)(record.cwd);
169
+ }
170
+ catch {
171
+ cwd = null;
172
+ }
228
173
  }
229
174
  }
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
- };
175
+ if (!sawMatchingSession)
176
+ return null;
177
+ return { sessionId: expectedSessionId, cwd, isSidechain };
178
+ }
179
+ catch {
180
+ return null;
243
181
  }
244
182
  finally {
245
- if (fd !== undefined)
246
- fs.closeSync(fd);
183
+ if (fd !== undefined) {
184
+ try {
185
+ fs.closeSync(fd);
186
+ }
187
+ catch {
188
+ // ignore
189
+ }
190
+ }
247
191
  }
248
192
  }
249
193
  function statSignals(filePath) {
@@ -256,6 +200,9 @@ function statSignals(filePath) {
256
200
  /**
257
201
  * Enumerate valid top-level Claude Code session files. Malformed, sidechain,
258
202
  * name-mismatched, and not-yet-complete files are skipped rather than failing.
203
+ *
204
+ * Optional `projectsDir` is Claude-private (tests / direct callers). The
205
+ * `SourceAdapter.discoverSessions` method does not expose this path override.
259
206
  */
260
207
  function discoverSessions(projectsDir = claudeProjectsDir()) {
261
208
  const sessions = [];
@@ -288,28 +235,35 @@ function discoverSessions(projectsDir = claudeProjectsDir()) {
288
235
  if (!UUID_RE.test(base))
289
236
  continue;
290
237
  const filePath = path.join(projectDir, file.name);
291
- const record = readFirstRecord(filePath);
292
- if (!record)
238
+ const facts = readHeadSessionFacts(filePath, base);
239
+ if (!facts)
293
240
  continue;
294
- if (record.isSidechain === true)
241
+ if (facts.isSidechain)
295
242
  continue;
296
- const sessionId = typeof record.sessionId === "string" ? record.sessionId : null;
297
- if (sessionId !== base)
298
- continue; // filename must agree with the transcript
299
243
  const signals = statSignals(filePath);
300
244
  sessions.push({
301
245
  path: filePath,
302
246
  real_path: (0, config_1.canonicalize)(filePath),
303
- session_id: sessionId,
304
- cwd: typeof record.cwd === "string" ? (0, config_1.canonicalize)(record.cwd) : null,
247
+ session_id: facts.sessionId,
248
+ cwd: facts.cwd,
305
249
  size: signals.size,
306
250
  inode: signals.inode,
307
251
  birthtime_ms: signals.birthtime_ms,
308
252
  mtime_ms: signals.mtime_ms,
309
- first_chunk_fp: firstChunkFingerprint(filePath),
253
+ first_chunk_fp: (0, transcript_bytes_1.firstChunkFingerprint)(filePath),
310
254
  });
311
255
  }
312
256
  }
313
257
  sessions.sort((a, b) => a.path.localeCompare(b.path));
314
258
  return sessions;
315
259
  }
260
+ exports.claudeSourceAdapter = {
261
+ sourceType: exports.CLAUDE_SOURCE_TYPE,
262
+ sourceKey: exports.CLAUDE_SOURCE_TYPE,
263
+ sourceId: exports.CLAUDE_SOURCE_ID,
264
+ adapterVersion: exports.CLAUDE_ADAPTER_VERSION,
265
+ displayName: exports.CLAUDE_DISPLAY_NAME,
266
+ validateSessionId: validateClaudeSessionId,
267
+ discoverSessions: () => discoverSessions(),
268
+ detect: () => detectClaudeCodeHarness(),
269
+ };