opencode-mem 1.0.0 → 2.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 (71) hide show
  1. package/README.md +80 -477
  2. package/dist/config.d.ts +5 -5
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +46 -20
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +28 -88
  7. package/dist/plugin.d.ts.map +1 -1
  8. package/dist/plugin.js +1 -8
  9. package/dist/services/ai/ai-provider-factory.d.ts +8 -0
  10. package/dist/services/ai/ai-provider-factory.d.ts.map +1 -0
  11. package/dist/services/ai/ai-provider-factory.js +25 -0
  12. package/dist/services/ai/providers/anthropic-messages.d.ts +13 -0
  13. package/dist/services/ai/providers/anthropic-messages.d.ts.map +1 -0
  14. package/dist/services/ai/providers/anthropic-messages.js +176 -0
  15. package/dist/services/ai/providers/base-provider.d.ts +21 -0
  16. package/dist/services/ai/providers/base-provider.d.ts.map +1 -0
  17. package/dist/services/ai/providers/base-provider.js +6 -0
  18. package/dist/services/ai/providers/openai-chat-completion.d.ts +12 -0
  19. package/dist/services/ai/providers/openai-chat-completion.d.ts.map +1 -0
  20. package/dist/services/ai/providers/openai-chat-completion.js +181 -0
  21. package/dist/services/ai/providers/openai-responses.d.ts +14 -0
  22. package/dist/services/ai/providers/openai-responses.d.ts.map +1 -0
  23. package/dist/services/ai/providers/openai-responses.js +191 -0
  24. package/dist/services/ai/session/ai-session-manager.d.ts +21 -0
  25. package/dist/services/ai/session/ai-session-manager.d.ts.map +1 -0
  26. package/dist/services/ai/session/ai-session-manager.js +165 -0
  27. package/dist/services/ai/session/session-types.d.ts +43 -0
  28. package/dist/services/ai/session/session-types.d.ts.map +1 -0
  29. package/dist/services/ai/session/session-types.js +1 -0
  30. package/dist/services/ai/tools/tool-schema.d.ts +41 -0
  31. package/dist/services/ai/tools/tool-schema.d.ts.map +1 -0
  32. package/dist/services/ai/tools/tool-schema.js +24 -0
  33. package/dist/services/api-handlers.d.ts +11 -3
  34. package/dist/services/api-handlers.d.ts.map +1 -1
  35. package/dist/services/api-handlers.js +143 -30
  36. package/dist/services/auto-capture.d.ts +1 -30
  37. package/dist/services/auto-capture.d.ts.map +1 -1
  38. package/dist/services/auto-capture.js +199 -396
  39. package/dist/services/cleanup-service.d.ts +3 -0
  40. package/dist/services/cleanup-service.d.ts.map +1 -1
  41. package/dist/services/cleanup-service.js +31 -4
  42. package/dist/services/client.d.ts +1 -0
  43. package/dist/services/client.d.ts.map +1 -1
  44. package/dist/services/client.js +3 -11
  45. package/dist/services/sqlite/connection-manager.d.ts.map +1 -1
  46. package/dist/services/sqlite/connection-manager.js +8 -4
  47. package/dist/services/user-memory-learning.d.ts +3 -0
  48. package/dist/services/user-memory-learning.d.ts.map +1 -0
  49. package/dist/services/user-memory-learning.js +157 -0
  50. package/dist/services/user-prompt/user-prompt-manager.d.ts +38 -0
  51. package/dist/services/user-prompt/user-prompt-manager.d.ts.map +1 -0
  52. package/dist/services/user-prompt/user-prompt-manager.js +164 -0
  53. package/dist/services/web-server-worker.js +27 -6
  54. package/dist/services/web-server.d.ts.map +1 -1
  55. package/dist/services/web-server.js +0 -5
  56. package/dist/types/index.d.ts +5 -1
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/dist/web/app.js +210 -120
  59. package/dist/web/index.html +14 -10
  60. package/dist/web/styles.css +326 -1
  61. package/package.json +4 -1
  62. package/dist/services/compaction.d.ts +0 -92
  63. package/dist/services/compaction.d.ts.map +0 -1
  64. package/dist/services/compaction.js +0 -421
  65. package/dist/services/sqlite-client.d.ts +0 -116
  66. package/dist/services/sqlite-client.d.ts.map +0 -1
  67. package/dist/services/sqlite-client.js +0 -284
  68. package/dist/services/web-server-lock.d.ts +0 -12
  69. package/dist/services/web-server-lock.d.ts.map +0 -1
  70. package/dist/services/web-server-lock.js +0 -157
  71. package/dist/web/favicon.svg +0 -14
