@dreb/coding-agent 2.5.2 → 2.6.0
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/CHANGELOG.md +1 -0
- package/README.md +1 -0
- package/dist/core/dream.d.ts +46 -0
- package/dist/core/dream.d.ts.map +1 -0
- package/dist/core/dream.js +587 -0
- package/dist/core/dream.js.map +1 -0
- package/dist/core/settings-manager.d.ts +5 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +17 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +8 -1
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +149 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { cp, readdir, stat } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import lockfile from "proper-lockfile";
|
|
9
|
+
import { getSessionsDir } from "../config.js";
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// =============================================================================
|
|
13
|
+
const DREAM_LAST_RUN_FILE = ".dream-last-run";
|
|
14
|
+
const DREAM_LOCK_FILE = ".dream.lock";
|
|
15
|
+
const DREAM_TMP_DIR = ".dream-tmp";
|
|
16
|
+
const BACKUP_PREFIX = "dream-backup-";
|
|
17
|
+
const DEFAULT_KEEP_COUNT = 10;
|
|
18
|
+
const LARGE_LISTING_THRESHOLD = 10_000;
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Command Parsing
|
|
21
|
+
// =============================================================================
|
|
22
|
+
export function parseDreamCommand(text) {
|
|
23
|
+
const stripped = text.replace(/^\/dream\s*/, "").trim();
|
|
24
|
+
if (!stripped) {
|
|
25
|
+
return { type: "run" };
|
|
26
|
+
}
|
|
27
|
+
if (stripped === "backup") {
|
|
28
|
+
return { type: "showBackup" };
|
|
29
|
+
}
|
|
30
|
+
const backupMatch = stripped.match(/^backup\s+(.+)$/);
|
|
31
|
+
if (backupMatch) {
|
|
32
|
+
return { type: "setBackup", path: backupMatch[1].trim() };
|
|
33
|
+
}
|
|
34
|
+
// Unknown subcommand — treat as a plain run
|
|
35
|
+
return { type: "run" };
|
|
36
|
+
}
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Discovery
|
|
39
|
+
// =============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Read the `cwd` field from the JSONL session header (first line) of a file.
|
|
42
|
+
* Returns `undefined` if the file cannot be read or the header is invalid.
|
|
43
|
+
* Uses synchronous low-level reads to avoid loading entire files.
|
|
44
|
+
*/
|
|
45
|
+
function readSessionCwd(filePath) {
|
|
46
|
+
let fd;
|
|
47
|
+
try {
|
|
48
|
+
fd = openSync(filePath, "r");
|
|
49
|
+
const buffer = Buffer.alloc(4096);
|
|
50
|
+
const bytesRead = readSync(fd, buffer, 0, 4096, 0);
|
|
51
|
+
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0];
|
|
52
|
+
if (!firstLine)
|
|
53
|
+
return undefined;
|
|
54
|
+
const header = JSON.parse(firstLine);
|
|
55
|
+
if (header.type === "session" && typeof header.cwd === "string") {
|
|
56
|
+
return header.cwd;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Corrupt or unreadable — skip
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (fd !== undefined) {
|
|
64
|
+
try {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Best-effort close
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
export function discoverAllProjectMemoryDirs(overrideSessionsDir) {
|
|
75
|
+
const sessionsDir = overrideSessionsDir ?? getSessionsDir();
|
|
76
|
+
if (!existsSync(sessionsDir))
|
|
77
|
+
return [];
|
|
78
|
+
let dirEntries;
|
|
79
|
+
try {
|
|
80
|
+
dirEntries = readdirSync(sessionsDir);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const memoryDirs = [];
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
for (const name of dirEntries) {
|
|
88
|
+
if (!name.startsWith("--") || !name.endsWith("--"))
|
|
89
|
+
continue;
|
|
90
|
+
const sessionSubDir = join(sessionsDir, name);
|
|
91
|
+
// Find any .jsonl file in this session directory
|
|
92
|
+
let jsonlFiles;
|
|
93
|
+
try {
|
|
94
|
+
jsonlFiles = readdirSync(sessionSubDir).filter((f) => f.endsWith(".jsonl"));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (jsonlFiles.length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
let cwd;
|
|
102
|
+
for (const f of jsonlFiles) {
|
|
103
|
+
cwd = readSessionCwd(join(sessionSubDir, f));
|
|
104
|
+
if (cwd)
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (!cwd)
|
|
108
|
+
continue;
|
|
109
|
+
const memDir = join(cwd, ".dreb", "memory");
|
|
110
|
+
if (!seen.has(memDir) && existsSync(memDir)) {
|
|
111
|
+
seen.add(memDir);
|
|
112
|
+
memoryDirs.push(memDir);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return memoryDirs;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Discover claude compatibility memory directories (read-only imports).
|
|
119
|
+
* Scans ~/.claude/projects/ for subdirectories containing a memory/ folder.
|
|
120
|
+
*/
|
|
121
|
+
function discoverClaudeMemoryDirs() {
|
|
122
|
+
const claudeProjectsDir = join(homedir(), ".claude", "projects");
|
|
123
|
+
if (!existsSync(claudeProjectsDir))
|
|
124
|
+
return [];
|
|
125
|
+
let dirEntries;
|
|
126
|
+
try {
|
|
127
|
+
dirEntries = readdirSync(claudeProjectsDir);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const memoryDirs = [];
|
|
133
|
+
for (const name of dirEntries) {
|
|
134
|
+
const memDir = join(claudeProjectsDir, name, "memory");
|
|
135
|
+
if (existsSync(memDir)) {
|
|
136
|
+
memoryDirs.push(memDir);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return memoryDirs;
|
|
140
|
+
}
|
|
141
|
+
// =============================================================================
|
|
142
|
+
// Context Resolution
|
|
143
|
+
// =============================================================================
|
|
144
|
+
export async function resolveDreamContext(settingsManager, overrideGlobalMemDir) {
|
|
145
|
+
// Archive path from settings, or default
|
|
146
|
+
const archivePath = settingsManager.getDreamArchivePath();
|
|
147
|
+
// Global memory dir
|
|
148
|
+
const globalMemoryDir = overrideGlobalMemDir ?? join(homedir(), ".dreb", "memory");
|
|
149
|
+
// Last run timestamp
|
|
150
|
+
let lastRunTimestamp = null;
|
|
151
|
+
const markerPath = join(globalMemoryDir, DREAM_LAST_RUN_FILE);
|
|
152
|
+
try {
|
|
153
|
+
if (existsSync(markerPath)) {
|
|
154
|
+
const content = readFileSync(markerPath, "utf-8").trim();
|
|
155
|
+
if (content) {
|
|
156
|
+
lastRunTimestamp = content;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Marker unreadable — treat as first run
|
|
162
|
+
}
|
|
163
|
+
// Discover memory directories
|
|
164
|
+
const projectMemoryDirs = discoverAllProjectMemoryDirs();
|
|
165
|
+
const claudeMemoryDirs = discoverClaudeMemoryDirs();
|
|
166
|
+
const sessionsDir = getSessionsDir();
|
|
167
|
+
return {
|
|
168
|
+
archivePath,
|
|
169
|
+
lastRunTimestamp,
|
|
170
|
+
globalMemoryDir,
|
|
171
|
+
projectMemoryDirs,
|
|
172
|
+
claudeMemoryDirs,
|
|
173
|
+
sessionsDir,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Validation
|
|
178
|
+
// =============================================================================
|
|
179
|
+
export function validateArchivePath(archivePath, memoryDirs) {
|
|
180
|
+
if (!archivePath || !archivePath.trim()) {
|
|
181
|
+
throw new Error("Dream archive path cannot be empty");
|
|
182
|
+
}
|
|
183
|
+
if (!isAbsolute(archivePath)) {
|
|
184
|
+
throw new Error(`Dream archive path must be absolute, got: ${archivePath}`);
|
|
185
|
+
}
|
|
186
|
+
const normalized = resolve(archivePath);
|
|
187
|
+
for (const memDir of memoryDirs) {
|
|
188
|
+
const normalizedMem = resolve(memDir);
|
|
189
|
+
// Archive is inside a memory dir
|
|
190
|
+
if (normalized.startsWith(`${normalizedMem}/`) || normalized === normalizedMem) {
|
|
191
|
+
throw new Error(`Dream archive path "${archivePath}" overlaps with memory directory "${memDir}". ` +
|
|
192
|
+
"The archive must be outside all memory directories.");
|
|
193
|
+
}
|
|
194
|
+
// Memory dir is inside the archive
|
|
195
|
+
if (normalizedMem.startsWith(`${normalized}/`) || normalizedMem === normalized) {
|
|
196
|
+
throw new Error(`Memory directory "${memDir}" is inside the dream archive path "${archivePath}". ` +
|
|
197
|
+
"The archive must be outside all memory directories.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export function validateMemoryLinks(memoryDirs) {
|
|
202
|
+
const brokenLinks = [];
|
|
203
|
+
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
204
|
+
for (const memDir of memoryDirs) {
|
|
205
|
+
const indexFile = join(memDir, "MEMORY.md");
|
|
206
|
+
if (!existsSync(indexFile))
|
|
207
|
+
continue;
|
|
208
|
+
let content;
|
|
209
|
+
try {
|
|
210
|
+
content = readFileSync(indexFile, "utf-8");
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
for (const match of content.matchAll(linkPattern)) {
|
|
216
|
+
const pointer = match[0];
|
|
217
|
+
const target = match[2];
|
|
218
|
+
// Skip external URLs
|
|
219
|
+
if (target.startsWith("http://") || target.startsWith("https://"))
|
|
220
|
+
continue;
|
|
221
|
+
const targetPath = join(memDir, target);
|
|
222
|
+
if (!existsSync(targetPath)) {
|
|
223
|
+
brokenLinks.push({ memoryDir: memDir, indexFile, pointer, target });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { valid: brokenLinks.length === 0, brokenLinks };
|
|
228
|
+
}
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// Backup
|
|
231
|
+
// =============================================================================
|
|
232
|
+
/** Convert a filesystem path to a collision-resistant directory name for backup staging. */
|
|
233
|
+
export function safeDirName(path) {
|
|
234
|
+
return encodeURIComponent(path).replace(/%2F/gi, "_");
|
|
235
|
+
}
|
|
236
|
+
/** Recursively count files and total size in a directory. */
|
|
237
|
+
async function countFilesAndSize(dir) {
|
|
238
|
+
let fileCount = 0;
|
|
239
|
+
let totalSize = 0;
|
|
240
|
+
async function walk(current) {
|
|
241
|
+
let entries;
|
|
242
|
+
try {
|
|
243
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
const fullPath = join(current, entry.name);
|
|
250
|
+
if (entry.isDirectory()) {
|
|
251
|
+
await walk(fullPath);
|
|
252
|
+
}
|
|
253
|
+
else if (entry.isFile()) {
|
|
254
|
+
try {
|
|
255
|
+
const st = await stat(fullPath);
|
|
256
|
+
fileCount++;
|
|
257
|
+
totalSize += st.size;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// File disappeared between readdir and stat — skip
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
await walk(dir);
|
|
266
|
+
return { fileCount, totalSize };
|
|
267
|
+
}
|
|
268
|
+
export async function performDreamBackup(context) {
|
|
269
|
+
const timestamp = `${new Date().toISOString().replace(/[:.]/g, "-")}_${randomUUID().slice(0, 8)}`;
|
|
270
|
+
const archiveName = `${BACKUP_PREFIX}${timestamp}`;
|
|
271
|
+
const backupPath = join(context.archivePath, `${archiveName}.tar.gz`);
|
|
272
|
+
const stagingDir = join(context.archivePath, `.staging-${timestamp}`);
|
|
273
|
+
// Ensure archive directory exists
|
|
274
|
+
try {
|
|
275
|
+
mkdirSync(context.archivePath, { recursive: true });
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
throw new Error(`Failed to create archive directory "${context.archivePath}": ${error instanceof Error ? error.message : String(error)}`);
|
|
279
|
+
}
|
|
280
|
+
// Build list of source directories
|
|
281
|
+
const allSourceDirs = [];
|
|
282
|
+
// Global memory
|
|
283
|
+
if (existsSync(context.globalMemoryDir)) {
|
|
284
|
+
allSourceDirs.push({ dir: context.globalMemoryDir, stagingTarget: join(stagingDir, "global") });
|
|
285
|
+
}
|
|
286
|
+
// Project memories
|
|
287
|
+
for (const dir of context.projectMemoryDirs) {
|
|
288
|
+
if (existsSync(dir)) {
|
|
289
|
+
allSourceDirs.push({
|
|
290
|
+
dir,
|
|
291
|
+
stagingTarget: join(stagingDir, "projects", safeDirName(dir)),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Claude compat memories
|
|
296
|
+
for (const dir of context.claudeMemoryDirs) {
|
|
297
|
+
if (existsSync(dir)) {
|
|
298
|
+
allSourceDirs.push({
|
|
299
|
+
dir,
|
|
300
|
+
stagingTarget: join(stagingDir, "claude", safeDirName(dir)),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Count source files before copying
|
|
305
|
+
let sourceFileCount = 0;
|
|
306
|
+
let sourceTotalSize = 0;
|
|
307
|
+
for (const { dir } of allSourceDirs) {
|
|
308
|
+
const counts = await countFilesAndSize(dir);
|
|
309
|
+
sourceFileCount += counts.fileCount;
|
|
310
|
+
sourceTotalSize += counts.totalSize;
|
|
311
|
+
}
|
|
312
|
+
// Copy source directories into staging area
|
|
313
|
+
try {
|
|
314
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
315
|
+
for (const { dir, stagingTarget } of allSourceDirs) {
|
|
316
|
+
try {
|
|
317
|
+
await cp(dir, stagingTarget, { recursive: true });
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
throw new Error(`Failed to copy "${dir}" to "${stagingTarget}": ${error instanceof Error ? error.message : String(error)}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Create .tar.gz archive from staging directory
|
|
324
|
+
const execFileAsync = promisify(execFile);
|
|
325
|
+
await execFileAsync("tar", ["czf", backupPath, "-C", dirname(stagingDir), basename(stagingDir)]);
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
// Clean up staging directory
|
|
329
|
+
try {
|
|
330
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Best-effort cleanup
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Verify: archive exists and has non-zero size
|
|
337
|
+
let verified = false;
|
|
338
|
+
try {
|
|
339
|
+
const archiveStat = statSync(backupPath);
|
|
340
|
+
verified = archiveStat.size > 0;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// Archive missing or unreadable
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
backupPath,
|
|
347
|
+
timestamp,
|
|
348
|
+
fileCount: sourceFileCount,
|
|
349
|
+
totalSize: sourceTotalSize,
|
|
350
|
+
verified,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// =============================================================================
|
|
354
|
+
// Prompt Building
|
|
355
|
+
// =============================================================================
|
|
356
|
+
function formatBytes(bytes) {
|
|
357
|
+
if (bytes < 1024)
|
|
358
|
+
return `${bytes} bytes`;
|
|
359
|
+
if (bytes < 1024 * 1024)
|
|
360
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
361
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
362
|
+
}
|
|
363
|
+
export function buildDreamPrompt(context, backupResult) {
|
|
364
|
+
const verifiedStatus = backupResult.verified ? "✓ Verified" : "⚠ Verification mismatch — check backup integrity";
|
|
365
|
+
const lastRun = context.lastRunTimestamp ?? "Never (first run)";
|
|
366
|
+
// Build the file listing section
|
|
367
|
+
let fileListing = "";
|
|
368
|
+
const allDirs = [
|
|
369
|
+
{ label: "Global", dir: context.globalMemoryDir },
|
|
370
|
+
...context.projectMemoryDirs.map((d) => ({ label: `Project: ${d}`, dir: d })),
|
|
371
|
+
...context.claudeMemoryDirs.map((d) => ({ label: `Claude (READ-ONLY): ${d}`, dir: d })),
|
|
372
|
+
];
|
|
373
|
+
for (const { label, dir } of allDirs) {
|
|
374
|
+
if (!existsSync(dir))
|
|
375
|
+
continue;
|
|
376
|
+
fileListing += `\n### ${label}\n`;
|
|
377
|
+
try {
|
|
378
|
+
const entries = readdirSync(dir);
|
|
379
|
+
for (const entry of entries) {
|
|
380
|
+
if (entry === DREAM_TMP_DIR ||
|
|
381
|
+
entry === DREAM_LOCK_FILE ||
|
|
382
|
+
entry === `${DREAM_LOCK_FILE}.lock` ||
|
|
383
|
+
entry === DREAM_LAST_RUN_FILE)
|
|
384
|
+
continue;
|
|
385
|
+
fileListing += `- ${entry}\n`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
fileListing += `- (unreadable)\n`;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// If file listing is too large, spill to a temp file
|
|
393
|
+
let fileListingSection;
|
|
394
|
+
if (fileListing.length > LARGE_LISTING_THRESHOLD) {
|
|
395
|
+
const tmpDir = join(context.globalMemoryDir, DREAM_TMP_DIR);
|
|
396
|
+
try {
|
|
397
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Best-effort
|
|
401
|
+
}
|
|
402
|
+
const tmpPath = join(tmpDir, `dream-listing-${Date.now()}.md`);
|
|
403
|
+
try {
|
|
404
|
+
writeFileSync(tmpPath, fileListing, "utf-8");
|
|
405
|
+
fileListingSection =
|
|
406
|
+
`File listing too large to include inline. Full listing written to: ${tmpPath}\n` +
|
|
407
|
+
"Read this file for the complete list of memory files.";
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Fallback: include inline anyway
|
|
411
|
+
fileListingSection = fileListing;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
fileListingSection = fileListing;
|
|
416
|
+
}
|
|
417
|
+
const projectDirsList = context.projectMemoryDirs.length > 0
|
|
418
|
+
? context.projectMemoryDirs.map((d) => ` - ${d}`).join("\n")
|
|
419
|
+
: " (none discovered)";
|
|
420
|
+
const claudeDirsList = context.claudeMemoryDirs.length > 0
|
|
421
|
+
? context.claudeMemoryDirs.map((d) => ` - ${d}`).join("\n")
|
|
422
|
+
: " (none discovered)";
|
|
423
|
+
const dreamTmpDirName = DREAM_TMP_DIR;
|
|
424
|
+
const dreamLastRunPath = join(context.globalMemoryDir, DREAM_LAST_RUN_FILE);
|
|
425
|
+
return `You are running a memory consolidation (/dream). A backup archive has been created at: ${backupResult.backupPath}
|
|
426
|
+
Backup verification: ${backupResult.fileCount} files, ${formatBytes(backupResult.totalSize)} — ${verifiedStatus}
|
|
427
|
+
|
|
428
|
+
## Context
|
|
429
|
+
- Global memory: ${context.globalMemoryDir}
|
|
430
|
+
- Project memories:
|
|
431
|
+
${projectDirsList}
|
|
432
|
+
- Claude Code memories (READ-ONLY):
|
|
433
|
+
${claudeDirsList}
|
|
434
|
+
- Last dream run: ${lastRun}
|
|
435
|
+
- Sessions directory: ${context.sessionsDir}
|
|
436
|
+
|
|
437
|
+
## Memory Files
|
|
438
|
+
${fileListingSection}
|
|
439
|
+
|
|
440
|
+
## HARD CONSTRAINTS
|
|
441
|
+
- The \`.claude/\` memory directories are read-only compatibility imports. Do NOT modify, delete, or rewrite any files under \`.claude/\` paths. Only \`~/.dreb/memory/\` and \`<project>/.dreb/memory/\` are writable.
|
|
442
|
+
- NEVER remove session JSONL data files.
|
|
443
|
+
- NEVER delete or modify \`.dream.lock\`, \`.dream.lock.lock\`, or \`.dream-last-run\` files — these are managed by the dream infrastructure, not by you.
|
|
444
|
+
- Explicitly EXCLUDE \`subagent-sessions/\` from scanning scope.
|
|
445
|
+
|
|
446
|
+
## Pipeline
|
|
447
|
+
|
|
448
|
+
### Step 0: Verify Backup
|
|
449
|
+
Before proceeding, verify the backup archive exists and is intact:
|
|
450
|
+
1. Check that the backup file exists at the path shown above
|
|
451
|
+
2. List its contents (e.g., \`tar tzf <path> | head -20\`) to confirm memory files are present
|
|
452
|
+
3. If the backup appears incomplete or missing, STOP and inform the user — do not proceed with consolidation
|
|
453
|
+
|
|
454
|
+
### Step 1: Read All Memories
|
|
455
|
+
Read every MEMORY.md index and every referenced memory file from global, all project scopes, and \`.claude/\` read-only paths.
|
|
456
|
+
|
|
457
|
+
### Step 2: Analyze & Plan
|
|
458
|
+
Group related entries, identify duplicates, overlapping content, stale references (deleted files, resolved issues). Present the consolidation plan to the user before making any changes.
|
|
459
|
+
|
|
460
|
+
### Step 3: Consolidate
|
|
461
|
+
Merge related entries, deduplicate, reorganize semantically. Write changes to temp files first (\`<memory-dir>/${dreamTmpDirName}/\`), then atomic rename per file. Maintain a rollback manifest listing every original→tmp→final path.
|
|
462
|
+
|
|
463
|
+
### Step 4: Rewrite Indexes
|
|
464
|
+
Produce new MEMORY.md indexes organized semantically, under 200 lines. Every pointer must reference an existing file. Write to temp first, then rename.
|
|
465
|
+
|
|
466
|
+
### Step 5: Remove Dead Files
|
|
467
|
+
Only after indexes are rewritten and validated, delete memory files that have ZERO remaining references in any MEMORY.md index. Never delete a file that is still referenced.
|
|
468
|
+
|
|
469
|
+
### Step 6: Scan Sessions
|
|
470
|
+
Spawn background Explore subagents to read session JSONL logs from ${context.sessionsDir} (EXCLUDE subagent-sessions/). Only scan sessions since ${lastRun}. First-run cap: 30 days maximum.
|
|
471
|
+
|
|
472
|
+
Session JSONL format: Each line is a JSON object. Relevant entry types:
|
|
473
|
+
- \`{"type":"message","message":{"role":"user"|"assistant","content":...}}\` — conversation messages
|
|
474
|
+
- \`{"type":"tool_use","name":"...","input":{...}}\` — tool calls
|
|
475
|
+
- \`{"type":"tool_result","content":...}\` — tool outputs
|
|
476
|
+
|
|
477
|
+
Each subagent should return findings in structured format:
|
|
478
|
+
\`{"findings": [{"type": "user-preferences|good-practices|project|navigation", "name": "...", "description": "...", "content": "..."}]}\`
|
|
479
|
+
|
|
480
|
+
### Step 7: STOP AND WAIT
|
|
481
|
+
After spawning subagents, stop generating and wait for ALL background-agent-complete messages before proceeding. Do not continue to Step 8 until every subagent has reported back.
|
|
482
|
+
|
|
483
|
+
### Step 8: Incorporate Findings
|
|
484
|
+
Create new memory entries from subagent findings, update MEMORY.md indexes.
|
|
485
|
+
|
|
486
|
+
### Step 9: Report
|
|
487
|
+
Structured summary: X merged, Y pruned, Z added from sessions, W files removed, backup location, any warnings.
|
|
488
|
+
|
|
489
|
+
### Step 10: Mark Complete
|
|
490
|
+
Write the current ISO timestamp to ${dreamLastRunPath}
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
493
|
+
// =============================================================================
|
|
494
|
+
// Locking
|
|
495
|
+
// =============================================================================
|
|
496
|
+
export async function acquireDreamLock(overrideMemDir) {
|
|
497
|
+
const memDir = overrideMemDir ?? join(homedir(), ".dreb", "memory");
|
|
498
|
+
const lockPath = join(memDir, DREAM_LOCK_FILE);
|
|
499
|
+
// Ensure directory and lock file exist (proper-lockfile requires the file to exist)
|
|
500
|
+
try {
|
|
501
|
+
mkdirSync(memDir, { recursive: true });
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Directory already exists
|
|
505
|
+
}
|
|
506
|
+
if (!existsSync(lockPath)) {
|
|
507
|
+
try {
|
|
508
|
+
writeFileSync(lockPath, "", "utf-8");
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
throw new Error(`Failed to create dream lock file "${lockPath}": ${error instanceof Error ? error.message : String(error)}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
let release;
|
|
515
|
+
try {
|
|
516
|
+
release = await lockfile.lock(lockPath, {
|
|
517
|
+
stale: 60000,
|
|
518
|
+
retries: {
|
|
519
|
+
retries: 5,
|
|
520
|
+
factor: 2,
|
|
521
|
+
minTimeout: 200,
|
|
522
|
+
maxTimeout: 5000,
|
|
523
|
+
randomize: true,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
529
|
+
? String(error.code)
|
|
530
|
+
: undefined;
|
|
531
|
+
if (code === "ELOCKED") {
|
|
532
|
+
throw new Error("Another /dream operation is already running. " +
|
|
533
|
+
"If this is stale, wait 60 seconds or manually remove " +
|
|
534
|
+
`the lock: ${lockPath}.lock`);
|
|
535
|
+
}
|
|
536
|
+
throw new Error(`Failed to acquire dream lock: ${error instanceof Error ? error.message : String(error)}`);
|
|
537
|
+
}
|
|
538
|
+
return () => {
|
|
539
|
+
release().catch(() => {
|
|
540
|
+
// Best-effort unlock — if it fails the stale timeout will clean up
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
// =============================================================================
|
|
545
|
+
// Backup Pruning
|
|
546
|
+
// =============================================================================
|
|
547
|
+
export async function pruneOldBackups(archivePath, keepCount = DEFAULT_KEEP_COUNT) {
|
|
548
|
+
if (!existsSync(archivePath))
|
|
549
|
+
return;
|
|
550
|
+
let dirEntries;
|
|
551
|
+
try {
|
|
552
|
+
dirEntries = readdirSync(archivePath);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const backupFiles = dirEntries.filter((name) => name.startsWith(BACKUP_PREFIX) && name.endsWith(".tar.gz")).sort();
|
|
558
|
+
if (backupFiles.length <= keepCount)
|
|
559
|
+
return;
|
|
560
|
+
const toRemove = backupFiles.slice(0, backupFiles.length - keepCount);
|
|
561
|
+
for (const fileName of toRemove) {
|
|
562
|
+
const fullPath = join(archivePath, fileName);
|
|
563
|
+
try {
|
|
564
|
+
unlinkSync(fullPath);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
// Best-effort — log would be nice but we don't have a logger here
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// =============================================================================
|
|
572
|
+
// Temp Dir Cleanup
|
|
573
|
+
// =============================================================================
|
|
574
|
+
export function cleanupDreamTmpDirs(memoryDirs) {
|
|
575
|
+
for (const memDir of memoryDirs) {
|
|
576
|
+
const tmpDir = join(memDir, DREAM_TMP_DIR);
|
|
577
|
+
if (existsSync(tmpDir)) {
|
|
578
|
+
try {
|
|
579
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// Best-effort cleanup
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
//# sourceMappingURL=dream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dream.js","sourceRoot":"","sources":["../../src/core/dream.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treaddirSync,\n\treadFileSync,\n\treadSync,\n\trmSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { cp, readdir, stat } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport lockfile from \"proper-lockfile\";\nimport { getSessionsDir } from \"../config.js\";\nimport type { SettingsManager } from \"./settings-manager.js\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface DreamContext {\n\tarchivePath: string;\n\tlastRunTimestamp: string | null;\n\tglobalMemoryDir: string;\n\tprojectMemoryDirs: string[];\n\tclaudeMemoryDirs: string[];\n\tsessionsDir: string;\n}\n\nexport interface BackupResult {\n\tbackupPath: string;\n\ttimestamp: string;\n\tfileCount: number;\n\ttotalSize: number;\n\tverified: boolean;\n}\n\nexport interface LinkValidationResult {\n\tvalid: boolean;\n\tbrokenLinks: Array<{ memoryDir: string; indexFile: string; pointer: string; target: string }>;\n}\n\nexport type DreamCommand = { type: \"run\" } | { type: \"setBackup\"; path: string } | { type: \"showBackup\" };\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DREAM_LAST_RUN_FILE = \".dream-last-run\";\nconst DREAM_LOCK_FILE = \".dream.lock\";\nconst DREAM_TMP_DIR = \".dream-tmp\";\nconst BACKUP_PREFIX = \"dream-backup-\";\nconst DEFAULT_KEEP_COUNT = 10;\nconst LARGE_LISTING_THRESHOLD = 10_000;\n\n// =============================================================================\n// Command Parsing\n// =============================================================================\n\nexport function parseDreamCommand(text: string): DreamCommand {\n\tconst stripped = text.replace(/^\\/dream\\s*/, \"\").trim();\n\n\tif (!stripped) {\n\t\treturn { type: \"run\" };\n\t}\n\n\tif (stripped === \"backup\") {\n\t\treturn { type: \"showBackup\" };\n\t}\n\n\tconst backupMatch = stripped.match(/^backup\\s+(.+)$/);\n\tif (backupMatch) {\n\t\treturn { type: \"setBackup\", path: backupMatch[1].trim() };\n\t}\n\n\t// Unknown subcommand — treat as a plain run\n\treturn { type: \"run\" };\n}\n\n// =============================================================================\n// Discovery\n// =============================================================================\n\n/**\n * Read the `cwd` field from the JSONL session header (first line) of a file.\n * Returns `undefined` if the file cannot be read or the header is invalid.\n * Uses synchronous low-level reads to avoid loading entire files.\n */\nfunction readSessionCwd(filePath: string): string | undefined {\n\tlet fd: number | undefined;\n\ttry {\n\t\tfd = openSync(filePath, \"r\");\n\t\tconst buffer = Buffer.alloc(4096);\n\t\tconst bytesRead = readSync(fd, buffer, 0, 4096, 0);\n\t\tconst firstLine = buffer.toString(\"utf8\", 0, bytesRead).split(\"\\n\")[0];\n\t\tif (!firstLine) return undefined;\n\t\tconst header = JSON.parse(firstLine);\n\t\tif (header.type === \"session\" && typeof header.cwd === \"string\") {\n\t\t\treturn header.cwd;\n\t\t}\n\t} catch {\n\t\t// Corrupt or unreadable — skip\n\t} finally {\n\t\tif (fd !== undefined) {\n\t\t\ttry {\n\t\t\t\tcloseSync(fd);\n\t\t\t} catch {\n\t\t\t\t// Best-effort close\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport function discoverAllProjectMemoryDirs(overrideSessionsDir?: string): string[] {\n\tconst sessionsDir = overrideSessionsDir ?? getSessionsDir();\n\tif (!existsSync(sessionsDir)) return [];\n\n\tlet dirEntries: string[];\n\ttry {\n\t\tdirEntries = readdirSync(sessionsDir);\n\t} catch {\n\t\treturn [];\n\t}\n\n\tconst memoryDirs: string[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const name of dirEntries) {\n\t\tif (!name.startsWith(\"--\") || !name.endsWith(\"--\")) continue;\n\n\t\tconst sessionSubDir = join(sessionsDir, name);\n\n\t\t// Find any .jsonl file in this session directory\n\t\tlet jsonlFiles: string[];\n\t\ttry {\n\t\t\tjsonlFiles = readdirSync(sessionSubDir).filter((f) => f.endsWith(\".jsonl\"));\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (jsonlFiles.length === 0) continue;\n\n\t\tlet cwd: string | undefined;\n\t\tfor (const f of jsonlFiles) {\n\t\t\tcwd = readSessionCwd(join(sessionSubDir, f));\n\t\t\tif (cwd) break;\n\t\t}\n\t\tif (!cwd) continue;\n\n\t\tconst memDir = join(cwd, \".dreb\", \"memory\");\n\t\tif (!seen.has(memDir) && existsSync(memDir)) {\n\t\t\tseen.add(memDir);\n\t\t\tmemoryDirs.push(memDir);\n\t\t}\n\t}\n\n\treturn memoryDirs;\n}\n\n/**\n * Discover claude compatibility memory directories (read-only imports).\n * Scans ~/.claude/projects/ for subdirectories containing a memory/ folder.\n */\nfunction discoverClaudeMemoryDirs(): string[] {\n\tconst claudeProjectsDir = join(homedir(), \".claude\", \"projects\");\n\tif (!existsSync(claudeProjectsDir)) return [];\n\n\tlet dirEntries: string[];\n\ttry {\n\t\tdirEntries = readdirSync(claudeProjectsDir);\n\t} catch {\n\t\treturn [];\n\t}\n\n\tconst memoryDirs: string[] = [];\n\tfor (const name of dirEntries) {\n\t\tconst memDir = join(claudeProjectsDir, name, \"memory\");\n\t\tif (existsSync(memDir)) {\n\t\t\tmemoryDirs.push(memDir);\n\t\t}\n\t}\n\treturn memoryDirs;\n}\n\n// =============================================================================\n// Context Resolution\n// =============================================================================\n\nexport async function resolveDreamContext(\n\tsettingsManager: SettingsManager,\n\toverrideGlobalMemDir?: string,\n): Promise<DreamContext> {\n\t// Archive path from settings, or default\n\tconst archivePath = settingsManager.getDreamArchivePath();\n\n\t// Global memory dir\n\tconst globalMemoryDir = overrideGlobalMemDir ?? join(homedir(), \".dreb\", \"memory\");\n\n\t// Last run timestamp\n\tlet lastRunTimestamp: string | null = null;\n\tconst markerPath = join(globalMemoryDir, DREAM_LAST_RUN_FILE);\n\ttry {\n\t\tif (existsSync(markerPath)) {\n\t\t\tconst content = readFileSync(markerPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tlastRunTimestamp = content;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Marker unreadable — treat as first run\n\t}\n\n\t// Discover memory directories\n\tconst projectMemoryDirs = discoverAllProjectMemoryDirs();\n\tconst claudeMemoryDirs = discoverClaudeMemoryDirs();\n\tconst sessionsDir = getSessionsDir();\n\n\treturn {\n\t\tarchivePath,\n\t\tlastRunTimestamp,\n\t\tglobalMemoryDir,\n\t\tprojectMemoryDirs,\n\t\tclaudeMemoryDirs,\n\t\tsessionsDir,\n\t};\n}\n\n// =============================================================================\n// Validation\n// =============================================================================\n\nexport function validateArchivePath(archivePath: string, memoryDirs: string[]): void {\n\tif (!archivePath || !archivePath.trim()) {\n\t\tthrow new Error(\"Dream archive path cannot be empty\");\n\t}\n\n\tif (!isAbsolute(archivePath)) {\n\t\tthrow new Error(`Dream archive path must be absolute, got: ${archivePath}`);\n\t}\n\n\tconst normalized = resolve(archivePath);\n\n\tfor (const memDir of memoryDirs) {\n\t\tconst normalizedMem = resolve(memDir);\n\n\t\t// Archive is inside a memory dir\n\t\tif (normalized.startsWith(`${normalizedMem}/`) || normalized === normalizedMem) {\n\t\t\tthrow new Error(\n\t\t\t\t`Dream archive path \"${archivePath}\" overlaps with memory directory \"${memDir}\". ` +\n\t\t\t\t\t\"The archive must be outside all memory directories.\",\n\t\t\t);\n\t\t}\n\n\t\t// Memory dir is inside the archive\n\t\tif (normalizedMem.startsWith(`${normalized}/`) || normalizedMem === normalized) {\n\t\t\tthrow new Error(\n\t\t\t\t`Memory directory \"${memDir}\" is inside the dream archive path \"${archivePath}\". ` +\n\t\t\t\t\t\"The archive must be outside all memory directories.\",\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function validateMemoryLinks(memoryDirs: string[]): LinkValidationResult {\n\tconst brokenLinks: LinkValidationResult[\"brokenLinks\"] = [];\n\tconst linkPattern = /\\[([^\\]]+)\\]\\(([^)]+)\\)/g;\n\n\tfor (const memDir of memoryDirs) {\n\t\tconst indexFile = join(memDir, \"MEMORY.md\");\n\t\tif (!existsSync(indexFile)) continue;\n\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = readFileSync(indexFile, \"utf-8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const match of content.matchAll(linkPattern)) {\n\t\t\tconst pointer = match[0];\n\t\t\tconst target = match[2];\n\n\t\t\t// Skip external URLs\n\t\t\tif (target.startsWith(\"http://\") || target.startsWith(\"https://\")) continue;\n\n\t\t\tconst targetPath = join(memDir, target);\n\t\t\tif (!existsSync(targetPath)) {\n\t\t\t\tbrokenLinks.push({ memoryDir: memDir, indexFile, pointer, target });\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { valid: brokenLinks.length === 0, brokenLinks };\n}\n\n// =============================================================================\n// Backup\n// =============================================================================\n\n/** Convert a filesystem path to a collision-resistant directory name for backup staging. */\nexport function safeDirName(path: string): string {\n\treturn encodeURIComponent(path).replace(/%2F/gi, \"_\");\n}\n\n/** Recursively count files and total size in a directory. */\nasync function countFilesAndSize(dir: string): Promise<{ fileCount: number; totalSize: number }> {\n\tlet fileCount = 0;\n\tlet totalSize = 0;\n\n\tasync function walk(current: string): Promise<void> {\n\t\tlet entries: import(\"node:fs\").Dirent[];\n\t\ttry {\n\t\t\tentries = await readdir(current, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(current, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tawait walk(fullPath);\n\t\t\t} else if (entry.isFile()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst st = await stat(fullPath);\n\t\t\t\t\tfileCount++;\n\t\t\t\t\ttotalSize += st.size;\n\t\t\t\t} catch {\n\t\t\t\t\t// File disappeared between readdir and stat — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tawait walk(dir);\n\treturn { fileCount, totalSize };\n}\n\nexport async function performDreamBackup(context: DreamContext): Promise<BackupResult> {\n\tconst timestamp = `${new Date().toISOString().replace(/[:.]/g, \"-\")}_${randomUUID().slice(0, 8)}`;\n\tconst archiveName = `${BACKUP_PREFIX}${timestamp}`;\n\tconst backupPath = join(context.archivePath, `${archiveName}.tar.gz`);\n\tconst stagingDir = join(context.archivePath, `.staging-${timestamp}`);\n\n\t// Ensure archive directory exists\n\ttry {\n\t\tmkdirSync(context.archivePath, { recursive: true });\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Failed to create archive directory \"${context.archivePath}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n\n\t// Build list of source directories\n\tconst allSourceDirs: Array<{ dir: string; stagingTarget: string }> = [];\n\n\t// Global memory\n\tif (existsSync(context.globalMemoryDir)) {\n\t\tallSourceDirs.push({ dir: context.globalMemoryDir, stagingTarget: join(stagingDir, \"global\") });\n\t}\n\n\t// Project memories\n\tfor (const dir of context.projectMemoryDirs) {\n\t\tif (existsSync(dir)) {\n\t\t\tallSourceDirs.push({\n\t\t\t\tdir,\n\t\t\t\tstagingTarget: join(stagingDir, \"projects\", safeDirName(dir)),\n\t\t\t});\n\t\t}\n\t}\n\n\t// Claude compat memories\n\tfor (const dir of context.claudeMemoryDirs) {\n\t\tif (existsSync(dir)) {\n\t\t\tallSourceDirs.push({\n\t\t\t\tdir,\n\t\t\t\tstagingTarget: join(stagingDir, \"claude\", safeDirName(dir)),\n\t\t\t});\n\t\t}\n\t}\n\n\t// Count source files before copying\n\tlet sourceFileCount = 0;\n\tlet sourceTotalSize = 0;\n\tfor (const { dir } of allSourceDirs) {\n\t\tconst counts = await countFilesAndSize(dir);\n\t\tsourceFileCount += counts.fileCount;\n\t\tsourceTotalSize += counts.totalSize;\n\t}\n\n\t// Copy source directories into staging area\n\ttry {\n\t\tmkdirSync(stagingDir, { recursive: true });\n\n\t\tfor (const { dir, stagingTarget } of allSourceDirs) {\n\t\t\ttry {\n\t\t\t\tawait cp(dir, stagingTarget, { recursive: true });\n\t\t\t} catch (error) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to copy \"${dir}\" to \"${stagingTarget}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Create .tar.gz archive from staging directory\n\t\tconst execFileAsync = promisify(execFile);\n\t\tawait execFileAsync(\"tar\", [\"czf\", backupPath, \"-C\", dirname(stagingDir), basename(stagingDir)]);\n\t} finally {\n\t\t// Clean up staging directory\n\t\ttry {\n\t\t\trmSync(stagingDir, { recursive: true, force: true });\n\t\t} catch {\n\t\t\t// Best-effort cleanup\n\t\t}\n\t}\n\n\t// Verify: archive exists and has non-zero size\n\tlet verified = false;\n\ttry {\n\t\tconst archiveStat = statSync(backupPath);\n\t\tverified = archiveStat.size > 0;\n\t} catch {\n\t\t// Archive missing or unreadable\n\t}\n\n\treturn {\n\t\tbackupPath,\n\t\ttimestamp,\n\t\tfileCount: sourceFileCount,\n\t\ttotalSize: sourceTotalSize,\n\t\tverified,\n\t};\n}\n\n// =============================================================================\n// Prompt Building\n// =============================================================================\n\nfunction formatBytes(bytes: number): string {\n\tif (bytes < 1024) return `${bytes} bytes`;\n\tif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function buildDreamPrompt(context: DreamContext, backupResult: BackupResult): string {\n\tconst verifiedStatus = backupResult.verified ? \"✓ Verified\" : \"⚠ Verification mismatch — check backup integrity\";\n\tconst lastRun = context.lastRunTimestamp ?? \"Never (first run)\";\n\n\t// Build the file listing section\n\tlet fileListing = \"\";\n\tconst allDirs = [\n\t\t{ label: \"Global\", dir: context.globalMemoryDir },\n\t\t...context.projectMemoryDirs.map((d) => ({ label: `Project: ${d}`, dir: d })),\n\t\t...context.claudeMemoryDirs.map((d) => ({ label: `Claude (READ-ONLY): ${d}`, dir: d })),\n\t];\n\n\tfor (const { label, dir } of allDirs) {\n\t\tif (!existsSync(dir)) continue;\n\t\tfileListing += `\\n### ${label}\\n`;\n\t\ttry {\n\t\t\tconst entries = readdirSync(dir);\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (\n\t\t\t\t\tentry === DREAM_TMP_DIR ||\n\t\t\t\t\tentry === DREAM_LOCK_FILE ||\n\t\t\t\t\tentry === `${DREAM_LOCK_FILE}.lock` ||\n\t\t\t\t\tentry === DREAM_LAST_RUN_FILE\n\t\t\t\t)\n\t\t\t\t\tcontinue;\n\t\t\t\tfileListing += `- ${entry}\\n`;\n\t\t\t}\n\t\t} catch {\n\t\t\tfileListing += `- (unreadable)\\n`;\n\t\t}\n\t}\n\n\t// If file listing is too large, spill to a temp file\n\tlet fileListingSection: string;\n\tif (fileListing.length > LARGE_LISTING_THRESHOLD) {\n\t\tconst tmpDir = join(context.globalMemoryDir, DREAM_TMP_DIR);\n\t\ttry {\n\t\t\tmkdirSync(tmpDir, { recursive: true });\n\t\t} catch {\n\t\t\t// Best-effort\n\t\t}\n\t\tconst tmpPath = join(tmpDir, `dream-listing-${Date.now()}.md`);\n\t\ttry {\n\t\t\twriteFileSync(tmpPath, fileListing, \"utf-8\");\n\t\t\tfileListingSection =\n\t\t\t\t`File listing too large to include inline. Full listing written to: ${tmpPath}\\n` +\n\t\t\t\t\"Read this file for the complete list of memory files.\";\n\t\t} catch {\n\t\t\t// Fallback: include inline anyway\n\t\t\tfileListingSection = fileListing;\n\t\t}\n\t} else {\n\t\tfileListingSection = fileListing;\n\t}\n\n\tconst projectDirsList =\n\t\tcontext.projectMemoryDirs.length > 0\n\t\t\t? context.projectMemoryDirs.map((d) => ` - ${d}`).join(\"\\n\")\n\t\t\t: \" (none discovered)\";\n\n\tconst claudeDirsList =\n\t\tcontext.claudeMemoryDirs.length > 0\n\t\t\t? context.claudeMemoryDirs.map((d) => ` - ${d}`).join(\"\\n\")\n\t\t\t: \" (none discovered)\";\n\n\tconst dreamTmpDirName = DREAM_TMP_DIR;\n\tconst dreamLastRunPath = join(context.globalMemoryDir, DREAM_LAST_RUN_FILE);\n\n\treturn `You are running a memory consolidation (/dream). A backup archive has been created at: ${backupResult.backupPath}\nBackup verification: ${backupResult.fileCount} files, ${formatBytes(backupResult.totalSize)} — ${verifiedStatus}\n\n## Context\n- Global memory: ${context.globalMemoryDir}\n- Project memories:\n${projectDirsList}\n- Claude Code memories (READ-ONLY):\n${claudeDirsList}\n- Last dream run: ${lastRun}\n- Sessions directory: ${context.sessionsDir}\n\n## Memory Files\n${fileListingSection}\n\n## HARD CONSTRAINTS\n- The \\`.claude/\\` memory directories are read-only compatibility imports. Do NOT modify, delete, or rewrite any files under \\`.claude/\\` paths. Only \\`~/.dreb/memory/\\` and \\`<project>/.dreb/memory/\\` are writable.\n- NEVER remove session JSONL data files.\n- NEVER delete or modify \\`.dream.lock\\`, \\`.dream.lock.lock\\`, or \\`.dream-last-run\\` files — these are managed by the dream infrastructure, not by you.\n- Explicitly EXCLUDE \\`subagent-sessions/\\` from scanning scope.\n\n## Pipeline\n\n### Step 0: Verify Backup\nBefore proceeding, verify the backup archive exists and is intact:\n1. Check that the backup file exists at the path shown above\n2. List its contents (e.g., \\`tar tzf <path> | head -20\\`) to confirm memory files are present\n3. If the backup appears incomplete or missing, STOP and inform the user — do not proceed with consolidation\n\n### Step 1: Read All Memories\nRead every MEMORY.md index and every referenced memory file from global, all project scopes, and \\`.claude/\\` read-only paths.\n\n### Step 2: Analyze & Plan\nGroup related entries, identify duplicates, overlapping content, stale references (deleted files, resolved issues). Present the consolidation plan to the user before making any changes.\n\n### Step 3: Consolidate\nMerge related entries, deduplicate, reorganize semantically. Write changes to temp files first (\\`<memory-dir>/${dreamTmpDirName}/\\`), then atomic rename per file. Maintain a rollback manifest listing every original→tmp→final path.\n\n### Step 4: Rewrite Indexes\nProduce new MEMORY.md indexes organized semantically, under 200 lines. Every pointer must reference an existing file. Write to temp first, then rename.\n\n### Step 5: Remove Dead Files\nOnly after indexes are rewritten and validated, delete memory files that have ZERO remaining references in any MEMORY.md index. Never delete a file that is still referenced.\n\n### Step 6: Scan Sessions\nSpawn background Explore subagents to read session JSONL logs from ${context.sessionsDir} (EXCLUDE subagent-sessions/). Only scan sessions since ${lastRun}. First-run cap: 30 days maximum.\n\nSession JSONL format: Each line is a JSON object. Relevant entry types:\n- \\`{\"type\":\"message\",\"message\":{\"role\":\"user\"|\"assistant\",\"content\":...}}\\` — conversation messages\n- \\`{\"type\":\"tool_use\",\"name\":\"...\",\"input\":{...}}\\` — tool calls\n- \\`{\"type\":\"tool_result\",\"content\":...}\\` — tool outputs\n\nEach subagent should return findings in structured format:\n\\`{\"findings\": [{\"type\": \"user-preferences|good-practices|project|navigation\", \"name\": \"...\", \"description\": \"...\", \"content\": \"...\"}]}\\`\n\n### Step 7: STOP AND WAIT\nAfter spawning subagents, stop generating and wait for ALL background-agent-complete messages before proceeding. Do not continue to Step 8 until every subagent has reported back.\n\n### Step 8: Incorporate Findings\nCreate new memory entries from subagent findings, update MEMORY.md indexes.\n\n### Step 9: Report\nStructured summary: X merged, Y pruned, Z added from sessions, W files removed, backup location, any warnings.\n\n### Step 10: Mark Complete\nWrite the current ISO timestamp to ${dreamLastRunPath}\n`;\n}\n\n// =============================================================================\n// Locking\n// =============================================================================\n\nexport async function acquireDreamLock(overrideMemDir?: string): Promise<() => void> {\n\tconst memDir = overrideMemDir ?? join(homedir(), \".dreb\", \"memory\");\n\tconst lockPath = join(memDir, DREAM_LOCK_FILE);\n\n\t// Ensure directory and lock file exist (proper-lockfile requires the file to exist)\n\ttry {\n\t\tmkdirSync(memDir, { recursive: true });\n\t} catch {\n\t\t// Directory already exists\n\t}\n\n\tif (!existsSync(lockPath)) {\n\t\ttry {\n\t\t\twriteFileSync(lockPath, \"\", \"utf-8\");\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to create dream lock file \"${lockPath}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tlet release: () => Promise<void>;\n\ttry {\n\t\trelease = await lockfile.lock(lockPath, {\n\t\t\tstale: 60000,\n\t\t\tretries: {\n\t\t\t\tretries: 5,\n\t\t\t\tfactor: 2,\n\t\t\t\tminTimeout: 200,\n\t\t\t\tmaxTimeout: 5000,\n\t\t\t\trandomize: true,\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\tconst code =\n\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t: undefined;\n\t\tif (code === \"ELOCKED\") {\n\t\t\tthrow new Error(\n\t\t\t\t\"Another /dream operation is already running. \" +\n\t\t\t\t\t\"If this is stale, wait 60 seconds or manually remove \" +\n\t\t\t\t\t`the lock: ${lockPath}.lock`,\n\t\t\t);\n\t\t}\n\t\tthrow new Error(`Failed to acquire dream lock: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\n\treturn () => {\n\t\trelease().catch(() => {\n\t\t\t// Best-effort unlock — if it fails the stale timeout will clean up\n\t\t});\n\t};\n}\n\n// =============================================================================\n// Backup Pruning\n// =============================================================================\n\nexport async function pruneOldBackups(archivePath: string, keepCount: number = DEFAULT_KEEP_COUNT): Promise<void> {\n\tif (!existsSync(archivePath)) return;\n\n\tlet dirEntries: string[];\n\ttry {\n\t\tdirEntries = readdirSync(archivePath);\n\t} catch {\n\t\treturn;\n\t}\n\n\tconst backupFiles = dirEntries.filter((name) => name.startsWith(BACKUP_PREFIX) && name.endsWith(\".tar.gz\")).sort();\n\n\tif (backupFiles.length <= keepCount) return;\n\n\tconst toRemove = backupFiles.slice(0, backupFiles.length - keepCount);\n\tfor (const fileName of toRemove) {\n\t\tconst fullPath = join(archivePath, fileName);\n\t\ttry {\n\t\t\tunlinkSync(fullPath);\n\t\t} catch {\n\t\t\t// Best-effort — log would be nice but we don't have a logger here\n\t\t}\n\t}\n}\n\n// =============================================================================\n// Temp Dir Cleanup\n// =============================================================================\n\nexport function cleanupDreamTmpDirs(memoryDirs: string[]): void {\n\tfor (const memDir of memoryDirs) {\n\t\tconst tmpDir = join(memDir, DREAM_TMP_DIR);\n\t\tif (existsSync(tmpDir)) {\n\t\t\ttry {\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\t// Best-effort cleanup\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|