@rlabs-inc/memory 0.3.5 → 0.3.6
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 +123 -30
- package/dist/index.js +803 -179
- package/dist/index.mjs +803 -179
- package/dist/server/index.js +36774 -2643
- package/dist/server/index.mjs +1034 -185
- package/package.json +3 -2
- package/skills/memory-management.md +686 -0
- package/src/cli/commands/migrate.ts +423 -0
- package/src/cli/commands/serve.ts +88 -0
- package/src/cli/index.ts +21 -0
- package/src/core/curator.ts +151 -17
- package/src/core/engine.ts +159 -11
- package/src/core/manager.ts +484 -0
- package/src/core/retrieval.ts +547 -420
- package/src/core/store.ts +383 -8
- package/src/server/index.ts +108 -8
- package/src/types/memory.ts +142 -0
- package/src/types/schema.ts +80 -7
- package/src/utils/logger.ts +310 -46
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// MIGRATE COMMAND - Upgrade memory files to latest schema version
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { homedir } from 'os'
|
|
7
|
+
import { Glob } from 'bun'
|
|
8
|
+
import { c, symbols, fmt } from '../colors.ts'
|
|
9
|
+
import { MEMORY_SCHEMA_VERSION } from '../../types/schema.ts'
|
|
10
|
+
import { V2_DEFAULTS } from '../../types/memory.ts'
|
|
11
|
+
import { EmbeddingGenerator } from '../../core/embeddings.ts'
|
|
12
|
+
|
|
13
|
+
interface MigrateOptions {
|
|
14
|
+
dryRun?: boolean
|
|
15
|
+
verbose?: boolean
|
|
16
|
+
path?: string // Custom path to migrate
|
|
17
|
+
embeddings?: boolean // Regenerate embeddings for all memories
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Module-level embedder for reuse across files
|
|
21
|
+
let embedder: ((text: string) => Promise<Float32Array>) | null = null
|
|
22
|
+
|
|
23
|
+
async function initEmbedder(): Promise<void> {
|
|
24
|
+
if (embedder) return
|
|
25
|
+
console.log(c.muted(` ${symbols.gear} Loading embedding model...`))
|
|
26
|
+
const generator = new EmbeddingGenerator()
|
|
27
|
+
await generator.initialize()
|
|
28
|
+
embedder = generator.createEmbedder()
|
|
29
|
+
console.log(c.success(` ${symbols.tick} Embedding model ready`))
|
|
30
|
+
console.log()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface MigrationResult {
|
|
34
|
+
total: number
|
|
35
|
+
migrated: number
|
|
36
|
+
skipped: number
|
|
37
|
+
embeddingsGenerated: number
|
|
38
|
+
errors: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse YAML frontmatter from markdown content
|
|
43
|
+
*/
|
|
44
|
+
function parseFrontmatter(content: string): { frontmatter: Record<string, any>; body: string } | null {
|
|
45
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
|
|
46
|
+
if (!match) return null
|
|
47
|
+
|
|
48
|
+
const yamlContent = match[1]
|
|
49
|
+
const body = match[2] ?? ''
|
|
50
|
+
|
|
51
|
+
// Simple YAML parser for our known structure
|
|
52
|
+
const frontmatter: Record<string, any> = {}
|
|
53
|
+
|
|
54
|
+
for (const line of yamlContent.split('\n')) {
|
|
55
|
+
const colonIndex = line.indexOf(':')
|
|
56
|
+
if (colonIndex === -1) continue
|
|
57
|
+
|
|
58
|
+
const key = line.slice(0, colonIndex).trim()
|
|
59
|
+
let value = line.slice(colonIndex + 1).trim()
|
|
60
|
+
|
|
61
|
+
// Handle arrays (simple case: ["a", "b"])
|
|
62
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
63
|
+
try {
|
|
64
|
+
frontmatter[key] = JSON.parse(value)
|
|
65
|
+
} catch {
|
|
66
|
+
frontmatter[key] = value
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Handle booleans
|
|
70
|
+
else if (value === 'true') frontmatter[key] = true
|
|
71
|
+
else if (value === 'false') frontmatter[key] = false
|
|
72
|
+
// Handle null
|
|
73
|
+
else if (value === 'null' || value === '') frontmatter[key] = null
|
|
74
|
+
// Handle numbers
|
|
75
|
+
else if (!isNaN(Number(value)) && value !== '') frontmatter[key] = Number(value)
|
|
76
|
+
// Handle quoted strings
|
|
77
|
+
else if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
78
|
+
frontmatter[key] = value.slice(1, -1)
|
|
79
|
+
}
|
|
80
|
+
// Plain string
|
|
81
|
+
else frontmatter[key] = value
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { frontmatter, body }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Serialize frontmatter back to YAML
|
|
89
|
+
*/
|
|
90
|
+
function serializeFrontmatter(frontmatter: Record<string, any>, body: string): string {
|
|
91
|
+
const lines: string[] = ['---']
|
|
92
|
+
|
|
93
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
94
|
+
if (value === null || value === undefined) {
|
|
95
|
+
lines.push(`${key}: null`)
|
|
96
|
+
} else if (Array.isArray(value)) {
|
|
97
|
+
lines.push(`${key}: ${JSON.stringify(value)}`)
|
|
98
|
+
} else if (typeof value === 'boolean') {
|
|
99
|
+
lines.push(`${key}: ${value}`)
|
|
100
|
+
} else if (typeof value === 'number') {
|
|
101
|
+
lines.push(`${key}: ${value}`)
|
|
102
|
+
} else {
|
|
103
|
+
// String - quote if contains special characters
|
|
104
|
+
const needsQuotes = /[:#\[\]{}'",]/.test(value) || value.includes('\n')
|
|
105
|
+
lines.push(`${key}: ${needsQuotes ? JSON.stringify(value) : value}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push('---')
|
|
110
|
+
if (body.trim()) {
|
|
111
|
+
lines.push('')
|
|
112
|
+
lines.push(body.trim())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return lines.join('\n') + '\n'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply v2 defaults to frontmatter
|
|
120
|
+
*/
|
|
121
|
+
function applyMigration(frontmatter: Record<string, any>): Record<string, any> {
|
|
122
|
+
const contextType = frontmatter.context_type ?? 'general'
|
|
123
|
+
const typeDefaults = V2_DEFAULTS.typeDefaults[contextType] ?? V2_DEFAULTS.typeDefaults.technical
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
...frontmatter,
|
|
127
|
+
|
|
128
|
+
// Lifecycle status
|
|
129
|
+
status: frontmatter.status ?? 'active',
|
|
130
|
+
scope: frontmatter.scope ?? typeDefaults?.scope ?? 'project',
|
|
131
|
+
|
|
132
|
+
// Temporal class & decay
|
|
133
|
+
temporal_class: frontmatter.temporal_class ?? typeDefaults?.temporal_class ?? 'medium_term',
|
|
134
|
+
fade_rate: frontmatter.fade_rate ?? typeDefaults?.fade_rate ?? 0.03,
|
|
135
|
+
|
|
136
|
+
// Temporal tracking (initialize with 0 since we don't know the session numbers)
|
|
137
|
+
sessions_since_surfaced: frontmatter.sessions_since_surfaced ?? 0,
|
|
138
|
+
|
|
139
|
+
// Lifecycle triggers
|
|
140
|
+
awaiting_implementation: frontmatter.awaiting_implementation ?? false,
|
|
141
|
+
awaiting_decision: frontmatter.awaiting_decision ?? false,
|
|
142
|
+
exclude_from_retrieval: frontmatter.exclude_from_retrieval ?? false,
|
|
143
|
+
|
|
144
|
+
// Retrieval weight
|
|
145
|
+
retrieval_weight: frontmatter.retrieval_weight ?? frontmatter.importance_weight ?? 0.5,
|
|
146
|
+
|
|
147
|
+
// Initialize empty arrays
|
|
148
|
+
related_to: frontmatter.related_to ?? [],
|
|
149
|
+
resolves: frontmatter.resolves ?? [],
|
|
150
|
+
child_ids: frontmatter.child_ids ?? [],
|
|
151
|
+
blocks: frontmatter.blocks ?? [],
|
|
152
|
+
related_files: frontmatter.related_files ?? [],
|
|
153
|
+
|
|
154
|
+
// Mark as migrated
|
|
155
|
+
schema_version: MEMORY_SCHEMA_VERSION,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if file needs migration
|
|
161
|
+
*/
|
|
162
|
+
function needsMigration(frontmatter: Record<string, any>): boolean {
|
|
163
|
+
return !frontmatter.schema_version || frontmatter.schema_version < MEMORY_SCHEMA_VERSION
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Migrate a single memory file
|
|
168
|
+
*/
|
|
169
|
+
async function migrateFile(filePath: string, options: MigrateOptions): Promise<{ migrated: boolean; embeddingGenerated: boolean; error?: string }> {
|
|
170
|
+
try {
|
|
171
|
+
const file = Bun.file(filePath)
|
|
172
|
+
const content = await file.text()
|
|
173
|
+
const parsed = parseFrontmatter(content)
|
|
174
|
+
|
|
175
|
+
if (!parsed) {
|
|
176
|
+
return { migrated: false, embeddingGenerated: false, error: 'Could not parse frontmatter' }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const needsSchema = needsMigration(parsed.frontmatter)
|
|
180
|
+
const needsEmbedding = options.embeddings && (
|
|
181
|
+
parsed.frontmatter.embedding === null ||
|
|
182
|
+
parsed.frontmatter.embedding === undefined ||
|
|
183
|
+
(Array.isArray(parsed.frontmatter.embedding) && parsed.frontmatter.embedding.length === 0)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
// Nothing to do
|
|
187
|
+
if (!needsSchema && !needsEmbedding) {
|
|
188
|
+
return { migrated: false, embeddingGenerated: false }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let newFrontmatter = parsed.frontmatter
|
|
192
|
+
|
|
193
|
+
// Apply schema migration if needed
|
|
194
|
+
if (needsSchema) {
|
|
195
|
+
newFrontmatter = applyMigration(parsed.frontmatter)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Generate embedding if needed
|
|
199
|
+
let embeddingGenerated = false
|
|
200
|
+
if (needsEmbedding && parsed.body.trim()) {
|
|
201
|
+
await initEmbedder()
|
|
202
|
+
const embedding = await embedder!(parsed.body.trim())
|
|
203
|
+
newFrontmatter.embedding = Array.from(embedding)
|
|
204
|
+
embeddingGenerated = true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const newContent = serializeFrontmatter(newFrontmatter, parsed.body)
|
|
208
|
+
|
|
209
|
+
if (!options.dryRun) {
|
|
210
|
+
await Bun.write(filePath, newContent)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { migrated: needsSchema, embeddingGenerated }
|
|
214
|
+
} catch (error: any) {
|
|
215
|
+
return { migrated: false, embeddingGenerated: false, error: error.message }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Find all memory directories to migrate
|
|
221
|
+
* Handles both central and local storage modes
|
|
222
|
+
* Global memories are ALWAYS in central location, even in local mode
|
|
223
|
+
*/
|
|
224
|
+
async function findMemoryPaths(customPath?: string): Promise<{ path: string; label: string }[]> {
|
|
225
|
+
const paths: { path: string; label: string }[] = []
|
|
226
|
+
|
|
227
|
+
if (customPath) {
|
|
228
|
+
// Custom path specified - just use it
|
|
229
|
+
paths.push({ path: customPath, label: customPath })
|
|
230
|
+
return paths
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const storageMode = process.env.MEMORY_STORAGE_MODE ?? 'central'
|
|
234
|
+
const centralPath = process.env.MEMORY_CENTRAL_PATH ?? join(homedir(), '.local', 'share', 'memory')
|
|
235
|
+
|
|
236
|
+
// ALWAYS check global memories (even in local mode, global is central)
|
|
237
|
+
const globalMemoriesPath = join(centralPath, 'global', 'memories')
|
|
238
|
+
try {
|
|
239
|
+
const globalGlob = new Glob('*.md')
|
|
240
|
+
let hasGlobalFiles = false
|
|
241
|
+
for await (const _ of globalGlob.scan({ cwd: globalMemoriesPath })) {
|
|
242
|
+
hasGlobalFiles = true
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
if (hasGlobalFiles) {
|
|
246
|
+
paths.push({ path: globalMemoriesPath, label: 'Global (shared across projects)' })
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Global directory doesn't exist yet
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (storageMode === 'local') {
|
|
253
|
+
// Local mode: check current directory for project memories
|
|
254
|
+
const localFolder = '.memory'
|
|
255
|
+
const cwd = process.cwd()
|
|
256
|
+
const localMemoriesPath = join(cwd, localFolder, 'memories')
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const localGlob = new Glob('*.md')
|
|
260
|
+
let hasLocalFiles = false
|
|
261
|
+
for await (const _ of localGlob.scan({ cwd: localMemoriesPath })) {
|
|
262
|
+
hasLocalFiles = true
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
if (hasLocalFiles) {
|
|
266
|
+
paths.push({ path: localMemoriesPath, label: `Local project: ${cwd}` })
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
// Local directory doesn't exist
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
// Central mode: check ~/.local/share/memory/[projects]/
|
|
273
|
+
try {
|
|
274
|
+
const projectGlob = new Glob('*/memories')
|
|
275
|
+
for await (const match of projectGlob.scan({ cwd: centralPath, onlyFiles: false })) {
|
|
276
|
+
// Skip global, we already handled it
|
|
277
|
+
if (match.startsWith('global/')) continue
|
|
278
|
+
|
|
279
|
+
const fullPath = join(centralPath, match)
|
|
280
|
+
const projectId = match.split('/')[0]
|
|
281
|
+
|
|
282
|
+
// Check if directory has any .md files
|
|
283
|
+
try {
|
|
284
|
+
const mdGlob = new Glob('*.md')
|
|
285
|
+
let hasFiles = false
|
|
286
|
+
for await (const _ of mdGlob.scan({ cwd: fullPath })) {
|
|
287
|
+
hasFiles = true
|
|
288
|
+
break
|
|
289
|
+
}
|
|
290
|
+
if (hasFiles) {
|
|
291
|
+
paths.push({ path: fullPath, label: `Project: ${projectId}` })
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// Skip if we can't read
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Central storage doesn't exist
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return paths
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Migrate all memory files in a directory using Bun's Glob
|
|
307
|
+
*/
|
|
308
|
+
async function migrateDirectory(dir: string, options: MigrateOptions): Promise<MigrationResult> {
|
|
309
|
+
const result: MigrationResult = { total: 0, migrated: 0, skipped: 0, embeddingsGenerated: 0, errors: [] }
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const glob = new Glob('*.md')
|
|
313
|
+
|
|
314
|
+
for await (const file of glob.scan({ cwd: dir })) {
|
|
315
|
+
result.total++
|
|
316
|
+
const filePath = join(dir, file)
|
|
317
|
+
|
|
318
|
+
const { migrated, embeddingGenerated, error } = await migrateFile(filePath, options)
|
|
319
|
+
|
|
320
|
+
if (error) {
|
|
321
|
+
result.errors.push(`${file}: ${error}`)
|
|
322
|
+
if (options.verbose) {
|
|
323
|
+
console.log(` ${c.error(symbols.cross)} ${file}: ${error}`)
|
|
324
|
+
}
|
|
325
|
+
} else if (migrated || embeddingGenerated) {
|
|
326
|
+
if (migrated) result.migrated++
|
|
327
|
+
if (embeddingGenerated) result.embeddingsGenerated++
|
|
328
|
+
if (options.verbose) {
|
|
329
|
+
const actions = []
|
|
330
|
+
if (migrated) actions.push('schema')
|
|
331
|
+
if (embeddingGenerated) actions.push('embedding')
|
|
332
|
+
console.log(` ${c.success(symbols.tick)} ${file} (${actions.join(', ')})`)
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
result.skipped++
|
|
336
|
+
if (options.verbose) {
|
|
337
|
+
console.log(` ${c.muted(symbols.bullet)} ${file} (up to date)`)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (error: any) {
|
|
342
|
+
result.errors.push(`Directory error: ${error.message}`)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return result
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function migrate(options: MigrateOptions) {
|
|
349
|
+
console.log()
|
|
350
|
+
console.log(c.header(`${symbols.gear} Memory Migration`))
|
|
351
|
+
console.log()
|
|
352
|
+
console.log(` ${fmt.kv('Target Version', `v${MEMORY_SCHEMA_VERSION}`)}`)
|
|
353
|
+
console.log(` ${fmt.kv('Storage Mode', process.env.MEMORY_STORAGE_MODE ?? 'central')}`)
|
|
354
|
+
console.log(` ${fmt.kv('Embeddings', options.embeddings ? c.success('Regenerate') : c.muted('Skip'))}`)
|
|
355
|
+
console.log(` ${fmt.kv('Mode', options.dryRun ? c.warn('Dry Run') : c.success('Live'))}`)
|
|
356
|
+
console.log()
|
|
357
|
+
|
|
358
|
+
const memoryPaths = await findMemoryPaths(options.path)
|
|
359
|
+
|
|
360
|
+
if (memoryPaths.length === 0) {
|
|
361
|
+
console.log(c.warn(` ${symbols.warning} No memory directories found`))
|
|
362
|
+
console.log()
|
|
363
|
+
console.log(c.muted(` Expected locations:`))
|
|
364
|
+
console.log(c.muted(` Central: ~/.local/share/memory/[project]/memories/`))
|
|
365
|
+
console.log(c.muted(` Local: ./.memory/memories/`))
|
|
366
|
+
console.log()
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const totals: MigrationResult = { total: 0, migrated: 0, skipped: 0, embeddingsGenerated: 0, errors: [] }
|
|
371
|
+
|
|
372
|
+
for (const { path, label } of memoryPaths) {
|
|
373
|
+
console.log(fmt.section(label))
|
|
374
|
+
console.log(c.muted(` ${path}`))
|
|
375
|
+
console.log()
|
|
376
|
+
|
|
377
|
+
const result = await migrateDirectory(path, options)
|
|
378
|
+
|
|
379
|
+
totals.total += result.total
|
|
380
|
+
totals.migrated += result.migrated
|
|
381
|
+
totals.skipped += result.skipped
|
|
382
|
+
totals.embeddingsGenerated += result.embeddingsGenerated
|
|
383
|
+
totals.errors.push(...result.errors)
|
|
384
|
+
|
|
385
|
+
if (!options.verbose) {
|
|
386
|
+
console.log(` ${fmt.kv('Files', result.total.toString())}`)
|
|
387
|
+
console.log(` ${fmt.kv('Schema Migrated', c.success(result.migrated.toString()))}`)
|
|
388
|
+
if (options.embeddings) {
|
|
389
|
+
console.log(` ${fmt.kv('Embeddings Generated', c.success(result.embeddingsGenerated.toString()))}`)
|
|
390
|
+
}
|
|
391
|
+
console.log(` ${fmt.kv('Skipped', c.muted(result.skipped.toString()))}`)
|
|
392
|
+
if (result.errors.length > 0) {
|
|
393
|
+
console.log(` ${fmt.kv('Errors', c.error(result.errors.length.toString()))}`)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
console.log()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Summary
|
|
400
|
+
console.log(fmt.section('Summary'))
|
|
401
|
+
console.log()
|
|
402
|
+
console.log(` ${fmt.kv('Total Files', totals.total.toString())}`)
|
|
403
|
+
console.log(` ${fmt.kv('Schema Migrated', c.success(totals.migrated.toString()))}`)
|
|
404
|
+
if (options.embeddings) {
|
|
405
|
+
console.log(` ${fmt.kv('Embeddings Generated', c.success(totals.embeddingsGenerated.toString()))}`)
|
|
406
|
+
}
|
|
407
|
+
console.log(` ${fmt.kv('Already Current', c.muted(totals.skipped.toString()))}`)
|
|
408
|
+
|
|
409
|
+
if (totals.errors.length > 0) {
|
|
410
|
+
console.log(` ${fmt.kv('Errors', c.error(totals.errors.length.toString()))}`)
|
|
411
|
+
console.log()
|
|
412
|
+
for (const error of totals.errors) {
|
|
413
|
+
console.log(` ${c.error(symbols.cross)} ${error}`)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log()
|
|
418
|
+
|
|
419
|
+
if (options.dryRun && totals.migrated > 0) {
|
|
420
|
+
console.log(c.warn(` ${symbols.warning} This was a dry run. Run without --dry-run to apply changes.`))
|
|
421
|
+
console.log()
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
// SERVE COMMAND - Start the memory server
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { homedir } from 'os'
|
|
7
|
+
import { Glob } from 'bun'
|
|
5
8
|
import { c, symbols, fmt, box } from '../colors.ts'
|
|
6
9
|
import { createServer } from '../../server/index.ts'
|
|
10
|
+
import { MEMORY_SCHEMA_VERSION } from '../../types/schema.ts'
|
|
11
|
+
import { logger } from '../../utils/logger.ts'
|
|
7
12
|
|
|
8
13
|
interface ServeOptions {
|
|
9
14
|
port?: string
|
|
@@ -11,6 +16,74 @@ interface ServeOptions {
|
|
|
11
16
|
quiet?: boolean
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Quick check for v1 memories that need migration
|
|
21
|
+
* Samples a few files to avoid slow startup
|
|
22
|
+
*/
|
|
23
|
+
async function checkSchemaVersions(): Promise<{ v1Count: number; checked: number }> {
|
|
24
|
+
const storageMode = process.env.MEMORY_STORAGE_MODE ?? 'central'
|
|
25
|
+
const centralPath = process.env.MEMORY_CENTRAL_PATH ?? join(homedir(), '.local', 'share', 'memory')
|
|
26
|
+
|
|
27
|
+
let v1Count = 0
|
|
28
|
+
let checked = 0
|
|
29
|
+
const maxSamples = 20 // Check up to 20 files for speed
|
|
30
|
+
|
|
31
|
+
// Determine paths to check
|
|
32
|
+
const pathsToCheck: string[] = []
|
|
33
|
+
|
|
34
|
+
// Always check global
|
|
35
|
+
pathsToCheck.push(join(centralPath, 'global', 'memories'))
|
|
36
|
+
|
|
37
|
+
if (storageMode === 'local') {
|
|
38
|
+
// Check local project memories
|
|
39
|
+
pathsToCheck.push(join(process.cwd(), '.memory', 'memories'))
|
|
40
|
+
} else {
|
|
41
|
+
// Check central storage - find project directories
|
|
42
|
+
try {
|
|
43
|
+
const projectGlob = new Glob('*/memories')
|
|
44
|
+
for await (const match of projectGlob.scan({ cwd: centralPath, onlyFiles: false })) {
|
|
45
|
+
if (!match.startsWith('global/')) {
|
|
46
|
+
pathsToCheck.push(join(centralPath, match))
|
|
47
|
+
}
|
|
48
|
+
if (pathsToCheck.length >= 10) break // Limit project scans
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Directory doesn't exist yet
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check files in each path
|
|
56
|
+
for (const memoryPath of pathsToCheck) {
|
|
57
|
+
if (checked >= maxSamples) break
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const glob = new Glob('*.md')
|
|
61
|
+
for await (const file of glob.scan({ cwd: memoryPath })) {
|
|
62
|
+
if (checked >= maxSamples) break
|
|
63
|
+
|
|
64
|
+
const filePath = join(memoryPath, file)
|
|
65
|
+
const content = await Bun.file(filePath).text()
|
|
66
|
+
|
|
67
|
+
// Quick check for schema_version in frontmatter
|
|
68
|
+
const match = content.match(/^---\n[\s\S]*?\n---/)
|
|
69
|
+
if (match) {
|
|
70
|
+
const frontmatter = match[0]
|
|
71
|
+
const versionMatch = frontmatter.match(/schema_version:\s*(\d+)/)
|
|
72
|
+
|
|
73
|
+
if (!versionMatch || parseInt(versionMatch[1]) < MEMORY_SCHEMA_VERSION) {
|
|
74
|
+
v1Count++
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
checked++
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Directory doesn't exist or can't read
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { v1Count, checked }
|
|
85
|
+
}
|
|
86
|
+
|
|
14
87
|
export async function serve(options: ServeOptions) {
|
|
15
88
|
const port = parseInt(options.port || process.env.MEMORY_PORT || '8765')
|
|
16
89
|
const host = process.env.MEMORY_HOST || 'localhost'
|
|
@@ -19,6 +92,11 @@ export async function serve(options: ServeOptions) {
|
|
|
19
92
|
| 'local'
|
|
20
93
|
const apiKey = process.env.ANTHROPIC_API_KEY
|
|
21
94
|
|
|
95
|
+
// Set verbose mode for logger
|
|
96
|
+
if (options.verbose) {
|
|
97
|
+
logger.setVerbose(true)
|
|
98
|
+
}
|
|
99
|
+
|
|
22
100
|
if (!options.quiet) {
|
|
23
101
|
console.log()
|
|
24
102
|
console.log(c.header(`${symbols.brain} Memory Server`))
|
|
@@ -47,6 +125,16 @@ export async function serve(options: ServeOptions) {
|
|
|
47
125
|
embeddings.isReady ? c.success('loaded') : c.warn('not loaded')
|
|
48
126
|
)}`
|
|
49
127
|
)
|
|
128
|
+
|
|
129
|
+
// Check for v1 memories that need migration
|
|
130
|
+
const { v1Count, checked } = await checkSchemaVersions()
|
|
131
|
+
if (v1Count > 0) {
|
|
132
|
+
console.log()
|
|
133
|
+
console.log(c.warn(` ${symbols.warning} Found ${v1Count}/${checked} memories using old schema (v1)`))
|
|
134
|
+
console.log(c.muted(` Run 'memory migrate' to upgrade to v${MEMORY_SCHEMA_VERSION}`))
|
|
135
|
+
console.log(c.muted(` Use 'memory migrate --dry-run' to preview changes first`))
|
|
136
|
+
}
|
|
137
|
+
|
|
50
138
|
console.log()
|
|
51
139
|
console.log(c.muted(` Press Ctrl+C to stop`))
|
|
52
140
|
console.log()
|
package/src/cli/index.ts
CHANGED
|
@@ -23,6 +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('migrate')} Upgrade memories to latest schema version
|
|
26
27
|
${c.command('doctor')} Check system health
|
|
27
28
|
${c.command('help')} Show this help message
|
|
28
29
|
|
|
@@ -30,6 +31,8 @@ ${c.bold('Options:')}
|
|
|
30
31
|
${c.cyan('-p, --port')} <port> Server port ${c.muted('(default: 8765)')}
|
|
31
32
|
${c.cyan('-v, --verbose')} Verbose output
|
|
32
33
|
${c.cyan('-q, --quiet')} Minimal output
|
|
34
|
+
${c.cyan('--dry-run')} Preview changes without applying ${c.muted('(migrate)')}
|
|
35
|
+
${c.cyan('--embeddings')} Regenerate embeddings for memories ${c.muted('(migrate)')}
|
|
33
36
|
${c.cyan('--claude')} Install hooks for Claude Code
|
|
34
37
|
${c.cyan('--gemini')} Install hooks for Gemini CLI
|
|
35
38
|
${c.cyan('--version')} Show version
|
|
@@ -40,6 +43,9 @@ ${fmt.cmd('memory serve --port 9000')} ${c.muted('# Start on custom port')}
|
|
|
40
43
|
${fmt.cmd('memory stats')} ${c.muted('# Show memory statistics')}
|
|
41
44
|
${fmt.cmd('memory install')} ${c.muted('# Install Claude Code hooks (default)')}
|
|
42
45
|
${fmt.cmd('memory install --gemini')} ${c.muted('# Install Gemini CLI hooks')}
|
|
46
|
+
${fmt.cmd('memory migrate')} ${c.muted('# Upgrade memories to v2 schema')}
|
|
47
|
+
${fmt.cmd('memory migrate --dry-run')} ${c.muted('# Preview migration without changes')}
|
|
48
|
+
${fmt.cmd('memory migrate --embeddings')} ${c.muted('# Regenerate embeddings for all memories')}
|
|
43
49
|
|
|
44
50
|
${c.muted('Documentation: https://github.com/RLabs-Inc/memory')}
|
|
45
51
|
`)
|
|
@@ -67,6 +73,9 @@ async function main() {
|
|
|
67
73
|
force: { type: 'boolean', default: false },
|
|
68
74
|
claude: { type: 'boolean', default: false },
|
|
69
75
|
gemini: { type: 'boolean', default: false },
|
|
76
|
+
'dry-run': { type: 'boolean', default: false },
|
|
77
|
+
embeddings: { type: 'boolean', default: false }, // Regenerate embeddings in migrate
|
|
78
|
+
path: { type: 'string' }, // Custom path for migrate
|
|
70
79
|
},
|
|
71
80
|
allowPositionals: true,
|
|
72
81
|
strict: false, // Allow unknown options for subcommands
|
|
@@ -116,6 +125,18 @@ async function main() {
|
|
|
116
125
|
break
|
|
117
126
|
}
|
|
118
127
|
|
|
128
|
+
case 'migrate':
|
|
129
|
+
case 'upgrade': {
|
|
130
|
+
const { migrate } = await import('./commands/migrate.ts')
|
|
131
|
+
await migrate({
|
|
132
|
+
dryRun: values['dry-run'],
|
|
133
|
+
verbose: values.verbose,
|
|
134
|
+
path: values.path,
|
|
135
|
+
embeddings: values.embeddings,
|
|
136
|
+
})
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
case 'help':
|
|
120
141
|
showHelp()
|
|
121
142
|
break
|