@rlabs-inc/memory 0.3.9 → 0.3.11
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 +11 -0
- package/package.json +2 -1
- package/src/cli/commands/ingest.ts +227 -23
- package/src/cli/index.ts +7 -3
- package/src/core/curator.ts +103 -5
- package/src/core/manager.ts +185 -2
- package/src/core/retrieval.ts +8 -6
- package/src/core/store.ts +10 -12
- package/src/types/memory.ts +1 -0
- package/src/utils/logger.ts +10 -0
package/README.md
CHANGED
|
@@ -336,6 +336,17 @@ This isn't just about remembering facts. It's about preserving:
|
|
|
336
336
|
|
|
337
337
|
## Changelog
|
|
338
338
|
|
|
339
|
+
### v0.3.11
|
|
340
|
+
- **Feature**: Agent SDK integration for curator and manager - no API key needed, uses Claude Code OAuth
|
|
341
|
+
- **Feature**: `memory ingest --session <id>` to ingest a single session (useful when automatic curation fails)
|
|
342
|
+
- **Feature**: Manager now runs after each session during ingestion to organize/link memories
|
|
343
|
+
- **Improvement**: Spinner activity indicator during long curator and manager operations
|
|
344
|
+
- **Improvement**: Better manager output with colored stats (superseded, resolved, linked)
|
|
345
|
+
- **Improvement**: DEBUG logs now only show with `--verbose` flag
|
|
346
|
+
|
|
347
|
+
### v0.3.10
|
|
348
|
+
- **Improvement**: Use `which claude` for universal CLI path discovery - works with any installation method (native, homebrew, npm)
|
|
349
|
+
|
|
339
350
|
### v0.3.9
|
|
340
351
|
- **Fix**: Claude Code v2.0.76+ changed `--output-format json` from single object to array of events. Updated curator and manager to handle both formats with backwards compatibility.
|
|
341
352
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rlabs-inc/memory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"cli": "bun src/cli/index.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.1",
|
|
42
43
|
"@huggingface/transformers": "^3.4.1",
|
|
43
44
|
"@rlabs-inc/fsdb": "^1.0.1",
|
|
44
45
|
"@rlabs-inc/signals": "^1.0.0"
|
|
@@ -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
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ ${c.bold('Commands:')}
|
|
|
23
23
|
${c.command('serve')} Start the memory server ${c.muted('(default)')}
|
|
24
24
|
${c.command('stats')} Show memory statistics
|
|
25
25
|
${c.command('install')} Set up hooks ${c.muted('(--claude or --gemini)')}
|
|
26
|
-
${c.command('ingest')} Ingest historical sessions into memory ${c.muted('(--project or --all)')}
|
|
26
|
+
${c.command('ingest')} Ingest historical sessions into memory ${c.muted('(--session, --project, or --all)')}
|
|
27
27
|
${c.command('migrate')} Upgrade memories to latest schema version
|
|
28
28
|
${c.command('doctor')} Check system health
|
|
29
29
|
${c.command('help')} Show this help message
|
|
@@ -32,8 +32,9 @@ ${c.bold('Options:')}
|
|
|
32
32
|
${c.cyan('-p, --port')} <port> Server port ${c.muted('(default: 8765)')}
|
|
33
33
|
${c.cyan('-v, --verbose')} Verbose output
|
|
34
34
|
${c.cyan('-q, --quiet')} Minimal output
|
|
35
|
-
${c.cyan('--dry-run')} Preview changes without applying ${c.muted('(migrate)')}
|
|
35
|
+
${c.cyan('--dry-run')} Preview changes without applying ${c.muted('(migrate, ingest)')}
|
|
36
36
|
${c.cyan('--embeddings')} Regenerate embeddings for memories ${c.muted('(migrate)')}
|
|
37
|
+
${c.cyan('--session')} <id> Ingest a specific session by ID ${c.muted('(ingest)')}
|
|
37
38
|
${c.cyan('--claude')} Install hooks for Claude Code
|
|
38
39
|
${c.cyan('--gemini')} Install hooks for Gemini CLI
|
|
39
40
|
${c.cyan('--version')} Show version
|
|
@@ -44,7 +45,8 @@ ${fmt.cmd('memory serve --port 9000')} ${c.muted('# Start on custom port')}
|
|
|
44
45
|
${fmt.cmd('memory stats')} ${c.muted('# Show memory statistics')}
|
|
45
46
|
${fmt.cmd('memory install')} ${c.muted('# Install Claude Code hooks (default)')}
|
|
46
47
|
${fmt.cmd('memory install --gemini')} ${c.muted('# Install Gemini CLI hooks')}
|
|
47
|
-
${fmt.cmd('memory ingest --
|
|
48
|
+
${fmt.cmd('memory ingest --session abc123')} ${c.muted('# Ingest a specific session')}
|
|
49
|
+
${fmt.cmd('memory ingest --project foo')} ${c.muted('# Ingest all sessions from a project')}
|
|
48
50
|
${fmt.cmd('memory ingest --all --dry-run')} ${c.muted('# Preview all sessions to ingest')}
|
|
49
51
|
${fmt.cmd('memory migrate')} ${c.muted('# Upgrade memories to v2 schema')}
|
|
50
52
|
${fmt.cmd('memory migrate --dry-run')} ${c.muted('# Preview migration without changes')}
|
|
@@ -78,6 +80,7 @@ async function main() {
|
|
|
78
80
|
'dry-run': { type: 'boolean', default: false },
|
|
79
81
|
embeddings: { type: 'boolean', default: false }, // Regenerate embeddings in migrate
|
|
80
82
|
path: { type: 'string' }, // Custom path for migrate
|
|
83
|
+
session: { type: 'string' }, // Session ID to ingest
|
|
81
84
|
project: { type: 'string' }, // Project to ingest
|
|
82
85
|
all: { type: 'boolean', default: false }, // Ingest all projects
|
|
83
86
|
limit: { type: 'string' }, // Limit sessions per project
|
|
@@ -146,6 +149,7 @@ async function main() {
|
|
|
146
149
|
case 'ingest': {
|
|
147
150
|
const { ingest } = await import('./commands/ingest.ts')
|
|
148
151
|
await ingest({
|
|
152
|
+
session: values.session,
|
|
149
153
|
project: values.project,
|
|
150
154
|
all: values.all,
|
|
151
155
|
dryRun: values['dry-run'],
|
package/src/core/curator.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { CuratedMemory, CurationResult, CurationTrigger } from '../types/me
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Get the correct Claude CLI command path
|
|
13
|
-
*
|
|
13
|
+
* Uses `which` for universal discovery across installation methods
|
|
14
14
|
*/
|
|
15
15
|
function getClaudeCommand(): string {
|
|
16
16
|
// 1. Check for explicit override
|
|
@@ -19,13 +19,19 @@ function getClaudeCommand(): string {
|
|
|
19
19
|
return envCommand
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// 2.
|
|
22
|
+
// 2. Use `which` to find claude in PATH (universal - works with native, homebrew, npm, etc.)
|
|
23
|
+
const result = Bun.spawnSync(['which', 'claude'])
|
|
24
|
+
if (result.exitCode === 0) {
|
|
25
|
+
return result.stdout.toString().trim()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 3. Legacy fallback - hardcoded native install path
|
|
23
29
|
const claudeLocal = join(homedir(), '.claude', 'local', 'claude')
|
|
24
30
|
if (existsSync(claudeLocal)) {
|
|
25
31
|
return claudeLocal
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
//
|
|
34
|
+
// 4. Last resort - assume it's in PATH
|
|
29
35
|
return 'claude'
|
|
30
36
|
}
|
|
31
37
|
|
|
@@ -392,15 +398,107 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
|
|
|
392
398
|
}
|
|
393
399
|
|
|
394
400
|
/**
|
|
395
|
-
* Curate using
|
|
401
|
+
* Curate using Claude Agent SDK (no API key needed - uses Claude Code OAuth)
|
|
396
402
|
* Takes the actual conversation messages in API format
|
|
397
403
|
*/
|
|
398
404
|
async curateWithSDK(
|
|
399
405
|
messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
|
|
400
406
|
triggerType: CurationTrigger = 'session_end'
|
|
407
|
+
): Promise<CurationResult> {
|
|
408
|
+
// Dynamic import to make Agent SDK optional
|
|
409
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk')
|
|
410
|
+
|
|
411
|
+
const systemPrompt = this.buildCurationPrompt(triggerType)
|
|
412
|
+
|
|
413
|
+
// Format the conversation as a readable transcript for the prompt
|
|
414
|
+
const transcript = this._formatConversationTranscript(messages)
|
|
415
|
+
|
|
416
|
+
// Build the prompt with transcript + curation request
|
|
417
|
+
const prompt = `Here is the conversation transcript to curate:
|
|
418
|
+
|
|
419
|
+
${transcript}
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
This session has ended. Please curate the memories from this conversation according to your system instructions. Return ONLY the JSON structure with no additional text.`
|
|
424
|
+
|
|
425
|
+
// Use Agent SDK - no API key needed, uses Claude Code OAuth
|
|
426
|
+
const q = query({
|
|
427
|
+
prompt,
|
|
428
|
+
options: {
|
|
429
|
+
systemPrompt,
|
|
430
|
+
permissionMode: 'bypassPermissions',
|
|
431
|
+
maxTurns: 1,
|
|
432
|
+
model: 'claude-opus-4-5-20251101',
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
// Iterate through the async generator to get the result
|
|
437
|
+
let resultText = ''
|
|
438
|
+
for await (const msg of q) {
|
|
439
|
+
if (msg.type === 'result' && 'result' in msg) {
|
|
440
|
+
resultText = msg.result
|
|
441
|
+
break
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!resultText) {
|
|
446
|
+
return { session_summary: '', memories: [] }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return this.parseCurationResponse(resultText)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Format conversation messages into a readable transcript
|
|
454
|
+
*/
|
|
455
|
+
private _formatConversationTranscript(
|
|
456
|
+
messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>
|
|
457
|
+
): string {
|
|
458
|
+
const lines: string[] = []
|
|
459
|
+
|
|
460
|
+
for (const msg of messages) {
|
|
461
|
+
const role = msg.role === 'user' ? 'User' : 'Assistant'
|
|
462
|
+
let content: string
|
|
463
|
+
|
|
464
|
+
if (typeof msg.content === 'string') {
|
|
465
|
+
content = msg.content
|
|
466
|
+
} else if (Array.isArray(msg.content)) {
|
|
467
|
+
// Extract text from content blocks
|
|
468
|
+
content = msg.content
|
|
469
|
+
.filter((block: any) => block.type === 'text' && block.text)
|
|
470
|
+
.map((block: any) => block.text)
|
|
471
|
+
.join('\n')
|
|
472
|
+
|
|
473
|
+
// Also note tool uses (but don't include full details)
|
|
474
|
+
const toolUses = msg.content.filter((block: any) => block.type === 'tool_use')
|
|
475
|
+
if (toolUses.length > 0) {
|
|
476
|
+
const toolNames = toolUses.map((t: any) => t.name).join(', ')
|
|
477
|
+
content += `\n[Used tools: ${toolNames}]`
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
content = '[empty message]'
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (content.trim()) {
|
|
484
|
+
lines.push(`**${role}:**\n${content}\n`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return lines.join('\n')
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Legacy method: Curate using Anthropic SDK with API key
|
|
493
|
+
* Kept for backwards compatibility
|
|
494
|
+
* @deprecated Use curateWithSDK() which uses Agent SDK (no API key needed)
|
|
495
|
+
*/
|
|
496
|
+
async curateWithAnthropicSDK(
|
|
497
|
+
messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
|
|
498
|
+
triggerType: CurationTrigger = 'session_end'
|
|
401
499
|
): Promise<CurationResult> {
|
|
402
500
|
if (!this._config.apiKey) {
|
|
403
|
-
throw new Error('API key required for SDK mode. Set ANTHROPIC_API_KEY environment variable.')
|
|
501
|
+
throw new Error('API key required for Anthropic SDK mode. Set ANTHROPIC_API_KEY environment variable.')
|
|
404
502
|
}
|
|
405
503
|
|
|
406
504
|
// Dynamic import to make SDK optional
|
package/src/core/manager.ts
CHANGED
|
@@ -11,15 +11,24 @@ import type { CurationResult } from '../types/memory.ts'
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Get the Claude CLI command path
|
|
14
|
-
*
|
|
14
|
+
* Uses `which` for universal discovery across installation methods
|
|
15
15
|
*/
|
|
16
16
|
function getClaudeCommand(): string {
|
|
17
|
+
// 1. Check for explicit override
|
|
17
18
|
const envCommand = process.env.CURATOR_COMMAND
|
|
18
19
|
if (envCommand) return envCommand
|
|
19
20
|
|
|
21
|
+
// 2. Use `which` to find claude in PATH (universal - works with native, homebrew, npm, etc.)
|
|
22
|
+
const result = Bun.spawnSync(['which', 'claude'])
|
|
23
|
+
if (result.exitCode === 0) {
|
|
24
|
+
return result.stdout.toString().trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 3. Legacy fallback - hardcoded native install path
|
|
20
28
|
const claudeLocal = join(homedir(), '.claude', 'local', 'claude')
|
|
21
29
|
if (existsSync(claudeLocal)) return claudeLocal
|
|
22
30
|
|
|
31
|
+
// 4. Last resort
|
|
23
32
|
return 'claude'
|
|
24
33
|
}
|
|
25
34
|
|
|
@@ -374,7 +383,135 @@ Please process these memories according to your management procedure. Use the ex
|
|
|
374
383
|
}
|
|
375
384
|
|
|
376
385
|
/**
|
|
377
|
-
* Manage using
|
|
386
|
+
* Manage using Claude Agent SDK (no API key needed - uses Claude Code OAuth)
|
|
387
|
+
* Use this for ingest command - cleaner than CLI subprocess
|
|
388
|
+
*/
|
|
389
|
+
async manageWithSDK(
|
|
390
|
+
projectId: string,
|
|
391
|
+
sessionNumber: number,
|
|
392
|
+
result: CurationResult,
|
|
393
|
+
storagePaths?: StoragePaths
|
|
394
|
+
): Promise<ManagementResult> {
|
|
395
|
+
// Skip if disabled via config or env var
|
|
396
|
+
if (!this._config.enabled || process.env.MEMORY_MANAGER_DISABLED === '1') {
|
|
397
|
+
return {
|
|
398
|
+
success: true,
|
|
399
|
+
superseded: 0,
|
|
400
|
+
resolved: 0,
|
|
401
|
+
linked: 0,
|
|
402
|
+
filesRead: 0,
|
|
403
|
+
filesWritten: 0,
|
|
404
|
+
primerUpdated: false,
|
|
405
|
+
actions: [],
|
|
406
|
+
summary: 'Management agent disabled',
|
|
407
|
+
fullReport: 'Management agent disabled via configuration',
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Skip if no memories
|
|
412
|
+
if (result.memories.length === 0) {
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
superseded: 0,
|
|
416
|
+
resolved: 0,
|
|
417
|
+
linked: 0,
|
|
418
|
+
filesRead: 0,
|
|
419
|
+
filesWritten: 0,
|
|
420
|
+
primerUpdated: false,
|
|
421
|
+
actions: [],
|
|
422
|
+
summary: 'No memories to process',
|
|
423
|
+
fullReport: 'No memories to process - skipped',
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Load skill file
|
|
428
|
+
const systemPrompt = await this.buildManagementPrompt()
|
|
429
|
+
if (!systemPrompt) {
|
|
430
|
+
return {
|
|
431
|
+
success: false,
|
|
432
|
+
superseded: 0,
|
|
433
|
+
resolved: 0,
|
|
434
|
+
linked: 0,
|
|
435
|
+
filesRead: 0,
|
|
436
|
+
filesWritten: 0,
|
|
437
|
+
primerUpdated: false,
|
|
438
|
+
actions: [],
|
|
439
|
+
summary: '',
|
|
440
|
+
fullReport: 'Error: Management skill file not found',
|
|
441
|
+
error: 'Management skill not found',
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const userMessage = this.buildUserMessage(projectId, sessionNumber, result, storagePaths)
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
// Dynamic import to make Agent SDK optional
|
|
449
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk')
|
|
450
|
+
|
|
451
|
+
// Build allowed directories for file access
|
|
452
|
+
const globalPath = storagePaths?.globalPath ?? join(homedir(), '.local', 'share', 'memory', 'global')
|
|
453
|
+
const projectPath = storagePaths?.projectPath ?? join(homedir(), '.local', 'share', 'memory')
|
|
454
|
+
|
|
455
|
+
// Use Agent SDK with file tools
|
|
456
|
+
const q = query({
|
|
457
|
+
prompt: userMessage,
|
|
458
|
+
options: {
|
|
459
|
+
systemPrompt,
|
|
460
|
+
permissionMode: 'bypassPermissions',
|
|
461
|
+
model: 'claude-opus-4-5-20251101',
|
|
462
|
+
// Only allow file tools - no Bash, no web
|
|
463
|
+
allowedTools: ['Read', 'Write', 'Edit', 'Glob', 'Grep'],
|
|
464
|
+
// Allow access to memory directories
|
|
465
|
+
additionalDirectories: [globalPath, projectPath],
|
|
466
|
+
// Limit turns if configured
|
|
467
|
+
maxTurns: this._config.maxTurns,
|
|
468
|
+
},
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
// Iterate through the async generator to get the result
|
|
472
|
+
let resultText = ''
|
|
473
|
+
for await (const msg of q) {
|
|
474
|
+
if (msg.type === 'result' && 'result' in msg) {
|
|
475
|
+
resultText = msg.result
|
|
476
|
+
break
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!resultText) {
|
|
481
|
+
return {
|
|
482
|
+
success: true,
|
|
483
|
+
superseded: 0,
|
|
484
|
+
resolved: 0,
|
|
485
|
+
linked: 0,
|
|
486
|
+
filesRead: 0,
|
|
487
|
+
filesWritten: 0,
|
|
488
|
+
primerUpdated: false,
|
|
489
|
+
actions: [],
|
|
490
|
+
summary: 'No result from management agent',
|
|
491
|
+
fullReport: 'Management agent completed but returned no result',
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return this._parseSDKManagementResult(resultText)
|
|
496
|
+
} catch (error: any) {
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
superseded: 0,
|
|
500
|
+
resolved: 0,
|
|
501
|
+
linked: 0,
|
|
502
|
+
filesRead: 0,
|
|
503
|
+
filesWritten: 0,
|
|
504
|
+
primerUpdated: false,
|
|
505
|
+
actions: [],
|
|
506
|
+
summary: '',
|
|
507
|
+
fullReport: `Error: Agent SDK failed: ${error.message}`,
|
|
508
|
+
error: error.message,
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Manage using CLI subprocess (for hooks - keeps working while we migrate)
|
|
378
515
|
* Similar to Curator.curateWithCLI
|
|
379
516
|
*/
|
|
380
517
|
async manageWithCLI(
|
|
@@ -487,6 +624,52 @@ Please process these memories according to your management procedure. Use the ex
|
|
|
487
624
|
|
|
488
625
|
return this.parseManagementResponse(stdout)
|
|
489
626
|
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Parse management result from Agent SDK response
|
|
630
|
+
* Similar to parseManagementResponse but for SDK output format
|
|
631
|
+
*/
|
|
632
|
+
private _parseSDKManagementResult(resultText: string): ManagementResult {
|
|
633
|
+
// Extract actions section
|
|
634
|
+
const actionsMatch = resultText.match(/=== MANAGEMENT ACTIONS ===([\s\S]*?)(?:=== SUMMARY ===|$)/)
|
|
635
|
+
const actions: string[] = []
|
|
636
|
+
if (actionsMatch) {
|
|
637
|
+
const actionsText = actionsMatch[1]
|
|
638
|
+
const actionLines = actionsText.split('\n')
|
|
639
|
+
.map((line: string) => line.trim())
|
|
640
|
+
.filter((line: string) => /^(READ|WRITE|RECEIVED|CREATED|UPDATED|SUPERSEDED|RESOLVED|LINKED|PRIMER|SKIPPED|NO_ACTION)/.test(line))
|
|
641
|
+
actions.push(...actionLines)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Extract the full report
|
|
645
|
+
const reportMatch = resultText.match(/(=== MANAGEMENT ACTIONS ===[\s\S]*)/)
|
|
646
|
+
const fullReport = reportMatch ? reportMatch[1].trim() : resultText
|
|
647
|
+
|
|
648
|
+
// Extract stats from result text
|
|
649
|
+
const supersededMatch = resultText.match(/memories_superseded[:\s]+(\d+)/i) || resultText.match(/superseded[:\s]+(\d+)/i)
|
|
650
|
+
const resolvedMatch = resultText.match(/memories_resolved[:\s]+(\d+)/i) || resultText.match(/resolved[:\s]+(\d+)/i)
|
|
651
|
+
const linkedMatch = resultText.match(/memories_linked[:\s]+(\d+)/i) || resultText.match(/linked[:\s]+(\d+)/i)
|
|
652
|
+
const filesReadMatch = resultText.match(/files_read[:\s]+(\d+)/i)
|
|
653
|
+
const filesWrittenMatch = resultText.match(/files_written[:\s]+(\d+)/i)
|
|
654
|
+
const primerUpdated = /primer_updated[:\s]+true/i.test(resultText) || /PRIMER\s+OK/i.test(resultText)
|
|
655
|
+
|
|
656
|
+
// Count file operations from actions if not in summary
|
|
657
|
+
const readActions = actions.filter((a: string) => a.startsWith('READ OK')).length
|
|
658
|
+
const writeActions = actions.filter((a: string) => a.startsWith('WRITE OK')).length
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
success: true,
|
|
662
|
+
superseded: supersededMatch ? parseInt(supersededMatch[1]) : 0,
|
|
663
|
+
resolved: resolvedMatch ? parseInt(resolvedMatch[1]) : 0,
|
|
664
|
+
linked: linkedMatch ? parseInt(linkedMatch[1]) : 0,
|
|
665
|
+
filesRead: filesReadMatch ? parseInt(filesReadMatch[1]) : readActions,
|
|
666
|
+
filesWritten: filesWrittenMatch ? parseInt(filesWrittenMatch[1]) : writeActions,
|
|
667
|
+
primerUpdated,
|
|
668
|
+
actions,
|
|
669
|
+
summary: resultText.slice(0, 500),
|
|
670
|
+
fullReport,
|
|
671
|
+
}
|
|
672
|
+
}
|
|
490
673
|
}
|
|
491
674
|
|
|
492
675
|
/**
|
package/src/core/retrieval.ts
CHANGED
|
@@ -260,7 +260,7 @@ export class SmartVectorRetrieval {
|
|
|
260
260
|
const min = Math.min(...samples)
|
|
261
261
|
const max = Math.max(...samples)
|
|
262
262
|
const avg = samples.reduce((a, b) => a + b, 0) / samples.length
|
|
263
|
-
|
|
263
|
+
logger.debug(`Vector similarities: min=${(min*100).toFixed(1)}% max=${(max*100).toFixed(1)}% avg=${(avg*100).toFixed(1)}% (${samples.length} samples)`, 'retrieval')
|
|
264
264
|
this._vectorDebugSamples = [] // Reset for next retrieval
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -516,11 +516,13 @@ export class SmartVectorRetrieval {
|
|
|
516
516
|
})
|
|
517
517
|
|
|
518
518
|
// Debug: show top 15 candidates with calculated scores
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
519
|
+
if (logger.isVerbose()) {
|
|
520
|
+
logger.debug(`Top 15 candidates (sorted):`, 'retrieval')
|
|
521
|
+
for (let i = 0; i < Math.min(15, projectsSorted.length); i++) {
|
|
522
|
+
const m = projectsSorted[i]
|
|
523
|
+
const action = m.memory.action_required ? '⚡' : ''
|
|
524
|
+
logger.debug(` ${i+1}. [${m.signals.count}sig] score=${m.importanceScore.toFixed(2)} ${action} ${m.memory.content.slice(0, 45)}...`, 'retrieval')
|
|
525
|
+
}
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
for (const item of projectsSorted) {
|
package/src/core/store.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { createDatabase, type Database, type PersistentCollection } from '@rlabs-inc/fsdb'
|
|
7
7
|
import { homedir } from 'os'
|
|
8
8
|
import { join } from 'path'
|
|
9
|
+
import { logger } from '../utils/logger.ts'
|
|
9
10
|
import {
|
|
10
11
|
type CuratedMemory,
|
|
11
12
|
type StoredMemory,
|
|
@@ -126,7 +127,7 @@ export class MemoryStore {
|
|
|
126
127
|
|
|
127
128
|
// Use the configured global path (always central)
|
|
128
129
|
const globalPath = this._config.globalPath
|
|
129
|
-
|
|
130
|
+
logger.debug(`Initializing global database at ${globalPath}`, 'store')
|
|
130
131
|
|
|
131
132
|
const db = createDatabase({
|
|
132
133
|
name: 'global',
|
|
@@ -414,13 +415,13 @@ export class MemoryStore {
|
|
|
414
415
|
*/
|
|
415
416
|
async getProject(projectId: string): Promise<ProjectDB> {
|
|
416
417
|
if (this._projects.has(projectId)) {
|
|
417
|
-
|
|
418
|
+
logger.debug(`Returning cached databases for ${projectId}`, 'store')
|
|
418
419
|
return this._projects.get(projectId)!
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
|
|
422
|
+
logger.debug(`Creating NEW databases for ${projectId}`, 'store')
|
|
422
423
|
const projectPath = join(this._config.basePath, projectId)
|
|
423
|
-
|
|
424
|
+
logger.debug(`Path: ${projectPath}`, 'store')
|
|
424
425
|
|
|
425
426
|
// Create the database for this project
|
|
426
427
|
const db = createDatabase({
|
|
@@ -743,9 +744,7 @@ export class MemoryStore {
|
|
|
743
744
|
): Promise<string> {
|
|
744
745
|
const { summaries } = await this.getProject(projectId)
|
|
745
746
|
|
|
746
|
-
|
|
747
|
-
console.log(` Summary length: ${summary.length} chars`)
|
|
748
|
-
console.log(` Summaries count before: ${summaries.all().length}`)
|
|
747
|
+
logger.debug(`Storing summary for ${projectId}: ${summary.length} chars`, 'store')
|
|
749
748
|
|
|
750
749
|
const id = summaries.insert({
|
|
751
750
|
session_id: sessionId,
|
|
@@ -754,8 +753,7 @@ export class MemoryStore {
|
|
|
754
753
|
interaction_tone: interactionTone,
|
|
755
754
|
})
|
|
756
755
|
|
|
757
|
-
|
|
758
|
-
console.log(` Inserted ID: ${id}`)
|
|
756
|
+
logger.debug(`Summary stored with ID: ${id}`, 'store')
|
|
759
757
|
|
|
760
758
|
return id
|
|
761
759
|
}
|
|
@@ -766,14 +764,14 @@ export class MemoryStore {
|
|
|
766
764
|
async getLatestSummary(projectId: string): Promise<SessionSummary | null> {
|
|
767
765
|
const { summaries } = await this.getProject(projectId)
|
|
768
766
|
|
|
769
|
-
|
|
767
|
+
logger.debug(`Getting latest summary for ${projectId}`, 'store')
|
|
770
768
|
const all = summaries.all()
|
|
771
|
-
console.log(` Summaries found: ${all.length}`)
|
|
772
769
|
|
|
773
770
|
if (!all.length) {
|
|
774
|
-
|
|
771
|
+
logger.debug(`No summaries found for ${projectId}`, 'store')
|
|
775
772
|
return null
|
|
776
773
|
}
|
|
774
|
+
logger.debug(`Found ${all.length} summaries for ${projectId}`, 'store')
|
|
777
775
|
|
|
778
776
|
// Sort by created timestamp (most recent first)
|
|
779
777
|
const sorted = [...all].sort((a, b) => b.created - a.created)
|
package/src/types/memory.ts
CHANGED
|
@@ -67,6 +67,7 @@ export type CurationTrigger =
|
|
|
67
67
|
| 'pre_compact' // Before context compression
|
|
68
68
|
| 'context_full' // Context window nearly full
|
|
69
69
|
| 'manual' // Manual trigger
|
|
70
|
+
| 'historical' // Historical session ingestion
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* A memory curated by Claude with semantic understanding
|
package/src/utils/logger.ts
CHANGED
|
@@ -64,6 +64,16 @@ export const logger = {
|
|
|
64
64
|
return _verbose
|
|
65
65
|
},
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Debug message (only shown in verbose mode)
|
|
69
|
+
*/
|
|
70
|
+
debug(message: string, prefix?: string) {
|
|
71
|
+
if (_verbose) {
|
|
72
|
+
const pfx = prefix ? style('dim', `[${prefix}] `) : ''
|
|
73
|
+
console.log(`${style('dim', `🔍 ${pfx}${message}`)}`)
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
67
77
|
/**
|
|
68
78
|
* Info message
|
|
69
79
|
*/
|