@rlabs-inc/memory 0.3.10 → 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.
@@ -8,21 +8,63 @@ import { styleText } from 'util'
8
8
  import {
9
9
  findAllSessions,
10
10
  findProjectSessions,
11
+ parseSessionFile,
11
12
  parseSessionFileWithSegments,
12
13
  getSessionSummary,
13
14
  calculateStats,
14
15
  type ParsedProject,
16
+ type ParsedSession,
15
17
  } from '../../core/session-parser.ts'
16
18
  import { Curator } from '../../core/curator.ts'
19
+ import { Manager, type StoragePaths } from '../../core/manager.ts'
17
20
  import { MemoryStore } from '../../core/store.ts'
21
+ import type { CurationResult, CuratedMemory } from '../../types/memory.ts'
18
22
  import { homedir } from 'os'
19
23
  import { join } from 'path'
24
+ import { readdir, stat } from 'fs/promises'
20
25
 
21
26
  type Style = Parameters<typeof styleText>[0]
22
27
  const style = (format: Style, text: string): string => styleText(format, text)
23
28
 
29
+ // Simple spinner for long operations
30
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
31
+
32
+ class Spinner {
33
+ private interval: ReturnType<typeof setInterval> | null = null
34
+ private frameIndex = 0
35
+ private message = ''
36
+
37
+ start(message: string) {
38
+ this.message = message
39
+ this.frameIndex = 0
40
+ process.stdout.write(` ${style('cyan', spinnerFrames[0])} ${style('dim', message)}`)
41
+ this.interval = setInterval(() => {
42
+ this.frameIndex = (this.frameIndex + 1) % spinnerFrames.length
43
+ process.stdout.write(`\r ${style('cyan', spinnerFrames[this.frameIndex])} ${style('dim', this.message)}`)
44
+ }, 80)
45
+ }
46
+
47
+ update(message: string) {
48
+ this.message = message
49
+ process.stdout.write(`\r ${style('cyan', spinnerFrames[this.frameIndex])} ${style('dim', this.message)}`)
50
+ }
51
+
52
+ stop(finalMessage?: string) {
53
+ if (this.interval) {
54
+ clearInterval(this.interval)
55
+ this.interval = null
56
+ }
57
+ // Clear the line and optionally write final message
58
+ process.stdout.write('\r' + ' '.repeat(80) + '\r')
59
+ if (finalMessage) {
60
+ console.log(finalMessage)
61
+ }
62
+ }
63
+ }
64
+
24
65
  interface IngestOptions {
25
66
  project?: string
67
+ session?: string
26
68
  all?: boolean
27
69
  dryRun?: boolean
28
70
  verbose?: boolean
@@ -30,6 +72,71 @@ interface IngestOptions {
30
72
  maxTokens?: number
31
73
  }
32
74
 
75
+ /**
76
+ * Find a specific session by ID
77
+ * Searches in specified project or across all projects
78
+ */
79
+ async function findSessionById(
80
+ sessionId: string,
81
+ projectsFolder: string,
82
+ projectPath?: string
83
+ ): Promise<{ session: ParsedSession; folderId: string } | null> {
84
+ const filename = sessionId.endsWith('.jsonl') ? sessionId : `${sessionId}.jsonl`
85
+
86
+ // If project path is specified, search only there
87
+ if (projectPath) {
88
+ const filepath = join(projectPath, filename)
89
+ try {
90
+ await stat(filepath)
91
+ const session = await parseSessionFile(filepath)
92
+ const folderId = projectPath.split('/').pop() ?? projectPath
93
+ return { session, folderId }
94
+ } catch {
95
+ return null
96
+ }
97
+ }
98
+
99
+ // Search all projects
100
+ try {
101
+ const projectFolders = await readdir(projectsFolder)
102
+
103
+ for (const folder of projectFolders) {
104
+ const folderPath = join(projectsFolder, folder)
105
+ const filepath = join(folderPath, filename)
106
+
107
+ try {
108
+ await stat(filepath)
109
+ const session = await parseSessionFile(filepath)
110
+ return { session, folderId: folder }
111
+ } catch {
112
+ continue
113
+ }
114
+ }
115
+ } catch {
116
+ // projectsFolder doesn't exist
117
+ }
118
+
119
+ return null
120
+ }
121
+
122
+ /**
123
+ * Build storage paths for the manager (mirrors engine.getStoragePaths)
124
+ */
125
+ function buildStoragePaths(projectId: string): StoragePaths {
126
+ const globalPath = join(homedir(), '.local', 'share', 'memory', 'global')
127
+ const centralPath = join(homedir(), '.local', 'share', 'memory')
128
+ const projectPath = join(centralPath, projectId)
129
+
130
+ return {
131
+ projectPath,
132
+ globalPath,
133
+ projectMemoriesPath: join(projectPath, 'memories'),
134
+ globalMemoriesPath: join(globalPath, 'memories'),
135
+ personalPrimerPath: join(globalPath, 'primer', 'personal-primer.md'),
136
+ storageMode: 'central',
137
+ }
138
+ }
139
+
33
140
  export async function ingest(options: IngestOptions) {
34
141
  logger.setVerbose(options.verbose ?? false)
35
142
 
@@ -40,18 +147,8 @@ export async function ingest(options: IngestOptions) {
40
147
  console.log(style(['bold', 'magenta'], '└──────────────────────────────────────────────────────────┘'))
41
148
  console.log()
42
149
 
43
- // Check for API key
44
- const apiKey = process.env.ANTHROPIC_API_KEY
45
- if (!apiKey && !options.dryRun) {
46
- logger.error('ANTHROPIC_API_KEY not set')
47
- console.log()
48
- console.log(style('dim', ' Set your API key to use SDK curation:'))
49
- console.log(style('dim', ' export ANTHROPIC_API_KEY=sk-...'))
50
- console.log()
51
- console.log(style('dim', ' Or use --dry-run to see what would be ingested'))
52
- console.log()
53
- process.exit(1)
54
- }
150
+ // Agent SDK uses Claude Code OAuth - no API key needed!
151
+ // Just need Claude Code installed and authenticated
55
152
 
56
153
  const projectsFolder = join(homedir(), '.claude', 'projects')
57
154
  const maxTokens = options.maxTokens ?? 150000
@@ -59,7 +156,35 @@ export async function ingest(options: IngestOptions) {
59
156
  // Find sessions to ingest
60
157
  let projects: ParsedProject[] = []
61
158
 
62
- if (options.project) {
159
+ if (options.session) {
160
+ // Find specific session by ID
161
+ const projectPath = options.project ? join(projectsFolder, options.project) : undefined
162
+ const result = await findSessionById(options.session, projectsFolder, projectPath)
163
+
164
+ if (!result) {
165
+ logger.error(`Session not found: ${options.session}`)
166
+ console.log()
167
+ if (options.project) {
168
+ console.log(style('dim', ` Searched in: ${projectPath}`))
169
+ } else {
170
+ console.log(style('dim', ` Searched in: ${projectsFolder}`))
171
+ console.log(style('dim', ' Tip: Use --project <name> to specify the project folder'))
172
+ }
173
+ console.log()
174
+ process.exit(1)
175
+ }
176
+
177
+ projects = [{
178
+ folderId: result.folderId,
179
+ name: result.session.projectName,
180
+ path: join(projectsFolder, result.folderId),
181
+ sessions: [result.session]
182
+ }]
183
+
184
+ logger.info(`Found session in project: ${result.session.projectName}`)
185
+ console.log(` ${style('dim', 'path:')} ${result.session.filepath}`)
186
+ console.log()
187
+ } else if (options.project) {
63
188
  // Find specific project
64
189
  const projectPath = join(projectsFolder, options.project)
65
190
  const sessions = await findProjectSessions(projectPath, { limit: options.limit })
@@ -86,9 +211,11 @@ export async function ingest(options: IngestOptions) {
86
211
  process.exit(1)
87
212
  }
88
213
  } else {
89
- logger.error('Specify --project <name> or --all')
214
+ logger.error('Specify --session <id>, --project <name>, or --all')
90
215
  console.log()
91
216
  console.log(style('dim', ' Examples:'))
217
+ console.log(style('dim', ' memory ingest --session abc123-def456'))
218
+ console.log(style('dim', ' memory ingest --session abc123-def456 --project my-project'))
92
219
  console.log(style('dim', ' memory ingest --project my-project'))
93
220
  console.log(style('dim', ' memory ingest --all'))
94
221
  console.log(style('dim', ' memory ingest --all --dry-run'))
@@ -137,21 +264,33 @@ export async function ingest(options: IngestOptions) {
137
264
  return
138
265
  }
139
266
 
140
- // Initialize curator and store
141
- const curator = new Curator({ apiKey })
267
+ // Initialize curator, manager, and store
268
+ // Curator uses Agent SDK (no API key needed - uses Claude Code OAuth)
269
+ const curator = new Curator()
270
+ const manager = new Manager()
142
271
  const store = new MemoryStore()
143
272
 
273
+ // Check if manager is enabled
274
+ const managerEnabled = process.env.MEMORY_MANAGER_DISABLED !== '1'
275
+
144
276
  logger.divider()
145
277
  logger.info('Starting ingestion...')
278
+ if (managerEnabled) {
279
+ console.log(` ${style('dim', 'manager:')} enabled (will organize memories after each session)`)
280
+ }
146
281
  console.log()
147
282
 
148
283
  let totalSegments = 0
149
284
  let totalMemories = 0
150
285
  let failedSegments = 0
286
+ let managedSessions = 0
151
287
 
152
288
  for (const project of projects) {
153
289
  console.log(` ${style('cyan', '📁')} ${style('bold', project.name)}`)
154
290
 
291
+ // Build storage paths for manager (same for all sessions in project)
292
+ const storagePaths = buildStoragePaths(project.folderId)
293
+
155
294
  for (const session of project.sessions) {
156
295
  const summary = getSessionSummary(session)
157
296
  const truncated = summary.length > 45 ? summary.slice(0, 42) + '...' : summary
@@ -164,29 +303,88 @@ export async function ingest(options: IngestOptions) {
164
303
  const segments = await parseSessionFileWithSegments(session.filepath, maxTokens)
165
304
  totalSegments += segments.length
166
305
 
306
+ // Accumulate all memories from this session for manager
307
+ const sessionMemories: CuratedMemory[] = []
308
+ let sessionSummary = ''
309
+
310
+ const spinner = new Spinner()
311
+
167
312
  for (const segment of segments) {
168
313
  try {
169
- if (options.verbose) {
170
- console.log(` ${style('dim', `→ Segment ${segment.segmentIndex + 1}/${segment.totalSegments} (${segment.estimatedTokens} tokens)`)}`)
171
- }
314
+ const segmentLabel = `Segment ${segment.segmentIndex + 1}/${segment.totalSegments}`
315
+ const tokensLabel = `${Math.round(segment.estimatedTokens / 1000)}k tokens`
316
+
317
+ // Start spinner for curation
318
+ spinner.start(`${segmentLabel} (${tokensLabel}) - curating with Opus 4.5...`)
172
319
 
173
320
  // Curate the segment
174
321
  const result = await curator.curateFromSegment(segment, 'historical')
175
322
 
323
+ // Stop spinner with success message
324
+ spinner.stop(` ${style('green', '✓')} ${segmentLabel}: ${result.memories.length} memories (${tokensLabel})`)
325
+
176
326
  // Store memories
177
327
  for (const memory of result.memories) {
178
328
  await store.storeMemory(project.folderId, session.id, memory)
329
+ sessionMemories.push(memory)
179
330
  totalMemories++
180
331
  }
181
332
 
182
- if (options.verbose && result.memories.length > 0) {
183
- console.log(` ${style('green', '✓')} Extracted ${result.memories.length} memories`)
333
+ // Keep the most recent session summary
334
+ if (result.session_summary) {
335
+ sessionSummary = result.session_summary
184
336
  }
185
337
  } catch (error: any) {
186
338
  failedSegments++
187
- if (options.verbose) {
188
- console.log(` ${style('red', '✗')} Failed: ${error.message}`)
339
+ spinner.stop(` ${style('red', '✗')} Segment ${segment.segmentIndex + 1}/${segment.totalSegments}: ${error.message}`)
340
+ }
341
+ }
342
+
343
+ // Run manager if we have memories and manager is enabled
344
+ if (sessionMemories.length > 0 && managerEnabled) {
345
+ try {
346
+ // Start spinner for manager
347
+ spinner.start(`Managing ${sessionMemories.length} memories - organizing with Opus 4.5...`)
348
+
349
+ // Build curation result for manager
350
+ const curationResult: CurationResult = {
351
+ memories: sessionMemories,
352
+ session_summary: sessionSummary,
353
+ project_snapshot: undefined,
354
+ }
355
+
356
+ const managerResult = await manager.manageWithSDK(
357
+ project.folderId,
358
+ 1, // session number not relevant for historical ingestion
359
+ curationResult,
360
+ storagePaths
361
+ )
362
+
363
+ if (managerResult.success) {
364
+ managedSessions++
365
+
366
+ // Build detailed action summary
367
+ const actions: string[] = []
368
+ if (managerResult.superseded > 0) actions.push(`${style('yellow', String(managerResult.superseded))} superseded`)
369
+ if (managerResult.resolved > 0) actions.push(`${style('blue', String(managerResult.resolved))} resolved`)
370
+ if (managerResult.linked > 0) actions.push(`${style('cyan', String(managerResult.linked))} linked`)
371
+ if (managerResult.primerUpdated) actions.push(`${style('magenta', 'primer')} updated`)
372
+
373
+ if (actions.length > 0) {
374
+ spinner.stop(` ${style('green', '✓')} Manager: ${actions.join(', ')}`)
375
+ } else {
376
+ spinner.stop(` ${style('green', '✓')} Manager: no changes needed`)
377
+ }
378
+
379
+ // Show file operations in verbose mode
380
+ if (options.verbose && (managerResult.filesRead > 0 || managerResult.filesWritten > 0)) {
381
+ console.log(` ${style('dim', `files: ${managerResult.filesRead} read, ${managerResult.filesWritten} written`)}`)
382
+ }
383
+ } else {
384
+ spinner.stop(` ${style('yellow', '⚠')} Manager: ${managerResult.error || 'unknown error'}`)
189
385
  }
386
+ } catch (error: any) {
387
+ spinner.stop(` ${style('yellow', '⚠')} Manager failed: ${error.message}`)
190
388
  }
191
389
  }
192
390
  }
@@ -200,6 +398,9 @@ export async function ingest(options: IngestOptions) {
200
398
  logger.info('Ingestion complete')
201
399
  console.log(` ${style('dim', 'segments:')} ${totalSegments}`)
202
400
  console.log(` ${style('dim', 'memories:')} ${style('green', String(totalMemories))}`)
401
+ if (managerEnabled && managedSessions > 0) {
402
+ console.log(` ${style('dim', 'managed:')} ${managedSessions} sessions`)
403
+ }
203
404
  if (failedSegments > 0) {
204
405
  console.log(` ${style('dim', 'failed:')} ${style('yellow', String(failedSegments))}`)
205
406
  }
@@ -207,6 +408,9 @@ export async function ingest(options: IngestOptions) {
207
408
 
208
409
  if (totalMemories > 0) {
209
410
  logger.success(`Extracted ${totalMemories} memories from ${totalSegments} segments`)
411
+ if (managerEnabled && managedSessions > 0) {
412
+ console.log(` ${style('dim', 'Manager organized memories in')} ${managedSessions} ${style('dim', 'sessions')}`)
413
+ }
210
414
  } else {
211
415
  logger.warn('No memories extracted. Try --verbose to see details.')
212
416
  }