opencode-mem 2.8.4 → 2.8.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA8ZA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;oBA2Bb,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwCT,OAAO,GACP,QAAQ;;CAEf,CAAC;AAEF,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAobA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;oBA2Bb,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwCT,OAAO,GACP,QAAQ;;CAEf,CAAC;AAEF,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
package/dist/config.js CHANGED
@@ -126,6 +126,7 @@ const CONFIG_TEMPLATE = `{
126
126
  // Optional: Use OpenAI-compatible API for embeddings
127
127
  // "embeddingApiUrl": "https://api.openai.com/v1",
128
128
  // "embeddingApiKey": "sk-...",
129
+ // "embeddingModel": "text-embedding-3-small", // 1536 dims, auto-detected
129
130
 
130
131
  // ============================================
131
132
  // Web Server Settings
@@ -302,6 +303,7 @@ function ensureConfigExists() {
302
303
  ensureConfigExists();
303
304
  function getEmbeddingDimensions(model) {
304
305
  const dimensionMap = {
306
+ // Local Xenova models
305
307
  "Xenova/nomic-embed-text-v1": 768,
306
308
  "Xenova/nomic-embed-text-v1-unsupervised": 768,
307
309
  "Xenova/nomic-embed-text-v1-ablated": 768,
@@ -317,6 +319,22 @@ function getEmbeddingDimensions(model) {
317
319
  "Xenova/gte-small": 384,
318
320
  "Xenova/GIST-small-Embedding-v0": 384,
319
321
  "Xenova/text-embedding-ada-002": 1536,
322
+ // OpenAI API models
323
+ "text-embedding-3-small": 1536,
324
+ "text-embedding-3-large": 3072,
325
+ "text-embedding-ada-002": 1536,
326
+ // Cohere API models
327
+ "embed-english-v3.0": 1024,
328
+ "embed-multilingual-v3.0": 1024,
329
+ "embed-english-light-v3.0": 384,
330
+ "embed-multilingual-light-v3.0": 384,
331
+ // Google API models
332
+ "text-embedding-004": 768,
333
+ "text-multilingual-embedding-002": 768,
334
+ // Voyage AI models
335
+ "voyage-3": 1024,
336
+ "voyage-3-lite": 512,
337
+ "voyage-code-3": 1024,
320
338
  };
321
339
  return dimensionMap[model] || 768;
322
340
  }
@@ -10,6 +10,17 @@ export declare class ShardManager {
10
10
  getAllShards(scope: "user" | "project", scopeHash: string): ShardInfo[];
11
11
  createShard(scope: "user" | "project", scopeHash: string, shardIndex: number): ShardInfo;
12
12
  private initShardDb;
13
+ /**
14
+ * Check if the shard DB file exists and contains the required 'memories' table.
15
+ * Returns false if the file is missing or the table doesn't exist.
16
+ */
17
+ private isShardValid;
18
+ /**
19
+ * Ensure the shard DB has all required tables. If tables are missing,
20
+ * re-initialize them. This handles cases where the DB file exists but
21
+ * was corrupted or partially created.
22
+ */
23
+ private ensureShardTables;
13
24
  getWriteShard(scope: "user" | "project", scopeHash: string): ShardInfo;
14
25
  private markShardReadOnly;
15
26
  incrementVectorCount(shardId: number): void;
@@ -1 +1 @@
1
- {"version":3,"file":"shard-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/shard-manager.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAW;IAC7B,OAAO,CAAC,YAAY,CAAS;;IAQ7B,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,iBAAiB;IAKzB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAsB9E,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE;IAgCvE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;IA2BxF,OAAO,CAAC,WAAW;IA2DnB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS;IAetE,OAAO,CAAC,iBAAiB;IAOzB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAkBhD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAqBnC;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
1
+ {"version":3,"file":"shard-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/shard-manager.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAW;IAC7B,OAAO,CAAC,YAAY,CAAS;;IAQ7B,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,iBAAiB;IAKzB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAsB9E,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE;IAgCvE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;IA2BxF,OAAO,CAAC,WAAW;IA2DnB;;;OAGG;IACH,OAAO,CAAC,YAAY;IA8BpB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAYzB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS;IAqCtE,OAAO,CAAC,iBAAiB;IAOzB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAoBhD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CA0BnC;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { join, basename, isAbsolute } from "node:path";
3
+ import { existsSync } from "node:fs";
3
4
  import { CONFIG } from "../../config.js";
4
5
  import { connectionManager } from "./connection-manager.js";
5
6
  import { log } from "../logger.js";
@@ -163,11 +164,75 @@ export class ShardManager {
163
164
  db.run(`CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at DESC)`);
164
165
  db.run(`CREATE INDEX IF NOT EXISTS idx_is_pinned ON memories(is_pinned)`);
165
166
  }
167
+ /**
168
+ * Check if the shard DB file exists and contains the required 'memories' table.
169
+ * Returns false if the file is missing or the table doesn't exist.
170
+ */
171
+ isShardValid(shard) {
172
+ if (!existsSync(shard.dbPath)) {
173
+ log("Shard DB file missing", { dbPath: shard.dbPath, shardId: shard.id });
174
+ return false;
175
+ }
176
+ try {
177
+ const db = connectionManager.getConnection(shard.dbPath);
178
+ const result = db
179
+ .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='memories'`)
180
+ .get();
181
+ if (!result) {
182
+ log("Shard DB missing 'memories' table", {
183
+ dbPath: shard.dbPath,
184
+ shardId: shard.id,
185
+ });
186
+ return false;
187
+ }
188
+ return true;
189
+ }
190
+ catch (error) {
191
+ log("Error validating shard DB", {
192
+ dbPath: shard.dbPath,
193
+ error: String(error),
194
+ });
195
+ return false;
196
+ }
197
+ }
198
+ /**
199
+ * Ensure the shard DB has all required tables. If tables are missing,
200
+ * re-initialize them. This handles cases where the DB file exists but
201
+ * was corrupted or partially created.
202
+ */
203
+ ensureShardTables(shard) {
204
+ try {
205
+ const db = connectionManager.getConnection(shard.dbPath);
206
+ this.initShardDb(db);
207
+ }
208
+ catch (error) {
209
+ log("Error ensuring shard tables", {
210
+ dbPath: shard.dbPath,
211
+ error: String(error),
212
+ });
213
+ }
214
+ }
166
215
  getWriteShard(scope, scopeHash) {
167
216
  let shard = this.getActiveShard(scope, scopeHash);
168
217
  if (!shard) {
169
218
  return this.createShard(scope, scopeHash, 0);
170
219
  }
220
+ // Validate that the shard DB file exists and has required tables
221
+ if (!this.isShardValid(shard)) {
222
+ log("Active shard is invalid, recreating", {
223
+ scope,
224
+ scopeHash,
225
+ shardIndex: shard.shardIndex,
226
+ dbPath: shard.dbPath,
227
+ });
228
+ // Close any cached connection to the invalid shard
229
+ connectionManager.closeConnection(shard.dbPath);
230
+ // Remove the stale metadata record
231
+ const deleteStmt = this.metadataDb.prepare(`DELETE FROM shards WHERE id = ?`);
232
+ deleteStmt.run(shard.id);
233
+ // Create a fresh shard with the same index
234
+ return this.createShard(scope, scopeHash, shard.shardIndex);
235
+ }
171
236
  if (shard.vectorCount >= CONFIG.maxVectorsPerShard) {
172
237
  this.markShardReadOnly(shard.id);
173
238
  return this.createShard(scope, scopeHash, shard.shardIndex + 1);
@@ -222,7 +287,10 @@ export class ShardManager {
222
287
  }
223
288
  }
224
289
  catch (error) {
225
- log("Error deleting shard file", { dbPath: fullPath, error: String(error) });
290
+ log("Error deleting shard file", {
291
+ dbPath: fullPath,
292
+ error: String(error),
293
+ });
226
294
  }
227
295
  const deleteStmt = this.metadataDb.prepare(`DELETE FROM shards WHERE id = ?`);
228
296
  deleteStmt.run(shardId);
@@ -4,7 +4,7 @@ interface WebServerConfig {
4
4
  enabled: boolean;
5
5
  }
6
6
  export declare class WebServer {
7
- private worker;
7
+ private server;
8
8
  private config;
9
9
  private isOwner;
10
10
  private startPromise;
@@ -22,6 +22,9 @@ export declare class WebServer {
22
22
  isServerOwner(): boolean;
23
23
  getUrl(): string;
24
24
  checkServerAvailable(): Promise<boolean>;
25
+ private handleRequest;
26
+ private serveStaticFile;
27
+ private jsonResponse;
25
28
  }
26
29
  export declare function startWebServer(config: WebServerConfig): Promise<WebServer>;
27
30
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"web-server.d.ts","sourceRoot":"","sources":["../../src/services/web-server.ts"],"names":[],"mappings":"AAOA,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAeD,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,mBAAmB,CAA+B;IAC1D,OAAO,CAAC,kBAAkB,CAAsC;gBAEpD,MAAM,EAAE,eAAe;IAInC,qBAAqB,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAIpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YASd,MAAM;IAiFpB,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,mBAAmB;YAOb,eAAe;IA2BvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmC3B,SAAS,IAAI,OAAO;IAIpB,aAAa,IAAI,OAAO;IAIxB,MAAM,IAAI,MAAM;IAIV,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;CAW/C;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAIhF"}
1
+ {"version":3,"file":"web-server.d.ts","sourceRoot":"","sources":["../../src/services/web-server.ts"],"names":[],"mappings":"AAiCA,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAA6C;IAC3D,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,mBAAmB,CAA+B;IAC1D,OAAO,CAAC,kBAAkB,CAAsC;gBAEpD,MAAM,EAAE,eAAe;IAInC,qBAAqB,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAIpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YASd,MAAM;IAgCpB,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,mBAAmB;YAOb,eAAe;IA+BvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,SAAS,IAAI,OAAO;IAIpB,aAAa,IAAI,OAAO;IAIxB,MAAM,IAAI,MAAM;IAIV,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;YAchC,aAAa;IAmN3B,OAAO,CAAC,eAAe;IA4BvB,OAAO,CAAC,YAAY;CAWrB;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAIhF"}
@@ -1,10 +1,12 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { join, dirname } from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { log } from "./logger.js";
5
+ import { handleListTags, handleListMemories, handleAddMemory, handleDeleteMemory, handleBulkDelete, handleUpdateMemory, handleSearch, handleStats, handlePinMemory, handleUnpinMemory, handleRunCleanup, handleRunDeduplication, handleDetectMigration, handleRunMigration, handleDetectTagMigration, handleRunTagMigrationBatch, handleGetTagMigrationProgress, handleDeletePrompt, handleBulkDeletePrompts, handleGetUserProfile, handleGetProfileChangelog, handleGetProfileSnapshot, handleRefreshProfile, } from "./api-handlers.js";
4
6
  const __filename = fileURLToPath(import.meta.url);
5
7
  const __dirname = dirname(__filename);
6
8
  export class WebServer {
7
- worker = null;
9
+ server = null;
8
10
  config;
9
11
  isOwner = false;
10
12
  startPromise = null;
@@ -28,70 +30,28 @@ export class WebServer {
28
30
  return;
29
31
  }
30
32
  try {
31
- const workerPath = join(__dirname, "web-server-worker.js");
32
- this.worker = new Worker(workerPath);
33
- const startedPromise = new Promise((resolve, reject) => {
34
- const timeout = setTimeout(() => {
35
- reject(new Error("Worker start timeout"));
36
- }, 10000);
37
- this.worker.onmessage = (event) => {
38
- clearTimeout(timeout);
39
- const response = event.data;
40
- if (response.type === "started") {
41
- this.isOwner = true;
42
- resolve();
43
- }
44
- else if (response.type === "error") {
45
- const errorMsg = response.error || "Unknown error";
46
- if (errorMsg.includes("EADDRINUSE") ||
47
- errorMsg.includes("address already in use") ||
48
- /^Error: Failed to start server\. Is port \d+ in use\?$/.test(errorMsg)) {
49
- this.isOwner = false;
50
- resolve();
51
- }
52
- else {
53
- log("Web server worker error", { error: errorMsg });
54
- reject(new Error(errorMsg));
55
- }
56
- }
57
- };
58
- this.worker.onerror = (error) => {
59
- clearTimeout(timeout);
60
- const errorDetails = {
61
- message: error.message || "Unknown error",
62
- filename: error.filename || "unknown",
63
- lineno: error.lineno || 0,
64
- colno: error.colno || 0,
65
- error: error.error ? String(error.error) : "no error object",
66
- type: error.type || "error",
67
- };
68
- log("Web server worker error (detailed)", errorDetails);
69
- const errorMsg = error.message
70
- ? `${error.message} (at ${error.filename}:${error.lineno}:${error.colno})`
71
- : error.error
72
- ? String(error.error)
73
- : `Worker failed: ${JSON.stringify(errorDetails)}`;
74
- reject(new Error(errorMsg));
75
- };
76
- });
77
- this.worker.postMessage({
78
- type: "start",
33
+ this.server = Bun.serve({
79
34
  port: this.config.port,
80
- host: this.config.host,
35
+ hostname: this.config.host,
36
+ fetch: this.handleRequest.bind(this),
81
37
  });
82
- await startedPromise;
83
- if (!this.isOwner) {
84
- this.startHealthCheckLoop();
85
- }
38
+ this.isOwner = true;
86
39
  }
87
40
  catch (error) {
88
- this.isOwner = false;
89
- if (this.worker) {
90
- this.worker.terminate();
91
- this.worker = null;
41
+ const errorMsg = String(error);
42
+ if (errorMsg.includes("EADDRINUSE") ||
43
+ errorMsg.includes("address already in use") ||
44
+ /Failed to start server.*Is port \d+ in use/.test(errorMsg)) {
45
+ this.isOwner = false;
46
+ this.server = null;
47
+ this.startHealthCheckLoop();
48
+ }
49
+ else {
50
+ this.isOwner = false;
51
+ this.server = null;
52
+ log("Web server failed to start", { error: errorMsg });
53
+ throw error;
92
54
  }
93
- log("Web server failed to start", { error: String(error) });
94
- throw error;
95
55
  }
96
56
  }
97
57
  startHealthCheckLoop() {
@@ -121,15 +81,18 @@ export class WebServer {
121
81
  return;
122
82
  }
123
83
  try {
84
+ // Reset startPromise so _start() can run again
85
+ this.startPromise = null;
124
86
  await this._start();
125
- this.isOwner = true;
126
- log("Web server takeover successful", { port: this.config.port });
127
- if (this.onTakeoverCallback) {
128
- try {
129
- await this.onTakeoverCallback();
130
- }
131
- catch (error) {
132
- log("Takeover callback error", { error: String(error) });
87
+ if (this.isOwner) {
88
+ log("Web server takeover successful", { port: this.config.port });
89
+ if (this.onTakeoverCallback) {
90
+ try {
91
+ await this.onTakeoverCallback();
92
+ }
93
+ catch (error) {
94
+ log("Takeover callback error", { error: String(error) });
95
+ }
133
96
  }
134
97
  }
135
98
  }
@@ -139,35 +102,15 @@ export class WebServer {
139
102
  }
140
103
  async stop() {
141
104
  this.stopHealthCheckLoop();
142
- if (!this.isOwner || !this.worker) {
105
+ if (!this.isOwner || !this.server) {
143
106
  return;
144
107
  }
145
- return new Promise((resolve) => {
146
- const timeout = setTimeout(() => {
147
- if (this.worker) {
148
- this.worker.terminate();
149
- this.worker = null;
150
- }
151
- resolve();
152
- }, 5000);
153
- this.worker.onmessage = (event) => {
154
- clearTimeout(timeout);
155
- const response = event.data;
156
- if (response.type === "stopped") {
157
- if (this.worker) {
158
- this.worker.terminate();
159
- this.worker = null;
160
- }
161
- resolve();
162
- }
163
- };
164
- this.worker.postMessage({
165
- type: "stop",
166
- });
167
- });
108
+ this.server.stop();
109
+ this.server = null;
110
+ this.isOwner = false;
168
111
  }
169
112
  isRunning() {
170
- return this.worker !== null;
113
+ return this.server !== null;
171
114
  }
172
115
  isServerOwner() {
173
116
  return this.isOwner;
@@ -187,6 +130,221 @@ export class WebServer {
187
130
  return false;
188
131
  }
189
132
  }
133
+ // --- HTTP request handling (inlined from web-server-worker.ts) ---
134
+ async handleRequest(req) {
135
+ const url = new URL(req.url);
136
+ const path = url.pathname;
137
+ const method = req.method;
138
+ try {
139
+ if (path === "/" || path === "/index.html") {
140
+ return this.serveStaticFile("index.html", "text/html");
141
+ }
142
+ if (path === "/styles.css") {
143
+ return this.serveStaticFile("styles.css", "text/css");
144
+ }
145
+ if (path === "/app.js") {
146
+ return this.serveStaticFile("app.js", "application/javascript");
147
+ }
148
+ if (path === "/favicon.ico") {
149
+ return this.serveStaticFile("favicon.ico", "image/x-icon");
150
+ }
151
+ if (path === "/api/tags" && method === "GET") {
152
+ const result = await handleListTags();
153
+ return this.jsonResponse(result);
154
+ }
155
+ if (path === "/api/memories" && method === "GET") {
156
+ const tag = url.searchParams.get("tag") || undefined;
157
+ const page = parseInt(url.searchParams.get("page") || "1");
158
+ const pageSize = parseInt(url.searchParams.get("pageSize") || "20");
159
+ const includePrompts = url.searchParams.get("includePrompts") !== "false";
160
+ const result = await handleListMemories(tag, page, pageSize, includePrompts);
161
+ return this.jsonResponse(result);
162
+ }
163
+ if (path === "/api/memories" && method === "POST") {
164
+ const body = (await req.json());
165
+ const result = await handleAddMemory(body);
166
+ return this.jsonResponse(result);
167
+ }
168
+ if (path.startsWith("/api/memories/") && method === "DELETE") {
169
+ const parts = path.split("/");
170
+ const id = parts[3];
171
+ if (!id || id === "bulk-delete") {
172
+ return this.jsonResponse({ success: false, error: "Invalid ID" });
173
+ }
174
+ const cascade = url.searchParams.get("cascade") === "true";
175
+ const result = await handleDeleteMemory(id, cascade);
176
+ return this.jsonResponse(result);
177
+ }
178
+ if (path.startsWith("/api/memories/") && method === "PUT") {
179
+ const id = path.split("/").pop();
180
+ if (!id) {
181
+ return this.jsonResponse({ success: false, error: "Invalid ID" });
182
+ }
183
+ const body = (await req.json());
184
+ const result = await handleUpdateMemory(id, body);
185
+ return this.jsonResponse(result);
186
+ }
187
+ if (path === "/api/memories/bulk-delete" && method === "POST") {
188
+ const body = (await req.json());
189
+ const cascade = body.cascade !== false;
190
+ const result = await handleBulkDelete(body.ids || [], cascade);
191
+ return this.jsonResponse(result);
192
+ }
193
+ if (path === "/api/search" && method === "GET") {
194
+ const query = url.searchParams.get("q");
195
+ const tag = url.searchParams.get("tag") || undefined;
196
+ const page = parseInt(url.searchParams.get("page") || "1");
197
+ const pageSize = parseInt(url.searchParams.get("pageSize") || "20");
198
+ if (!query) {
199
+ return this.jsonResponse({ success: false, error: "query parameter required" });
200
+ }
201
+ const result = await handleSearch(query, tag, page, pageSize);
202
+ return this.jsonResponse(result);
203
+ }
204
+ if (path === "/api/stats" && method === "GET") {
205
+ const result = await handleStats();
206
+ return this.jsonResponse(result);
207
+ }
208
+ if (path.match(/^\/api\/memories\/[^/]+\/pin$/) && method === "POST") {
209
+ const id = path.split("/")[3];
210
+ if (!id) {
211
+ return this.jsonResponse({ success: false, error: "Invalid ID" });
212
+ }
213
+ const result = await handlePinMemory(id);
214
+ return this.jsonResponse(result);
215
+ }
216
+ if (path.match(/^\/api\/memories\/[^/]+\/unpin$/) && method === "POST") {
217
+ const id = path.split("/")[3];
218
+ if (!id) {
219
+ return this.jsonResponse({ success: false, error: "Invalid ID" });
220
+ }
221
+ const result = await handleUnpinMemory(id);
222
+ return this.jsonResponse(result);
223
+ }
224
+ if (path === "/api/cleanup" && method === "POST") {
225
+ const result = await handleRunCleanup();
226
+ return this.jsonResponse(result);
227
+ }
228
+ if (path === "/api/deduplicate" && method === "POST") {
229
+ const result = await handleRunDeduplication();
230
+ return this.jsonResponse(result);
231
+ }
232
+ if (path === "/api/migration/detect" && method === "GET") {
233
+ const result = await handleDetectMigration();
234
+ return this.jsonResponse(result);
235
+ }
236
+ if (path === "/api/migration/tags/detect" && method === "GET") {
237
+ const result = await handleDetectTagMigration();
238
+ return this.jsonResponse(result);
239
+ }
240
+ if (path === "/api/migration/tags/run-batch" && method === "POST") {
241
+ const body = (await req.json());
242
+ const batchSize = body?.batchSize || 5;
243
+ const result = await handleRunTagMigrationBatch(batchSize);
244
+ return this.jsonResponse(result);
245
+ }
246
+ if (path === "/api/migration/tags/progress" && method === "GET") {
247
+ const result = await handleGetTagMigrationProgress();
248
+ return this.jsonResponse(result);
249
+ }
250
+ if (path === "/api/migration/run" && method === "POST") {
251
+ const body = (await req.json());
252
+ const strategy = body.strategy || "fresh-start";
253
+ if (strategy !== "fresh-start" && strategy !== "re-embed") {
254
+ return this.jsonResponse({ success: false, error: "Invalid strategy" });
255
+ }
256
+ const result = await handleRunMigration(strategy);
257
+ return this.jsonResponse(result);
258
+ }
259
+ if (path.startsWith("/api/prompts/") && method === "DELETE") {
260
+ const parts = path.split("/");
261
+ const id = parts[3];
262
+ if (!id || id === "bulk-delete") {
263
+ return this.jsonResponse({ success: false, error: "Invalid ID" });
264
+ }
265
+ const cascade = url.searchParams.get("cascade") === "true";
266
+ const result = await handleDeletePrompt(id, cascade);
267
+ return this.jsonResponse(result);
268
+ }
269
+ if (path === "/api/prompts/bulk-delete" && method === "POST") {
270
+ const body = (await req.json());
271
+ const cascade = body.cascade !== false;
272
+ const result = await handleBulkDeletePrompts(body.ids || [], cascade);
273
+ return this.jsonResponse(result);
274
+ }
275
+ if (path === "/api/user-profile" && method === "GET") {
276
+ const userId = url.searchParams.get("userId") || undefined;
277
+ const result = await handleGetUserProfile(userId);
278
+ return this.jsonResponse(result);
279
+ }
280
+ if (path === "/api/user-profile/changelog" && method === "GET") {
281
+ const profileId = url.searchParams.get("profileId");
282
+ const limit = parseInt(url.searchParams.get("limit") || "5");
283
+ if (!profileId) {
284
+ return this.jsonResponse({ success: false, error: "profileId parameter required" });
285
+ }
286
+ const result = await handleGetProfileChangelog(profileId, limit);
287
+ return this.jsonResponse(result);
288
+ }
289
+ if (path === "/api/user-profile/snapshot" && method === "GET") {
290
+ const changelogId = url.searchParams.get("chlogId");
291
+ if (!changelogId) {
292
+ return this.jsonResponse({ success: false, error: "changelogId parameter required" });
293
+ }
294
+ const result = await handleGetProfileSnapshot(changelogId);
295
+ return this.jsonResponse(result);
296
+ }
297
+ if (path === "/api/user-profile/refresh" && method === "POST") {
298
+ const body = (await req.json().catch(() => ({})));
299
+ const userId = body.userId || undefined;
300
+ const result = await handleRefreshProfile(userId);
301
+ return this.jsonResponse(result);
302
+ }
303
+ return new Response("Not Found", { status: 404 });
304
+ }
305
+ catch (error) {
306
+ return this.jsonResponse({
307
+ success: false,
308
+ error: String(error),
309
+ }, 500);
310
+ }
311
+ }
312
+ serveStaticFile(filename, contentType) {
313
+ try {
314
+ const webDir = join(__dirname, "..", "web");
315
+ const filePath = join(webDir, filename);
316
+ if (contentType.startsWith("image/")) {
317
+ const content = readFileSync(filePath);
318
+ return new Response(content, {
319
+ headers: {
320
+ "Content-Type": contentType,
321
+ "Cache-Control": "public, max-age=86400",
322
+ },
323
+ });
324
+ }
325
+ const content = readFileSync(filePath, "utf-8");
326
+ return new Response(content, {
327
+ headers: {
328
+ "Content-Type": contentType,
329
+ "Cache-Control": "no-cache",
330
+ },
331
+ });
332
+ }
333
+ catch (error) {
334
+ return new Response("File not found", { status: 404 });
335
+ }
336
+ }
337
+ jsonResponse(data, status = 200) {
338
+ return new Response(JSON.stringify(data), {
339
+ status,
340
+ headers: {
341
+ "Content-Type": "application/json",
342
+ "Access-Control-Allow-Origin": "*",
343
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
344
+ "Access-Control-Allow-Headers": "Content-Type",
345
+ },
346
+ });
347
+ }
190
348
  }
191
349
  export async function startWebServer(config) {
192
350
  const server = new WebServer(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-mem",
3
- "version": "2.8.4",
3
+ "version": "2.8.7",
4
4
  "description": "OpenCode plugin that gives coding agents persistent memory using local vector database",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",