@prmichaelsen/remember-mcp 3.0.0 → 3.13.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.
Files changed (208) hide show
  1. package/AGENT.md +296 -250
  2. package/CHANGELOG.md +358 -0
  3. package/README.md +68 -45
  4. package/agent/commands/acp.clarification-create.md +382 -0
  5. package/agent/commands/acp.project-info.md +309 -0
  6. package/agent/commands/acp.project-remove.md +379 -0
  7. package/agent/commands/acp.project-update.md +296 -0
  8. package/agent/commands/acp.task-create.md +17 -9
  9. package/agent/commands/git.commit.md +13 -1
  10. package/agent/design/comment-memory-type.md +2 -2
  11. package/agent/design/local.collaborative-memory-sync.md +265 -0
  12. package/agent/design/local.content-flags.md +210 -0
  13. package/agent/design/local.ghost-persona-system.md +273 -0
  14. package/agent/design/local.group-acl-integration.md +338 -0
  15. package/agent/design/local.memory-acl-schema.md +352 -0
  16. package/agent/design/local.memory-collection-pattern-v2.md +348 -0
  17. package/agent/design/local.moderation-and-space-config.md +257 -0
  18. package/agent/design/local.v2-api-reference.md +621 -0
  19. package/agent/design/local.v2-migration-guide.md +191 -0
  20. package/agent/design/local.v2-usage-examples.md +265 -0
  21. package/agent/design/permissions-storage-architecture.md +11 -3
  22. package/agent/design/trust-escalation-prevention.md +9 -2
  23. package/agent/design/trust-system-implementation.md +12 -3
  24. package/agent/milestones/milestone-14-memory-collection-v2.md +182 -0
  25. package/agent/milestones/milestone-15-moderation-space-config.md +126 -0
  26. package/agent/progress.yaml +628 -49
  27. package/agent/scripts/acp.common.sh +2 -0
  28. package/agent/scripts/acp.install.sh +11 -1
  29. package/agent/scripts/acp.package-install-optimized.sh +454 -0
  30. package/agent/scripts/acp.package-install.sh +247 -300
  31. package/agent/scripts/acp.project-info.sh +218 -0
  32. package/agent/scripts/acp.project-remove.sh +302 -0
  33. package/agent/scripts/acp.project-update.sh +296 -0
  34. package/agent/scripts/acp.yaml-parser.sh +128 -10
  35. package/agent/tasks/milestone-14-memory-collection-v2/task-165-core-infrastructure-setup.md +171 -0
  36. package/agent/tasks/milestone-14-memory-collection-v2/task-166-update-remember-publish.md +191 -0
  37. package/agent/tasks/milestone-14-memory-collection-v2/task-167-update-remember-retract.md +186 -0
  38. package/agent/tasks/milestone-14-memory-collection-v2/task-168-implement-remember-revise.md +184 -0
  39. package/agent/tasks/milestone-14-memory-collection-v2/task-169-update-remember-search-space.md +179 -0
  40. package/agent/tasks/milestone-14-memory-collection-v2/task-170-update-remember-create-update.md +139 -0
  41. package/agent/tasks/milestone-14-memory-collection-v2/task-172-performance-testing-optimization.md +161 -0
  42. package/agent/tasks/milestone-14-memory-collection-v2/task-173-documentation-examples.md +258 -0
  43. package/agent/tasks/milestone-15-moderation-space-config/task-174-add-moderation-schema-fields.md +57 -0
  44. package/agent/tasks/milestone-15-moderation-space-config/task-175-create-space-config-service.md +64 -0
  45. package/agent/tasks/milestone-15-moderation-space-config/task-176-wire-moderation-publish-flow.md +45 -0
  46. package/agent/tasks/milestone-15-moderation-space-config/task-177-add-moderation-search-filters.md +70 -0
  47. package/agent/tasks/milestone-15-moderation-space-config/task-178-create-remember-moderate-tool.md +69 -0
  48. package/agent/tasks/milestone-15-moderation-space-config/task-179-documentation-integration-tests.md +58 -0
  49. package/agent/tasks/milestone-16-ghost-system/task-187-ghost-config-firestore.md +41 -0
  50. package/agent/tasks/milestone-16-ghost-system/task-188-trust-filter-integration.md +44 -0
  51. package/agent/tasks/milestone-16-ghost-system/task-189-ghost-memory-filtering.md +43 -0
  52. package/agent/tasks/milestone-16-ghost-system/task-190-ghost-config-tools.md +45 -0
  53. package/agent/tasks/milestone-16-ghost-system/task-191-escalation-firestore.md +38 -0
  54. package/agent/tasks/milestone-16-ghost-system/task-192-documentation-verification.md +39 -0
  55. package/agent/tasks/milestone-7-trust-permissions/task-180-access-result-permission-types.md +69 -0
  56. package/agent/tasks/milestone-7-trust-permissions/task-181-firestore-permissions-access-logs.md +56 -0
  57. package/agent/tasks/milestone-7-trust-permissions/task-182-trust-enforcement-service.md +68 -0
  58. package/agent/tasks/milestone-7-trust-permissions/task-183-access-control-service.md +70 -0
  59. package/agent/tasks/milestone-7-trust-permissions/task-184-permission-tools.md +79 -0
  60. package/agent/tasks/milestone-7-trust-permissions/task-185-wire-trust-into-search-query.md +55 -0
  61. package/agent/tasks/milestone-7-trust-permissions/task-186-documentation-verification.md +56 -0
  62. package/agent/tasks/task-76-fix-indexnullstate-schema-bug.md +197 -0
  63. package/dist/collections/composite-ids.d.ts +106 -0
  64. package/dist/collections/core-infrastructure.spec.d.ts +11 -0
  65. package/dist/collections/dot-notation.d.ts +106 -0
  66. package/dist/collections/tracking-arrays.d.ts +176 -0
  67. package/dist/constants/content-types.d.ts +1 -0
  68. package/dist/schema/v2-collections-comments.spec.d.ts +8 -0
  69. package/dist/schema/v2-collections.d.ts +210 -0
  70. package/dist/server-factory.d.ts +15 -0
  71. package/dist/server-factory.js +2798 -1029
  72. package/dist/server.js +2526 -1012
  73. package/dist/services/access-control.d.ts +103 -0
  74. package/dist/services/access-control.spec.d.ts +2 -0
  75. package/dist/services/credentials-provider.d.ts +24 -0
  76. package/dist/services/credentials-provider.spec.d.ts +2 -0
  77. package/dist/services/escalation.service.d.ts +22 -0
  78. package/dist/services/escalation.service.spec.d.ts +2 -0
  79. package/dist/services/ghost-config.service.d.ts +55 -0
  80. package/dist/services/ghost-config.service.spec.d.ts +2 -0
  81. package/dist/services/space-config.service.d.ts +23 -0
  82. package/dist/services/space-config.service.spec.d.ts +2 -0
  83. package/dist/services/trust-enforcement.d.ts +83 -0
  84. package/dist/services/trust-enforcement.spec.d.ts +2 -0
  85. package/dist/services/trust-validator.d.ts +43 -0
  86. package/dist/services/trust-validator.spec.d.ts +2 -0
  87. package/dist/tools/confirm-publish-moderation.spec.d.ts +8 -0
  88. package/dist/tools/confirm.d.ts +8 -1
  89. package/dist/tools/create-memory.d.ts +2 -1
  90. package/dist/tools/create-memory.spec.d.ts +10 -0
  91. package/dist/tools/create-relationship.d.ts +2 -1
  92. package/dist/tools/delete-memory.d.ts +2 -1
  93. package/dist/tools/delete-relationship.d.ts +2 -1
  94. package/dist/tools/deny.d.ts +2 -1
  95. package/dist/tools/find-similar.d.ts +2 -1
  96. package/dist/tools/get-preferences.d.ts +2 -1
  97. package/dist/tools/ghost-config.d.ts +27 -0
  98. package/dist/tools/ghost-config.spec.d.ts +2 -0
  99. package/dist/tools/moderate.d.ts +20 -0
  100. package/dist/tools/moderate.spec.d.ts +5 -0
  101. package/dist/tools/publish.d.ts +11 -3
  102. package/dist/tools/query-memory.d.ts +3 -1
  103. package/dist/tools/query-space.d.ts +4 -1
  104. package/dist/tools/retract.d.ts +29 -0
  105. package/dist/tools/revise.d.ts +45 -0
  106. package/dist/tools/revise.spec.d.ts +8 -0
  107. package/dist/tools/search-memory.d.ts +2 -1
  108. package/dist/tools/search-relationship.d.ts +2 -1
  109. package/dist/tools/search-space.d.ts +25 -5
  110. package/dist/tools/search-space.spec.d.ts +9 -0
  111. package/dist/tools/set-preference.d.ts +2 -1
  112. package/dist/tools/update-memory.d.ts +2 -1
  113. package/dist/tools/update-relationship.d.ts +2 -1
  114. package/dist/types/access-result.d.ts +48 -0
  115. package/dist/types/access-result.spec.d.ts +2 -0
  116. package/dist/types/auth.d.ts +46 -0
  117. package/dist/types/ghost-config.d.ts +36 -0
  118. package/dist/types/memory.d.ts +3 -1
  119. package/dist/types/preferences.d.ts +1 -1
  120. package/dist/utils/auth-helpers.d.ts +14 -0
  121. package/dist/utils/auth-helpers.spec.d.ts +2 -0
  122. package/dist/utils/test-data-generator.d.ts +124 -0
  123. package/dist/utils/test-data-generator.spec.d.ts +12 -0
  124. package/dist/v2-performance.e2e.d.ts +17 -0
  125. package/dist/v2-smoke.e2e.d.ts +14 -0
  126. package/dist/weaviate/client.d.ts +5 -8
  127. package/dist/weaviate/space-schema.d.ts +2 -2
  128. package/docs/performance/v2-benchmarks.md +80 -0
  129. package/jest.e2e.config.js +14 -3
  130. package/package.json +1 -1
  131. package/scripts/.collection-recreation-state.yaml +16 -0
  132. package/scripts/.gitkeep +5 -0
  133. package/scripts/README-collection-recreation.md +224 -0
  134. package/scripts/README.md +51 -0
  135. package/scripts/backup-collections.ts +543 -0
  136. package/scripts/delete-collection.ts +137 -0
  137. package/scripts/migrate-recreate-collections.ts +578 -0
  138. package/scripts/migrate-v1-to-v2.ts +1094 -0
  139. package/scripts/package-lock.json +1113 -0
  140. package/scripts/package.json +27 -0
  141. package/src/collections/composite-ids.ts +193 -0
  142. package/src/collections/core-infrastructure.spec.ts +353 -0
  143. package/src/collections/dot-notation.ts +212 -0
  144. package/src/collections/tracking-arrays.ts +298 -0
  145. package/src/constants/content-types.ts +20 -0
  146. package/src/schema/v2-collections-comments.spec.ts +141 -0
  147. package/src/schema/v2-collections.ts +433 -0
  148. package/src/server-factory.ts +89 -20
  149. package/src/server.ts +45 -17
  150. package/src/services/access-control.spec.ts +383 -0
  151. package/src/services/access-control.ts +291 -0
  152. package/src/services/credentials-provider.spec.ts +22 -0
  153. package/src/services/credentials-provider.ts +34 -0
  154. package/src/services/escalation.service.spec.ts +183 -0
  155. package/src/services/escalation.service.ts +150 -0
  156. package/src/services/ghost-config.service.spec.ts +339 -0
  157. package/src/services/ghost-config.service.ts +219 -0
  158. package/src/services/space-config.service.spec.ts +102 -0
  159. package/src/services/space-config.service.ts +79 -0
  160. package/src/services/trust-enforcement.spec.ts +309 -0
  161. package/src/services/trust-enforcement.ts +197 -0
  162. package/src/services/trust-validator.spec.ts +108 -0
  163. package/src/services/trust-validator.ts +105 -0
  164. package/src/tools/confirm-publish-moderation.spec.ts +240 -0
  165. package/src/tools/confirm.ts +869 -135
  166. package/src/tools/create-memory.spec.ts +126 -0
  167. package/src/tools/create-memory.ts +20 -27
  168. package/src/tools/create-relationship.ts +17 -8
  169. package/src/tools/delete-memory.ts +13 -6
  170. package/src/tools/delete-relationship.ts +15 -6
  171. package/src/tools/deny.ts +8 -1
  172. package/src/tools/find-similar.ts +21 -8
  173. package/src/tools/get-preferences.ts +10 -1
  174. package/src/tools/ghost-config.spec.ts +180 -0
  175. package/src/tools/ghost-config.ts +230 -0
  176. package/src/tools/moderate.spec.ts +277 -0
  177. package/src/tools/moderate.ts +219 -0
  178. package/src/tools/publish.ts +99 -41
  179. package/src/tools/query-memory.ts +28 -6
  180. package/src/tools/query-space.ts +39 -4
  181. package/src/tools/retract.ts +292 -0
  182. package/src/tools/revise.spec.ts +146 -0
  183. package/src/tools/revise.ts +283 -0
  184. package/src/tools/search-memory.ts +30 -7
  185. package/src/tools/search-relationship.ts +11 -2
  186. package/src/tools/search-space.spec.ts +341 -0
  187. package/src/tools/search-space.ts +323 -99
  188. package/src/tools/set-preference.ts +10 -1
  189. package/src/tools/update-memory.ts +16 -5
  190. package/src/tools/update-relationship.ts +10 -1
  191. package/src/types/access-result.spec.ts +193 -0
  192. package/src/types/access-result.ts +62 -0
  193. package/src/types/auth.ts +52 -0
  194. package/src/types/ghost-config.ts +46 -0
  195. package/src/types/memory.ts +9 -1
  196. package/src/types/preferences.ts +2 -2
  197. package/src/utils/auth-helpers.spec.ts +75 -0
  198. package/src/utils/auth-helpers.ts +25 -0
  199. package/src/utils/test-data-generator.spec.ts +317 -0
  200. package/src/utils/test-data-generator.ts +292 -0
  201. package/src/utils/weaviate-filters.ts +4 -4
  202. package/src/v2-performance.e2e.ts +173 -0
  203. package/src/v2-smoke.e2e.ts +401 -0
  204. package/src/weaviate/client.spec.ts +5 -5
  205. package/src/weaviate/client.ts +51 -36
  206. package/src/weaviate/schema.ts +11 -256
  207. package/src/weaviate/space-schema.spec.ts +24 -24
  208. package/src/weaviate/space-schema.ts +18 -6
