codemini-cli 0.5.9 → 0.5.11

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.
Files changed (59) hide show
  1. package/OPERATIONS.md +242 -242
  2. package/README.md +588 -489
  3. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-HgeDi9HJ.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
  4. package/codemini-web/dist/assets/{index-C4tKT3v4.js → index-B71xykPM.js} +108 -108
  5. package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
  6. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
  7. package/codemini-web/dist/index.html +23 -23
  8. package/codemini-web/lib/approval-manager.js +32 -32
  9. package/codemini-web/lib/runtime-bridge.js +17 -11
  10. package/codemini-web/server.js +534 -205
  11. package/deployment.md +212 -212
  12. package/package.json +1 -1
  13. package/skills/brainstorm/SKILL.md +77 -72
  14. package/skills/codemini.skills.json +40 -40
  15. package/skills/grill-me/SKILL.md +30 -30
  16. package/skills/superpowers-lite/SKILL.md +82 -82
  17. package/src/cli.js +74 -74
  18. package/src/commands/chat.js +210 -210
  19. package/src/commands/run.js +313 -313
  20. package/src/commands/skill.js +438 -304
  21. package/src/commands/web.js +57 -57
  22. package/src/core/agent-loop.js +980 -980
  23. package/src/core/ast.js +309 -292
  24. package/src/core/chat-runtime.js +6261 -6240
  25. package/src/core/command-evaluator.js +72 -72
  26. package/src/core/command-loader.js +311 -311
  27. package/src/core/command-policy.js +301 -301
  28. package/src/core/command-risk.js +156 -156
  29. package/src/core/config-store.js +289 -287
  30. package/src/core/constants.js +18 -1
  31. package/src/core/context-compact.js +365 -365
  32. package/src/core/default-system-prompt.js +114 -107
  33. package/src/core/dream-audit.js +105 -105
  34. package/src/core/dream-consolidate.js +229 -229
  35. package/src/core/dream-evaluator.js +185 -185
  36. package/src/core/fff-adapter.js +383 -383
  37. package/src/core/memory-store.js +543 -543
  38. package/src/core/project-index.js +737 -529
  39. package/src/core/project-instructions.js +98 -0
  40. package/src/core/provider/anthropic.js +514 -514
  41. package/src/core/provider/openai-compatible.js +501 -501
  42. package/src/core/reflect-skill.js +178 -178
  43. package/src/core/reply-language.js +40 -40
  44. package/src/core/session-store.js +474 -474
  45. package/src/core/shell-profile.js +237 -237
  46. package/src/core/shell.js +323 -317
  47. package/src/core/soul.js +69 -69
  48. package/src/core/system-prompt-composer.js +52 -42
  49. package/src/core/tool-args.js +199 -154
  50. package/src/core/tool-output.js +184 -184
  51. package/src/core/tool-result-store.js +206 -206
  52. package/src/core/tools.js +3024 -2893
  53. package/src/core/version.js +11 -11
  54. package/src/tui/chat-app.js +5171 -5171
  55. package/src/tui/tool-activity/presenters/misc.js +30 -30
  56. package/src/tui/tool-activity/presenters/system.js +20 -20
  57. package/templates/project-requirements/report-shell.html +582 -582
  58. package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
  59. package/codemini-web/dist/assets/mermaid-GHXKKRXX-CDgkkDBg.js +0 -1
