@haisto/opencode-mem 2.14.3-beta.7 → 2.16.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +36 -0
  2. package/dist/config.d.ts +3 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +22 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +10 -3
  7. package/dist/services/api-handlers.d.ts.map +1 -1
  8. package/dist/services/api-handlers.js +67 -28
  9. package/dist/services/client.d.ts.map +1 -1
  10. package/dist/services/client.js +27 -1
  11. package/dist/services/context.d.ts +1 -1
  12. package/dist/services/context.d.ts.map +1 -1
  13. package/dist/services/context.js +2 -2
  14. package/dist/services/embedding.d.ts.map +1 -1
  15. package/dist/services/embedding.js +17 -0
  16. package/dist/services/logger.js +1 -1
  17. package/dist/services/sqlite/connection-manager.d.ts +1 -1
  18. package/dist/services/sqlite/connection-manager.d.ts.map +1 -1
  19. package/dist/services/sqlite/sqlite-bootstrap.d.ts +3 -1
  20. package/dist/services/sqlite/sqlite-bootstrap.d.ts.map +1 -1
  21. package/dist/services/sqlite/sqlite-bootstrap.js +90 -4
  22. package/dist/services/sqlite/vector-search.d.ts +1 -1
  23. package/dist/services/sqlite/vector-search.d.ts.map +1 -1
  24. package/dist/services/user-memory-learning.js +6 -0
  25. package/dist/services/user-profile/profile-context.d.ts +1 -1
  26. package/dist/services/user-profile/profile-context.d.ts.map +1 -1
  27. package/dist/services/user-profile/profile-context.js +113 -22
  28. package/dist/services/web-server.d.ts.map +1 -1
  29. package/dist/services/web-server.js +62 -1
  30. package/dist/web/app.js +6 -3
  31. package/dist/web/i18n.js +150 -0
  32. package/dist/web/styles.css +3 -2
  33. package/package.json +5 -1
package/README.md CHANGED
@@ -99,6 +99,9 @@ Configure at `~/.config/opencode/opencode-mem.jsonc`:
99
99
  "excludeCurrentSession": true,
100
100
  "maxAgeDays": undefined,
101
101
  "injectOn": "first",
102
+ "injectMaxPreferences": 5,
103
+ "injectMaxPatterns": 5,
104
+ "injectMaxWorkflows": 3,
102
105
  },
103
106
  }
104
107
  ```
@@ -141,6 +144,39 @@ Supported providers: any provider listed by `opencode providers list` (e.g. `ant
141
144
 
142
145
  Full documentation available in this README.
143
146
 
147
+ ## Public Subpath Exports
148
+
149
+ In addition to the main plugin entry, `opencode-mem` exposes one stable subpath
150
+ that other opencode plugins can import directly. This avoids having to
151
+ reverse-engineer container-tag conventions when writing third-party tools that
152
+ read or write into the same memory store.
153
+
154
+ ### `opencode-mem/tags`
155
+
156
+ Canonical container-tag helpers. The same functions opencode-mem itself uses
157
+ to scope auto-captured memories.
158
+
159
+ ```ts
160
+ import { getProjectTagInfo, getUserTagInfo, getTags } from "opencode-mem/tags";
161
+
162
+ // Canonical project tag derived from cwd (git remote URL if present, else
163
+ // the project root path). Format: `opencode_project_<sha16>`.
164
+ const projectTag = getProjectTagInfo(process.cwd()).tag;
165
+
166
+ // Canonical user tag derived from `git config user.email`.
167
+ // Format: `opencode_user_<sha16>`.
168
+ const userTag = getUserTagInfo().tag;
169
+
170
+ // Both at once.
171
+ const { user, project } = getTags(process.cwd());
172
+ ```
173
+
174
+ Tags produced by these helpers match what auto-capture writes, so third-party
175
+ plugins that call `POST /api/memories` will land in the same shards the rest
176
+ of the system already understands. Hand-rolled tags whose substring isn't
177
+ `_project_` or `_user_` end up in shadow shards that `/api/stats` and
178
+ `/api/memories` silently filter out — using these helpers avoids that pitfall.
179
+
144
180
  ## Development & Contribution
145
181
 
146
182
  Build and test locally:
package/dist/config.d.ts CHANGED
@@ -55,6 +55,9 @@ export declare let CONFIG: {
55
55
  excludeCurrentSession: boolean | undefined;
56
56
  maxAgeDays: number | undefined;
57
57
  injectOn: "first" | "always";
58
+ injectMaxPreferences: number | undefined;
59
+ injectMaxPatterns: number | undefined;
60
+ injectMaxWorkflows: number | undefined;
58
61
  };
59
62
  };
60
63
  export declare function initConfig(directory: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA2jBA,eAAO,IAAI,MAAM;;;;;;;;;;;;;;;;;oBA3DT,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;;;mBASX,eAAe,GACf,SAAS,GACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAuCV,OAAO,GACP,QAAQ;;CAMgC,CAAC;AAEnD,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CASlD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAulBA,eAAO,IAAI,MAAM;;;;;;;;;;;;;;;;;oBAjET,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;;;mBASX,eAAe,GACf,SAAS,GACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAuCV,OAAO,GACP,QAAQ;;;;;CAYgC,CAAC;AAEnD,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CASlD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
package/dist/config.js CHANGED
@@ -59,6 +59,9 @@ const DEFAULTS = {
59
59
  excludeCurrentSession: true,
60
60
  maxAgeDays: undefined,
61
61
  injectOn: "first",
62
+ injectMaxPreferences: 5,
63
+ injectMaxPatterns: 5,
64
+ injectMaxWorkflows: 3,
62
65
  },
63
66
  };
