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.
@@ -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 dbPath = this.getShardPath(scope, scopeHash, shardIndex);
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, dbPath, now);
96
- const db = connectionManager.getConnection(dbPath);
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, dbPath });
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 FLOAT[${CONFIG.embeddingDimensions}]
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 stmt = this.metadataDb.prepare(`SELECT * FROM shards WHERE db_path = ?`);
187
- const row = stmt.get(dbPath);
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 db_path FROM shards WHERE id = ?`);
215
+ const stmt = this.metadataDb.prepare(`SELECT * FROM shards WHERE id = ?`);
203
216
  const row = stmt.get(shardId);
204
217
  if (row) {
205
- connectionManager.closeConnection(row.db_path);
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(row.db_path)) {
209
- fs.unlinkSync(row.db_path);
222
+ if (fs.existsSync(fullPath)) {
223
+ fs.unlinkSync(fullPath);
210
224
  }
211
225
  }
212
226
  catch (error) {
213
- log("Error deleting shard file", { dbPath: row.db_path, error: String(error) });
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: row.db_path });
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;IAkCtD,aAAa,CACX,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,GACZ,YAAY,EAAE;IA4CX,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;IAiB1B,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQlD,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"}
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 rows = stmt.all(queryBuffer, limit * 2, containerTag);
44
- return rows.map((row) => ({
45
- id: row.id,
46
- memory: row.content,
47
- similarity: 1 - row.distance,
48
- metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
49
- containerTag: row.container_tag,
50
- displayName: row.display_name,
51
- userName: row.user_name,
52
- userEmail: row.user_email,
53
- projectPath: row.project_path,
54
- projectName: row.project_name,
55
- gitRepoUrl: row.git_repo_url,
56
- isPinned: row.is_pinned,
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
- const deleteVec = db.prepare(`DELETE FROM vec_memories WHERE memory_id = ?`);
75
- deleteVec.run(memoryId);
76
- const deleteMemory = db.prepare(`DELETE FROM memories WHERE id = ?`);
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(`
@@ -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) {
@@ -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">
@@ -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: grid;
1280
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
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
- grid-column: 1 / -1;
1342
+ width: 100%;
1294
1343
  }
1295
1344
 
1296
1345
  .dashboard-section h4 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-mem",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
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",