@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.
- package/README.md +8 -0
- package/dist/index.js +32924 -11907
- package/dist/index.mjs +33020 -12003
- package/dist/server/index.js +22512 -1351
- package/dist/server/index.mjs +22719 -1558
- package/package.json +2 -1
- package/skills/memory-management.md +143 -154
- package/src/cli/commands/ingest.ts +227 -23
- package/src/cli/commands/migrate.ts +561 -80
- package/src/cli/index.ts +17 -4
- package/src/core/curator.ts +132 -22
- package/src/core/engine.test.ts +6 -14
- package/src/core/manager.ts +175 -1
- package/src/core/retrieval.ts +9 -22
- package/src/core/store.ts +24 -44
- package/src/migrations/v3-schema.ts +392 -0
- package/src/types/memory.ts +85 -123
- package/src/types/schema.ts +12 -11
- package/src/utils/logger.ts +10 -1
|
@@ -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
|
-
//
|
|
44
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
}
|