64
67
  function expandPath(path) {
@@ -319,6 +322,22 @@ const CONFIG_TEMPLATE = `{
319
322
  // Maximum number of memories to return in search results
320
323
  "maxMemories": 10,
321
324
 
325
+ // ============================================
326
+ // Chat Message Injection
327
+ // ============================================
328
+
329
+ // Inject relevant memories into AI chat context
330
+ "chatMessage": {
331
+ "enabled": true,
332
+ "maxMemories": 3,
333
+ "excludeCurrentSession": true,
334
+ "maxAgeDays": undefined,
335
+ "injectOn": "first",
336
+ "injectMaxPreferences": 5,
337
+ "injectMaxPatterns": 5,
338
+ "injectMaxWorkflows": 3
339
+ },
340
+
322
341
  // ============================================
323
342
  // Advanced Settings
324
343
  // ============================================
@@ -437,6 +456,9 @@ function buildConfig(fileConfig) {
437
456
  excludeCurrentSession: fileConfig.chatMessage?.excludeCurrentSession ?? DEFAULTS.chatMessage.excludeCurrentSession,
438
457
  maxAgeDays: fileConfig.chatMessage?.maxAgeDays,
439
458
  injectOn: (fileConfig.chatMessage?.injectOn ?? DEFAULTS.chatMessage.injectOn),
459
+ injectMaxPreferences: fileConfig.chatMessage?.injectMaxPreferences ?? DEFAULTS.chatMessage.injectMaxPreferences,
460
+ injectMaxPatterns: fileConfig.chatMessage?.injectMaxPatterns ?? DEFAULTS.chatMessage.injectMaxPatterns,
461
+ injectMaxWorkflows: fileConfig.chatMessage?.injectMaxWorkflows ?? DEFAULTS.chatMessage.injectMaxWorkflows,
440
462
  },
441
463
  };
442
464
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAmB/D,eAAO,MAAM,iBAAiB,EAAE,MA4hB/B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAmB/D,eAAO,MAAM,iBAAiB,EAAE,MAqiB/B,CAAC"}
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { performUserProfileLearning } from "./services/user-memory-learning.js";
8
8
  import { userPromptManager } from "./services/user-prompt/user-prompt-manager.js";
9
9
  import { startWebServer, WebServer } from "./services/web-server.js";
10
10
  import { isConfigured, CONFIG, initConfig } from "./config.js";
11
- import { log } from "./services/logger.js";
11
+ import { log, logTrace } from "./services/logger.js";
12
12
  import { getLanguageName } from "./services/language-detector.js";
13
13
  export const OpenCodeMemPlugin = async (ctx) => {
14
14
  const { directory } = ctx;
@@ -165,8 +165,14 @@ export const OpenCodeMemPlugin = async (ctx) => {
165
165
  const cutoffDate = Date.now() - CONFIG.chatMessage.maxAgeDays * 86400000;
166
166
  memories = memories.filter((m) => new Date(m.createdAt).getTime() > cutoffDate);
167
167
  }
168
- if (memories.length === 0)
168
+ if (memories.length === 0) {
169
+ logTrace("inject: no memories found, skipping injection");
169
170
  return;
171
+ }
172
+ logTrace("inject: memories loaded", {
173
+ count: memories.length,
174
+ summaries: memories.map((m) => m.summary?.slice(0, 120)),
175
+ });
170
176
  const projectMemories = {
171
177
  results: memories.map((m) => ({
172
178
  similarity: 1.0,
@@ -176,8 +182,9 @@ export const OpenCodeMemPlugin = async (ctx) => {
176
182
  timing: 0,
177
183
  };
178
184
  const userId = tags.user.userEmail || null;
179
- const memoryContext = formatContextForPrompt(userId, projectMemories);
185
+ const memoryContext = formatContextForPrompt(userId, projectMemories, userMessage);
180
186
  if (memoryContext) {
187
+ logTrace("inject: memoryContext", { context: memoryContext });
181
188
  const contextPart = {
182
189
  id: `prt-memory-context-${Date.now()}`,
183
190
  sessionID: input.sessionID,
@@ -1 +1 @@
1
- {"version":3,"file":"api-handlers.d.ts","sourceRoot":"","sources":["../../src/services/api-handlers.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,UAAU,WAAW,CAAC,CAAC,GAAG,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,MAAM;IACd,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,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;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,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,UAAU,iBAAiB,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAmDD,wBAAsB,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;IAAE,OAAO,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CAAC,CAoCnF;AAED,wBAAsB,kBAAkB,CACtC,GAAG,CAAC,EAAE,MAAM,EACZ,IAAI,GAAE,MAAU,EAChB,QAAQ,GAAE,MAAW,EACrB,cAAc,GAAE,OAAc,GAC7B,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAuIvD;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,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,GAAG,OAAO,CAAC,WAAW,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAiDvC;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,OAAe,GACvB,OAAO,CAAC,WAAW,CAAC;IAAE,aAAa,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC,CA0BlD;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EAAE,EACb,OAAO,GAAE,OAAe,GACvB,OAAO,CAAC,WAAW,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAa3C;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC7D,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAuD5B;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,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;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,KAAK,gBAAgB,GAAG,eAAe,GAAG,eAAe,CAAC;AAE1D,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,CAAC,EAAE,MAAM,EACZ,IAAI,GAAE,MAAU,EAChB,QAAQ,GAAE,MAAW,GACpB,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC,CAAC,CA6J3D;AAED,wBAAsB,WAAW,IAAI,OAAO,CAC1C,WAAW,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,CAAC,CACH,CA6BA;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAiB5E;AAED,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAiB9E;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAC/C,WAAW,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CAC/E,CASA;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CACrD,WAAW,CAAC;IAAE,sBAAsB,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,CAC5E,CASA;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CACpD,WAAW,CAAC;IACV,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,GAAG,EAAE,CAAC;CACxB,CAAC,CACH,CASA;AAED,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,UAAU,GAAG,OAAO,CACrF,WAAW,CAAC;IACV,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CACH,CASA;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,OAAe,GACvB,OAAO,CAAC,WAAW,CAAC;IAAE,aAAa,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC,CAgBlD;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EAAE,EACb,OAAO,GAAE,OAAe,GACvB,OAAO,CAAC,WAAW,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAa3C;AAED,wBAAsB,oBAAoB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAwCrF;AAED,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,MAAU,GAChB,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,CAkB7B;AAED,wBAAsB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAoB7F;AAED,wBAAsB,oBAAoB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAgDrF;AAED,wBAAsB,8BAA8B,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAczF;AAED,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CActF;AAED,wBAAsB,4BAA4B,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAcvF;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CACvD,WAAW,CAAC;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CACxD,CAeA;AAED,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAWD,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,CAAC,CAE7F;AAED,wBAAsB,0BAA0B,CAC9C,SAAS,GAAE,MAAU,GACpB,OAAO,CAAC,WAAW,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC,CA6G9E"}
1
+ {"version":3,"file":"api-handlers.d.ts","sourceRoot":"","sources":["../../src/services/api-handlers.ts"],"names":[],"mappings":"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"}
@@ -31,6 +31,9 @@ function safeJSONParse(jsonString) {
31
31
  return undefined;
32
32
  }
33
33
  }
34
+ function toBlob(vector) {
35
+ return vector ? new Uint8Array(vector.buffer) : null;
36
+ }
34
37
  function extractScopeFromTag(tag) {
35
38
  const parts = tag.split("_");
36
39
  if (parts.length >= 3) {
@@ -105,11 +108,20 @@ export async function handleListMemories(tag, page = 1, pageSize = 20, includePr
105
108
  }
106
109
  }
107
110
  else {
108
- const shards = shardManager.getAllShards("project", "");
109
- for (const shard of shards) {
111
+ // Iterate both project- and user-scoped shards. Previously this only
112
+ // walked project shards, which silently hid user-scope memories from the
113
+ // listing endpoint (Web UI navigation, /api/memories without a tag
114
+ // filter, …). User-scope memories still showed up in /api/search and
115
+ // /api/stats `byType`, but were invisible in /api/stats `byScope` and
116
+ // unbrowseable in the UI — a confusing UX gap. The filter keeps the
117
+ // defense-in-depth check on container_tag, just widens it to both
118
+ // canonical scope markers.
119
+ const projectShards = shardManager.getAllShards("project", "");
120
+ const userShards = shardManager.getAllShards("user", "");
121
+ for (const shard of [...projectShards, ...userShards]) {
110
122
  const db = connectionManager.getConnection(shard.dbPath);
111
123
  const memories = vectorSearch.getAllMemories(db);
112
- allMemories.push(...memories.filter((m) => m.container_tag?.includes(`_project_`)));
124
+ allMemories.push(...memories.filter((m) => m.container_tag?.includes("_project_") || m.container_tag?.includes("_user_")));
113
125
  }
114
126
  }
115
127
  const memoriesWithType = allMemories.map((r) => {
@@ -262,7 +274,30 @@ export async function handleAddMemory(data) {
262
274
  metadata: JSON.stringify({ source: "api" }),
263
275
  };
264
276
  const db = connectionManager.getConnection(shard.dbPath);
265
- await vectorSearch.insertVector(db, record, shard);
277
+ // Use transaction for atomic SQLite insert
278
+ const insertMemory = db.transaction(() => {
279
+ const insertStmt = db.prepare(`
280
+ INSERT INTO memories (
281
+ id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
282
+ metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
283
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
284
+ `);
285
+ insertStmt.run(record.id, record.content, toBlob(record.vector), toBlob(record.tagsVector), 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);
286
+ });
287
+ insertMemory();
288
+ // Vector index update (outside transaction — vector backend is async)
289
+ try {
290
+ const backend = await vectorSearch.getBackend();
291
+ await backend.insert({ id: record.id, vector: record.vector, shard, kind: "content" });
292
+ if (record.tagsVector) {
293
+ await backend.insert({ id: record.id, vector: record.tagsVector, shard, kind: "tags" });
294
+ }
295
+ }
296
+ catch (error) {
297
+ // Rollback SQLite insert on vector backend failure
298
+ db.prepare(`DELETE FROM memories WHERE id = ?`).run(record.id);
299
+ throw error;
300
+ }
266
301
  shardManager.incrementVectorCount(shard.id);
267
302
  return { success: true, data: { id } };
268
303
  }
@@ -323,6 +358,7 @@ export async function handleUpdateMemory(id, data) {
323
358
  if (!id)
324
359
  return { success: false, error: "id is required" };
325
360
  await embeddingService.warmup();
361
+ // Find the existing memory first (read-only — no data modified yet)
326
362
  const projectShards = shardManager.getAllShards("project", "");
327
363
  let foundShard = null, existingMemory = null;
328
364
  for (const shard of projectShards) {
@@ -336,36 +372,39 @@ export async function handleUpdateMemory(id, data) {
336
372
  }
337
373
  if (!foundShard || !existingMemory)
338
374
  return { success: false, error: "Memory not found" };
339
- const db = connectionManager.getConnection(foundShard.dbPath);
340
- await vectorSearch.deleteVector(db, id, foundShard);
341
- shardManager.decrementVectorCount(foundShard.id);
375
+ // STEP 1: Generate new embeddings FIRST (safe — no data deleted yet)
342
376
  const newContent = data.content || existingMemory.content;
343
- const tags = data.tags || (existingMemory.tags ? existingMemory.tags.split(",") : []);
377
+ const tags = data.tags ||
378
+ (existingMemory.tags ? existingMemory.tags.split(",").map((t) => t.trim()) : []);
344
379
  const vector = await embeddingService.embedWithTimeout(newContent);
345
380
  let tagsVector = undefined;
346
381
  if (tags.length > 0) {
347
382
  tagsVector = await embeddingService.embedWithTimeout(tags.join(", "));
348
383
  }
349
- const updatedRecord = {
350
- id,
351
- content: newContent,
352
- vector,
353
- tagsVector,
354
- containerTag: existingMemory.container_tag,
355
- tags: tags.length > 0 ? tags.join(",") : undefined,
356
- type: data.type || existingMemory.type,
357
- createdAt: existingMemory.created_at,
358
- updatedAt: Date.now(),
359
- metadata: existingMemory.metadata,
360
- displayName: existingMemory.display_name,
361
- userName: existingMemory.user_name,
362
- userEmail: existingMemory.user_email,
363
- projectPath: existingMemory.project_path,
364
- projectName: existingMemory.project_name,
365
- gitRepoUrl: existingMemory.git_repo_url,
366
- };
367
- await vectorSearch.insertVector(db, updatedRecord, foundShard);
368
- shardManager.incrementVectorCount(foundShard.id);
384
+ const db = connectionManager.getConnection(foundShard.dbPath);
385
+ // STEP 2: Wrap SQLite delete + insert in a transaction
386
+ const updateTransaction = db.transaction(() => {
387
+ // Delete old record
388
+ db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
389
+ // Insert updated record
390
+ const insertStmt = db.prepare(`
391
+ INSERT INTO memories (
392
+ id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
393
+ metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
394
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
395
+ `);
396
+ insertStmt.run(id, newContent, toBlob(vector), toBlob(tagsVector), existingMemory.container_tag, tags.length > 0 ? tags.join(",") : null, data.type || existingMemory.type, existingMemory.created_at, Date.now(), existingMemory.metadata, existingMemory.display_name, existingMemory.user_name, existingMemory.user_email, existingMemory.project_path, existingMemory.project_name, existingMemory.git_repo_url);
397
+ });
398
+ // Execute the SQLite transaction atomically
399
+ updateTransaction();
400
+ // STEP 3: Update vector index (outside transaction — vector backend is async/in-memory)
401
+ const backend = await vectorSearch.getBackend();
402
+ await backend.delete({ id, shard: foundShard, kind: "content" });
403
+ await backend.delete({ id, shard: foundShard, kind: "tags" });
404
+ await backend.insert({ id, vector, shard: foundShard, kind: "content" });
405
+ if (tagsVector) {
406
+ await backend.insert({ id, vector: tagsVector, shard: foundShard, kind: "tags" });
407
+ }
369
408
  return { success: true };
370
409
  }
371
410
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/services/client.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,cAAc,CAAC;AAqDrD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,aAAa,CAAkB;;YAIzB,UAAU;IAiBlB,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjC,SAAS,IAAI;QACX,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;QACrB,KAAK,EAAE,OAAO,CAAC;KAChB;IAQD,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,GAAE,WAAuB;;;;;;;;;;;;;IA6BlF,SAAS,CACb,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,KAAK,CAAC;QACtD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB;;;;;;;;;IA+DG,YAAY,CAAC,QAAQ,EAAE,MAAM;;;;;;;IA2B7B,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,SAAK,EAAE,KAAK,GAAE,WAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2D7E,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4C5F;AAED,eAAO,MAAM,YAAY,mBAA0B,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/services/client.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,cAAc,CAAC;AAyDrD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,aAAa,CAAkB;;YAIzB,UAAU;IAiBlB,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjC,SAAS,IAAI;QACX,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;QACrB,KAAK,EAAE,OAAO,CAAC;KAChB;IAQD,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,GAAE,WAAuB;;;;;;;;;;;;;IA6BlF,SAAS,CACb,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,KAAK,CAAC;QACtD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB;;;;;;;;;IAyGG,YAAY,CAAC,QAAQ,EAAE,MAAM;;;;;;;IA2B7B,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,SAAK,EAAE,KAAK,GAAE,WAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2D7E,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4C5F;AAED,eAAO,MAAM,YAAY,mBAA0B,CAAC"}
@@ -30,6 +30,9 @@ function safeJSONParse(jsonString) {
30
30
  return undefined;
31
31
  }
32
32
  }
33
+ function toBlob(vector) {
34
+ return vector ? new Uint8Array(vector.buffer) : null;
35
+ }
33
36
  function extractScopeFromContainerTag(containerTag) {
34
37
  const parts = containerTag.split("_");
35
38
  if (parts.length >= 3) {
@@ -134,7 +137,30 @@ export class LocalMemoryClient {
134
137
  metadata: Object.keys(dynamicMetadata).length > 0 ? JSON.stringify(dynamicMetadata) : undefined,
135
138
  };
136
139
  const db = connectionManager.getConnection(shard.dbPath);
137
- await vectorSearch.insertVector(db, record, shard);
140
+ // Use transaction for atomic SQLite insert
141
+ const insertMemory = db.transaction(() => {
142
+ const insertStmt = db.prepare(`
143
+ INSERT INTO memories (
144
+ id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
145
+ metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
146
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
147
+ `);
148
+ insertStmt.run(record.id, record.content, toBlob(record.vector), toBlob(record.tagsVector), 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);
149
+ });
150
+ insertMemory();
151
+ // Vector index update (outside transaction — vector backend is async/in-memory)
152
+ try {
153
+ const backend = await vectorSearch.getBackend();
154
+ await backend.insert({ id: record.id, vector: record.vector, shard, kind: "content" });
155
+ if (record.tagsVector) {
156
+ await backend.insert({ id: record.id, vector: record.tagsVector, shard, kind: "tags" });
157
+ }
158
+ }
159
+ catch (error) {
160
+ // Rollback SQLite insert on vector backend failure
161
+ db.prepare(`DELETE FROM memories WHERE id = ?`).run(record.id);
162
+ throw error;
163
+ }
138
164
  shardManager.incrementVectorCount(shard.id);
139
165
  return { success: true, id };
140
166
  }
@@ -6,6 +6,6 @@ interface MemoryResultMinimal {
6
6
  interface MemoriesResponseMinimal {
7
7
  results?: MemoryResultMinimal[];
8
8
  }
9
- export declare function formatContextForPrompt(userId: string | null, projectMemories: MemoriesResponseMinimal): string;
9
+ export declare function formatContextForPrompt(userId: string | null, projectMemories: MemoriesResponseMinimal, userMessage?: string): string;
10
10
  export {};
11
11
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/services/context.ts"],"names":[],"mappings":"AAIA,UAAU,mBAAmB;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,uBAAuB;IAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,eAAe,EAAE,uBAAuB,GACvC,MAAM,CAkCR"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/services/context.ts"],"names":[],"mappings":"AAIA,UAAU,mBAAmB;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,uBAAuB;IAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,eAAe,EAAE,uBAAuB,EACxC,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAkCR"}
@@ -1,10 +1,10 @@
1
1
  import { CONFIG } from "../config.js";
2
2
  import { getUserProfileContext } from "./user-profile/profile-context.js";
3
3
  import { logDebug } from "./logger.js";
4
- export function formatContextForPrompt(userId, projectMemories) {
4
+ export function formatContextForPrompt(userId, projectMemories, userMessage) {
5
5
  const parts = ["[MEMORY]"];
6
6
  if (CONFIG.injectProfile && userId) {
7
- const profileContext = getUserProfileContext(userId);
7
+ const profileContext = getUserProfileContext(userId, userMessage);
8
8
  if (profileContext) {
9
9
  parts.push("\n" + profileContext);
10
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"embedding.d.ts","sourceRoot":"","sources":["../../src/services/embedding.ts"],"names":[],"mappings":"AA6CA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,WAAW,CAA8B;IAC1C,UAAU,EAAE,OAAO,CAAS;IACnC,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,eAAe,CAAuB;IAE9C,MAAM,CAAC,WAAW,IAAI,gBAAgB;IAOhC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;YAOzD,eAAe;IAkBvB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAmD1C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI3D,UAAU,IAAI,IAAI;CAGnB;AAED,eAAO,MAAM,gBAAgB,kBAAiC,CAAC"}
1
+ {"version":3,"file":"embedding.d.ts","sourceRoot":"","sources":["../../src/services/embedding.ts"],"names":[],"mappings":"AA6CA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,WAAW,CAA8B;IAC1C,UAAU,EAAE,OAAO,CAAS;IACnC,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,eAAe,CAAuB;IAE9C,MAAM,CAAC,WAAW,IAAI,gBAAgB;IAOhC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;YAOzD,eAAe;IA2CvB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAmD1C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI3D,UAAU,IAAI,IAAI;CAGnB;AAED,eAAO,MAAM,gBAAgB,kBAAiC,CAAC"}
@@ -57,9 +57,26 @@ export class EmbeddingService {
57
57
  async initializeModel(progressCallback) {
58
58
  try {
59
59
  if (CONFIG.embeddingApiUrl && CONFIG.embeddingApiKey) {
60
+ // Send a probe request to verify the API endpoint is actually reachable
61
+ // Uses a minimal embedding of "ping" to test the full request pipeline
62
+ const probeResponse = await withTimeout(fetch(`${CONFIG.embeddingApiUrl}/embeddings`, {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ Authorization: `Bearer ${CONFIG.embeddingApiKey}`,
67
+ },
68
+ body: JSON.stringify({
69
+ input: "ping",
70
+ model: CONFIG.embeddingModel,
71
+ }),
72
+ }), TIMEOUT_MS);
73
+ if (!probeResponse.ok) {
74
+ throw new Error(`Embedding API health check failed: ${probeResponse.status} ${probeResponse.statusText}`);
75
+ }
60
76
  this.isWarmedUp = true;
61
77
  return;
62
78
  }
79
+ // Local model path
63
80
  const { pipeline } = await ensureTransformersLoaded();
64
81
  this.pipe = await pipeline("feature-extraction", CONFIG.embeddingModel, {
65
82
  progress_callback: progressCallback,
@@ -72,7 +72,7 @@ function writeLog(level, message, data) {
72
72
  const timestamp = localTimestamp();
73
73
  const levelTag = level.toUpperCase().padEnd(5);
74
74
  const line = data
75
- ? `[${timestamp}] [${levelTag}] ${message}: ${JSON.stringify(data)}\n`
75
+ ? `[${timestamp}] [${levelTag}] ${message}: ${JSON.stringify(data, null, 2)}\n`
76
76
  : `[${timestamp}] [${levelTag}] ${message}\n`;
77
77
  appendFileSync(logFile, line);
78
78
  }
@@ -1,4 +1,4 @@
1
- declare const Database: typeof import("bun:sqlite").Database;
1
+ declare const Database: new (filename?: string, options?: unknown) => unknown;
2
2
  export declare class ConnectionManager {
3
3
  private connections;
4
4
  private initDatabase;
@@ -1 +1 @@
1
- {"version":3,"file":"connection-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/connection-manager.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,QAAQ,sCAAgB,CAAC;AAE/B,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAAqD;IAExE,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAarB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,QAAQ,CAAC,SAAS;IAiBxD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IASrC,QAAQ,IAAI,IAAI;IAYhB,aAAa,IAAI,IAAI;CAStB;AAED,eAAO,MAAM,iBAAiB,mBAA0B,CAAC"}
1
+ {"version":3,"file":"connection-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/connection-manager.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,QAAQ,uDAAgB,CAAC;AAE/B,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAAqD;IAExE,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAarB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,QAAQ,CAAC,SAAS;IAiBxD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IASrC,QAAQ,IAAI,IAAI;IAYhB,aAAa,IAAI,IAAI;CAStB;AAED,eAAO,MAAM,iBAAiB,mBAA0B,CAAC"}
@@ -1,2 +1,4 @@
1
- export declare function getDatabase(): typeof import("bun:sqlite").Database;
1
+ type DatabaseCtor = new (filename?: string, options?: unknown) => unknown;
2
+ export declare function getDatabase(): DatabaseCtor;
3
+ export {};
2
4
  //# sourceMappingURL=sqlite-bootstrap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/sqlite-bootstrap.ts"],"names":[],"mappings":"AAEA,wBAAgB,WAAW,IAAI,cAAc,YAAY,EAAE,QAAQ,CAMlE"}
1
+ {"version":3,"file":"sqlite-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/sqlite-bootstrap.ts"],"names":[],"mappings":"AAmBA,KAAK,YAAY,GAAG,KAAK,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC;AAM1E,wBAAgB,WAAW,IAAI,YAAY,CAuF1C"}
@@ -1,8 +1,94 @@
1
+ /**
2
+ * SQLite binding bootstrap — works under Bun and Node.
3
+ *
4
+ * Resolution order:
5
+ * 1. Bun runtime → `bun:sqlite` (built-in, fastest, zero-install)
6
+ * 2. Node runtime → `node:sqlite` `DatabaseSync` (built-in, Node 22.5+ experimental,
7
+ * stable in Node 24+)
8
+ * 3. Fallback → `better-sqlite3` (peer dependency, full native binary)
9
+ *
10
+ * Required because opencode 1.15.x loads plugins under Node, not Bun — `bun:sqlite`
11
+ * is a Bun-only built-in and Node's ESM loader rejects the `bun:` URL scheme.
12
+ *
13
+ * The detection runs once at first call; the resolved Database class is cached.
14
+ */
15
+ import { createRequire } from "node:module";
1
16
  let Database;
17
+ const isBun = typeof globalThis.Bun !== "undefined";
2
18
  export function getDatabase() {
3
- if (!Database) {
4
- const bunSqlite = require("bun:sqlite");
5
- Database = bunSqlite.Database;
19
+ if (Database)
20
+ return Database;
21
+ const req = createRequire(import.meta.url);
22
+ if (isBun) {
23
+ Database = req("bun:sqlite").Database;
24
+ return Database;
25
+ }
26
+ // Node runtime — try built-in `node:sqlite` first. It exposes `DatabaseSync`
27
+ // with the synchronous prepare/all/get/close API surface that matches
28
+ // bun:sqlite. One gap: bun:sqlite (and better-sqlite3) expose `db.run(sql)`
29
+ // for executing a single SQL statement without bindings — used throughout
30
+ // this project for PRAGMA and CREATE INDEX setup. `node:sqlite`'s
31
+ // DatabaseSync uses `db.exec(sql)` for that surface, so we subclass to
32
+ // alias `db.run(sql)` onto `db.exec(sql)` (param-bound `db.run(sql, ...)`
33
+ // is preserved for any future callers, falling back to a prepared statement).
34
+ try {
35
+ const DatabaseSync = req("node:sqlite")
36
+ .DatabaseSync;
37
+ class DatabaseSyncCompat extends DatabaseSync {
38
+ run(sql, ...params) {
39
+ if (params.length === 0) {
40
+ return this.exec(sql);
41
+ }
42
+ return this.prepare(sql).run(...params);
43
+ }
44
+ // bun:sqlite and better-sqlite3 expose `db.transaction(fn)` that returns
45
+ // a callable wrapping `fn` in BEGIN/COMMIT (auto-ROLLBACK on throw).
46
+ // `node:sqlite`'s DatabaseSync has no equivalent. Used by
47
+ // `api-handlers.handleAddMemory` and `services/client.addMemory`, so
48
+ // POST /api/memories and any auto-capture path crash without it.
49
+ //
50
+ // Single-mode semantics only (BEGIN); the `.deferred` / `.immediate` /
51
+ // `.exclusive` variants from better-sqlite3 are not exercised by this
52
+ // codebase.
53
+ transaction(fn) {
54
+ const self = this;
55
+ const wrapped = function (...args) {
56
+ self.exec("BEGIN");
57
+ try {
58
+ const result = fn.apply(this, args);
59
+ self.exec("COMMIT");
60
+ return result;
61
+ }
62
+ catch (err) {
63
+ try {
64
+ self.exec("ROLLBACK");
65
+ }
66
+ catch {
67
+ /* rollback failures after partial state are best-effort */
68
+ }
69
+ throw err;
70
+ }
71
+ };
72
+ return wrapped;
73
+ }
74
+ }
75
+ Database = DatabaseSyncCompat;
76
+ return Database;
77
+ }
78
+ catch {
79
+ // node:sqlite isn't available (Node < 22.5, or experimental flag not set
80
+ // in some embedded runtimes). Fall back to better-sqlite3 — wire-compatible
81
+ // API, requires a native postinstall but ships prebuilt binaries for
82
+ // common platforms.
83
+ try {
84
+ const betterSqlite = req("better-sqlite3");
85
+ Database = betterSqlite;
86
+ return Database;
87
+ }
88
+ catch (error) {
89
+ throw new Error("opencode-mem: no SQLite binding available. Install better-sqlite3, " +
90
+ "or run on Node ≥22.5 with `--experimental-sqlite`, or use Bun. " +
91
+ `Underlying error: ${error instanceof Error ? error.message : String(error)}`);
92
+ }
6
93
  }
7
- return Database;
8
94
  }
@@ -1,6 +1,6 @@
1
1
  import type { MemoryRecord, SearchResult, ShardInfo } from "./types.js";
2
2
  import type { VectorBackend } from "../vector-backends/types.js";
3
- declare const Database: typeof import("bun:sqlite").Database;
3
+ declare const Database: new (filename?: string, options?: unknown) => unknown;
4
4
  type DatabaseType = typeof Database.prototype;
5
5
  export declare class VectorSearch {
6
6
  private readonly backendPromise;
@@ -1 +1 @@
1
- {"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,QAAA,MAAM,QAAQ,sCAAgB,CAAC;AAC/B,KAAK,YAAY,GAAG,OAAO,QAAQ,CAAC,SAAS,CAAC;AAM9C,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgB;gBAEpC,OAAO,CAAC,EAAE,aAAa,EAAE,eAAe,GAAE,aAAsC;YAO9E,UAAU;IAIlB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCtF,aAAa,CACjB,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IA8HpB,kBAAkB,CACtB,MAAM,EAAE,SAAS,EAAE,EACnB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,mBAAmB,EAAE,MAAM,EAC3B,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAiBpB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlF,YAAY,CAChB,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,EACpB,KAAK,CAAC,EAAE,SAAS,EACjB,UAAU,CAAC,EAAE,YAAY,GACxB,OAAO,CAAC,IAAI,CAAC;IAkBhB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;IAmB1E,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAKvC,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAK7D,sBAAsB,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,EAAE;IAgBlE,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAM5D,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,MAAM;IAMzC,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAexC,SAAS,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKnD,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/C,oBAAoB,CACxB,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAgBV,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CAI1D;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
1
+ {"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,QAAA,MAAM,QAAQ,uDAAgB,CAAC;AAC/B,KAAK,YAAY,GAAG,OAAO,QAAQ,CAAC,SAAS,CAAC;AAM9C,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgB;gBAEpC,OAAO,CAAC,EAAE,aAAa,EAAE,eAAe,GAAE,aAAsC;YAO9E,UAAU;IAIlB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCtF,aAAa,CACjB,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IA8HpB,kBAAkB,CACtB,MAAM,EAAE,SAAS,EAAE,EACnB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,mBAAmB,EAAE,MAAM,EAC3B,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAiBpB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlF,YAAY,CAChB,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,EACpB,KAAK,CAAC,EAAE,SAAS,EACjB,UAAU,CAAC,EAAE,YAAY,GACxB,OAAO,CAAC,IAAI,CAAC;IAkBhB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;IAmB1E,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAKvC,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAK7D,sBAAsB,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,EAAE;IAgBlE,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAM5D,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,MAAM;IAMzC,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAexC,SAAS,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKnD,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/C,oBAAoB,CACxB,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAgBV,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CAI1D;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
@@ -88,6 +88,8 @@ ${prompts.map((p, i) => `${i + 1}. ${p.content}`).join("\n\n")}
88
88
 
89
89
  ## Analysis Guidelines
90
90
 
91
+ This profile is **cross-project** — it should capture the user's personal style, not project-specific details.
92
+
91
93
  Identify and ${existingProfile ? "update" : "create"}:
92
94
 
93
95
  1. **Preferences** (max ${CONFIG.userProfileMaxPreferences})
@@ -119,6 +121,8 @@ async function analyzeUserProfile(context, existingProfile) {
119
121
 
120
122
  Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
121
123
 
124
+ The profile is **cross-project** — capture the user's personal style and preferences, not project-specific details.
125
+
122
126
  CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
123
127
 
124
128
  Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
@@ -169,6 +173,8 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne
169
173
 
170
174
  Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
171
175
 
176
+ The profile is **cross-project** — capture the user's personal style and preferences, not project-specific details.
177
+
172
178
  CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
173
179
 
174
180
  Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
@@ -1,2 +1,2 @@
1
- export declare function getUserProfileContext(userId: string): string | null;
1
+ export declare function getUserProfileContext(userId: string, userMessage?: string): string | null;
2
2
  //# sourceMappingURL=profile-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"profile-context.d.ts","sourceRoot":"","sources":["../../../src/services/user-profile/profile-context.ts"],"names":[],"mappings":"AAGA,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6CnE"}
1
+ {"version":3,"file":"profile-context.d.ts","sourceRoot":"","sources":["../../../src/services/user-profile/profile-context.ts"],"names":[],"mappings":"AAuGA,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoDzF"}
@@ -1,37 +1,128 @@
1
1
  import { userProfileManager } from "./user-profile-manager.js";
2
- export function getUserProfileContext(userId) {
2
+ import { CONFIG } from "../../config.js";
3
+ /**
4
+ * Compute relevance score between a query and a target text.
5
+ * Uses term overlap across all languages via Intl.Segmenter + CJK bigrams.
6
+ */
7
+ function computeRelevance(query, target) {
8
+ const q = query.toLowerCase();
9
+ const t = target.toLowerCase();
10
+ const terms = extractTerms(q);
11
+ if (terms.length === 0)
12
+ return 0;
13
+ let matches = 0;
14
+ for (const term of terms) {
15
+ if (t.includes(term))
16
+ matches++;
17
+ }
18
+ return matches / terms.length;
19
+ }
20
+ /**
21
+ * Extract indexable terms from text, supporting all languages.
22
+ *
23
+ * Strategy:
24
+ * 1. Intl.Segmenter (word granularity) across the whole text — captures
25
+ * words/identifiers in any script (Latin, Arabic, Cyrillic, Devanagari, etc.)
26
+ * 2. CJK bigram pass — adds adjacent pairs of CJK characters because word
27
+ * segmenters often under-split Chinese/Japanese/Korean text.
28
+ * 3. All terms are lowercased, deduplicated, and must be >= 2 characters.
29
+ */
30
+ function extractTerms(text) {
31
+ const terms = new Set();
32
+ // Step 1: Intl.Segmenter — works for all languages
33
+ try {
34
+ const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
35
+ for (const seg of segmenter.segment(text)) {
36
+ const word = seg.segment.toLowerCase();
37
+ if (seg.isWordLike && word.length >= 2) {
38
+ terms.add(word);
39
+ }
40
+ }
41
+ }
42
+ catch {
43
+ // Intl.Segmenter not available (very old runtime), fallback to simple regex
44
+ const words = text.toLowerCase().match(/[a-z][a-z0-9_]{2,}/g);
45
+ if (words)
46
+ words.forEach((w) => terms.add(w));
47
+ }
48
+ // Step 2: CJK bigram — catches sub-word units in CJK text
49
+ const cjkChars = [...text].filter((ch) => /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(ch));
50
+ if (cjkChars.length > 1) {
51
+ for (let i = 0; i < cjkChars.length - 1; i++) {
52
+ const bigram = (cjkChars[i] + cjkChars[i + 1]).toLowerCase();
53
+ terms.add(bigram);
54
+ }
55
+ }
56
+ return Array.from(terms);
57
+ }
58
+ /**
59
+ * Filter items by relevance to query.
60
+ * When query is empty/undefined, returns top items (must be pre-sorted).
61
+ */
62
+ function filterRelevant(items, query, maxItems, getText) {
63
+ if (!query || !query.trim()) {
64
+ return items.slice(0, maxItems);
65
+ }
66
+ const q = query.trim();
67
+ const scored = items.map((item, index) => ({
68
+ item,
69
+ index,
70
+ score: computeRelevance(q, getText(item)),
71
+ }));
72
+ const matched = scored
73
+ .filter(({ score }) => score > 0)
74
+ .sort((a, b) => {
75
+ if (b.score !== a.score)
76
+ return b.score - a.score;
77
+ return a.index - b.index; // preserve original order
78
+ })
79
+ .slice(0, maxItems)
80
+ .map(({ item }) => item);
81
+ // Fallback: no items matched the query, return default top maxItems
82
+ if (matched.length === 0) {
83
+ return items.slice(0, maxItems);
84
+ }
85
+ return matched;
86
+ }
87
+ export function getUserProfileContext(userId, userMessage) {
3
88
  const profile = userProfileManager.getActiveProfile(userId);
4
89
  if (!profile) {
5
90
  return null;
6
91
  }
7
92
  const profileData = JSON.parse(profile.profileData);
8
93
  const parts = [];
94
+ const prefMax = CONFIG.chatMessage.injectMaxPreferences ?? 5;
95
+ const patternMax = CONFIG.chatMessage.injectMaxPatterns ?? 5;
96
+ const workflowMax = CONFIG.chatMessage.injectMaxWorkflows ?? 3;
9
97
  if (profileData.preferences.length > 0) {
10
- parts.push("User Preferences:");
11
- profileData.preferences
12
- .sort((a, b) => b.confidence - a.confidence)
13
- .slice(0, 5)
14
- .forEach((pref) => {
15
- parts.push(`- [${pref.category}] ${pref.description}`);
16
- });
98
+ const items = [...profileData.preferences].sort((a, b) => b.confidence - a.confidence);
99
+ const relevant = filterRelevant(items, userMessage, prefMax, (p) => `${p.category} ${p.description}`);
100
+ if (relevant.length > 0) {
101
+ parts.push("User Preferences:");
102
+ relevant.forEach((pref) => {
103
+ parts.push(`- [${pref.category}] ${pref.description}`);
104
+ });
105
+ }
17
106
  }
18
107
  if (profileData.patterns.length > 0) {
19
- parts.push("\nUser Patterns:");
20
- profileData.patterns
21
- .sort((a, b) => b.frequency - a.frequency)
22
- .slice(0, 5)
23
- .forEach((pattern) => {
24
- parts.push(`- [${pattern.category}] ${pattern.description}`);
25
- });
108
+ const items = [...profileData.patterns].sort((a, b) => b.frequency - a.frequency);
109
+ const relevant = filterRelevant(items, userMessage, patternMax, (p) => `${p.category} ${p.description}`);
110
+ if (relevant.length > 0) {
111
+ parts.push("\nUser Patterns:");
112
+ relevant.forEach((pattern) => {
113
+ parts.push(`- [${pattern.category}] ${pattern.description}`);
114
+ });
115
+ }
26
116
  }
27
117
  if (profileData.workflows.length > 0) {
28
- parts.push("\nUser Workflows:");
29
- profileData.workflows
30
- .sort((a, b) => b.frequency - a.frequency)
31
- .slice(0, 3)
32
- .forEach((workflow) => {
33
- parts.push(`- ${workflow.description}`);
34
- });
118
+ const items = [...profileData.workflows].sort((a, b) => b.frequency - a.frequency);
119
+ const relevant = filterRelevant(items, userMessage, workflowMax, (w) => w.description);
120
+ if (relevant.length > 0) {
121
+ parts.push("\nUser Workflows:");
122
+ relevant.forEach((workflow) => {
123
+ parts.push(`- ${workflow.description}`);
124
+ });
125
+ }
35
126
  }
36
127
  if (parts.length === 0) {
37
128
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"web-server.d.ts","sourceRoot":"","sources":["../../src/services/web-server.ts"],"names":[],"mappings":"AAoCA,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;IAyO3B,OAAO,CAAC,eAAe;IA4BvB,OAAO,CAAC,YAAY;CAWrB;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":"AA8HA,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,CAAqC;IACnD,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;IAyO3B,OAAO,CAAC,eAAe;IA4BvB,OAAO,CAAC,YAAY;CAWrB;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAIhF"}
@@ -1,8 +1,69 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { createServer } from "node:http";
3
+ import { Readable } from "node:stream";
2
4
  import { join, dirname } from "node:path";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import { log } from "./logger.js";
5
7
  import { handleListTags, handleListMemories, handleAddMemory, handleDeleteMemory, handleBulkDelete, handleUpdateMemory, handleSearch, handleStats, handlePinMemory, handleUnpinMemory, handleRunCleanup, handleRunDeduplication, handleDetectMigration, handleRunMigration, handleDetectTagMigration, handleRunTagMigrationBatch, handleGetTagMigrationProgress, handleDeletePrompt, handleBulkDeletePrompts, handleGetUserProfile, handleGetProfileChangelog, handleGetProfileSnapshot, handleRefreshProfile, handleDeleteProfilePreferences, handleDeleteProfilePatterns, handleDeleteProfileWorkflows, } from "./api-handlers.js";
8
+ const isBun = typeof globalThis.Bun !== "undefined";
9
+ function serveFetch(opts) {
10
+ if (isBun) {
11
+ const bunHandle = globalThis.Bun.serve({
12
+ port: opts.port,
13
+ hostname: opts.hostname,
14
+ fetch: opts.fetch,
15
+ });
16
+ return { stop: () => bunHandle.stop() };
17
+ }
18
+ // Node path: wrap node:http around the fetch-style handler. The adapter
19
+ // converts IncomingMessage → Web Request and Web Response → ServerResponse.
20
+ // Bodies stream both directions via the WHATWG Streams ↔ Node Streams
21
+ // helpers that ship with Node 18+.
22
+ const server = createServer(async (req, res) => {
23
+ try {
24
+ const url = `http://${opts.hostname}:${opts.port}${req.url ?? "/"}`;
25
+ const method = req.method ?? "GET";
26
+ const hasBody = method !== "GET" && method !== "HEAD";
27
+ const webReq = new Request(url, {
28
+ method,
29
+ headers: req.headers,
30
+ body: hasBody ? Readable.toWeb(req) : undefined,
31
+ // `duplex: "half"` is required by Node fetch when sending a body
32
+ // stream. Cast keeps TS happy on older lib.dom.d.ts revisions.
33
+ ...(hasBody ? { duplex: "half" } : {}),
34
+ });
35
+ const webRes = await opts.fetch(webReq);
36
+ res.statusCode = webRes.status;
37
+ webRes.headers.forEach((value, name) => res.setHeader(name, value));
38
+ if (webRes.body) {
39
+ Readable.fromWeb(webRes.body).pipe(res);
40
+ }
41
+ else {
42
+ res.end();
43
+ }
44
+ }
45
+ catch (error) {
46
+ if (!res.headersSent) {
47
+ res.statusCode = 500;
48
+ res.setHeader("Content-Type", "text/plain");
49
+ }
50
+ res.end(`Internal Server Error: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ });
53
+ // Surface EADDRINUSE synchronously so callers can detect the
54
+ // already-running-instance case the same way they do under Bun.
55
+ let listenError;
56
+ server.on("error", (err) => {
57
+ if (err.code === "EADDRINUSE") {
58
+ listenError = err;
59
+ }
60
+ });
61
+ server.listen(opts.port, opts.hostname);
62
+ if (listenError) {
63
+ throw listenError;
64
+ }
65
+ return { stop: () => server.close() };
66
+ }
6
67
  const __filename = fileURLToPath(import.meta.url);
7
68
  const __dirname = dirname(__filename);
8
69
  export class WebServer {
@@ -30,7 +91,7 @@ export class WebServer {
30
91
  return;
31
92
  }
32
93
  try {
33
- this.server = Bun.serve({
94
+ this.server = serveFetch({
34
95
  port: this.config.port,
35
96
  hostname: this.config.host,
36
97
  fetch: this.handleRequest.bind(this),
package/dist/web/app.js CHANGED
@@ -648,7 +648,8 @@ function showRefreshIndicator(show) {
648
648
 
649
649
  function formatDate(isoString) {
650
650
  const date = new Date(isoString);
651
- const locale = getLanguage() === "zh" ? "zh-CN" : "en-US";
651
+ const lang = getLanguage();
652
+ const locale = lang === "zh" ? "zh-CN" : lang === "ar" ? "ar-SA" : "en-US";
652
653
  return date.toLocaleString(locale, {
653
654
  year: "numeric",
654
655
  month: "short",
@@ -1384,10 +1385,12 @@ document.addEventListener("DOMContentLoaded", async () => {
1384
1385
  });
1385
1386
 
1386
1387
  document.getElementById("lang-toggle").addEventListener("click", () => {
1387
- const newLang = getLanguage() === "en" ? "zh" : "en";
1388
+ const langCycle = ["en", "zh", "ar"];
1389
+ const currentLang = getLanguage();
1390
+ const currentIndex = langCycle.indexOf(currentLang);
1391
+ const newLang = langCycle[(currentIndex + 1) % langCycle.length];
1388
1392
  setLanguage(newLang);
1389
1393
  document.getElementById("lang-toggle").textContent = newLang.toUpperCase();
1390
- // Re-render dynamic content
1391
1394
  loadMemories();
1392
1395
  loadStats();
1393
1396
  if (state.currentView === "profile") loadUserProfile();
package/dist/web/i18n.js CHANGED
@@ -277,6 +277,151 @@ const translations = {
277
277
  "migration-complete-fresh": "已删除 {shards} 个分片。耗时:{duration}秒",
278
278
  "migration-complete-reembed": "已重新嵌入 {count} 条记忆。耗时:{duration}秒",
279
279
  },
280
+ ar: {
281
+ title: "┌─ مستكشف ذاكرة OpenCode ─┐",
282
+ "tab-project": "ذكريات المشروع",
283
+ "tab-profile": "ملف المستخدم",
284
+
285
+ "label-tag": "الوسم:",
286
+ "label-type": "النوع:",
287
+ "label-tags": "الوسوم:",
288
+ "label-content": "المحتوى:",
289
+
290
+ "btn-cleanup": "تنظيف",
291
+ "btn-deduplicate": "إزالة التكرار",
292
+ "btn-delete-selected": "حذف المحدد",
293
+ "btn-select-all": "تحديد الصفحة",
294
+ "btn-deselect-all": "إلغاء التحديد",
295
+ "btn-add-memory": "إضافة ذكرى",
296
+
297
+ "section-project": "└─ ذكريات المشروع ({count}) ──",
298
+ "section-profile": "└─ ملف المستخدم ──",
299
+ "section-add": "└─ إضافة ذكرى جديدة ──",
300
+
301
+ "opt-all-tags": "جميع الوسوم",
302
+ "opt-select-tag": "اختر وسمًا",
303
+ "opt-other": "أخرى",
304
+ "opt-feature": "ميزة",
305
+ "opt-bug-fix": "إصلاح خطأ",
306
+ "opt-refactor": "إعادة هيكلة",
307
+ "opt-architecture": "معمارية",
308
+ "opt-rule": "قاعدة",
309
+ "opt-documentation": "توثيق",
310
+ "opt-discussion": "نقاش",
311
+ "opt-analysis": "تحليل",
312
+ "opt-configuration": "إعدادات",
313
+
314
+ "modal-edit-title": "تعديل الذكرى",
315
+ "modal-migration-title": "ترحيل وسوم الذكريات",
316
+ "modal-changelog-title": "سجل إصدارات الملف الشخصي",
317
+
318
+ "btn-cancel": "إلغاء",
319
+ "btn-save": "حفظ التغييرات",
320
+ "btn-start-migration": "بدء الترحيل",
321
+
322
+ "loading-init": "جاري التهيئة...",
323
+ "loading-profile": "جاري تحميل الملف الشخصي...",
324
+ "loading-changelog": "جاري تحميل السجل...",
325
+
326
+ "migration-mismatch": "تم اكتشاف عدم تطابق في أبعاد النموذج!",
327
+ "migration-understand":
328
+ "أفهم أن هذه العملية غير قابلة للتراجع وستؤثر على جميع الذكريات المخزنة",
329
+
330
+ "btn-fresh-start": "بداية جديدة (حذف الكل)",
331
+ "btn-reembed": "إعادة إنشاء المتجهات (مع الاحتفاظ بالبيانات)",
332
+
333
+ "migration-note":
334
+ "يرجى عدم إغلاق المتصفح. سيتم إعادة فهرسة الذكريات باستخدام وسوم تقنية لتحسين دقة البحث.",
335
+
336
+ "placeholder-search": "ابحث في الذكريات...",
337
+ "placeholder-tags": "react, hooks, auth (مفصولة بفواصل)",
338
+ "placeholder-content": "أدخل محتوى الذكرى...",
339
+
340
+ "toast-add-success": "تمت إضافة الذكرى بنجاح",
341
+ "toast-add-error": "المحتوى والوسم مطلوبان",
342
+ "toast-add-failed": "فشلت إضافة الذكرى",
343
+
344
+ "toast-delete-success": "تم حذف الذكرى بنجاح",
345
+ "toast-delete-failed": "فشل حذف الذكرى",
346
+
347
+ "toast-update-success": "تم تحديث الذكرى بنجاح",
348
+ "toast-update-failed": "فشل تحديث الذكرى",
349
+
350
+ "toast-cleanup-success": "اكتملت عملية التنظيف بنجاح",
351
+ "toast-cleanup-failed": "فشلت عملية التنظيف",
352
+
353
+ "toast-dedup-success": "اكتملت إزالة التكرار بنجاح",
354
+ "toast-dedup-failed": "فشلت إزالة التكرار",
355
+
356
+ "toast-bulk-delete-success": "تم حذف الذكريات المحددة بنجاح",
357
+ "toast-bulk-delete-failed": "فشل حذف الذكريات المحددة",
358
+
359
+ "toast-migration-success": "اكتملت عملية الترحيل بنجاح",
360
+ "toast-migration-failed": "فشلت عملية الترحيل",
361
+
362
+ "toast-fresh-start-success": "تمت البداية الجديدة بنجاح",
363
+ "toast-fresh-start-failed": "فشلت البداية الجديدة",
364
+
365
+ "confirm-delete": "هل تريد حذف هذه الذكرى؟",
366
+ "confirm-delete-pair": "هل تريد حذف هذه الذكرى والموجه المرتبط بها؟",
367
+ "confirm-delete-prompt": "هل تريد حذف هذا الموجه والذكرى المرتبطة به؟",
368
+
369
+ "confirm-bulk-delete": "هل تريد حذف {count} من الذكريات المحددة؟",
370
+
371
+ "confirm-cleanup": "سيؤدي هذا إلى حذف جميع الذكريات التي لم تعد ذات صلة. هل تريد المتابعة؟",
372
+
373
+ "confirm-dedup": "سيؤدي هذا إلى دمج الذكريات المتكررة أو المتشابهة جدًا. هل تريد المتابعة؟",
374
+
375
+ "text-selected": "تم تحديد {count}",
376
+ "text-page": "الصفحة {current} من {total}",
377
+ "text-total": "الإجمالي: {count}",
378
+
379
+ "empty-memories": "لم يتم العثور على ذكريات",
380
+ "empty-changelog": "لا يوجد سجل تغييرات",
381
+
382
+ "status-cleanup": "جاري التنظيف...",
383
+ "status-dedup": "جاري إزالة التكرار...",
384
+ "status-migration-init": "جاري تهيئة الترحيل...",
385
+ "status-migration-progress": "جاري الترحيل... {current}/{total}",
386
+
387
+ "profile-version": "الإصدار",
388
+ "profile-prompts": "الموجهات",
389
+ "profile-updated": "آخر تحديث",
390
+ "profile-preferences": "التفضيلات",
391
+ "profile-patterns": "الأنماط",
392
+ "profile-workflows": "سير العمل",
393
+
394
+ "badge-prompt": "موجه المستخدم",
395
+ "badge-memory": "ذكرى",
396
+ "badge-pinned": "مثبتة",
397
+ "badge-linked": "مرتبطة",
398
+
399
+ "date-created": "تاريخ الإنشاء:",
400
+ "date-updated": "تاريخ التحديث:",
401
+
402
+ "empty-preferences": "لم يتم تعلم أي تفضيلات بعد",
403
+ "empty-patterns": "لم يتم اكتشاف أي أنماط بعد",
404
+ "empty-workflows": "لم يتم التعرف على أي سير عمل بعد",
405
+
406
+ "btn-delete-pair": "حذف الزوج",
407
+ "btn-delete": "حذف",
408
+
409
+ "text-generated-above": "تم إنشاء الذكرى أعلاه",
410
+ "text-from-below": "من الموجه أدناه",
411
+
412
+ "btn-refresh": "تحديث",
413
+
414
+ "migration-found-tags": "تم العثور على {count} من الذكريات التي تحتاج إلى وسوم تقنية.",
415
+
416
+ "migration-stopped": "تم إيقاف الترحيل: تم الوصول إلى الحد الأقصى للمحاولات",
417
+
418
+ "migration-shards-mismatch": "{count} من الأجزاء تحتوي على أبعاد مختلفة",
419
+
420
+ "migration-dimension-mismatch": "تم اكتشاف عدم تطابق في الأبعاد",
421
+
422
+ "migration-mismatch-details":
423
+ "عدم تطابق النموذج: يستخدم الإعداد {configDimensions}D ({configModel}) بينما {shardInfo}.",
424
+ },
280
425
  };
281
426
 
282
427
  function getLanguage() {
@@ -285,6 +430,11 @@ function getLanguage() {
285
430
 
286
431
  function setLanguage(lang) {
287
432
  localStorage.setItem("opencode-mem-lang", lang);
433
+
434
+ document.documentElement.dir = lang === "ar" ? "rtl" : "ltr";
435
+
436
+ document.documentElement.lang = lang;
437
+
288
438
  applyLanguage();
289
439
  }
290
440
 
@@ -1539,6 +1539,7 @@ textarea:focus-visible {
1539
1539
  font-size: 12px;
1540
1540
  color: var(--om-text-card);
1541
1541
  line-height: 1.4;
1542
+ word-break: break-all;
1542
1543
  }
1543
1544
 
1544
1545
  .card-footer {
@@ -1841,7 +1842,7 @@ textarea:focus-visible {
1841
1842
  .pref-bar-btn:hover {
1842
1843
  background: var(--om-accent-red);
1843
1844
  color: var(--om-text-inverse);
1844
- }
1845
+ }
1845
1846
 
1846
1847
  .pref-delete-bar .btn-danger {
1847
1848
  background: var(--om-accent-red);
@@ -1908,4 +1909,4 @@ textarea:focus-visible {
1908
1909
  border-width: 2px;
1909
1910
  background: var(--om-blue-tint);
1910
1911
  box-shadow: 0 0 0 1px var(--om-blue-border-tint);
1911
- }
1912
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haisto/opencode-mem",
3
- "version": "2.14.3-beta.7",
3
+ "version": "2.16.0-beta.2",
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",
@@ -13,6 +13,10 @@
13
13
  "./server": {
14
14
  "import": "./dist/plugin.js",
15
15
  "types": "./dist/index.d.ts"
16
+ },
17
+ "./tags": {
18
+ "import": "./dist/services/tags.js",
19
+ "types": "./dist/services/tags.d.ts"
16
20
  }
17
21
  },
18
22
  "scripts": {