baby-daemon 1.0.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/bin/memory.js ADDED
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * bin/memory.js
5
+ * ─────────────
6
+ * CLI tool to search, dump, and archive memories.
7
+ *
8
+ * Usage:
9
+ * memory search "authentication issue" --type bug --since "2 days ago"
10
+ * memory "authentication issue" --type bug
11
+ * memory dump --since "yesterday"
12
+ * memory archive --age 15
13
+ */
14
+
15
+ import path from 'path';
16
+ import fs from 'fs';
17
+ import { fileURLToPath } from 'url';
18
+ import * as chrono from 'chrono-node';
19
+ import { searchMemories, archiveMemories } from '../src/vectorStore.js';
20
+ import { readAllMemories } from '../src/memoryStore.js';
21
+
22
+ // Resolve __dirname equivalent in ES Modules
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+ const PROJECT_ROOT = path.join(__dirname, '..');
25
+ const DEFAULT_LOGS_DIR = path.join(PROJECT_ROOT, 'logs');
26
+
27
+ // CLI Styling Tokens
28
+ const RESET = '\x1b[0m';
29
+ const BOLD = '\x1b[1m';
30
+ const DIM = '\x1b[2m';
31
+ const CYAN = '\x1b[36m';
32
+ const GREEN = '\x1b[32m';
33
+ const YELLOW = '\x1b[33m';
34
+ const RED = '\x1b[31m';
35
+ const BLUE = '\x1b[34m';
36
+ const MAGENTA = '\x1b[35m';
37
+
38
+ // Parse CLI args
39
+ const args = process.argv.slice(2);
40
+
41
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
42
+ printHelp();
43
+ process.exit(0);
44
+ }
45
+
46
+ // ─────────────────────────────────────────────────────────
47
+ // ROUTE SUBCOMMAND
48
+ // ─────────────────────────────────────────────────────────
49
+
50
+ const command = args[0].toLowerCase();
51
+
52
+ if (command === 'dump') {
53
+ await handleDump();
54
+ } else if (command === 'archive') {
55
+ await handleArchive();
56
+ } else if (command === 'read') {
57
+ await handleRead();
58
+ } else {
59
+ // If first argument is 'search', execute search with subsequent args.
60
+ // Otherwise, treat the entire set of args as search (implicit search).
61
+ await handleSearch();
62
+ }
63
+
64
+ // ─────────────────────────────────────────────────────────
65
+ // SEARCH HANDLER
66
+ // ─────────────────────────────────────────────────────────
67
+ async function handleSearch() {
68
+ let queryStartIndex = 0;
69
+ if (args[0].toLowerCase() === 'search') {
70
+ queryStartIndex = 1;
71
+ }
72
+
73
+ // Find the query (everything before the first option flag)
74
+ const queryParts = [];
75
+ let i = queryStartIndex;
76
+ while (i < args.length && !args[i].startsWith('-')) {
77
+ queryParts.push(args[i]);
78
+ i++;
79
+ }
80
+
81
+ const queryText = queryParts.join(' ').trim();
82
+
83
+ if (!queryText) {
84
+ console.error(`\n ${RED}✗ Error:${RESET} No search query specified.\n`);
85
+ printHelp();
86
+ process.exit(1);
87
+ }
88
+
89
+ // Extract flag values
90
+ const sinceVal = getFlagValue('--since', args);
91
+ const typeVal = getFlagValue('--type', args);
92
+ const fileVal = getFlagValue('--file', args);
93
+ const limitVal = getFlagValue('--limit', args) || '5';
94
+
95
+ const limit = parseInt(limitVal, 10);
96
+
97
+ // Parse natural language date using chrono-node
98
+ let sinceDate = null;
99
+ if (sinceVal) {
100
+ const parsed = chrono.parseDate(sinceVal);
101
+ if (!parsed) {
102
+ console.warn(` ${YELLOW}⚠️ Warning:${RESET} Could not parse date description "${sinceVal}". Ignoring date filter.`);
103
+ } else {
104
+ sinceDate = parsed.toISOString();
105
+ }
106
+ }
107
+
108
+ console.log(`\n ${CYAN}🔍 Querying memories...${RESET}`);
109
+ if (sinceVal && sinceDate) console.log(` ${DIM}• Since: ${sinceVal} (${new Date(sinceDate).toLocaleDateString()})${RESET}`);
110
+ if (typeVal) console.log(` ${DIM}• Type: ${typeVal}${RESET}`);
111
+ if (fileVal) console.log(` ${DIM}• File: ${fileVal}${RESET}`);
112
+
113
+ try {
114
+ const searchResult = await searchMemories(queryText, {
115
+ since: sinceDate,
116
+ type: typeVal,
117
+ file: fileVal,
118
+ limit,
119
+ });
120
+
121
+ const { method, results } = searchResult;
122
+
123
+ if (results.length === 0) {
124
+ console.log(`\n ${YELLOW}No matching memories found.${RESET}\n`);
125
+ return;
126
+ }
127
+
128
+ const headerIcon = method === 'semantic' ? '🧠' : '🔍';
129
+ const methodNameText = method === 'semantic' ? 'Semantic Vector Search' : 'Keyword Fallback Search';
130
+
131
+ console.log(`\n ${headerIcon} ${BOLD}${CYAN}[${methodNameText}]${RESET} Found ${results.length} matches:`);
132
+
133
+ results.forEach((item, index) => {
134
+ const displayScore = item.score ? ` (similarity: ${(item.score * 100).toFixed(0)}%)` : '';
135
+ const formattedDate = new Date(item.timestamp).toLocaleString();
136
+ const relativeSource = item.chat_file;
137
+
138
+ console.log(`\n ${CYAN}${BOLD}${index + 1}. [${item.type.toUpperCase()}]${RESET}${displayScore}`);
139
+ console.log(` ${DIM}Date: ${formattedDate} | Source: ${relativeSource}${RESET}`);
140
+ console.log(` ${BOLD}Content:${RESET} ${item.content}`);
141
+ if (item.related_files && item.related_files.length > 0) {
142
+ console.log(` ${DIM}Related files:${RESET} ${item.related_files.join(', ')}`);
143
+ }
144
+ if (item.original_text) {
145
+ console.log(` ${DIM}Evidence:${RESET}`);
146
+ const formattedContext = item.original_text
147
+ .split('\n')
148
+ .map(line => ` | ${line}`)
149
+ .join('\n');
150
+ console.log(`${DIM}${formattedContext}${RESET}`);
151
+ }
152
+ console.log(` ${DIM}─────────────────────────────────────────${RESET}`);
153
+ });
154
+ console.log('');
155
+ } catch (error) {
156
+ console.error(`\n ${RED}✗ Search failed:${RESET}`, error.message);
157
+ }
158
+ }
159
+
160
+ // ─────────────────────────────────────────────────────────
161
+ // DUMP HANDLER
162
+ // ─────────────────────────────────────────────────────────
163
+ async function handleDump() {
164
+ const sinceVal = getFlagValue('--since', args);
165
+ const watchDirVal = getFlagValue('--watch-dir', args);
166
+
167
+ const logsDir = watchDirVal ? path.resolve(watchDirVal) : DEFAULT_LOGS_DIR;
168
+
169
+ if (!fs.existsSync(logsDir)) {
170
+ console.error(`\n ${RED}✗ Error:${RESET} Logs directory not found at: ${logsDir}\n`);
171
+ process.exit(1);
172
+ }
173
+
174
+ let cutoffDate = null;
175
+ if (sinceVal) {
176
+ cutoffDate = chrono.parseDate(sinceVal);
177
+ if (!cutoffDate) {
178
+ console.error(`\n ${RED}✗ Error:${RESET} Could not parse date "${sinceVal}".\n`);
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ console.log(`\n ${MAGENTA}📁 Dumping raw logs from:${RESET} ${logsDir}`);
184
+ if (cutoffDate) {
185
+ console.log(` ${DIM}Filtering: modified since ${cutoffDate.toLocaleString()}${RESET}`);
186
+ }
187
+ console.log(` ${DIM}─────────────────────────────────────────${RESET}`);
188
+
189
+ try {
190
+ const files = fs.readdirSync(logsDir);
191
+ let dumpedCount = 0;
192
+
193
+ for (const file of files) {
194
+ const fullPath = path.join(logsDir, file);
195
+ const stat = fs.statSync(fullPath);
196
+
197
+ if (!stat.isFile()) continue;
198
+
199
+ if (cutoffDate && stat.mtime < cutoffDate) {
200
+ continue;
201
+ }
202
+
203
+ console.log(`\n ${BOLD}${CYAN}📄 Log File: ${file}${RESET}`);
204
+ console.log(` ${DIM}Modified: ${stat.mtime.toLocaleString()} | Size: ${stat.size} bytes${RESET}`);
205
+ console.log(` ${DIM}─────────────────────────────────────────${RESET}`);
206
+
207
+ const content = fs.readFileSync(fullPath, 'utf-8');
208
+ console.log(content.trim() ? content : ' (empty file)');
209
+ console.log(`\n ${DIM}─────────────────────────────────────────${RESET}`);
210
+ dumpedCount++;
211
+ }
212
+
213
+ if (dumpedCount === 0) {
214
+ console.log(`\n ${YELLOW}No files matched dump criteria.${RESET}\n`);
215
+ } else {
216
+ console.log(`\n ${GREEN}✓ Dumped ${dumpedCount} raw log files.${RESET}\n`);
217
+ }
218
+ } catch (error) {
219
+ console.error(`\n ${RED}✗ Dump failed:${RESET}`, error.message);
220
+ }
221
+ }
222
+
223
+ // ─────────────────────────────────────────────────────────
224
+ // ARCHIVE HANDLER
225
+ // ─────────────────────────────────────────────────────────
226
+ async function handleArchive() {
227
+ const ageVal = getFlagValue('--age', args) || '30';
228
+ const ageDays = parseInt(ageVal, 10);
229
+
230
+ if (isNaN(ageDays) || ageDays < 0) {
231
+ console.error(`\n ${RED}✗ Error:${RESET} Invalid age value "${ageVal}".\n`);
232
+ process.exit(1);
233
+ }
234
+
235
+ console.log(`\n ${YELLOW}📦 Running LanceDB archival...${RESET}`);
236
+ console.log(` ${DIM}Archiving memories older than ${ageDays} days...${RESET}\n`);
237
+
238
+ try {
239
+ const stats = await archiveMemories({ ageDays });
240
+ console.log(` ${GREEN}✓ Success:${RESET} ${stats.msg}\n`);
241
+ } catch (error) {
242
+ console.error(` ${RED}✗ Archival failed:${RESET}`, error.message);
243
+ console.log(` ${DIM}(If '@lancedb/lancedb' native bindings are missing on this OS, archival is not supported.)${RESET}\n`);
244
+ }
245
+ }
246
+
247
+ // ─────────────────────────────────────────────────────────
248
+ // READ HANDLER
249
+ // ─────────────────────────────────────────────────────────
250
+ async function handleRead() {
251
+ console.log(`\n ${BOLD}${CYAN}🧠 Loading Project Knowledge Briefing...${RESET}`);
252
+ try {
253
+ const allMemories = readAllMemories();
254
+ const activeMemories = allMemories.filter(m => m.status === 'active');
255
+
256
+ if (activeMemories.length === 0) {
257
+ console.log(`\n ${YELLOW}No active memories found in this project.${RESET}\n`);
258
+ return;
259
+ }
260
+
261
+ console.log(` ${GREEN}Loaded ${activeMemories.length} active memories.${RESET}`);
262
+ console.log(` ${DIM}──────────────────────────────────────────────────${RESET}`);
263
+
264
+ // Grouping
265
+ const groups = {
266
+ decision: [],
267
+ architecture_note: [],
268
+ bug: [],
269
+ resolved_bug: [],
270
+ file_change: [],
271
+ proposed_idea: [],
272
+ open_question: [],
273
+ other: []
274
+ };
275
+
276
+ activeMemories.forEach(mem => {
277
+ const type = mem.type;
278
+ if (groups[type]) {
279
+ groups[type].push(mem);
280
+ } else {
281
+ groups[type] = groups[type] || [];
282
+ groups[type].push(mem);
283
+ }
284
+ });
285
+
286
+ const displayOrder = [
287
+ { key: 'decision', title: 'Decisions', emoji: '📢', color: GREEN },
288
+ { key: 'architecture_note', title: 'Architecture Notes', emoji: '🏗️', color: BLUE },
289
+ { key: 'bug', title: 'Active Bugs', emoji: '🐛', color: RED },
290
+ { key: 'resolved_bug', title: 'Resolved Bugs', emoji: '✅', color: GREEN },
291
+ { key: 'file_change', title: 'File Changes', emoji: '📝', color: CYAN },
292
+ { key: 'proposed_idea', title: 'Proposed Ideas', emoji: '💡', color: YELLOW },
293
+ { key: 'open_question', title: 'Open Questions', emoji: '❓', color: MAGENTA }
294
+ ];
295
+
296
+ // Find any other groups that are not in displayOrder
297
+ const otherKeys = Object.keys(groups).filter(k => !displayOrder.find(o => o.key === k) && k !== 'other');
298
+ otherKeys.forEach(k => {
299
+ if (groups[k] && groups[k].length > 0) {
300
+ groups.other.push(...groups[k]);
301
+ }
302
+ });
303
+
304
+ displayOrder.forEach(({ key, title, emoji, color }) => {
305
+ const items = groups[key] || [];
306
+ if (items.length === 0) return;
307
+
308
+ console.log(`\n ${color}${BOLD}${emoji} ${title.toUpperCase()} (${items.length})${RESET}`);
309
+ console.log(` ${DIM}─────────────────────────────────────────${RESET}`);
310
+
311
+ items.forEach((item, index) => {
312
+ const formattedDate = new Date(item.timestamp).toLocaleDateString();
313
+ const chatFile = item.source?.chat_file || 'unknown source';
314
+
315
+ console.log(` ${color}${BOLD}${index + 1}.${RESET} ${BOLD}${item.content}${RESET}`);
316
+ console.log(` ${DIM}• Source: ${chatFile} (${formattedDate})${RESET}`);
317
+ if (item.related_files && item.related_files.length > 0) {
318
+ console.log(` ${DIM}• Files: ${item.related_files.join(', ')}${RESET}`);
319
+ }
320
+ if (item.tags && item.tags.length > 0) {
321
+ console.log(` ${DIM}• Tags: ${item.tags.join(', ')}${RESET}`);
322
+ }
323
+ });
324
+ });
325
+
326
+ // Handle other/unknown types
327
+ if (groups.other.length > 0) {
328
+ console.log(`\n ${MAGENTA}${BOLD}🔮 Other Memories (${groups.other.length})${RESET}`);
329
+ console.log(` ${DIM}─────────────────────────────────────────${RESET}`);
330
+ groups.other.forEach((item, index) => {
331
+ const formattedDate = new Date(item.timestamp).toLocaleDateString();
332
+ const chatFile = item.source?.chat_file || 'unknown source';
333
+ console.log(` ${MAGENTA}${BOLD}${index + 1}.${RESET} [${item.type.toUpperCase()}] ${BOLD}${item.content}${RESET}`);
334
+ console.log(` ${DIM}• Source: ${chatFile} (${formattedDate})${RESET}`);
335
+ if (item.related_files && item.related_files.length > 0) {
336
+ console.log(` ${DIM}• Files: ${item.related_files.join(', ')}${RESET}`);
337
+ }
338
+ if (item.tags && item.tags.length > 0) {
339
+ console.log(` ${DIM}• Tags: ${item.tags.join(', ')}${RESET}`);
340
+ }
341
+ });
342
+ }
343
+
344
+ console.log(`\n ${DIM}──────────────────────────────────────────────────${RESET}`);
345
+ console.log(` ${GREEN}✓ Done. Use these active memories for project context!${RESET}\n`);
346
+
347
+ } catch (error) {
348
+ console.error(`\n ${RED}✗ Read failed:${RESET}`, error.message);
349
+ }
350
+ }
351
+
352
+ // ─────────────────────────────────────────────────────────
353
+ // HELPERS
354
+ // ─────────────────────────────────────────────────────────
355
+
356
+ function getFlagValue(flag, argList) {
357
+ const index = argList.indexOf(flag);
358
+ if (index !== -1 && index + 1 < argList.length) {
359
+ return argList[index + 1];
360
+ }
361
+ return null;
362
+ }
363
+
364
+ function printHelp() {
365
+ console.log(`
366
+ ${BOLD}Baby Daemon — memory CLI (Phase 3)${RESET}
367
+
368
+ ${BOLD}USAGE:${RESET}
369
+ memory [search] "<query>" [options]
370
+ memory read
371
+ memory dump [options]
372
+ memory archive [options]
373
+
374
+ ${BOLD}SUBCOMMANDS:${RESET}
375
+ ${CYAN}search${RESET} Query stored memories (vector search with keyword fallback)
376
+ ${CYAN}read${RESET} Output all active project memories grouped by category
377
+ ${CYAN}dump${RESET} Output raw log file contents directly (bypassing vector search)
378
+ ${CYAN}archive${RESET} Move old memories to an archive table to optimize searches
379
+
380
+ ${BOLD}SEARCH OPTIONS:${RESET}
381
+ --since "<date>" Filter memories created since natural language date (e.g. "yesterday", "2 days ago")
382
+ --type <type> Filter by category (decision, proposed_idea, bug, resolved_bug, architecture_note)
383
+ --file <filename> Filter by related file path or source chat filename
384
+ --limit <number> Max results to return (default: 5)
385
+
386
+ ${BOLD}DUMP OPTIONS:${RESET}
387
+ --since "<date>" Dump only logs modified since natural language date
388
+ --watch-dir <path> Specify folder to read logs from (default: project logs/ folder)
389
+
390
+ ${BOLD}ARCHIVE OPTIONS:${RESET}
391
+ --age <days> Days threshold to move memories to archive (default: 30)
392
+
393
+ ${BOLD}EXAMPLES:${RESET}
394
+ memory "caching database strategy"
395
+ memory search "auth bug" --type resolved_bug --since "3 days ago"
396
+ memory dump --since "2 hours ago"
397
+ memory archive --age 14
398
+ `);
399
+ }