opencode-mem 1.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 (84) hide show
  1. package/README.md +588 -0
  2. package/dist/config.d.ts +33 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +258 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +618 -0
  8. package/dist/plugin.d.ts +5 -0
  9. package/dist/plugin.d.ts.map +1 -0
  10. package/dist/plugin.js +15 -0
  11. package/dist/services/api-handlers.d.ts +102 -0
  12. package/dist/services/api-handlers.d.ts.map +1 -0
  13. package/dist/services/api-handlers.js +494 -0
  14. package/dist/services/auto-capture.d.ts +32 -0
  15. package/dist/services/auto-capture.d.ts.map +1 -0
  16. package/dist/services/auto-capture.js +451 -0
  17. package/dist/services/cleanup-service.d.ts +20 -0
  18. package/dist/services/cleanup-service.d.ts.map +1 -0
  19. package/dist/services/cleanup-service.js +88 -0
  20. package/dist/services/client.d.ts +104 -0
  21. package/dist/services/client.d.ts.map +1 -0
  22. package/dist/services/client.js +251 -0
  23. package/dist/services/compaction.d.ts +92 -0
  24. package/dist/services/compaction.d.ts.map +1 -0
  25. package/dist/services/compaction.js +421 -0
  26. package/dist/services/context.d.ts +17 -0
  27. package/dist/services/context.d.ts.map +1 -0
  28. package/dist/services/context.js +41 -0
  29. package/dist/services/deduplication-service.d.ts +30 -0
  30. package/dist/services/deduplication-service.d.ts.map +1 -0
  31. package/dist/services/deduplication-service.js +131 -0
  32. package/dist/services/embedding.d.ts +10 -0
  33. package/dist/services/embedding.d.ts.map +1 -0
  34. package/dist/services/embedding.js +77 -0
  35. package/dist/services/jsonc.d.ts +7 -0
  36. package/dist/services/jsonc.d.ts.map +1 -0
  37. package/dist/services/jsonc.js +76 -0
  38. package/dist/services/logger.d.ts +2 -0
  39. package/dist/services/logger.d.ts.map +1 -0
  40. package/dist/services/logger.js +16 -0
  41. package/dist/services/migration-service.d.ts +42 -0
  42. package/dist/services/migration-service.d.ts.map +1 -0
  43. package/dist/services/migration-service.js +258 -0
  44. package/dist/services/privacy.d.ts +4 -0
  45. package/dist/services/privacy.d.ts.map +1 -0
  46. package/dist/services/privacy.js +10 -0
  47. package/dist/services/sqlite/connection-manager.d.ts +10 -0
  48. package/dist/services/sqlite/connection-manager.d.ts.map +1 -0
  49. package/dist/services/sqlite/connection-manager.js +45 -0
  50. package/dist/services/sqlite/shard-manager.d.ts +20 -0
  51. package/dist/services/sqlite/shard-manager.d.ts.map +1 -0
  52. package/dist/services/sqlite/shard-manager.js +221 -0
  53. package/dist/services/sqlite/types.d.ts +39 -0
  54. package/dist/services/sqlite/types.d.ts.map +1 -0
  55. package/dist/services/sqlite/types.js +1 -0
  56. package/dist/services/sqlite/vector-search.d.ts +18 -0
  57. package/dist/services/sqlite/vector-search.d.ts.map +1 -0
  58. package/dist/services/sqlite/vector-search.js +129 -0
  59. package/dist/services/sqlite-client.d.ts +116 -0
  60. package/dist/services/sqlite-client.d.ts.map +1 -0
  61. package/dist/services/sqlite-client.js +284 -0
  62. package/dist/services/tags.d.ts +20 -0
  63. package/dist/services/tags.d.ts.map +1 -0
  64. package/dist/services/tags.js +76 -0
  65. package/dist/services/web-server-lock.d.ts +12 -0
  66. package/dist/services/web-server-lock.d.ts.map +1 -0
  67. package/dist/services/web-server-lock.js +157 -0
  68. package/dist/services/web-server-worker.d.ts +2 -0
  69. package/dist/services/web-server-worker.d.ts.map +1 -0
  70. package/dist/services/web-server-worker.js +221 -0
  71. package/dist/services/web-server.d.ts +22 -0
  72. package/dist/services/web-server.d.ts.map +1 -0
  73. package/dist/services/web-server.js +134 -0
  74. package/dist/types/index.d.ts +48 -0
  75. package/dist/types/index.d.ts.map +1 -0
  76. package/dist/types/index.js +1 -0
  77. package/dist/web/app.d.ts +2 -0
  78. package/dist/web/app.d.ts.map +1 -0
  79. package/dist/web/app.js +691 -0
  80. package/dist/web/favicon.ico +0 -0
  81. package/dist/web/favicon.svg +14 -0
  82. package/dist/web/index.html +202 -0
  83. package/dist/web/styles.css +851 -0
  84. package/package.json +52 -0
