codemini-cli 0.3.8 → 0.4.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.
@@ -0,0 +1,386 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { LANGUAGE_FILE_TYPES } from './constants.js';
3
+
4
+ const DEFAULT_COMMAND = 'fff-mcp';
5
+ const DEFAULT_TIMEOUT_MS = 15_000;
6
+
7
+ function clampNumber(value, min, max, fallback) {
8
+ const num = Number(value);
9
+ if (!Number.isFinite(num)) return fallback;
10
+ return Math.min(max, Math.max(min, num));
11
+ }
12
+
13
+ function encodeMessage(payload) {
14
+ const body = Buffer.from(JSON.stringify(payload), 'utf8');
15
+ return Buffer.concat([
16
+ Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'utf8'),
17
+ body
18
+ ]);
19
+ }
20
+
21
+ function createMessageParser(onMessage) {
22
+ let buffer = Buffer.alloc(0);
23
+ return (chunk) => {
24
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
25
+ while (buffer.length > 0) {
26
+ const headerEnd = buffer.indexOf('\r\n\r\n');
27
+ if (headerEnd < 0) return;
28
+ const headerText = buffer.slice(0, headerEnd).toString('utf8');
29
+ const match = headerText.match(/Content-Length:\s*(\d+)/i);
30
+ if (!match) {
31
+ buffer = Buffer.alloc(0);
32
+ return;
33
+ }
34
+ const bodyLength = Number(match[1]);
35
+ const totalLength = headerEnd + 4 + bodyLength;
36
+ if (buffer.length < totalLength) return;
37
+ const body = buffer.slice(headerEnd + 4, totalLength).toString('utf8');
38
+ buffer = buffer.slice(totalLength);
39
+ try {
40
+ onMessage(JSON.parse(body));
41
+ } catch {
42
+ // Ignore malformed frames.
43
+ }
44
+ }
45
+ };
46
+ }
47
+
48
+ class FffMcpClient {
49
+ constructor({ workspaceRoot, command, timeoutMs }) {
50
+ this.workspaceRoot = workspaceRoot;
51
+ this.command = command;
52
+ this.timeoutMs = timeoutMs;
53
+ this.child = null;
54
+ this.pending = new Map();
55
+ this.nextId = 1;
56
+ this.connectPromise = null;
57
+ this.connected = false;
58
+ this.closed = false;
59
+ this.parser = createMessageParser((message) => this.handleMessage(message));
60
+ }
61
+
62
+ handleMessage(message) {
63
+ if (!message || typeof message !== 'object') return;
64
+ if (typeof message.id !== 'number') return;
65
+ const pending = this.pending.get(message.id);
66
+ if (!pending) return;
67
+ this.pending.delete(message.id);
68
+ if (message.error) {
69
+ pending.reject(new Error(String(message.error?.message || 'Unknown MCP error')));
70
+ return;
71
+ }
72
+ pending.resolve(message.result);
73
+ }
74
+
75
+ async connect() {
76
+ if (this.connected) return;
77
+ if (this.connectPromise) return this.connectPromise;
78
+ this.connectPromise = this.start();
79
+ try {
80
+ await this.connectPromise;
81
+ this.connected = true;
82
+ } finally {
83
+ this.connectPromise = null;
84
+ }
85
+ }
86
+
87
+ async start() {
88
+ if (this.closed) {
89
+ throw new Error('FFF MCP client already disposed');
90
+ }
91
+ this.child = spawn(this.command, [], {
92
+ cwd: this.workspaceRoot,
93
+ stdio: ['pipe', 'pipe', 'pipe']
94
+ });
95
+
96
+ this.child.stdout.on('data', this.parser);
97
+ this.child.stderr.on('data', () => {});
98
+ this.child.on('error', (error) => {
99
+ this.rejectAll(error);
100
+ });
101
+ this.child.on('exit', (code) => {
102
+ this.connected = false;
103
+ this.child = null;
104
+ if (!this.closed && code !== 0) {
105
+ this.rejectAll(new Error(`FFF MCP exited with code ${code}`));
106
+ }
107
+ });
108
+
109
+ await this.sendRequest('initialize', {
110
+ protocolVersion: '2024-11-05',
111
+ capabilities: {},
112
+ clientInfo: {
113
+ name: 'codemini-cli',
114
+ version: '0.4.0'
115
+ }
116
+ });
117
+ this.sendNotification('notifications/initialized', {});
118
+ }
119
+
120
+ rejectAll(error) {
121
+ for (const { reject, timer } of this.pending.values()) {
122
+ clearTimeout(timer);
123
+ reject(error);
124
+ }
125
+ this.pending.clear();
126
+ }
127
+
128
+ sendNotification(method, params) {
129
+ if (!this.child?.stdin) throw new Error('FFF MCP client is not connected');
130
+ this.child.stdin.write(
131
+ encodeMessage({
132
+ jsonrpc: '2.0',
133
+ method,
134
+ params
135
+ })
136
+ );
137
+ }
138
+
139
+ sendRequest(method, params) {
140
+ if (!this.child?.stdin) {
141
+ return Promise.reject(new Error('FFF MCP client is not connected'));
142
+ }
143
+ return new Promise((resolve, reject) => {
144
+ const id = this.nextId++;
145
+ const timer = setTimeout(() => {
146
+ this.pending.delete(id);
147
+ reject(new Error(`FFF MCP request timed out after ${this.timeoutMs}ms`));
148
+ }, this.timeoutMs);
149
+ this.pending.set(id, {
150
+ resolve: (result) => {
151
+ clearTimeout(timer);
152
+ resolve(result);
153
+ },
154
+ reject: (error) => {
155
+ clearTimeout(timer);
156
+ reject(error);
157
+ },
158
+ timer
159
+ });
160
+ this.child.stdin.write(
161
+ encodeMessage({
162
+ jsonrpc: '2.0',
163
+ id,
164
+ method,
165
+ params
166
+ })
167
+ );
168
+ });
169
+ }
170
+
171
+ async callTool(name, args) {
172
+ await this.connect();
173
+ return this.sendRequest('tools/call', {
174
+ name,
175
+ arguments: args
176
+ });
177
+ }
178
+
179
+ async dispose() {
180
+ this.closed = true;
181
+ this.connected = false;
182
+ if (this.child?.stdin) {
183
+ try {
184
+ this.child.stdin.end();
185
+ } catch {}
186
+ }
187
+ if (this.child && !this.child.killed) {
188
+ this.child.kill();
189
+ }
190
+ this.child = null;
191
+ this.rejectAll(new Error('FFF MCP client disposed'));
192
+ }
193
+ }
194
+
195
+ function extractTextContent(result) {
196
+ const content = Array.isArray(result?.content) ? result.content : [];
197
+ return content
198
+ .filter((item) => item?.type === 'text' && typeof item?.text === 'string')
199
+ .map((item) => item.text)
200
+ .join('\n')
201
+ .trim();
202
+ }
203
+
204
+ function stripFffFileSuffix(line) {
205
+ return String(line || '')
206
+ .replace(/\s+-\s+(?:hot|warm|frequent)(?:\s+git:[a-z_]+)?$/i, '')
207
+ .replace(/\s+git:[a-z_]+$/i, '')
208
+ .trim();
209
+ }
210
+
211
+ function parseFindFilesOutput(text) {
212
+ const lines = String(text || '')
213
+ .split(/\r?\n/)
214
+ .map((line) => line.trimEnd())
215
+ .filter(Boolean);
216
+ const matches = [];
217
+ for (const line of lines) {
218
+ if (
219
+ line.startsWith('→ ') ||
220
+ line.startsWith('cursor:') ||
221
+ /^\d+\/\d+\s+matches$/i.test(line) ||
222
+ /^0 results\b/i.test(line)
223
+ ) {
224
+ continue;
225
+ }
226
+ const normalized = stripFffFileSuffix(line);
227
+ if (normalized) matches.push(normalized);
228
+ }
229
+ return matches;
230
+ }
231
+
232
+ function parseGrepOutput(text, fallbackPattern = '') {
233
+ const lines = String(text || '')
234
+ .split(/\r?\n/)
235
+ .map((line) => line.trimEnd())
236
+ .filter(Boolean);
237
+ const matches = [];
238
+ let currentPath = '';
239
+ for (const line of lines) {
240
+ if (
241
+ line.startsWith('→ ') ||
242
+ line.startsWith('cursor:') ||
243
+ /^! regex failed:/i.test(line) ||
244
+ /^\d+\/\d+\s+matches shown$/i.test(line) ||
245
+ /^0 (?:exact )?matches\b/i.test(line) ||
246
+ /^Auto-broadened to\b/i.test(line)
247
+ ) {
248
+ continue;
249
+ }
250
+ const sectionMatch = line.match(/^\s*(\d+)\s*[:|-]\s*(.*)$/);
251
+ if (sectionMatch && currentPath) {
252
+ const [, lineNumber, preview] = sectionMatch;
253
+ matches.push({
254
+ path: currentPath,
255
+ line: Number(lineNumber),
256
+ column: 1,
257
+ preview: String(preview || '').trim()
258
+ });
259
+ continue;
260
+ }
261
+ const fileCandidate = stripFffFileSuffix(line);
262
+ if (fileCandidate && !/^\d/.test(fileCandidate)) {
263
+ currentPath = fileCandidate;
264
+ }
265
+ }
266
+ return {
267
+ pattern: fallbackPattern,
268
+ matches,
269
+ truncated: /cursor:/i.test(text)
270
+ };
271
+ }
272
+
273
+ function normalizePathPrefix(value) {
274
+ const text = String(value || '').trim().replace(/\\/g, '/').replace(/^\.\/+/, '');
275
+ if (!text || text === '.') return '';
276
+ return text.endsWith('/') ? text : `${text}/`;
277
+ }
278
+
279
+ function buildGrepQuery(pattern, args = {}) {
280
+ const pieces = [];
281
+ const pathPrefix = normalizePathPrefix(args.path);
282
+ if (pathPrefix) pieces.push(pathPrefix);
283
+ const fileTypes = Array.isArray(args.file_types) ? args.file_types : [];
284
+ const language = String(args.language || '').trim().toLowerCase();
285
+ const mergedTypes = [...new Set([...fileTypes, ...(LANGUAGE_FILE_TYPES[language] || [])])];
286
+ if (mergedTypes.length === 1) {
287
+ pieces.push(`*.${mergedTypes[0]}`);
288
+ } else if (mergedTypes.length > 1) {
289
+ pieces.push(`*.{${mergedTypes.join(',')}}`);
290
+ }
291
+ pieces.push(String(pattern || '').trim());
292
+ return pieces.filter(Boolean).join(' ');
293
+ }
294
+
295
+ function buildImmediateItems(relativePath, filePaths, includeHidden = false) {
296
+ const prefix = normalizePathPrefix(relativePath);
297
+ const directories = new Set();
298
+ const files = new Set();
299
+ for (const filePath of filePaths) {
300
+ const normalized = String(filePath || '').replace(/\\/g, '/');
301
+ if (!normalized.startsWith(prefix)) continue;
302
+ const remainder = normalized.slice(prefix.length);
303
+ if (!remainder) continue;
304
+ const [head, ...rest] = remainder.split('/');
305
+ if (!head) continue;
306
+ if (!includeHidden && head.startsWith('.')) continue;
307
+ if (rest.length > 0) {
308
+ directories.add(head);
309
+ } else {
310
+ files.add(head);
311
+ }
312
+ }
313
+ const dirItems = [...directories].sort((a, b) => a.localeCompare(b)).map((name) => ({
314
+ name,
315
+ path: `${prefix}${name}`.replace(/\/$/, ''),
316
+ type: 'dir'
317
+ }));
318
+ const fileItems = [...files].sort((a, b) => a.localeCompare(b)).map((name) => ({
319
+ name,
320
+ path: `${prefix}${name}`,
321
+ type: 'file'
322
+ }));
323
+ return [...dirItems, ...fileItems];
324
+ }
325
+
326
+ export function createFffAdapter({ workspaceRoot, config }) {
327
+ const command = String(config?.search?.fff_command || config?.tooling?.fff_command || DEFAULT_COMMAND).trim() || DEFAULT_COMMAND;
328
+ const timeoutMs = clampNumber(
329
+ config?.search?.fff_timeout_ms || config?.tooling?.fff_timeout_ms,
330
+ 1_000,
331
+ 120_000,
332
+ DEFAULT_TIMEOUT_MS
333
+ );
334
+ const client = new FffMcpClient({ workspaceRoot, command, timeoutMs });
335
+
336
+ return {
337
+ async connect() {
338
+ await client.connect();
339
+ },
340
+
341
+ async grep(args) {
342
+ const pattern = String(args?.pattern || '').trim();
343
+ if (!pattern) return null;
344
+ const result = await client.callTool('grep', {
345
+ query: buildGrepQuery(pattern, args),
346
+ max_results: clampNumber(args?.max_results, 1, 200, 50)
347
+ });
348
+ return parseGrepOutput(extractTextContent(result), pattern);
349
+ },
350
+
351
+ async glob(args) {
352
+ const pattern = String(args?.pattern || '').trim();
353
+ if (!pattern) return null;
354
+ const limit = clampNumber(args?.max_results, 1, 500, 200);
355
+ const result = await client.callTool('find_files', {
356
+ query: pattern,
357
+ max_results: limit
358
+ });
359
+ const matches = parseFindFilesOutput(extractTextContent(result));
360
+ return {
361
+ pattern,
362
+ matches,
363
+ truncated: matches.length >= limit
364
+ };
365
+ },
366
+
367
+ async list(args) {
368
+ const relativePath = String(args?.path || '.').trim();
369
+ if (!relativePath || relativePath === '.') return null;
370
+ const result = await client.callTool('find_files', {
371
+ query: normalizePathPrefix(relativePath),
372
+ max_results: 500
373
+ });
374
+ const filePaths = parseFindFilesOutput(extractTextContent(result));
375
+ return {
376
+ path: relativePath,
377
+ items: buildImmediateItems(relativePath, filePaths, Boolean(args?.include_hidden))
378
+ };
379
+ },
380
+
381
+ async dispose() {
382
+ await client.dispose();
383
+ }
384
+ };
385
+ }
386
+
@@ -11,6 +11,15 @@ function renderScope(title, items = []) {
11
11
  return `${title}\n${lines.join('\n')}`;
12
12
  }
