@memrosetta/cli 0.5.1 → 0.5.3

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 (52) hide show
  1. package/dist/chunk-47SU2YUJ.js +64 -0
  2. package/dist/chunk-C4ANKSCI.js +151 -0
  3. package/dist/chunk-CEHRM6IW.js +151 -0
  4. package/dist/chunk-G2W4YK2T.js +56 -0
  5. package/dist/chunk-GGXC7TAJ.js +139 -0
  6. package/dist/chunk-GRNZVSAF.js +56 -0
  7. package/dist/chunk-GZINXXM4.js +139 -0
  8. package/dist/chunk-RZFCVYTK.js +71 -0
  9. package/dist/chunk-US6CEDMU.js +66 -0
  10. package/dist/chunk-VMGX5FCY.js +64 -0
  11. package/dist/chunk-WYHEAKPC.js +71 -0
  12. package/dist/clear-32Y3U2WR.js +39 -0
  13. package/dist/clear-AFEJPCDA.js +39 -0
  14. package/dist/compress-CL5D4VVJ.js +33 -0
  15. package/dist/compress-UUEO7WCU.js +33 -0
  16. package/dist/count-U2ML5ZON.js +24 -0
  17. package/dist/count-VVOGYSM7.js +24 -0
  18. package/dist/duplicates-CEJ7WSGW.js +149 -0
  19. package/dist/duplicates-IBUS7CJS.js +149 -0
  20. package/dist/enforce-T7AS4PVD.js +381 -0
  21. package/dist/enforce-TC5SDPEZ.js +381 -0
  22. package/dist/feedback-3PJTTEOD.js +51 -0
  23. package/dist/feedback-IB7BHIRP.js +51 -0
  24. package/dist/get-TQ2U7HCD.js +30 -0
  25. package/dist/get-WPZIHQKW.js +30 -0
  26. package/dist/hooks/on-prompt.js +3 -3
  27. package/dist/hooks/on-stop.js +3 -3
  28. package/dist/index.js +30 -20
  29. package/dist/ingest-37UXPVT5.js +97 -0
  30. package/dist/ingest-TPQRH34A.js +97 -0
  31. package/dist/init-6YQL3RCQ.js +210 -0
  32. package/dist/init-LHXRCCLX.js +210 -0
  33. package/dist/invalidate-ER2TFFWK.js +40 -0
  34. package/dist/invalidate-PVHUGAJ6.js +40 -0
  35. package/dist/maintain-NICAXFK6.js +37 -0
  36. package/dist/maintain-Q553GBSF.js +37 -0
  37. package/dist/migrate-CZL3YNQK.js +255 -0
  38. package/dist/migrate-FI26FSBP.js +255 -0
  39. package/dist/relate-5TN2WEG3.js +57 -0
  40. package/dist/relate-KLBMYWB3.js +57 -0
  41. package/dist/reset-IPOAKTJM.js +132 -0
  42. package/dist/search-AYZBKRXF.js +48 -0
  43. package/dist/search-JQ3MLRKS.js +48 -0
  44. package/dist/status-JF2V7ZBX.js +184 -0
  45. package/dist/status-UV66PWUD.js +184 -0
  46. package/dist/store-AAJCT3PX.js +101 -0
  47. package/dist/store-OVDS57U5.js +101 -0
  48. package/dist/sync-56KJTKE7.js +542 -0
  49. package/dist/sync-BCKBYRXY.js +542 -0
  50. package/dist/working-memory-CJARSGEK.js +53 -0
  51. package/dist/working-memory-Z3RUGSTQ.js +53 -0
  52. package/package.json +4 -4
