@rlabs-inc/memory 0.5.13 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlabs-inc/memory",
3
- "version": "0.5.13",
3
+ "version": "0.6.0",
4
4
  "description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,6 +42,7 @@
42
42
  "@anthropic-ai/claude-agent-sdk": "^0.2.12",
43
43
  "@huggingface/transformers": "^3.4.1",
44
44
  "@rlabs-inc/fsdb": "^1.0.1",
45
+ "@rlabs-inc/prism": "^0.4.0",
45
46
  "@rlabs-inc/signals": "^1.0.0"
46
47
  },
47
48
  "devDependencies": {
@@ -183,6 +183,15 @@ export async function createServer(config: ServerConfig = {}) {
183
183
  }
184
184
  }
185
185
 
186
+ // Verbose: show the full payload that gets injected into AI context
187
+ if (result.formatted) {
188
+ logger.logInjectedPayload(
189
+ result.formatted,
190
+ result.primer ? 'primer' : (mode === 'action_items' ? 'action_items' : 'memories'),
191
+ result.memories.length
192
+ )
193
+ }
194
+
186
195
  return Response.json({
187
196
  success: true,
188
197
  session_id: body.session_id,
@@ -359,6 +368,15 @@ export async function createServer(config: ServerConfig = {}) {
359
368
  filesWritten: managementResult.filesWritten,
360
369
  },
361
370
  })
371
+
372
+ // Notify @rlabs-inc/mind (fire-and-forget, never blocks memory pipeline)
373
+ if (process.env.MEMORY_MIND_WEBHOOK_URL) {
374
+ fetch(process.env.MEMORY_MIND_WEBHOOK_URL, {
375
+ method: 'POST',
376
+ headers: { 'Content-Type': 'application/json' },
377
+ body: JSON.stringify({ projectId: body.project_id, sessionNumber, curationResult: result, managementResult }),
378
+ }).catch(() => {})
379
+ }
362
380
  } catch (error) {
363
381
  logger.error(`Management failed: ${error}`)
364
382
  }
@@ -1,44 +1,35 @@
1
1
  // ============================================================================
2
2
  // LOGGER - Beautiful console output for the memory system
3
- // Uses Node's built-in util.styleText for proper terminal support
3
+ // Powered by @rlabs-inc/prism for terminal rendering
4
4
  // ============================================================================
5
5
 
6
- import { styleText } from 'util'
7
-
8
- type Style = Parameters<typeof styleText>[0]
9
-
10
- const style = (format: Style, text: string): string => styleText(format, text)
6
+ import { s, log, box, table, badge, divider as prismDivider, kv, writeln } from '@rlabs-inc/prism'
11
7
 
12
8
  /**
13
9
  * Format a timestamp (HH:MM:SS)
14
10
  */