@@ -35,14 +35,12 @@ export class WebServer {
35
35
  const response = event.data;
36
36
  if (response.type === "started") {
37
37
  this.isOwner = true;
38
- log("Web server started (owner)", { url: response.url });
39
38
  resolve();
40
39
  }
41
40
  else if (response.type === "error") {
42
41
  const errorMsg = response.error || "Unknown error";
43
42
  if (errorMsg.includes("EADDRINUSE") || errorMsg.includes("address already in use")) {
44
43
  this.isOwner = false;
45
- log("Web server already running (port in use)");
46
44
  resolve();
47
45
  }
48
46
  else {
@@ -76,7 +74,6 @@ export class WebServer {
76
74
  }
77
75
  async stop() {
78
76
  if (!this.isOwner || !this.worker) {
79
- log("Web server stop skipped (not owner or no worker)");
80
77
  return;
81
78
  }
82
79
  return new Promise((resolve) => {
@@ -85,7 +82,6 @@ export class WebServer {
85
82
  this.worker.terminate();
86
83
  this.worker = null;
87
84
  }
88
- log("Web server stopped (timeout, forced termination)");
89
85
  resolve();
90
86
  }, 5000);
91
87
  this.worker.onmessage = (event) => {
@@ -96,7 +92,6 @@ export class WebServer {
96
92
  this.worker.terminate();
97
93
  this.worker = null;
98
94
  }
99
- log("Web server stopped (owner exiting)");
100
95
  resolve();
101
96
  }
102
97
  };
@@ -32,11 +32,14 @@ export interface ConversationIngestResponse {
32
32
  }
33
33
  export interface MemoryMetadata {
34
34
  type?: MemoryType;
35
- source?: "manual" | "auto-capture" | "import" | "api";
35
+ source?: "manual" | "auto-capture" | "import" | "api" | "user-learning";
36
36
  tool?: string;
37
37
  sessionID?: string;
38
38
  reasoning?: string;
39
39
  captureTimestamp?: number;
40
+ promptCount?: number;
41
+ analysisTimestamp?: number;
42
+ promptId?: string;
40
43
  displayName?: string;
41
44
  userName?: string;
42
45
  userEmail?: string;
@@ -45,4 +48,5 @@ export interface MemoryMetadata {
45
48
  gitRepoUrl?: string;
46
49
  [key: string]: unknown;
47
50
  }
51
+ export type AIProviderType = "openai-chat" | "openai-responses" | "anthropic";
48
52
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,CAAC;AAE7C,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExE,MAAM,MAAM,uBAAuB,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,uBAAuB,EAAE,CAAC;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,CAAC;AAE7C,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExE,MAAM,MAAM,uBAAuB,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,uBAAuB,EAAE,CAAC;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,KAAK,GAAG,eAAe,CAAC;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,kBAAkB,GAAG,WAAW,CAAC"}
package/dist/web/app.js CHANGED
@@ -8,13 +8,25 @@ const state = {
8
8
  totalPages: 1,
9
9
  totalItems: 0,
10
10
  selectedTag: "",
11
- selectedScope: "",
11
+ currentScope: "project",
12
12
  searchQuery: "",
13
13
  isSearching: false,
14
14
  selectedMemories: new Set(),
15
15
  autoRefreshInterval: null,
16
16
  };
17
17
 
18
+ marked.setOptions({
19
+ gfm: true,
20
+ breaks: true,
21
+ headerIds: false,
22
+ mangle: false,
23
+ });
24
+
25
+ function renderMarkdown(markdown) {
26
+ const html = marked.parse(markdown);
27
+ return DOMPurify.sanitize(html);
28
+ }
29
+
18
30
  async function fetchAPI(endpoint, options = {}) {
19
31
  try {
20
32
  const response = await fetch(API_BASE + endpoint, options);
@@ -41,21 +53,21 @@ function populateTagDropdowns() {
41
53
  tagFilter.innerHTML = '<option value="">All Tags</option>';
42
54
  addTag.innerHTML = '<option value="">Select tag</option>';
43
55
 
44
- const allTags = [...state.tags.user, ...state.tags.project];
45
- allTags.forEach((tagInfo) => {
46
- const scope = tagInfo.tag.includes("_user_") ? "user" : "project";
56
+ const scopeTags = state.currentScope === "user" ? state.tags.user : state.tags.project;
57
+
58
+ scopeTags.forEach((tagInfo) => {
47
59
  const displayText = tagInfo.displayName || tagInfo.tag;
48
60
  const shortDisplay =
49
61
  displayText.length > 50 ? displayText.substring(0, 50) + "..." : displayText;
50
62
 
51
63
  const option1 = document.createElement("option");
52
64
  option1.value = tagInfo.tag;
53
- option1.textContent = `[${scope}] ${shortDisplay}`;
65
+ option1.textContent = shortDisplay;
54
66
  tagFilter.appendChild(option1);
55
67
 
56
68
  const option2 = document.createElement("option");
57
69
  option2.value = tagInfo.tag;
58
- option2.textContent = `[${scope}] ${shortDisplay}`;
70
+ option2.textContent = shortDisplay;
59
71
  addTag.appendChild(option2);
60
72
  });
61
73
  }
@@ -63,7 +75,7 @@ function populateTagDropdowns() {
63
75
  async function loadMemories() {
64
76
  showRefreshIndicator(true);
65
77
 
66
- let endpoint = `/api/memories?page=${state.currentPage}&pageSize=${state.pageSize}`;
78
+ let endpoint = `/api/memories?page=${state.currentPage}&pageSize=${state.pageSize}&scope=${state.currentScope}&includePrompts=true`;
67
79
 
68
80
  if (state.isSearching && state.searchQuery) {
69
81
  endpoint = `/api/search?q=${encodeURIComponent(state.searchQuery)}&page=${state.currentPage}&pageSize=${state.pageSize}`;
@@ -103,66 +115,12 @@ function renderMemories() {
103
115
  }
104
116
 
105
117
  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
+ .map((item) => {
119
+ if (item.type === "prompt") {
120
+ return renderPromptCard(item);
121
+ } else {
122
+ return renderMemoryCard(item);
118
123
  }
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
124
  })
167
125
  .join("");
168
126
 
@@ -173,6 +131,102 @@ function renderMemories() {
173
131
  lucide.createIcons();
174
132
  }
175
133
 
134
+ function renderPromptCard(prompt) {
135
+ const isLinked = !!prompt.linkedMemoryId;
136
+ const isSelected = state.selectedMemories.has(prompt.id);
137
+ const promptDate = formatDate(prompt.createdAt);
138
+
139
+ return `
140
+ <div class="prompt-card ${isSelected ? "selected" : ""}" data-id="${prompt.id}">
141
+ <div class="prompt-header">
142
+ <div class="meta">
143
+ <input type="checkbox" class="memory-checkbox" data-id="${prompt.id}" ${isSelected ? "checked" : ""} />
144
+ <i data-lucide="message-circle" class="icon"></i>
145
+ <span class="badge badge-prompt">USER PROMPT</span>
146
+ ${isLinked ? '<span class="badge badge-linked">🔗 LINKED</span>' : ""}
147
+ <span class="prompt-date">${promptDate}</span>
148
+ </div>
149
+ <div class="prompt-actions">
150
+ <button class="btn-delete" onclick="deletePromptWithLink('${prompt.id}', ${isLinked})">
151
+ <i data-lucide="trash-2" class="icon"></i>
152
+ ${isLinked ? "Delete Pair" : "Delete"}
153
+ </button>
154
+ </div>
155
+ </div>
156
+ <div class="prompt-content">
157
+ ${escapeHtml(prompt.content)}
158
+ </div>
159
+ ${isLinked ? '<div class="link-indicator">↓ Generated memory above ↑</div>' : ""}
160
+ </div>
161
+ `;
162
+ }
163
+
164
+ function renderMemoryCard(memory) {
165
+ const isSelected = state.selectedMemories.has(memory.id);
166
+ const isPinned = memory.isPinned || false;
167
+ const isLinked = !!memory.linkedPromptId;
168
+ const similarityHtml =
169
+ memory.similarity !== undefined
170
+ ? `<span class="similarity-score">${memory.similarity}%</span>`
171
+ : "";
172
+
173
+ let displayInfo = memory.displayName || memory.id;
174
+ if (memory.scope === "project" && memory.projectPath) {
175
+ const pathParts = memory.projectPath.split("/");
176
+ displayInfo = pathParts[pathParts.length - 1] || memory.projectPath;
177
+ }
178
+
179
+ let subtitle = "";
180
+ if (memory.scope === "user" && memory.userEmail) {
181
+ subtitle = `<span class="memory-subtitle">${escapeHtml(memory.userEmail)}</span>`;
182
+ } else if (memory.scope === "project" && memory.projectPath) {
183
+ subtitle = `<span class="memory-subtitle">${escapeHtml(memory.projectPath)}</span>`;
184
+ }
185
+
186
+ const pinButton = isPinned
187
+ ? `<button class="btn-pin pinned" onclick="unpinMemory('${memory.id}')" title="Unpin"><i data-lucide="pin" class="icon icon-filled"></i></button>`
188
+ : `<button class="btn-pin" onclick="pinMemory('${memory.id}')" title="Pin"><i data-lucide="pin" class="icon"></i></button>`;
189
+
190
+ const createdDate = formatDate(memory.createdAt);
191
+ const updatedDate =
192
+ memory.updatedAt && memory.updatedAt !== memory.createdAt ? formatDate(memory.updatedAt) : null;
193
+
194
+ const dateInfo = updatedDate
195
+ ? `<span>Created: ${createdDate}</span><span>Updated: ${updatedDate}</span>`
196
+ : `<span>Created: ${createdDate}</span>`;
197
+
198
+ return `
199
+ <div class="memory-card ${isSelected ? "selected" : ""} ${isPinned ? "pinned" : ""}" data-id="${memory.id}">
200
+ <div class="memory-header">
201
+ <div class="meta">
202
+ <input type="checkbox" class="memory-checkbox" data-id="${memory.id}" ${isSelected ? "checked" : ""} />
203
+ <span class="badge badge-${memory.scope}">${memory.scope}</span>
204
+ ${memory.memoryType ? `<span class="badge badge-type">${memory.memoryType}</span>` : ""}
205
+ ${isLinked ? '<span class="badge badge-linked">🔗 LINKED</span>' : ""}
206
+ ${similarityHtml}
207
+ ${isPinned ? '<span class="badge badge-pinned">PINNED</span>' : ""}
208
+ <span class="memory-display-name">${escapeHtml(displayInfo)}</span>
209
+ ${subtitle}
210
+ </div>
211
+ <div class="memory-actions">
212
+ ${pinButton}
213
+ <button class="btn-edit" onclick="editMemory('${memory.id}')"><i data-lucide="edit-3" class="icon"></i></button>
214
+ <button class="btn-delete" onclick="deleteMemoryWithLink('${memory.id}', ${isLinked})">
215
+ <i data-lucide="trash-2" class="icon"></i>
216
+ ${isLinked ? "Delete Pair" : "Delete"}
217
+ </button>
218
+ </div>
219
+ </div>
220
+ <div class="memory-content markdown-content">${renderMarkdown(memory.content)}</div>
221
+ ${isLinked ? '<div class="link-indicator">↑ From prompt below ↓</div>' : ""}
222
+ <div class="memory-footer">
223
+ ${dateInfo}
224
+ <span>ID: ${memory.id}</span>
225
+ </div>
226
+ </div>
227
+ `;
228
+ }
229
+
176
230
  function handleCheckboxChange(e) {
177
231
  const id = e.target.dataset.id;
178
232
  if (e.target.checked) {
@@ -185,7 +239,9 @@ function handleCheckboxChange(e) {
185
239
  }
186
240
 
187
241
  function updateCardSelection(id, selected) {
188
- const card = document.querySelector(`.memory-card[data-id="${id}"]`);
242
+ const card = document.querySelector(
243
+ `.memory-card[data-id="${id}"], .prompt-card[data-id="${id}"]`
244
+ );
189
245
  if (card) {
190
246
  if (selected) {
191
247
  card.classList.add("selected");
@@ -222,9 +278,10 @@ function updatePagination() {
222
278
  }
223
279
 
224
280
  function updateSectionTitle() {
281
+ const scopeName = state.currentScope.toUpperCase();
225
282
  const title = state.isSearching
226
283
  ? `└─ SEARCH RESULTS (${state.totalItems}) ──`
227
- : `└─ MEMORIES (${state.totalItems}) ──`;
284
+ : `└─ ${scopeName} MEMORIES (${state.totalItems}) ──`;
228
285
  document.getElementById("section-title").textContent = title;
229
286
  }
230
287
 
@@ -238,6 +295,20 @@ async function loadStats() {
238
295
  }
239
296
  }
240
297
 
298
+ function switchScope(scope) {
299
+ state.currentScope = scope;
300
+ state.currentPage = 1;
301
+ state.selectedTag = "";
302
+
303
+ document.querySelectorAll(".tab-btn").forEach((btn) => {
304
+ btn.classList.remove("active");
305
+ });
306
+ document.getElementById(`tab-${scope}`).classList.add("active");
307
+
308
+ populateTagDropdowns();
309
+ loadMemories();
310
+ }
311
+
241
312
  async function addMemory(e) {
242
313
  e.preventDefault();
243
314
 
@@ -266,58 +337,103 @@ async function addMemory(e) {
266
337
  }
267
338
  }
268
339
 
269
- async function deleteMemory(id) {
270
- if (!confirm("Delete this memory?")) return;
340
+ async function deleteMemoryWithLink(id, isLinked) {
341
+ const message = isLinked ? "Delete this memory AND its linked prompt?" : "Delete this memory?";
271
342
 
272
- const result = await fetchAPI(`/api/memories/${id}`, { method: "DELETE" });
343
+ if (!confirm(message)) return;
344
+
345
+ const result = await fetchAPI(`/api/memories/${id}?cascade=true`, {
346
+ method: "DELETE",
347
+ });
273
348
 
274
349
  if (result.success) {
275
- showToast("Memory deleted", "success");
350
+ const msg = result.data?.deletedPrompt ? "Memory and linked prompt deleted" : "Memory deleted";
351
+ showToast(msg, "success");
352
+
276
353
  state.selectedMemories.delete(id);
277
354
  await loadMemories();
278
355
  await loadStats();
279
- updateBulkActions();
280
356
  } else {
281
- showToast(result.error || "Failed to delete memory", "error");
357
+ showToast(result.error || "Failed to delete", "error");
282
358
  }
283
359
  }
284
360
 
285
- async function bulkDelete() {
286
- if (state.selectedMemories.size === 0) return;
361
+ async function deletePromptWithLink(id, isLinked) {
362
+ const message = isLinked
363
+ ? "Delete this prompt AND its linked memory summary?"
364
+ : "Delete this prompt?";
287
365
 
288
- if (!confirm(`Delete ${state.selectedMemories.size} selected memories?`)) return;
366
+ if (!confirm(message)) return;
289
367
 
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 }),
368
+ const result = await fetchAPI(`/api/prompts/${id}?cascade=true`, {
369
+ method: "DELETE",
295
370
  });
296
371
 
297
372
  if (result.success) {
298
- showToast(`Deleted ${result.data.deleted} memories`, "success");
299
- state.selectedMemories.clear();
373
+ const msg = result.data?.deletedMemory ? "Prompt and linked memory deleted" : "Prompt deleted";
374
+ showToast(msg, "success");
375
+
376
+ state.selectedMemories.delete(id);
300
377
  await loadMemories();
301
378
  await loadStats();
302
- updateBulkActions();
303
379
  } else {
304
- showToast(result.error || "Failed to delete memories", "error");
380
+ showToast(result.error || "Failed to delete", "error");
381
+ }
382
+ }
383
+
384
+ async function bulkDelete() {
385
+ if (state.selectedMemories.size === 0) return;
386
+
387
+ const message = `Delete ${state.selectedMemories.size} selected items (including linked pairs)?`;
388
+ if (!confirm(message)) return;
389
+
390
+ const ids = Array.from(state.selectedMemories);
391
+
392
+ const promptIds = ids.filter((id) => id.startsWith("prompt_"));
393
+ const memoryIds = ids.filter((id) => !id.startsWith("prompt_"));
394
+
395
+ let deletedCount = 0;
396
+
397
+ if (promptIds.length > 0) {
398
+ const result = await fetchAPI("/api/prompts/bulk-delete", {
399
+ method: "POST",
400
+ headers: { "Content-Type": "application/json" },
401
+ body: JSON.stringify({ ids: promptIds, cascade: true }),
402
+ });
403
+ if (result.success) deletedCount += result.data.deleted;
305
404
  }
405
+
406
+ if (memoryIds.length > 0) {
407
+ const result = await fetchAPI("/api/memories/bulk-delete", {
408
+ method: "POST",
409
+ headers: { "Content-Type": "application/json" },
410
+ body: JSON.stringify({ ids: memoryIds, cascade: true }),
411
+ });
412
+ if (result.success) deletedCount += result.data.deleted;
413
+ }
414
+
415
+ showToast(`Deleted ${deletedCount} items (including linked pairs)`, "success");
416
+ state.selectedMemories.clear();
417
+ await loadMemories();
418
+ await loadStats();
419
+ updateBulkActions();
306
420
  }
307
421
 
308
422
  function deselectAll() {
309
423
  state.selectedMemories.clear();
310
424
  document.querySelectorAll(".memory-checkbox").forEach((cb) => (cb.checked = false));
311
- document.querySelectorAll(".memory-card").forEach((card) => card.classList.remove("selected"));
425
+ document
426
+ .querySelectorAll(".memory-card, .prompt-card")
427
+ .forEach((card) => card.classList.remove("selected"));
312
428
  updateBulkActions();
313
429
  }
314
430
 
315
431
  function editMemory(id) {
316
- const memory = state.memories.find((m) => m.id === id);
432
+ const memory = state.memories.find((m) => m.id === id && m.type === "memory");
317
433
  if (!memory) return;
318
434
 
319
435
  document.getElementById("edit-id").value = memory.id;
320
- document.getElementById("edit-type").value = memory.type || "";
436
+ document.getElementById("edit-type").value = memory.memoryType || "";
321
437
  document.getElementById("edit-content").value = memory.content;
322
438
 
323
439
  document.getElementById("edit-modal").classList.remove("hidden");
@@ -390,34 +506,6 @@ function changePage(delta) {
390
506
  loadMemories();
391
507
  }
392
508
 
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
509
  function handleAddScopeChange() {
422
510
  const scope = document.getElementById("add-scope").value;
423
511
  const tagDropdown = document.getElementById("add-tag");
@@ -598,7 +686,7 @@ async function runMigration(strategy) {
598
686
 
599
687
  if (
600
688
  !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?`
689
+ `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 several minutes"}\n\nContinue?`
602
690
  )
603
691
  ) {
604
692
  return;
@@ -619,7 +707,7 @@ async function runMigration(strategy) {
619
707
  if (strategy === "fresh-start") {
620
708
  message += `Deleted ${data.deletedShards} shard(s). Duration: ${(data.duration / 1000).toFixed(2)}s`;
621
709
  } else {
622
- message += `Re-embedded ${data.reEddedMemories} memories. Duration: ${(data.duration / 1000).toFixed(2)}s`;
710
+ message += `Re-embedded ${data.reEmbeddedMemories} memories. Duration: ${(data.duration / 1000).toFixed(2)}s`;
623
711
  }
624
712
 
625
713
  showToast(message, "success");
@@ -635,7 +723,9 @@ async function runMigration(strategy) {
635
723
  }
636
724
 
637
725
  document.addEventListener("DOMContentLoaded", async () => {
638
- document.getElementById("scope-filter").addEventListener("change", handleFilterChange);
726
+ document.getElementById("tab-project").addEventListener("click", () => switchScope("project"));
727
+ document.getElementById("tab-user").addEventListener("click", () => switchScope("user"));
728
+
639
729
  document.getElementById("tag-filter").addEventListener("change", () => {
640
730
  state.selectedTag = document.getElementById("tag-filter").value;
641
731
  state.currentPage = 1;
@@ -7,6 +7,8 @@
7
7
  <link rel="stylesheet" href="/styles.css" />
8
8
  <link rel="icon" type="image/x-icon" href="/favicon.ico" />
9
9
  <script src="https://unpkg.com/lucide@latest"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/marked@17.0.1/lib/marked.umd.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.2/dist/purify.min.js"></script>
10
12
  </head>
11
13
  <body>
12
14
  <div class="container">
@@ -33,16 +35,18 @@
33
35
  </div>
34
36
  </header>
35
37
 
36
- <div class="controls">
37
- <div class="filter-group">
38
- <label>Scope:</label>
39
- <select id="scope-filter">
40
- <option value="">All</option>
41
- <option value="user">User</option>
42
- <option value="project">Project</option>
43
- </select>
44
- </div>
38
+ <div class="scope-tabs">
39
+ <button id="tab-project" class="tab-btn active">
40
+ <i data-lucide="folder" class="icon"></i>
41
+ PROJECT TIMELINE
42
+ </button>
43
+ <button id="tab-user" class="tab-btn">
44
+ <i data-lucide="user" class="icon"></i>
45
+ USER TIMELINE
46
+ </button>
47
+ </div>
45
48
 
49
+ <div class="controls">
46
50
  <div class="filter-group">
47
51
  <label>Tag:</label>
48
52
  <select id="tag-filter">
@@ -97,7 +101,7 @@
97
101
 
98
102
  <div class="memories-section">
99
103
  <div class="section-header">
100
- <h2 id="section-title">└─ MEMORIES (0) ──</h2>
104
+ <h2 id="section-title">└─ PROJECT MEMORIES (0) ──</h2>
101
105
  <div class="pagination" id="pagination-top">
102
106
  <button id="prev-page-top" disabled>
103
107
  <i data-lucide="chevron-left" class="icon"></i>