@@ -0,0 +1,210 @@
1
+ import {
2
+ getAgentsMdPath,
3
+ getCodexConfigFilePath,
4
+ getCodexHooksPath,
5
+ getCursorMcpConfigPath,
6
+ getCursorRulesPath,
7
+ getGeminiMdPath,
8
+ getGeminiSettingsFilePath,
9
+ getGenericMCPPath,
10
+ isClaudeCodeInstalled,
11
+ registerClaudeCodeHooks,
12
+ registerCodexHooks,
13
+ registerCodexMCP,
14
+ registerCursorMCP,
15
+ registerGeminiMCP,
16
+ registerGenericMCP,
17
+ updateClaudeMd
18
+ } from "./chunk-4LNXT25H.js";
19
+ import {
20
+ hasFlag,
21
+ optionalOption
22
+ } from "./chunk-US6CEDMU.js";
23
+ import {
24
+ getDefaultDbPath,
25
+ getEngine
26
+ } from "./chunk-47SU2YUJ.js";
27
+ import {
28
+ output
29
+ } from "./chunk-ET6TNQOJ.js";
30
+ import {
31
+ getConfig,
32
+ writeConfig
33
+ } from "./chunk-WYHEAKPC.js";
34
+
35
+ // src/commands/init.ts
36
+ import { existsSync } from "fs";
37
+ var LANG_FLAG_TO_PRESET = {
38
+ en: "en",
39
+ multi: "multilingual",
40
+ ko: "ko"
41
+ };
42
+ async function run(options) {
43
+ const { args, format, db, noEmbeddings } = options;
44
+ const wantClaudeCode = hasFlag(args, "--claude-code");
45
+ const wantCursor = hasFlag(args, "--cursor");
46
+ const wantCodex = hasFlag(args, "--codex");
47
+ const wantGemini = hasFlag(args, "--gemini");
48
+ const langFlag = optionalOption(args, "--lang");
49
+ const embeddingPreset = langFlag ? LANG_FLAG_TO_PRESET[langFlag] : void 0;
50
+ if (langFlag && !LANG_FLAG_TO_PRESET[langFlag]) {
51
+ process.stderr.write(
52
+ `Unknown --lang value: "${langFlag}". Supported: en, multi, ko
53
+ `
54
+ );
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+ {
59
+ const config2 = getConfig();
60
+ const updates = {};
61
+ if (db) {
62
+ updates.dbPath = db;
63
+ }
64
+ if (noEmbeddings) {
65
+ updates.enableEmbeddings = false;
66
+ }
67
+ if (embeddingPreset) {
68
+ updates.embeddingPreset = embeddingPreset;
69
+ }
70
+ if (Object.keys(updates).length > 0) {
71
+ writeConfig({ ...config2, ...updates });
72
+ }
73
+ }
74
+ const config = getConfig();
75
+ const dbPath = db ?? config.dbPath ?? getDefaultDbPath();
76
+ const existed = existsSync(dbPath);
77
+ const engine = await getEngine({ db: dbPath, noEmbeddings });
78
+ await engine.close();
79
+ const result = {
80
+ database: {
81
+ path: dbPath,
82
+ created: !existed
83
+ },
84
+ integrations: {}
85
+ };
86
+ registerGenericMCP();
87
+ result.integrations.mcp = {
88
+ registered: true,
89
+ path: getGenericMCPPath()
90
+ };
91
+ if (wantClaudeCode) {
92
+ const hooksOk = registerClaudeCodeHooks();
93
+ const claudeMdOk = updateClaudeMd();
94
+ result.integrations.claudeCode = {
95
+ hooks: hooksOk,
96
+ mcp: true,
97
+ claudeMd: claudeMdOk
98
+ };
99
+ }
100
+ if (wantCursor) {
101
+ const cursorRulesUpdated = registerCursorMCP();
102
+ result.integrations.cursor = {
103
+ mcp: true,
104
+ path: getCursorMcpConfigPath(),
105
+ cursorRules: cursorRulesUpdated,
106
+ cursorRulesPath: getCursorRulesPath()
107
+ };
108
+ }
109
+ if (wantCodex) {
110
+ const agentsMdUpdated = registerCodexMCP();
111
+ const hooksRegistered = registerCodexHooks();
112
+ result.integrations.codex = {
113
+ mcp: true,
114
+ path: getCodexConfigFilePath(),
115
+ agentsMd: agentsMdUpdated,
116
+ agentsMdPath: getAgentsMdPath(),
117
+ stopHook: hooksRegistered,
118
+ stopHookPath: getCodexHooksPath()
119
+ };
120
+ }
121
+ if (wantGemini) {
122
+ const geminiMdUpdated = registerGeminiMCP();
123
+ result.integrations.gemini = {
124
+ mcp: true,
125
+ path: getGeminiSettingsFilePath(),
126
+ geminiMd: geminiMdUpdated,
127
+ geminiMdPath: getGeminiMdPath()
128
+ };
129
+ }
130
+ if (format === "text") {
131
+ printTextOutput(result, wantClaudeCode, wantCursor, wantCodex, wantGemini);
132
+ return;
133
+ }
134
+ output(result, format);
135
+ }
136
+ function printTextOutput(result, claudeCode, cursor, codex = false, gemini = false) {
137
+ const w = (s) => process.stdout.write(s);
138
+ w("\nMemRosetta initialized successfully.\n\n");
139
+ w(" What was set up:\n");
140
+ w(" ----------------------------------------\n");
141
+ w(` Database: ${result.database.path}`);
142
+ w(result.database.created ? " (created)\n" : " (already exists)\n");
143
+ w(` MCP Server: ${result.integrations.mcp.path} (always included)
144
+ `);
145
+ const currentConfig = getConfig();
146
+ if (currentConfig.embeddingPreset && currentConfig.embeddingPreset !== "en") {
147
+ const presetLabels = {
148
+ multilingual: "multilingual (multilingual-e5-small)",
149
+ ko: "Korean (ko-sroberta-multitask)"
150
+ };
151
+ w(` Embeddings: ${presetLabels[currentConfig.embeddingPreset] ?? currentConfig.embeddingPreset}
152
+ `);
153
+ }
154
+ if (claudeCode) {
155
+ const cc = result.integrations.claudeCode;
156
+ if (cc.hooks) {
157
+ w(" Stop Hook: ~/.claude/settings.json (auto-save on session end)\n");
158
+ } else if (!isClaudeCodeInstalled()) {
159
+ w(" Stop Hook: SKIPPED (Claude Code not found at ~/.claude)\n");
160
+ w(' Install Claude Code first, then run "memrosetta init --claude-code" again.\n');
161
+ }
162
+ if (cc.claudeMd) {
163
+ w(" CLAUDE.md: ~/.claude/CLAUDE.md (memory instructions added)\n");
164
+ } else {
165
+ w(" CLAUDE.md: already configured\n");
166
+ }
167
+ }
168
+ if (cursor) {
169
+ w(` Cursor MCP: ${result.integrations.cursor.path}
170
+ `);
171
+ if (result.integrations.cursor.cursorRules) {
172
+ w(` .cursorrules: ${result.integrations.cursor.cursorRulesPath} (memory instructions added)
173
+ `);
174
+ } else {
175
+ w(" .cursorrules: already configured\n");
176
+ }
177
+ }
178
+ if (codex) {
179
+ w(` Codex MCP: ${result.integrations.codex.path}
180
+ `);
181
+ if (result.integrations.codex.agentsMd) {
182
+ w(` AGENTS.md: ${result.integrations.codex.agentsMdPath} (memory instructions added)
183
+ `);
184
+ } else {
185
+ w(" AGENTS.md: already configured\n");
186
+ }
187
+ }
188
+ if (gemini) {
189
+ w(` Gemini MCP: ${result.integrations.gemini.path}
190
+ `);
191
+ if (result.integrations.gemini.geminiMd) {
192
+ w(` GEMINI.md: ${result.integrations.gemini.geminiMdPath} (memory instructions added)
193
+ `);
194
+ } else {
195
+ w(" GEMINI.md: already configured\n");
196
+ }
197
+ }
198
+ w("\n");
199
+ if (!claudeCode && !cursor && !codex && !gemini) {
200
+ w(" MCP is ready. Add --claude-code, --cursor, --codex, or --gemini for tool-specific setup.\n");
201
+ w(" Example: memrosetta init --claude-code\n");
202
+ w("\n");
203
+ }
204
+ if (claudeCode) {
205
+ w(" Restart Claude Code to activate.\n\n");
206
+ }
207
+ }
208
+ export {
209
+ run
210
+ };
@@ -0,0 +1,40 @@
1
+ import {
2
+ buildMemoryInvalidatedOp,
3
+ openCliSyncContext
4
+ } from "./chunk-GGXC7TAJ.js";
5
+ import {
6
+ optionalOption
7
+ } from "./chunk-US6CEDMU.js";
8
+ import {
9
+ getEngine,
10
+ resolveDbPath
11
+ } from "./chunk-47SU2YUJ.js";
12
+ import {
13
+ output,
14
+ outputError
15
+ } from "./chunk-ET6TNQOJ.js";
16
+ import "./chunk-WYHEAKPC.js";
17
+
18
+ // src/commands/invalidate.ts
19
+ async function run(options) {
20
+ const { args, format, db, noEmbeddings } = options;
21
+ const memoryId = args.find((a) => !a.startsWith("-"));
22
+ if (!memoryId) {
23
+ outputError("Usage: memrosetta invalidate <memoryId>", format);
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const reason = optionalOption(args, "--reason");
28
+ const engine = await getEngine({ db, noEmbeddings });
29
+ const now = (/* @__PURE__ */ new Date()).toISOString();
30
+ await engine.invalidate(memoryId, reason);
31
+ const sync = await openCliSyncContext(resolveDbPath(db));
32
+ if (sync.enabled) {
33
+ sync.enqueue(buildMemoryInvalidatedOp(sync, memoryId, now, reason));
34
+ sync.close();
35
+ }
36
+ output({ memoryId, invalidated: true }, format);
37
+ }
38
+ export {
39
+ run
40
+ };
@@ -0,0 +1,40 @@
1
+ import {
2
+ buildMemoryInvalidatedOp,
3
+ openCliSyncContext
4
+ } from "./chunk-GZINXXM4.js";
5
+ import {
6
+ optionalOption
7
+ } from "./chunk-US6CEDMU.js";
8
+ import {
9
+ getEngine,
10
+ resolveDbPath
11
+ } from "./chunk-VMGX5FCY.js";
12
+ import {
13
+ output,
14
+ outputError
15
+ } from "./chunk-ET6TNQOJ.js";
16
+ import "./chunk-RZFCVYTK.js";
17
+
18
+ // src/commands/invalidate.ts
19
+ async function run(options) {
20
+ const { args, format, db, noEmbeddings } = options;
21
+ const memoryId = args.find((a) => !a.startsWith("-"));
22
+ if (!memoryId) {
23
+ outputError("Usage: memrosetta invalidate <memoryId>", format);
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const reason = optionalOption(args, "--reason");
28
+ const engine = await getEngine({ db, noEmbeddings });
29
+ const now = (/* @__PURE__ */ new Date()).toISOString();
30
+ await engine.invalidate(memoryId, reason);
31
+ const sync = await openCliSyncContext(resolveDbPath(db));
32
+ if (sync.enabled) {
33
+ sync.enqueue(buildMemoryInvalidatedOp(sync, memoryId, now, reason));
34
+ sync.close();
35
+ }
36
+ output({ memoryId, invalidated: true }, format);
37
+ }
38
+ export {
39
+ run
40
+ };
@@ -0,0 +1,37 @@
1
+ import {
2
+ optionalOption
3
+ } from "./chunk-US6CEDMU.js";
4
+ import {
5
+ getEngine
6
+ } from "./chunk-VMGX5FCY.js";
7
+ import {
8
+ output
9
+ } from "./chunk-ET6TNQOJ.js";
10
+ import {
11
+ getDefaultUserId
12
+ } from "./chunk-RZFCVYTK.js";
13
+
14
+ // src/commands/maintain.ts
15
+ async function run(options) {
16
+ const { args, format, db, noEmbeddings } = options;
17
+ const userId = optionalOption(args, "--user") ?? getDefaultUserId();
18
+ const engine = await getEngine({ db, noEmbeddings });
19
+ const result = await engine.maintain(userId);
20
+ if (format === "text") {
21
+ process.stdout.write(`Maintenance completed for user: ${userId}
22
+ `);
23
+ process.stdout.write(` Activation scores updated: ${result.activationUpdated}
24
+ `);
25
+ process.stdout.write(` Tiers updated: ${result.tiersUpdated}
26
+ `);
27
+ process.stdout.write(` Groups compressed: ${result.compressed}
28
+ `);
29
+ process.stdout.write(` Memories archived: ${result.removed}
30
+ `);
31
+ return;
32
+ }
33
+ output({ userId, ...result }, format);
34
+ }
35
+ export {
36
+ run
37
+ };
@@ -0,0 +1,37 @@
1
+ import {
2
+ optionalOption
3
+ } from "./chunk-US6CEDMU.js";
4
+ import {
5
+ getEngine
6
+ } from "./chunk-47SU2YUJ.js";
7
+ import {
8
+ output
9
+ } from "./chunk-ET6TNQOJ.js";
10
+ import {
11
+ getDefaultUserId
12
+ } from "./chunk-WYHEAKPC.js";
13
+
14
+ // src/commands/maintain.ts
15
+ async function run(options) {
16
+ const { args, format, db, noEmbeddings } = options;
17
+ const userId = optionalOption(args, "--user") ?? getDefaultUserId();
18
+ const engine = await getEngine({ db, noEmbeddings });
19
+ const result = await engine.maintain(userId);
20
+ if (format === "text") {
21
+ process.stdout.write(`Maintenance completed for user: ${userId}
22
+ `);
23
+ process.stdout.write(` Activation scores updated: ${result.activationUpdated}
24
+ `);
25
+ process.stdout.write(` Tiers updated: ${result.tiersUpdated}
26
+ `);
27
+ process.stdout.write(` Groups compressed: ${result.compressed}
28
+ `);
29
+ process.stdout.write(` Memories archived: ${result.removed}
30
+ `);
31
+ return;
32
+ }
33
+ output({ userId, ...result }, format);
34
+ }
35
+ export {
36
+ run
37
+ };
@@ -0,0 +1,255 @@
1
+ import {
2
+ hasFlag,
3
+ optionalOption
4
+ } from "./chunk-US6CEDMU.js";
5
+ import {
6
+ resolveDbPath
7
+ } from "./chunk-47SU2YUJ.js";
8
+ import {
9
+ output,
10
+ outputError
11
+ } from "./chunk-ET6TNQOJ.js";
12
+ import {
13
+ resolveCanonicalUserId
14
+ } from "./chunk-WYHEAKPC.js";
15
+
16
+ // src/commands/migrate.ts
17
+ import { createInterface } from "readline";
18
+ var MIGRATION_NAME = "legacy-user-id-to-canonical-v1";
19
+ function scanLegacyImpact(db, canonicalUserId) {
20
+ const totalRows = db.prepare("SELECT COUNT(*) AS c FROM memories").get().c;
21
+ const legacyRows = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE user_id != ?").get(canonicalUserId).c;
22
+ const breakdownRows = db.prepare(
23
+ `SELECT user_id AS legacyUserId, COUNT(*) AS rows, COUNT(DISTINCT namespace) AS distinctNamespaces
24
+ FROM memories
25
+ WHERE user_id != ?
26
+ GROUP BY user_id
27
+ ORDER BY rows DESC`
28
+ ).all(canonicalUserId);
29
+ const queuePending = hasTable(db, "sync_outbox") ? db.prepare("SELECT COUNT(*) AS c FROM sync_outbox WHERE pushed_at IS NULL").get().c : 0;
30
+ const crossPartitionDuplicateGroups = db.prepare(
31
+ `WITH x AS (
32
+ SELECT content, COUNT(DISTINCT user_id) AS u
33
+ FROM memories
34
+ GROUP BY content
35
+ )
36
+ SELECT COUNT(*) AS c FROM x WHERE u > 1`
37
+ ).get().c;
38
+ const alreadyMigrated = hasTable(db, "migration_version") ? Boolean(
39
+ db.prepare("SELECT 1 FROM migration_version WHERE name = ?").get(MIGRATION_NAME)
40
+ ) : false;
41
+ return {
42
+ canonicalUserId,
43
+ totalRows,
44
+ legacyRows,
45
+ distinctLegacyUserIds: breakdownRows.length,
46
+ breakdown: breakdownRows,
47
+ queuePending,
48
+ crossPartitionDuplicateGroups,
49
+ alreadyMigrated
50
+ };
51
+ }
52
+ function hasTable(db, name) {
53
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(name);
54
+ return Boolean(row);
55
+ }
56
+ function runLegacyUserIdMigration(db, canonicalUserId) {
57
+ const run2 = db.transaction(() => {
58
+ const insert = db.prepare(
59
+ `INSERT OR IGNORE INTO memory_legacy_scope (
60
+ memory_id, legacy_user_id, legacy_namespace, migrated_at
61
+ )
62
+ SELECT memory_id, user_id, namespace, CURRENT_TIMESTAMP
63
+ FROM memories
64
+ WHERE user_id != ?`
65
+ );
66
+ const insertInfo = insert.run(canonicalUserId);
67
+ const legacyScopeRows = insertInfo.changes;
68
+ const update = db.prepare(
69
+ "UPDATE memories SET user_id = ? WHERE user_id != ?"
70
+ );
71
+ const updateInfo = update.run(canonicalUserId, canonicalUserId);
72
+ const movedRows = updateInfo.changes;
73
+ let outboxCleared = 0;
74
+ let inboxCleared = 0;
75
+ if (hasTable(db, "sync_outbox")) {
76
+ outboxCleared = db.prepare("DELETE FROM sync_outbox").run().changes;
77
+ }
78
+ if (hasTable(db, "sync_inbox")) {
79
+ inboxCleared = db.prepare("DELETE FROM sync_inbox").run().changes;
80
+ }
81
+ let cursorReset = false;
82
+ if (hasTable(db, "sync_state")) {
83
+ const r = db.prepare(
84
+ `DELETE FROM sync_state WHERE key IN (
85
+ 'last_cursor',
86
+ 'pull_cursor',
87
+ 'last_push_attempt_at',
88
+ 'last_push_success_at',
89
+ 'last_pull_attempt_at',
90
+ 'last_pull_success_at'
91
+ )`
92
+ ).run();
93
+ cursorReset = r.changes > 0;
94
+ }
95
+ db.prepare(
96
+ `INSERT OR IGNORE INTO migration_version (name, applied_at)
97
+ VALUES (?, CURRENT_TIMESTAMP)`
98
+ ).run(MIGRATION_NAME);
99
+ return {
100
+ movedRows,
101
+ legacyScopeRows,
102
+ outboxCleared,
103
+ inboxCleared,
104
+ cursorReset
105
+ };
106
+ });
107
+ return run2();
108
+ }
109
+ async function confirmInteractive(question) {
110
+ if (!process.stdin.isTTY) return false;
111
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
112
+ try {
113
+ const answer = await new Promise((resolve) => {
114
+ rl.question(`${question} [y/N] `, (a) => resolve(a));
115
+ });
116
+ return /^y(es)?$/i.test(answer.trim());
117
+ } finally {
118
+ rl.close();
119
+ }
120
+ }
121
+ async function run(options) {
122
+ const { args, format, db: dbOverride } = options;
123
+ const sub = args[0];
124
+ if (sub !== "legacy-user-ids") {
125
+ outputError(
126
+ "Usage: memrosetta migrate legacy-user-ids [--dry-run] [--canonical <user>] [--yes]",
127
+ format
128
+ );
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ const sliced = args.slice(1);
133
+ const dryRun = hasFlag(sliced, "--dry-run");
134
+ const autoYes = hasFlag(sliced, "--yes") || hasFlag(sliced, "-y");
135
+ const canonicalOverride = optionalOption(sliced, "--canonical");
136
+ const canonicalUserId = resolveCanonicalUserId(canonicalOverride ?? null);
137
+ const dbPath = resolveDbPath(dbOverride);
138
+ const { default: Database } = await import("better-sqlite3");
139
+ const db = new Database(dbPath);
140
+ try {
141
+ const { ensureSchema } = await import("@memrosetta/core");
142
+ ensureSchema(db, { vectorEnabled: false });
143
+ const report = scanLegacyImpact(db, canonicalUserId);
144
+ if (report.alreadyMigrated && report.legacyRows === 0) {
145
+ output(
146
+ {
147
+ status: "noop",
148
+ reason: `migration ${MIGRATION_NAME} already applied and no legacy rows remain`,
149
+ report
150
+ },
151
+ format
152
+ );
153
+ return;
154
+ }
155
+ if (report.legacyRows === 0) {
156
+ output(
157
+ {
158
+ status: "noop",
159
+ reason: "no legacy user_id partitions found",
160
+ report
161
+ },
162
+ format
163
+ );
164
+ return;
165
+ }
166
+ if (dryRun) {
167
+ output(
168
+ {
169
+ status: "dry-run",
170
+ canonicalUserId,
171
+ report,
172
+ wouldClear: {
173
+ syncOutbox: true,
174
+ syncInbox: true,
175
+ cursorState: true
176
+ },
177
+ nextSteps: [
178
+ "Run without --dry-run to apply the migration.",
179
+ "After migration: `memrosetta sync backfill` then `memrosetta sync now`."
180
+ ]
181
+ },
182
+ format
183
+ );
184
+ return;
185
+ }
186
+ if (!autoYes) {
187
+ printImpactPreview(report, canonicalUserId);
188
+ const ok = await confirmInteractive(
189
+ `Apply migration and move ${report.legacyRows} row(s) onto '${canonicalUserId}'?`
190
+ );
191
+ if (!ok) {
192
+ output(
193
+ {
194
+ status: "aborted",
195
+ reason: "user declined or non-interactive session (pass --yes to skip prompt)",
196
+ report
197
+ },
198
+ format
199
+ );
200
+ return;
201
+ }
202
+ }
203
+ const result = runLegacyUserIdMigration(db, canonicalUserId);
204
+ output(
205
+ {
206
+ status: "applied",
207
+ canonicalUserId,
208
+ migration: MIGRATION_NAME,
209
+ result,
210
+ nextSteps: [
211
+ `Run \`memrosetta sync backfill --user ${canonicalUserId}\` to republish memories onto the canonical partition.`,
212
+ "Then `memrosetta sync now` to push them to the hub.",
213
+ "Run `memrosetta duplicates report` to audit cross-partition duplicates before any future dedupe pass."
214
+ ]
215
+ },
216
+ format
217
+ );
218
+ } finally {
219
+ db.close();
220
+ }
221
+ }
222
+ function printImpactPreview(report, canonicalUserId) {
223
+ process.stderr.write(
224
+ [
225
+ "",
226
+ "Migration impact preview",
227
+ "------------------------",
228
+ ` canonical user : ${canonicalUserId}`,
229
+ ` total memories : ${report.totalRows}`,
230
+ ` legacy rows to move : ${report.legacyRows}`,
231
+ ` distinct legacy partitions: ${report.distinctLegacyUserIds}`,
232
+ ` sync_outbox pending : ${report.queuePending}`,
233
+ ` cross-partition dup groups: ${report.crossPartitionDuplicateGroups}`,
234
+ "",
235
+ "Top legacy partitions:",
236
+ ...report.breakdown.slice(0, 10).map(
237
+ (r) => ` - ${r.legacyUserId.padEnd(40)} rows=${r.rows} namespaces=${r.distinctNamespaces}`
238
+ ),
239
+ "",
240
+ "This will:",
241
+ " * copy legacy rows into memory_legacy_scope (non-destructive)",
242
+ " * rewrite memories.user_id to the canonical user",
243
+ " * leave memories.namespace untouched",
244
+ " * clear sync_outbox / sync_inbox / sync cursor state",
245
+ "",
246
+ "Back up ~/.memrosetta/memories.db before continuing if you have not already.",
247
+ ""
248
+ ].join("\n")
249
+ );
250
+ }
251
+ export {
252
+ run,
253
+ runLegacyUserIdMigration,
254
+ scanLegacyImpact
255
+ };