opencode-mem 2.8.3 → 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.
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/services/sqlite/shard-manager.d.ts +11 -0
- package/dist/services/sqlite/shard-manager.d.ts.map +1 -1
- package/dist/services/sqlite/shard-manager.js +69 -1
- package/dist/services/web-server.d.ts +4 -1
- package/dist/services/web-server.d.ts.map +1 -1
- package/dist/services/web-server.js +251 -91
- package/package.json +1 -1
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
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":"
|
|
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", {
|
|
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
|
|
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":"
|
|
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
|
-
|
|
9
|
+
server = null;
|
|
8
10
|
config;
|
|
9
11
|
isOwner = false;
|
|
10
12
|
startPromise = null;
|
|
@@ -28,68 +30,28 @@ export class WebServer {
|
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
32
|
try {
|
|
31
|
-
|
|
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") || errorMsg.includes("address already in use")) {
|
|
47
|
-
this.isOwner = false;
|
|
48
|
-
resolve();
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
log("Web server worker error", { error: errorMsg });
|
|
52
|
-
reject(new Error(errorMsg));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
this.worker.onerror = (error) => {
|
|
57
|
-
clearTimeout(timeout);
|
|
58
|
-
const errorDetails = {
|
|
59
|
-
message: error.message || "Unknown error",
|
|
60
|
-
filename: error.filename || "unknown",
|
|
61
|
-
lineno: error.lineno || 0,
|
|
62
|
-
colno: error.colno || 0,
|
|
63
|
-
error: error.error ? String(error.error) : "no error object",
|
|
64
|
-
type: error.type || "error",
|
|
65
|
-
};
|
|
66
|
-
log("Web server worker error (detailed)", errorDetails);
|
|
67
|
-
const errorMsg = error.message
|
|
68
|
-
? `${error.message} (at ${error.filename}:${error.lineno}:${error.colno})`
|
|
69
|
-
: error.error
|
|
70
|
-
? String(error.error)
|
|
71
|
-
: `Worker failed: ${JSON.stringify(errorDetails)}`;
|
|
72
|
-
reject(new Error(errorMsg));
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
this.worker.postMessage({
|
|
76
|
-
type: "start",
|
|
33
|
+
this.server = Bun.serve({
|
|
77
34
|
port: this.config.port,
|
|
78
|
-
|
|
35
|
+
hostname: this.config.host,
|
|
36
|
+
fetch: this.handleRequest.bind(this),
|
|
79
37
|
});
|
|
80
|
-
|
|
81
|
-
if (!this.isOwner) {
|
|
82
|
-
this.startHealthCheckLoop();
|
|
83
|
-
}
|
|
38
|
+
this.isOwner = true;
|
|
84
39
|
}
|
|
85
40
|
catch (error) {
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
90
54
|
}
|
|
91
|
-
log("Web server failed to start", { error: String(error) });
|
|
92
|
-
throw error;
|
|
93
55
|
}
|
|
94
56
|
}
|
|
95
57
|
startHealthCheckLoop() {
|
|
@@ -119,15 +81,18 @@ export class WebServer {
|
|
|
119
81
|
return;
|
|
120
82
|
}
|
|
121
83
|
try {
|
|
84
|
+
// Reset startPromise so _start() can run again
|
|
85
|
+
this.startPromise = null;
|
|
122
86
|
await this._start();
|
|
123
|
-
this.isOwner
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
96
|
}
|
|
132
97
|
}
|
|
133
98
|
}
|
|
@@ -137,35 +102,15 @@ export class WebServer {
|
|
|
137
102
|
}
|
|
138
103
|
async stop() {
|
|
139
104
|
this.stopHealthCheckLoop();
|
|
140
|
-
if (!this.isOwner || !this.
|
|
105
|
+
if (!this.isOwner || !this.server) {
|
|
141
106
|
return;
|
|
142
107
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.worker.terminate();
|
|
147
|
-
this.worker = null;
|
|
148
|
-
}
|
|
149
|
-
resolve();
|
|
150
|
-
}, 5000);
|
|
151
|
-
this.worker.onmessage = (event) => {
|
|
152
|
-
clearTimeout(timeout);
|
|
153
|
-
const response = event.data;
|
|
154
|
-
if (response.type === "stopped") {
|
|
155
|
-
if (this.worker) {
|
|
156
|
-
this.worker.terminate();
|
|
157
|
-
this.worker = null;
|
|
158
|
-
}
|
|
159
|
-
resolve();
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
this.worker.postMessage({
|
|
163
|
-
type: "stop",
|
|
164
|
-
});
|
|
165
|
-
});
|
|
108
|
+
this.server.stop();
|
|
109
|
+
this.server = null;
|
|
110
|
+
this.isOwner = false;
|
|
166
111
|
}
|
|
167
112
|
isRunning() {
|
|
168
|
-
return this.
|
|
113
|
+
return this.server !== null;
|
|
169
114
|
}
|
|
170
115
|
isServerOwner() {
|
|
171
116
|
return this.isOwner;
|
|
@@ -185,6 +130,221 @@ export class WebServer {
|
|
|
185
130
|
return false;
|
|
186
131
|
}
|
|
187
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
|
+
}
|
|
188
348
|
}
|
|
189
349
|
export async function startWebServer(config) {
|
|
190
350
|
const server = new WebServer(config);
|