@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.
@@ -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