opencode-mem 1.0.0 → 2.0.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.
Files changed (71) hide show
  1. package/README.md +80 -477
  2. package/dist/config.d.ts +5 -5
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +46 -20
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +28 -88
  7. package/dist/plugin.d.ts.map +1 -1
  8. package/dist/plugin.js +1 -8
  9. package/dist/services/ai/ai-provider-factory.d.ts +8 -0
  10. package/dist/services/ai/ai-provider-factory.d.ts.map +1 -0
  11. package/dist/services/ai/ai-provider-factory.js +25 -0
  12. package/dist/services/ai/providers/anthropic-messages.d.ts +13 -0
  13. package/dist/services/ai/providers/anthropic-messages.d.ts.map +1 -0
  14. package/dist/services/ai/providers/anthropic-messages.js +176 -0
  15. package/dist/services/ai/providers/base-provider.d.ts +21 -0
  16. package/dist/services/ai/providers/base-provider.d.ts.map +1 -0
  17. package/dist/services/ai/providers/base-provider.js +6 -0
  18. package/dist/services/ai/providers/openai-chat-completion.d.ts +12 -0
  19. package/dist/services/ai/providers/openai-chat-completion.d.ts.map +1 -0
  20. package/dist/services/ai/providers/openai-chat-completion.js +181 -0
  21. package/dist/services/ai/providers/openai-responses.d.ts +14 -0
  22. package/dist/services/ai/providers/openai-responses.d.ts.map +1 -0
  23. package/dist/services/ai/providers/openai-responses.js +191 -0
  24. package/dist/services/ai/session/ai-session-manager.d.ts +21 -0
  25. package/dist/services/ai/session/ai-session-manager.d.ts.map +1 -0
  26. package/dist/services/ai/session/ai-session-manager.js +165 -0
  27. package/dist/services/ai/session/session-types.d.ts +43 -0
  28. package/dist/services/ai/session/session-types.d.ts.map +1 -0
  29. package/dist/services/ai/session/session-types.js +1 -0
  30. package/dist/services/ai/tools/tool-schema.d.ts +41 -0
  31. package/dist/services/ai/tools/tool-schema.d.ts.map +1 -0
  32. package/dist/services/ai/tools/tool-schema.js +24 -0
  33. package/dist/services/api-handlers.d.ts +11 -3
  34. package/dist/services/api-handlers.d.ts.map +1 -1
  35. package/dist/services/api-handlers.js +143 -30
  36. package/dist/services/auto-capture.d.ts +1 -30
  37. package/dist/services/auto-capture.d.ts.map +1 -1
  38. package/dist/services/auto-capture.js +199 -396
  39. package/dist/services/cleanup-service.d.ts +3 -0
  40. package/dist/services/cleanup-service.d.ts.map +1 -1
  41. package/dist/services/cleanup-service.js +31 -4
  42. package/dist/services/client.d.ts +1 -0
  43. package/dist/services/client.d.ts.map +1 -1
  44. package/dist/services/client.js +3 -11
  45. package/dist/services/sqlite/connection-manager.d.ts.map +1 -1
  46. package/dist/services/sqlite/connection-manager.js +8 -4
  47. package/dist/services/user-memory-learning.d.ts +3 -0
  48. package/dist/services/user-memory-learning.d.ts.map +1 -0
  49. package/dist/services/user-memory-learning.js +157 -0
  50. package/dist/services/user-prompt/user-prompt-manager.d.ts +38 -0
  51. package/dist/services/user-prompt/user-prompt-manager.d.ts.map +1 -0
  52. package/dist/services/user-prompt/user-prompt-manager.js +164 -0
  53. package/dist/services/web-server-worker.js +27 -6
  54. package/dist/services/web-server.d.ts.map +1 -1
  55. package/dist/services/web-server.js +0 -5
  56. package/dist/types/index.d.ts +5 -1
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/dist/web/app.js +210 -120
  59. package/dist/web/index.html +14 -10
  60. package/dist/web/styles.css +326 -1
  61. package/package.json +4 -1
  62. package/dist/services/compaction.d.ts +0 -92
  63. package/dist/services/compaction.d.ts.map +0 -1
  64. package/dist/services/compaction.js +0 -421
  65. package/dist/services/sqlite-client.d.ts +0 -116
  66. package/dist/services/sqlite-client.d.ts.map +0 -1
  67. package/dist/services/sqlite-client.js +0 -284
  68. package/dist/services/web-server-lock.d.ts +0 -12
  69. package/dist/services/web-server-lock.d.ts.map +0 -1
  70. package/dist/services/web-server-lock.js +0 -157
  71. package/dist/web/favicon.svg +0 -14