@@ -0,0 +1,691 @@
1
+ const API_BASE = "";
2
+
3
+ const state = {
4
+ tags: { user: [], project: [] },
5
+ memories: [],
6
+ currentPage: 1,
7
+ pageSize: 20,
8
+ totalPages: 1,
9
+ totalItems: 0,
10
+ selectedTag: "",
11
+ selectedScope: "",
12
+ searchQuery: "",
13
+ isSearching: false,
14
+ selectedMemories: new Set(),
15
+ autoRefreshInterval: null,
16
+ };
17
+
18
+ async function fetchAPI(endpoint, options = {}) {
19
+ try {
20
+ const response = await fetch(API_BASE + endpoint, options);
21
+ const data = await response.json();
22
+ return data;
23
+ } catch (error) {
24
+ console.error("API Error:", error);
25
+ return { success: false, error: error.message };
26
+ }
27
+ }
28
+
29
+ async function loadTags() {
30
+ const result = await fetchAPI("/api/tags");
31
+ if (result.success) {
32
+ state.tags = result.data;
33
+ populateTagDropdowns();
34
+ }
35
+ }
36
+
37
+ function populateTagDropdowns() {
38
+ const tagFilter = document.getElementById("tag-filter");
39
+ const addTag = document.getElementById("add-tag");
40
+
41
+ tagFilter.innerHTML = '<option value="">All Tags</option>';
42
+ addTag.innerHTML = '<option value="">Select tag</option>';
43
+
44
+ const allTags = [...state.tags.user, ...state.tags.project];
45
+ allTags.forEach((tagInfo) => {
46
+ const scope = tagInfo.tag.includes("_user_") ? "user" : "project";
47
+ const displayText = tagInfo.displayName || tagInfo.tag;
48
+ const shortDisplay =
49
+ displayText.length > 50 ? displayText.substring(0, 50) + "..." : displayText;
50
+
51
+ const option1 = document.createElement("option");
52
+ option1.value = tagInfo.tag;
53
+ option1.textContent = `[${scope}] ${shortDisplay}`;
54
+ tagFilter.appendChild(option1);
55
+
56
+ const option2 = document.createElement("option");
57
+ option2.value = tagInfo.tag;
58
+ option2.textContent = `[${scope}] ${shortDisplay}`;
59
+ addTag.appendChild(option2);
60
+ });
61
+ }
62
+
63
+ async function loadMemories() {
64
+ showRefreshIndicator(true);
65
+
66
+ let endpoint = `/api/memories?page=${state.currentPage}&pageSize=${state.pageSize}`;
67
+
68
+ if (state.isSearching && state.searchQuery) {
69
+ endpoint = `/api/search?q=${encodeURIComponent(state.searchQuery)}&page=${state.currentPage}&pageSize=${state.pageSize}`;
70
+ if (state.selectedTag) {
71
+ endpoint += `&tag=${encodeURIComponent(state.selectedTag)}`;
72
+ }
73
+ } else {
74
+ if (state.selectedTag) {
75
+ endpoint += `&tag=${encodeURIComponent(state.selectedTag)}`;
76
+ }
77
+ }
78
+
79
+ const result = await fetchAPI(endpoint);
80
+
81
+ showRefreshIndicator(false);
82
+
83
+ if (result.success) {
84
+ state.memories = result.data.items;
85
+ state.totalPages = result.data.totalPages;
86
+ state.totalItems = result.data.total;
87
+ state.currentPage = result.data.page;
88
+
89
+ renderMemories();
90
+ updatePagination();
91
+ updateSectionTitle();
92
+ } else {
93
+ showError(result.error || "Failed to load memories");
94
+ }
95
+ }
96
+
97
+ function renderMemories() {
98
+ const container = document.getElementById("memories-list");
99
+
100
+ if (state.memories.length === 0) {
101
+ container.innerHTML = '<div class="empty-state">No memories found</div>';
102
+ return;
103
+ }
104
+
105
+ container.innerHTML = state.memories
106
+ .map((memory) => {
107
+ const isSelected = state.selectedMemories.has(memory.id);
108
+ const isPinned = memory.isPinned || false;
109
+ const similarityHtml =
110
+ memory.similarity !== undefined
111
+ ? `<span class="similarity-score">${memory.similarity}%</span>`
112
+ : "";
113
+
114
+ let displayInfo = memory.displayName || memory.id;
115
+ if (memory.scope === "project" && memory.projectPath) {
116
+ const pathParts = memory.projectPath.split("/");
117
+ displayInfo = pathParts[pathParts.length - 1] || memory.projectPath;
118
+ }
119
+
120
+ let subtitle = "";
121
+ if (memory.scope === "user" && memory.userEmail) {
122
+ subtitle = `<span class="memory-subtitle">${escapeHtml(memory.userEmail)}</span>`;
123
+ } else if (memory.scope === "project" && memory.projectPath) {
124
+ subtitle = `<span class="memory-subtitle">${escapeHtml(memory.projectPath)}</span>`;
125
+ }
126
+
127
+ const pinButton = isPinned
128
+ ? `<button class="btn-pin pinned" onclick="unpinMemory('${memory.id}')" title="Unpin"><i data-lucide="pin" class="icon icon-filled"></i></button>`
129
+ : `<button class="btn-pin" onclick="pinMemory('${memory.id}')" title="Pin"><i data-lucide="pin" class="icon"></i></button>`;
130
+
131
+ const createdDate = formatDate(memory.createdAt);
132
+ const updatedDate =
133
+ memory.updatedAt && memory.updatedAt !== memory.createdAt
134
+ ? formatDate(memory.updatedAt)
135
+ : null;
136
+
137
+ const dateInfo = updatedDate
138
+ ? `<span>Created: ${createdDate}</span><span>Updated: ${updatedDate}</span>`
139
+ : `<span>Created: ${createdDate}</span>`;
140
+
141
+ return `
142
+ <div class="memory-card ${isSelected ? "selected" : ""} ${isPinned ? "pinned" : ""}" data-id="${memory.id}">
143
+ <div class="memory-header">
144
+ <div class="meta">
145
+ <input type="checkbox" class="memory-checkbox" data-id="${memory.id}" ${isSelected ? "checked" : ""} />
146
+ <span class="badge badge-${memory.scope}">${memory.scope}</span>
147
+ ${memory.type ? `<span class="badge badge-type">${memory.type}</span>` : ""}
148
+ ${similarityHtml}
149
+ ${isPinned ? '<span class="badge badge-pinned">PINNED</span>' : ""}
150
+ <span class="memory-display-name">${escapeHtml(displayInfo)}</span>
151
+ ${subtitle}
152
+ </div>
153
+ <div class="memory-actions">
154
+ ${pinButton}
155
+ <button class="btn-edit" onclick="editMemory('${memory.id}')"><i data-lucide="edit-3" class="icon"></i></button>
156
+ <button class="btn-delete" onclick="deleteMemory('${memory.id}')"><i data-lucide="trash-2" class="icon"></i></button>
157
+ </div>
158
+ </div>
159
+ <div class="memory-content">${escapeHtml(memory.content)}</div>
160
+ <div class="memory-footer">
161
+ ${dateInfo}
162
+ <span>ID: ${memory.id}</span>
163
+ </div>
164
+ </div>
165
+ `;
166
+ })
167
+ .join("");
168
+
169
+ document.querySelectorAll(".memory-checkbox").forEach((checkbox) => {
170
+ checkbox.addEventListener("change", handleCheckboxChange);
171
+ });
172
+
173
+ lucide.createIcons();
174
+ }
175
+
176
+ function handleCheckboxChange(e) {
177
+ const id = e.target.dataset.id;
178
+ if (e.target.checked) {
179
+ state.selectedMemories.add(id);
180
+ } else {
181
+ state.selectedMemories.delete(id);
182
+ }
183
+ updateBulkActions();
184
+ updateCardSelection(id, e.target.checked);
185
+ }
186
+
187
+ function updateCardSelection(id, selected) {
188
+ const card = document.querySelector(`.memory-card[data-id="${id}"]`);
189
+ if (card) {
190
+ if (selected) {
191
+ card.classList.add("selected");
192
+ } else {
193
+ card.classList.remove("selected");
194
+ }
195
+ }
196
+ }
197
+
198
+ function updateBulkActions() {
199
+ const bulkActions = document.getElementById("bulk-actions");
200
+ const selectedCount = document.getElementById("selected-count");
201
+
202
+ if (state.selectedMemories.size > 0) {
203
+ bulkActions.classList.remove("hidden");
204
+ selectedCount.textContent = `${state.selectedMemories.size} selected`;
205
+ } else {
206
+ bulkActions.classList.add("hidden");
207
+ }
208
+ }
209
+
210
+ function updatePagination() {
211
+ const pageInfo = `Page ${state.currentPage} of ${state.totalPages}`;
212
+ document.getElementById("page-info-top").textContent = pageInfo;
213
+ document.getElementById("page-info-bottom").textContent = pageInfo;
214
+
215
+ const hasPrev = state.currentPage > 1;
216
+ const hasNext = state.currentPage < state.totalPages;
217
+
218
+ document.getElementById("prev-page-top").disabled = !hasPrev;
219
+ document.getElementById("next-page-top").disabled = !hasNext;
220
+ document.getElementById("prev-page-bottom").disabled = !hasPrev;
221
+ document.getElementById("next-page-bottom").disabled = !hasNext;
222
+ }
223
+
224
+ function updateSectionTitle() {
225
+ const title = state.isSearching
226
+ ? `└─ SEARCH RESULTS (${state.totalItems}) ──`
227
+ : `└─ MEMORIES (${state.totalItems}) ──`;
228
+ document.getElementById("section-title").textContent = title;
229
+ }
230
+
231
+ async function loadStats() {
232
+ const result = await fetchAPI("/api/stats");
233
+ if (result.success) {
234
+ document.getElementById("stats-total").textContent = `Total: ${result.data.total}`;
235
+ document.getElementById("stats-user").textContent = `User: ${result.data.byScope.user}`;
236
+ document.getElementById("stats-project").textContent =
237
+ `Project: ${result.data.byScope.project}`;
238
+ }
239
+ }
240
+
241
+ async function addMemory(e) {
242
+ e.preventDefault();
243
+
244
+ const content = document.getElementById("add-content").value.trim();
245
+ const containerTag = document.getElementById("add-tag").value;
246
+ const type = document.getElementById("add-type").value.trim();
247
+
248
+ if (!content || !containerTag) {
249
+ showToast("Content and tag are required", "error");
250
+ return;
251
+ }
252
+
253
+ const result = await fetchAPI("/api/memories", {
254
+ method: "POST",
255
+ headers: { "Content-Type": "application/json" },
256
+ body: JSON.stringify({ content, containerTag, type: type || undefined }),
257
+ });
258
+
259
+ if (result.success) {
260
+ showToast("Memory added successfully", "success");
261
+ document.getElementById("add-form").reset();
262
+ await loadMemories();
263
+ await loadStats();
264
+ } else {
265
+ showToast(result.error || "Failed to add memory", "error");
266
+ }
267
+ }
268
+
269
+ async function deleteMemory(id) {
270
+ if (!confirm("Delete this memory?")) return;
271
+
272
+ const result = await fetchAPI(`/api/memories/${id}`, { method: "DELETE" });
273
+
274
+ if (result.success) {
275
+ showToast("Memory deleted", "success");
276
+ state.selectedMemories.delete(id);
277
+ await loadMemories();
278
+ await loadStats();
279
+ updateBulkActions();
280
+ } else {
281
+ showToast(result.error || "Failed to delete memory", "error");
282
+ }
283
+ }
284
+
285
+ async function bulkDelete() {
286
+ if (state.selectedMemories.size === 0) return;
287
+
288
+ if (!confirm(`Delete ${state.selectedMemories.size} selected memories?`)) return;
289
+
290
+ const ids = Array.from(state.selectedMemories);
291
+ const result = await fetchAPI("/api/memories/bulk-delete", {
292
+ method: "POST",
293
+ headers: { "Content-Type": "application/json" },
294
+ body: JSON.stringify({ ids }),
295
+ });
296
+
297
+ if (result.success) {
298
+ showToast(`Deleted ${result.data.deleted} memories`, "success");
299
+ state.selectedMemories.clear();
300
+ await loadMemories();
301
+ await loadStats();
302
+ updateBulkActions();
303
+ } else {
304
+ showToast(result.error || "Failed to delete memories", "error");
305
+ }
306
+ }
307
+
308
+ function deselectAll() {
309
+ state.selectedMemories.clear();
310
+ document.querySelectorAll(".memory-checkbox").forEach((cb) => (cb.checked = false));
311
+ document.querySelectorAll(".memory-card").forEach((card) => card.classList.remove("selected"));
312
+ updateBulkActions();
313
+ }
314
+
315
+ function editMemory(id) {
316
+ const memory = state.memories.find((m) => m.id === id);
317
+ if (!memory) return;
318
+
319
+ document.getElementById("edit-id").value = memory.id;
320
+ document.getElementById("edit-type").value = memory.type || "";
321
+ document.getElementById("edit-content").value = memory.content;
322
+
323
+ document.getElementById("edit-modal").classList.remove("hidden");
324
+ }
325
+
326
+ async function saveEdit(e) {
327
+ e.preventDefault();
328
+
329
+ const id = document.getElementById("edit-id").value;
330
+ const type = document.getElementById("edit-type").value.trim();
331
+ const content = document.getElementById("edit-content").value.trim();
332
+
333
+ if (!content) {
334
+ showToast("Content is required", "error");
335
+ return;
336
+ }
337
+
338
+ const result = await fetchAPI(`/api/memories/${id}`, {
339
+ method: "PUT",
340
+ headers: { "Content-Type": "application/json" },
341
+ body: JSON.stringify({ content, type: type || undefined }),
342
+ });
343
+
344
+ if (result.success) {
345
+ showToast("Memory updated", "success");
346
+ closeModal();
347
+ await loadMemories();
348
+ } else {
349
+ showToast(result.error || "Failed to update memory", "error");
350
+ }
351
+ }
352
+
353
+ function closeModal() {
354
+ document.getElementById("edit-modal").classList.add("hidden");
355
+ }
356
+
357
+ function performSearch() {
358
+ const query = document.getElementById("search-input").value.trim();
359
+
360
+ if (!query) {
361
+ clearSearch();
362
+ return;
363
+ }
364
+
365
+ state.searchQuery = query;
366
+ state.isSearching = true;
367
+ state.currentPage = 1;
368
+
369
+ document.getElementById("clear-search-btn").classList.remove("hidden");
370
+
371
+ loadMemories();
372
+ }
373
+
374
+ function clearSearch() {
375
+ state.searchQuery = "";
376
+ state.isSearching = false;
377
+ state.currentPage = 1;
378
+
379
+ document.getElementById("search-input").value = "";
380
+ document.getElementById("clear-search-btn").classList.add("hidden");
381
+
382
+ loadMemories();
383
+ }
384
+
385
+ function changePage(delta) {
386
+ const newPage = state.currentPage + delta;
387
+ if (newPage < 1 || newPage > state.totalPages) return;
388
+
389
+ state.currentPage = newPage;
390
+ loadMemories();
391
+ }
392
+
393
+ function handleFilterChange() {
394
+ const scopeFilter = document.getElementById("scope-filter").value;
395
+ const tagFilter = document.getElementById("tag-filter").value;
396
+
397
+ if (scopeFilter) {
398
+ const filteredTags = scopeFilter === "user" ? state.tags.user : state.tags.project;
399
+ state.selectedTag = filteredTags.length > 0 ? filteredTags[0].tag : "";
400
+
401
+ const tagDropdown = document.getElementById("tag-filter");
402
+ tagDropdown.innerHTML = '<option value="">All Tags</option>';
403
+ filteredTags.forEach((tagInfo) => {
404
+ const displayText = tagInfo.displayName || tagInfo.tag;
405
+ const shortDisplay =
406
+ displayText.length > 50 ? displayText.substring(0, 50) + "..." : displayText;
407
+ const option = document.createElement("option");
408
+ option.value = tagInfo.tag;
409
+ option.textContent = `[${scopeFilter}] ${shortDisplay}`;
410
+ if (tagInfo.tag === state.selectedTag) option.selected = true;
411
+ tagDropdown.appendChild(option);
412
+ });
413
+ } else {
414
+ state.selectedTag = tagFilter;
415
+ }
416
+
417
+ state.currentPage = 1;
418
+ loadMemories();
419
+ }
420
+
421
+ function handleAddScopeChange() {
422
+ const scope = document.getElementById("add-scope").value;
423
+ const tagDropdown = document.getElementById("add-tag");
424
+
425
+ tagDropdown.innerHTML = '<option value="">Select tag</option>';
426
+
427
+ if (!scope) return;
428
+
429
+ const tags = scope === "user" ? state.tags.user : state.tags.project;
430
+ tags.forEach((tagInfo) => {
431
+ const displayText = tagInfo.displayName || tagInfo.tag;
432
+ const shortDisplay =
433
+ displayText.length > 50 ? displayText.substring(0, 50) + "..." : displayText;
434
+ const option = document.createElement("option");
435
+ option.value = tagInfo.tag;
436
+ option.textContent = `[${scope}] ${shortDisplay}`;
437
+ tagDropdown.appendChild(option);
438
+ });
439
+ }
440
+
441
+ function showToast(message, type = "success") {
442
+ const toast = document.getElementById("toast");
443
+ toast.textContent = message;
444
+ toast.className = `toast ${type}`;
445
+ toast.classList.remove("hidden");
446
+
447
+ setTimeout(() => {
448
+ toast.classList.add("hidden");
449
+ }, 3000);
450
+ }
451
+
452
+ function showError(message) {
453
+ const container = document.getElementById("memories-list");
454
+ container.innerHTML = `<div class="error-state">Error: ${escapeHtml(message)}</div>`;
455
+ }
456
+
457
+ function showRefreshIndicator(show) {
458
+ const indicator = document.getElementById("refresh-indicator");
459
+ if (show) {
460
+ indicator.classList.remove("hidden");
461
+ } else {
462
+ indicator.classList.add("hidden");
463
+ }
464
+ }
465
+
466
+ function escapeHtml(text) {
467
+ const div = document.createElement("div");
468
+ div.textContent = text;
469
+ return div.innerHTML;
470
+ }
471
+
472
+ function formatDate(isoString) {
473
+ const date = new Date(isoString);
474
+ return date.toLocaleString("en-US", {
475
+ year: "numeric",
476
+ month: "short",
477
+ day: "numeric",
478
+ hour: "2-digit",
479
+ minute: "2-digit",
480
+ });
481
+ }
482
+
483
+ async function pinMemory(id) {
484
+ const result = await fetchAPI(`/api/memories/${id}/pin`, { method: "POST" });
485
+
486
+ if (result.success) {
487
+ showToast("Memory pinned", "success");
488
+ await loadMemories();
489
+ } else {
490
+ showToast(result.error || "Failed to pin memory", "error");
491
+ }
492
+ }
493
+
494
+ async function unpinMemory(id) {
495
+ const result = await fetchAPI(`/api/memories/${id}/unpin`, { method: "POST" });
496
+
497
+ if (result.success) {
498
+ showToast("Memory unpinned", "success");
499
+ await loadMemories();
500
+ } else {
501
+ showToast(result.error || "Failed to unpin memory", "error");
502
+ }
503
+ }
504
+
505
+ async function runCleanup() {
506
+ if (!confirm("Run cleanup? This will delete old memories (respects pinned memories).")) return;
507
+
508
+ showToast("Running cleanup...", "info");
509
+ const result = await fetchAPI("/api/cleanup", { method: "POST" });
510
+
511
+ if (result.success) {
512
+ const data = result.data;
513
+ showToast(
514
+ `Cleanup complete: ${data.deletedCount} deleted (user: ${data.userCount}, project: ${data.projectCount})`,
515
+ "success"
516
+ );
517
+ await loadMemories();
518
+ await loadStats();
519
+ } else {
520
+ showToast(result.error || "Cleanup failed", "error");
521
+ }
522
+ }
523
+
524
+ async function runDeduplication() {
525
+ if (!confirm("Run deduplication? This will find and remove duplicate memories.")) return;
526
+
527
+ showToast("Running deduplication...", "info");
528
+ const result = await fetchAPI("/api/deduplicate", { method: "POST" });
529
+
530
+ if (result.success) {
531
+ const data = result.data;
532
+ let message = `Deduplication complete: ${data.exactDuplicatesDeleted} exact duplicates deleted`;
533
+ if (data.nearDuplicateGroups.length > 0) {
534
+ message += `, ${data.nearDuplicateGroups.length} near-duplicate groups found`;
535
+ }
536
+ showToast(message, "success");
537
+ await loadMemories();
538
+ await loadStats();
539
+ } else {
540
+ showToast(result.error || "Deduplication failed", "error");
541
+ }
542
+ }
543
+
544
+ function startAutoRefresh() {
545
+ if (state.autoRefreshInterval) {
546
+ clearInterval(state.autoRefreshInterval);
547
+ }
548
+
549
+ state.autoRefreshInterval = setInterval(() => {
550
+ loadStats();
551
+ if (!state.isSearching) {
552
+ loadMemories();
553
+ }
554
+ }, 30000);
555
+ }
556
+
557
+ async function checkMigrationStatus() {
558
+ const result = await fetchAPI("/api/migration/detect");
559
+ if (result.success && result.data.needsMigration) {
560
+ showMigrationWarning(result.data);
561
+ }
562
+ }
563
+
564
+ function showMigrationWarning(data) {
565
+ const section = document.getElementById("migration-section");
566
+ const message = document.getElementById("migration-message");
567
+
568
+ const shardInfo =
569
+ data.shardMismatches.length > 0
570
+ ? `${data.shardMismatches.length} shard(s) have different dimensions`
571
+ : "dimension mismatch detected";
572
+
573
+ message.textContent = `Model mismatch: Config uses ${data.configDimensions}D (${data.configModel}), but ${shardInfo}.`;
574
+ section.classList.remove("hidden");
575
+
576
+ lucide.createIcons();
577
+ }
578
+
579
+ function toggleMigrationButtons() {
580
+ const checkbox = document.getElementById("migration-confirm-checkbox");
581
+ const freshBtn = document.getElementById("migration-fresh-btn");
582
+ const reembedBtn = document.getElementById("migration-reembed-btn");
583
+
584
+ freshBtn.disabled = !checkbox.checked;
585
+ reembedBtn.disabled = !checkbox.checked;
586
+ }
587
+
588
+ async function runMigration(strategy) {
589
+ const checkbox = document.getElementById("migration-confirm-checkbox");
590
+
591
+ if (!checkbox.checked) {
592
+ showToast("Please confirm you understand this operation is irreversible", "error");
593
+ return;
594
+ }
595
+
596
+ const strategyName =
597
+ strategy === "fresh-start" ? "Fresh Start (Delete All)" : "Re-embed (Preserve Data)";
598
+
599
+ if (
600
+ !confirm(
601
+ `Run ${strategyName} migration?\n\nThis operation is IRREVERSIBLE and will:\n${strategy === "fresh-start" ? "- DELETE all existing memories\n- Remove all shards" : "- Re-embed all memories with new model\n- This may take severales"}\n\nContinue?`
602
+ )
603
+ ) {
604
+ return;
605
+ }
606
+
607
+ showToast("Running migration... This may take a while.", "info");
608
+
609
+ const result = await fetchAPI("/api/migration/run", {
610
+ method: "POST",
611
+ headers: { "Content-Type": "application/json" },
612
+ body: JSON.stringify({ strategy }),
613
+ });
614
+
615
+ if (result.success) {
616
+ const data = result.data;
617
+ let message = `Migration complete! `;
618
+
619
+ if (strategy === "fresh-start") {
620
+ message += `Deleted ${data.deletedShards} shard(s). Duration: ${(data.duration / 1000).toFixed(2)}s`;
621
+ } else {
622
+ message += `Re-embedded ${data.reEddedMemories} memories. Duration: ${(data.duration / 1000).toFixed(2)}s`;
623
+ }
624
+
625
+ showToast(message, "success");
626
+
627
+ document.getElementById("migration-section").classList.add("hidden");
628
+ document.getElementById("migration-confirm-checkbox").checked = false;
629
+
630
+ await loadMemories();
631
+ await loadStats();
632
+ } else {
633
+ showToast(result.error || "Migration failed", "error");
634
+ }
635
+ }
636
+
637
+ document.addEventListener("DOMContentLoaded", async () => {
638
+ document.getElementById("scope-filter").addEventListener("change", handleFilterChange);
639
+ document.getElementById("tag-filter").addEventListener("change", () => {
640
+ state.selectedTag = document.getElementById("tag-filter").value;
641
+ state.currentPage = 1;
642
+ loadMemories();
643
+ });
644
+
645
+ document.getElementById("add-scope").addEventListener("change", handleAddScopeChange);
646
+
647
+ document.getElementById("search-btn").addEventListener("click", performSearch);
648
+ document.getElementById("clear-search-btn").addEventListener("click", clearSearch);
649
+ document.getElementById("search-input").addEventListener("keypress", (e) => {
650
+ if (e.key === "Enter") performSearch();
651
+ });
652
+
653
+ document.getElementById("add-form").addEventListener("submit", addMemory);
654
+ document.getElementById("edit-form").addEventListener("submit", saveEdit);
655
+ document.getElementById("modal-close").addEventListener("click", closeModal);
656
+ document.getElementById("cancel-edit").addEventListener("click", closeModal);
657
+
658
+ document.getElementById("prev-page-top").addEventListener("click", () => changePage(-1));
659
+ document.getElementById("next-page-top").addEventListener("click", () => changePage(1));
660
+ document.getElementById("prev-page-bottom").addEventListener("click", () => changePage(-1));
661
+ document.getElementById("next-page-bottom").addEventListener("click", () => changePage(1));
662
+
663
+ document.getElementById("bulk-delete-btn").addEventListener("click", bulkDelete);
664
+ document.getElementById("deselect-all-btn").addEventListener("click", deselectAll);
665
+
666
+ document.getElementById("cleanup-btn").addEventListener("click", runCleanup);
667
+ document.getElementById("deduplicate-btn").addEventListener("click", runDeduplication);
668
+
669
+ document
670
+ .getElementById("migration-confirm-checkbox")
671
+ .addEventListener("change", toggleMigrationButtons);
672
+ document
673
+ .getElementById("migration-fresh-btn")
674
+ .addEventListener("click", () => runMigration("fresh-start"));
675
+ document
676
+ .getElementById("migration-reembed-btn")
677
+ .addEventListener("click", () => runMigration("re-embed"));
678
+
679
+ document.getElementById("edit-modal").addEventListener("click", (e) => {
680
+ if (e.target.id === "edit-modal") closeModal();
681
+ });
682
+
683
+ await loadTags();
684
+ await loadMemories();
685
+ await loadStats();
686
+ await checkMigrationStatus();
687
+
688
+ startAutoRefresh();
689
+
690
+ lucide.createIcons();
691
+ });
Binary file
@@ -0,0 +1,14 @@
1
+ <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="64" height="64" fill="#0a0a0a"/>
3
+ <rect x="4" y="4" width="56" height="56" stroke="#00ff00" stroke-width="2" fill="none"/>
4
+ <path d="M16 20 L24 20 L24 28 L16 28 Z" fill="#00ff00"/>
5
+ <path d="M28 20 L36 20 L36 28 L28 28 Z" fill="#00ff00" opacity="0.7"/>
6
+ <path d="M40 20 L48 20 L48 28 L40 28 Z" fill="#00ff00" opacity="0.4"/>
7
+ <path d="M16 32 L24 32 L24 40 L16 40 Z" fill="#00ff00" opacity="0.7"/>
8
+ <path d="M28 32 L36 32 L36 40 L28 40 Z" fill="#00ff00"/>
9
+ <path d="M40 32 L48 32 L48 40 L40 40 Z" fill="#00ff00" opacity="0.7"/>
10
+ <path d="M16 44 L24 44 L24 52 L16 52 Z" fill="#00ff00" opacity="0.4"/>
11
+ <path d="M28 44 L36 44 L36 52 L28 52 Z" fill="#00ff00" opacity="0.7"/>
12
+ <path d="M40 44 L48 44 L48 52 L40 52 Z" fill="#00ff00"/>
13
+ <circle cx="32" cy="32" r="3" fill="#00ff00"/>
14
+ </svg>