om-memory-system 3.0.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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +587 -0
  3. package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
  4. package/dist/adapters/opencode/auto-capture-host.js +29 -0
  5. package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
  6. package/dist/adapters/opencode/auto-capture-summary.js +147 -0
  7. package/dist/adapters/opencode/conversation.d.ts +2 -0
  8. package/dist/adapters/opencode/conversation.js +50 -0
  9. package/dist/adapters/opencode/user-prompt.d.ts +9 -0
  10. package/dist/adapters/opencode/user-prompt.js +26 -0
  11. package/dist/adapters/pi/capture.d.ts +41 -0
  12. package/dist/adapters/pi/capture.js +91 -0
  13. package/dist/adapters/pi/conversation.d.ts +38 -0
  14. package/dist/adapters/pi/conversation.js +118 -0
  15. package/dist/adapters/pi/extension.d.ts +18 -0
  16. package/dist/adapters/pi/extension.js +234 -0
  17. package/dist/adapters/pi/import-command.d.ts +29 -0
  18. package/dist/adapters/pi/import-command.js +232 -0
  19. package/dist/adapters/pi/profile.d.ts +39 -0
  20. package/dist/adapters/pi/profile.js +148 -0
  21. package/dist/adapters/pi/provider.d.ts +50 -0
  22. package/dist/adapters/pi/provider.js +77 -0
  23. package/dist/config.d.ts +290 -0
  24. package/dist/config.js +742 -0
  25. package/dist/core/capture-context.d.ts +4 -0
  26. package/dist/core/capture-context.js +132 -0
  27. package/dist/core/capture.d.ts +15 -0
  28. package/dist/core/capture.js +68 -0
  29. package/dist/core/extraction.d.ts +80 -0
  30. package/dist/core/extraction.js +140 -0
  31. package/dist/core/host.d.ts +52 -0
  32. package/dist/core/host.js +1 -0
  33. package/dist/core/memory-operations.d.ts +27 -0
  34. package/dist/core/memory-operations.js +247 -0
  35. package/dist/core/retrieval.d.ts +12 -0
  36. package/dist/core/retrieval.js +72 -0
  37. package/dist/importer/discovery.d.ts +31 -0
  38. package/dist/importer/discovery.js +88 -0
  39. package/dist/importer/importer.d.ts +87 -0
  40. package/dist/importer/importer.js +260 -0
  41. package/dist/importer/ledger.d.ts +44 -0
  42. package/dist/importer/ledger.js +137 -0
  43. package/dist/importer/session-loader.d.ts +19 -0
  44. package/dist/importer/session-loader.js +12 -0
  45. package/dist/index.d.ts +24 -0
  46. package/dist/index.js +601 -0
  47. package/dist/plugin.d.ts +9 -0
  48. package/dist/plugin.js +11 -0
  49. package/dist/services/ai/ai-provider-factory.d.ts +7 -0
  50. package/dist/services/ai/ai-provider-factory.js +41 -0
  51. package/dist/services/ai/api-key-placeholder.d.ts +1 -0
  52. package/dist/services/ai/api-key-placeholder.js +14 -0
  53. package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
  54. package/dist/services/ai/internal-capture-sessions.js +33 -0
  55. package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
  56. package/dist/services/ai/opencode-diagnostics.js +36 -0
  57. package/dist/services/ai/opencode-host-config.d.ts +10 -0
  58. package/dist/services/ai/opencode-host-config.js +51 -0
  59. package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
  60. package/dist/services/ai/opencode-provider-loader.js +4 -0
  61. package/dist/services/ai/opencode-provider.d.ts +92 -0
  62. package/dist/services/ai/opencode-provider.js +419 -0
  63. package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
  64. package/dist/services/ai/opencode-sdk-client.js +38 -0
  65. package/dist/services/ai/profile-llm-client.d.ts +2 -0
  66. package/dist/services/ai/profile-llm-client.js +27 -0
  67. package/dist/services/ai/provider-config.d.ts +17 -0
  68. package/dist/services/ai/provider-config.js +30 -0
  69. package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
  70. package/dist/services/ai/providers/anthropic-messages.js +220 -0
  71. package/dist/services/ai/providers/base-provider.d.ts +24 -0
  72. package/dist/services/ai/providers/base-provider.js +24 -0
  73. package/dist/services/ai/providers/google-gemini.d.ts +15 -0
  74. package/dist/services/ai/providers/google-gemini.js +228 -0
  75. package/dist/services/ai/providers/minimax.d.ts +35 -0
  76. package/dist/services/ai/providers/minimax.js +55 -0
  77. package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
  78. package/dist/services/ai/providers/openai-chat-completion.js +387 -0
  79. package/dist/services/ai/providers/openai-responses.d.ts +13 -0
  80. package/dist/services/ai/providers/openai-responses.js +182 -0
  81. package/dist/services/ai/providers/orcarouter.d.ts +51 -0
  82. package/dist/services/ai/providers/orcarouter.js +66 -0
  83. package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
  84. package/dist/services/ai/session/ai-session-manager.js +211 -0
  85. package/dist/services/ai/session/session-types.d.ts +42 -0
  86. package/dist/services/ai/session/session-types.js +1 -0
  87. package/dist/services/ai/tools/tool-schema.d.ts +40 -0
  88. package/dist/services/ai/tools/tool-schema.js +24 -0
  89. package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
  90. package/dist/services/ai/validators/user-profile-validator.js +114 -0
  91. package/dist/services/api-handlers.d.ts +186 -0
  92. package/dist/services/api-handlers.js +1284 -0
  93. package/dist/services/auth-token.d.ts +14 -0
  94. package/dist/services/auth-token.js +59 -0
  95. package/dist/services/auto-capture.d.ts +3 -0
  96. package/dist/services/auto-capture.js +127 -0
  97. package/dist/services/cleanup-service.d.ts +22 -0
  98. package/dist/services/cleanup-service.js +109 -0
  99. package/dist/services/client.d.ts +134 -0
  100. package/dist/services/client.js +327 -0
  101. package/dist/services/context.d.ts +10 -0
  102. package/dist/services/context.js +27 -0
  103. package/dist/services/cors.d.ts +12 -0
  104. package/dist/services/cors.js +52 -0
  105. package/dist/services/deduplication-service.d.ts +28 -0
  106. package/dist/services/deduplication-service.js +111 -0
  107. package/dist/services/embedding.d.ts +36 -0
  108. package/dist/services/embedding.js +200 -0
  109. package/dist/services/jsonc.d.ts +6 -0
  110. package/dist/services/jsonc.js +76 -0
  111. package/dist/services/language-detector.d.ts +2 -0
  112. package/dist/services/language-detector.js +33 -0
  113. package/dist/services/legacy-migration.d.ts +77 -0
  114. package/dist/services/legacy-migration.js +343 -0
  115. package/dist/services/logger.d.ts +1 -0
  116. package/dist/services/logger.js +119 -0
  117. package/dist/services/memory-portability-service.d.ts +37 -0
  118. package/dist/services/memory-portability-service.js +340 -0
  119. package/dist/services/memory-scope.d.ts +16 -0
  120. package/dist/services/memory-scope.js +43 -0
  121. package/dist/services/migration-service.d.ts +42 -0
  122. package/dist/services/migration-service.js +307 -0
  123. package/dist/services/onnxruntime-resolve.d.ts +24 -0
  124. package/dist/services/onnxruntime-resolve.js +174 -0
  125. package/dist/services/privacy.d.ts +26 -0
  126. package/dist/services/privacy.js +69 -0
  127. package/dist/services/request-timeouts.d.ts +9 -0
  128. package/dist/services/request-timeouts.js +11 -0
  129. package/dist/services/runtime-require.d.ts +17 -0
  130. package/dist/services/runtime-require.js +75 -0
  131. package/dist/services/secret-resolver.d.ts +1 -0
  132. package/dist/services/secret-resolver.js +65 -0
  133. package/dist/services/shard-inventory-service.d.ts +69 -0
  134. package/dist/services/shard-inventory-service.js +315 -0
  135. package/dist/services/shard-path-migration-service.d.ts +31 -0
  136. package/dist/services/shard-path-migration-service.js +367 -0
  137. package/dist/services/tag-prefix-migration.d.ts +60 -0
  138. package/dist/services/tag-prefix-migration.js +439 -0
  139. package/dist/services/tag-prefix-scan.d.ts +26 -0
  140. package/dist/services/tag-prefix-scan.js +91 -0
  141. package/dist/services/tags.d.ts +29 -0
  142. package/dist/services/tags.js +251 -0
  143. package/dist/services/turso/connection-manager.d.ts +15 -0
  144. package/dist/services/turso/connection-manager.js +163 -0
  145. package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
  146. package/dist/services/turso/cross-process-write-lock.js +89 -0
  147. package/dist/services/turso/legacy-migrator.d.ts +1 -0
  148. package/dist/services/turso/legacy-migrator.js +540 -0
  149. package/dist/services/turso/lifecycle.d.ts +1 -0
  150. package/dist/services/turso/lifecycle.js +14 -0
  151. package/dist/services/turso/operation-lock.d.ts +2 -0
  152. package/dist/services/turso/operation-lock.js +61 -0
  153. package/dist/services/turso/ready.d.ts +2 -0
  154. package/dist/services/turso/ready.js +36 -0
  155. package/dist/services/turso/shard-manager.d.ts +38 -0
  156. package/dist/services/turso/shard-manager.js +496 -0
  157. package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
  158. package/dist/services/turso/sqlite-handle-release.js +56 -0
  159. package/dist/services/turso/turso-db.d.ts +18 -0
  160. package/dist/services/turso/turso-db.js +53 -0
  161. package/dist/services/turso/types.d.ts +42 -0
  162. package/dist/services/turso/types.js +1 -0
  163. package/dist/services/turso/vector-search.d.ts +41 -0
  164. package/dist/services/turso/vector-search.js +384 -0
  165. package/dist/services/turso/vector-utils.d.ts +7 -0
  166. package/dist/services/turso/vector-utils.js +67 -0
  167. package/dist/services/user-memory-learning.d.ts +97 -0
  168. package/dist/services/user-memory-learning.js +673 -0
  169. package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
  170. package/dist/services/user-profile/ai-cleanup.js +486 -0
  171. package/dist/services/user-profile/profile-context.d.ts +1 -0
  172. package/dist/services/user-profile/profile-context.js +105 -0
  173. package/dist/services/user-profile/profile-utils.d.ts +2 -0
  174. package/dist/services/user-profile/profile-utils.js +45 -0
  175. package/dist/services/user-profile/types.d.ts +92 -0
  176. package/dist/services/user-profile/types.js +1 -0
  177. package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
  178. package/dist/services/user-profile/user-profile-manager.js +1660 -0
  179. package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
  180. package/dist/services/user-prompt/user-prompt-manager.js +303 -0
  181. package/dist/services/web-api-auth.d.ts +3 -0
  182. package/dist/services/web-api-auth.js +25 -0
  183. package/dist/services/web-auth.d.ts +46 -0
  184. package/dist/services/web-auth.js +105 -0
  185. package/dist/services/web-server.d.ts +57 -0
  186. package/dist/services/web-server.js +659 -0
  187. package/dist/shared/api/index.d.ts +4 -0
  188. package/dist/shared/api/index.js +2 -0
  189. package/dist/shared/api/portability-schemas.d.ts +56 -0
  190. package/dist/shared/api/portability-schemas.js +39 -0
  191. package/dist/shared/api/schemas.d.ts +295 -0
  192. package/dist/shared/api/schemas.js +134 -0
  193. package/dist/shared/api/types.d.ts +28 -0
  194. package/dist/shared/api/types.js +1 -0
  195. package/dist/types/index.d.ts +27 -0
  196. package/dist/types/index.js +1 -0
  197. package/dist/utils/context-limit.d.ts +10 -0
  198. package/dist/utils/context-limit.js +43 -0
  199. package/dist/utils/math.d.ts +7 -0
  200. package/dist/utils/math.js +48 -0
  201. package/dist/utils/profile.d.ts +2 -0
  202. package/dist/utils/profile.js +15 -0
  203. package/dist/v2/adapter.d.ts +5 -0
  204. package/dist/v2/adapter.js +218 -0
  205. package/dist/v2/legacy-client.d.ts +15 -0
  206. package/dist/v2/legacy-client.js +257 -0
  207. package/dist/v2/memory-bridge.d.ts +18 -0
  208. package/dist/v2/memory-bridge.js +29 -0
  209. package/dist/v2/plugin.d.ts +3 -0
  210. package/dist/v2/plugin.js +24 -0
  211. package/dist/web/assets/index-DPjEpD1h.css +2 -0
  212. package/dist/web/assets/index-do26c4JJ.js +122 -0
  213. package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  214. package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  215. package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  216. package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  217. package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  218. package/dist/web/favicon.ico +0 -0
  219. package/dist/web/index.html +14 -0
  220. package/dist/web/omms-icon.png +0 -0
  221. package/package.json +138 -0