15
- function timestamp(): string {
16
- return style('dim', new Date().toISOString().slice(11, 19))
11
+ function ts(): string {
12
+ return s.dim(new Date().toISOString().slice(11, 19))
17
13
  }
18
14
 
19
15
  /**
20
16
  * Format a short session ID
21
17
  */
22
18
  function shortId(id: string): string {
23
- return style('dim', id.slice(0, 8) + '...')
19
+ return s.dim(id.slice(0, 8) + '...')
24
20
  }
25
21
 
26
22
  /**
27
- * Symbols
23
+ * Emoji map for quick visual scanning of context types
28
24
  */
29
- const sym = {
30
- brain: '🧠',
31
- sparkles: '',
32
- book: '📖',
33
- calendar: '📅',
34
- arrow: '',
35
- check: '',
36
- cross: '',
37
- warning: '⚠',
38
- info: 'ℹ',
39
- bullet: '•',
40
- fire: '🔥',
41
- target: '🎯',
25
+ const emojiMap: Record<string, string> = {
26
+ breakthrough: '💡', decision: '⚖️', personal: '💜', technical: '🔧',
27
+ technical_state: '📍', unresolved: '❓', preference: '⚙️', workflow: '🔄',
28
+ architectural: '🏗️', debugging: '🐛', philosophy: '🌀', todo: '🎯',
29
+ implementation: '', problem_solution: '✅', project_context: '📦',
30
+ milestone: '🏆', general: '📝', project_state: '📍', pending_task: '⏳',
31
+ work_in_progress: '🔨', system_feedback: '📣', project_milestone: '🏆',
32
+ architectural_insight: '🏛️', architectural_direction: '🧭',
42
33
  }
43
34
 
44
35
  /**
@@ -68,81 +59,79 @@ export const logger = {
68
59
  * Debug message (only shown in verbose mode)
69
60
  */
70
61
  debug(message: string, prefix?: string) {
71
- if (_verbose) {
72
- const pfx = prefix ? style('dim', `[${prefix}] `) : ''
73
- console.log(`${style('dim', `🔍 ${pfx}${message}`)}`)
74
- }
62
+ if (!_verbose) return
63
+ log.debug(prefix ? `[${prefix}] ${message}` : message)
75
64
  },
76
65
 
77
66
  /**
78
67
  * Info message
79
68
  */
80
69
  info(message: string) {
81
- console.log(`${timestamp()} ${style('cyan', sym.info)} ${message}`)
70
+ log.info(message, { timestamp: true })
82
71
  },
83
72
 
84
73
  /**
85
74
  * Success message
86
75
  */
87
76
  success(message: string) {
88
- console.log(`${timestamp()} ${style('green', sym.check)} ${message}`)
77
+ log.success(message, { timestamp: true })
89
78
  },
90
79
 
91
80
  /**
92
81
  * Warning message
93
82
  */
94
83
  warn(message: string) {
95
- console.log(`${timestamp()} ${style('yellow', sym.warning)} ${message}`)
84
+ log.warn(message, { timestamp: true })
96
85
  },
97
86
 
98
87
  /**
99
88
  * Error message
100
89
  */
101
90
  error(message: string) {
102
- console.error(`${timestamp()} ${style('red', sym.cross)} ${message}`)
91
+ log.error(message, { timestamp: true })
103
92
  },
104
93
 
105
94
  /**
106
95
  * Memory event (curation, storage)
107
96
  */
108
97
  memory(message: string) {
109
- console.log(`${timestamp()} ${style('magenta', sym.brain)} ${message}`)
98
+ writeln(`${ts()} ${s.magenta('🧠')} ${message}`)
110
99
  },
111
100
 
112
101
  /**
113
102
  * Injection event (memories surfaced)
114
103
  */
115
104
  inject(message: string) {
116
- console.log(`${timestamp()} ${style('cyan', sym.sparkles)} ${message}`)
105
+ writeln(`${ts()} ${s.cyan('')} ${message}`)
117
106
  },
118
107
 
119
108
  /**
120
109
  * Session event
121
110
  */
122
111
  session(message: string) {
123
- console.log(`${timestamp()} ${style('blue', sym.calendar)} ${message}`)
112
+ writeln(`${ts()} ${s.blue('📅')} ${message}`)
124
113
  },
125
114
 
126
115
  /**
127
116
  * Primer shown
128
117
  */
129
118
  primer(message: string) {
130
- console.log(`${timestamp()} ${style('yellow', sym.book)} ${message}`)
119
+ writeln(`${ts()} ${s.yellow('📖')} ${message}`)
131
120
  },
132
121
 
133
122
  /**
134
123
  * Divider line
135
124
  */
136
125
  divider() {
137
- console.log(style('dim', '─'.repeat(60)))
126
+ writeln(prismDivider())
138
127
  },
139
128
 
140
129
  /**
141
130
  * Request received (incoming)
142
131
  */
143
132
  request(method: string, path: string, projectId?: string) {
144
- const proj = projectId ? style('dim', ` [${projectId}]`) : ''
145
- console.log(`${timestamp()} ${style('dim', sym.arrow)} ${style('cyan', method)} ${path}${proj}`)
133
+ const proj = projectId ? s.dim(` [${projectId}]`) : ''
134
+ writeln(`${ts()} ${s.dim('')} ${s.cyan(method)} ${path}${proj}`)
146
135
  },
147
136
 
148
137
  /**
@@ -155,34 +144,34 @@ export const logger = {
155
144
  semantic_tags?: string[]
156
145
  action_required?: boolean
157
146
  }>) {
158
- console.log()
159
- console.log(`${timestamp()} ${style('magenta', sym.brain)} ${style(['bold', 'magenta'], `CURATED ${memories.length} MEMORIES`)}`)
160
- console.log()
147
+ writeln()
148
+ writeln(`${ts()} ${s.magenta('🧠')} ${s.bold.magenta(`CURATED ${memories.length} MEMORIES`)}`)
149
+ writeln()
161
150
 
162
151
  memories.forEach((m, i) => {
163
- const importance = style('yellow', `${(m.importance_weight * 100).toFixed(0)}%`)
164
- const type = style('cyan', m.context_type.toUpperCase())
165
- const num = style('dim', `${i + 1}.`)
152
+ const num = s.dim(`${i + 1}.`)
153
+ const type = badge(m.context_type.toUpperCase(), { color: s.cyan, variant: 'bracket' })
154
+ const importance = s.yellow(`${(m.importance_weight * 100).toFixed(0)}%`)
166
155
 
167
- console.log(` ${num} [${type}] ${importance}`)
156
+ writeln(` ${num} ${type} ${importance}`)
168
157
 
169
- // Content preview
170
- const preview = m.content.length > 70
171
- ? m.content.slice(0, 70) + style('dim', '...')
158
+ // Content: full in verbose, truncated in normal
159
+ const content = !_verbose && m.content.length > 70
160
+ ? m.content.slice(0, 70) + s.dim('...')
172
161
  : m.content
173
- console.log(` ${style('white', preview)}`)
162
+ writeln(` ${content}`)
174
163
 
175
164
  // Tags
176
165
  if (m.semantic_tags?.length) {
177
- const tags = m.semantic_tags.slice(0, 4).join(style('dim', ', '))
178
- console.log(` ${style('dim', 'tags:')} ${tags}`)
166
+ const tags = m.semantic_tags.slice(0, _verbose ? 8 : 4).join(s.dim(', '))
167
+ writeln(` ${s.dim('tags:')} ${tags}`)
179
168
  }
180
169
 
181
170
  // Special flags
182
171
  if (m.action_required) {
183
- console.log(` ${style('red', '⚡ ACTION REQUIRED')}`)
172
+ writeln(` ${s.red('⚡ ACTION REQUIRED')}`)
184
173
  }
185
- console.log()
174
+ writeln()
186
175
  })
187
176
  },
188
177
 
@@ -194,88 +183,79 @@ export const logger = {
194
183
  score: number // signals.count / 6
195
184
  context_type: string
196
185
  }>, query: string) {
197
- const queryPreview = query.length > 40
186
+ const queryPreview = !_verbose && query.length > 40
198
187
  ? query.slice(0, 40) + '...'
199
188
  : query
200
189
 
201
- // Emoji map for quick visual scanning
202
- const emojiMap: Record<string, string> = {
203
- breakthrough: '💡', decision: '⚖️', personal: '💜', technical: '🔧',
204
- technical_state: '📍', unresolved: '❓', preference: '⚙️', workflow: '🔄',
205
- architectural: '🏗️', debugging: '🐛', philosophy: '🌀', todo: '🎯',
206
- implementation: '⚡', problem_solution: '✅', project_context: '📦',
207
- milestone: '🏆', general: '📝', project_state: '📍', pending_task: '⏳',
208
- work_in_progress: '🔨', system_feedback: '📣', project_milestone: '🏆',
209
- architectural_insight: '🏛️', architectural_direction: '🧭',
210
- }
211
-
212
- console.log()
213
- console.log(`${timestamp()} ${style('cyan', sym.sparkles)} ${style('bold', `SURFACING ${memories.length} MEMORIES`)}`)
214
- console.log(` ${style('dim', 'query:')} "${queryPreview}"`)
215
- console.log()
190
+ writeln()
191
+ writeln(`${ts()} ${s.cyan('✨')} ${s.bold(`SURFACING ${memories.length} MEMORIES`)}`)
192
+ writeln(` ${s.dim('query:')} "${queryPreview}"`)
193
+ writeln()
216
194
 
217
195
  if (memories.length === 0) {
218
- console.log(` ${style('dim', '(no relevant memories for this context)')}`)
219
- console.log()
196
+ writeln(` ${s.dim('(no relevant memories for this context)')}`)
197
+ writeln()
220
198
  return
221
199
  }
222
200
 
223
201
  memories.forEach((m, i) => {
224
- // Convert score back to signal count (score is count/6)
225
202
  const signalCount = Math.round(m.score * 6)
226
- const signalStr = style('green', `${signalCount}sig`)
203
+ const signalBadge = badge(`${signalCount}sig`, { color: s.green, variant: 'bracket' })
227
204
  const emoji = emojiMap[m.context_type?.toLowerCase()] ?? '📝'
228
- const num = style('dim', `${i + 1}.`)
205
+ const num = s.dim(`${i + 1}.`)
229
206
 
230
- const preview = m.content.length > 55
231
- ? m.content.slice(0, 55) + style('dim', '...')
207
+ // Content: full in verbose, truncated in normal
208
+ const content = !_verbose && m.content.length > 55
209
+ ? m.content.slice(0, 55) + s.dim('...')
232
210
  : m.content
233
211
 
234
- console.log(` ${num} [${signalStr}] ${emoji}`)
235
- console.log(` ${preview}`)
212
+ writeln(` ${num} ${signalBadge} ${emoji}`)
213
+ writeln(` ${content}`)
236
214
  })
237
- console.log()
215
+ writeln()
238
216
  },
239
217
 
240
218
  /**
241
219
  * Log server startup
242
220
  */
243
221
  startup(port: number, host: string, mode: string) {
244
- console.log()
245
- console.log(style(['bold', 'magenta'], '┌──────────────────────────────────────────────────────────┐'))
246
- console.log(style(['bold', 'magenta'], '│') + style('bold', ` ${sym.brain} MEMORY SERVER `) + style(['bold', 'magenta'], '│'))
247
- console.log(style(['bold', 'magenta'], '└──────────────────────────────────────────────────────────┘'))
248
- console.log()
249
- console.log(` ${style('dim', 'url:')} ${style('cyan', `http://${host}:${port}`)}`)
250
- console.log(` ${style('dim', 'storage:')} ${mode}`)
251
- console.log(` ${style('dim', 'engine:')} TypeScript + fsdb`)
252
- console.log()
253
- this.divider()
254
- console.log()
222
+ const info = kv([
223
+ ['url', s.cyan(`http://${host}:${port}`)],
224
+ ['storage', mode],
225
+ ['engine', 'TypeScript + fsdb'],
226
+ ['verbose', _verbose ? s.green('on') : s.dim('off')],
227
+ ], { keyColor: s.dim, indent: 1 })
228
+
229
+ writeln()
230
+ writeln(box(info, {
231
+ border: 'double',
232
+ borderColor: 'magenta',
233
+ title: '🧠 Memory Server',
234
+ titleColor: s.bold,
235
+ }))
236
+ writeln()
255
237
  },
256
238
 
257
239
  /**
258
240
  * Log session start
259
241
  */
260
242
  logSessionStart(sessionId: string, projectId: string, isNew: boolean) {
261
- const status = isNew
262
- ? style('green', 'new session')
263
- : style('blue', 'continuing')
264
-
265
- console.log()
266
- console.log(`${timestamp()} ${style('blue', sym.calendar)} ${style('bold', 'SESSION')} ${shortId(sessionId)}`)
267
- console.log(` ${style('dim', 'project:')} ${projectId}`)
268
- console.log(` ${style('dim', 'status:')} ${status}`)
269
- console.log()
243
+ const status = isNew ? s.green('new session') : s.blue('continuing')
244
+
245
+ writeln()
246
+ writeln(`${ts()} ${s.blue('📅')} ${s.bold('SESSION')} ${shortId(sessionId)}`)
247
+ writeln(` ${s.dim('project:')} ${projectId}`)
248
+ writeln(` ${s.dim('status:')} ${status}`)
249
+ writeln()
270
250
  },
271
251
 
272
252
  /**
273
253
  * Log curation start
274
254
  */
275
255
  logCurationStart(sessionId: string, trigger: string) {
276
- console.log()
277
- console.log(`${timestamp()} ${style('magenta', sym.brain)} ${style('bold', 'CURATING')} ${shortId(sessionId)}`)
278
- console.log(` ${style('dim', 'trigger:')} ${trigger}`)
256
+ writeln()
257
+ writeln(`${ts()} ${s.magenta('🧠')} ${s.bold('CURATING')} ${shortId(sessionId)}`)
258
+ writeln(` ${s.dim('trigger:')} ${trigger}`)
279
259
  },
280
260
 
281
261
  /**
@@ -283,31 +263,31 @@ export const logger = {
283
263
  */
284
264
  logCurationComplete(memoriesCount: number, summary?: string) {
285
265
  if (memoriesCount > 0) {
286
- console.log(` ${style('dim', 'memories:')} ${style('green', String(memoriesCount))} extracted`)
266
+ writeln(` ${s.dim('memories:')} ${s.green(String(memoriesCount))} extracted`)
287
267
  if (summary) {
288
- const shortSummary = summary.length > 50
268
+ const text = !_verbose && summary.length > 50
289
269
  ? summary.slice(0, 50) + '...'
290
270
  : summary
291
- console.log(` ${style('dim', 'summary:')} ${shortSummary}`)
271
+ writeln(` ${s.dim('summary:')} ${text}`)
292
272
  }
293
273
  } else {
294
- console.log(` ${style('dim', 'result:')} no memories to extract`)
274
+ writeln(` ${s.dim('result:')} no memories to extract`)
295
275
  }
296
- console.log()
276
+ writeln()
297
277
  },
298
278
 
299
279
  /**
300
280
  * Log management agent starting
301
281
  */
302
282
  logManagementStart(memoriesCount: number) {
303
- console.log(`${timestamp()} ${style('blue', '🔧')} ${style('bold', 'MANAGEMENT AGENT')}`)
304
- console.log(` ${style('dim', 'processing:')} ${memoriesCount} new memories`)
283
+ writeln(`${ts()} ${s.blue('🔧')} ${s.bold('MANAGEMENT AGENT')}`)
284
+ writeln(` ${s.dim('processing:')} ${memoriesCount} new memories`)
305
285
  },
306
286
 
307
287
  /**
308
288
  * Log management agent results
309
- * In verbose mode: shows all details beautifully formatted
310
- * In normal mode: shows compact summary
289
+ * Verbose: full details beautifully formatted
290
+ * Normal: compact summary
311
291
  */
312
292
  logManagementComplete(result: {
313
293
  success: boolean
@@ -322,88 +302,75 @@ export const logger = {
322
302
  fullReport?: string
323
303
  error?: string
324
304
  }) {
325
- // Helper to format action with icon
326
- const formatAction = (action: string, truncate = true): string => {
327
- let icon = ''
328
- if (action.startsWith('READ OK')) icon = style('dim', ' 📖')
329
- else if (action.startsWith('READ FAILED')) icon = style('red', ' ❌')
330
- else if (action.startsWith('WRITE OK')) icon = style('green', ' ✏️')
331
- else if (action.startsWith('WRITE FAILED')) icon = style('red', ' ❌')
332
- else if (action.startsWith('RECEIVED')) icon = style('cyan', ' 📥')
333
- else if (action.startsWith('CREATED')) icon = style('green', ' ✨')
334
- else if (action.startsWith('UPDATED')) icon = style('blue', ' 📝')
335
- else if (action.startsWith('SUPERSEDED')) icon = style('yellow', ' 🔄')
336
- else if (action.startsWith('RESOLVED')) icon = style('green', ' ✅')
337
- else if (action.startsWith('LINKED')) icon = style('cyan', ' 🔗')
338
- else if (action.startsWith('PRIMER')) icon = style('magenta', ' 💜')
339
- else if (action.startsWith('SKIPPED')) icon = style('dim', ' ⏭️')
340
- else if (action.startsWith('NO_ACTION')) icon = style('dim', ' ◦')
341
-
342
- const text = truncate && action.length > 70 ? action.slice(0, 67) + '...' : action
343
- return `${icon} ${style('dim', text)}`
305
+ const actionIcon = (action: string): string => {
306
+ if (action.startsWith('READ OK')) return s.dim('📖')
307
+ if (action.startsWith('READ FAILED')) return s.red('')
308
+ if (action.startsWith('WRITE OK')) return s.green('✏️')
309
+ if (action.startsWith('WRITE FAILED')) return s.red('❌')
310
+ if (action.startsWith('RECEIVED')) return s.cyan('📥')
311
+ if (action.startsWith('CREATED')) return s.green('')
312
+ if (action.startsWith('UPDATED')) return s.blue('📝')
313
+ if (action.startsWith('SUPERSEDED')) return s.yellow('🔄')
314
+ if (action.startsWith('RESOLVED')) return s.green('')
315
+ if (action.startsWith('LINKED')) return s.cyan('🔗')
316
+ if (action.startsWith('PRIMER')) return s.magenta('💜')
317
+ if (action.startsWith('SKIPPED')) return s.dim('⏭️')
318
+ if (action.startsWith('NO_ACTION')) return s.dim('')
319
+ return ''
344
320
  }
345
321
 
346
322
  if (result.success) {
347
- console.log(` ${style('green', sym.check)} ${style('bold', 'Completed')}`)
323
+ writeln(` ${s.green('')} ${s.bold('Completed')}`)
348
324
 
349
325
  if (_verbose) {
350
- // ═══════════════════════════════════════════════════════════════
351
- // VERBOSE MODE: Show everything beautifully formatted
352
- // ═══════════════════════════════════════════════════════════════
353
-
354
- // File I/O stats section
355
- console.log(` ${style('dim', '─'.repeat(50))}`)
356
- console.log(` ${style('cyan', '📊')} ${style('bold', 'Statistics')}`)
357
-
326
+ // ── VERBOSE: Full details ──
358
327
  const filesRead = result.filesRead ?? 0
359
328
  const filesWritten = result.filesWritten ?? 0
360
- console.log(` ${style('dim', 'Files read:')} ${filesRead > 0 ? style('green', String(filesRead)) : style('dim', '0')}`)
361
- console.log(` ${style('dim', 'Files written:')} ${filesWritten > 0 ? style('green', String(filesWritten)) : style('dim', '0')}`)
362
-
363
- // Memory changes section
364
329
  const superseded = result.superseded ?? 0
365
330
  const resolved = result.resolved ?? 0
366
331
  const linked = result.linked ?? 0
367
- console.log(` ${style('dim', 'Superseded:')} ${superseded > 0 ? style('yellow', String(superseded)) : style('dim', '0')}`)
368
- console.log(` ${style('dim', 'Resolved:')} ${resolved > 0 ? style('green', String(resolved)) : style('dim', '0')}`)
369
- console.log(` ${style('dim', 'Linked:')} ${linked > 0 ? style('cyan', String(linked)) : style('dim', '0')}`)
370
- console.log(` ${style('dim', 'Primer:')} ${result.primerUpdated ? style('magenta', 'updated') : style('dim', 'unchanged')}`)
371
332
 
372
- // Actions section - show ALL actions in verbose mode
333
+ writeln(` ${s.dim('─'.repeat(50))}`)
334
+ writeln(` ${s.cyan('📊')} ${s.bold('Statistics')}`)
335
+
336
+ const stats = kv([
337
+ ['Files read', filesRead > 0 ? s.green(String(filesRead)) : s.dim('0')],
338
+ ['Files written', filesWritten > 0 ? s.green(String(filesWritten)) : s.dim('0')],
339
+ ['Superseded', superseded > 0 ? s.yellow(String(superseded)) : s.dim('0')],
340
+ ['Resolved', resolved > 0 ? s.green(String(resolved)) : s.dim('0')],
341
+ ['Linked', linked > 0 ? s.cyan(String(linked)) : s.dim('0')],
342
+ ['Primer', result.primerUpdated ? s.magenta('updated') : s.dim('unchanged')],
343
+ ], { keyColor: s.dim, indent: 4 })
344
+ writeln(stats)
345
+
346
+ // Actions - no truncation in verbose
373
347
  if (result.actions && result.actions.length > 0) {
374
- console.log(` ${style('dim', '─'.repeat(50))}`)
375
- console.log(` ${style('cyan', '🎬')} ${style('bold', 'Actions')} ${style('dim', `(${result.actions.length} total)`)}`)
348
+ writeln(` ${s.dim('─'.repeat(50))}`)
349
+ writeln(` ${s.cyan('🎬')} ${s.bold('Actions')} ${s.dim(`(${result.actions.length} total)`)}`)
376
350
  for (const action of result.actions) {
377
- console.log(` ${formatAction(action, false)}`) // No truncation in verbose
351
+ writeln(` ${actionIcon(action)} ${s.dim(action)}`)
378
352
  }
379
353
  }
380
354
 
381
- // Full report section
355
+ // Full report
382
356
  if (result.fullReport) {
383
- console.log(` ${style('dim', '─'.repeat(50))}`)
384
- console.log(` ${style('cyan', '📋')} ${style('bold', 'Full Report')}`)
385
- const reportLines = result.fullReport.split('\n')
386
- for (const line of reportLines) {
387
- // Highlight section headers
357
+ writeln(` ${s.dim('─'.repeat(50))}`)
358
+ writeln(` ${s.cyan('📋')} ${s.bold('Full Report')}`)
359
+ for (const line of result.fullReport.split('\n')) {
388
360
  if (line.includes('===')) {
389
- console.log(` ${style('bold', line)}`)
361
+ writeln(` ${s.bold(line)}`)
390
362
  } else if (line.match(/^[A-Z_]+:/)) {
391
- // Highlight stat lines like "memories_processed: 5"
392
- console.log(` ${style('cyan', line)}`)
363
+ writeln(` ${s.cyan(line)}`)
393
364
  } else {
394
- console.log(` ${style('dim', line)}`)
365
+ writeln(` ${s.dim(line)}`)
395
366
  }
396
367
  }
397
368
  }
398
369
 
399
- console.log(` ${style('dim', '─'.repeat(50))}`)
370
+ writeln(` ${s.dim('─'.repeat(50))}`)
400
371
 
401
372
  } else {
402
- // ═══════════════════════════════════════════════════════════════
403
- // NORMAL MODE: Compact summary
404
- // ═══════════════════════════════════════════════════════════════
405
-
406
- // Memory change stats (one line)
373
+ // ── NORMAL: Compact summary ──
407
374
  const stats: string[] = []
408
375
  if (result.superseded && result.superseded > 0) stats.push(`${result.superseded} superseded`)
409
376
  if (result.resolved && result.resolved > 0) stats.push(`${result.resolved} resolved`)
@@ -411,43 +378,39 @@ export const logger = {
411
378
  if (result.primerUpdated) stats.push('primer updated')
412
379
 
413
380
  if (stats.length > 0) {
414
- console.log(` ${style('dim', 'changes:')} ${stats.join(style('dim', ', '))}`)
381
+ writeln(` ${s.dim('changes:')} ${stats.join(s.dim(', '))}`)
415
382
  } else {
416
- console.log(` ${style('dim', 'changes:')} none (memories are current)`)
383
+ writeln(` ${s.dim('changes:')} none (memories are current)`)
417
384
  }
418
385
 
419
- // Show limited actions
420
386
  if (result.actions && result.actions.length > 0) {
421
- console.log(` ${style('dim', 'actions:')}`)
387
+ writeln(` ${s.dim('actions:')}`)
422
388
  for (const action of result.actions.slice(0, 10)) {
423
- console.log(` ${formatAction(action, true)}`)
389
+ const text = action.length > 70 ? action.slice(0, 67) + '...' : action
390
+ writeln(` ${actionIcon(action)} ${s.dim(text)}`)
424
391
  }
425
392
  if (result.actions.length > 10) {
426
- console.log(` ${style('dim', ` ... and ${result.actions.length - 10} more actions`)}`)
393
+ writeln(` ${s.dim(` ... and ${result.actions.length - 10} more actions`)}`)
427
394
  }
428
395
  }
429
396
  }
430
397
 
431
398
  } else {
432
- // ═══════════════════════════════════════════════════════════════
433
- // ERROR: Always show error details
434
- // ═══════════════════════════════════════════════════════════════
435
- console.log(` ${style('yellow', sym.warning)} ${style('bold', 'Failed')}`)
399
+ // ── ERROR: Always show details ──
400
+ writeln(` ${s.yellow('⚠')} ${s.bold('Failed')}`)
436
401
  if (result.error) {
437
- console.log(` ${style('red', 'error:')} ${result.error}`)
402
+ writeln(` ${s.red('error:')} ${result.error}`)
438
403
  }
439
404
 
440
- // Show full report on error (always, for debugging)
441
405
  if (result.fullReport) {
442
- console.log(` ${style('dim', '─'.repeat(50))}`)
443
- console.log(` ${style('red', '📋')} ${style('bold', 'Error Report:')}`)
444
- const reportLines = result.fullReport.split('\n')
445
- for (const line of reportLines) {
446
- console.log(` ${style('dim', line)}`)
406
+ writeln(` ${s.dim('─'.repeat(50))}`)
407
+ writeln(` ${s.red('📋')} ${s.bold('Error Report:')}`)
408
+ for (const line of result.fullReport.split('\n')) {
409
+ writeln(` ${s.dim(line)}`)
447
410
  }
448
411
  }
449
412
  }
450
- console.log()
413
+ writeln()
451
414
  },
452
415
 
453
416
  /**
@@ -464,13 +427,12 @@ export const logger = {
464
427
  durationMs?: number
465
428
  selectedMemories: Array<{
466
429
  content: string
467
- reasoning: string // "Activated: trigger:67%, tags:2, content (3 signals)"
430
+ reasoning: string
468
431
  signalCount: number
469
432
  importance_weight: number
470
433
  context_type: string
471
434
  semantic_tags: string[]
472
435
  isGlobal: boolean
473
- // Activation signals
474
436
  signals: {
475
437
  trigger: boolean
476
438
  triggerStrength: number
@@ -486,76 +448,114 @@ export const logger = {
486
448
  }) {
487
449
  const { totalMemories, currentMessage, alreadyInjected, preFiltered, globalCount, projectCount, finalCount, durationMs, selectedMemories } = params
488
450
 
489
- const timeStr = durationMs !== undefined ? style('cyan', `${durationMs.toFixed(1)}ms`) : ''
451
+ const timeStr = durationMs !== undefined ? s.cyan(`${durationMs.toFixed(1)}ms`) : ''
490
452
 
491
- console.log()
492
- console.log(`${timestamp()} ${style('magenta', sym.brain)} ${style('bold', 'RETRIEVAL')} ${timeStr}`)
493
- console.log(` ${style('dim', 'total:')} ${totalMemories} → ${style('dim', 'filtered:')} ${preFiltered} → ${style('dim', 'candidates:')} ${totalMemories - preFiltered}`)
494
- console.log(` ${style('dim', 'already injected:')} ${alreadyInjected}`)
453
+ writeln()
454
+ writeln(`${ts()} ${s.magenta('🧠')} ${s.bold('RETRIEVAL')} ${timeStr}`)
495
455
 
496
- const msgPreview = currentMessage.length > 60
497
- ? currentMessage.slice(0, 60) + '...'
498
- : currentMessage
499
- console.log(` ${style('dim', 'message:')} "${msgPreview}"`)
500
- console.log()
456
+ // Pipeline summary
457
+ const pipeline = kv([
458
+ ['total', `${totalMemories} → ${s.dim('filtered:')} ${preFiltered} → ${s.dim('candidates:')} ${totalMemories - preFiltered}`],
459
+ ['already injected', String(alreadyInjected)],
460
+ ['message', `"${!_verbose && currentMessage.length > 60 ? currentMessage.slice(0, 60) + '...' : currentMessage}"`],
461
+ ], { keyColor: s.dim, indent: 3 })
462
+ writeln(pipeline)
463
+ writeln()
501
464
 
502
465
  // Selection summary
503
- console.log(` ${style('cyan', 'Global:')} ${globalCount} candidates → max 2 selected`)
504
- console.log(` ${style('cyan', 'Project:')} ${projectCount} candidates`)
505
- console.log(` ${style('green', 'Final:')} ${finalCount} memories selected`)
506
- console.log()
466
+ writeln(` ${s.cyan('Global:')} ${globalCount} candidates → max 2 selected`)
467
+ writeln(` ${s.cyan('Project:')} ${projectCount} candidates`)
468
+ writeln(` ${s.green('Final:')} ${finalCount} memories selected`)
469
+ writeln()
507
470
 
508
471
  if (selectedMemories.length === 0) {
509
- console.log(` ${style('dim', '📭 No relevant memories for this context')}`)
510
- console.log()
472
+ writeln(` ${s.dim('📭 No relevant memories for this context')}`)
473
+ writeln()
511
474
  return
512
475
  }
513
476
 
514
- // Detailed breakdown
515
- console.log(style('dim', ' ─'.repeat(30)))
516
- console.log(` ${style('bold', 'SELECTION DETAILS')}`)
517
- console.log()
518
-
519
- selectedMemories.forEach((m, i) => {
520
- const num = style('dim', `${i + 1}.`)
521
- const signalsStr = style('green', `${m.signalCount} signals`)
522
- const imp = style('magenta', `imp:${(m.importance_weight * 100).toFixed(0)}%`)
523
- const type = style('yellow', m.context_type.toUpperCase())
524
- const scope = m.isGlobal ? style('blue', ' [G]') : ''
525
-
526
- console.log(` ${num} [${signalsStr} • ${imp}] ${type}${scope}`)
527
-
528
- // Content preview
529
- const preview = m.content.length > 60
530
- ? m.content.slice(0, 60) + style('dim', '...')
531
- : m.content
532
- console.log(` ${style('white', preview)}`)
533
-
534
- // Show which signals fired with their strengths
535
- const firedSignals: string[] = []
536
- if (m.signals.trigger) {
537
- firedSignals.push(`trigger:${(m.signals.triggerStrength * 100).toFixed(0)}%`)
538
- }
539
- if (m.signals.tags) {
540
- firedSignals.push(`tags:${m.signals.tagCount}`)
541
- }
542
- if (m.signals.domain) firedSignals.push('domain')
543
- if (m.signals.feature) firedSignals.push('feature')
544
- if (m.signals.content) firedSignals.push('content')
545
- if (m.signals.vector) {
546
- firedSignals.push(`vector:${(m.signals.vectorSimilarity * 100).toFixed(0)}%`)
477
+ // ── Verbose: Table view ──
478
+ if (_verbose) {
479
+ const formatSignals = (sig: typeof selectedMemories[0]['signals']): string => {
480
+ const parts: string[] = []
481
+ if (sig.trigger) parts.push(`trig:${(sig.triggerStrength * 100).toFixed(0)}%`)
482
+ if (sig.tags) parts.push(`tags:${sig.tagCount}`)
483
+ if (sig.domain) parts.push('dom')
484
+ if (sig.feature) parts.push('feat')
485
+ if (sig.content) parts.push('content')
486
+ if (sig.vector) parts.push(`vec:${(sig.vectorSimilarity * 100).toFixed(0)}%`)
487
+ return parts.join(', ')
547
488
  }
548
489
 
549
- if (firedSignals.length > 0) {
550
- console.log(` ${style('cyan', 'signals:')} ${firedSignals.join(', ')}`)
551
- }
490
+ const rows = selectedMemories.map((m, i) => ({
491
+ '#': String(i + 1),
492
+ sig: String(m.signalCount),
493
+ imp: `${(m.importance_weight * 100).toFixed(0)}%`,
494
+ type: m.context_type.toUpperCase(),
495
+ scope: m.isGlobal ? 'G' : 'P',
496
+ signals: formatSignals(m.signals),
497
+ }))
498
+
499
+ writeln(table(rows, {
500
+ columns: [
501
+ { key: '#', align: 'right' as const, width: 4 },
502
+ { key: 'sig', align: 'center' as const, width: 5 },
503
+ { key: 'imp', align: 'right' as const, width: 6 },
504
+ { key: 'type', label: 'type', minWidth: 12 },
505
+ { key: 'scope', align: 'center' as const, width: 7 },
506
+ { key: 'signals', minWidth: 20 },
507
+ ],
508
+ border: 'rounded',
509
+ borderColor: 'magenta',
510
+ }))
511
+ writeln()
512
+
513
+ // Full content for each memory in verbose
514
+ selectedMemories.forEach((m, i) => {
515
+ const emoji = emojiMap[m.context_type?.toLowerCase()] ?? '📝'
516
+ writeln(` ${s.dim(`${i + 1}.`)} ${emoji} ${m.content}`)
517
+ writeln()
518
+ })
552
519
 
553
- console.log()
554
- })
520
+ } else {
521
+ // ── Normal: Compact list ──
522
+ writeln(s.dim(' ─'.repeat(30)))
523
+ writeln(` ${s.bold('SELECTION DETAILS')}`)
524
+ writeln()
525
+
526
+ selectedMemories.forEach((m, i) => {
527
+ const num = s.dim(`${i + 1}.`)
528
+ const signalsStr = s.green(`${m.signalCount} signals`)
529
+ const imp = s.magenta(`imp:${(m.importance_weight * 100).toFixed(0)}%`)
530
+ const type = s.yellow(m.context_type.toUpperCase())
531
+ const scope = m.isGlobal ? s.blue(' [G]') : ''
532
+
533
+ writeln(` ${num} [${signalsStr} • ${imp}] ${type}${scope}`)
534
+
535
+ const preview = m.content.length > 60
536
+ ? m.content.slice(0, 60) + s.dim('...')
537
+ : m.content
538
+ writeln(` ${preview}`)
539
+
540
+ // Signal details
541
+ const firedSignals: string[] = []
542
+ if (m.signals.trigger) firedSignals.push(`trigger:${(m.signals.triggerStrength * 100).toFixed(0)}%`)
543
+ if (m.signals.tags) firedSignals.push(`tags:${m.signals.tagCount}`)
544
+ if (m.signals.domain) firedSignals.push('domain')
545
+ if (m.signals.feature) firedSignals.push('feature')
546
+ if (m.signals.content) firedSignals.push('content')
547
+ if (m.signals.vector) firedSignals.push(`vector:${(m.signals.vectorSimilarity * 100).toFixed(0)}%`)
548
+
549
+ if (firedSignals.length > 0) {
550
+ writeln(` ${s.cyan('signals:')} ${firedSignals.join(', ')}`)
551
+ }
552
+ writeln()
553
+ })
554
+ }
555
555
  },
556
556
 
557
557
  /**
558
- * Log score distribution for diagnostics (supports both old and new algorithm)
558
+ * Log score distribution for diagnostics
559
559
  */
560
560
  logScoreDistribution(params: {
561
561
  totalCandidates: number
@@ -581,46 +581,74 @@ export const logger = {
581
581
  }) {
582
582
  const { totalCandidates, passedGatekeeper, rejectedByGatekeeper, buckets, stats, signalBreakdown } = params
583
583
 
584
- console.log()
585
- console.log(style('dim', ' ─'.repeat(30)))
586
- console.log(` ${style('bold', 'ACTIVATION SIGNALS')}`)
587
- console.log()
584
+ writeln()
585
+ writeln(s.dim(' ─'.repeat(30)))
586
+ writeln(` ${s.bold('ACTIVATION SIGNALS')}`)
587
+ writeln()
588
588
 
589
589
  // Gatekeeper stats
590
590
  const passRate = totalCandidates > 0 ? ((passedGatekeeper / totalCandidates) * 100).toFixed(0) : '0'
591
- console.log(` ${style('dim', 'Activated:')} ${style('green', String(passedGatekeeper))}/${totalCandidates} (${passRate}%)`)
592
- console.log(` ${style('dim', 'Rejected:')} ${rejectedByGatekeeper} (< 2 signals)`)
593
- console.log()
591
+ writeln(` ${s.dim('Activated:')} ${s.green(String(passedGatekeeper))}/${totalCandidates} (${passRate}%)`)
592
+ writeln(` ${s.dim('Rejected:')} ${rejectedByGatekeeper} (< 2 signals)`)
593
+ writeln()
594
594
 
595
595
  // Signal breakdown
596
596
  if (signalBreakdown && signalBreakdown.total > 0) {
597
- console.log(` ${style('cyan', 'Signal Breakdown:')}`)
598
- const signals = [
599
- { name: 'trigger', count: signalBreakdown.trigger },
600
- { name: 'tags', count: signalBreakdown.tags },
601
- { name: 'domain', count: signalBreakdown.domain },
602
- { name: 'feature', count: signalBreakdown.feature },
603
- { name: 'content', count: signalBreakdown.content },
604
- { name: 'files', count: signalBreakdown.files },
605
- { name: 'vector', count: signalBreakdown.vector },
606
- ]
607
- for (const sig of signals) {
608
- const pct = ((sig.count / signalBreakdown.total) * 100).toFixed(0)
609
- const bar = '█'.repeat(Math.round(sig.count / signalBreakdown.total * 20))
610
- console.log(` ${sig.name.padEnd(8)} ${bar.padEnd(20)} ${sig.count} (${pct}%)`)
597
+ if (_verbose) {
598
+ // Verbose: table view
599
+ const signals = [
600
+ { signal: 'trigger', count: signalBreakdown.trigger },
601
+ { signal: 'tags', count: signalBreakdown.tags },
602
+ { signal: 'domain', count: signalBreakdown.domain },
603
+ { signal: 'feature', count: signalBreakdown.feature },
604
+ { signal: 'content', count: signalBreakdown.content },
605
+ { signal: 'files', count: signalBreakdown.files },
606
+ { signal: 'vector', count: signalBreakdown.vector },
607
+ ].filter(sig => sig.count > 0).map(sig => ({
608
+ signal: sig.signal,
609
+ count: String(sig.count),
610
+ pct: `${((sig.count / signalBreakdown.total) * 100).toFixed(0)}%`,
611
+ }))
612
+
613
+ writeln(table(signals, {
614
+ border: 'rounded',
615
+ borderColor: 'cyan',
616
+ columns: [
617
+ { key: 'signal', label: 'Signal' },
618
+ { key: 'count', label: 'Count', align: 'right' as const },
619
+ { key: 'pct', label: '%', align: 'right' as const },
620
+ ],
621
+ }))
622
+ } else {
623
+ // Normal: bar chart
624
+ writeln(` ${s.cyan('Signal Breakdown:')}`)
625
+ const signals = [
626
+ { name: 'trigger', count: signalBreakdown.trigger },
627
+ { name: 'tags', count: signalBreakdown.tags },
628
+ { name: 'domain', count: signalBreakdown.domain },
629
+ { name: 'feature', count: signalBreakdown.feature },
630
+ { name: 'content', count: signalBreakdown.content },
631
+ { name: 'files', count: signalBreakdown.files },
632
+ { name: 'vector', count: signalBreakdown.vector },
633
+ ]
634
+ for (const sig of signals) {
635
+ const pct = ((sig.count / signalBreakdown.total) * 100).toFixed(0)
636
+ const bar = '█'.repeat(Math.round(sig.count / signalBreakdown.total * 20))
637
+ writeln(` ${sig.name.padEnd(8)} ${bar.padEnd(20)} ${sig.count} (${pct}%)`)
638
+ }
611
639
  }
612
- console.log()
640
+ writeln()
613
641
  }
614
642
 
615
643
  // Stats
616
644
  if (stats.max > 0) {
617
- console.log(` ${style('cyan', 'Signals:')} min=${stats.min} max=${stats.max} mean=${stats.mean}`)
618
- console.log()
645
+ writeln(` ${s.cyan('Signals:')} min=${stats.min} max=${stats.max} mean=${stats.mean}`)
646
+ writeln()
619
647
  }
620
648
 
621
649
  // Histogram by signal count
622
650
  if (Object.keys(buckets).length > 0) {
623
- console.log(` ${style('bold', 'Distribution:')}`)
651
+ writeln(` ${s.bold('Distribution:')}`)
624
652
  const maxBucketCount = Math.max(...Object.values(buckets), 1)
625
653
  const bucketOrder = ['2 signals', '3 signals', '4 signals', '5 signals', '6 signals', '7 signals']
626
654
 
@@ -628,14 +656,37 @@ export const logger = {
628
656
  const count = buckets[bucket] ?? 0
629
657
  if (count > 0 || bucket === '2 signals') {
630
658
  const barLen = Math.round((count / maxBucketCount) * 25)
631
- const bar = '█'.repeat(barLen) + style('dim', '░'.repeat(25 - barLen))
659
+ const bar = '█'.repeat(barLen) + s.dim('░'.repeat(25 - barLen))
632
660
  const countStr = count.toString().padStart(3)
633
- console.log(` ${style('dim', bucket.padEnd(10))} ${bar} ${style('cyan', countStr)}`)
661
+ writeln(` ${s.dim(bucket.padEnd(10))} ${bar} ${s.cyan(countStr)}`)
634
662
  }
635
663
  }
636
- console.log()
664
+ writeln()
637
665
  }
638
666
  },
667
+
668
+ /**
669
+ * Log the full injected payload (verbose only)
670
+ * Shows exactly what gets sent to the AI's context
671
+ */
672
+ logInjectedPayload(payload: string, type: 'primer' | 'memories' | 'action_items', count?: number) {
673
+ if (!_verbose) return
674
+
675
+ const titles: Record<string, string> = {
676
+ primer: '📖 Injected Payload (Session Primer)',
677
+ memories: `✨ Injected Payload (${count ?? 0} memor${count === 1 ? 'y' : 'ies'})`,
678
+ action_items: `🎯 Injected Payload (${count ?? 0} action item${count === 1 ? '' : 's'})`,
679
+ }
680
+
681
+ writeln()
682
+ writeln(box(payload, {
683
+ border: 'rounded',
684
+ borderColor: 'cyan',
685
+ title: titles[type],
686
+ titleColor: s.bold,
687
+ }))
688
+ writeln()
689
+ },
639
690
  }
640
691
 
641
692
  export default logger