@letta-ai/dreams 0.0.1
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 +146 -0
- package/bin/dreams.cjs +19 -0
- package/dist/auth/browser.js +43 -0
- package/dist/auth/commands.js +141 -0
- package/dist/auth/credentials.js +222 -0
- package/dist/auth/index.js +65 -0
- package/dist/auth/lock.js +92 -0
- package/dist/auth/oauth-refresh.js +68 -0
- package/dist/auth/oauth.js +193 -0
- package/dist/cli.js +149 -0
- package/dist/cloud/client.js +223 -0
- package/dist/cloud/upload.js +384 -0
- package/dist/local/blobs.js +190 -0
- package/dist/local/claude.js +315 -0
- package/dist/local/commands.js +160 -0
- package/dist/local/config.js +126 -0
- package/dist/local/db.js +265 -0
- package/dist/local/dreamignore.js +124 -0
- package/dist/local/ids.js +78 -0
- package/dist/local/leases.js +117 -0
- package/dist/local/scan.js +475 -0
- package/dist/managed-install.js +200 -0
- package/dist/onboard.js +131 -0
- package/dist/skill.js +138 -0
- package/dist/store.js +133 -0
- package/dist/version.js +48 -0
- package/package.json +32 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Durable content-addressed storage for immutable queued upload payloads.
|
|
4
|
+
* Blob publication happens before the SQLite reference commit; reconciliation
|
|
5
|
+
* removes files that were published by an invocation that died before commit.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.blobsDir = blobsDir;
|
|
42
|
+
exports.blobPath = blobPath;
|
|
43
|
+
exports.hasBlob = hasBlob;
|
|
44
|
+
exports.readBlob = readBlob;
|
|
45
|
+
exports.writeBlob = writeBlob;
|
|
46
|
+
exports.deleteBlob = deleteBlob;
|
|
47
|
+
exports.pruneOrphanBlobs = pruneOrphanBlobs;
|
|
48
|
+
exports.ensureBlobsDir = ensureBlobsDir;
|
|
49
|
+
const crypto = __importStar(require("node:crypto"));
|
|
50
|
+
const fs = __importStar(require("node:fs"));
|
|
51
|
+
const path = __importStar(require("node:path"));
|
|
52
|
+
const store_1 = require("../store");
|
|
53
|
+
function blobsDir() {
|
|
54
|
+
return path.join((0, store_1.dreamsDir)(), "blobs");
|
|
55
|
+
}
|
|
56
|
+
function blobPath(sha256) {
|
|
57
|
+
return path.join(blobsDir(), sha256.slice(0, 2), sha256.slice(2, 4), sha256);
|
|
58
|
+
}
|
|
59
|
+
function hash(bytes) {
|
|
60
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
61
|
+
}
|
|
62
|
+
function fsyncDirectory(dir) {
|
|
63
|
+
if (process.platform === "win32")
|
|
64
|
+
return;
|
|
65
|
+
const fd = fs.openSync(dir, "r");
|
|
66
|
+
try {
|
|
67
|
+
fs.fsyncSync(fd);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
fs.closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function ensureBlobDirectory(dir) {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
75
|
+
if (process.platform === "win32")
|
|
76
|
+
return;
|
|
77
|
+
const stop = (0, store_1.dreamsDir)();
|
|
78
|
+
let current = dir;
|
|
79
|
+
while (true) {
|
|
80
|
+
fsyncDirectory(current);
|
|
81
|
+
if (current === stop)
|
|
82
|
+
break;
|
|
83
|
+
const parent = path.dirname(current);
|
|
84
|
+
if (parent === current)
|
|
85
|
+
break;
|
|
86
|
+
current = parent;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function verifyBlob(file, expected) {
|
|
90
|
+
const actual = hash(fs.readFileSync(file));
|
|
91
|
+
if (actual !== expected)
|
|
92
|
+
throw new Error(`blob hash mismatch at ${file} (expected ${expected}, got ${actual})`);
|
|
93
|
+
}
|
|
94
|
+
function hasBlob(sha256) {
|
|
95
|
+
return fs.existsSync(blobPath(sha256));
|
|
96
|
+
}
|
|
97
|
+
function readBlob(sha256) {
|
|
98
|
+
const file = blobPath(sha256);
|
|
99
|
+
const bytes = fs.readFileSync(file);
|
|
100
|
+
const actual = hash(bytes);
|
|
101
|
+
if (actual !== sha256)
|
|
102
|
+
throw new Error(`blob hash mismatch at ${file} (expected ${sha256}, got ${actual})`);
|
|
103
|
+
return bytes;
|
|
104
|
+
}
|
|
105
|
+
/** Persist and durably publish bytes before a queue row may reference them. */
|
|
106
|
+
function writeBlob(bytes) {
|
|
107
|
+
const sha256 = hash(bytes);
|
|
108
|
+
const finalPath = blobPath(sha256);
|
|
109
|
+
if (fs.existsSync(finalPath)) {
|
|
110
|
+
verifyBlob(finalPath, sha256);
|
|
111
|
+
return sha256;
|
|
112
|
+
}
|
|
113
|
+
const dir = path.dirname(finalPath);
|
|
114
|
+
ensureBlobDirectory(dir);
|
|
115
|
+
const tmp = path.join(dir, `.tmp-${process.pid}-${crypto.randomUUID()}`);
|
|
116
|
+
const fd = fs.openSync(tmp, "wx", 0o600);
|
|
117
|
+
try {
|
|
118
|
+
fs.writeFileSync(fd, bytes);
|
|
119
|
+
fs.fsyncSync(fd);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
fs.closeSync(fd);
|
|
123
|
+
}
|
|
124
|
+
verifyBlob(tmp, sha256);
|
|
125
|
+
try {
|
|
126
|
+
fs.renameSync(tmp, finalPath);
|
|
127
|
+
fsyncDirectory(dir);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
fs.rmSync(tmp, { force: true });
|
|
131
|
+
if (!fs.existsSync(finalPath))
|
|
132
|
+
throw error;
|
|
133
|
+
verifyBlob(finalPath, sha256);
|
|
134
|
+
}
|
|
135
|
+
return sha256;
|
|
136
|
+
}
|
|
137
|
+
function deleteBlob(sha256) {
|
|
138
|
+
const file = blobPath(sha256);
|
|
139
|
+
const dir = path.dirname(file);
|
|
140
|
+
fs.rmSync(file, { force: true });
|
|
141
|
+
if (fs.existsSync(dir))
|
|
142
|
+
fsyncDirectory(dir);
|
|
143
|
+
}
|
|
144
|
+
/** Remove unreferenced final blobs and temp files left by interrupted writers. */
|
|
145
|
+
function pruneOrphanBlobs(liveHashes) {
|
|
146
|
+
const root = blobsDir();
|
|
147
|
+
let removed = 0;
|
|
148
|
+
let firstLevel;
|
|
149
|
+
try {
|
|
150
|
+
firstLevel = fs.readdirSync(root, { withFileTypes: true });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
for (const first of firstLevel) {
|
|
156
|
+
if (!first.isDirectory())
|
|
157
|
+
continue;
|
|
158
|
+
const firstPath = path.join(root, first.name);
|
|
159
|
+
for (const second of fs.readdirSync(firstPath, { withFileTypes: true })) {
|
|
160
|
+
if (!second.isDirectory())
|
|
161
|
+
continue;
|
|
162
|
+
const dir = path.join(firstPath, second.name);
|
|
163
|
+
let removedFromDir = false;
|
|
164
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
165
|
+
if (!entry.isFile())
|
|
166
|
+
continue;
|
|
167
|
+
const file = path.join(dir, entry.name);
|
|
168
|
+
if (entry.name.startsWith(".tmp-") || (/^[0-9a-f]{64}$/.test(entry.name) && !liveHashes.has(entry.name))) {
|
|
169
|
+
fs.rmSync(file, { force: true });
|
|
170
|
+
removedFromDir = true;
|
|
171
|
+
removed++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (removedFromDir)
|
|
175
|
+
fsyncDirectory(dir);
|
|
176
|
+
if (fs.readdirSync(dir).length === 0) {
|
|
177
|
+
fs.rmdirSync(dir);
|
|
178
|
+
fsyncDirectory(firstPath);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (fs.existsSync(firstPath) && fs.readdirSync(firstPath).length === 0) {
|
|
182
|
+
fs.rmdirSync(firstPath);
|
|
183
|
+
fsyncDirectory(root);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return removed;
|
|
187
|
+
}
|
|
188
|
+
function ensureBlobsDir() {
|
|
189
|
+
ensureBlobDirectory(blobsDir());
|
|
190
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code source adapter — discovery, file identity, and byte reading.
|
|
4
|
+
*
|
|
5
|
+
* Discovery rule (filesystem shape is authoritative for the pilot):
|
|
6
|
+
* - only DIRECT children of ~/.claude/projects/<encoded-project>/
|
|
7
|
+
* - filename must be a UUID-like `<sessionId>.jsonl`
|
|
8
|
+
* - the transcript's own `sessionId` must agree with the filename
|
|
9
|
+
* - never recurse; the `subagents/` directory is excluded outright
|
|
10
|
+
* - `isSidechain: true` records are a secondary signal to exclude sidechains
|
|
11
|
+
* `parentUuid` is intentionally NOT used — ordinary main-session records also
|
|
12
|
+
* carry parent relationships.
|
|
13
|
+
*
|
|
14
|
+
* This module never normalizes transcripts; it only reads raw bytes.
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.OversizedLineError = exports.IdentityChangedError = exports.CLAUDE_ADAPTER_VERSION = exports.CLAUDE_SOURCE_TYPE = void 0;
|
|
51
|
+
exports.claudeProjectsDir = claudeProjectsDir;
|
|
52
|
+
exports.firstChunkFingerprint = firstChunkFingerprint;
|
|
53
|
+
exports.prefixFingerprint = prefixFingerprint;
|
|
54
|
+
exports.scanCompleteLinesVerified = scanCompleteLinesVerified;
|
|
55
|
+
exports.discoverSessions = discoverSessions;
|
|
56
|
+
const crypto = __importStar(require("node:crypto"));
|
|
57
|
+
const fs = __importStar(require("node:fs"));
|
|
58
|
+
const os = __importStar(require("node:os"));
|
|
59
|
+
const path = __importStar(require("node:path"));
|
|
60
|
+
const config_1 = require("./config");
|
|
61
|
+
exports.CLAUDE_SOURCE_TYPE = "claude-code";
|
|
62
|
+
exports.CLAUDE_ADAPTER_VERSION = "claude-code-1";
|
|
63
|
+
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
|
+
const HEAD_READ_BYTES = 256 * 1024;
|
|
65
|
+
const FIRST_CHUNK_BYTES = 4096;
|
|
66
|
+
function claudeProjectsDir() {
|
|
67
|
+
return path.join(os.homedir(), ".claude", "projects");
|
|
68
|
+
}
|
|
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
|
+
}
|
|
103
|
+
}
|
|
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
|
+
function statSignals(filePath) {
|
|
250
|
+
const stat = fs.statSync(filePath);
|
|
251
|
+
const inode = stat.ino ? String(stat.ino) : null;
|
|
252
|
+
const birthtime = Number.isFinite(stat.birthtimeMs) && stat.birthtimeMs > 0 ? Math.floor(stat.birthtimeMs) : null;
|
|
253
|
+
const mtime = Number.isFinite(stat.mtimeMs) ? Math.floor(stat.mtimeMs) : null;
|
|
254
|
+
return { size: stat.size, inode, birthtime_ms: birthtime, mtime_ms: mtime };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Enumerate valid top-level Claude Code session files. Malformed, sidechain,
|
|
258
|
+
* name-mismatched, and not-yet-complete files are skipped rather than failing.
|
|
259
|
+
*/
|
|
260
|
+
function discoverSessions(projectsDir = claudeProjectsDir()) {
|
|
261
|
+
const sessions = [];
|
|
262
|
+
let projectEntries;
|
|
263
|
+
try {
|
|
264
|
+
projectEntries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return sessions;
|
|
268
|
+
}
|
|
269
|
+
for (const projectEntry of projectEntries) {
|
|
270
|
+
if (!projectEntry.isDirectory())
|
|
271
|
+
continue;
|
|
272
|
+
if (projectEntry.name === "subagents")
|
|
273
|
+
continue;
|
|
274
|
+
const projectDir = path.join(projectsDir, projectEntry.name);
|
|
275
|
+
let files;
|
|
276
|
+
try {
|
|
277
|
+
files = fs.readdirSync(projectDir, { withFileTypes: true });
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
for (const file of files) {
|
|
283
|
+
if (!file.isFile())
|
|
284
|
+
continue;
|
|
285
|
+
if (!file.name.endsWith(".jsonl"))
|
|
286
|
+
continue;
|
|
287
|
+
const base = file.name.slice(0, -".jsonl".length);
|
|
288
|
+
if (!UUID_RE.test(base))
|
|
289
|
+
continue;
|
|
290
|
+
const filePath = path.join(projectDir, file.name);
|
|
291
|
+
const record = readFirstRecord(filePath);
|
|
292
|
+
if (!record)
|
|
293
|
+
continue;
|
|
294
|
+
if (record.isSidechain === true)
|
|
295
|
+
continue;
|
|
296
|
+
const sessionId = typeof record.sessionId === "string" ? record.sessionId : null;
|
|
297
|
+
if (sessionId !== base)
|
|
298
|
+
continue; // filename must agree with the transcript
|
|
299
|
+
const signals = statSignals(filePath);
|
|
300
|
+
sessions.push({
|
|
301
|
+
path: filePath,
|
|
302
|
+
real_path: (0, config_1.canonicalize)(filePath),
|
|
303
|
+
session_id: sessionId,
|
|
304
|
+
cwd: typeof record.cwd === "string" ? (0, config_1.canonicalize)(record.cwd) : null,
|
|
305
|
+
size: signals.size,
|
|
306
|
+
inode: signals.inode,
|
|
307
|
+
birthtime_ms: signals.birthtime_ms,
|
|
308
|
+
mtime_ms: signals.mtime_ms,
|
|
309
|
+
first_chunk_fp: firstChunkFingerprint(filePath),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
sessions.sort((a, b) => a.path.localeCompare(b.path));
|
|
314
|
+
return sessions;
|
|
315
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `dreams source ...` command handlers for the local source subsystem ([06]).
|
|
4
|
+
*
|
|
5
|
+
* These are the only entry points that open the local SQLite database, so the
|
|
6
|
+
* mocked onboarding flow (`init`/`status`) and auth keep working untouched.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.commandSourceScan = commandSourceScan;
|
|
10
|
+
exports.commandUpload = commandUpload;
|
|
11
|
+
exports.commandSourceApproveRoot = commandSourceApproveRoot;
|
|
12
|
+
exports.commandSourceList = commandSourceList;
|
|
13
|
+
exports.commandSource = commandSource;
|
|
14
|
+
const store_1 = require("../store");
|
|
15
|
+
const config_1 = require("./config");
|
|
16
|
+
const db_1 = require("./db");
|
|
17
|
+
const leases_1 = require("./leases");
|
|
18
|
+
const scan_1 = require("./scan");
|
|
19
|
+
const upload_1 = require("../cloud/upload");
|
|
20
|
+
function open() {
|
|
21
|
+
const installation = (0, store_1.loadOrCreateInstallation)();
|
|
22
|
+
const db = (0, db_1.openDb)({
|
|
23
|
+
installationId: installation.installation_id,
|
|
24
|
+
installationCreatedAt: installation.created_at,
|
|
25
|
+
});
|
|
26
|
+
return { db, installationId: installation.installation_id };
|
|
27
|
+
}
|
|
28
|
+
function emit(json, payload, human) {
|
|
29
|
+
if (json)
|
|
30
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
31
|
+
else
|
|
32
|
+
console.log(human());
|
|
33
|
+
}
|
|
34
|
+
/** Optional override for the per-segment byte cap (advanced/testing); ignored if not a positive integer. */
|
|
35
|
+
function maxSegmentBytesOverride() {
|
|
36
|
+
const raw = process.env.DREAMS_MAX_SEGMENT_BYTES;
|
|
37
|
+
if (!raw)
|
|
38
|
+
return undefined;
|
|
39
|
+
const value = Number(raw);
|
|
40
|
+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
function commandSourceScan(json) {
|
|
43
|
+
const { db, installationId } = open();
|
|
44
|
+
try {
|
|
45
|
+
const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
|
|
46
|
+
if (owner === null) {
|
|
47
|
+
emit(json, { status: "busy", reason: "another scan holds the local lease" }, () => "Another dreams scan is in progress; skipping (its work covers this invocation).");
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
let summary;
|
|
51
|
+
try {
|
|
52
|
+
summary = (0, scan_1.reconcile)(db, { installationId, leaseOwner: owner, maxSegmentBytes: maxSegmentBytesOverride() });
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
(0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
|
|
56
|
+
}
|
|
57
|
+
emit(json, { status: "ok", ...summary }, () => [
|
|
58
|
+
`Scan complete for source ${summary.source_id}`,
|
|
59
|
+
``,
|
|
60
|
+
` Sessions discovered: ${summary.sessions_discovered}`,
|
|
61
|
+
` Sessions queued: ${summary.sessions_queued}`,
|
|
62
|
+
` Sessions excluded: ${summary.sessions_excluded}`,
|
|
63
|
+
` Skipped (root unapproved): ${summary.sessions_skipped_unapproved}`,
|
|
64
|
+
` Failed (isolated): ${summary.sessions_failed}`,
|
|
65
|
+
` Generations reset: ${summary.generations_reset}`,
|
|
66
|
+
` Objects canceled (privacy): ${summary.objects_canceled}`,
|
|
67
|
+
` Segments enqueued: ${summary.segments_enqueued} (${summary.bytes_enqueued} bytes)`,
|
|
68
|
+
].join("\n"));
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
(0, db_1.closeDb)();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function commandUpload(json) {
|
|
76
|
+
const { db, installationId } = open();
|
|
77
|
+
try {
|
|
78
|
+
const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
|
|
79
|
+
if (owner === null) {
|
|
80
|
+
emit(json, { status: "busy", reason: "another source operation holds the local lease" }, () => "Another dreams source operation is in progress; skipping.");
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
let summary;
|
|
84
|
+
try {
|
|
85
|
+
summary = await (0, upload_1.runUpload)(db, { installationId, leaseOwner: owner });
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
(0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
|
|
89
|
+
}
|
|
90
|
+
emit(json, { status: summary.stopped_reason ? "error" : summary.partial ? "partial" : "ok", ...summary }, () => [
|
|
91
|
+
`Upload ${summary.stopped_reason ? "stopped" : summary.partial ? "completed with issues" : "complete"}`,
|
|
92
|
+
``,
|
|
93
|
+
` Due: ${summary.due}`,
|
|
94
|
+
` Accepted: ${summary.accepted}`,
|
|
95
|
+
` Duplicate: ${summary.duplicate}`,
|
|
96
|
+
` Quarantined: ${summary.quarantined}`,
|
|
97
|
+
` Retry scheduled: ${summary.retry_scheduled}`,
|
|
98
|
+
` Canceled (policy): ${summary.canceled_by_policy}`,
|
|
99
|
+
` Bytes uploaded: ${summary.bytes_uploaded}`,
|
|
100
|
+
summary.stopped_reason ? ` Stopped: ${summary.stopped_reason}` : ``,
|
|
101
|
+
].filter((line) => line !== ``).join("\n"));
|
|
102
|
+
// Partial failure (quarantines) or a hard stop reports a non-zero exit.
|
|
103
|
+
if (summary.stopped_reason)
|
|
104
|
+
return 1;
|
|
105
|
+
return summary.quarantined > 0 ? 2 : 0;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
(0, db_1.closeDb)();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function commandSourceApproveRoot(json, root) {
|
|
112
|
+
if (!root) {
|
|
113
|
+
process.stderr.write("Usage: dreams source approve-root <path>\n");
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
const { db } = open();
|
|
117
|
+
try {
|
|
118
|
+
const roots = (0, config_1.approveRoot)(db, root);
|
|
119
|
+
emit(json, { status: "ok", approved_roots: roots }, () => `Approved roots:\n${roots.map((r) => ` ${r}`).join("\n") || " (none)"}`);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
(0, db_1.closeDb)();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function commandSourceList(json) {
|
|
127
|
+
const { db } = open();
|
|
128
|
+
try {
|
|
129
|
+
const roots = (0, config_1.getApprovedRoots)(db);
|
|
130
|
+
const sources = db
|
|
131
|
+
.prepare("SELECT id, source_type, source_key, display_name, status FROM sources ORDER BY id")
|
|
132
|
+
.all();
|
|
133
|
+
const queue = (0, scan_1.listPendingQueue)(db);
|
|
134
|
+
emit(json, { status: "ok", db_path: (0, db_1.dbFilePath)(), approved_roots: roots, sources, pending_uploads: queue }, () => [
|
|
135
|
+
`Dream local sources`,
|
|
136
|
+
``,
|
|
137
|
+
` DB: ${(0, db_1.dbFilePath)()}`,
|
|
138
|
+
` Approved roots: ${roots.length ? roots.join(", ") : "(none — deny by default)"}`,
|
|
139
|
+
` Sources: ${sources.length}`,
|
|
140
|
+
` Pending uploads: ${queue.length}`,
|
|
141
|
+
].join("\n"));
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
(0, db_1.closeDb)();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function commandSource(subcommand, positional, json) {
|
|
149
|
+
switch (subcommand) {
|
|
150
|
+
case "scan":
|
|
151
|
+
return commandSourceScan(json);
|
|
152
|
+
case "approve-root":
|
|
153
|
+
return commandSourceApproveRoot(json, positional);
|
|
154
|
+
case "list":
|
|
155
|
+
return commandSourceList(json);
|
|
156
|
+
default:
|
|
157
|
+
process.stderr.write(`Unknown source subcommand: ${subcommand ?? "(none)"} — expected scan, approve-root, or list\n`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
}
|