13
13
 
14
+ function renderLifecycleGroup(title, items = []) {
15
+ if (!Array.isArray(items) || items.length === 0) return '';
16
+ const lines = items.map((item) => {
17
+ const prefix = item.lifecycle ? `[${item.lifecycle}]` : '';
18
+ return `- ${prefix} ${JSON.stringify(String(item.summary || item.content || ''))}`;
19
+ });
20
+ return `${title}\n${lines.join('\n')}`;
21
+ }
22
+
14
23
  export async function buildMemorySnapshot({
15
24
  config = {},
16
25
  workspaceRoot = process.cwd()
@@ -24,12 +33,26 @@ export async function buildMemorySnapshot({
24
33
  ]);
25
34
 
26
35
  const maxItems = Math.max(1, Number(config?.memory?.max_items_per_scope || 12));
36
+
37
+ // Separate lifecycle-tagged items for projections
38
+ const allItems = [...user, ...globalItems, ...project];
39
+ const operational = allItems.filter((item) => item.lifecycle === 'operational');
40
+ const longterm = allItems.filter((item) => item.lifecycle === 'longterm');
41
+
27
42
  const sections = [
28
43
  renderScope('User Memory:', user.slice(0, maxItems)),
29
44
  renderScope('Global Memory:', globalItems.slice(0, maxItems)),
30
45
  renderScope('Project Memory:', project.slice(0, maxItems))
31
46
  ].filter(Boolean);
32
47
 
48
+ // Add lifecycle projection sections
49
+ if (operational.length > 0) {
50
+ sections.push(renderLifecycleGroup('Active Guidance (Operational — temporary but important for current phase):', operational));
51
+ }
52
+ if (longterm.length > 0) {
53
+ sections.push(renderLifecycleGroup('Stable Learnings (LongTerm — proven patterns across tasks):', longterm));
54
+ }
55
+
33
56
  if (sections.length === 0) return '';
34
57
 
35
58
  const snapshot = [
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { sha256 } from './crypto-utils.js';
4
- import { getMemoryDir, getProjectMemoryDir } from './paths.js';
4
+ import { getMemoryDir, getProjectMemoryDir, getInboxDir, getArchiveDir } from './paths.js';
5
5
  import { assertSafeMemoryContent, normalizeMemoryText, summarizeMemoryContent } from './memory-policy.js';
6
6
 
7
7
  const ALLOWED_SCOPES = new Set(['user', 'global', 'project']);
@@ -179,3 +179,230 @@ export async function searchMemories({ scope, query, workspaceRoot = process.cwd
179
179
  if (!needle) return items;
180
180
  return items.filter((item) => item.content.toLowerCase().includes(needle) || item.summary.toLowerCase().includes(needle));
181
181
  }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Dream Loop: inbox capture, lifecycle, archive, promotion
185
+ // ---------------------------------------------------------------------------
186
+
187
+ const VALID_LIFECYCLE = new Set(['observed', 'candidate', 'operational', 'longterm', 'archived']);
188
+ const VALID_INBOX_SCOPES = new Set(['global', 'repo', 'thread', 'project', 'user']);
189
+
190
+ function validateLifecycle(value) {
191
+ const lc = String(value || '').trim().toLowerCase();
192
+ if (!VALID_LIFECYCLE.has(lc)) throw new Error(`Invalid lifecycle state: ${value}`);
193
+ return lc;
194
+ }
195
+
196
+ function normalizeInboxScope(value) {
197
+ const scope = String(value || 'global').trim().toLowerCase();
198
+ if (!VALID_INBOX_SCOPES.has(scope)) throw new Error(`Unsupported inbox scope: ${value}`);
199
+ return scope;
200
+ }
201
+
202
+ function todayDir(baseDir) {
203
+ const date = new Date().toISOString().slice(0, 10);
204
+ return path.join(baseDir, date);
205
+ }
206
+
207
+ async function readJsonArray(filePath) {
208
+ try {
209
+ const raw = await fs.readFile(filePath, 'utf8');
210
+ const parsed = JSON.parse(raw);
211
+ return Array.isArray(parsed) ? parsed : [];
212
+ } catch {
213
+ return [];
214
+ }
215
+ }
216
+
217
+ async function writeJsonArray(filePath, items) {
218
+ await ensureParent(filePath);
219
+ await fs.writeFile(filePath, `${JSON.stringify(items, null, 2)}\n`, 'utf8');
220
+ }
221
+
222
+ export async function captureToInbox({
223
+ scope = 'global',
224
+ type = 'observation',
225
+ summary,
226
+ details = '',
227
+ suggestedAction = '',
228
+ tags = [],
229
+ source = 'tool'
230
+ } = {}) {
231
+ const normalizedSummary = normalizeMemoryText(summary);
232
+ if (!normalizedSummary) throw new Error('Inbox capture summary is required');
233
+ assertSafeMemoryContent(normalizedSummary);
234
+
235
+ const dir = todayDir(getInboxDir());
236
+ await fs.mkdir(dir, { recursive: true });
237
+ const now = nowIso();
238
+ const id = `inbox_${sha256(`${normalizedSummary}:${now}:${Math.random()}`).slice(0, 12)}`;
239
+ const entry = {
240
+ id,
241
+ timestamp: now,
242
+ scope: normalizeInboxScope(scope),
243
+ source,
244
+ type: String(type || 'observation').trim().toLowerCase(),
245
+ summary: normalizedSummary,
246
+ details: normalizeMemoryText(details),
247
+ suggestedAction: normalizeMemoryText(suggestedAction),
248
+ tags: Array.isArray(tags) ? tags.map((t) => String(t).trim()).filter(Boolean) : [],
249
+ lifecycle: 'observed'
250
+ };
251
+
252
+ const indexPath = path.join(dir, 'index.json');
253
+ const entries = await readJsonArray(indexPath);
254
+ entries.push(entry);
255
+ await writeJsonArray(indexPath, entries);
256
+ return entry;
257
+ }
258
+
259
+ export async function listInbox({ since, scope } = {}) {
260
+ const inboxBase = getInboxDir();
261
+ let dayDirs;
262
+ try {
263
+ const entries = await fs.readdir(inboxBase);
264
+ dayDirs = entries.filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
265
+ } catch {
266
+ return [];
267
+ }
268
+ if (since) {
269
+ const sinceStr = String(since).slice(0, 10);
270
+ dayDirs = dayDirs.filter((d) => d >= sinceStr);
271
+ }
272
+ const all = [];
273
+ for (const day of dayDirs) {
274
+ const indexPath = path.join(inboxBase, day, 'index.json');
275
+ const entries = await readJsonArray(indexPath);
276
+ all.push(...entries);
277
+ }
278
+ if (scope) {
279
+ const sc = String(scope).trim().toLowerCase();
280
+ return all.filter((e) => e.scope === sc);
281
+ }
282
+ return all;
283
+ }
284
+
285
+ export async function updateInboxEntry(id, updates = {}) {
286
+ const inboxBase = getInboxDir();
287
+ let dayDirs;
288
+ try {
289
+ dayDirs = (await fs.readdir(inboxBase)).filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
290
+ } catch {
291
+ return null;
292
+ }
293
+ for (const day of dayDirs) {
294
+ const indexPath = path.join(inboxBase, day, 'index.json');
295
+ const entries = await readJsonArray(indexPath);
296
+ const idx = entries.findIndex((e) => e.id === id);
297
+ if (idx === -1) continue;
298
+ if (updates.lifecycle) updates.lifecycle = validateLifecycle(updates.lifecycle);
299
+ entries[idx] = { ...entries[idx], ...updates };
300
+ await writeJsonArray(indexPath, entries);
301
+ return entries[idx];
302
+ }
303
+ return null;
304
+ }
305
+
306
+ export async function removeInboxEntry(id) {
307
+ const inboxBase = getInboxDir();
308
+ let dayDirs;
309
+ try {
310
+ dayDirs = (await fs.readdir(inboxBase)).filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
311
+ } catch {
312
+ return false;
313
+ }
314
+ for (const day of dayDirs) {
315
+ const indexPath = path.join(inboxBase, day, 'index.json');
316
+ const entries = await readJsonArray(indexPath);
317
+ const idx = entries.findIndex((e) => e.id === id);
318
+ if (idx === -1) continue;
319
+ entries.splice(idx, 1);
320
+ await writeJsonArray(indexPath, entries);
321
+ return true;
322
+ }
323
+ return false;
324
+ }
325
+
326
+ export async function archiveEntry(entry, reason = '', auditNote = '') {
327
+ const archiveDir = getArchiveDir();
328
+ const date = new Date().toISOString().slice(0, 10);
329
+ const dir = path.join(archiveDir, date);
330
+ await fs.mkdir(dir, { recursive: true });
331
+ const archived = {
332
+ ...entry,
333
+ lifecycle: 'archived',
334
+ archivedAt: nowIso(),
335
+ archiveReason: normalizeMemoryText(reason),
336
+ auditNote: normalizeMemoryText(auditNote)
337
+ };
338
+ const indexPath = path.join(dir, 'index.json');
339
+ const entries = await readJsonArray(indexPath);
340
+ entries.push(archived);
341
+ await writeJsonArray(indexPath, entries);
342
+ await removeInboxEntry(entry.id);
343
+ return archived;
344
+ }
345
+
346
+ export async function listArchive({ since, scope } = {}) {
347
+ const archiveBase = getArchiveDir();
348
+ let dayDirs;
349
+ try {
350
+ const entries = await fs.readdir(archiveBase);
351
+ dayDirs = entries.filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort();
352
+ } catch {
353
+ return [];
354
+ }
355
+ if (since) {
356
+ const sinceStr = String(since).slice(0, 10);
357
+ dayDirs = dayDirs.filter((d) => d >= sinceStr);
358
+ }
359
+ const all = [];
360
+ for (const day of dayDirs) {
361
+ const indexPath = path.join(archiveBase, day, 'index.json');
362
+ const entries = await readJsonArray(indexPath);
363
+ all.push(...entries);
364
+ }
365
+ if (scope) {
366
+ const sc = String(scope).trim().toLowerCase();
367
+ return all.filter((e) => e.scope === sc);
368
+ }
369
+ return all;
370
+ }
371
+
372
+ export async function promoteMemory({
373
+ entry,
374
+ scope = 'global',
375
+ lifecycle = 'operational',
376
+ workspaceRoot = process.cwd(),
377
+ projectAlias = '',
378
+ config = {},
379
+ confidence = 0.9
380
+ } = {}) {
381
+ if (!entry?.summary) throw new Error('Entry with summary is required for promotion');
382
+ const lc = validateLifecycle(lifecycle);
383
+ const content = normalizeMemoryText(entry.details || entry.summary);
384
+ const saved = await rememberMemory({
385
+ scope,
386
+ content,
387
+ kind: entry.type || 'note',
388
+ summary: normalizeMemoryText(entry.summary),
389
+ source: `dream-promote:${entry.id}`,
390
+ confidence: Math.min(1, Math.max(0.5, confidence)),
391
+ replaceSimilar: true,
392
+ workspaceRoot,
393
+ projectAlias,
394
+ config
395
+ });
396
+ // Tag the saved item with lifecycle
397
+ const filePath = buildFilePath(scope, workspaceRoot, projectAlias);
398
+ const projectKey = scope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
399
+ const items = (await readMemoryBucket(filePath)).map((item) => normalizeMemoryItem(item, scope, projectKey));
400
+ const target = items.find((item) => item.id === saved.id);
401
+ if (target) {
402
+ target.lifecycle = lc;
403
+ await writeMemoryBucket(filePath, items);
404
+ }
405
+ // Remove from inbox
406
+ await removeInboxEntry(entry.id);
407
+ return { promoted: saved, lifecycle: lc };
408
+ }
package/src/core/paths.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  const GLOBAL_APP_DIR = 'codemini-global';
5
5
  const PROJECT_APP_DIR = '.codemini';
6
- const PROJECT_INDEX_DIR = '.codemini-project';
6
+ const PROJECT_INDEX_DIR = '.codemini';
7
7
 
8
8
  export function getBaseConfigDir() {
9
9
  if (process.env.CODEMINI_GLOBAL_DIR) {
@@ -109,3 +109,15 @@ export function getProjectIndexDir(cwd = process.cwd()) {
109
109
  export function getProjectMemoryDir(cwd = process.cwd()) {
110
110
  return path.join(getProjectIndexDir(cwd), 'memory');
111
111
  }
112
+
113
+ export function getInboxDir() {
114
+ return path.join(getMemoryDir(), 'inbox');
115
+ }
116
+
117
+ export function getArchiveDir() {
118
+ return path.join(getMemoryDir(), 'archive');
119
+ }
120
+
121
+ export function getDreamAuditDir() {
122
+ return path.join(getMemoryDir(), 'audit');
123
+ }
@@ -387,7 +387,7 @@ export async function initializeProjectIndex(cwd = process.cwd()) {
387
387
  projectRoot: targetRoot,
388
388
  projectMap,
389
389
  fileIndex,
390
- summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini-project (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
390
+ summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
391
391
  };
392
392
  })();
393
393
  initCache.set(cacheKey, promise);
@@ -447,7 +447,7 @@ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '')
447
447
  path: projectRelativePath,
448
448
  projectRoot,
449
449
  action,
450
- summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini-project for ${projectRelativePath}`
450
+ summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini for ${projectRelativePath}`
451
451
  };
452
452
  }
453
453