@rlabs-inc/memory 0.3.11 → 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/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 +1 -1
- package/skills/memory-management.md +143 -154
- package/src/cli/commands/migrate.ts +561 -80
- package/src/cli/index.ts +10 -1
- package/src/core/curator.ts +38 -20
- package/src/core/engine.test.ts +6 -14
- package/src/core/retrieval.ts +1 -16
- package/src/core/store.ts +14 -32
- package/src/migrations/v3-schema.ts +392 -0
- package/src/types/memory.ts +84 -123
- package/src/types/schema.ts +12 -11
- package/src/utils/logger.ts +0 -1
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// MIGRATE COMMAND - Upgrade memory files to latest schema version
|
|
3
|
+
// Supports: v1→v3, v2→v3 migrations
|
|
3
4
|
// ============================================================================
|
|
4
5
|
|
|
5
6
|
import { join } from 'path'
|
|
6
7
|
import { homedir } from 'os'
|
|
7
8
|
import { Glob } from 'bun'
|
|
8
9
|
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
10
|
import { EmbeddingGenerator } from '../../core/embeddings.ts'
|
|
11
|
+
import {
|
|
12
|
+
V3_SCHEMA_VERSION,
|
|
13
|
+
CONTEXT_TYPE_MIGRATION_MAP,
|
|
14
|
+
V3_TYPE_DEFAULTS,
|
|
15
|
+
V3_DELETED_FIELDS,
|
|
16
|
+
CANONICAL_CONTEXT_TYPES,
|
|
17
|
+
migrateContextType,
|
|
18
|
+
isKnownContextType,
|
|
19
|
+
migrateTemporalRelevance,
|
|
20
|
+
type CanonicalContextType,
|
|
21
|
+
} from '../../migrations/v3-schema.ts'
|
|
22
|
+
|
|
23
|
+
// Custom mapping loaded from file
|
|
24
|
+
let customMapping: Record<string, string> | null = null
|
|
12
25
|
|
|
13
26
|
interface MigrateOptions {
|
|
14
27
|
dryRun?: boolean
|
|
15
28
|
verbose?: boolean
|
|
16
29
|
path?: string // Custom path to migrate
|
|
17
30
|
embeddings?: boolean // Regenerate embeddings for all memories
|
|
31
|
+
analyze?: boolean // Analyze fragmentation, show what would change
|
|
32
|
+
generateMapping?: string // Generate a mapping file for customization
|
|
33
|
+
mapping?: string // Path to custom mapping file
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
// Module-level embedder for reuse across files
|
|
@@ -35,7 +51,13 @@ interface MigrationResult {
|
|
|
35
51
|
migrated: number
|
|
36
52
|
skipped: number
|
|
37
53
|
embeddingsGenerated: number
|
|
54
|
+
contextTypesMigrated: number
|
|
55
|
+
fieldsDeleted: number
|
|
38
56
|
errors: string[]
|
|
57
|
+
// Tracking what changed
|
|
58
|
+
contextTypeChanges: Map<string, string> // old → new
|
|
59
|
+
unknownTypes: Map<string, number> // types not in our mapping
|
|
60
|
+
nullEmbeddings: number // count of null embeddings found
|
|
39
61
|
}
|
|
40
62
|
|
|
41
63
|
/**
|
|
@@ -116,83 +138,167 @@ function serializeFrontmatter(frontmatter: Record<string, any>, body: string): s
|
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
/**
|
|
119
|
-
*
|
|
141
|
+
* Get the migrated context type, using custom mapping if available
|
|
120
142
|
*/
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
function getMigratedContextType(oldType: string): CanonicalContextType {
|
|
144
|
+
// Check custom mapping first
|
|
145
|
+
if (customMapping && oldType in customMapping) {
|
|
146
|
+
const mapped = customMapping[oldType]
|
|
147
|
+
// Validate it's a canonical type
|
|
148
|
+
if (CANONICAL_CONTEXT_TYPES.includes(mapped as CanonicalContextType)) {
|
|
149
|
+
return mapped as CanonicalContextType
|
|
150
|
+
}
|
|
151
|
+
}
|
|
127
152
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
// Fall back to default migration
|
|
154
|
+
return migrateContextType(oldType) ?? 'technical'
|
|
155
|
+
}
|
|
131
156
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Apply v3 migration to frontmatter
|
|
159
|
+
* Handles both v1→v3 and v2→v3
|
|
160
|
+
*/
|
|
161
|
+
function applyV3Migration(frontmatter: Record<string, any>): {
|
|
162
|
+
migrated: Record<string, any>
|
|
163
|
+
contextTypeChanged: boolean
|
|
164
|
+
oldContextType: string
|
|
165
|
+
newContextType: string
|
|
166
|
+
deletedFieldsCount: number
|
|
167
|
+
isUnknownType: boolean
|
|
168
|
+
} {
|
|
169
|
+
const oldContextType = frontmatter.context_type ?? 'general'
|
|
170
|
+
const newContextType = getMigratedContextType(oldContextType)
|
|
171
|
+
const isUnknownType = !isKnownContextType(oldContextType) && !CANONICAL_CONTEXT_TYPES.includes(oldContextType as CanonicalContextType)
|
|
172
|
+
const contextTypeChanged = oldContextType !== newContextType
|
|
173
|
+
|
|
174
|
+
// Get type-specific defaults for the NEW context type
|
|
175
|
+
const typeDefaults = V3_TYPE_DEFAULTS[newContextType]
|
|
176
|
+
|
|
177
|
+
// Start with migrated frontmatter
|
|
178
|
+
const migrated: Record<string, any> = { ...frontmatter }
|
|
179
|
+
|
|
180
|
+
// 1. Migrate context_type to canonical
|
|
181
|
+
migrated.context_type = newContextType
|
|
182
|
+
|
|
183
|
+
// 2. Apply v3 defaults (also handles v1→v3 by filling in missing v2 fields)
|
|
184
|
+
migrated.status = migrated.status ?? 'active'
|
|
185
|
+
migrated.scope = migrated.scope ?? typeDefaults.scope
|
|
186
|
+
|
|
187
|
+
// Migrate temporal_relevance → temporal_class (if temporal_class not already set)
|
|
188
|
+
if (!migrated.temporal_class && migrated.temporal_relevance) {
|
|
189
|
+
const migratedTemporal = migrateTemporalRelevance(migrated.temporal_relevance)
|
|
190
|
+
if (migratedTemporal) {
|
|
191
|
+
migrated.temporal_class = migratedTemporal
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
migrated.temporal_class = migrated.temporal_class ?? typeDefaults.temporal_class
|
|
195
|
+
migrated.fade_rate = migrated.fade_rate ?? typeDefaults.fade_rate
|
|
196
|
+
migrated.sessions_since_surfaced = migrated.sessions_since_surfaced ?? 0
|
|
197
|
+
migrated.awaiting_implementation = migrated.awaiting_implementation ?? false
|
|
198
|
+
migrated.awaiting_decision = migrated.awaiting_decision ?? false
|
|
199
|
+
migrated.exclude_from_retrieval = migrated.exclude_from_retrieval ?? false
|
|
200
|
+
|
|
201
|
+
// Initialize arrays if missing
|
|
202
|
+
migrated.related_to = migrated.related_to ?? []
|
|
203
|
+
migrated.resolves = migrated.resolves ?? []
|
|
204
|
+
migrated.blocks = migrated.blocks ?? []
|
|
205
|
+
migrated.related_files = migrated.related_files ?? []
|
|
206
|
+
migrated.anti_triggers = migrated.anti_triggers ?? []
|
|
207
|
+
|
|
208
|
+
// 3. DELETE obsolete fields
|
|
209
|
+
let deletedFieldsCount = 0
|
|
210
|
+
for (const field of V3_DELETED_FIELDS) {
|
|
211
|
+
if (field in migrated) {
|
|
212
|
+
delete migrated[field]
|
|
213
|
+
deletedFieldsCount++
|
|
214
|
+
}
|
|
215
|
+
}
|
|
135
216
|
|
|
136
|
-
|
|
137
|
-
|
|
217
|
+
// 4. Mark as v3
|
|
218
|
+
migrated.schema_version = V3_SCHEMA_VERSION
|
|
138
219
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
awaiting_decision: frontmatter.awaiting_decision ?? false,
|
|
142
|
-
exclude_from_retrieval: frontmatter.exclude_from_retrieval ?? false,
|
|
220
|
+
return { migrated, contextTypeChanged, oldContextType, newContextType, deletedFieldsCount, isUnknownType }
|
|
221
|
+
}
|
|
143
222
|
|
|
144
|
-
|
|
145
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Check if file needs v3 migration
|
|
225
|
+
*/
|
|
226
|
+
function needsV3Migration(frontmatter: Record<string, any>): boolean {
|
|
227
|
+
// Needs migration if:
|
|
228
|
+
// 1. Schema version < 3
|
|
229
|
+
// 2. context_type is not canonical
|
|
230
|
+
// 3. Has deleted fields
|
|
231
|
+
if (!frontmatter.schema_version || frontmatter.schema_version < V3_SCHEMA_VERSION) {
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
146
234
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
related_files: frontmatter.related_files ?? [],
|
|
235
|
+
// Check if context_type needs migration even if already v3
|
|
236
|
+
const contextType = frontmatter.context_type ?? ''
|
|
237
|
+
if (!(contextType in V3_TYPE_DEFAULTS)) {
|
|
238
|
+
return true
|
|
239
|
+
}
|
|
153
240
|
|
|
154
|
-
|
|
155
|
-
|
|
241
|
+
// Check for deleted fields that shouldn't exist
|
|
242
|
+
for (const field of V3_DELETED_FIELDS) {
|
|
243
|
+
if (field in frontmatter) {
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
156
246
|
}
|
|
157
|
-
}
|
|
158
247
|
|
|
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
|
|
248
|
+
return false
|
|
164
249
|
}
|
|
165
250
|
|
|
166
251
|
/**
|
|
167
252
|
* Migrate a single memory file
|
|
168
253
|
*/
|
|
169
|
-
async function migrateFile(
|
|
254
|
+
async function migrateFile(
|
|
255
|
+
filePath: string,
|
|
256
|
+
options: MigrateOptions
|
|
257
|
+
): Promise<{
|
|
258
|
+
migrated: boolean
|
|
259
|
+
embeddingGenerated: boolean
|
|
260
|
+
contextTypeChanged: boolean
|
|
261
|
+
oldContextType?: string
|
|
262
|
+
newContextType?: string
|
|
263
|
+
deletedFieldsCount: number
|
|
264
|
+
isUnknownType: boolean
|
|
265
|
+
hasNullEmbedding: boolean
|
|
266
|
+
error?: string
|
|
267
|
+
}> {
|
|
170
268
|
try {
|
|
171
269
|
const file = Bun.file(filePath)
|
|
172
270
|
const content = await file.text()
|
|
173
271
|
const parsed = parseFrontmatter(content)
|
|
174
272
|
|
|
175
273
|
if (!parsed) {
|
|
176
|
-
return { migrated: false, embeddingGenerated: false, error: 'Could not parse frontmatter' }
|
|
274
|
+
return { migrated: false, embeddingGenerated: false, contextTypeChanged: false, deletedFieldsCount: 0, isUnknownType: false, hasNullEmbedding: false, error: 'Could not parse frontmatter' }
|
|
177
275
|
}
|
|
178
276
|
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
parsed.frontmatter.embedding === undefined ||
|
|
183
|
-
(Array.isArray(parsed.frontmatter.embedding) && parsed.frontmatter.embedding.length === 0)
|
|
184
|
-
)
|
|
277
|
+
const hasNullEmbedding = !parsed.frontmatter.embedding || parsed.frontmatter.embedding === null
|
|
278
|
+
const needsSchema = needsV3Migration(parsed.frontmatter)
|
|
279
|
+
const needsEmbedding = options.embeddings && hasNullEmbedding
|
|
185
280
|
|
|
186
281
|
// Nothing to do
|
|
187
282
|
if (!needsSchema && !needsEmbedding) {
|
|
188
|
-
return { migrated: false, embeddingGenerated: false }
|
|
283
|
+
return { migrated: false, embeddingGenerated: false, contextTypeChanged: false, deletedFieldsCount: 0, isUnknownType: false, hasNullEmbedding }
|
|
189
284
|
}
|
|
190
285
|
|
|
191
286
|
let newFrontmatter = parsed.frontmatter
|
|
287
|
+
let contextTypeChanged = false
|
|
288
|
+
let oldContextType: string | undefined
|
|
289
|
+
let newContextType: string | undefined
|
|
290
|
+
let deletedFieldsCount = 0
|
|
291
|
+
let isUnknownType = false
|
|
192
292
|
|
|
193
293
|
// Apply schema migration if needed
|
|
194
294
|
if (needsSchema) {
|
|
195
|
-
|
|
295
|
+
const result = applyV3Migration(parsed.frontmatter)
|
|
296
|
+
newFrontmatter = result.migrated
|
|
297
|
+
contextTypeChanged = result.contextTypeChanged
|
|
298
|
+
oldContextType = result.oldContextType
|
|
299
|
+
newContextType = result.newContextType
|
|
300
|
+
deletedFieldsCount = result.deletedFieldsCount
|
|
301
|
+
isUnknownType = result.isUnknownType
|
|
196
302
|
}
|
|
197
303
|
|
|
198
304
|
// Generate embedding if needed
|
|
@@ -210,22 +316,28 @@ async function migrateFile(filePath: string, options: MigrateOptions): Promise<{
|
|
|
210
316
|
await Bun.write(filePath, newContent)
|
|
211
317
|
}
|
|
212
318
|
|
|
213
|
-
return {
|
|
319
|
+
return {
|
|
320
|
+
migrated: needsSchema,
|
|
321
|
+
embeddingGenerated,
|
|
322
|
+
contextTypeChanged,
|
|
323
|
+
oldContextType,
|
|
324
|
+
newContextType,
|
|
325
|
+
deletedFieldsCount,
|
|
326
|
+
isUnknownType,
|
|
327
|
+
hasNullEmbedding
|
|
328
|
+
}
|
|
214
329
|
} catch (error: any) {
|
|
215
|
-
return { migrated: false, embeddingGenerated: false, error: error.message }
|
|
330
|
+
return { migrated: false, embeddingGenerated: false, contextTypeChanged: false, deletedFieldsCount: 0, isUnknownType: false, hasNullEmbedding: false, error: error.message }
|
|
216
331
|
}
|
|
217
332
|
}
|
|
218
333
|
|
|
219
334
|
/**
|
|
220
335
|
* 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
336
|
*/
|
|
224
337
|
async function findMemoryPaths(customPath?: string): Promise<{ path: string; label: string }[]> {
|
|
225
338
|
const paths: { path: string; label: string }[] = []
|
|
226
339
|
|
|
227
340
|
if (customPath) {
|
|
228
|
-
// Custom path specified - just use it
|
|
229
341
|
paths.push({ path: customPath, label: customPath })
|
|
230
342
|
return paths
|
|
231
343
|
}
|
|
@@ -233,7 +345,7 @@ async function findMemoryPaths(customPath?: string): Promise<{ path: string; lab
|
|
|
233
345
|
const storageMode = process.env.MEMORY_STORAGE_MODE ?? 'central'
|
|
234
346
|
const centralPath = process.env.MEMORY_CENTRAL_PATH ?? join(homedir(), '.local', 'share', 'memory')
|
|
235
347
|
|
|
236
|
-
// ALWAYS check global memories
|
|
348
|
+
// ALWAYS check global memories
|
|
237
349
|
const globalMemoriesPath = join(centralPath, 'global', 'memories')
|
|
238
350
|
try {
|
|
239
351
|
const globalGlob = new Glob('*.md')
|
|
@@ -250,7 +362,6 @@ async function findMemoryPaths(customPath?: string): Promise<{ path: string; lab
|
|
|
250
362
|
}
|
|
251
363
|
|
|
252
364
|
if (storageMode === 'local') {
|
|
253
|
-
// Local mode: check current directory for project memories
|
|
254
365
|
const localFolder = '.memory'
|
|
255
366
|
const cwd = process.cwd()
|
|
256
367
|
const localMemoriesPath = join(cwd, localFolder, 'memories')
|
|
@@ -269,17 +380,15 @@ async function findMemoryPaths(customPath?: string): Promise<{ path: string; lab
|
|
|
269
380
|
// Local directory doesn't exist
|
|
270
381
|
}
|
|
271
382
|
} else {
|
|
272
|
-
// Central mode: check
|
|
383
|
+
// Central mode: check all project directories
|
|
273
384
|
try {
|
|
274
385
|
const projectGlob = new Glob('*/memories')
|
|
275
386
|
for await (const match of projectGlob.scan({ cwd: centralPath, onlyFiles: false })) {
|
|
276
|
-
// Skip global, we already handled it
|
|
277
387
|
if (match.startsWith('global/')) continue
|
|
278
388
|
|
|
279
389
|
const fullPath = join(centralPath, match)
|
|
280
390
|
const projectId = match.split('/')[0]
|
|
281
391
|
|
|
282
|
-
// Check if directory has any .md files
|
|
283
392
|
try {
|
|
284
393
|
const mdGlob = new Glob('*.md')
|
|
285
394
|
let hasFiles = false
|
|
@@ -303,10 +412,21 @@ async function findMemoryPaths(customPath?: string): Promise<{ path: string; lab
|
|
|
303
412
|
}
|
|
304
413
|
|
|
305
414
|
/**
|
|
306
|
-
* Migrate all memory files in a directory
|
|
415
|
+
* Migrate all memory files in a directory
|
|
307
416
|
*/
|
|
308
417
|
async function migrateDirectory(dir: string, options: MigrateOptions): Promise<MigrationResult> {
|
|
309
|
-
const result: MigrationResult = {
|
|
418
|
+
const result: MigrationResult = {
|
|
419
|
+
total: 0,
|
|
420
|
+
migrated: 0,
|
|
421
|
+
skipped: 0,
|
|
422
|
+
embeddingsGenerated: 0,
|
|
423
|
+
contextTypesMigrated: 0,
|
|
424
|
+
fieldsDeleted: 0,
|
|
425
|
+
errors: [],
|
|
426
|
+
contextTypeChanges: new Map(),
|
|
427
|
+
unknownTypes: new Map(),
|
|
428
|
+
nullEmbeddings: 0
|
|
429
|
+
}
|
|
310
430
|
|
|
311
431
|
try {
|
|
312
432
|
const glob = new Glob('*.md')
|
|
@@ -315,20 +435,45 @@ async function migrateDirectory(dir: string, options: MigrateOptions): Promise<M
|
|
|
315
435
|
result.total++
|
|
316
436
|
const filePath = join(dir, file)
|
|
317
437
|
|
|
318
|
-
const
|
|
438
|
+
const migrationResult = await migrateFile(filePath, options)
|
|
319
439
|
|
|
320
|
-
|
|
321
|
-
|
|
440
|
+
// Track null embeddings
|
|
441
|
+
if (migrationResult.hasNullEmbedding) {
|
|
442
|
+
result.nullEmbeddings++
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (migrationResult.error) {
|
|
446
|
+
result.errors.push(`${file}: ${migrationResult.error}`)
|
|
322
447
|
if (options.verbose) {
|
|
323
|
-
console.log(` ${c.error(symbols.cross)} ${file}: ${error}`)
|
|
448
|
+
console.log(` ${c.error(symbols.cross)} ${file}: ${migrationResult.error}`)
|
|
324
449
|
}
|
|
325
|
-
} else if (migrated || embeddingGenerated) {
|
|
326
|
-
if (migrated) result.migrated++
|
|
327
|
-
if (embeddingGenerated) result.embeddingsGenerated++
|
|
450
|
+
} else if (migrationResult.migrated || migrationResult.embeddingGenerated) {
|
|
451
|
+
if (migrationResult.migrated) result.migrated++
|
|
452
|
+
if (migrationResult.embeddingGenerated) result.embeddingsGenerated++
|
|
453
|
+
if (migrationResult.contextTypeChanged) {
|
|
454
|
+
result.contextTypesMigrated++
|
|
455
|
+
// Track the change
|
|
456
|
+
const key = `${migrationResult.oldContextType} → ${migrationResult.newContextType}`
|
|
457
|
+
result.contextTypeChanges.set(key, (result.contextTypeChanges.get(key) ?? '') + '.')
|
|
458
|
+
|
|
459
|
+
// Track unknown types
|
|
460
|
+
if (migrationResult.isUnknownType && migrationResult.oldContextType) {
|
|
461
|
+
result.unknownTypes.set(
|
|
462
|
+
migrationResult.oldContextType,
|
|
463
|
+
(result.unknownTypes.get(migrationResult.oldContextType) ?? 0) + 1
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
result.fieldsDeleted += migrationResult.deletedFieldsCount
|
|
468
|
+
|
|
328
469
|
if (options.verbose) {
|
|
329
470
|
const actions = []
|
|
330
|
-
if (migrated) actions.push('
|
|
331
|
-
if (embeddingGenerated) actions.push('embedding')
|
|
471
|
+
if (migrationResult.migrated) actions.push('v3')
|
|
472
|
+
if (migrationResult.embeddingGenerated) actions.push('embedding')
|
|
473
|
+
if (migrationResult.contextTypeChanged) {
|
|
474
|
+
const prefix = migrationResult.isUnknownType ? '⚠️' : ''
|
|
475
|
+
actions.push(`${prefix}${migrationResult.oldContextType}→${migrationResult.newContextType}`)
|
|
476
|
+
}
|
|
332
477
|
console.log(` ${c.success(symbols.tick)} ${file} (${actions.join(', ')})`)
|
|
333
478
|
}
|
|
334
479
|
} else {
|
|
@@ -345,14 +490,268 @@ async function migrateDirectory(dir: string, options: MigrateOptions): Promise<M
|
|
|
345
490
|
return result
|
|
346
491
|
}
|
|
347
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Analyze all memories and report on fragmentation
|
|
495
|
+
*/
|
|
496
|
+
async function analyzeMemories(memoryPaths: { path: string; label: string }[]): Promise<{
|
|
497
|
+
contextTypes: Map<string, number>
|
|
498
|
+
nullEmbeddings: number
|
|
499
|
+
totalMemories: number
|
|
500
|
+
deletedFieldsFound: Map<string, number>
|
|
501
|
+
}> {
|
|
502
|
+
const contextTypes = new Map<string, number>()
|
|
503
|
+
const deletedFieldsFound = new Map<string, number>()
|
|
504
|
+
let nullEmbeddings = 0
|
|
505
|
+
let totalMemories = 0
|
|
506
|
+
|
|
507
|
+
for (const { path: dir } of memoryPaths) {
|
|
508
|
+
try {
|
|
509
|
+
const glob = new Glob('*.md')
|
|
510
|
+
for await (const file of glob.scan({ cwd: dir })) {
|
|
511
|
+
totalMemories++
|
|
512
|
+
const filePath = join(dir, file)
|
|
513
|
+
const content = await Bun.file(filePath).text()
|
|
514
|
+
const parsed = parseFrontmatter(content)
|
|
515
|
+
|
|
516
|
+
if (!parsed) continue
|
|
517
|
+
|
|
518
|
+
// Count context types
|
|
519
|
+
const ct = parsed.frontmatter.context_type ?? 'unknown'
|
|
520
|
+
contextTypes.set(ct, (contextTypes.get(ct) ?? 0) + 1)
|
|
521
|
+
|
|
522
|
+
// Count null embeddings
|
|
523
|
+
if (!parsed.frontmatter.embedding || parsed.frontmatter.embedding === null) {
|
|
524
|
+
nullEmbeddings++
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Count deleted fields that still exist
|
|
528
|
+
for (const field of V3_DELETED_FIELDS) {
|
|
529
|
+
if (field in parsed.frontmatter) {
|
|
530
|
+
deletedFieldsFound.set(field, (deletedFieldsFound.get(field) ?? 0) + 1)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
// Skip directories we can't read
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { contextTypes, nullEmbeddings, totalMemories, deletedFieldsFound }
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Show analysis of what migration would do
|
|
544
|
+
*/
|
|
545
|
+
async function showAnalysis(options: MigrateOptions) {
|
|
546
|
+
console.log()
|
|
547
|
+
console.log(c.header(`${symbols.search} Memory Migration Analysis`))
|
|
548
|
+
console.log()
|
|
549
|
+
|
|
550
|
+
const memoryPaths = await findMemoryPaths(options.path)
|
|
551
|
+
|
|
552
|
+
if (memoryPaths.length === 0) {
|
|
553
|
+
console.log(c.warn(` ${symbols.warning} No memory directories found`))
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
console.log(c.muted(' Scanning memories...'))
|
|
558
|
+
console.log()
|
|
559
|
+
|
|
560
|
+
const { contextTypes, nullEmbeddings, totalMemories, deletedFieldsFound } = await analyzeMemories(memoryPaths)
|
|
561
|
+
|
|
562
|
+
console.log(fmt.section('Overview'))
|
|
563
|
+
console.log(` ${fmt.kv('Total Memories', totalMemories.toString())}`)
|
|
564
|
+
console.log(` ${fmt.kv('Unique context_types', contextTypes.size.toString())}`)
|
|
565
|
+
console.log(` ${fmt.kv('Null Embeddings', nullEmbeddings > 0 ? c.warn(nullEmbeddings.toString()) : c.success('0'))}`)
|
|
566
|
+
console.log()
|
|
567
|
+
|
|
568
|
+
// Show context_type breakdown
|
|
569
|
+
console.log(fmt.section('Context Type Analysis'))
|
|
570
|
+
console.log()
|
|
571
|
+
|
|
572
|
+
const sorted = [...contextTypes.entries()].sort((a, b) => b[1] - a[1])
|
|
573
|
+
|
|
574
|
+
// Categorize types
|
|
575
|
+
const canonical: [string, number][] = []
|
|
576
|
+
const willMigrate: [string, number, string][] = []
|
|
577
|
+
const unknown: [string, number, string][] = []
|
|
578
|
+
|
|
579
|
+
for (const [type, count] of sorted) {
|
|
580
|
+
if (CANONICAL_CONTEXT_TYPES.includes(type as CanonicalContextType)) {
|
|
581
|
+
canonical.push([type, count])
|
|
582
|
+
} else {
|
|
583
|
+
const migrated = migrateContextType(type)
|
|
584
|
+
if (isKnownContextType(type)) {
|
|
585
|
+
willMigrate.push([type, count, migrated!])
|
|
586
|
+
} else {
|
|
587
|
+
unknown.push([type, count, migrated!])
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Show canonical (already good)
|
|
593
|
+
if (canonical.length > 0) {
|
|
594
|
+
console.log(c.success(' Already Canonical (no change needed):'))
|
|
595
|
+
for (const [type, count] of canonical) {
|
|
596
|
+
console.log(c.muted(` ${type}: ${count}`))
|
|
597
|
+
}
|
|
598
|
+
console.log()
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Show known migrations
|
|
602
|
+
if (willMigrate.length > 0) {
|
|
603
|
+
console.log(c.warn(' Known Fragmented Types (will be migrated):'))
|
|
604
|
+
for (const [oldType, count, newType] of willMigrate.slice(0, 30)) {
|
|
605
|
+
console.log(` ${c.muted(oldType)} → ${c.success(newType)} (${count})`)
|
|
606
|
+
}
|
|
607
|
+
if (willMigrate.length > 30) {
|
|
608
|
+
console.log(c.muted(` ... and ${willMigrate.length - 30} more`))
|
|
609
|
+
}
|
|
610
|
+
console.log()
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Show unknown (will use fuzzy matching or fallback)
|
|
614
|
+
if (unknown.length > 0) {
|
|
615
|
+
console.log(c.error(' Unknown Types (will use fuzzy matching → fallback):'))
|
|
616
|
+
for (const [oldType, count, newType] of unknown.slice(0, 20)) {
|
|
617
|
+
console.log(` ${c.error(oldType)} → ${c.warn(newType)} (${count})`)
|
|
618
|
+
}
|
|
619
|
+
if (unknown.length > 20) {
|
|
620
|
+
console.log(c.muted(` ... and ${unknown.length - 20} more`))
|
|
621
|
+
}
|
|
622
|
+
console.log()
|
|
623
|
+
console.log(c.muted(' To customize unknown type mappings:'))
|
|
624
|
+
console.log(c.muted(' 1. Run: memory migrate --generate-mapping mapping.json'))
|
|
625
|
+
console.log(c.muted(' 2. Edit mapping.json with your preferred mappings'))
|
|
626
|
+
console.log(c.muted(' 3. Run: memory migrate --mapping mapping.json'))
|
|
627
|
+
console.log()
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Show deleted fields
|
|
631
|
+
if (deletedFieldsFound.size > 0) {
|
|
632
|
+
console.log(fmt.section('Fields to be Removed'))
|
|
633
|
+
for (const [field, count] of deletedFieldsFound.entries()) {
|
|
634
|
+
console.log(` ${c.muted(field)}: ${count} memories`)
|
|
635
|
+
}
|
|
636
|
+
console.log()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Summary
|
|
640
|
+
console.log(fmt.section('Migration Summary'))
|
|
641
|
+
console.log(` ${fmt.kv('Types already canonical', c.success(canonical.length.toString()))}`)
|
|
642
|
+
console.log(` ${fmt.kv('Types to migrate (known)', c.warn(willMigrate.length.toString()))}`)
|
|
643
|
+
console.log(` ${fmt.kv('Types to migrate (unknown)', unknown.length > 0 ? c.error(unknown.length.toString()) : '0')}`)
|
|
644
|
+
console.log(` ${fmt.kv('Fields to remove', deletedFieldsFound.size.toString())}`)
|
|
645
|
+
console.log()
|
|
646
|
+
|
|
647
|
+
if (willMigrate.length > 0 || unknown.length > 0) {
|
|
648
|
+
console.log(c.muted(' To run migration:'))
|
|
649
|
+
console.log(c.muted(' memory migrate --dry-run # Preview changes'))
|
|
650
|
+
console.log(c.muted(' memory migrate # Apply changes'))
|
|
651
|
+
} else {
|
|
652
|
+
console.log(c.success(' All context_types are already canonical!'))
|
|
653
|
+
}
|
|
654
|
+
console.log()
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Generate a mapping file for customization
|
|
659
|
+
*/
|
|
660
|
+
async function generateMappingFile(outputPath: string, options: MigrateOptions) {
|
|
661
|
+
console.log()
|
|
662
|
+
console.log(c.header(`${symbols.gear} Generate Custom Mapping File`))
|
|
663
|
+
console.log()
|
|
664
|
+
|
|
665
|
+
const memoryPaths = await findMemoryPaths(options.path)
|
|
666
|
+
|
|
667
|
+
if (memoryPaths.length === 0) {
|
|
668
|
+
console.log(c.warn(` ${symbols.warning} No memory directories found`))
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.log(c.muted(' Scanning memories...'))
|
|
673
|
+
|
|
674
|
+
const { contextTypes } = await analyzeMemories(memoryPaths)
|
|
675
|
+
|
|
676
|
+
// Build mapping with our defaults, user can customize
|
|
677
|
+
const mapping: Record<string, string> = {}
|
|
678
|
+
|
|
679
|
+
for (const [type] of contextTypes.entries()) {
|
|
680
|
+
if (CANONICAL_CONTEXT_TYPES.includes(type as CanonicalContextType)) {
|
|
681
|
+
// Already canonical, map to itself
|
|
682
|
+
mapping[type] = type
|
|
683
|
+
} else {
|
|
684
|
+
// Use our migration logic
|
|
685
|
+
const migrated = migrateContextType(type) ?? 'technical'
|
|
686
|
+
mapping[type] = migrated
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const output = {
|
|
691
|
+
_comment: 'Edit the values to customize how context_types are migrated',
|
|
692
|
+
_canonical_types: CANONICAL_CONTEXT_TYPES,
|
|
693
|
+
_generated: new Date().toISOString(),
|
|
694
|
+
mapping
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
await Bun.write(outputPath, JSON.stringify(output, null, 2))
|
|
698
|
+
|
|
699
|
+
console.log()
|
|
700
|
+
console.log(c.success(` ${symbols.tick} Generated: ${outputPath}`))
|
|
701
|
+
console.log()
|
|
702
|
+
console.log(c.muted(' Edit the file to customize mappings, then run:'))
|
|
703
|
+
console.log(c.muted(` memory migrate --mapping ${outputPath} --dry-run`))
|
|
704
|
+
console.log(c.muted(` memory migrate --mapping ${outputPath}`))
|
|
705
|
+
console.log()
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Load custom mapping from file
|
|
710
|
+
*/
|
|
711
|
+
async function loadCustomMapping(mappingPath: string): Promise<Record<string, string>> {
|
|
712
|
+
try {
|
|
713
|
+
const content = await Bun.file(mappingPath).text()
|
|
714
|
+
const parsed = JSON.parse(content)
|
|
715
|
+
return parsed.mapping ?? parsed
|
|
716
|
+
} catch (error: any) {
|
|
717
|
+
throw new Error(`Could not load mapping file: ${error.message}`)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
348
721
|
export async function migrate(options: MigrateOptions) {
|
|
722
|
+
// Handle analyze mode
|
|
723
|
+
if (options.analyze) {
|
|
724
|
+
return showAnalysis(options)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Handle generate-mapping mode
|
|
728
|
+
if (options.generateMapping) {
|
|
729
|
+
return generateMappingFile(options.generateMapping, options)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Load custom mapping if provided
|
|
733
|
+
if (options.mapping) {
|
|
734
|
+
try {
|
|
735
|
+
customMapping = await loadCustomMapping(options.mapping)
|
|
736
|
+
console.log(c.success(` ${symbols.tick} Loaded custom mapping from ${options.mapping}`))
|
|
737
|
+
} catch (error: any) {
|
|
738
|
+
console.log(c.error(` ${symbols.cross} ${error.message}`))
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
}
|
|
349
742
|
console.log()
|
|
350
|
-
console.log(c.header(`${symbols.gear} Memory Migration`))
|
|
743
|
+
console.log(c.header(`${symbols.gear} Memory Migration to v${V3_SCHEMA_VERSION}`))
|
|
351
744
|
console.log()
|
|
352
|
-
console.log(` ${fmt.kv('Target Version', `v${
|
|
745
|
+
console.log(` ${fmt.kv('Target Version', `v${V3_SCHEMA_VERSION}`)}`)
|
|
353
746
|
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('
|
|
747
|
+
console.log(` ${fmt.kv('Embeddings', options.embeddings ? c.success('Regenerate null') : c.muted('Skip'))}`)
|
|
748
|
+
console.log(` ${fmt.kv('Mode', options.dryRun ? c.warn('DRY RUN') : c.success('LIVE'))}`)
|
|
749
|
+
console.log()
|
|
750
|
+
|
|
751
|
+
console.log(c.muted(' v3 Changes:'))
|
|
752
|
+
console.log(c.muted(' • Consolidates 170+ context_types → 11 canonical'))
|
|
753
|
+
console.log(c.muted(' • Removes 10 unused fields'))
|
|
754
|
+
console.log(c.muted(' • Applies type-specific defaults'))
|
|
356
755
|
console.log()
|
|
357
756
|
|
|
358
757
|
const memoryPaths = await findMemoryPaths(options.path)
|
|
@@ -367,7 +766,18 @@ export async function migrate(options: MigrateOptions) {
|
|
|
367
766
|
return
|
|
368
767
|
}
|
|
369
768
|
|
|
370
|
-
const totals: MigrationResult = {
|
|
769
|
+
const totals: MigrationResult = {
|
|
770
|
+
total: 0,
|
|
771
|
+
migrated: 0,
|
|
772
|
+
skipped: 0,
|
|
773
|
+
embeddingsGenerated: 0,
|
|
774
|
+
contextTypesMigrated: 0,
|
|
775
|
+
fieldsDeleted: 0,
|
|
776
|
+
errors: [],
|
|
777
|
+
contextTypeChanges: new Map(),
|
|
778
|
+
unknownTypes: new Map(),
|
|
779
|
+
nullEmbeddings: 0
|
|
780
|
+
}
|
|
371
781
|
|
|
372
782
|
for (const { path, label } of memoryPaths) {
|
|
373
783
|
console.log(fmt.section(label))
|
|
@@ -380,15 +790,37 @@ export async function migrate(options: MigrateOptions) {
|
|
|
380
790
|
totals.migrated += result.migrated
|
|
381
791
|
totals.skipped += result.skipped
|
|
382
792
|
totals.embeddingsGenerated += result.embeddingsGenerated
|
|
793
|
+
totals.contextTypesMigrated += result.contextTypesMigrated
|
|
794
|
+
totals.fieldsDeleted += result.fieldsDeleted
|
|
383
795
|
totals.errors.push(...result.errors)
|
|
384
796
|
|
|
797
|
+
// Merge context type changes
|
|
798
|
+
for (const [key, value] of result.contextTypeChanges) {
|
|
799
|
+
const existing = totals.contextTypeChanges.get(key) ?? ''
|
|
800
|
+
totals.contextTypeChanges.set(key, existing + value)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Merge unknown types
|
|
804
|
+
for (const [key, value] of result.unknownTypes) {
|
|
805
|
+
totals.unknownTypes.set(key, (totals.unknownTypes.get(key) ?? 0) + value)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Track null embeddings
|
|
809
|
+
totals.nullEmbeddings += result.nullEmbeddings
|
|
810
|
+
|
|
385
811
|
if (!options.verbose) {
|
|
386
812
|
console.log(` ${fmt.kv('Files', result.total.toString())}`)
|
|
387
|
-
console.log(` ${fmt.kv('
|
|
813
|
+
console.log(` ${fmt.kv('Migrated to v3', c.success(result.migrated.toString()))}`)
|
|
814
|
+
if (result.contextTypesMigrated > 0) {
|
|
815
|
+
console.log(` ${fmt.kv('Context Types Fixed', c.success(result.contextTypesMigrated.toString()))}`)
|
|
816
|
+
}
|
|
817
|
+
if (result.fieldsDeleted > 0) {
|
|
818
|
+
console.log(` ${fmt.kv('Dead Fields Removed', c.success(result.fieldsDeleted.toString()))}`)
|
|
819
|
+
}
|
|
388
820
|
if (options.embeddings) {
|
|
389
821
|
console.log(` ${fmt.kv('Embeddings Generated', c.success(result.embeddingsGenerated.toString()))}`)
|
|
390
822
|
}
|
|
391
|
-
console.log(` ${fmt.kv('
|
|
823
|
+
console.log(` ${fmt.kv('Already v3', c.muted(result.skipped.toString()))}`)
|
|
392
824
|
if (result.errors.length > 0) {
|
|
393
825
|
console.log(` ${fmt.kv('Errors', c.error(result.errors.length.toString()))}`)
|
|
394
826
|
}
|
|
@@ -400,24 +832,73 @@ export async function migrate(options: MigrateOptions) {
|
|
|
400
832
|
console.log(fmt.section('Summary'))
|
|
401
833
|
console.log()
|
|
402
834
|
console.log(` ${fmt.kv('Total Files', totals.total.toString())}`)
|
|
403
|
-
console.log(` ${fmt.kv('
|
|
835
|
+
console.log(` ${fmt.kv('Migrated to v3', c.success(totals.migrated.toString()))}`)
|
|
836
|
+
console.log(` ${fmt.kv('Context Types Fixed', c.success(totals.contextTypesMigrated.toString()))}`)
|
|
837
|
+
console.log(` ${fmt.kv('Dead Fields Removed', c.success(totals.fieldsDeleted.toString()))}`)
|
|
404
838
|
if (options.embeddings) {
|
|
405
839
|
console.log(` ${fmt.kv('Embeddings Generated', c.success(totals.embeddingsGenerated.toString()))}`)
|
|
406
840
|
}
|
|
407
|
-
console.log(` ${fmt.kv('Already
|
|
841
|
+
console.log(` ${fmt.kv('Already v3', c.muted(totals.skipped.toString()))}`)
|
|
842
|
+
|
|
843
|
+
// Show unknown types warning
|
|
844
|
+
if (totals.unknownTypes.size > 0) {
|
|
845
|
+
console.log()
|
|
846
|
+
console.log(c.warn(' Unknown Types (used fuzzy matching):'))
|
|
847
|
+
const sorted = [...totals.unknownTypes.entries()].sort((a, b) => b[1] - a[1])
|
|
848
|
+
for (const [type, count] of sorted.slice(0, 10)) {
|
|
849
|
+
console.log(` ${c.warn(type)}: ${count}`)
|
|
850
|
+
}
|
|
851
|
+
if (sorted.length > 10) {
|
|
852
|
+
console.log(c.muted(` ... and ${sorted.length - 10} more`))
|
|
853
|
+
}
|
|
854
|
+
console.log()
|
|
855
|
+
console.log(c.muted(' To customize these mappings:'))
|
|
856
|
+
console.log(c.muted(' memory migrate --generate-mapping mapping.json'))
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Show context type migration breakdown
|
|
860
|
+
if (totals.contextTypeChanges.size > 0) {
|
|
861
|
+
console.log()
|
|
862
|
+
console.log(c.muted(' Context Type Migrations:'))
|
|
863
|
+
const sorted = [...totals.contextTypeChanges.entries()]
|
|
864
|
+
.map(([key, dots]) => ({ key, count: dots.length }))
|
|
865
|
+
.sort((a, b) => b.count - a.count)
|
|
866
|
+
.slice(0, 15)
|
|
867
|
+
|
|
868
|
+
for (const { key, count } of sorted) {
|
|
869
|
+
console.log(c.muted(` ${key}: ${count}`))
|
|
870
|
+
}
|
|
871
|
+
if (totals.contextTypeChanges.size > 15) {
|
|
872
|
+
console.log(c.muted(` ... and ${totals.contextTypeChanges.size - 15} more`))
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Show null embeddings warning
|
|
877
|
+
if (totals.nullEmbeddings > 0 && !options.embeddings) {
|
|
878
|
+
console.log()
|
|
879
|
+
console.log(c.warn(` ${symbols.warning} ${totals.nullEmbeddings} memories have null embeddings`))
|
|
880
|
+
console.log(c.muted(' Run with --embeddings to regenerate them'))
|
|
881
|
+
}
|
|
408
882
|
|
|
409
883
|
if (totals.errors.length > 0) {
|
|
410
|
-
console.log(` ${fmt.kv('Errors', c.error(totals.errors.length.toString()))}`)
|
|
411
884
|
console.log()
|
|
412
|
-
|
|
885
|
+
console.log(` ${fmt.kv('Errors', c.error(totals.errors.length.toString()))}`)
|
|
886
|
+
for (const error of totals.errors.slice(0, 10)) {
|
|
413
887
|
console.log(` ${c.error(symbols.cross)} ${error}`)
|
|
414
888
|
}
|
|
889
|
+
if (totals.errors.length > 10) {
|
|
890
|
+
console.log(` ... and ${totals.errors.length - 10} more errors`)
|
|
891
|
+
}
|
|
415
892
|
}
|
|
416
893
|
|
|
417
894
|
console.log()
|
|
418
895
|
|
|
419
896
|
if (options.dryRun && totals.migrated > 0) {
|
|
420
|
-
console.log(c.warn(` ${symbols.warning} This was a
|
|
897
|
+
console.log(c.warn(` ${symbols.warning} This was a DRY RUN. No files were modified.`))
|
|
898
|
+
console.log(c.muted(` Run without --dry-run to apply changes.`))
|
|
899
|
+
console.log()
|
|
900
|
+
} else if (totals.migrated > 0) {
|
|
901
|
+
console.log(c.success(` ${symbols.tick} Migration complete!`))
|
|
421
902
|
console.log()
|
|
422
903
|
}
|
|
423
904
|
}
|