@@ -1,543 +1,543 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { sha256 } from './crypto-utils.js';
4
- import { getMemoryDir, getProjectMemoryDir, getInboxDir, getArchiveDir } from './paths.js';
5
- import { assertSafeMemoryContent, normalizeMemoryText, summarizeMemoryContent } from './memory-policy.js';
6
-
7
- const ALLOWED_SCOPES = new Set(['user', 'global', 'project']);
8
-
9
- function nowIso() {
10
- return new Date().toISOString();
11
- }
12
-
13
- function slugify(value) {
14
- const text = String(value || '')
15
- .toLowerCase()
16
- .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
17
- .replace(/^-+|-+$/g, '');
18
- return text || 'project';
19
- }
20
-
21
- export function getProjectMemoryKey(workspaceRoot = process.cwd(), projectAlias = '') {
22
- const alias = normalizeMemoryText(projectAlias);
23
- if (alias) return slugify(alias);
24
- const root = path.resolve(workspaceRoot || process.cwd());
25
- const base = path.basename(root);
26
- return `${slugify(base)}-${sha256(root).slice(0, 10)}`;
27
- }
28
-
29
- function ensureScope(scope) {
30
- const value = String(scope || '').trim().toLowerCase();
31
- if (!ALLOWED_SCOPES.has(value)) {
32
- throw new Error(`Unsupported memory scope: ${scope}`);
33
- }
34
- return value;
35
- }
36
-
37
- async function ensureParent(filePath) {
38
- await fs.mkdir(path.dirname(filePath), { recursive: true });
39
- }
40
-
41
- function buildFilePath(scope, workspaceRoot = process.cwd(), projectAlias = '') {
42
- if (scope === 'user') return path.join(getMemoryDir(), 'user.json');
43
- if (scope === 'global') return path.join(getMemoryDir(), 'global.json');
44
- return path.join(getProjectMemoryDir(workspaceRoot), 'project.json');
45
- }
46
-
47
- async function listProjectMemoryFiles(workspaceRoot = process.cwd()) {
48
- const dir = getProjectMemoryDir(workspaceRoot);
49
- try {
50
- const entries = await fs.readdir(dir, { withFileTypes: true });
51
- return entries
52
- .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
53
- .map((entry) => path.join(dir, entry.name))
54
- .sort();
55
- } catch {
56
- return [];
57
- }
58
- }
59
-
60
- async function readMemoryBucket(filePath) {
61
- const doc = await readMemoryBucketDocument(filePath);
62
- return doc.items;
63
- }
64
-
65
- async function readMemoryBucketDocument(filePath) {
66
- try {
67
- const raw = await fs.readFile(filePath, 'utf8');
68
- const parsed = JSON.parse(raw);
69
- return {
70
- items: Array.isArray(parsed?.items) ? parsed.items : [],
71
- maintenance: parsed?.maintenance && typeof parsed.maintenance === 'object' ? parsed.maintenance : null
72
- };
73
- } catch {
74
- return { items: [], maintenance: null };
75
- }
76
- }
77
-
78
- function memoryBucketHash(items = []) {
79
- const stable = (Array.isArray(items) ? items : [])
80
- .map((item) => ({
81
- id: String(item?.id || ''),
82
- kind: String(item?.kind || ''),
83
- content: normalizeMemoryText(item?.content || ''),
84
- summary: normalizeMemoryText(item?.summary || ''),
85
- lifecycle: String(item?.lifecycle || ''),
86
- pinned: item?.pinned === true
87
- }))
88
- .sort((left, right) => left.id.localeCompare(right.id));
89
- return sha256(JSON.stringify(stable));
90
- }
91
-
92
- async function writeMemoryBucket(filePath, items, { maintenance = null } = {}) {
93
- await ensureParent(filePath);
94
- const doc = { items };
95
- if (maintenance) doc.maintenance = maintenance;
96
- await fs.writeFile(filePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
97
- }
98
-
99
- function dedupeMemoryItems(items = []) {
100
- const deduped = [];
101
- const seen = new Set();
102
- for (const item of items) {
103
- const key = item.id ? `id:${item.id}` : `${item.kind}:${normalizeMemoryText(item.content)}`;
104
- if (seen.has(key)) continue;
105
- seen.add(key);
106
- deduped.push(item);
107
- }
108
- return deduped;
109
- }
110
-
111
- async function readProjectMemoryItems(workspaceRoot = process.cwd(), projectAlias = '') {
112
- const projectKey = getProjectMemoryKey(workspaceRoot, projectAlias);
113
- const files = await listProjectMemoryFiles(workspaceRoot);
114
- const items = [];
115
- for (const file of files) {
116
- const bucket = await readMemoryBucket(file);
117
- items.push(...bucket.map((item) => normalizeMemoryItem(item, 'project', projectKey)));
118
- }
119
- return dedupeMemoryItems(items)
120
- .sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
121
- }
122
-
123
- async function readScopeMemoryItems(scope, workspaceRoot = process.cwd(), projectAlias = '') {
124
- const normalizedScope = ensureScope(scope);
125
- if (normalizedScope === 'project') return readProjectMemoryItems(workspaceRoot, projectAlias);
126
- const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
127
- return (await readMemoryBucket(filePath))
128
- .map((item) => normalizeMemoryItem(item, normalizedScope, ''))
129
- .sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
130
- }
131
-
132
- function normalizeMemoryItem(item, scope, projectKey = '') {
133
- const now = nowIso();
134
- const content = normalizeMemoryText(item?.content || '');
135
- return {
136
- id: String(item?.id || `mem_${sha256(`${scope}:${projectKey}:${content}:${now}:${Math.random()}`).slice(0, 12)}`),
137
- scope,
138
- projectKey: projectKey || undefined,
139
- kind: String(item?.kind || 'note').trim() || 'note',
140
- content,
141
- summary: normalizeMemoryText(item?.summary || summarizeMemoryContent(content)),
142
- source: String(item?.source || 'tool').trim() || 'tool',
143
- confidence: Number.isFinite(Number(item?.confidence)) ? Number(item.confidence) : 0.9,
144
- createdAt: String(item?.createdAt || now),
145
- updatedAt: String(item?.updatedAt || now),
146
- hits: Number.isFinite(Number(item?.hits)) ? Number(item.hits) : 0,
147
- pinned: item?.pinned === true,
148
- ...(item?.lifecycle ? { lifecycle: String(item.lifecycle) } : {})
149
- };
150
- }
151
-
152
- function sameMemory(left, right) {
153
- const a = normalizeMemoryText(left?.content);
154
- const b = normalizeMemoryText(right?.content);
155
- if (!a || !b) return false;
156
- return a === b;
157
- }
158
-
159
- function measureMemoryChars(item) {
160
- return normalizeMemoryText(item?.content).length + normalizeMemoryText(item?.summary).length;
161
- }
162
-
163
- function budgetForScope(scope, config = {}) {
164
- if (scope === 'user') return Math.max(80, Number(config?.memory?.max_user_chars || 1375));
165
- if (scope === 'global') return Math.max(80, Number(config?.memory?.max_global_chars || 2200));
166
- return Math.max(80, Number(config?.memory?.max_project_chars || 2200));
167
- }
168
-
169
- export async function listMemories({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
170
- const normalizedScope = ensureScope(scope);
171
- return readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
172
- }
173
-
174
- export async function getMemoryBucketMaintenance({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
175
- const normalizedScope = ensureScope(scope);
176
- const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
177
- const doc = await readMemoryBucketDocument(filePath);
178
- const items = normalizedScope === 'project'
179
- ? await readProjectMemoryItems(workspaceRoot, projectAlias)
180
- : doc.items.map((item) => normalizeMemoryItem(item, normalizedScope, ''));
181
- const currentHash = memoryBucketHash(items);
182
- const storedHash = String(doc.maintenance?.contentHash || '');
183
- const maintainedAt = String(doc.maintenance?.maintainedAt || '');
184
- return {
185
- scope: normalizedScope,
186
- itemCount: items.length,
187
- contentHash: currentHash,
188
- storedHash,
189
- maintainedAt,
190
- fresh: Boolean(maintainedAt && storedHash && storedHash === currentHash)
191
- };
192
- }
193
-
194
- export async function markMemoryBucketMaintained({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
195
- const normalizedScope = ensureScope(scope);
196
- const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
197
- const items = await readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
198
- const maintenance = {
199
- maintainedAt: nowIso(),
200
- contentHash: memoryBucketHash(items),
201
- itemCount: items.length
202
- };
203
- await writeMemoryBucket(filePath, items, { maintenance });
204
- return { scope: normalizedScope, ...maintenance };
205
- }
206
-
207
- export async function replaceMemoryBucket({
208
- scope,
209
- items = [],
210
- workspaceRoot = process.cwd(),
211
- projectAlias = '',
212
- markMaintained = false
213
- } = {}) {
214
- const normalizedScope = ensureScope(scope);
215
- const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
216
- const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
217
- const normalizedItems = (Array.isArray(items) ? items : [])
218
- .map((item) => normalizeMemoryItem(item, normalizedScope, projectKey))
219
- .filter((item) => item.content);
220
- const maintenance = markMaintained
221
- ? {
222
- maintainedAt: nowIso(),
223
- contentHash: memoryBucketHash(normalizedItems),
224
- itemCount: normalizedItems.length
225
- }
226
- : null;
227
- await writeMemoryBucket(filePath, normalizedItems, { maintenance });
228
- return {
229
- scope: normalizedScope,
230
- items: normalizedItems,
231
- maintenance
232
- };
233
- }
234
-
235
- export async function rememberMemory({
236
- scope,
237
- content,
238
- kind = 'note',
239
- summary = '',
240
- source = 'tool',
241
- confidence = 0.9,
242
- replaceSimilar = true,
243
- pinned = false,
244
- workspaceRoot = process.cwd(),
245
- projectAlias = '',
246
- config = {}
247
- }) {
248
- const normalizedScope = ensureScope(scope);
249
- const normalizedContent = normalizeMemoryText(content);
250
- if (!normalizedContent) throw new Error('Memory content is required');
251
- assertSafeMemoryContent(normalizedContent);
252
-
253
- const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
254
- const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
255
- const existing = await readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
256
- const probe = normalizeMemoryItem({ content: normalizedContent, kind, summary, source, confidence, pinned }, normalizedScope, projectKey);
257
-
258
- const replaceIndex = replaceSimilar ? existing.findIndex((item) => sameMemory(item, probe)) : -1;
259
- let saved;
260
- if (replaceIndex >= 0) {
261
- saved = {
262
- ...existing[replaceIndex],
263
- ...probe,
264
- id: existing[replaceIndex].id,
265
- createdAt: existing[replaceIndex].createdAt,
266
- updatedAt: nowIso()
267
- };
268
- existing.splice(replaceIndex, 1, saved);
269
- } else {
270
- saved = probe;
271
- existing.unshift(saved);
272
- }
273
-
274
- const maxItems = Math.max(1, Number(config?.memory?.max_items_per_scope || 12));
275
- const maxChars = budgetForScope(normalizedScope, config);
276
- const deduped = [];
277
- const seen = new Set();
278
- for (const item of existing) {
279
- const key = `${item.kind}:${normalizeMemoryText(item.content)}`;
280
- if (seen.has(key)) continue;
281
- seen.add(key);
282
- deduped.push(item);
283
- if (deduped.length >= maxItems) break;
284
- }
285
- let totalChars = deduped.reduce((sum, item) => sum + measureMemoryChars(item), 0);
286
- while (deduped.length > 1 && totalChars > maxChars) {
287
- const removed = deduped.pop();
288
- totalChars -= measureMemoryChars(removed);
289
- }
290
- await writeMemoryBucket(filePath, deduped);
291
- return saved;
292
- }
293
-
294
- export async function forgetMemory({ scope, id, workspaceRoot = process.cwd(), projectAlias = '' }) {
295
- const normalizedScope = ensureScope(scope);
296
- const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
297
- const existing = await listMemories({ scope: normalizedScope, workspaceRoot, projectAlias });
298
- const kept = existing.filter((item) => item.id !== id);
299
- await writeMemoryBucket(filePath, kept);
300
- if (normalizedScope === 'project') {
301
- const files = (await listProjectMemoryFiles(workspaceRoot)).filter((file) => file !== filePath);
302
- await Promise.all(files.map(async (file) => {
303
- const bucket = await readMemoryBucket(file);
304
- const next = bucket.filter((item) => String(item?.id || '') !== id);
305
- if (next.length !== bucket.length) await writeMemoryBucket(file, next);
306
- }));
307
- }
308
- return { removed: existing.length - kept.length };
309
- }
310
-
311
- export async function searchMemories({ scope, query, workspaceRoot = process.cwd(), projectAlias = '' }) {
312
- const items = await listMemories({ scope, workspaceRoot, projectAlias });
313
- const needle = normalizeMemoryText(query).toLowerCase();
314
- if (!needle) return items;
315
- return items.filter((item) => item.content.toLowerCase().includes(needle) || item.summary.toLowerCase().includes(needle));
316
- }
317
-
318
- // ---------------------------------------------------------------------------
319
- // Dream Loop: inbox capture, lifecycle, archive, promotion
320
- // ---------------------------------------------------------------------------
321
-
322
- const VALID_LIFECYCLE = new Set(['observed', 'candidate', 'operational', 'longterm', 'archived']);
323
- const VALID_INBOX_SCOPES = new Set(['global', 'repo', 'thread', 'project', 'user']);
324
-
325
- function validateLifecycle(value) {
326
- const lc = String(value || '').trim().toLowerCase();
327
- if (!VALID_LIFECYCLE.has(lc)) throw new Error(`Invalid lifecycle state: ${value}`);
328
- return lc;
329
- }
330
-
331
- function normalizeInboxScope(value) {
332
- const scope = String(value || 'global').trim().toLowerCase();
333
- if (!VALID_INBOX_SCOPES.has(scope)) throw new Error(`Unsupported inbox scope: ${value}`);
334
- return scope;
335
- }
336
-
337
- function todayDir(baseDir) {
338
- const date = new Date().toISOString().slice(0, 10);
339
- return path.join(baseDir, date);
340
- }
341
-
342
- async function readJsonArray(filePath) {
343
- try {
344
- const raw = await fs.readFile(filePath, 'utf8');
345
- const parsed = JSON.parse(raw);
346
- return Array.isArray(parsed) ? parsed : [];
347
- } catch {
348
- return [];
349
- }
350
- }
351
-
352
- async function writeJsonArray(filePath, items) {
353
- await ensureParent(filePath);
354
- await fs.writeFile(filePath, `${JSON.stringify(items, null, 2)}\n`, 'utf8');
355
- }
356
-
357
- export async function captureToInbox({
358
- scope = 'global',
359
- type = 'observation',
360
- summary,
361
- details = '',
362
- suggestedAction = '',
363
- tags = [],
364
- source = 'tool'
365
- } = {}) {
366
- const normalizedSummary = normalizeMemoryText(summary);
367
- if (!normalizedSummary) throw new Error('Inbox capture summary is required');
368
- assertSafeMemoryContent(normalizedSummary);
369
-
370
- const dir = todayDir(getInboxDir());
371
- await fs.mkdir(dir, { recursive: true });
372
- const now = nowIso();
373
- const id = `inbox_${sha256(`${normalizedSummary}:${now}:${Math.random()}`).slice(0, 12)}`;
374
- const entry = {
375
- id,
376
- timestamp: now,
377
- scope: normalizeInboxScope(scope),
378
- source,
379
- type: String(type || 'observation').trim().toLowerCase(),
380
- summary: normalizedSummary,
381
- details: normalizeMemoryText(details),
382
- suggestedAction: normalizeMemoryText(suggestedAction),
383
- tags: Array.isArray(tags) ? tags.map((t) => String(t).trim()).filter(Boolean) : [],
384
- lifecycle: 'observed'
385
- };
386
-
387
- const indexPath = path.join(dir, 'index.json');
388
- const entries = await readJsonArray(indexPath);
389
- entries.push(entry);
390
- await writeJsonArray(indexPath, entries);
391
- return entry;
392
- }
393
-
394
- export async function listInbox({ since, scope } = {}) {
395
- const inboxBase = getInboxDir();
396
- let dayDirs;
397
- try {
398
- const entries = await fs.readdir(inboxBase);
399
- dayDirs = entries.filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
400
- } catch {
401
- return [];
402
- }
403
- if (since) {
404
- const sinceStr = String(since).slice(0, 10);
405
- dayDirs = dayDirs.filter((d) => d >= sinceStr);
406
- }
407
- const all = [];
408
- for (const day of dayDirs) {
409
- const indexPath = path.join(inboxBase, day, 'index.json');
410
- const entries = await readJsonArray(indexPath);
411
- all.push(...entries);
412
- }
413
- if (scope) {
414
- const sc = String(scope).trim().toLowerCase();
415
- return all.filter((e) => e.scope === sc);
416
- }
417
- return all;
418
- }
419
-
420
- export async function updateInboxEntry(id, updates = {}) {
421
- const inboxBase = getInboxDir();
422
- let dayDirs;
423
- try {
424
- dayDirs = (await fs.readdir(inboxBase)).filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
425
- } catch {
426
- return null;
427
- }
428
- for (const day of dayDirs) {
429
- const indexPath = path.join(inboxBase, day, 'index.json');
430
- const entries = await readJsonArray(indexPath);
431
- const idx = entries.findIndex((e) => e.id === id);
432
- if (idx === -1) continue;
433
- if (updates.lifecycle) updates.lifecycle = validateLifecycle(updates.lifecycle);
434
- entries[idx] = { ...entries[idx], ...updates };
435
- await writeJsonArray(indexPath, entries);
436
- return entries[idx];
437
- }
438
- return null;
439
- }
440
-
441
- export async function removeInboxEntry(id) {
442
- const inboxBase = getInboxDir();
443
- let dayDirs;
444
- try {
445
- dayDirs = (await fs.readdir(inboxBase)).filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
446
- } catch {
447
- return false;
448
- }
449
- for (const day of dayDirs) {
450
- const indexPath = path.join(inboxBase, day, 'index.json');
451
- const entries = await readJsonArray(indexPath);
452
- const idx = entries.findIndex((e) => e.id === id);
453
- if (idx === -1) continue;
454
- entries.splice(idx, 1);
455
- await writeJsonArray(indexPath, entries);
456
- return true;
457
- }
458
- return false;
459
- }
460
-
461
- export async function archiveEntry(entry, reason = '', auditNote = '') {
462
- const archiveDir = getArchiveDir();
463
- const date = new Date().toISOString().slice(0, 10);
464
- const dir = path.join(archiveDir, date);
465
- await fs.mkdir(dir, { recursive: true });
466
- const archived = {
467
- ...entry,
468
- lifecycle: 'archived',
469
- archivedAt: nowIso(),
470
- archiveReason: normalizeMemoryText(reason),
471
- auditNote: normalizeMemoryText(auditNote)
472
- };
473
- const indexPath = path.join(dir, 'index.json');
474
- const entries = await readJsonArray(indexPath);
475
- entries.push(archived);
476
- await writeJsonArray(indexPath, entries);
477
- await removeInboxEntry(entry.id);
478
- return archived;
479
- }
480
-
481
- export async function listArchive({ since, scope } = {}) {
482
- const archiveBase = getArchiveDir();
483
- let dayDirs;
484
- try {
485
- const entries = await fs.readdir(archiveBase);
486
- dayDirs = entries.filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
487
- } catch {
488
- return [];
489
- }
490
- if (since) {
491
- const sinceStr = String(since).slice(0, 10);
492
- dayDirs = dayDirs.filter((d) => d >= sinceStr);
493
- }
494
- const all = [];
495
- for (const day of dayDirs) {
496
- const indexPath = path.join(archiveBase, day, 'index.json');
497
- const entries = await readJsonArray(indexPath);
498
- all.push(...entries);
499
- }
500
- if (scope) {
501
- const sc = String(scope).trim().toLowerCase();
502
- return all.filter((e) => e.scope === sc);
503
- }
504
- return all;
505
- }
506
-
507
- export async function promoteMemory({
508
- entry,
509
- scope = 'global',
510
- lifecycle = 'operational',
511
- workspaceRoot = process.cwd(),
512
- projectAlias = '',
513
- config = {},
514
- confidence = 0.9
515
- } = {}) {
516
- if (!entry?.summary) throw new Error('Entry with summary is required for promotion');
517
- const lc = validateLifecycle(lifecycle);
518
- const content = normalizeMemoryText(entry.details || entry.summary);
519
- const saved = await rememberMemory({
520
- scope,
521
- content,
522
- kind: entry.type || 'note',
523
- summary: normalizeMemoryText(entry.summary),
524
- source: `dream-promote:${entry.id}`,
525
- confidence: Math.min(1, Math.max(0.5, confidence)),
526
- replaceSimilar: true,
527
- workspaceRoot,
528
- projectAlias,
529
- config
530
- });
531
- // Tag the saved item with lifecycle
532
- const filePath = buildFilePath(scope, workspaceRoot, projectAlias);
533
- const projectKey = scope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
534
- const items = (await readMemoryBucket(filePath)).map((item) => normalizeMemoryItem(item, scope, projectKey));
535
- const target = items.find((item) => item.id === saved.id);
536
- if (target) {
537
- target.lifecycle = lc;
538
- await writeMemoryBucket(filePath, items);
539
- }
540
- // Remove from inbox
541
- await removeInboxEntry(entry.id);
542
- return { promoted: saved, lifecycle: lc };
543
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { sha256 } from './crypto-utils.js';
4
+ import { getMemoryDir, getProjectMemoryDir, getInboxDir, getArchiveDir } from './paths.js';
5
+ import { assertSafeMemoryContent, normalizeMemoryText, summarizeMemoryContent } from './memory-policy.js';
6
+
7
+ const ALLOWED_SCOPES = new Set(['user', 'global', 'project']);
8
+
9
+ function nowIso() {
10
+ return new Date().toISOString();
11
+ }
12
+
13
+ function slugify(value) {
14
+ const text = String(value || '')
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
17
+ .replace(/^-+|-+$/g, '');
18
+ return text || 'project';
19
+ }
20
+
21
+ export function getProjectMemoryKey(workspaceRoot = process.cwd(), projectAlias = '') {
22
+ const alias = normalizeMemoryText(projectAlias);
23
+ if (alias) return slugify(alias);
24
+ const root = path.resolve(workspaceRoot || process.cwd());
25
+ const base = path.basename(root);
26
+ return `${slugify(base)}-${sha256(root).slice(0, 10)}`;
27
+ }
28
+
29
+ function ensureScope(scope) {
30
+ const value = String(scope || '').trim().toLowerCase();
31
+ if (!ALLOWED_SCOPES.has(value)) {
32
+ throw new Error(`Unsupported memory scope: ${scope}`);
33
+ }
34
+ return value;
35
+ }
36
+
37
+ async function ensureParent(filePath) {
38
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
39
+ }
40
+
41
+ function buildFilePath(scope, workspaceRoot = process.cwd(), projectAlias = '') {
42
+ if (scope === 'user') return path.join(getMemoryDir(), 'user.json');
43
+ if (scope === 'global') return path.join(getMemoryDir(), 'global.json');
44
+ return path.join(getProjectMemoryDir(workspaceRoot), 'project.json');
45
+ }
46
+
47
+ async function listProjectMemoryFiles(workspaceRoot = process.cwd()) {
48
+ const dir = getProjectMemoryDir(workspaceRoot);
49
+ try {
50
+ const entries = await fs.readdir(dir, { withFileTypes: true });
51
+ return entries
52
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
53
+ .map((entry) => path.join(dir, entry.name))
54
+ .sort();
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ async function readMemoryBucket(filePath) {
61
+ const doc = await readMemoryBucketDocument(filePath);
62
+ return doc.items;
63
+ }
64
+
65
+ async function readMemoryBucketDocument(filePath) {
66
+ try {
67
+ const raw = await fs.readFile(filePath, 'utf8');
68
+ const parsed = JSON.parse(raw);
69
+ return {
70
+ items: Array.isArray(parsed?.items) ? parsed.items : [],
71
+ maintenance: parsed?.maintenance && typeof parsed.maintenance === 'object' ? parsed.maintenance : null
72
+ };
73
+ } catch {
74
+ return { items: [], maintenance: null };
75
+ }
76
+ }
77
+
78
+ function memoryBucketHash(items = []) {
79
+ const stable = (Array.isArray(items) ? items : [])
80
+ .map((item) => ({
81
+ id: String(item?.id || ''),
82
+ kind: String(item?.kind || ''),
83
+ content: normalizeMemoryText(item?.content || ''),
84
+ summary: normalizeMemoryText(item?.summary || ''),
85
+ lifecycle: String(item?.lifecycle || ''),
86
+ pinned: item?.pinned === true
87
+ }))
88
+ .sort((left, right) => left.id.localeCompare(right.id));
89
+ return sha256(JSON.stringify(stable));
90
+ }
91
+
92
+ async function writeMemoryBucket(filePath, items, { maintenance = null } = {}) {
93
+ await ensureParent(filePath);
94
+ const doc = { items };
95
+ if (maintenance) doc.maintenance = maintenance;
96
+ await fs.writeFile(filePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
97
+ }
98
+
99
+ function dedupeMemoryItems(items = []) {
100
+ const deduped = [];
101
+ const seen = new Set();
102
+ for (const item of items) {
103
+ const key = item.id ? `id:${item.id}` : `${item.kind}:${normalizeMemoryText(item.content)}`;
104
+ if (seen.has(key)) continue;
105
+ seen.add(key);
106
+ deduped.push(item);
107
+ }
108
+ return deduped;
109
+ }
110
+
111
+ async function readProjectMemoryItems(workspaceRoot = process.cwd(), projectAlias = '') {
112
+ const projectKey = getProjectMemoryKey(workspaceRoot, projectAlias);
113
+ const files = await listProjectMemoryFiles(workspaceRoot);
114
+ const items = [];
115
+ for (const file of files) {
116
+ const bucket = await readMemoryBucket(file);
117
+ items.push(...bucket.map((item) => normalizeMemoryItem(item, 'project', projectKey)));
118
+ }
119
+ return dedupeMemoryItems(items)
120
+ .sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
121
+ }
122
+
123
+ async function readScopeMemoryItems(scope, workspaceRoot = process.cwd(), projectAlias = '') {
124
+ const normalizedScope = ensureScope(scope);
125
+ if (normalizedScope === 'project') return readProjectMemoryItems(workspaceRoot, projectAlias);
126
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
127
+ return (await readMemoryBucket(filePath))
128
+ .map((item) => normalizeMemoryItem(item, normalizedScope, ''))
129
+ .sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
130
+ }
131
+
132
+ function normalizeMemoryItem(item, scope, projectKey = '') {
133
+ const now = nowIso();
134
+ const content = normalizeMemoryText(item?.content || '');
135
+ return {
136
+ id: String(item?.id || `mem_${sha256(`${scope}:${projectKey}:${content}:${now}:${Math.random()}`).slice(0, 12)}`),
137
+ scope,
138
+ projectKey: projectKey || undefined,
139
+ kind: String(item?.kind || 'note').trim() || 'note',
140
+ content,
141
+ summary: normalizeMemoryText(item?.summary || summarizeMemoryContent(content)),
142
+ source: String(item?.source || 'tool').trim() || 'tool',
143
+ confidence: Number.isFinite(Number(item?.confidence)) ? Number(item.confidence) : 0.9,
144
+ createdAt: String(item?.createdAt || now),
145
+ updatedAt: String(item?.updatedAt || now),
146
+ hits: Number.isFinite(Number(item?.hits)) ? Number(item.hits) : 0,
147
+ pinned: item?.pinned === true,
148
+ ...(item?.lifecycle ? { lifecycle: String(item.lifecycle) } : {})
149
+ };
150
+ }
151
+
152
+ function sameMemory(left, right) {
153
+ const a = normalizeMemoryText(left?.content);
154
+ const b = normalizeMemoryText(right?.content);
155
+ if (!a || !b) return false;
156
+ return a === b;
157
+ }
158
+
159
+ function measureMemoryChars(item) {
160
+ return normalizeMemoryText(item?.content).length + normalizeMemoryText(item?.summary).length;
161
+ }
162
+
163
+ function budgetForScope(scope, config = {}) {
164
+ if (scope === 'user') return Math.max(80, Number(config?.memory?.max_user_chars || 1375));
165
+ if (scope === 'global') return Math.max(80, Number(config?.memory?.max_global_chars || 2200));
166
+ return Math.max(80, Number(config?.memory?.max_project_chars || 2200));
167
+ }
168
+
169
+ export async function listMemories({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
170
+ const normalizedScope = ensureScope(scope);
171
+ return readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
172
+ }
173
+
174
+ export async function getMemoryBucketMaintenance({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
175
+ const normalizedScope = ensureScope(scope);
176
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
177
+ const doc = await readMemoryBucketDocument(filePath);
178
+ const items = normalizedScope === 'project'
179
+ ? await readProjectMemoryItems(workspaceRoot, projectAlias)
180
+ : doc.items.map((item) => normalizeMemoryItem(item, normalizedScope, ''));
181
+ const currentHash = memoryBucketHash(items);
182
+ const storedHash = String(doc.maintenance?.contentHash || '');
183
+ const maintainedAt = String(doc.maintenance?.maintainedAt || '');
184
+ return {
185
+ scope: normalizedScope,
186
+ itemCount: items.length,
187
+ contentHash: currentHash,
188
+ storedHash,
189
+ maintainedAt,
190
+ fresh: Boolean(maintainedAt && storedHash && storedHash === currentHash)
191
+ };
192
+ }
193
+
194
+ export async function markMemoryBucketMaintained({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
195
+ const normalizedScope = ensureScope(scope);
196
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
197
+ const items = await readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
198
+ const maintenance = {
199
+ maintainedAt: nowIso(),
200
+ contentHash: memoryBucketHash(items),
201
+ itemCount: items.length
202
+ };
203
+ await writeMemoryBucket(filePath, items, { maintenance });
204
+ return { scope: normalizedScope, ...maintenance };
205
+ }
206
+
207
+ export async function replaceMemoryBucket({
208
+ scope,
209
+ items = [],
210
+ workspaceRoot = process.cwd(),
211
+ projectAlias = '',
212
+ markMaintained = false
213
+ } = {}) {
214
+ const normalizedScope = ensureScope(scope);
215
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
216
+ const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
217
+ const normalizedItems = (Array.isArray(items) ? items : [])
218
+ .map((item) => normalizeMemoryItem(item, normalizedScope, projectKey))
219
+ .filter((item) => item.content);
220
+ const maintenance = markMaintained
221
+ ? {
222
+ maintainedAt: nowIso(),
223
+ contentHash: memoryBucketHash(normalizedItems),
224
+ itemCount: normalizedItems.length
225
+ }
226
+ : null;
227
+ await writeMemoryBucket(filePath, normalizedItems, { maintenance });
228
+ return {
229
+ scope: normalizedScope,
230
+ items: normalizedItems,
231
+ maintenance
232
+ };
233
+ }
234
+
235
+ export async function rememberMemory({
236
+ scope,
237
+ content,
238
+ kind = 'note',
239
+ summary = '',
240
+ source = 'tool',
241
+ confidence = 0.9,
242
+ replaceSimilar = true,
243
+ pinned = false,
244
+ workspaceRoot = process.cwd(),
245
+ projectAlias = '',
246
+ config = {}
247
+ }) {
248
+ const normalizedScope = ensureScope(scope);
249
+ const normalizedContent = normalizeMemoryText(content);
250
+ if (!normalizedContent) throw new Error('Memory content is required');
251
+ assertSafeMemoryContent(normalizedContent);
252
+
253
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
254
+ const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
255
+ const existing = await readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
256
+ const probe = normalizeMemoryItem({ content: normalizedContent, kind, summary, source, confidence, pinned }, normalizedScope, projectKey);
257
+
258
+ const replaceIndex = replaceSimilar ? existing.findIndex((item) => sameMemory(item, probe)) : -1;
259
+ let saved;
260
+ if (replaceIndex >= 0) {
261
+ saved = {
262
+ ...existing[replaceIndex],
263
+ ...probe,
264
+ id: existing[replaceIndex].id,
265
+ createdAt: existing[replaceIndex].createdAt,
266
+ updatedAt: nowIso()
267
+ };
268
+ existing.splice(replaceIndex, 1, saved);
269
+ } else {
270
+ saved = probe;
271
+ existing.unshift(saved);
272
+ }
273
+
274
+ const maxItems = Math.max(1, Number(config?.memory?.max_items_per_scope || 12));
275
+ const maxChars = budgetForScope(normalizedScope, config);
276
+ const deduped = [];
277
+ const seen = new Set();
278
+ for (const item of existing) {
279
+ const key = `${item.kind}:${normalizeMemoryText(item.content)}`;
280
+ if (seen.has(key)) continue;
281
+ seen.add(key);
282
+ deduped.push(item);
283
+ if (deduped.length >= maxItems) break;
284
+ }
285
+ let totalChars = deduped.reduce((sum, item) => sum + measureMemoryChars(item), 0);
286
+ while (deduped.length > 1 && totalChars > maxChars) {
287
+ const removed = deduped.pop();
288
+ totalChars -= measureMemoryChars(removed);
289
+ }
290
+ await writeMemoryBucket(filePath, deduped);
291
+ return saved;
292
+ }
293
+
294
+ export async function forgetMemory({ scope, id, workspaceRoot = process.cwd(), projectAlias = '' }) {
295
+ const normalizedScope = ensureScope(scope);
296
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
297
+ const existing = await listMemories({ scope: normalizedScope, workspaceRoot, projectAlias });
298
+ const kept = existing.filter((item) => item.id !== id);
299
+ await writeMemoryBucket(filePath, kept);
300
+ if (normalizedScope === 'project') {
301
+ const files = (await listProjectMemoryFiles(workspaceRoot)).filter((file) => file !== filePath);
302
+ await Promise.all(files.map(async (file) => {
303
+ const bucket = await readMemoryBucket(file);
304
+ const next = bucket.filter((item) => String(item?.id || '') !== id);
305
+ if (next.length !== bucket.length) await writeMemoryBucket(file, next);
306
+ }));
307
+ }
308
+ return { removed: existing.length - kept.length };
309
+ }
310
+
311
+ export async function searchMemories({ scope, query, workspaceRoot = process.cwd(), projectAlias = '' }) {
312
+ const items = await listMemories({ scope, workspaceRoot, projectAlias });
313
+ const needle = normalizeMemoryText(query).toLowerCase();
314
+ if (!needle) return items;
315
+ return items.filter((item) => item.content.toLowerCase().includes(needle) || item.summary.toLowerCase().includes(needle));
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Dream Loop: inbox capture, lifecycle, archive, promotion
320
+ // ---------------------------------------------------------------------------
321
+
322
+ const VALID_LIFECYCLE = new Set(['observed', 'candidate', 'operational', 'longterm', 'archived']);
323
+ const VALID_INBOX_SCOPES = new Set(['global', 'repo', 'thread', 'project', 'user']);
324
+
325
+ function validateLifecycle(value) {
326
+ const lc = String(value || '').trim().toLowerCase();
327
+ if (!VALID_LIFECYCLE.has(lc)) throw new Error(`Invalid lifecycle state: ${value}`);
328
+ return lc;
329
+ }
330
+
331
+ function normalizeInboxScope(value) {
332
+ const scope = String(value || 'global').trim().toLowerCase();
333
+ if (!VALID_INBOX_SCOPES.has(scope)) throw new Error(`Unsupported inbox scope: ${value}`);
334
+ return scope;
335
+ }
336
+
337
+ function todayDir(baseDir) {
338
+ const date = new Date().toISOString().slice(0, 10);
339
+ return path.join(baseDir, date);
340
+ }
341
+
342
+ async function readJsonArray(filePath) {
343
+ try {
344
+ const raw = await fs.readFile(filePath, 'utf8');
345
+ const parsed = JSON.parse(raw);
346
+ return Array.isArray(parsed) ? parsed : [];
347
+ } catch {
348
+ return [];
349
+ }
350
+ }
351
+
352
+ async function writeJsonArray(filePath, items) {
353
+ await ensureParent(filePath);
354
+ await fs.writeFile(filePath, `${JSON.stringify(items, null, 2)}\n`, 'utf8');
355
+ }
356
+
357
+ export async function captureToInbox({
358
+ scope = 'global',
359
+ type = 'observation',
360
+ summary,
361
+ details = '',
362
+ suggestedAction = '',
363
+ tags = [],
364
+ source = 'tool'
365
+ } = {}) {
366
+ const normalizedSummary = normalizeMemoryText(summary);
367
+ if (!normalizedSummary) throw new Error('Inbox capture summary is required');
368
+ assertSafeMemoryContent(normalizedSummary);
369
+
370
+ const dir = todayDir(getInboxDir());
371
+ await fs.mkdir(dir, { recursive: true });
372
+ const now = nowIso();
373
+ const id = `inbox_${sha256(`${normalizedSummary}:${now}:${Math.random()}`).slice(0, 12)}`;
374
+ const entry = {
375
+ id,
376
+ timestamp: now,
377
+ scope: normalizeInboxScope(scope),
378
+ source,
379
+ type: String(type || 'observation').trim().toLowerCase(),
380
+ summary: normalizedSummary,
381
+ details: normalizeMemoryText(details),
382
+ suggestedAction: normalizeMemoryText(suggestedAction),
383
+ tags: Array.isArray(tags) ? tags.map((t) => String(t).trim()).filter(Boolean) : [],
384
+ lifecycle: 'observed'
385
+ };
386
+
387
+ const indexPath = path.join(dir, 'index.json');
388
+ const entries = await readJsonArray(indexPath);
389
+ entries.push(entry);
390
+ await writeJsonArray(indexPath, entries);
391
+ return entry;
392
+ }
393
+
394
+ export async function listInbox({ since, scope } = {}) {
395
+ const inboxBase = getInboxDir();
396
+ let dayDirs;
397
+ try {
398
+ const entries = await fs.readdir(inboxBase);
399
+ dayDirs = entries.filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
400
+ } catch {
401
+ return [];
402
+ }
403
+ if (since) {
404
+ const sinceStr = String(since).slice(0, 10);
405
+ dayDirs = dayDirs.filter((d) => d >= sinceStr);
406
+ }
407
+ const all = [];
408
+ for (const day of dayDirs) {
409
+ const indexPath = path.join(inboxBase, day, 'index.json');
410
+ const entries = await readJsonArray(indexPath);
411
+ all.push(...entries);
412
+ }
413
+ if (scope) {
414
+ const sc = String(scope).trim().toLowerCase();
415
+ return all.filter((e) => e.scope === sc);
416
+ }
417
+ return all;
418
+ }
419
+
420
+ export async function updateInboxEntry(id, updates = {}) {
421
+ const inboxBase = getInboxDir();
422
+ let dayDirs;
423
+ try {
424
+ dayDirs = (await fs.readdir(inboxBase)).filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
425
+ } catch {
426
+ return null;
427
+ }
428
+ for (const day of dayDirs) {
429
+ const indexPath = path.join(inboxBase, day, 'index.json');
430
+ const entries = await readJsonArray(indexPath);
431
+ const idx = entries.findIndex((e) => e.id === id);
432
+ if (idx === -1) continue;
433
+ if (updates.lifecycle) updates.lifecycle = validateLifecycle(updates.lifecycle);
434
+ entries[idx] = { ...entries[idx], ...updates };
435
+ await writeJsonArray(indexPath, entries);
436
+ return entries[idx];
437
+ }
438
+ return null;
439
+ }
440
+
441
+ export async function removeInboxEntry(id) {
442
+ const inboxBase = getInboxDir();
443
+ let dayDirs;
444
+ try {
445
+ dayDirs = (await fs.readdir(inboxBase)).filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
446
+ } catch {
447
+ return false;
448
+ }
449
+ for (const day of dayDirs) {
450
+ const indexPath = path.join(inboxBase, day, 'index.json');
451
+ const entries = await readJsonArray(indexPath);
452
+ const idx = entries.findIndex((e) => e.id === id);
453
+ if (idx === -1) continue;
454
+ entries.splice(idx, 1);
455
+ await writeJsonArray(indexPath, entries);
456
+ return true;
457
+ }
458
+ return false;
459
+ }
460
+
461
+ export async function archiveEntry(entry, reason = '', auditNote = '') {
462
+ const archiveDir = getArchiveDir();
463
+ const date = new Date().toISOString().slice(0, 10);
464
+ const dir = path.join(archiveDir, date);
465
+ await fs.mkdir(dir, { recursive: true });
466
+ const archived = {
467
+ ...entry,
468
+ lifecycle: 'archived',
469
+ archivedAt: nowIso(),
470
+ archiveReason: normalizeMemoryText(reason),
471
+ auditNote: normalizeMemoryText(auditNote)
472
+ };
473
+ const indexPath = path.join(dir, 'index.json');
474
+ const entries = await readJsonArray(indexPath);
475
+ entries.push(archived);
476
+ await writeJsonArray(indexPath, entries);
477
+ await removeInboxEntry(entry.id);
478
+ return archived;
479
+ }
480
+
481
+ export async function listArchive({ since, scope } = {}) {
482
+ const archiveBase = getArchiveDir();
483
+ let dayDirs;
484
+ try {
485
+ const entries = await fs.readdir(archiveBase);
486
+ dayDirs = entries.filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
487
+ } catch {
488
+ return [];
489
+ }
490
+ if (since) {
491
+ const sinceStr = String(since).slice(0, 10);
492
+ dayDirs = dayDirs.filter((d) => d >= sinceStr);
493
+ }
494
+ const all = [];
495
+ for (const day of dayDirs) {
496
+ const indexPath = path.join(archiveBase, day, 'index.json');
497
+ const entries = await readJsonArray(indexPath);
498
+ all.push(...entries);
499
+ }
500
+ if (scope) {
501
+ const sc = String(scope).trim().toLowerCase();
502
+ return all.filter((e) => e.scope === sc);
503
+ }
504
+ return all;
505
+ }
506
+
507
+ export async function promoteMemory({
508
+ entry,
509
+ scope = 'global',
510
+ lifecycle = 'operational',
511
+ workspaceRoot = process.cwd(),
512
+ projectAlias = '',
513
+ config = {},
514
+ confidence = 0.9
515
+ } = {}) {
516
+ if (!entry?.summary) throw new Error('Entry with summary is required for promotion');
517
+ const lc = validateLifecycle(lifecycle);
518
+ const content = normalizeMemoryText(entry.details || entry.summary);
519
+ const saved = await rememberMemory({
520
+ scope,
521
+ content,
522
+ kind: entry.type || 'note',
523
+ summary: normalizeMemoryText(entry.summary),
524
+ source: `dream-promote:${entry.id}`,
525
+ confidence: Math.min(1, Math.max(0.5, confidence)),
526
+ replaceSimilar: true,
527
+ workspaceRoot,
528
+ projectAlias,
529
+ config
530
+ });
531
+ // Tag the saved item with lifecycle
532
+ const filePath = buildFilePath(scope, workspaceRoot, projectAlias);
533
+ const projectKey = scope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
534
+ const items = (await readMemoryBucket(filePath)).map((item) => normalizeMemoryItem(item, scope, projectKey));
535
+ const target = items.find((item) => item.id === saved.id);
536
+ if (target) {
537
+ target.lifecycle = lc;
538
+ await writeMemoryBucket(filePath, items);
539
+ }
540
+ // Remove from inbox
541
+ await removeInboxEntry(entry.id);
542
+ return { promoted: saved, lifecycle: lc };
543
+ }