@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.
- package/README.md +111 -35
- package/bin/dreams-codex-hook.cjs +91 -0
- package/bin/dreams.cjs +21 -0
- package/dist/auth/commands.js +67 -5
- package/dist/auth/credentials.js +101 -22
- package/dist/auth/index.js +106 -13
- package/dist/cli.js +239 -13
- 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 +625 -0
- package/dist/local/bindings.js +30 -0
- package/dist/local/claude-hooks.js +169 -0
- package/dist/local/claude.js +130 -176
- package/dist/local/codex-hooks.js +235 -0
- package/dist/local/codex.js +510 -0
- package/dist/local/commands.js +153 -19
- package/dist/local/db.js +68 -0
- package/dist/local/detect.js +148 -0
- package/dist/local/discovered.js +6 -0
- package/dist/local/hooks-install.js +102 -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 +71 -0
- package/dist/local/state-lock.js +88 -0
- package/dist/local/sync-control.js +432 -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 +373 -16
- package/dist/snapshots.js +210 -0
- package/dist/suppress-sqlite-warning.js +34 -0
- package/dist/sync.js +950 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/local/claude.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
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>/
|
|
7
7
|
* - filename must be a UUID-like `<sessionId>.jsonl`
|
|
8
|
-
* -
|
|
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`
|
|
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.
|
|
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.
|
|
53
|
-
exports.
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
*
|
|
106
|
-
*
|
|
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
|
|
112
|
-
|
|
81
|
+
function detectClaudeCodeHarness(home = os.homedir()) {
|
|
82
|
+
const claudeDir = path.join(home, ".claude");
|
|
83
|
+
const evidence = [];
|
|
113
84
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
return digest.digest("hex");
|
|
95
|
+
if (fs.statSync(projectsDir).isDirectory())
|
|
96
|
+
evidence.push(projectsDir);
|
|
158
97
|
}
|
|
159
|
-
|
|
160
|
-
|
|
98
|
+
catch {
|
|
99
|
+
// projects/ is optional evidence; ~/.claude alone is enough.
|
|
161
100
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
|
122
|
+
function readHeadSessionFacts(filePath, expectedSessionId) {
|
|
184
123
|
let fd;
|
|
185
124
|
try {
|
|
186
|
-
fd = fs.openSync(
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
|
292
|
-
if (!
|
|
238
|
+
const facts = readHeadSessionFacts(filePath, base);
|
|
239
|
+
if (!facts)
|
|
293
240
|
continue;
|
|
294
|
-
if (
|
|
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:
|
|
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
|
+
};
|