@@ -3,6 +3,7 @@ import { vectorSearch } from "./sqlite/vector-search.js";
3
3
  import { connectionManager } from "./sqlite/connection-manager.js";
4
4
  import { CONFIG } from "../config.js";
5
5
  import { log } from "./logger.js";
6
+ import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
6
7
  export class CleanupService {
7
8
  lastCleanupTime = 0;
8
9
  isRunning = false;
@@ -30,21 +31,40 @@ export class CleanupService {
30
31
  const userShards = shardManager.getAllShards("user", "");
31
32
  const projectShards = shardManager.getAllShards("project", "");
32
33
  const allShards = [...userShards, ...projectShards];
34
+ const pinnedMemoryIds = new Set();
35
+ for (const shard of allShards) {
36
+ const db = connectionManager.getConnection(shard.dbPath);
37
+ const pinned = db.prepare(`SELECT id FROM memories WHERE is_pinned = 1`).all();
38
+ pinned.forEach((row) => pinnedMemoryIds.add(row.id));
39
+ }
40
+ const promptCleanupResult = userPromptManager.deleteOldPrompts(cutoffTime);
41
+ const linkedMemoryIds = new Set(promptCleanupResult.linkedMemoryIds);
42
+ const protectedMemoryIds = new Set([...pinnedMemoryIds, ...linkedMemoryIds]);
33
43
  let totalDeleted = 0;
34
44
  let userDeleted = 0;
35
45
  let projectDeleted = 0;
46
+ let linkedMemoriesDeleted = 0;
47
+ let pinnedSkipped = 0;
36
48
  for (const shard of allShards) {
37
49
  const db = connectionManager.getConnection(shard.dbPath);
38
50
  const oldMemories = db
39
51
  .prepare(`
40
- SELECT id, container_tag FROM memories
41
- WHERE updated_at < ? AND is_pinned = 0
52
+ SELECT id, container_tag, is_pinned FROM memories
53
+ WHERE updated_at < ?
42
54
  `)
43
55
  .all(cutoffTime);
44
- if (oldMemories.length === 0)
45
- continue;
46
56
  for (const memory of oldMemories) {
47
57
  try {
58
+ if (memory.is_pinned === 1) {
59
+ pinnedSkipped++;
60
+ continue;
61
+ }
62
+ if (protectedMemoryIds.has(memory.id)) {
63
+ if (linkedMemoryIds.has(memory.id)) {
64
+ log("Cleanup: skipped linked memory", { memoryId: memory.id });
65
+ }
66
+ continue;
67
+ }
48
68
  vectorSearch.deleteVector(db, memory.id);
49
69
  shardManager.decrementVectorCount(shard.id);
50
70
  totalDeleted++;
@@ -60,16 +80,23 @@ export class CleanupService {
60
80
  }
61
81
  }
62
82
  }
83
+ const promptsDeleted = promptCleanupResult.deleted - linkedMemoryIds.size;
63
84
  log("Cleanup: completed", {
64
85
  totalDeleted,
65
86
  userDeleted,
66
87
  projectDeleted,
88
+ promptsDeleted,
89
+ linkedMemoriesProtected: linkedMemoryIds.size,
90
+ pinnedMemoriesSkipped: pinnedSkipped,
67
91
  cutoffTime: new Date(cutoffTime).toISOString(),
68
92
  });
69
93
  return {
70
94
  deletedCount: totalDeleted,
71
95
  userCount: userDeleted,
72
96
  projectCount: projectDeleted,
97
+ promptsDeleted,
98
+ linkedMemoriesDeleted,
99
+ pinnedMemoriesSkipped: pinnedSkipped,
73
100
  };
74
101
  }
75
102
  finally {
@@ -16,6 +16,7 @@ export declare class LocalMemoryClient {
16
16
  modelLoaded: boolean;
17
17
  ready: boolean;
18
18
  };
19
+ close(): void;
19
20
  searchMemories(query: string, containerTag: string): Promise<{
20
21
  success: true;
21
22
  results: SearchResult[];
@@ -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;AACpD,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AA2CpE,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,aAAa,CAAkB;;YAIzB,UAAU;IAkBlB,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;IAQK,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;;;;;;;;;;;;;IA+BlD,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;;;;;;;;;IA2C/C,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,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;;;;;;;;;IAuDG,YAAY,CAAC,QAAQ,EAAE,MAAM;;;;;;;IA6B7B,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,SAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyDpD;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;AACpD,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AA2CpE,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,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;;;;;;;;;;;;;IA6BlD,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;;;;;;;;;IAyC/C,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,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;;;;;;;;;IAqDG,YAAY,CAAC,QAAQ,EAAE,MAAM;;;;;;;IA2B7B,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,SAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDpD;AAED,eAAO,MAAM,YAAY,mBAA0B,CAAC"}
@@ -51,7 +51,6 @@ export class LocalMemoryClient {
51
51
  this.initPromise = (async () => {
52
52
  try {
53
53
  this.isInitialized = true;
54
- log("SQLite memory client initialized");
55
54
  }
56
55
  catch (error) {
57
56
  this.initPromise = null;
@@ -75,8 +74,10 @@ export class LocalMemoryClient {
75
74
  ready: this.isInitialized && embeddingService.isWarmedUp,
76
75
  };
77
76
  }
77
+ close() {
78
+ connectionManager.closeAll();
79
+ }
78
80
  async searchMemories(query, containerTag) {
79
- log("searchMemories: start", { containerTag });
80
81
  try {
81
82
  await this.initialize();
82
83
  const queryVector = await embeddingService.embedWithTimeout(query);
@@ -87,7 +88,6 @@ export class LocalMemoryClient {
87
88
  return { success: true, results: [], total: 0, timing: 0 };
88
89
  }
89
90
  const results = await vectorSearch.searchAcrossShards(shards, queryVector, containerTag, CONFIG.maxMemories, CONFIG.similarityThreshold);
90
- log("searchMemories: success", { count: results.length });
91
91
  return { success: true, results, total: results.length, timing: 0 };
92
92
  }
93
93
  catch (error) {
@@ -97,7 +97,6 @@ export class LocalMemoryClient {
97
97
  }
98
98
  }
99
99
  async getProfile(containerTag, query) {
100
- log("getProfile: start", { containerTag });
101
100
  try {
102
101
  await this.initialize();
103
102
  const { scope, hash } = extractScopeFromContainerTag(containerTag);
@@ -124,7 +123,6 @@ export class LocalMemoryClient {
124
123
  static: staticFacts.slice(0, CONFIG.maxProfileItems),
125
124
  dynamic: dynamicFacts.slice(0, CONFIG.maxProfileItems),
126
125
  };
127
- log("getProfile: success", { hasProfile: true });
128
126
  return { success: true, profile };
129
127
  }
130
128
  catch (error) {
@@ -134,7 +132,6 @@ export class LocalMemoryClient {
134
132
  }
135
133
  }
136
134
  async addMemory(content, containerTag, metadata) {
137
- log("addMemory: start", { containerTag, contentLength: content.length });
138
135
  try {
139
136
  await this.initialize();
140
137
  const vector = await embeddingService.embedWithTimeout(content);
@@ -162,7 +159,6 @@ export class LocalMemoryClient {
162
159
  const db = connectionManager.getConnection(shard.dbPath);
163
160
  vectorSearch.insertVector(db, record);
164
161
  shardManager.incrementVectorCount(shard.id);
165
- log("addMemory: success", { id, shardId: shard.id });
166
162
  return { success: true, id };
167
163
  }
168
164
  catch (error) {
@@ -172,7 +168,6 @@ export class LocalMemoryClient {
172
168
  }
173
169
  }
174
170
  async deleteMemory(memoryId) {
175
- log("deleteMemory: start", { memoryId });
176
171
  try {
177
172
  await this.initialize();
178
173
  const { scope, hash } = extractScopeFromContainerTag(memoryId);
@@ -183,7 +178,6 @@ export class LocalMemoryClient {
183
178
  if (memory) {
184
179
  vectorSearch.deleteVector(db, memoryId);
185
180
  shardManager.decrementVectorCount(shard.id);
186
- log("deleteMemory: success", { memoryId, shardId: shard.id });
187
181
  return { success: true };
188
182
  }
189
183
  }
@@ -197,7 +191,6 @@ export class LocalMemoryClient {
197
191
  }
198
192
  }
199
193
  async listMemories(containerTag, limit = 20) {
200
- log("listMemories: start", { containerTag, limit });
201
194
  try {
202
195
  await this.initialize();
203
196
  const { scope, hash } = extractScopeFromContainerTag(containerTag);
@@ -229,7 +222,6 @@ export class LocalMemoryClient {
229
222
  projectName: r.project_name,
230
223
  gitRepoUrl: r.git_repo_url,
231
224
  }));
232
- log("listMemories: success", { count: memories.length });
233
225
  return {
234
226
  success: true,
235
227
  memories,
@@ -1 +1 @@
1
- {"version":3,"file":"connection-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/connection-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAKtC,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAAoC;IAEvD,OAAO,CAAC,YAAY;IAUpB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAkBvC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IASrC,QAAQ,IAAI,IAAI;CAOjB;AAED,eAAO,MAAM,iBAAiB,mBAA0B,CAAC"}
1
+ {"version":3,"file":"connection-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/connection-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAKtC,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAAoC;IAEvD,OAAO,CAAC,YAAY;IAUpB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAiBvC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IASrC,QAAQ,IAAI,IAAI;CAWjB;AAED,eAAO,MAAM,iBAAiB,mBAA0B,CAAC"}
@@ -23,21 +23,25 @@ export class ConnectionManager {
23
23
  const db = new Database(dbPath);
24
24
  this.initDatabase(db);
25
25
  this.connections.set(dbPath, db);
26
- log("SQLite connection opened", { path: dbPath });
27
26
  return db;
28
27
  }
29
28
  closeConnection(dbPath) {
30
29
  const db = this.connections.get(dbPath);
31
30
  if (db) {
31
+ db.run("PRAGMA wal_checkpoint(TRUNCATE)");
32
32
  db.close();
33
33
  this.connections.delete(dbPath);
34
- log("SQLite connection closed", { path: dbPath });
35
34
  }
36
35
  }
37
36
  closeAll() {
38
37
  for (const [path, db] of this.connections) {
39
- db.close();
40
- log("SQLite connection closed", { path });
38
+ try {
39
+ db.run("PRAGMA wal_checkpoint(TRUNCATE)");
40
+ db.close();
41
+ }
42
+ catch (error) {
43
+ log("Error closing database", { path, error: String(error) });
44
+ }
41
45
  }
42
46
  this.connections.clear();
43
47
  }
@@ -0,0 +1,3 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ export declare function performUserMemoryLearning(ctx: PluginInput, directory: string): Promise<void>;
3
+ //# sourceMappingURL=user-memory-learning.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-memory-learning.d.ts","sourceRoot":"","sources":["../../src/services/user-memory-learning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAQvD,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,WAAW,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAyEf"}
@@ -0,0 +1,157 @@
1
+ import { memoryClient } from "./client.js";
2
+ import { getTags } from "./tags.js";
3
+ import { log } from "./logger.js";
4
+ import { CONFIG } from "../config.js";
5
+ import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
6
+ export async function performUserMemoryLearning(ctx, directory) {
7
+ try {
8
+ const count = userPromptManager.countUnanalyzedForUserLearning();
9
+ const threshold = CONFIG.userMemoryAnalysisInterval;
10
+ if (count < threshold) {
11
+ return;
12
+ }
13
+ const prompts = userPromptManager.getPromptsForUserLearning(threshold);
14
+ if (prompts.length === 0) {
15
+ return;
16
+ }
17
+ const context = buildUserAnalysisContext(prompts);
18
+ const memories = await analyzeUserPatterns(ctx, context);
19
+ if (!memories || memories.length === 0) {
20
+ log("User memory learning: no patterns identified", { promptCount: prompts.length });
21
+ userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
22
+ return;
23
+ }
24
+ const tags = getTags(directory);
25
+ let savedCount = 0;
26
+ for (const memory of memories) {
27
+ const result = await memoryClient.addMemory(memory.summary, tags.user.tag, {
28
+ type: memory.type,
29
+ source: "user-learning",
30
+ promptCount: prompts.length,
31
+ analysisTimestamp: Date.now(),
32
+ reasoning: memory.reasoning,
33
+ displayName: tags.user.displayName,
34
+ userName: tags.user.userName,
35
+ userEmail: tags.user.userEmail,
36
+ });
37
+ if (result.success) {
38
+ savedCount++;
39
+ }
40
+ }
41
+ userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
42
+ if (savedCount > 0) {
43
+ await ctx.client?.tui
44
+ .showToast({
45
+ body: {
46
+ title: "User Memory Learning",
47
+ message: `Learned ${savedCount} pattern${savedCount > 1 ? "s" : ""} from ${prompts.length} prompts`,
48
+ variant: "success",
49
+ duration: 3000,
50
+ },
51
+ })
52
+ .catch(() => { });
53
+ }
54
+ }
55
+ catch (error) {
56
+ log("User memory learning error", { error: String(error) });
57
+ await ctx.client?.tui
58
+ .showToast({
59
+ body: {
60
+ title: "User Memory Learning Failed",
61
+ message: String(error),
62
+ variant: "error",
63
+ duration: 5000,
64
+ },
65
+ })
66
+ .catch(() => { });
67
+ }
68
+ }
69
+ function buildUserAnalysisContext(prompts) {
70
+ return `# User Prompt History Analysis
71
+
72
+ Analyze the following ${prompts.length} user prompts to identify patterns, preferences, and workflows.
73
+
74
+ ## Prompts
75
+
76
+ ${prompts.map((p, i) => `${i + 1}. ${p.content}`).join("\n\n")}
77
+
78
+ ## Analysis Instructions
79
+
80
+ Identify user patterns and preferences from these prompts. Look for:
81
+
82
+ 1. **Preferences**: How the user likes things done
83
+ - Code style preferences (e.g., "prefers code without comments")
84
+ - Communication preferences (e.g., "likes concise responses")
85
+ - Tool preferences (e.g., "prefers TypeScript over JavaScript")
86
+
87
+ 2. **Patterns**: Recurring topics or requests
88
+ - Technical topics (e.g., "often asks about database optimization")
89
+ - Problem domains (e.g., "frequently works on authentication")
90
+ - Skill level indicators (e.g., "asks detailed questions about async patterns")
91
+
92
+ 3. **Workflows**n sequences or habits
93
+ - Development flow (e.g., "usually asks for tests after implementation")
94
+ - Review habits (e.g., "always requests code review before committing")
95
+ - Learning style (e.g., "prefers examples over explanations")
96
+
97
+ Generate 1-5 key insights as memories. Only include patterns that are clearly evident from multiple prompts.`;
98
+ }
99
+ async function analyzeUserPatterns(ctx, context) {
100
+ if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl || !CONFIG.memoryApiKey) {
101
+ throw new Error("External API not configured for user memory learning");
102
+ }
103
+ const { AIProviderFactory } = await import("./ai/ai-provider-factory.js");
104
+ const providerConfig = {
105
+ model: CONFIG.memoryModel,
106
+ apiUrl: CONFIG.memoryApiUrl,
107
+ apiKey: CONFIG.memoryApiKey,
108
+ maxIterations: CONFIG.autoCaptureMaxIterations,
109
+ iterationTimeout: CONFIG.autoCaptureIterationTimeout,
110
+ };
111
+ const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig);
112
+ const systemPrompt = `You are a user behavior analyst for a coding assistant.
113
+
114
+ Your task is to analyze user prompts fy patterns, preferences, and workflows.
115
+
116
+ Use the save_user_memories tool to save identified patterns. Only save patterns that are clearly evident from the prompts.`;
117
+ const toolSchema = {
118
+ type: "function",
119
+ function: {
120
+ name: "save_user_memories",
121
+ description: "Save identified user patterns and preferences",
122
+ parameters: {
123
+ type: "object",
124
+ properties: {
125
+ memories: {
126
+ type: "array",
127
+ description: "Array of identified patterns (1-5 memories)",
128
+ items: {
129
+ type: "object",
130
+ properties: {
131
+ summary: {
132
+ type: "string",
133
+ description: "Clear description of the pattern or preference",
134
+ },
135
+ type: {
136
+ type: "string",
137
+ description: "Type of insight (e.g., preference, pattern, workflow, skill-level, communication-style)",
138
+ },
139
+ reasoning: {
140
+ type: "string",
141
+ description: "Why this pattern is significant (optial)",
142
+ },
143
+ },
144
+ required: ["summary", "type"],
145
+ },
146
+ },
147
+ },
148
+ required: ["memories"],
149
+ },
150
+ },
151
+ };
152
+ const result = await provider.executeToolCall(systemPrompt, context, toolSchema, "user-learning");
153
+ if (!result.success || !result.data) {
154
+ throw new Error(result.error || "Failed to analyze user patterns");
155
+ }
156
+ return result.data.memories || [];
157
+ }
@@ -0,0 +1,38 @@
1
+ export interface UserPrompt {
2
+ id: string;
3
+ sessionId: string;
4
+ messageId: string;
5
+ projectPath: string | null;
6
+ content: string;
7
+ createdAt: number;
8
+ captured: boolean;
9
+ userLearningCaptured: boolean;
10
+ linkedMemoryId: string | null;
11
+ }
12
+ export declare class UserPromptManager {
13
+ private db;
14
+ private readonly dbPath;
15
+ constructor();
16
+ private initDatabase;
17
+ savePrompt(sessionId: string, messageId: string, projectPath: string, content: string): string;
18
+ getLastUncapturedPrompt(sessionId: string): UserPrompt | null;
19
+ deletePrompt(promptId: string): void;
20
+ markAsCaptured(promptId: string): void;
21
+ countUncapturedPrompts(): number;
22
+ getUncapturedPrompts(limit: number): UserPrompt[];
23
+ markMultipleAsCaptured(promptIds: string[]): void;
24
+ countUnanalyzedForUserLearning(): number;
25
+ getPromptsForUserLearning(limit: number): UserPrompt[];
26
+ markAsUserLearningCaptured(promptId: string): void;
27
+ markMultipleAsUserLearningCaptured(promptIds: string[]): void;
28
+ deleteOldPrompts(cutoffTime: number): {
29
+ deleted: number;
30
+ linkedMemoryIds: string[];
31
+ };
32
+ linkMemoryToPrompt(promptId: string, memoryId: string): void;
33
+ getPromptById(promptId: string): UserPrompt | null;
34
+ getCapturedPrompts(projectPath?: string): UserPrompt[];
35
+ private rowToPrompt;
36
+ }
37
+ export declare const userPromptManager: UserPromptManager;
38
+ //# sourceMappingURL=user-prompt-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-prompt-manager.d.ts","sourceRoot":"","sources":["../../../src/services/user-prompt/user-prompt-manager.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,EAAE,CAAW;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAQhC,OAAO,CAAC,YAAY;IA+BpB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAa9F,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAc7D,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKpC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKtC,sBAAsB,IAAI,MAAM;IAMhC,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE;IAYjD,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI;IAUjD,8BAA8B,IAAI,MAAM;IAQxC,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE;IAYtD,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKlD,kCAAkC,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI;IAU7D,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,EAAE,CAAA;KAAE;IAiBpF,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK5D,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAOlD,kBAAkB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,EAAE;IAgBtD,OAAO,CAAC,WAAW;CAapB;AAED,eAAO,MAAM,iBAAiB,mBAA0B,CAAC"}
@@ -0,0 +1,164 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { join } from "node:path";
3
+ import { connectionManager } from "../sqlite/connection-manager.js";
4
+ import { CONFIG } from "../../config.js";
5
+ const USER_PROMPTS_DB_NAME = "user-prompts.db";
6
+ export class UserPromptManager {
7
+ db;
8
+ dbPath;
9
+ constructor() {
10
+ this.dbPath = join(CONFIG.storagePath, USER_PROMPTS_DB_NAME);
11
+ this.db = connectionManager.getConnection(this.dbPath);
12
+ this.initDatabase();
13
+ }
14
+ initDatabase() {
15
+ this.db.run(`
16
+ CREATE TABLE IF NOT EXISTS user_prompts (
17
+ id TEXT PRIMARY KEY,
18
+ session_id TEXT NOT NULL,
19
+ message_id TEXT NOT NULL,
20
+ project_path TEXT,
21
+ content TEXT NOT NULL,
22
+ created_at INTEGER NOT NULL,
23
+ captured BOOLEAN DEFAULT 0,
24
+ user_learning_captured BOOLEAN DEFAULT 0,
25
+ linked_memory_id TEXT
26
+ )
27
+ `);
28
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_user_prompts_session ON user_prompts(session_id)");
29
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_user_prompts_captured ON user_prompts(captured)");
30
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_user_prompts_created ON user_prompts(created_at DESC)");
31
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_user_prompts_project ON user_prompts(project_path)");
32
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_user_prompts_linked ON user_prompts(linked_memory_id)");
33
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_user_prompts_user_learning ON user_prompts(user_learning_captured)");
34
+ }
35
+ savePrompt(sessionId, messageId, projectPath, content) {
36
+ const id = `prompt_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
37
+ const now = Date.now();
38
+ const stmt = this.db.prepare(`
39
+ INSERT INTO user_prompts (id, session_id, message_id, project_path, content, created_at, captured)
40
+ VALUES (?, ?, ?, ?, ?, ?, 0)
41
+ `);
42
+ stmt.run(id, sessionId, messageId, projectPath, content, now);
43
+ return id;
44
+ }
45
+ getLastUncapturedPrompt(sessionId) {
46
+ const stmt = this.db.prepare(`
47
+ SELECT * FROM user_prompts
48
+ WHERE session_id = ? AND captured = 0
49
+ ORDER BY created_at DESC
50
+ LIMIT 1
51
+ `);
52
+ const row = stmt.get(sessionId);
53
+ if (!row)
54
+ return null;
55
+ return this.rowToPrompt(row);
56
+ }
57
+ deletePrompt(promptId) {
58
+ const stmt = this.db.prepare(`DELETE FROM user_prompts WHERE id = ?`);
59
+ stmt.run(promptId);
60
+ }
61
+ markAsCaptured(promptId) {
62
+ const stmt = this.db.prepare(`UPDATE user_prompts SET captured = 1 WHERE id = ?`);
63
+ stmt.run(promptId);
64
+ }
65
+ countUncapturedPrompts() {
66
+ const stmt = this.db.prepare(`SELECT COUNT(*) as count FROM user_prompts WHERE captured = 0`);
67
+ const row = stmt.get();
68
+ return row?.count || 0;
69
+ }
70
+ getUncapturedPrompts(limit) {
71
+ const stmt = this.db.prepare(`
72
+ SELECT * FROM user_prompts
73
+ WHERE captured = 0
74
+ ORDER BY created_at ASC
75
+ LIMIT ?
76
+ `);
77
+ const rows = stmt.all(limit);
78
+ return rows.map((row) => this.rowToPrompt(row));
79
+ }
80
+ markMultipleAsCaptured(promptIds) {
81
+ if (promptIds.length === 0)
82
+ return;
83
+ const placeholders = promptIds.map(() => "?").join(",");
84
+ const stmt = this.db.prepare(`UPDATE user_prompts SET captured = 1 WHERE id IN (${placeholders})`);
85
+ stmt.run(...promptIds);
86
+ }
87
+ countUnanalyzedForUserLearning() {
88
+ const stmt = this.db.prepare(`SELECT COUNT(*) as count FROM user_prompts WHERE user_learning_captured = 0`);
89
+ const row = stmt.get();
90
+ return row?.count || 0;
91
+ }
92
+ getPromptsForUserLearning(limit) {
93
+ const stmt = this.db.prepare(`
94
+ SELECT * FROM user_prompts
95
+ WHERE user_learning_captured = 0
96
+ ORDER BY created_at ASC
97
+ LIMIT ?
98
+ `);
99
+ const rows = stmt.all(limit);
100
+ return rows.map((row) => this.rowToPrompt(row));
101
+ }
102
+ markAsUserLearningCaptured(promptId) {
103
+ const stmt = this.db.prepare(`UPDATE user_prompts SET user_learning_captured = 1 WHERE id = ?`);
104
+ stmt.run(promptId);
105
+ }
106
+ markMultipleAsUserLearningCaptured(promptIds) {
107
+ if (promptIds.length === 0)
108
+ return;
109
+ const placeholders = promptIds.map(() => "?").join(",");
110
+ const stmt = this.db.prepare(`UPDATE user_prompts SET user_learning_captured = 1 WHERE id IN (${placeholders})`);
111
+ stmt.run(...promptIds);
112
+ }
113
+ deleteOldPrompts(cutoffTime) {
114
+ const getLinkedStmt = this.db.prepare(`
115
+ SELECT linked_memory_id FROM user_prompts
116
+ WHERE created_at < ? AND linked_memory_id IS NOT NULL
117
+ `);
118
+ const linkedRows = getLinkedStmt.all(cutoffTime);
119
+ const linkedMemoryIds = linkedRows.map((row) => row.linked_memory_id).filter((id) => id);
120
+ const deleteStmt = this.db.prepare(`DELETE FROM user_prompts WHERE created_at < ?`);
121
+ const result = deleteStmt.run(cutoffTime);
122
+ return {
123
+ deleted: result.changes,
124
+ linkedMemoryIds,
125
+ };
126
+ }
127
+ linkMemoryToPrompt(promptId, memoryId) {
128
+ const stmt = this.db.prepare(`UPDATE user_prompts SET linked_memory_id = ? WHERE id = ?`);
129
+ stmt.run(memoryId, promptId);
130
+ }
131
+ getPromptById(promptId) {
132
+ const stmt = this.db.prepare(`SELECT * FROM user_prompts WHERE id = ?`);
133
+ const row = stmt.get(promptId);
134
+ if (!row)
135
+ return null;
136
+ return this.rowToPrompt(row);
137
+ }
138
+ getCapturedPrompts(projectPath) {
139
+ let query = `SELECT * FROM user_prompts WHERE captured = 1`;
140
+ const params = [];
141
+ if (projectPath) {
142
+ query += ` AND project_path = ?`;
143
+ params.push(projectPath);
144
+ }
145
+ query += ` ORDER BY created_at DESC`;
146
+ const stmt = this.db.prepare(query);
147
+ const rows = stmt.all(...params);
148
+ return rows.map((row) => this.rowToPrompt(row));
149
+ }
150
+ rowToPrompt(row) {
151
+ return {
152
+ id: row.id,
153
+ sessionId: row.session_id,
154
+ messageId: row.message_id,
155
+ projectPath: row.project_path,
156
+ content: row.content,
157
+ createdAt: row.created_at,
158
+ captured: row.captured === 1,
159
+ userLearningCaptured: row.user_learning_captured === 1,
160
+ linkedMemoryId: row.linked_memory_id,
161
+ };
162
+ }
163
+ }
164
+ export const userPromptManager = new UserPromptManager();
@@ -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, } from "./api-handlers.js";
4
+ import { handleListTags, handleListMemories, handleAddMemory, handleDeleteMemory, handleBulkDelete, handleUpdateMemory, handleSearch, handleStats, handlePinMemory, handleUnpinMemory, handleRunCleanup, handleRunDeduplication, handleDetectMigration, handleRunMigration, handleDeletePrompt, handleBulkDeletePrompts, } from "./api-handlers.js";
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
7
  let server = null;
@@ -30,7 +30,9 @@ async function handleRequest(req) {
30
30
  const tag = url.searchParams.get("tag") || undefined;
31
31
  const page = parseInt(url.searchParams.get("page") || "1");
32
32
  const pageSize = parseInt(url.searchParams.get("pageSize") || "20");
33
- const result = await handleListMemories(tag, page, pageSize);
33
+ const scope = url.searchParams.get("scope");
34
+ const includePrompts = url.searchParams.get("includePrompts") !== "false";
35
+ const result = await handleListMemories(tag, page, pageSize, scope, includePrompts);
34
36
  return jsonResponse(result);
35
37
  }
36
38
  if (path === "/api/memories" && method === "POST") {
@@ -39,11 +41,13 @@ async function handleRequest(req) {
39
41
  return jsonResponse(result);
40
42
  }
41
43
  if (path.startsWith("/api/memories/") && method === "DELETE") {
42
- const id = path.split("/").pop();
43
- if (!id) {
44
+ const parts = path.split("/");
45
+ const id = parts[3];
46
+ if (!id || id === "bulk-delete") {
44
47
  return jsonResponse({ success: false, error: "Invalid ID" });
45
48
  }
46
- const result = await handleDeleteMemory(id);
49
+ const cascade = url.searchParams.get("cascade") === "true";
50
+ const result = await handleDeleteMemory(id, cascade);
47
51
  return jsonResponse(result);
48
52
  }
49
53
  if (path.startsWith("/api/memories/") && method === "PUT") {
@@ -57,7 +61,8 @@ async function handleRequest(req) {
57
61
  }
58
62
  if (path === "/api/memories/bulk-delete" && method === "POST") {
59
63
  const body = (await req.json());
60
- const result = await handleBulkDelete(body.ids || []);
64
+ const cascade = body.cascade !== false;
65
+ const result = await handleBulkDelete(body.ids || [], cascade);
61
66
  return jsonResponse(result);
62
67
  }
63
68
  if (path === "/api/search" && method === "GET") {
@@ -112,6 +117,22 @@ async function handleRequest(req) {
112
117
  const result = await handleRunMigration(strategy);
113
118
  return jsonResponse(result);
114
119
  }
120
+ if (path.startsWith("/api/prompts/") && method === "DELETE") {
121
+ const parts = path.split("/");
122
+ const id = parts[3];
123
+ if (!id || id === "bulk-delete") {
124
+ return jsonResponse({ success: false, error: "Invalid ID" });
125
+ }
126
+ const cascade = url.searchParams.get("cascade") === "true";
127
+ const result = await handleDeletePrompt(id, cascade);
128
+ return jsonResponse(result);
129
+ }
130
+ if (path === "/api/prompts/bulk-delete" && method === "POST") {
131
+ const body = (await req.json());
132
+ const cascade = body.cascade !== false;
133
+ const result = await handleBulkDeletePrompts(body.ids || [], cascade);
134
+ return jsonResponse(result);
135
+ }
115
136
  return new Response("Not Found", { status: 404 });
116
137
  }
117
138
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"web-server.d.ts","sourceRoot":"","sources":["../../src/services/web-server.ts"],"names":[],"mappings":"AAOA,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAeD,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,YAAY,CAA8B;gBAEtC,MAAM,EAAE,eAAe;IAI7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YASd,MAAM;IA8Dd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC3B,SAAS,IAAI,OAAO;IAIpB,aAAa,IAAI,OAAO;IAIxB,MAAM,IAAI,MAAM;IAIV,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;CAW/C;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAIhF"}
1
+ {"version":3,"file":"web-server.d.ts","sourceRoot":"","sources":["../../src/services/web-server.ts"],"names":[],"mappings":"AAOA,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAeD,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,YAAY,CAA8B;gBAEtC,MAAM,EAAE,eAAe;IAI7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YASd,MAAM;IA4Dd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiC3B,SAAS,IAAI,OAAO;IAIpB,aAAa,IAAI,OAAO;IAIxB,MAAM,IAAI,MAAM;IAIV,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;CAW/C;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAIhF"}