opencode-mem 2.5.0 → 2.6.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.
- package/README.md +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -162
- package/dist/services/api-handlers.d.ts +14 -0
- package/dist/services/api-handlers.d.ts.map +1 -1
- package/dist/services/api-handlers.js +145 -97
- package/dist/services/auto-capture.d.ts.map +1 -1
- package/dist/services/auto-capture.js +11 -3
- package/dist/services/client.d.ts +1 -0
- package/dist/services/client.d.ts.map +1 -1
- package/dist/services/client.js +8 -1
- package/dist/services/sqlite/connection-manager.d.ts +1 -0
- package/dist/services/sqlite/connection-manager.d.ts.map +1 -1
- package/dist/services/sqlite/connection-manager.js +20 -0
- package/dist/services/sqlite/shard-manager.d.ts +1 -0
- package/dist/services/sqlite/shard-manager.d.ts.map +1 -1
- package/dist/services/sqlite/shard-manager.js +32 -18
- package/dist/services/sqlite/types.d.ts +3 -0
- package/dist/services/sqlite/types.d.ts.map +1 -1
- package/dist/services/sqlite/vector-search.d.ts.map +1 -1
- package/dist/services/sqlite/vector-search.js +67 -43
- package/dist/services/tags.js +3 -3
- package/dist/services/web-server-worker.js +9 -1
- package/dist/web/app.js +111 -1
- package/dist/web/index.html +28 -0
- package/dist/web/styles.css +52 -3
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, basename, isAbsolute } from "node:path";
|
|
3
3
|
import { CONFIG } from "../../config.js";
|
|
4
4
|
import { connectionManager } from "./connection-manager.js";
|
|
5
5
|
import { log } from "../logger.js";
|
|
@@ -35,6 +35,10 @@ export class ShardManager {
|
|
|
35
35
|
const dir = join(CONFIG.storagePath, `${scope}s`);
|
|
36
36
|
return join(dir, `${scope}_${scopeHash}_shard_${shardIndex}.db`);
|
|
37
37
|
}
|
|
38
|
+
resolveStoredPath(storedPath, scope) {
|
|
39
|
+
const fileName = basename(storedPath);
|
|
40
|
+
return join(CONFIG.storagePath, `${scope}s`, fileName);
|
|
41
|
+
}
|
|
38
42
|
getActiveShard(scope, scopeHash) {
|
|
39
43
|
const stmt = this.metadataDb.prepare(`
|
|
40
44
|
SELECT * FROM shards
|
|
@@ -49,7 +53,7 @@ export class ShardManager {
|
|
|
49
53
|
scope: row.scope,
|
|
50
54
|
scopeHash: row.scope_hash,
|
|
51
55
|
shardIndex: row.shard_index,
|
|
52
|
-
dbPath: row.db_path,
|
|
56
|
+
dbPath: this.resolveStoredPath(row.db_path, row.scope),
|
|
53
57
|
vectorCount: row.vector_count,
|
|
54
58
|
isActive: row.is_active === 1,
|
|
55
59
|
createdAt: row.created_at,
|
|
@@ -79,29 +83,30 @@ export class ShardManager {
|
|
|
79
83
|
scope: row.scope,
|
|
80
84
|
scopeHash: row.scope_hash,
|
|
81
85
|
shardIndex: row.shard_index,
|
|
82
|
-
dbPath: row.db_path,
|
|
86
|
+
dbPath: this.resolveStoredPath(row.db_path, row.scope),
|
|
83
87
|
vectorCount: row.vector_count,
|
|
84
88
|
isActive: row.is_active === 1,
|
|
85
89
|
createdAt: row.created_at,
|
|
86
90
|
}));
|
|
87
91
|
}
|
|
88
92
|
createShard(scope, scopeHash, shardIndex) {
|
|
89
|
-
const
|
|
93
|
+
const fullPath = this.getShardPath(scope, scopeHash, shardIndex);
|
|
94
|
+
const storedPath = join(`${scope}s`, basename(fullPath)).replace(/\\/g, "/");
|
|
90
95
|
const now = Date.now();
|
|
91
96
|
const stmt = this.metadataDb.prepare(`
|
|
92
97
|
INSERT INTO shards (scope, scope_hash, shard_index, db_path, vector_count, is_active, created_at)
|
|
93
98
|
VALUES (?, ?, ?, ?, 0, 1, ?)
|
|
94
99
|
`);
|
|
95
|
-
const result = stmt.run(scope, scopeHash, shardIndex,
|
|
96
|
-
const db = connectionManager.getConnection(
|
|
100
|
+
const result = stmt.run(scope, scopeHash, shardIndex, storedPath, now);
|
|
101
|
+
const db = connectionManager.getConnection(fullPath);
|
|
97
102
|
this.initShardDb(db);
|
|
98
|
-
log("Shard created", { scope, scopeHash, shardIndex,
|
|
103
|
+
log("Shard created", { scope, scopeHash, shardIndex, fullPath });
|
|
99
104
|
return {
|
|
100
105
|
id: Number(result.lastInsertRowid),
|
|
101
106
|
scope,
|
|
102
107
|
scopeHash,
|
|
103
108
|
shardIndex,
|
|
104
|
-
dbPath,
|
|
109
|
+
dbPath: fullPath,
|
|
105
110
|
vectorCount: 0,
|
|
106
111
|
isActive: true,
|
|
107
112
|
createdAt: now,
|
|
@@ -128,6 +133,7 @@ export class ShardManager {
|
|
|
128
133
|
content TEXT NOT NULL,
|
|
129
134
|
vector BLOB NOT NULL,
|
|
130
135
|
container_tag TEXT NOT NULL,
|
|
136
|
+
tags TEXT,
|
|
131
137
|
type TEXT,
|
|
132
138
|
created_at INTEGER NOT NULL,
|
|
133
139
|
updated_at INTEGER NOT NULL,
|
|
@@ -144,7 +150,13 @@ export class ShardManager {
|
|
|
144
150
|
db.run(`
|
|
145
151
|
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
|
|
146
152
|
memory_id TEXT PRIMARY KEY,
|
|
147
|
-
embedding
|
|
153
|
+
embedding BLOB float32
|
|
154
|
+
)
|
|
155
|
+
`);
|
|
156
|
+
db.run(`
|
|
157
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_tags USING vec0(
|
|
158
|
+
memory_id TEXT PRIMARY KEY,
|
|
159
|
+
embedding BLOB float32
|
|
148
160
|
)
|
|
149
161
|
`);
|
|
150
162
|
db.run(`CREATE INDEX IF NOT EXISTS idx_container_tag ON memories(container_tag)`);
|
|
@@ -183,8 +195,9 @@ export class ShardManager {
|
|
|
183
195
|
stmt.run(shardId);
|
|
184
196
|
}
|
|
185
197
|
getShardByPath(dbPath) {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
198
|
+
const fileName = basename(dbPath);
|
|
199
|
+
const stmt = this.metadataDb.prepare(`SELECT * FROM shards WHERE db_path LIKE '%' || ?`);
|
|
200
|
+
const row = stmt.get(fileName);
|
|
188
201
|
if (!row)
|
|
189
202
|
return null;
|
|
190
203
|
return {
|
|
@@ -192,29 +205,30 @@ export class ShardManager {
|
|
|
192
205
|
scope: row.scope,
|
|
193
206
|
scopeHash: row.scope_hash,
|
|
194
207
|
shardIndex: row.shard_index,
|
|
195
|
-
dbPath: row.db_path,
|
|
208
|
+
dbPath: this.resolveStoredPath(row.db_path, row.scope),
|
|
196
209
|
vectorCount: row.vector_count,
|
|
197
210
|
isActive: row.is_active === 1,
|
|
198
211
|
createdAt: row.created_at,
|
|
199
212
|
};
|
|
200
213
|
}
|
|
201
214
|
deleteShard(shardId) {
|
|
202
|
-
const stmt = this.metadataDb.prepare(`SELECT
|
|
215
|
+
const stmt = this.metadataDb.prepare(`SELECT * FROM shards WHERE id = ?`);
|
|
203
216
|
const row = stmt.get(shardId);
|
|
204
217
|
if (row) {
|
|
205
|
-
|
|
218
|
+
const fullPath = this.resolveStoredPath(row.db_path, row.scope);
|
|
219
|
+
connectionManager.closeConnection(fullPath);
|
|
206
220
|
try {
|
|
207
221
|
const fs = require("node:fs");
|
|
208
|
-
if (fs.existsSync(
|
|
209
|
-
fs.unlinkSync(
|
|
222
|
+
if (fs.existsSync(fullPath)) {
|
|
223
|
+
fs.unlinkSync(fullPath);
|
|
210
224
|
}
|
|
211
225
|
}
|
|
212
226
|
catch (error) {
|
|
213
|
-
log("Error deleting shard file", { dbPath:
|
|
227
|
+
log("Error deleting shard file", { dbPath: fullPath, error: String(error) });
|
|
214
228
|
}
|
|
215
229
|
const deleteStmt = this.metadataDb.prepare(`DELETE FROM shards WHERE id = ?`);
|
|
216
230
|
deleteStmt.run(shardId);
|
|
217
|
-
log("Shard deleted", { shardId, dbPath:
|
|
231
|
+
log("Shard deleted", { shardId, dbPath: fullPath });
|
|
218
232
|
}
|
|
219
233
|
}
|
|
220
234
|
}
|
|
@@ -12,7 +12,9 @@ export interface MemoryRecord {
|
|
|
12
12
|
id: string;
|
|
13
13
|
content: string;
|
|
14
14
|
vector: Float32Array;
|
|
15
|
+
tagsVector?: Float32Array;
|
|
15
16
|
containerTag: string;
|
|
17
|
+
tags?: string;
|
|
16
18
|
type?: string;
|
|
17
19
|
createdAt: number;
|
|
18
20
|
updatedAt: number;
|
|
@@ -28,6 +30,7 @@ export interface SearchResult {
|
|
|
28
30
|
id: string;
|
|
29
31
|
memory: string;
|
|
30
32
|
similarity: number;
|
|
33
|
+
tags?: string[];
|
|
31
34
|
metadata?: Record<string, unknown>;
|
|
32
35
|
displayName?: string;
|
|
33
36
|
userName?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,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;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,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;CACrB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,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;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,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;CACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExE,qBAAa,YAAY;IACvB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExE,qBAAa,YAAY;IACvB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI;IA0CtD,aAAa,CACX,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,GACZ,YAAY,EAAE;IAyEX,kBAAkB,CACtB,MAAM,EAAE,SAAS,EAAE,EACnB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC;IAgB1B,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAMlD,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;IAWtE,cAAc,CAAC,EAAE,EAAE,QAAQ,GAAG,GAAG,EAAE;IAKnC,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAKzD,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAMxD,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM;IAMrC,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,GAAG,EAAE;IAepC,SAAS,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/C,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;CAIlD;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
|
|
@@ -5,56 +5,81 @@ export class VectorSearch {
|
|
|
5
5
|
insertVector(db, record) {
|
|
6
6
|
const insertMemory = db.prepare(`
|
|
7
7
|
INSERT INTO memories (
|
|
8
|
-
id, content, vector, container_tag, type, created_at, updated_at,
|
|
8
|
+
id, content, vector, container_tag, tags, type, created_at, updated_at,
|
|
9
9
|
metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
|
|
10
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
10
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
11
11
|
`);
|
|
12
12
|
const vectorBuffer = new Uint8Array(record.vector.buffer);
|
|
13
|
-
insertMemory.run(record.id, record.content, vectorBuffer, record.containerTag, record.type || null, record.createdAt, record.updatedAt, record.metadata || null, record.displayName || null, record.userName || null, record.userEmail || null, record.projectPath || null, record.projectName || null, record.gitRepoUrl || null);
|
|
13
|
+
insertMemory.run(record.id, record.content, vectorBuffer, record.containerTag, record.tags || null, record.type || null, record.createdAt, record.updatedAt, record.metadata || null, record.displayName || null, record.userName || null, record.userEmail || null, record.projectPath || null, record.projectName || null, record.gitRepoUrl || null);
|
|
14
14
|
const insertVec = db.prepare(`
|
|
15
15
|
INSERT INTO vec_memories (memory_id, embedding) VALUES (?, ?)
|
|
16
16
|
`);
|
|
17
17
|
insertVec.run(record.id, vectorBuffer);
|
|
18
|
+
if (record.tagsVector) {
|
|
19
|
+
const tagsVectorBuffer = new Uint8Array(record.tagsVector.buffer);
|
|
20
|
+
const insertTagsVec = db.prepare(`
|
|
21
|
+
INSERT INTO vec_tags (memory_id, embedding) VALUES (?, ?)
|
|
22
|
+
`);
|
|
23
|
+
insertTagsVec.run(record.id, tagsVectorBuffer);
|
|
24
|
+
}
|
|
18
25
|
}
|
|
19
26
|
searchInShard(shard, queryVector, containerTag, limit) {
|
|
20
27
|
const db = connectionManager.getConnection(shard.dbPath);
|
|
21
|
-
const stmt = db.prepare(`
|
|
22
|
-
SELECT
|
|
23
|
-
m.id,
|
|
24
|
-
m.content,
|
|
25
|
-
m.container_tag,
|
|
26
|
-
m.metadata,
|
|
27
|
-
m.display_name,
|
|
28
|
-
m.user_name,
|
|
29
|
-
m.user_email,
|
|
30
|
-
m.project_path,
|
|
31
|
-
m.project_name,
|
|
32
|
-
m.git_repo_url,
|
|
33
|
-
m.is_pinned,
|
|
34
|
-
v.distance
|
|
35
|
-
FROM vec_memories v
|
|
36
|
-
INNER JOIN memories m ON v.memory_id = m.id
|
|
37
|
-
WHERE v.embedding MATCH ?
|
|
38
|
-
AND v.k = ?
|
|
39
|
-
AND m.container_tag = ?
|
|
40
|
-
ORDER BY v.distance
|
|
41
|
-
`);
|
|
42
28
|
const queryBuffer = new Uint8Array(queryVector.buffer);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
29
|
+
const contentResults = db
|
|
30
|
+
.prepare(`
|
|
31
|
+
SELECT memory_id, distance FROM vec_memories
|
|
32
|
+
WHERE embedding MATCH ? AND k = ?
|
|
33
|
+
ORDER BY distance
|
|
34
|
+
`)
|
|
35
|
+
.all(queryBuffer, limit * 4);
|
|
36
|
+
const tagsResults = db
|
|
37
|
+
.prepare(`
|
|
38
|
+
SELECT memory_id, distance FROM vec_tags
|
|
39
|
+
WHERE embedding MATCH ? AND k = ?
|
|
40
|
+
ORDER BY distance
|
|
41
|
+
`)
|
|
42
|
+
.all(queryBuffer, limit * 4);
|
|
43
|
+
const scoreMap = new Map();
|
|
44
|
+
for (const r of contentResults) {
|
|
45
|
+
scoreMap.set(r.memory_id, { contentDist: r.distance, tagsDist: 1 });
|
|
46
|
+
}
|
|
47
|
+
for (const r of tagsResults) {
|
|
48
|
+
const entry = scoreMap.get(r.memory_id) || { contentDist: 1, tagsDist: 1 };
|
|
49
|
+
entry.tagsDist = r.distance;
|
|
50
|
+
scoreMap.set(r.memory_id, entry);
|
|
51
|
+
}
|
|
52
|
+
const ids = Array.from(scoreMap.keys());
|
|
53
|
+
if (ids.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
56
|
+
const rows = db
|
|
57
|
+
.prepare(`
|
|
58
|
+
SELECT * FROM memories
|
|
59
|
+
WHERE id IN (${placeholders}) AND container_tag = ?
|
|
60
|
+
`)
|
|
61
|
+
.all(...ids, containerTag);
|
|
62
|
+
return rows.map((row) => {
|
|
63
|
+
const scores = scoreMap.get(row.id);
|
|
64
|
+
const contentSim = 1 - scores.contentDist;
|
|
65
|
+
const tagsSim = 1 - scores.tagsDist;
|
|
66
|
+
const similarity = tagsSim * 0.8 + contentSim * 0.2;
|
|
67
|
+
return {
|
|
68
|
+
id: row.id,
|
|
69
|
+
memory: row.content,
|
|
70
|
+
similarity,
|
|
71
|
+
tags: row.tags ? row.tags.split(",").map((t) => t.trim()) : [],
|
|
72
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
73
|
+
containerTag: row.container_tag,
|
|
74
|
+
displayName: row.display_name,
|
|
75
|
+
userName: row.user_name,
|
|
76
|
+
userEmail: row.user_email,
|
|
77
|
+
projectPath: row.project_path,
|
|
78
|
+
projectName: row.project_name,
|
|
79
|
+
gitRepoUrl: row.git_repo_url,
|
|
80
|
+
isPinned: row.is_pinned,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
58
83
|
}
|
|
59
84
|
async searchAcrossShards(shards, queryVector, containerTag, limit, similarityThreshold) {
|
|
60
85
|
const allResults = [];
|
|
@@ -71,10 +96,9 @@ export class VectorSearch {
|
|
|
71
96
|
return allResults.filter((r) => r.similarity >= similarityThreshold).slice(0, limit);
|
|
72
97
|
}
|
|
73
98
|
deleteVector(db, memoryId) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
deleteMemory.run(memoryId);
|
|
99
|
+
db.prepare(`DELETE FROM vec_memories WHERE memory_id = ?`).run(memoryId);
|
|
100
|
+
db.prepare(`DELETE FROM vec_tags WHERE memory_id = ?`).run(memoryId);
|
|
101
|
+
db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
|
|
78
102
|
}
|
|
79
103
|
listMemories(db, containerTag, limit) {
|
|
80
104
|
const stmt = db.prepare(`
|
package/dist/services/tags.js
CHANGED
|
@@ -42,8 +42,8 @@ export function getProjectName(directory) {
|
|
|
42
42
|
return parts[parts.length - 1] || directory;
|
|
43
43
|
}
|
|
44
44
|
export function getUserTagInfo() {
|
|
45
|
-
const email = getGitEmail();
|
|
46
|
-
const name = getGitName();
|
|
45
|
+
const email = CONFIG.userEmailOverride || getGitEmail();
|
|
46
|
+
const name = CONFIG.userNameOverride || getGitName();
|
|
47
47
|
if (email) {
|
|
48
48
|
return {
|
|
49
49
|
tag: `${CONFIG.containerTagPrefix}_user_${sha256(email)}`,
|
|
@@ -52,7 +52,7 @@ export function getUserTagInfo() {
|
|
|
52
52
|
userEmail: email,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
-
const fallback = process.env.USER || process.env.USERNAME || "anonymous";
|
|
55
|
+
const fallback = name || process.env.USER || process.env.USERNAME || "anonymous";
|
|
56
56
|
return {
|
|
57
57
|
tag: `${CONFIG.containerTagPrefix}_user_${sha256(fallback)}`,
|
|
58
58
|
displayName: fallback,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { handleListTags, handleListMemories, handleAddMemory, handleDeleteMemory, handleBulkDelete, handleUpdateMemory, handleSearch, handleStats, handlePinMemory, handleUnpinMemory, handleRunCleanup, handleRunDeduplication, handleDetectMigration, handleRunMigration, handleDeletePrompt, handleBulkDeletePrompts, handleGetUserProfile, handleGetProfileChangelog, handleGetProfileSnapshot, handleRefreshProfile, } from "./api-handlers.js";
|
|
4
|
+
import { handleListTags, handleListMemories, handleAddMemory, handleDeleteMemory, handleBulkDelete, handleUpdateMemory, handleSearch, handleStats, handlePinMemory, handleUnpinMemory, handleRunCleanup, handleRunDeduplication, handleDetectMigration, handleRunMigration, handleDetectTagMigration, handleRunTagMigration, handleDeletePrompt, handleBulkDeletePrompts, handleGetUserProfile, handleGetProfileChangelog, handleGetProfileSnapshot, handleRefreshProfile, } from "./api-handlers.js";
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
7
|
let server = null;
|
|
@@ -107,6 +107,14 @@ async function handleRequest(req) {
|
|
|
107
107
|
const result = await handleDetectMigration();
|
|
108
108
|
return jsonResponse(result);
|
|
109
109
|
}
|
|
110
|
+
if (path === "/api/migration/tags/detect" && method === "GET") {
|
|
111
|
+
const result = await handleDetectTagMigration();
|
|
112
|
+
return jsonResponse(result);
|
|
113
|
+
}
|
|
114
|
+
if (path === "/api/migration/tags/run" && method === "POST") {
|
|
115
|
+
const result = await handleRunTagMigration();
|
|
116
|
+
return jsonResponse(result);
|
|
117
|
+
}
|
|
110
118
|
if (path === "/api/migration/run" && method === "POST") {
|
|
111
119
|
const body = (await req.json());
|
|
112
120
|
const strategy = body.strategy || "fresh-start";
|
package/dist/web/app.js
CHANGED
|
@@ -175,6 +175,11 @@ function renderCombinedCard(pair) {
|
|
|
175
175
|
? `<span class="similarity-score">${Math.round(memory.similarity * 100)}%</span>`
|
|
176
176
|
: "";
|
|
177
177
|
|
|
178
|
+
const tagsHtml =
|
|
179
|
+
memory.tags && memory.tags.length > 0
|
|
180
|
+
? `<div class="tags-list">${memory.tags.map((t) => `<span class="tag-badge">${escapeHtml(t)}</span>`).join("")}</div>`
|
|
181
|
+
: "";
|
|
182
|
+
|
|
178
183
|
return `
|
|
179
184
|
<div class="combined-card ${isSelected ? "selected" : ""}" data-id="${memory.id}">
|
|
180
185
|
<div class="combined-prompt-section">
|
|
@@ -204,6 +209,7 @@ function renderCombinedCard(pair) {
|
|
|
204
209
|
</button>
|
|
205
210
|
</div>
|
|
206
211
|
</div>
|
|
212
|
+
${tagsHtml}
|
|
207
213
|
<div class="memory-content markdown-content">${renderMarkdown(memory.content)}</div>
|
|
208
214
|
</div>
|
|
209
215
|
</div>
|
|
@@ -275,6 +281,11 @@ function renderMemoryCard(memory) {
|
|
|
275
281
|
? `<span>Created: ${createdDate}</span><span>Updated: ${updatedDate}</span>`
|
|
276
282
|
: `<span>Created: ${createdDate}</span>`;
|
|
277
283
|
|
|
284
|
+
const tagsHtml =
|
|
285
|
+
memory.tags && memory.tags.length > 0
|
|
286
|
+
? `<div class="tags-list">${memory.tags.map((t) => `<span class="tag-badge">${escapeHtml(t)}</span>`).join("")}</div>`
|
|
287
|
+
: "";
|
|
288
|
+
|
|
278
289
|
return `
|
|
279
290
|
<div class="memory-card ${isSelected ? "selected" : ""} ${isPinned ? "pinned" : ""}" data-id="${memory.id}">
|
|
280
291
|
<div class="memory-header">
|
|
@@ -296,6 +307,7 @@ function renderMemoryCard(memory) {
|
|
|
296
307
|
</button>
|
|
297
308
|
</div>
|
|
298
309
|
</div>
|
|
310
|
+
${tagsHtml}
|
|
299
311
|
<div class="memory-content markdown-content">${renderMarkdown(memory.content)}</div>
|
|
300
312
|
${isLinked ? '<div class="link-indicator"><i data-lucide="arrow-up" class="icon-sm"></i> From prompt below <i data-lucide="arrow-down" class="icon-sm"></i></div>' : ""}
|
|
301
313
|
<div class="memory-footer">
|
|
@@ -376,6 +388,13 @@ async function addMemory(e) {
|
|
|
376
388
|
const content = document.getElementById("add-content").value.trim();
|
|
377
389
|
const containerTag = document.getElementById("add-tag").value;
|
|
378
390
|
const type = document.getElementById("add-type").value.trim();
|
|
391
|
+
const tagsStr = document.getElementById("add-tags").value.trim();
|
|
392
|
+
const tags = tagsStr
|
|
393
|
+
? tagsStr
|
|
394
|
+
.split(",")
|
|
395
|
+
.map((t) => t.trim())
|
|
396
|
+
.filter((t) => t)
|
|
397
|
+
: [];
|
|
379
398
|
|
|
380
399
|
if (!content || !containerTag) {
|
|
381
400
|
showToast("Content and tag are required", "error");
|
|
@@ -385,7 +404,7 @@ async function addMemory(e) {
|
|
|
385
404
|
const result = await fetchAPI("/api/memories", {
|
|
386
405
|
method: "POST",
|
|
387
406
|
headers: { "Content-Type": "application/json" },
|
|
388
|
-
body: JSON.stringify({ content, containerTag, type: type || undefined }),
|
|
407
|
+
body: JSON.stringify({ content, containerTag, type: type || undefined, tags }),
|
|
389
408
|
});
|
|
390
409
|
|
|
391
410
|
if (result.success) {
|
|
@@ -398,6 +417,57 @@ async function addMemory(e) {
|
|
|
398
417
|
}
|
|
399
418
|
}
|
|
400
419
|
|
|
420
|
+
function performSearch() {
|
|
421
|
+
const input = document.getElementById("search-input").value.trim();
|
|
422
|
+
|
|
423
|
+
if (!input) {
|
|
424
|
+
clearSearch();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
state.searchQuery = input;
|
|
429
|
+
state.isSearching = true;
|
|
430
|
+
state.currentPage = 1;
|
|
431
|
+
|
|
432
|
+
document.getElementById("clear-search-btn").classList.remove("hidden");
|
|
433
|
+
|
|
434
|
+
loadMemories();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function loadMemories() {
|
|
438
|
+
showRefreshIndicator(true);
|
|
439
|
+
|
|
440
|
+
let endpoint = `/api/memories?page=${state.currentPage}&pageSize=${state.pageSize}&includePrompts=true`;
|
|
441
|
+
|
|
442
|
+
if (state.isSearching) {
|
|
443
|
+
endpoint = `/api/search?q=${encodeURIComponent(state.searchQuery || "")}&page=${state.currentPage}&pageSize=${state.pageSize}`;
|
|
444
|
+
if (state.selectedTag) {
|
|
445
|
+
endpoint += `&tag=${encodeURIComponent(state.selectedTag)}`;
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
if (state.selectedTag) {
|
|
449
|
+
endpoint += `&tag=${encodeURIComponent(state.selectedTag)}`;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const result = await fetchAPI(endpoint);
|
|
454
|
+
|
|
455
|
+
showRefreshIndicator(false);
|
|
456
|
+
|
|
457
|
+
if (result.success) {
|
|
458
|
+
state.memories = result.data.items;
|
|
459
|
+
state.totalPages = result.data.totalPages;
|
|
460
|
+
state.totalItems = result.data.total;
|
|
461
|
+
state.currentPage = result.data.page;
|
|
462
|
+
|
|
463
|
+
renderMemories();
|
|
464
|
+
updatePagination();
|
|
465
|
+
updateSectionTitle();
|
|
466
|
+
} else {
|
|
467
|
+
showError(result.error || "Failed to load memories");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
401
471
|
async function deleteMemoryWithLink(id, isLinked) {
|
|
402
472
|
const message = isLinked ? "Delete this memory AND its linked prompt?" : "Delete this memory?";
|
|
403
473
|
|
|
@@ -697,6 +767,46 @@ async function checkMigrationStatus() {
|
|
|
697
767
|
if (result.success && result.data.needsMigration) {
|
|
698
768
|
showMigrationWarning(result.data);
|
|
699
769
|
}
|
|
770
|
+
|
|
771
|
+
const tagResult = await fetchAPI("/api/migration/tags/detect");
|
|
772
|
+
if (tagResult.success && tagResult.data.needsMigration) {
|
|
773
|
+
showTagMigrationModal(tagResult.data.count);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function showTagMigrationModal(count) {
|
|
778
|
+
const overlay = document.getElementById("tag-migration-overlay");
|
|
779
|
+
const status = document.getElementById("tag-migration-status");
|
|
780
|
+
status.textContent = `Found ${count} memories needing technical tags.`;
|
|
781
|
+
overlay.classList.remove("hidden");
|
|
782
|
+
|
|
783
|
+
document.getElementById("start-tag-migration-btn").onclick = runTagMigration;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function runTagMigration() {
|
|
787
|
+
const actions = document.getElementById("tag-migration-actions");
|
|
788
|
+
const status = document.getElementById("tag-migration-status");
|
|
789
|
+
const progress = document.getElementById("tag-migration-progress");
|
|
790
|
+
|
|
791
|
+
actions.classList.add("hidden");
|
|
792
|
+
status.textContent = "Running technical tagging... This uses AI and may take a while.";
|
|
793
|
+
progress.style.width = "50%"; // Simple progress for now as it is non-streaming
|
|
794
|
+
|
|
795
|
+
const result = await fetchAPI("/api/migration/tags/run", { method: "POST" });
|
|
796
|
+
|
|
797
|
+
if (result.success) {
|
|
798
|
+
progress.style.width = "100%";
|
|
799
|
+
status.textContent = `Successfully tagged ${result.data.processed} memories!`;
|
|
800
|
+
showToast("Migration complete", "success");
|
|
801
|
+
setTimeout(() => {
|
|
802
|
+
document.getElementById("tag-migration-overlay").classList.add("hidden");
|
|
803
|
+
loadMemories();
|
|
804
|
+
loadStats();
|
|
805
|
+
}, 2000);
|
|
806
|
+
} else {
|
|
807
|
+
status.textContent = "Migration failed: " + result.error;
|
|
808
|
+
actions.classList.remove("hidden");
|
|
809
|
+
}
|
|
700
810
|
}
|
|
701
811
|
|
|
702
812
|
function showMigrationWarning(data) {
|
package/dist/web/index.html
CHANGED
|
@@ -154,6 +154,11 @@
|
|
|
154
154
|
<label>Type:</label>
|
|
155
155
|
<input type="text" id="add-type" placeholder="preference, architecture, etc." />
|
|
156
156
|
</div>
|
|
157
|
+
|
|
158
|
+
<div class="form-group">
|
|
159
|
+
<label>Tags:</label>
|
|
160
|
+
<input type="text" id="add-tags" placeholder="react, hooks, auth (comma separated)" />
|
|
161
|
+
</div>
|
|
157
162
|
</div>
|
|
158
163
|
|
|
159
164
|
<div class="form-group">
|
|
@@ -199,6 +204,29 @@
|
|
|
199
204
|
|
|
200
205
|
<div id="toast" class="toast hidden"></div>
|
|
201
206
|
|
|
207
|
+
<div id="tag-migration-overlay" class="modal hidden">
|
|
208
|
+
<div class="modal-content">
|
|
209
|
+
<div class="modal-header">
|
|
210
|
+
<h3>Memory Tagging Migration</h3>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="migration-progress-body">
|
|
213
|
+
<p id="tag-migration-status">Initializing migration...</p>
|
|
214
|
+
<div class="progress-bar-container">
|
|
215
|
+
<div id="tag-migration-progress" class="progress-bar" style="width: 0%"></div>
|
|
216
|
+
</div>
|
|
217
|
+
<p class="migration-note">
|
|
218
|
+
Please don't close the browser. This will re-vectorize your memories with technical tags
|
|
219
|
+
to improve search accuracy.
|
|
220
|
+
</p>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="modal-actions" id="tag-migration-actions">
|
|
223
|
+
<button id="start-tag-migration-btn" class="btn-primary">
|
|
224
|
+
<i data-lucide="play" class="icon"></i> Start Migration
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
202
230
|
<div id="changelog-modal" class="modal hidden">
|
|
203
231
|
<div class="modal-content">
|
|
204
232
|
<div class="modal-header">
|
package/dist/web/styles.css
CHANGED
|
@@ -458,11 +458,60 @@ button:disabled {
|
|
|
458
458
|
background: rgba(0, 255, 0, 0.05);
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
.tags-list {
|
|
462
|
+
display: flex;
|
|
463
|
+
flex-wrap: wrap;
|
|
464
|
+
gap: 6px;
|
|
465
|
+
margin: 8px 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.tag-badge {
|
|
469
|
+
background: rgba(0, 255, 0, 0.1);
|
|
470
|
+
color: #00ff00;
|
|
471
|
+
border: 1px solid rgba(0, 255, 0, 0.3);
|
|
472
|
+
padding: 2px 8px;
|
|
473
|
+
font-size: 11px;
|
|
474
|
+
border-radius: 12px;
|
|
475
|
+
white-space: nowrap;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.migration-progress-body {
|
|
479
|
+
padding: 20px;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.progress-bar-container {
|
|
483
|
+
width: 100%;
|
|
484
|
+
height: 8px;
|
|
485
|
+
background: #222;
|
|
486
|
+
border-radius: 4px;
|
|
487
|
+
margin: 15px 0;
|
|
488
|
+
overflow: hidden;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.progress-bar {
|
|
492
|
+
height: 100%;
|
|
493
|
+
background: #00ff00;
|
|
494
|
+
transition: width 0.3s ease;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.migration-note {
|
|
498
|
+
font-size: 12px;
|
|
499
|
+
color: #888;
|
|
500
|
+
margin-top: 10px;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#tag-migration-actions {
|
|
504
|
+
padding: 20px;
|
|
505
|
+
border-top: 1px solid #333;
|
|
506
|
+
}
|
|
507
|
+
|
|
461
508
|
.memory-header {
|
|
462
509
|
display: flex;
|
|
463
510
|
justify-content: space-between;
|
|
464
511
|
align-items: flex-start;
|
|
465
512
|
margin-bottom: 10px;
|
|
513
|
+
flex-wrap: wrap;
|
|
514
|
+
gap: 10px;
|
|
466
515
|
}
|
|
467
516
|
|
|
468
517
|
.memory-meta {
|
|
@@ -1276,8 +1325,8 @@ textarea:focus-visible {
|
|
|
1276
1325
|
}
|
|
1277
1326
|
|
|
1278
1327
|
.dashboard-grid {
|
|
1279
|
-
display:
|
|
1280
|
-
|
|
1328
|
+
display: flex;
|
|
1329
|
+
flex-direction: column;
|
|
1281
1330
|
gap: 20px;
|
|
1282
1331
|
}
|
|
1283
1332
|
|
|
@@ -1290,7 +1339,7 @@ textarea:focus-visible {
|
|
|
1290
1339
|
}
|
|
1291
1340
|
|
|
1292
1341
|
.dashboard-section.full-width {
|
|
1293
|
-
|
|
1342
|
+
width: 100%;
|
|
1294
1343
|
}
|
|
1295
1344
|
|
|
1296
1345
|
.dashboard-section h4 {
|