@@ -0,0 +1,1284 @@
1
+ import { embeddingService } from "./embedding.js";
2
+ import { tursoShardManager } from "./turso/shard-manager.js";
3
+ import { tursoVectorSearch } from "./turso/vector-search.js";
4
+ import { tursoConnectionManager } from "./turso/connection-manager.js";
5
+ import { ensureTursoReady } from "./turso/ready.js";
6
+ import { formatTagsForEmbedding } from "./turso/vector-utils.js";
7
+ import { extractScopeFromContainerTag, tryExtractScopeFromContainerTag } from "./memory-scope.js";
8
+ import { log } from "./logger.js";
9
+ import { CONFIG } from "../config.js";
10
+ import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
11
+ import { sortProfileItems } from "../utils/profile.js";
12
+ async function getAllMemoryShards() {
13
+ await ensureTursoReady();
14
+ const projectShards = await tursoShardManager.getAllShards("project", "");
15
+ const userShards = await tursoShardManager.getAllShards("user", "");
16
+ return [...projectShards, ...userShards];
17
+ }
18
+ function safeToISOString(timestamp) {
19
+ try {
20
+ if (timestamp === null || timestamp === undefined) {
21
+ return new Date().toISOString();
22
+ }
23
+ const numValue = typeof timestamp === "bigint" ? Number(timestamp) : Number(timestamp);
24
+ if (isNaN(numValue) || numValue < 0) {
25
+ return new Date().toISOString();
26
+ }
27
+ return new Date(numValue).toISOString();
28
+ }
29
+ catch {
30
+ return new Date().toISOString();
31
+ }
32
+ }
33
+ function safeJSONParse(jsonString) {
34
+ if (!jsonString || typeof jsonString !== "string") {
35
+ return undefined;
36
+ }
37
+ try {
38
+ return JSON.parse(jsonString);
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ function extractScopeFromTag(tag) {
45
+ return extractScopeFromContainerTag(tag);
46
+ }
47
+ function getProjectPathFromTag(tag) {
48
+ return (async () => {
49
+ const shards = await getAllMemoryShards();
50
+ for (const shard of shards) {
51
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
52
+ const tags = await tursoVectorSearch.getDistinctTags(db);
53
+ for (const t of tags) {
54
+ if (t.container_tag === tag && t.project_path) {
55
+ return String(t.project_path);
56
+ }
57
+ }
58
+ }
59
+ return undefined;
60
+ })();
61
+ }
62
+ export async function handleListTags() {
63
+ try {
64
+ await ensureTursoReady();
65
+ // Tags are stored as SQLite metadata; embedding model is not needed.
66
+ // Calling warmup() here would block on local transformer init in the worker
67
+ // thread and hang every read API. Only handlers that compute similarity
68
+ // (e.g. handleSearch) should warm up the embedding service.
69
+ const projectShards = await tursoShardManager.getAllShards("project", "");
70
+ const tagsMap = new Map();
71
+ for (const shard of projectShards) {
72
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
73
+ const tags = await tursoVectorSearch.getDistinctTags(db);
74
+ for (const t of tags) {
75
+ if (t.container_tag && !tagsMap.has(String(t.container_tag))) {
76
+ tagsMap.set(String(t.container_tag), {
77
+ tag: String(t.container_tag),
78
+ displayName: t.display_name ? String(t.display_name) : undefined,
79
+ userName: t.user_name ? String(t.user_name) : undefined,
80
+ userEmail: t.user_email ? String(t.user_email) : undefined,
81
+ projectPath: t.project_path ? String(t.project_path) : undefined,
82
+ projectName: t.project_name ? String(t.project_name) : undefined,
83
+ gitRepoUrl: t.git_repo_url ? String(t.git_repo_url) : undefined,
84
+ });
85
+ }
86
+ }
87
+ }
88
+ const projectTags = [];
89
+ for (const tagInfo of tagsMap.values()) {
90
+ if (tagInfo.tag.includes("_project_")) {
91
+ projectTags.push(tagInfo);
92
+ }
93
+ }
94
+ return { success: true, data: { project: projectTags } };
95
+ }
96
+ catch (error) {
97
+ log("handleListTags: error", { error: String(error) });
98
+ return { success: false, error: String(error) };
99
+ }
100
+ }
101
+ export async function handleListMemories(tag, page = 1, pageSize = 20, includePrompts = true) {
102
+ try {
103
+ await ensureTursoReady();
104
+ // Listing only reads SQLite rows; no vector ops happen here.
105
+ // See handleListTags comment - keep embedding init out of read paths.
106
+ const allMemories = [];
107
+ if (tag) {
108
+ const { scope: tagScope, hash } = extractScopeFromTag(tag);
109
+ const shards = await tursoShardManager.getAllShards(tagScope, hash);
110
+ for (const shard of shards) {
111
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
112
+ const memories = await tursoVectorSearch.listMemories(db, tag, 10000);
113
+ allMemories.push(...memories);
114
+ }
115
+ }
116
+ else {
117
+ // Iterate both project- and user-scoped shards. Previously this only
118
+ // walked project shards, which silently hid user-scope memories from the
119
+ // listing endpoint (Web UI navigation, /api/memories without a tag
120
+ // filter, …). User-scope memories still showed up in /api/search and
121
+ // /api/stats `byType`, but were invisible in /api/stats `byScope` and
122
+ // unbrowseable in the UI — a confusing UX gap. The filter keeps the
123
+ // defense-in-depth check on container_tag, just widens it to both
124
+ // canonical scope markers.
125
+ const projectShards = await tursoShardManager.getAllShards("project", "");
126
+ const userShards = await tursoShardManager.getAllShards("user", "");
127
+ for (const shard of [...projectShards, ...userShards]) {
128
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
129
+ const memories = await tursoVectorSearch.getAllMemories(db);
130
+ allMemories.push(...memories.filter((m) => m.container_tag?.includes("_project_") || m.container_tag?.includes("_user_")));
131
+ }
132
+ }
133
+ const memoriesWithType = allMemories.map((r) => {
134
+ const metadata = safeJSONParse(r.metadata);
135
+ return {
136
+ type: "memory",
137
+ id: r.id,
138
+ content: r.content,
139
+ memoryType: r.type,
140
+ tags: r.tags ? r.tags.split(",").map((t) => t.trim()) : [],
141
+ createdAt: Number(r.created_at),
142
+ updatedAt: r.updated_at ? Number(r.updated_at) : undefined,
143
+ metadata,
144
+ linkedPromptId: metadata?.promptId,
145
+ displayName: r.display_name,
146
+ userName: r.user_name,
147
+ userEmail: r.user_email,
148
+ projectPath: r.project_path,
149
+ projectName: r.project_name,
150
+ gitRepoUrl: r.git_repo_url,
151
+ isPinned: r.is_pinned === 1,
152
+ };
153
+ });
154
+ let timeline = memoriesWithType;
155
+ if (includePrompts) {
156
+ const projectPath = tag ? await getProjectPathFromTag(tag) : undefined;
157
+ const prompts = await userPromptManager.getCapturedPrompts(projectPath);
158
+ const promptsWithType = prompts.map((p) => ({
159
+ type: "prompt",
160
+ id: p.id,
161
+ sessionId: p.sessionId,
162
+ content: p.content,
163
+ createdAt: p.createdAt,
164
+ projectPath: p.projectPath,
165
+ linkedMemoryId: p.linkedMemoryId,
166
+ }));
167
+ timeline = [...memoriesWithType, ...promptsWithType];
168
+ }
169
+ const linkedPairs = new Map();
170
+ const standalone = [];
171
+ for (const item of timeline) {
172
+ if (item.type === "memory" && item.linkedPromptId) {
173
+ if (!linkedPairs.has(item.linkedPromptId)) {
174
+ linkedPairs.set(item.linkedPromptId, { memory: item, prompt: null });
175
+ }
176
+ else {
177
+ linkedPairs.get(item.linkedPromptId).memory = item;
178
+ }
179
+ }
180
+ else if (item.type === "prompt" && item.linkedMemoryId) {
181
+ if (!linkedPairs.has(item.id)) {
182
+ linkedPairs.set(item.id, { memory: null, prompt: item });
183
+ }
184
+ else {
185
+ linkedPairs.get(item.id).prompt = item;
186
+ }
187
+ }
188
+ else {
189
+ standalone.push(item);
190
+ }
191
+ }
192
+ const sortedTimeline = [];
193
+ const pairValues = Array.from(linkedPairs.values());
194
+ const pairs = pairValues
195
+ .filter((p) => p.memory && p.prompt)
196
+ .sort((a, b) => b.memory.createdAt - a.memory.createdAt);
197
+ // A memory or prompt whose counterpart is missing (linked prompt deleted,
198
+ // or prompt capture off) must still show up in the timeline, unlinked.
199
+ for (const pair of pairValues) {
200
+ if (pair.memory && !pair.prompt)
201
+ standalone.push(pair.memory);
202
+ else if (pair.prompt && !pair.memory)
203
+ standalone.push(pair.prompt);
204
+ }
205
+ for (const pair of pairs) {
206
+ sortedTimeline.push(pair.memory);
207
+ sortedTimeline.push(pair.prompt);
208
+ }
209
+ standalone.sort((a, b) => b.createdAt - a.createdAt);
210
+ sortedTimeline.push(...standalone);
211
+ timeline = sortedTimeline;
212
+ const total = timeline.length;
213
+ const totalPages = Math.ceil(total / pageSize);
214
+ const offset = (page - 1) * pageSize;
215
+ const paginatedResults = timeline.slice(offset, offset + pageSize);
216
+ const items = paginatedResults.map((item) => {
217
+ if (item.type === "memory") {
218
+ return {
219
+ type: "memory",
220
+ id: item.id,
221
+ content: item.content,
222
+ memoryType: item.memoryType,
223
+ tags: item.tags,
224
+ createdAt: safeToISOString(item.createdAt),
225
+ updatedAt: item.updatedAt ? safeToISOString(item.updatedAt) : undefined,
226
+ metadata: item.metadata,
227
+ linkedPromptId: item.linkedPromptId,
228
+ displayName: item.displayName,
229
+ userName: item.userName,
230
+ userEmail: item.userEmail,
231
+ projectPath: item.projectPath,
232
+ projectName: item.projectName,
233
+ gitRepoUrl: item.gitRepoUrl,
234
+ isPinned: item.isPinned,
235
+ };
236
+ }
237
+ else {
238
+ return {
239
+ type: "prompt",
240
+ id: item.id,
241
+ sessionId: item.sessionId,
242
+ content: item.content,
243
+ createdAt: safeToISOString(item.createdAt),
244
+ projectPath: item.projectPath,
245
+ linkedMemoryId: item.linkedMemoryId,
246
+ };
247
+ }
248
+ });
249
+ return { success: true, data: { items, total, page, pageSize, totalPages } };
250
+ }
251
+ catch (error) {
252
+ log("handleListMemories: error", { error: String(error) });
253
+ return { success: false, error: String(error) };
254
+ }
255
+ }
256
+ export async function handleAddMemory(data) {
257
+ try {
258
+ if (!data.content || !data.containerTag) {
259
+ return { success: false, error: "content and containerTag are required" };
260
+ }
261
+ await ensureTursoReady();
262
+ await embeddingService.warmup();
263
+ const tags = (data.tags || []).map((t) => t.trim().toLowerCase());
264
+ const embeddingInput = tags.length > 0 ? `${data.content}\nTags: ${tags.join(", ")}` : data.content;
265
+ const vector = await embeddingService.embedWithTimeout(embeddingInput, { task: "document" });
266
+ let tagsVector = undefined;
267
+ if (tags.length > 0) {
268
+ tagsVector = await embeddingService.embedWithTimeout(formatTagsForEmbedding(tags), {
269
+ task: "document",
270
+ });
271
+ }
272
+ const { scope, hash } = extractScopeFromTag(data.containerTag);
273
+ return tursoShardManager.withScopeWriteLock(scope, hash, async () => {
274
+ const shard = await tursoShardManager.getWriteShard(scope, hash);
275
+ const id = `mem_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
276
+ const now = Date.now();
277
+ const record = {
278
+ id,
279
+ content: data.content,
280
+ vector,
281
+ tagsVector,
282
+ containerTag: data.containerTag,
283
+ tags: tags.length > 0 ? tags.join(",") : undefined,
284
+ type: data.type,
285
+ createdAt: now,
286
+ updatedAt: now,
287
+ displayName: data.displayName,
288
+ userName: data.userName,
289
+ userEmail: data.userEmail,
290
+ projectPath: data.projectPath,
291
+ projectName: data.projectName,
292
+ gitRepoUrl: data.gitRepoUrl,
293
+ metadata: JSON.stringify({ source: "api" }),
294
+ };
295
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
296
+ await tursoVectorSearch.insertVector(db, record);
297
+ await tursoShardManager.incrementVectorCount(shard.id);
298
+ return { success: true, data: { id } };
299
+ });
300
+ }
301
+ catch (error) {
302
+ log("handleAddMemory: error", { error: String(error) });
303
+ return { success: false, error: String(error) };
304
+ }
305
+ }
306
+ export async function handleDeleteMemory(id, cascade = false) {
307
+ try {
308
+ if (!id)
309
+ return { success: false, error: "id is required" };
310
+ const shards = await getAllMemoryShards();
311
+ for (const shard of shards) {
312
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
313
+ const memory = await tursoVectorSearch.getMemoryById(db, id);
314
+ if (memory) {
315
+ if (cascade) {
316
+ const metadata = safeJSONParse(memory.metadata);
317
+ const linkedPromptId = metadata?.promptId;
318
+ if (linkedPromptId)
319
+ await userPromptManager.deletePrompt(linkedPromptId);
320
+ }
321
+ await tursoVectorSearch.deleteVector(db, id);
322
+ await tursoShardManager.decrementVectorCount(shard.id);
323
+ return {
324
+ success: true,
325
+ data: { deletedPrompt: cascade && !!safeJSONParse(memory.metadata)?.promptId },
326
+ };
327
+ }
328
+ }
329
+ return { success: false, error: "Memory not found" };
330
+ }
331
+ catch (error) {
332
+ log("handleDeleteMemory: error", { error: String(error) });
333
+ return { success: false, error: String(error) };
334
+ }
335
+ }
336
+ export async function handleBulkDelete(ids, cascade = false) {
337
+ try {
338
+ if (!ids || ids.length === 0)
339
+ return { success: false, error: "ids array is required" };
340
+ let deleted = 0;
341
+ for (const id of ids) {
342
+ const result = await handleDeleteMemory(id, cascade);
343
+ if (result.success)
344
+ deleted++;
345
+ }
346
+ return { success: true, data: { deleted } };
347
+ }
348
+ catch (error) {
349
+ log("handleBulkDelete: error", { error: String(error) });
350
+ return { success: false, error: String(error) };
351
+ }
352
+ }
353
+ export async function handleUpdateMemory(id, data) {
354
+ try {
355
+ if (!id)
356
+ return { success: false, error: "id is required" };
357
+ await embeddingService.warmup();
358
+ // Find the existing memory first (read-only — no data modified yet)
359
+ const shards = await getAllMemoryShards();
360
+ let foundShard = null, existingMemory = null;
361
+ for (const shard of shards) {
362
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
363
+ const memory = await tursoVectorSearch.getMemoryById(db, id);
364
+ if (memory) {
365
+ foundShard = shard;
366
+ existingMemory = memory;
367
+ break;
368
+ }
369
+ }
370
+ if (!foundShard || !existingMemory)
371
+ return { success: false, error: "Memory not found" };
372
+ // STEP 1: Generate new embeddings FIRST (safe — no data deleted yet)
373
+ const newContent = data.content || String(existingMemory.content);
374
+ const existingTags = existingMemory.tags ? String(existingMemory.tags) : "";
375
+ const tags = data.tags || (existingTags ? existingTags.split(",").map((t) => t.trim()) : []);
376
+ const vector = await embeddingService.embedWithTimeout(newContent, { task: "document" });
377
+ let tagsVector = undefined;
378
+ if (tags.length > 0) {
379
+ tagsVector = await embeddingService.embedWithTimeout(formatTagsForEmbedding(tags), {
380
+ task: "document",
381
+ });
382
+ }
383
+ const db = await tursoConnectionManager.getConnection(foundShard.dbPath);
384
+ await db.transaction("write", async (tx) => {
385
+ await tx.execute({ sql: `DELETE FROM memories WHERE id = ?`, args: [id] });
386
+ await tx.execute({
387
+ sql: `
388
+ INSERT INTO memories (
389
+ id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
390
+ metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
391
+ ) VALUES (?, ?, vector32(?), ${tagsVector ? "vector32(?)" : "NULL"}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
392
+ `,
393
+ args: [
394
+ id,
395
+ newContent,
396
+ JSON.stringify(Array.from(vector)),
397
+ ...(tagsVector ? [JSON.stringify(Array.from(tagsVector))] : []),
398
+ String(existingMemory.container_tag),
399
+ tags.length > 0 ? tags.join(",") : null,
400
+ data.type || (existingMemory.type ? String(existingMemory.type) : null),
401
+ Number(existingMemory.created_at),
402
+ Date.now(),
403
+ existingMemory.metadata ? String(existingMemory.metadata) : null,
404
+ existingMemory.display_name ? String(existingMemory.display_name) : null,
405
+ existingMemory.user_name ? String(existingMemory.user_name) : null,
406
+ existingMemory.user_email ? String(existingMemory.user_email) : null,
407
+ existingMemory.project_path ? String(existingMemory.project_path) : null,
408
+ existingMemory.project_name ? String(existingMemory.project_name) : null,
409
+ existingMemory.git_repo_url ? String(existingMemory.git_repo_url) : null,
410
+ ],
411
+ });
412
+ });
413
+ return { success: true };
414
+ }
415
+ catch (error) {
416
+ log("handleUpdateMemory: error", { error: String(error) });
417
+ return { success: false, error: String(error) };
418
+ }
419
+ }
420
+ export async function handleSearch(query, tag, page = 1, pageSize = 20) {
421
+ try {
422
+ if (!query)
423
+ return { success: false, error: "query is required" };
424
+ await ensureTursoReady();
425
+ await embeddingService.warmup();
426
+ const queryVector = await embeddingService.embedWithTimeout(query, { task: "query" });
427
+ const memoryResults = [];
428
+ let promptResults = [];
429
+ if (tag) {
430
+ const { scope, hash } = extractScopeFromTag(tag);
431
+ const shards = await tursoShardManager.getAllShards(scope, hash);
432
+ for (const shard of shards) {
433
+ try {
434
+ const results = await tursoVectorSearch.searchInShard(shard, queryVector, tag, pageSize * 2);
435
+ memoryResults.push(...results);
436
+ }
437
+ catch (error) {
438
+ log("Shard search error", { shardId: shard.id, error: String(error) });
439
+ }
440
+ }
441
+ const projectPath = await getProjectPathFromTag(tag);
442
+ promptResults = await userPromptManager.searchPrompts(query, projectPath, pageSize * 2);
443
+ }
444
+ else {
445
+ const allShards = await getAllMemoryShards();
446
+ const uniqueTags = new Set();
447
+ for (const shard of allShards) {
448
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
449
+ const tags = await tursoVectorSearch.getDistinctTags(db);
450
+ for (const t of tags) {
451
+ if (t.container_tag)
452
+ uniqueTags.add(String(t.container_tag));
453
+ }
454
+ }
455
+ for (const containerTag of uniqueTags) {
456
+ const parsed = tryExtractScopeFromContainerTag(containerTag);
457
+ if (!parsed) {
458
+ log("Skipping invalid container_tag during global search", { containerTag });
459
+ continue;
460
+ }
461
+ const { scope, hash } = parsed;
462
+ const shards = await tursoShardManager.getAllShards(scope, hash);
463
+ for (const shard of shards) {
464
+ try {
465
+ const results = await tursoVectorSearch.searchInShard(shard, queryVector, containerTag, pageSize);
466
+ memoryResults.push(...results);
467
+ }
468
+ catch (error) {
469
+ log("Shard search error", { shardId: shard.id, error: String(error) });
470
+ }
471
+ }
472
+ }
473
+ promptResults = await userPromptManager.searchPrompts(query, undefined, pageSize * 2);
474
+ }
475
+ const formattedPrompts = promptResults.map((p) => ({
476
+ type: "prompt",
477
+ id: p.id,
478
+ sessionId: p.sessionId,
479
+ content: p.content,
480
+ createdAt: safeToISOString(p.createdAt),
481
+ projectPath: p.projectPath,
482
+ linkedMemoryId: p.linkedMemoryId,
483
+ similarity: 1.0,
484
+ }));
485
+ const formattedMemories = memoryResults.map((r) => ({
486
+ type: "memory",
487
+ id: r.id,
488
+ content: r.memory,
489
+ memoryType: r.metadata?.type,
490
+ tags: r.tags,
491
+ createdAt: safeToISOString(r.createdAt ?? r.metadata?.createdAt),
492
+ updatedAt: r.metadata?.updatedAt ? safeToISOString(r.metadata.updatedAt) : undefined,
493
+ similarity: r.similarity,
494
+ metadata: r.metadata,
495
+ displayName: r.displayName,
496
+ userName: r.userName,
497
+ userEmail: r.userEmail,
498
+ projectPath: r.projectPath,
499
+ projectName: r.projectName,
500
+ gitRepoUrl: r.gitRepoUrl,
501
+ isPinned: r.isPinned === 1,
502
+ linkedPromptId: r.metadata?.promptId,
503
+ }));
504
+ const combinedResults = [...formattedMemories, ...formattedPrompts].sort((a, b) => (b.similarity || 0) - (a.similarity || 0) || b.createdAt.localeCompare(a.createdAt));
505
+ const total = combinedResults.length;
506
+ const totalPages = Math.ceil(total / pageSize);
507
+ const offset = (page - 1) * pageSize;
508
+ const paginatedResults = combinedResults.slice(offset, offset + pageSize);
509
+ const missingPromptIds = new Set();
510
+ const missingMemoryIds = new Set();
511
+ for (const item of paginatedResults) {
512
+ if (item.type === "memory" && item.linkedPromptId) {
513
+ if (!paginatedResults.some((p) => p.id === item.linkedPromptId))
514
+ missingPromptIds.add(item.linkedPromptId);
515
+ }
516
+ else if (item.type === "prompt" && item.linkedMemoryId) {
517
+ if (!paginatedResults.some((m) => m.id === item.linkedMemoryId))
518
+ missingMemoryIds.add(item.linkedMemoryId);
519
+ }
520
+ }
521
+ if (missingPromptIds.size > 0) {
522
+ const extraPrompts = await userPromptManager.getPromptsByIds(Array.from(missingPromptIds));
523
+ for (const p of extraPrompts) {
524
+ paginatedResults.push({
525
+ type: "prompt",
526
+ id: p.id,
527
+ sessionId: p.sessionId,
528
+ content: p.content,
529
+ createdAt: safeToISOString(p.createdAt),
530
+ projectPath: p.projectPath,
531
+ linkedMemoryId: p.linkedMemoryId,
532
+ similarity: 0,
533
+ isContext: true,
534
+ });
535
+ }
536
+ }
537
+ if (missingMemoryIds.size > 0) {
538
+ const shards = await getAllMemoryShards();
539
+ for (const shard of shards) {
540
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
541
+ for (const mid of missingMemoryIds) {
542
+ const m = await tursoVectorSearch.getMemoryById(db, mid);
543
+ if (m && !paginatedResults.some((existing) => existing.id === m.id)) {
544
+ paginatedResults.push({
545
+ type: "memory",
546
+ id: String(m.id),
547
+ content: String(m.content),
548
+ memoryType: m.type ? String(m.type) : undefined,
549
+ tags: m.tags
550
+ ? String(m.tags)
551
+ .split(",")
552
+ .map((t) => t.trim())
553
+ : [],
554
+ createdAt: safeToISOString(m.created_at),
555
+ updatedAt: m.updated_at ? safeToISOString(m.updated_at) : undefined,
556
+ similarity: 0,
557
+ metadata: safeJSONParse(m.metadata),
558
+ displayName: m.display_name ? String(m.display_name) : undefined,
559
+ userName: m.user_name ? String(m.user_name) : undefined,
560
+ userEmail: m.user_email ? String(m.user_email) : undefined,
561
+ projectPath: m.project_path ? String(m.project_path) : undefined,
562
+ projectName: m.project_name ? String(m.project_name) : undefined,
563
+ gitRepoUrl: m.git_repo_url ? String(m.git_repo_url) : undefined,
564
+ isPinned: Number(m.is_pinned) === 1,
565
+ linkedPromptId: safeJSONParse(m.metadata)?.promptId,
566
+ isContext: true,
567
+ });
568
+ }
569
+ }
570
+ }
571
+ }
572
+ return { success: true, data: { items: paginatedResults, total, page, pageSize, totalPages } };
573
+ }
574
+ catch (error) {
575
+ log("handleSearch: error", { error: String(error) });
576
+ return { success: false, error: String(error) };
577
+ }
578
+ }
579
+ export async function handleStats() {
580
+ try {
581
+ // Stats only counts Turso/libSQL rows; no embedding needed.
582
+ // See handleListTags comment - keep embedding init out of read paths.
583
+ const shards = await getAllMemoryShards();
584
+ let userCount = 0, projectCount = 0;
585
+ const typeCount = {};
586
+ for (const shard of shards) {
587
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
588
+ const memories = await tursoVectorSearch.getAllMemories(db);
589
+ for (const r of memories) {
590
+ const containerTag = String(r.container_tag ?? "");
591
+ const type = r.type ? String(r.type) : "";
592
+ if (containerTag.includes("_user_"))
593
+ userCount++;
594
+ else if (containerTag.includes("_project_"))
595
+ projectCount++;
596
+ if (type)
597
+ typeCount[type] = (typeCount[type] || 0) + 1;
598
+ }
599
+ }
600
+ return {
601
+ success: true,
602
+ data: {
603
+ total: userCount + projectCount,
604
+ byScope: { user: userCount, project: projectCount },
605
+ byType: typeCount,
606
+ },
607
+ };
608
+ }
609
+ catch (error) {
610
+ log("handleStats: error", { error: String(error) });
611
+ return { success: false, error: String(error) };
612
+ }
613
+ }
614
+ export async function handlePinMemory(id) {
615
+ try {
616
+ if (!id)
617
+ return { success: false, error: "id is required" };
618
+ const shards = await getAllMemoryShards();
619
+ for (const shard of shards) {
620
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
621
+ const memory = await tursoVectorSearch.getMemoryById(db, id);
622
+ if (memory) {
623
+ await tursoVectorSearch.pinMemory(db, id);
624
+ return { success: true };
625
+ }
626
+ }
627
+ return { success: false, error: "Memory not found" };
628
+ }
629
+ catch (error) {
630
+ log("handlePinMemory: error", { error: String(error) });
631
+ return { success: false, error: String(error) };
632
+ }
633
+ }
634
+ export async function handleUnpinMemory(id) {
635
+ try {
636
+ if (!id)
637
+ return { success: false, error: "id is required" };
638
+ const shards = await getAllMemoryShards();
639
+ for (const shard of shards) {
640
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
641
+ const memory = await tursoVectorSearch.getMemoryById(db, id);
642
+ if (memory) {
643
+ await tursoVectorSearch.unpinMemory(db, id);
644
+ return { success: true };
645
+ }
646
+ }
647
+ return { success: false, error: "Memory not found" };
648
+ }
649
+ catch (error) {
650
+ log("handleUnpinMemory: error", { error: String(error) });
651
+ return { success: false, error: String(error) };
652
+ }
653
+ }
654
+ export async function handleRunCleanup() {
655
+ try {
656
+ const { cleanupService } = await import("./cleanup-service.js");
657
+ const result = await cleanupService.runCleanup();
658
+ return { success: true, data: result };
659
+ }
660
+ catch (error) {
661
+ log("handleRunCleanup: error", { error: String(error) });
662
+ return { success: false, error: String(error) };
663
+ }
664
+ }
665
+ export async function handleRunDeduplication() {
666
+ try {
667
+ const { deduplicationService } = await import("./deduplication-service.js");
668
+ const result = await deduplicationService.detectAndRemoveDuplicates();
669
+ return { success: true, data: result };
670
+ }
671
+ catch (error) {
672
+ log("handleRunDeduplication: error", { error: String(error) });
673
+ return { success: false, error: String(error) };
674
+ }
675
+ }
676
+ export async function handleDetectMigration() {
677
+ try {
678
+ const { migrationService } = await import("./migration-service.js");
679
+ const result = await migrationService.detectDimensionMismatch();
680
+ return { success: true, data: result };
681
+ }
682
+ catch (error) {
683
+ log("handleDetectMigration: error", { error: String(error) });
684
+ return { success: false, error: String(error) };
685
+ }
686
+ }
687
+ export async function handleRunMigration(strategy) {
688
+ try {
689
+ const { migrationService } = await import("./migration-service.js");
690
+ const result = await migrationService.migrateToNewModel(strategy);
691
+ return { success: result.success, data: result };
692
+ }
693
+ catch (error) {
694
+ log("handleRunMigration: error", { error: String(error) });
695
+ return { success: false, error: String(error) };
696
+ }
697
+ }
698
+ export async function handleDeletePrompt(id, cascade = false) {
699
+ try {
700
+ if (!id)
701
+ return { success: false, error: "id is required" };
702
+ const prompt = await userPromptManager.getPromptById(id);
703
+ if (!prompt)
704
+ return { success: false, error: "Prompt not found" };
705
+ let deletedMemory = false;
706
+ if (cascade && prompt.linkedMemoryId) {
707
+ const result = await handleDeleteMemory(prompt.linkedMemoryId, false);
708
+ if (result.success)
709
+ deletedMemory = true;
710
+ }
711
+ await userPromptManager.deletePrompt(id);
712
+ return { success: true, data: { deletedMemory } };
713
+ }
714
+ catch (error) {
715
+ log("handleDeletePrompt: error", { error: String(error) });
716
+ return { success: false, error: String(error) };
717
+ }
718
+ }
719
+ export async function handleBulkDeletePrompts(ids, cascade = false) {
720
+ try {
721
+ if (!ids || ids.length === 0)
722
+ return { success: false, error: "ids array is required" };
723
+ let deleted = 0;
724
+ for (const id of ids) {
725
+ const result = await handleDeletePrompt(id, cascade);
726
+ if (result.success)
727
+ deleted++;
728
+ }
729
+ return { success: true, data: { deleted } };
730
+ }
731
+ catch (error) {
732
+ log("handleBulkDeletePrompts: error", { error: String(error) });
733
+ return { success: false, error: String(error) };
734
+ }
735
+ }
736
+ export async function handleGetUserProfile(userId) {
737
+ try {
738
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
739
+ const { getTags } = await import("./tags.js");
740
+ let targetUserId = userId;
741
+ if (!targetUserId) {
742
+ const tags = getTags(process.cwd());
743
+ targetUserId = tags.user.userEmail || "unknown";
744
+ }
745
+ const profile = await userProfileManager.getActiveProfile(targetUserId);
746
+ if (!profile)
747
+ return {
748
+ success: true,
749
+ data: {
750
+ exists: false,
751
+ userId: targetUserId,
752
+ message: "No profile found. Keep chatting to build your profile.",
753
+ },
754
+ };
755
+ const profileData = JSON.parse(profile.profileData);
756
+ profileData.preferences = sortProfileItems(profileData.preferences, "confidence");
757
+ profileData.patterns = sortProfileItems(profileData.patterns, "frequency");
758
+ profileData.workflows = sortProfileItems(profileData.workflows, "frequency");
759
+ return {
760
+ success: true,
761
+ data: {
762
+ exists: true,
763
+ id: profile.id,
764
+ userId: profile.userId,
765
+ displayName: profile.displayName,
766
+ userName: profile.userName,
767
+ userEmail: profile.userEmail,
768
+ version: profile.version,
769
+ createdAt: safeToISOString(profile.createdAt),
770
+ lastAnalyzedAt: safeToISOString(profile.lastAnalyzedAt),
771
+ totalPromptsAnalyzed: profile.totalPromptsAnalyzed,
772
+ profileData,
773
+ },
774
+ };
775
+ }
776
+ catch (error) {
777
+ log("handleGetUserProfile: error", { error: String(error) });
778
+ return { success: false, error: String(error) };
779
+ }
780
+ }
781
+ export async function handleGetProfileChangelog(profileId, limit = 5) {
782
+ try {
783
+ if (!profileId)
784
+ return { success: false, error: "profileId is required" };
785
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
786
+ const changelogs = await userProfileManager.getProfileChangelogs(profileId, limit);
787
+ const formattedChangelogs = changelogs.map((c) => ({
788
+ id: c.id,
789
+ profileId: c.profileId,
790
+ version: c.version,
791
+ changeType: c.changeType,
792
+ changeSummary: c.changeSummary,
793
+ createdAt: safeToISOString(c.createdAt),
794
+ }));
795
+ return { success: true, data: formattedChangelogs };
796
+ }
797
+ catch (error) {
798
+ log("handleGetProfileChangelog: error", { error: String(error) });
799
+ return { success: false, error: String(error) };
800
+ }
801
+ }
802
+ export async function handleGetProfileSnapshot(changelogId) {
803
+ try {
804
+ if (!changelogId)
805
+ return { success: false, error: "changelogId is required" };
806
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
807
+ const changelog = await userProfileManager.getChangelogById(changelogId);
808
+ if (!changelog)
809
+ return { success: false, error: "Changelog not found" };
810
+ const profileData = JSON.parse(changelog.profileDataSnapshot);
811
+ return {
812
+ success: true,
813
+ data: {
814
+ version: changelog.version,
815
+ createdAt: safeToISOString(changelog.createdAt),
816
+ profileData,
817
+ },
818
+ };
819
+ }
820
+ catch (error) {
821
+ log("handleGetProfileSnapshot: error", { error: String(error) });
822
+ return { success: false, error: String(error) };
823
+ }
824
+ }
825
+ export async function handleRefreshProfile(userId) {
826
+ try {
827
+ const { getTags } = await import("./tags.js");
828
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
829
+ const { userPromptManager } = await import("./user-prompt/user-prompt-manager.js");
830
+ let targetUserId = userId;
831
+ if (!targetUserId) {
832
+ const tags = getTags(process.cwd());
833
+ targetUserId = tags.user.userEmail || "unknown";
834
+ }
835
+ const profile = await userProfileManager.getActiveProfile(targetUserId);
836
+ let decayApplied = false;
837
+ if (profile) {
838
+ const pData = JSON.parse(profile.profileData);
839
+ const { data: decayed, hasChanges } = userProfileManager.decayInMemory(pData);
840
+ if (hasChanges) {
841
+ await userProfileManager.updateProfile(profile.id, decayed, 0, "Applied confidence decay");
842
+ decayApplied = true;
843
+ }
844
+ }
845
+ const unanalyzedCount = await userPromptManager.countUnanalyzedForUserLearning();
846
+ return {
847
+ success: true,
848
+ data: {
849
+ message: decayApplied ? "Profile confidence decay applied" : "Profile refresh queued",
850
+ profileExists: Boolean(profile),
851
+ decayApplied,
852
+ unanalyzedPrompts: unanalyzedCount,
853
+ note: "Confidence decay runs immediately; AI profile learning still runs when the prompt threshold is reached",
854
+ },
855
+ };
856
+ }
857
+ catch (error) {
858
+ log("handleRefreshProfile: error", { error: String(error) });
859
+ return { success: false, error: String(error) };
860
+ }
861
+ }
862
+ // Temporary storage for pending AI cleanup results (userId → result)
863
+ const pendingCleanups = new Map();
864
+ export async function handleAICleanup(userId, includeIds, profileVersion) {
865
+ try {
866
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
867
+ const { getTags } = await import("./tags.js");
868
+ const { aiCleanupProfile, aiCleanupProfileFromIndexed, filterProfileForCleanup } = await import("./user-profile/ai-cleanup.js");
869
+ let targetUserId = userId;
870
+ if (!targetUserId) {
871
+ const tags = getTags(process.cwd());
872
+ targetUserId = tags.user.userEmail || "unknown";
873
+ }
874
+ const profile = await userProfileManager.getActiveProfile(targetUserId);
875
+ if (!profile) {
876
+ return { success: false, error: "No profile found to clean up" };
877
+ }
878
+ if (profileVersion !== undefined && profile.version !== profileVersion) {
879
+ return { success: false, error: "Profile changed. Reload it before running AI cleanup." };
880
+ }
881
+ const profileData = JSON.parse(profile.profileData);
882
+ profileData.preferences = sortProfileItems(profileData.preferences, "confidence");
883
+ profileData.patterns = sortProfileItems(profileData.patterns, "frequency");
884
+ profileData.workflows = sortProfileItems(profileData.workflows, "frequency");
885
+ let indexed;
886
+ let result;
887
+ const scopedIds = includeIds && includeIds.length > 0 ? includeIds : undefined;
888
+ if (scopedIds) {
889
+ indexed = filterProfileForCleanup(profileData, scopedIds);
890
+ result = await aiCleanupProfileFromIndexed(indexed);
891
+ }
892
+ else {
893
+ result = await aiCleanupProfile(profileData);
894
+ }
895
+ pendingCleanups.set(targetUserId, {
896
+ cleaned: result.cleaned,
897
+ oldProfileData: profileData,
898
+ diff: result.diff,
899
+ allMerged: result.diff?.merged || [],
900
+ allRemovedIds: (result.diff?.removed || []).map((r) => r.id),
901
+ includeIds: scopedIds,
902
+ profileVersion: profile.version,
903
+ expiresAt: Date.now() + 30 * 60 * 1000,
904
+ });
905
+ return {
906
+ success: true,
907
+ data: {
908
+ old: profileData,
909
+ new: result.cleaned,
910
+ changes: result.diff,
911
+ },
912
+ };
913
+ }
914
+ catch (error) {
915
+ log("handleAICleanup: error", { error: String(error) });
916
+ return { success: false, error: String(error) };
917
+ }
918
+ }
919
+ /**
920
+ * Merge an AI-cleanup result into a full profile.
921
+ * When includeIds is set, only scoped items are mutated; everything else is preserved.
922
+ */
923
+ export function mergeCleanupIntoProfile(args) {
924
+ const { currentProfile, oldProfileData, cleanedData, includeIds, acceptedMerged = [], acceptedRemoved = [], allMerged = [], allRemovedIds = [], explicitAcceptance = false, } = args;
925
+ // Start from cleaned data, then restore any rejected removals/merges.
926
+ const scopedResult = {
927
+ preferences: [...cleanedData.preferences],
928
+ patterns: [...cleanedData.patterns],
929
+ workflows: [...cleanedData.workflows],
930
+ };
931
+ if (explicitAcceptance) {
932
+ for (const id of acceptedRemoved) {
933
+ removeItemFromProfile(scopedResult, oldProfileData, id);
934
+ }
935
+ for (const ids of acceptedMerged) {
936
+ for (let i = 1; i < ids.length; i++) {
937
+ removeItemFromProfile(scopedResult, oldProfileData, ids[i] ?? "");
938
+ }
939
+ }
940
+ const acceptedTargetIds = new Set(acceptedMerged.map((g) => g[0]));
941
+ for (const merge of allMerged) {
942
+ const groupIds = merge.ids;
943
+ if (groupIds.length <= 1)
944
+ continue;
945
+ if (acceptedTargetIds.has(groupIds[0]))
946
+ continue;
947
+ removeOneByDescription(scopedResult, merge.result, itemTypeFromId(groupIds[0] ?? ""));
948
+ for (const id of groupIds) {
949
+ if (id)
950
+ pushItemFromProfile(scopedResult, oldProfileData, id);
951
+ }
952
+ }
953
+ const acceptedRemovedSet = new Set(acceptedRemoved);
954
+ for (const removedId of allRemovedIds) {
955
+ if (acceptedRemovedSet.has(removedId))
956
+ continue;
957
+ pushItemFromProfile(scopedResult, oldProfileData, removedId);
958
+ }
959
+ }
960
+ // Full-profile cleanup: cleaned (+ acceptance) replaces the whole profile.
961
+ if (!includeIds || includeIds.length === 0) {
962
+ return scopedResult;
963
+ }
964
+ // Partial selection: mutate only the analyzed scope inside the current full profile.
965
+ const result = {
966
+ preferences: [...currentProfile.preferences],
967
+ patterns: [...currentProfile.patterns],
968
+ workflows: [...currentProfile.workflows],
969
+ };
970
+ for (const id of includeIds) {
971
+ removeItemFromProfile(result, oldProfileData, id);
972
+ }
973
+ result.preferences.push(...scopedResult.preferences);
974
+ result.patterns.push(...scopedResult.patterns);
975
+ result.workflows.push(...scopedResult.workflows);
976
+ return result;
977
+ }
978
+ function pushItemFromProfile(target, source, id) {
979
+ const srcItem = findItemById(source, id);
980
+ if (!srcItem)
981
+ return;
982
+ const { id: _id, ...rest } = srcItem;
983
+ if (id.startsWith("pref_"))
984
+ target.preferences.push(rest);
985
+ else if (id.startsWith("pat_"))
986
+ target.patterns.push(rest);
987
+ else if (id.startsWith("wf_"))
988
+ target.workflows.push(rest);
989
+ }
990
+ export async function handleApplyCleanup(userId, body) {
991
+ try {
992
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
993
+ const { getTags } = await import("./tags.js");
994
+ let targetUserId = userId;
995
+ if (!targetUserId) {
996
+ const tags = getTags(process.cwd());
997
+ targetUserId = tags.user.userEmail || "unknown";
998
+ }
999
+ const pending = pendingCleanups.get(targetUserId);
1000
+ if (!pending) {
1001
+ return { success: false, error: "No pending cleanup found. Run AI cleanup first." };
1002
+ }
1003
+ if (Date.now() > pending.expiresAt) {
1004
+ pendingCleanups.delete(targetUserId);
1005
+ return { success: false, error: "Cleanup session expired. Run AI cleanup again." };
1006
+ }
1007
+ const profile = await userProfileManager.getActiveProfile(targetUserId);
1008
+ if (!profile) {
1009
+ return { success: false, error: "Profile not found" };
1010
+ }
1011
+ if (profile.version !== pending.profileVersion) {
1012
+ pendingCleanups.delete(targetUserId);
1013
+ return { success: false, error: "Profile changed. Run AI cleanup again." };
1014
+ }
1015
+ const cleanedData = body?.profile || pending.cleaned;
1016
+ const acceptedMerged = Array.isArray(body?.acceptedMerged)
1017
+ ? body.acceptedMerged
1018
+ : [];
1019
+ const acceptedRemoved = Array.isArray(body?.acceptedRemoved)
1020
+ ? body.acceptedRemoved
1021
+ : [];
1022
+ const explicitAcceptance = Array.isArray(body?.acceptedMerged) || Array.isArray(body?.acceptedRemoved);
1023
+ const existingData = JSON.parse(profile.profileData);
1024
+ const result = mergeCleanupIntoProfile({
1025
+ currentProfile: existingData,
1026
+ oldProfileData: pending.oldProfileData,
1027
+ cleanedData,
1028
+ includeIds: pending.includeIds,
1029
+ acceptedMerged,
1030
+ acceptedRemoved,
1031
+ allMerged: pending.allMerged,
1032
+ allRemovedIds: pending.allRemovedIds,
1033
+ explicitAcceptance,
1034
+ });
1035
+ const partial = (pending.includeIds && pending.includeIds.length > 0) || explicitAcceptance;
1036
+ const success = await userProfileManager.updateProfile(profile.id, result, 0, partial ? "AI cleanup applied (partial)" : "AI cleanup applied");
1037
+ if (!success) {
1038
+ return { success: false, error: "Profile was modified by another session. Please retry." };
1039
+ }
1040
+ pendingCleanups.delete(targetUserId);
1041
+ return {
1042
+ success: true,
1043
+ data: {
1044
+ message: partial ? "Partial cleanup applied" : "Cleanup applied successfully",
1045
+ version: profile.version + 1,
1046
+ },
1047
+ };
1048
+ }
1049
+ catch (error) {
1050
+ log("handleApplyCleanup: error", { error: String(error) });
1051
+ return { success: false, error: String(error) };
1052
+ }
1053
+ }
1054
+ function itemTypeFromId(id) {
1055
+ if (id.startsWith("pref_"))
1056
+ return "preferences";
1057
+ if (id.startsWith("pat_"))
1058
+ return "patterns";
1059
+ return "workflows";
1060
+ }
1061
+ function findItemById(profile, id) {
1062
+ if (typeof id !== "string" || !id.includes("_"))
1063
+ return null;
1064
+ const parts = id.split("_");
1065
+ const prefix = parts[0];
1066
+ const idx = parseInt(parts[1] || "", 10);
1067
+ if (isNaN(idx))
1068
+ return null;
1069
+ if (prefix === "pref")
1070
+ return profile.preferences[idx] || null;
1071
+ if (prefix === "pat")
1072
+ return profile.patterns[idx] || null;
1073
+ if (prefix === "wf")
1074
+ return profile.workflows[idx] || null;
1075
+ return null;
1076
+ }
1077
+ function removeItemFromProfile(target, source, id) {
1078
+ const sourceItem = findItemById(source, id);
1079
+ if (!sourceItem)
1080
+ return false;
1081
+ const itemType = itemTypeFromId(id);
1082
+ const items = target[itemType];
1083
+ const sourceKey = profileItemIdentityKey(sourceItem);
1084
+ const index = items.findIndex((item) => profileItemIdentityKey(item) === sourceKey);
1085
+ if (index < 0)
1086
+ return false;
1087
+ items.splice(index, 1);
1088
+ return true;
1089
+ }
1090
+ function profileItemIdentityKey(item) {
1091
+ return JSON.stringify({
1092
+ category: item.category ?? null,
1093
+ description: item.description ?? null,
1094
+ steps: Array.isArray(item.steps) ? item.steps : null,
1095
+ });
1096
+ }
1097
+ function removeOneByDescription(profile, desc, itemType) {
1098
+ const items = profile[itemType];
1099
+ const index = items.findIndex((item) => item.description === desc);
1100
+ if (index < 0)
1101
+ return false;
1102
+ items.splice(index, 1);
1103
+ return true;
1104
+ }
1105
+ export async function handleUpdateProfileItem(body) {
1106
+ try {
1107
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
1108
+ const { getTags } = await import("./tags.js");
1109
+ const tags = getTags(process.cwd());
1110
+ const userId = tags.user.userEmail || "unknown";
1111
+ if (!userId)
1112
+ return { success: false, error: "Unable to resolve user identity" };
1113
+ const profile = await userProfileManager.getActiveProfile(userId);
1114
+ if (!profile)
1115
+ return { success: false, error: "No profile found" };
1116
+ const { type, index, action, category, description, steps } = body || {};
1117
+ if (!type || index === undefined || !action) {
1118
+ return { success: false, error: "type, index, and action are required" };
1119
+ }
1120
+ if (!["preferences", "patterns", "workflows"].includes(type)) {
1121
+ return { success: false, error: "type must be preferences, patterns, or workflows" };
1122
+ }
1123
+ if (!["edit", "delete"].includes(action)) {
1124
+ return { success: false, error: "action must be edit or delete" };
1125
+ }
1126
+ const profileData = JSON.parse(profile.profileData);
1127
+ const items = profileData[type] || [];
1128
+ // Re-sort to match handleGetUserProfile's display order
1129
+ const metric = type === "preferences" ? "confidence" : "frequency";
1130
+ const sorted = sortProfileItems(items, metric);
1131
+ if (index < 0 || index >= sorted.length) {
1132
+ return { success: false, error: "index out of range" };
1133
+ }
1134
+ if (action === "delete") {
1135
+ sorted.splice(index, 1);
1136
+ }
1137
+ else {
1138
+ const item = sorted[index];
1139
+ if (!item)
1140
+ return { success: false, error: "Item not found" };
1141
+ if (category !== undefined && type !== "workflows")
1142
+ item.category = category;
1143
+ if (description !== undefined && description !== item.description) {
1144
+ item.description = description;
1145
+ item.centroid = undefined;
1146
+ item.anchor = undefined;
1147
+ }
1148
+ if (steps !== undefined && Array.isArray(steps) && type === "workflows")
1149
+ item.steps = steps;
1150
+ }
1151
+ profileData[type] = sorted;
1152
+ const changeSummary = action === "delete"
1153
+ ? `Deleted ${type.slice(0, -1)} at index ${index}`
1154
+ : `Edited ${type.slice(0, -1)} at index ${index}`;
1155
+ const success = await userProfileManager.updateProfile(profile.id, profileData, 0, changeSummary);
1156
+ if (!success)
1157
+ return { success: false, error: "Profile was modified by another session. Please retry." };
1158
+ return {
1159
+ success: true,
1160
+ data: { message: `${action} successful`, version: profile.version + 1 },
1161
+ };
1162
+ }
1163
+ catch (error) {
1164
+ log("handleUpdateProfileItem: error", { error: String(error) });
1165
+ return { success: false, error: String(error) };
1166
+ }
1167
+ }
1168
+ export async function handleDetectTagMigration() {
1169
+ try {
1170
+ await ensureTursoReady();
1171
+ const projectShards = await tursoShardManager.getAllShards("project", "");
1172
+ let untaggedCount = 0;
1173
+ for (const shard of projectShards) {
1174
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
1175
+ const row = await db.get("SELECT COUNT(*) as count FROM memories WHERE tags IS NULL OR tags = ''");
1176
+ untaggedCount += Number(row?.count ?? 0);
1177
+ }
1178
+ return { success: true, data: { needsMigration: untaggedCount > 0, count: untaggedCount } };
1179
+ }
1180
+ catch (error) {
1181
+ return { success: false, error: String(error) };
1182
+ }
1183
+ }
1184
+ const migrationProgress = {
1185
+ processed: 0,
1186
+ total: 0,
1187
+ currentBatch: 0,
1188
+ totalBatches: 0,
1189
+ isComplete: true,
1190
+ errors: [],
1191
+ };
1192
+ export async function handleGetTagMigrationProgress() {
1193
+ return { success: true, data: migrationProgress };
1194
+ }
1195
+ export async function handleRunTagMigrationBatch(batchSize = 5) {
1196
+ try {
1197
+ await ensureTursoReady();
1198
+ const { AIProviderFactory } = await import("./ai/ai-provider-factory.js");
1199
+ const { buildMemoryProviderConfig } = await import("./ai/provider-config.js");
1200
+ const providerConfig = buildMemoryProviderConfig(CONFIG, {
1201
+ maxIterations: 1,
1202
+ iterationTimeout: 30000,
1203
+ });
1204
+ const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig);
1205
+ const projectShards = await tursoShardManager.getAllShards("project", "");
1206
+ const allMemories = [];
1207
+ for (const shard of projectShards) {
1208
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
1209
+ const memories = await db.all("SELECT * FROM memories");
1210
+ for (const m of memories) {
1211
+ allMemories.push({ memory: m, shard });
1212
+ }
1213
+ }
1214
+ if (migrationProgress.total === 0) {
1215
+ migrationProgress.total = allMemories.length;
1216
+ migrationProgress.totalBatches = Math.ceil(allMemories.length / batchSize);
1217
+ migrationProgress.isComplete = false;
1218
+ }
1219
+ const startIdx = migrationProgress.processed;
1220
+ const endIdx = Math.min(startIdx + batchSize, allMemories.length);
1221
+ for (let i = startIdx; i < endIdx; i++) {
1222
+ const item = allMemories[i];
1223
+ if (!item)
1224
+ continue;
1225
+ const { memory: m, shard } = item;
1226
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
1227
+ try {
1228
+ let currentTags = m.tags
1229
+ ? m.tags
1230
+ .split(",")
1231
+ .map((t) => t.trim().toLowerCase())
1232
+ .filter((t) => t)
1233
+ : [];
1234
+ if (currentTags.length === 0) {
1235
+ const prompt = `Generate 2-4 short technical tags for this memory content:\n\n${m.content}\n\nReturn ONLY a comma-separated list of tags.`;
1236
+ const result = await provider.executeToolCall("You are a technical tagger.", prompt, {
1237
+ type: "function",
1238
+ function: {
1239
+ name: "save_tags",
1240
+ description: "Save generated tags",
1241
+ parameters: {
1242
+ type: "object",
1243
+ properties: { tags: { type: "array", items: { type: "string" } } },
1244
+ required: ["tags"],
1245
+ },
1246
+ },
1247
+ }, `migration_${m.id}`);
1248
+ if (result.success && result.data?.tags) {
1249
+ currentTags = result.data.tags;
1250
+ await db.run("UPDATE memories SET tags = ? WHERE id = ?", [
1251
+ currentTags.join(","),
1252
+ m.id,
1253
+ ]);
1254
+ }
1255
+ }
1256
+ const vector = await embeddingService.embedWithTimeout(m.content, { task: "document" });
1257
+ const tagsVector = currentTags.length
1258
+ ? await embeddingService.embedWithTimeout(formatTagsForEmbedding(currentTags), {
1259
+ task: "document",
1260
+ })
1261
+ : undefined;
1262
+ await tursoVectorSearch.updateVector(db, m.id, vector, tagsVector);
1263
+ migrationProgress.processed++;
1264
+ }
1265
+ catch (e) {
1266
+ const errorMsg = String(e);
1267
+ migrationProgress.errors.push(errorMsg);
1268
+ log("Migration error for memory", { id: m.id, error: errorMsg });
1269
+ }
1270
+ }
1271
+ migrationProgress.currentBatch++;
1272
+ const hasMore = migrationProgress.processed < migrationProgress.total;
1273
+ if (!hasMore) {
1274
+ migrationProgress.isComplete = true;
1275
+ }
1276
+ return {
1277
+ success: true,
1278
+ data: { processed: migrationProgress.processed, total: migrationProgress.total, hasMore },
1279
+ };
1280
+ }
1281
+ catch (error) {
1282
+ return { success: false, error: String(error) };
1283
+ }
1284
+ }