@@ -0,0 +1,1094 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * V1 → V2 Collection Migration Script
4
+ *
5
+ * Migrates Weaviate collections from v1 naming to v2 naming:
6
+ * Memory_{SanitizedUserId} → Memory_users_{literalUserId}
7
+ * Memory_public → Memory_spaces_public
8
+ * Memory_{spaceId} → Memory_spaces_public (merged)
9
+ *
10
+ * Safety: V1 collections are NEVER modified or deleted.
11
+ *
12
+ * Usage:
13
+ * npx tsx scripts/migrate-v1-to-v2.ts [options]
14
+ * --dry-run Preview changes without writing
15
+ * --skip-backup Skip backup step (if already backed up)
16
+ * --verify-only Only run verification checks
17
+ * --batch-size N Documents per batch (default: 100)
18
+ */
19
+
20
+ import weaviate, { WeaviateClient } from 'weaviate-client';
21
+ import * as fs from 'fs';
22
+ import * as yaml from 'yaml';
23
+ import * as dotenv from 'dotenv';
24
+
25
+ // Load environment
26
+ dotenv.config();
27
+
28
+ // ============================================================================
29
+ // Types
30
+ // ============================================================================
31
+
32
+ interface MigrationConfig {
33
+ weaviate: {
34
+ url: string;
35
+ apiKey?: string;
36
+ openaiApiKey?: string;
37
+ };
38
+ options: {
39
+ batchSize: number;
40
+ dryRun: boolean;
41
+ skipBackup: boolean;
42
+ verifyOnly: boolean;
43
+ stateFile: string;
44
+ };
45
+ }
46
+
47
+ interface CollectionClassification {
48
+ name: string;
49
+ type: 'user' | 'public' | 'space' | 'backup' | 'unknown';
50
+ v2Name?: string;
51
+ userId?: string; // literal userId from documents
52
+ spaceId?: string;
53
+ }
54
+
55
+ interface MigrationStep {
56
+ name: string;
57
+ status: 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
58
+ error?: string;
59
+ }
60
+
61
+ interface MigrationState {
62
+ migration: {
63
+ id: string;
64
+ started_at: string;
65
+ updated_at: string;
66
+ status: 'not_started' | 'discovering' | 'backing_up' | 'creating' | 'copying' | 'verifying' | 'completed' | 'failed';
67
+ };
68
+ collections: CollectionClassification[];
69
+ steps: MigrationStep[];
70
+ copy_progress: {
71
+ [collectionName: string]: {
72
+ total: number;
73
+ copied: number;
74
+ status: 'pending' | 'in_progress' | 'completed' | 'failed';
75
+ };
76
+ };
77
+ verification: {
78
+ passed: boolean;
79
+ checks: Array<{
80
+ name: string;
81
+ passed: boolean;
82
+ details?: string;
83
+ }>;
84
+ };
85
+ errors: Array<{
86
+ step: string;
87
+ error: string;
88
+ timestamp: string;
89
+ }>;
90
+ }
91
+
92
+ /**
93
+ * V1 → V2 property name mapping for data transformation during copy.
94
+ * Keys are v1 names, values are v2 names.
95
+ */
96
+ const PROPERTY_RENAMES: Record<string, string> = {
97
+ type: 'content_type',
98
+ trust: 'trust_score',
99
+ location_gps_lat: 'location_lat',
100
+ location_gps_lng: 'location_lon',
101
+ relationships: 'relationship_ids',
102
+ memory_ids: 'related_memory_ids',
103
+ };
104
+
105
+ // ============================================================================
106
+ // State Manager
107
+ // ============================================================================
108
+
109
+ class StateManager {
110
+ private stateFile: string;
111
+ private state!: MigrationState;
112
+
113
+ constructor(stateFile: string) {
114
+ this.stateFile = stateFile;
115
+ }
116
+
117
+ async initialize(): Promise<void> {
118
+ if (fs.existsSync(this.stateFile)) {
119
+ this.load();
120
+ console.log(` Resuming migration from ${this.stateFile}\n`);
121
+ } else {
122
+ this.state = this.createInitialState();
123
+ await this.save();
124
+ console.log(` Created state file: ${this.stateFile}\n`);
125
+ }
126
+ }
127
+
128
+ private createInitialState(): MigrationState {
129
+ const now = new Date().toISOString();
130
+ return {
131
+ migration: {
132
+ id: `v1-to-v2-${now.replace(/[:.]/g, '-').slice(0, 19)}`,
133
+ started_at: now,
134
+ updated_at: now,
135
+ status: 'not_started',
136
+ },
137
+ collections: [],
138
+ steps: [],
139
+ copy_progress: {},
140
+ verification: { passed: false, checks: [] },
141
+ errors: [],
142
+ };
143
+ }
144
+
145
+ private load(): void {
146
+ const content = fs.readFileSync(this.stateFile, 'utf8');
147
+ this.state = yaml.parse(content);
148
+ }
149
+
150
+ async save(): Promise<void> {
151
+ this.state.migration.updated_at = new Date().toISOString();
152
+ fs.writeFileSync(this.stateFile, yaml.stringify(this.state), 'utf8');
153
+ }
154
+
155
+ getState(): MigrationState {
156
+ return this.state;
157
+ }
158
+
159
+ setStatus(status: MigrationState['migration']['status']): void {
160
+ this.state.migration.status = status;
161
+ }
162
+
163
+ setCollections(collections: CollectionClassification[]): void {
164
+ this.state.collections = collections;
165
+ }
166
+
167
+ addStep(name: string, status: MigrationStep['status'] = 'pending'): void {
168
+ const existing = this.state.steps.find(s => s.name === name);
169
+ if (existing) {
170
+ existing.status = status;
171
+ } else {
172
+ this.state.steps.push({ name, status });
173
+ }
174
+ }
175
+
176
+ updateStep(name: string, status: MigrationStep['status'], error?: string): void {
177
+ const step = this.state.steps.find(s => s.name === name);
178
+ if (step) {
179
+ step.status = status;
180
+ if (error) step.error = error;
181
+ }
182
+ }
183
+
184
+ updateCopyProgress(collection: string, total: number, copied: number, status: 'pending' | 'in_progress' | 'completed' | 'failed'): void {
185
+ this.state.copy_progress[collection] = { total, copied, status };
186
+ }
187
+
188
+ addVerificationCheck(name: string, passed: boolean, details?: string): void {
189
+ this.state.verification.checks.push({ name, passed, details });
190
+ }
191
+
192
+ setVerificationPassed(passed: boolean): void {
193
+ this.state.verification.passed = passed;
194
+ }
195
+
196
+ addError(step: string, error: string): void {
197
+ this.state.errors.push({ step, error, timestamp: new Date().toISOString() });
198
+ }
199
+
200
+ async cleanup(): Promise<void> {
201
+ if (fs.existsSync(this.stateFile)) {
202
+ fs.unlinkSync(this.stateFile);
203
+ }
204
+ }
205
+ }
206
+
207
+ // ============================================================================
208
+ // Migration Engine
209
+ // ============================================================================
210
+
211
+ class V1ToV2Migration {
212
+ private client!: WeaviateClient;
213
+ private config: MigrationConfig;
214
+ private state: StateManager;
215
+
216
+ constructor(config: MigrationConfig) {
217
+ this.config = config;
218
+ this.state = new StateManager(config.options.stateFile);
219
+ }
220
+
221
+ // --------------------------------------------------------------------------
222
+ // Connection
223
+ // --------------------------------------------------------------------------
224
+
225
+ async connect(): Promise<void> {
226
+ console.log('Connecting to Weaviate...');
227
+
228
+ const clientConfig: any = {
229
+ authCredentials: this.config.weaviate.apiKey
230
+ ? new weaviate.ApiKey(this.config.weaviate.apiKey)
231
+ : undefined,
232
+ };
233
+ if (this.config.weaviate.openaiApiKey) {
234
+ clientConfig.headers = { 'X-Openai-Api-Key': this.config.weaviate.openaiApiKey };
235
+ }
236
+
237
+ this.client = await weaviate.connectToWeaviateCloud(
238
+ this.config.weaviate.url,
239
+ clientConfig,
240
+ );
241
+
242
+ console.log(' Connected\n');
243
+ }
244
+
245
+ async disconnect(): Promise<void> {
246
+ await this.client?.close();
247
+ }
248
+
249
+ // --------------------------------------------------------------------------
250
+ // Step 1: Discover
251
+ // --------------------------------------------------------------------------
252
+
253
+ async discover(): Promise<CollectionClassification[]> {
254
+ console.log('Step 1: Discovering collections...');
255
+ this.state.addStep('discover', 'in_progress');
256
+ this.state.setStatus('discovering');
257
+ await this.state.save();
258
+
259
+ const allCollections = await this.client.collections.listAll();
260
+ const memoryCollections = allCollections
261
+ .map(c => c.name)
262
+ .filter(name => name.startsWith('Memory_') && !name.startsWith('Backup_'));
263
+
264
+ console.log(` Found ${memoryCollections.length} Memory_ collections`);
265
+
266
+ const classified: CollectionClassification[] = [];
267
+
268
+ for (const name of memoryCollections) {
269
+ // Skip v2 collections that already exist
270
+ if (name.startsWith('Memory_users_') || name === 'Memory_spaces_public' || name.startsWith('Memory_groups_')) {
271
+ console.log(` [skip] ${name} (already v2 format)`);
272
+ continue;
273
+ }
274
+
275
+ if (name === 'Memory_public') {
276
+ classified.push({ name, type: 'public', v2Name: 'Memory_spaces_public' });
277
+ console.log(` [public] ${name} -> Memory_spaces_public`);
278
+ continue;
279
+ }
280
+
281
+ // Try to determine if this is a user collection or a space collection
282
+ // by reading a document and checking for user_id
283
+ const classification = await this.classifyCollection(name);
284
+ classified.push(classification);
285
+ console.log(` [${classification.type}] ${name} -> ${classification.v2Name || '(merged)'}`);
286
+ }
287
+
288
+ this.state.setCollections(classified);
289
+ this.state.updateStep('discover', 'completed');
290
+ await this.state.save();
291
+
292
+ console.log(`\n Classified: ${classified.filter(c => c.type === 'user').length} user, ` +
293
+ `${classified.filter(c => c.type === 'public').length} public, ` +
294
+ `${classified.filter(c => c.type === 'space').length} space\n`);
295
+
296
+ return classified;
297
+ }
298
+
299
+ private async classifyCollection(name: string): Promise<CollectionClassification> {
300
+ const collection = this.client.collections.get(name);
301
+
302
+ try {
303
+ // Fetch a sample document to inspect
304
+ const result = await collection.query.fetchObjects({ limit: 1 });
305
+
306
+ if (result.objects.length === 0) {
307
+ return { name, type: 'unknown' };
308
+ }
309
+
310
+ const doc = result.objects[0];
311
+ const props = doc.properties as Record<string, any>;
312
+
313
+ // Space collections have 'spaces' or 'author_id' fields
314
+ // User collections have 'user_id' without 'author_id'
315
+ if (props.author_id || props.spaces) {
316
+ // This is a space collection (per-space v1 collection)
317
+ const suffix = name.replace('Memory_', '');
318
+ return {
319
+ name,
320
+ type: 'space',
321
+ v2Name: 'Memory_spaces_public', // All spaces merge into single collection
322
+ spaceId: suffix,
323
+ };
324
+ }
325
+
326
+ // User collection — extract literal userId from user_id property
327
+ const literalUserId = props.user_id as string;
328
+ if (literalUserId) {
329
+ return {
330
+ name,
331
+ type: 'user',
332
+ v2Name: `Memory_users_${literalUserId}`,
333
+ userId: literalUserId,
334
+ };
335
+ }
336
+
337
+ return { name, type: 'unknown' };
338
+ } catch (error) {
339
+ console.log(` Warning: Could not classify ${name}: ${(error as Error).message}`);
340
+ return { name, type: 'unknown' };
341
+ }
342
+ }
343
+
344
+ // --------------------------------------------------------------------------
345
+ // Step 2: Backup
346
+ // --------------------------------------------------------------------------
347
+
348
+ async backup(collections: CollectionClassification[]): Promise<void> {
349
+ if (this.config.options.skipBackup) {
350
+ console.log('Step 2: Backup (SKIPPED — --skip-backup)\n');
351
+ this.state.addStep('backup', 'skipped');
352
+ await this.state.save();
353
+ return;
354
+ }
355
+
356
+ console.log('Step 2: Backing up v1 collections...');
357
+ this.state.addStep('backup', 'in_progress');
358
+ this.state.setStatus('backing_up');
359
+ await this.state.save();
360
+
361
+ for (const col of collections) {
362
+ const backupName = `Backup_${col.name}`;
363
+
364
+ const backupExists = await this.client.collections.exists(backupName);
365
+ if (backupExists) {
366
+ console.log(` [exists] ${backupName}`);
367
+ continue;
368
+ }
369
+
370
+ if (this.config.options.dryRun) {
371
+ console.log(` [dry-run] Would create ${backupName}`);
372
+ continue;
373
+ }
374
+
375
+ await this.copyCollection(col.name, backupName, false);
376
+ console.log(` [backed up] ${col.name} -> ${backupName}`);
377
+ }
378
+
379
+ this.state.updateStep('backup', 'completed');
380
+ await this.state.save();
381
+ console.log('');
382
+ }
383
+
384
+ // --------------------------------------------------------------------------
385
+ // Step 3: Create v2 collections
386
+ // --------------------------------------------------------------------------
387
+
388
+ async createV2Collections(collections: CollectionClassification[]): Promise<void> {
389
+ console.log('Step 3: Creating v2 collections...');
390
+ this.state.addStep('create_v2', 'in_progress');
391
+ this.state.setStatus('creating');
392
+ await this.state.save();
393
+
394
+ // Determine unique v2 collection names
395
+ const v2Names = new Set<string>();
396
+ for (const col of collections) {
397
+ if (col.v2Name) v2Names.add(col.v2Name);
398
+ }
399
+
400
+ for (const v2Name of v2Names) {
401
+ const exists = await this.client.collections.exists(v2Name);
402
+ if (exists) {
403
+ console.log(` [exists] ${v2Name}`);
404
+ continue;
405
+ }
406
+
407
+ if (this.config.options.dryRun) {
408
+ console.log(` [dry-run] Would create ${v2Name}`);
409
+ continue;
410
+ }
411
+
412
+ // Create with v2 schema
413
+ await this.createV2Collection(v2Name, collections);
414
+ console.log(` [created] ${v2Name}`);
415
+ }
416
+
417
+ this.state.updateStep('create_v2', 'completed');
418
+ await this.state.save();
419
+ console.log('');
420
+ }
421
+
422
+ private async createV2Collection(v2Name: string, _collections: CollectionClassification[]): Promise<void> {
423
+ // Import v2 schema functions
424
+ const { createUserCollectionSchema, createSpaceCollectionSchema } =
425
+ await import('../src/schema/v2-collections.js');
426
+
427
+ if (v2Name === 'Memory_spaces_public') {
428
+ const schema = createSpaceCollectionSchema();
429
+ await this.client.collections.create(schema);
430
+ } else if (v2Name.startsWith('Memory_users_')) {
431
+ const userId = v2Name.replace('Memory_users_', '');
432
+ const schema = createUserCollectionSchema(userId);
433
+ await this.client.collections.create(schema);
434
+ }
435
+ }
436
+
437
+ // --------------------------------------------------------------------------
438
+ // Step 4: Copy user memories
439
+ // --------------------------------------------------------------------------
440
+
441
+ async copyUserMemories(collections: CollectionClassification[]): Promise<void> {
442
+ const userCollections = collections.filter(c => c.type === 'user');
443
+ if (userCollections.length === 0) {
444
+ console.log('Step 4: Copy user memories (no user collections found)\n');
445
+ return;
446
+ }
447
+
448
+ console.log(`Step 4: Copying ${userCollections.length} user collection(s)...`);
449
+ this.state.addStep('copy_users', 'in_progress');
450
+ this.state.setStatus('copying');
451
+ await this.state.save();
452
+
453
+ for (const col of userCollections) {
454
+ if (!col.v2Name || !col.userId) {
455
+ console.log(` [skip] ${col.name} (no userId resolved)`);
456
+ continue;
457
+ }
458
+
459
+ if (this.config.options.dryRun) {
460
+ const srcCol = this.client.collections.get(col.name);
461
+ const aggregate = await srcCol.aggregate.overAll();
462
+ console.log(` [dry-run] ${col.name} -> ${col.v2Name} (${aggregate.totalCount || 0} docs)`);
463
+ continue;
464
+ }
465
+
466
+ await this.copyUserCollection(col);
467
+ }
468
+
469
+ this.state.updateStep('copy_users', 'completed');
470
+ await this.state.save();
471
+ console.log('');
472
+ }
473
+
474
+ private async copyUserCollection(col: CollectionClassification): Promise<void> {
475
+ const srcCollection = this.client.collections.get(col.name);
476
+ const dstCollection = this.client.collections.get(col.v2Name!);
477
+
478
+ const aggregate = await srcCollection.aggregate.overAll();
479
+ const totalCount = aggregate.totalCount || 0;
480
+ console.log(` Copying ${col.name} -> ${col.v2Name} (${totalCount} docs)`);
481
+
482
+ this.state.updateCopyProgress(col.name, totalCount, 0, 'in_progress');
483
+ await this.state.save();
484
+
485
+ let offset = 0;
486
+ let copied = 0;
487
+
488
+ while (offset < totalCount) {
489
+ const result = await srcCollection.query.fetchObjects({
490
+ limit: this.config.options.batchSize,
491
+ offset,
492
+ includeVector: true,
493
+ });
494
+
495
+ if (result.objects.length === 0) break;
496
+
497
+ const objects = result.objects.map(doc => {
498
+ const props = this.transformProperties(doc.properties as Record<string, any>);
499
+
500
+ // Add v2 tracking arrays if not present
501
+ if (!props.space_ids) props.space_ids = [];
502
+ if (!props.group_ids) props.group_ids = [];
503
+
504
+ return {
505
+ properties: props,
506
+ vectors: doc.vectors,
507
+ uuid: doc.uuid,
508
+ };
509
+ });
510
+
511
+ await dstCollection.data.insertMany(objects);
512
+
513
+ copied += result.objects.length;
514
+ offset += result.objects.length;
515
+
516
+ this.state.updateCopyProgress(col.name, totalCount, copied, 'in_progress');
517
+ await this.state.save();
518
+
519
+ const pct = ((copied / totalCount) * 100).toFixed(1);
520
+ process.stdout.write(`\r ${pct}% (${copied}/${totalCount})`);
521
+ }
522
+
523
+ this.state.updateCopyProgress(col.name, totalCount, copied, 'completed');
524
+ await this.state.save();
525
+ console.log(`\n Done: ${copied} docs copied`);
526
+ }
527
+
528
+ // --------------------------------------------------------------------------
529
+ // Step 5: Copy/merge published memories
530
+ // --------------------------------------------------------------------------
531
+
532
+ async copyPublishedMemories(collections: CollectionClassification[]): Promise<void> {
533
+ const publicCollections = collections.filter(c => c.type === 'public' || c.type === 'space');
534
+ if (publicCollections.length === 0) {
535
+ console.log('Step 5: Copy published memories (no public/space collections found)\n');
536
+ return;
537
+ }
538
+
539
+ console.log(`Step 5: Merging ${publicCollections.length} public/space collection(s) -> Memory_spaces_public...`);
540
+ this.state.addStep('copy_published', 'in_progress');
541
+ await this.state.save();
542
+
543
+ for (const col of publicCollections) {
544
+ if (this.config.options.dryRun) {
545
+ const srcCol = this.client.collections.get(col.name);
546
+ const aggregate = await srcCol.aggregate.overAll();
547
+ console.log(` [dry-run] ${col.name} -> Memory_spaces_public (${aggregate.totalCount || 0} docs)`);
548
+ continue;
549
+ }
550
+
551
+ await this.copyPublicCollection(col);
552
+ }
553
+
554
+ this.state.updateStep('copy_published', 'completed');
555
+ await this.state.save();
556
+ console.log('');
557
+ }
558
+
559
+ private async copyPublicCollection(col: CollectionClassification): Promise<void> {
560
+ const srcCollection = this.client.collections.get(col.name);
561
+ const dstCollection = this.client.collections.get('Memory_spaces_public');
562
+
563
+ const aggregate = await srcCollection.aggregate.overAll();
564
+ const totalCount = aggregate.totalCount || 0;
565
+ console.log(` Copying ${col.name} -> Memory_spaces_public (${totalCount} docs)`);
566
+
567
+ this.state.updateCopyProgress(col.name, totalCount, 0, 'in_progress');
568
+ await this.state.save();
569
+
570
+ let offset = 0;
571
+ let copied = 0;
572
+
573
+ while (offset < totalCount) {
574
+ const result = await srcCollection.query.fetchObjects({
575
+ limit: this.config.options.batchSize,
576
+ offset,
577
+ includeVector: true,
578
+ });
579
+
580
+ if (result.objects.length === 0) break;
581
+
582
+ const objects = result.objects.map(doc => {
583
+ const props = this.transformProperties(doc.properties as Record<string, any>);
584
+ const authorId = (props.author_id || props.user_id || '') as string;
585
+ const originalId = doc.uuid;
586
+
587
+ // Generate composite ID: {authorId}.{originalUUID}
588
+ const compositeId = authorId ? `${authorId}.${originalId}` : originalId;
589
+
590
+ // Set space_ids from existing spaces field or from collection's spaceId
591
+ if (!props.space_ids || (props.space_ids as string[]).length === 0) {
592
+ if (props.spaces && Array.isArray(props.spaces) && props.spaces.length > 0) {
593
+ props.space_ids = props.spaces;
594
+ } else if (col.spaceId) {
595
+ props.space_ids = [col.spaceId];
596
+ } else {
597
+ props.space_ids = [];
598
+ }
599
+ }
600
+
601
+ // Ensure group_ids
602
+ if (!props.group_ids) props.group_ids = [];
603
+
604
+ // Add revision fields
605
+ if (props.revision_count === undefined) props.revision_count = 0;
606
+ if (props.revised_at === undefined) props.revised_at = null;
607
+
608
+ return {
609
+ properties: props,
610
+ vectors: doc.vectors,
611
+ uuid: compositeId,
612
+ };
613
+ });
614
+
615
+ try {
616
+ await dstCollection.data.insertMany(objects);
617
+ } catch (insertError) {
618
+ // Some may already exist (if re-running), insert individually
619
+ for (const obj of objects) {
620
+ try {
621
+ await dstCollection.data.insert({
622
+ properties: obj.properties,
623
+ vectors: obj.vectors as any,
624
+ id: obj.uuid,
625
+ });
626
+ } catch {
627
+ // Already exists or other error — skip
628
+ }
629
+ }
630
+ }
631
+
632
+ copied += result.objects.length;
633
+ offset += result.objects.length;
634
+
635
+ this.state.updateCopyProgress(col.name, totalCount, copied, 'in_progress');
636
+ await this.state.save();
637
+
638
+ const pct = ((copied / totalCount) * 100).toFixed(1);
639
+ process.stdout.write(`\r ${pct}% (${copied}/${totalCount})`);
640
+ }
641
+
642
+ this.state.updateCopyProgress(col.name, totalCount, copied, 'completed');
643
+ await this.state.save();
644
+ console.log(`\n Done: ${copied} docs merged`);
645
+ }
646
+
647
+ // --------------------------------------------------------------------------
648
+ // Step 6: Backfill tracking arrays
649
+ // --------------------------------------------------------------------------
650
+
651
+ async backfillTrackingArrays(collections: CollectionClassification[]): Promise<void> {
652
+ console.log('Step 6: Backfilling tracking arrays on source user memories...');
653
+ this.state.addStep('backfill_tracking', 'in_progress');
654
+ await this.state.save();
655
+
656
+ const userCollections = collections.filter(c => c.type === 'user' && c.v2Name);
657
+
658
+ if (this.config.options.dryRun) {
659
+ console.log(` [dry-run] Would backfill tracking arrays for ${userCollections.length} user collection(s)`);
660
+ this.state.updateStep('backfill_tracking', 'completed');
661
+ await this.state.save();
662
+ console.log('');
663
+ return;
664
+ }
665
+
666
+ // For each published memory in Memory_spaces_public, find the source memory
667
+ // and update its space_ids
668
+ const spacesCollectionExists = await this.client.collections.exists('Memory_spaces_public');
669
+ if (!spacesCollectionExists) {
670
+ console.log(' No Memory_spaces_public collection to backfill from');
671
+ this.state.updateStep('backfill_tracking', 'completed');
672
+ await this.state.save();
673
+ console.log('');
674
+ return;
675
+ }
676
+
677
+ const spacesCollection = this.client.collections.get('Memory_spaces_public');
678
+ const aggregate = await spacesCollection.aggregate.overAll();
679
+ const totalCount = aggregate.totalCount || 0;
680
+
681
+ let offset = 0;
682
+ let updated = 0;
683
+
684
+ while (offset < totalCount) {
685
+ const result = await spacesCollection.query.fetchObjects({
686
+ limit: this.config.options.batchSize,
687
+ offset,
688
+ });
689
+
690
+ if (result.objects.length === 0) break;
691
+
692
+ for (const doc of result.objects) {
693
+ const props = doc.properties as Record<string, any>;
694
+ const compositeId = doc.uuid;
695
+
696
+ // Parse composite ID: {userId}.{memoryId}
697
+ const dotIndex = compositeId.indexOf('.');
698
+ if (dotIndex === -1) continue;
699
+
700
+ const userId = compositeId.substring(0, dotIndex);
701
+ const memoryId = compositeId.substring(dotIndex + 1);
702
+ const spaceIds = (props.space_ids as string[]) || [];
703
+
704
+ if (spaceIds.length === 0) continue;
705
+
706
+ // Find the user's v2 collection
707
+ const userV2Name = `Memory_users_${userId}`;
708
+ const userColExists = await this.client.collections.exists(userV2Name);
709
+ if (!userColExists) continue;
710
+
711
+ try {
712
+ const userCollection = this.client.collections.get(userV2Name);
713
+ await userCollection.data.update({
714
+ id: memoryId,
715
+ properties: {
716
+ space_ids: spaceIds,
717
+ },
718
+ });
719
+ updated++;
720
+ } catch {
721
+ // Memory might not exist in user collection — skip
722
+ }
723
+ }
724
+
725
+ offset += result.objects.length;
726
+ }
727
+
728
+ console.log(` Updated ${updated} source memories with tracking arrays`);
729
+ this.state.updateStep('backfill_tracking', 'completed');
730
+ await this.state.save();
731
+ console.log('');
732
+ }
733
+
734
+ // --------------------------------------------------------------------------
735
+ // Step 7: Verify
736
+ // --------------------------------------------------------------------------
737
+
738
+ async verify(collections: CollectionClassification[]): Promise<boolean> {
739
+ console.log('Step 7: Verifying migration...');
740
+
741
+ if (this.config.options.dryRun) {
742
+ console.log(' [dry-run] Skipping verification — v2 collections were not created');
743
+ this.state.addStep('verify', 'skipped');
744
+ await this.state.save();
745
+ return true;
746
+ }
747
+
748
+ this.state.addStep('verify', 'in_progress');
749
+ this.state.setStatus('verifying');
750
+ await this.state.save();
751
+
752
+ let allPassed = true;
753
+
754
+ // Check 1: Document count validation
755
+ for (const col of collections) {
756
+ if (!col.v2Name) continue;
757
+
758
+ const srcCol = this.client.collections.get(col.name);
759
+ const srcAggregate = await srcCol.aggregate.overAll();
760
+ const srcCount = srcAggregate.totalCount || 0;
761
+
762
+ if (col.type === 'user') {
763
+ const dstCol = this.client.collections.get(col.v2Name);
764
+ const dstAggregate = await dstCol.aggregate.overAll();
765
+ const dstCount = dstAggregate.totalCount || 0;
766
+
767
+ const passed = dstCount >= srcCount;
768
+ this.state.addVerificationCheck(
769
+ `count:${col.name}`,
770
+ passed,
771
+ `source=${srcCount}, dest=${dstCount}`,
772
+ );
773
+
774
+ if (!passed) {
775
+ console.log(` [FAIL] ${col.name}: source=${srcCount}, dest=${dstCount}`);
776
+ allPassed = false;
777
+ } else {
778
+ console.log(` [OK] ${col.name}: ${dstCount} docs (source: ${srcCount})`);
779
+ }
780
+ } else {
781
+ // Public/space collections merged — check that Memory_spaces_public exists
782
+ const dstExists = await this.client.collections.exists('Memory_spaces_public');
783
+ const passed = dstExists;
784
+ this.state.addVerificationCheck(
785
+ `exists:Memory_spaces_public`,
786
+ passed,
787
+ `exists=${dstExists}`,
788
+ );
789
+
790
+ if (!passed) {
791
+ console.log(` [FAIL] Memory_spaces_public does not exist`);
792
+ allPassed = false;
793
+ } else {
794
+ const dstCol = this.client.collections.get('Memory_spaces_public');
795
+ const dstAggregate = await dstCol.aggregate.overAll();
796
+ console.log(` [OK] Memory_spaces_public: ${dstAggregate.totalCount || 0} docs`);
797
+ }
798
+ }
799
+ }
800
+
801
+ // Check 2: Composite ID format validation (sample)
802
+ const spacesExists = await this.client.collections.exists('Memory_spaces_public');
803
+ if (spacesExists) {
804
+ const spacesCol = this.client.collections.get('Memory_spaces_public');
805
+ const sample = await spacesCol.query.fetchObjects({ limit: 10 });
806
+
807
+ let compositeValid = 0;
808
+ let compositeInvalid = 0;
809
+
810
+ for (const doc of sample.objects) {
811
+ if (doc.uuid.includes('.')) {
812
+ compositeValid++;
813
+ } else {
814
+ compositeInvalid++;
815
+ }
816
+ }
817
+
818
+ const passed = compositeInvalid === 0 || sample.objects.length === 0;
819
+ this.state.addVerificationCheck(
820
+ 'composite_ids',
821
+ passed,
822
+ `valid=${compositeValid}, invalid=${compositeInvalid} (sample of ${sample.objects.length})`,
823
+ );
824
+
825
+ if (!passed) {
826
+ console.log(` [WARN] Composite IDs: ${compositeInvalid} of ${sample.objects.length} lack dot format`);
827
+ } else {
828
+ console.log(` [OK] Composite IDs: ${compositeValid} valid in sample`);
829
+ }
830
+ }
831
+
832
+ // Check 3: Tracking array consistency (sample)
833
+ for (const col of collections.filter(c => c.type === 'user' && c.v2Name)) {
834
+ const userCol = this.client.collections.get(col.v2Name!);
835
+ const sample = await userCol.query.fetchObjects({ limit: 5 });
836
+
837
+ let hasTrackingArrays = 0;
838
+ for (const doc of sample.objects) {
839
+ const props = doc.properties as Record<string, any>;
840
+ if (Array.isArray(props.space_ids) && Array.isArray(props.group_ids)) {
841
+ hasTrackingArrays++;
842
+ }
843
+ }
844
+
845
+ const passed = hasTrackingArrays === sample.objects.length || sample.objects.length === 0;
846
+ this.state.addVerificationCheck(
847
+ `tracking_arrays:${col.v2Name}`,
848
+ passed,
849
+ `${hasTrackingArrays}/${sample.objects.length} have tracking arrays`,
850
+ );
851
+
852
+ if (!passed) {
853
+ console.log(` [WARN] ${col.v2Name}: ${hasTrackingArrays}/${sample.objects.length} have tracking arrays`);
854
+ } else {
855
+ console.log(` [OK] ${col.v2Name}: tracking arrays present`);
856
+ }
857
+ }
858
+
859
+ this.state.setVerificationPassed(allPassed);
860
+ this.state.updateStep('verify', allPassed ? 'completed' : 'failed');
861
+ await this.state.save();
862
+ console.log(`\n Verification: ${allPassed ? 'PASSED' : 'FAILED'}\n`);
863
+
864
+ return allPassed;
865
+ }
866
+
867
+ // --------------------------------------------------------------------------
868
+ // Helpers
869
+ // --------------------------------------------------------------------------
870
+
871
+ /**
872
+ * Transform v1 property names to v2 property names.
873
+ */
874
+ private transformProperties(props: Record<string, any>): Record<string, any> {
875
+ const result: Record<string, any> = {};
876
+
877
+ for (const [key, value] of Object.entries(props)) {
878
+ if (key === '_additional') continue; // Skip Weaviate internals
879
+
880
+ const v2Key = PROPERTY_RENAMES[key] || key;
881
+ // Only write renamed key if the v2 key doesn't already have a value
882
+ if (v2Key !== key && result[v2Key] !== undefined) {
883
+ // v2 key already set, skip v1 value
884
+ continue;
885
+ }
886
+ result[v2Key] = value;
887
+ }
888
+
889
+ return result;
890
+ }
891
+
892
+ /**
893
+ * Copy all documents from one collection to another (for backup).
894
+ */
895
+ private async copyCollection(srcName: string, dstName: string, transform: boolean): Promise<void> {
896
+ const srcCollection = this.client.collections.get(srcName);
897
+
898
+ // Get source schema and create destination with same schema
899
+ const srcSchema = await srcCollection.config.get();
900
+ await this.client.collections.create({
901
+ name: dstName,
902
+ vectorizers: weaviate.configure.vectorizer.text2VecOpenAI({
903
+ model: 'text-embedding-3-small',
904
+ sourceProperties: ['content', 'title', 'summary', 'observation'],
905
+ }),
906
+ invertedIndex: weaviate.configure.invertedIndex({
907
+ indexNullState: true,
908
+ }),
909
+ properties: srcSchema.properties as any,
910
+ });
911
+
912
+ const dstCollection = this.client.collections.get(dstName);
913
+
914
+ // Copy documents in batches
915
+ const aggregate = await srcCollection.aggregate.overAll();
916
+ const totalCount = aggregate.totalCount || 0;
917
+ let offset = 0;
918
+
919
+ while (offset < totalCount) {
920
+ const result = await srcCollection.query.fetchObjects({
921
+ limit: this.config.options.batchSize,
922
+ offset,
923
+ includeVector: true,
924
+ });
925
+
926
+ if (result.objects.length === 0) break;
927
+
928
+ const objects = result.objects.map(doc => ({
929
+ properties: transform
930
+ ? this.transformProperties(doc.properties as Record<string, any>)
931
+ : doc.properties,
932
+ vectors: doc.vectors,
933
+ uuid: doc.uuid,
934
+ }));
935
+
936
+ await dstCollection.data.insertMany(objects);
937
+ offset += result.objects.length;
938
+ }
939
+ }
940
+
941
+ // --------------------------------------------------------------------------
942
+ // Main
943
+ // --------------------------------------------------------------------------
944
+
945
+ async run(): Promise<void> {
946
+ console.log('='.repeat(60));
947
+ console.log(' V1 -> V2 Weaviate Migration');
948
+ console.log('='.repeat(60));
949
+ console.log(` Weaviate URL: ${this.config.weaviate.url}`);
950
+ console.log(` Batch Size: ${this.config.options.batchSize}`);
951
+ console.log(` Dry Run: ${this.config.options.dryRun}`);
952
+ console.log(` Skip Backup: ${this.config.options.skipBackup}`);
953
+ console.log(` Verify Only: ${this.config.options.verifyOnly}`);
954
+ console.log('='.repeat(60));
955
+ console.log('');
956
+
957
+ await this.state.initialize();
958
+
959
+ try {
960
+ await this.connect();
961
+
962
+ // Step 1: Discover
963
+ const collections = await this.discover();
964
+
965
+ if (collections.length === 0) {
966
+ console.log('No v1 collections found to migrate. Done.\n');
967
+ return;
968
+ }
969
+
970
+ // Verify-only mode
971
+ if (this.config.options.verifyOnly) {
972
+ await this.verify(collections);
973
+ return;
974
+ }
975
+
976
+ // Step 2: Backup
977
+ await this.backup(collections);
978
+
979
+ // Step 3: Create v2 collections
980
+ await this.createV2Collections(collections);
981
+
982
+ // Step 4: Copy user memories
983
+ await this.copyUserMemories(collections);
984
+
985
+ // Step 5: Copy/merge published memories
986
+ await this.copyPublishedMemories(collections);
987
+
988
+ // Step 6: Backfill tracking arrays
989
+ await this.backfillTrackingArrays(collections);
990
+
991
+ // Step 7: Verify
992
+ const passed = await this.verify(collections);
993
+
994
+ // Summary
995
+ console.log('='.repeat(60));
996
+ if (this.config.options.dryRun) {
997
+ console.log(' DRY RUN COMPLETE — no changes were made');
998
+ } else if (passed) {
999
+ this.state.setStatus('completed');
1000
+ await this.state.save();
1001
+ await this.state.cleanup();
1002
+ console.log(' MIGRATION COMPLETE');
1003
+ console.log('');
1004
+ console.log(' Next steps:');
1005
+ console.log(' 1. Apply code switch (Deliverable 3)');
1006
+ console.log(' 2. Run full test suite');
1007
+ console.log(' 3. Deploy');
1008
+ console.log(' 4. Verify production functionality');
1009
+ console.log(' 5. Delete v1 and backup collections');
1010
+ } else {
1011
+ this.state.setStatus('failed');
1012
+ await this.state.save();
1013
+ console.log(' MIGRATION COMPLETED WITH WARNINGS');
1014
+ console.log(' Review verification results above');
1015
+ }
1016
+ console.log('='.repeat(60));
1017
+ console.log('');
1018
+
1019
+ } catch (error) {
1020
+ this.state.setStatus('failed');
1021
+ this.state.addError('global', (error as Error).message);
1022
+ await this.state.save();
1023
+ throw error;
1024
+ } finally {
1025
+ await this.disconnect();
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ // ============================================================================
1031
+ // Configuration
1032
+ // ============================================================================
1033
+
1034
+ function loadConfig(): MigrationConfig {
1035
+ const args = process.argv.slice(2);
1036
+ const cliArgs: Record<string, string> = {};
1037
+
1038
+ for (let i = 0; i < args.length; i++) {
1039
+ if (args[i].startsWith('--')) {
1040
+ const key = args[i].slice(2);
1041
+ const value = args[i + 1];
1042
+ if (value && !value.startsWith('--')) {
1043
+ cliArgs[key] = value;
1044
+ i++;
1045
+ } else {
1046
+ cliArgs[key] = 'true';
1047
+ }
1048
+ }
1049
+ }
1050
+
1051
+ const config: MigrationConfig = {
1052
+ weaviate: {
1053
+ url: cliArgs['weaviate-url'] || process.env.WEAVIATE_REST_URL || '',
1054
+ apiKey: cliArgs['weaviate-key'] || process.env.WEAVIATE_API_KEY,
1055
+ openaiApiKey: cliArgs['openai-key'] || process.env.OPENAI_EMBEDDINGS_API_KEY,
1056
+ },
1057
+ options: {
1058
+ batchSize: parseInt(cliArgs['batch-size'] || '100'),
1059
+ dryRun: cliArgs['dry-run'] === 'true',
1060
+ skipBackup: cliArgs['skip-backup'] === 'true',
1061
+ verifyOnly: cliArgs['verify-only'] === 'true',
1062
+ stateFile: cliArgs['state-file'] || '.v1-to-v2-migration-state.yaml',
1063
+ },
1064
+ };
1065
+
1066
+ if (!config.weaviate.url) {
1067
+ throw new Error('Weaviate URL is required (--weaviate-url or WEAVIATE_REST_URL)');
1068
+ }
1069
+
1070
+ return config;
1071
+ }
1072
+
1073
+ // ============================================================================
1074
+ // Main
1075
+ // ============================================================================
1076
+
1077
+ async function main() {
1078
+ try {
1079
+ const config = loadConfig();
1080
+ const migration = new V1ToV2Migration(config);
1081
+ await migration.run();
1082
+ process.exit(0);
1083
+ } catch (error: any) {
1084
+ console.error(`\nFatal error: ${error.message}`);
1085
+ console.error(error.stack);
1086
+ process.exit(1);
1087
+ }
1088
+ }
1089
+
1090
+ if (import.meta.url === `file://${process.argv[1]}`) {
1091
+ main();
1092
+ }
1093
+
1094
+ export { V1ToV2Migration, type MigrationConfig };