feique 1.1.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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +220 -0
  3. package/README.md +265 -0
  4. package/dist/backend/claude.d.ts +36 -0
  5. package/dist/backend/claude.js +358 -0
  6. package/dist/backend/claude.js.map +1 -0
  7. package/dist/backend/codex.d.ts +31 -0
  8. package/dist/backend/codex.js +100 -0
  9. package/dist/backend/codex.js.map +1 -0
  10. package/dist/backend/factory.d.ts +9 -0
  11. package/dist/backend/factory.js +56 -0
  12. package/dist/backend/factory.js.map +1 -0
  13. package/dist/backend/types.d.ts +54 -0
  14. package/dist/backend/types.js +2 -0
  15. package/dist/backend/types.js.map +1 -0
  16. package/dist/bridge/commands.d.ts +135 -0
  17. package/dist/bridge/commands.js +860 -0
  18. package/dist/bridge/commands.js.map +1 -0
  19. package/dist/bridge/service.d.ts +160 -0
  20. package/dist/bridge/service.js +3785 -0
  21. package/dist/bridge/service.js.map +1 -0
  22. package/dist/bridge/task-queue.d.ts +14 -0
  23. package/dist/bridge/task-queue.js +81 -0
  24. package/dist/bridge/task-queue.js.map +1 -0
  25. package/dist/bridge/types.d.ts +39 -0
  26. package/dist/bridge/types.js +2 -0
  27. package/dist/bridge/types.js.map +1 -0
  28. package/dist/cli.d.ts +2 -0
  29. package/dist/cli.js +1199 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/codex/capabilities.d.ts +20 -0
  32. package/dist/codex/capabilities.js +41 -0
  33. package/dist/codex/capabilities.js.map +1 -0
  34. package/dist/codex/runner.d.ts +47 -0
  35. package/dist/codex/runner.js +294 -0
  36. package/dist/codex/runner.js.map +1 -0
  37. package/dist/codex/session-index.d.ts +22 -0
  38. package/dist/codex/session-index.js +205 -0
  39. package/dist/codex/session-index.js.map +1 -0
  40. package/dist/collaboration/awareness.d.ts +36 -0
  41. package/dist/collaboration/awareness.js +107 -0
  42. package/dist/collaboration/awareness.js.map +1 -0
  43. package/dist/collaboration/digest.d.ts +65 -0
  44. package/dist/collaboration/digest.js +178 -0
  45. package/dist/collaboration/digest.js.map +1 -0
  46. package/dist/collaboration/handoff.d.ts +66 -0
  47. package/dist/collaboration/handoff.js +94 -0
  48. package/dist/collaboration/handoff.js.map +1 -0
  49. package/dist/collaboration/insights.d.ts +24 -0
  50. package/dist/collaboration/insights.js +243 -0
  51. package/dist/collaboration/insights.js.map +1 -0
  52. package/dist/collaboration/knowledge.d.ts +26 -0
  53. package/dist/collaboration/knowledge.js +105 -0
  54. package/dist/collaboration/knowledge.js.map +1 -0
  55. package/dist/collaboration/timeline.d.ts +31 -0
  56. package/dist/collaboration/timeline.js +150 -0
  57. package/dist/collaboration/timeline.js.map +1 -0
  58. package/dist/collaboration/trust.d.ts +49 -0
  59. package/dist/collaboration/trust.js +176 -0
  60. package/dist/collaboration/trust.js.map +1 -0
  61. package/dist/config/codex-skill.d.ts +7 -0
  62. package/dist/config/codex-skill.js +44 -0
  63. package/dist/config/codex-skill.js.map +1 -0
  64. package/dist/config/doctor.d.ts +12 -0
  65. package/dist/config/doctor.js +314 -0
  66. package/dist/config/doctor.js.map +1 -0
  67. package/dist/config/init.d.ts +3 -0
  68. package/dist/config/init.js +123 -0
  69. package/dist/config/init.js.map +1 -0
  70. package/dist/config/load.d.ts +33 -0
  71. package/dist/config/load.js +252 -0
  72. package/dist/config/load.js.map +1 -0
  73. package/dist/config/mutate.d.ts +21 -0
  74. package/dist/config/mutate.js +86 -0
  75. package/dist/config/mutate.js.map +1 -0
  76. package/dist/config/paths.d.ts +3 -0
  77. package/dist/config/paths.js +33 -0
  78. package/dist/config/paths.js.map +1 -0
  79. package/dist/config/schema.d.ts +308 -0
  80. package/dist/config/schema.js +250 -0
  81. package/dist/config/schema.js.map +1 -0
  82. package/dist/control-plane/project-session.d.ts +67 -0
  83. package/dist/control-plane/project-session.js +234 -0
  84. package/dist/control-plane/project-session.js.map +1 -0
  85. package/dist/feishu/base.d.ts +19 -0
  86. package/dist/feishu/base.js +93 -0
  87. package/dist/feishu/base.js.map +1 -0
  88. package/dist/feishu/cards.d.ts +22 -0
  89. package/dist/feishu/cards.js +144 -0
  90. package/dist/feishu/cards.js.map +1 -0
  91. package/dist/feishu/client.d.ts +61 -0
  92. package/dist/feishu/client.js +315 -0
  93. package/dist/feishu/client.js.map +1 -0
  94. package/dist/feishu/diagnostics.d.ts +42 -0
  95. package/dist/feishu/diagnostics.js +194 -0
  96. package/dist/feishu/diagnostics.js.map +1 -0
  97. package/dist/feishu/doc.d.ts +13 -0
  98. package/dist/feishu/doc.js +59 -0
  99. package/dist/feishu/doc.js.map +1 -0
  100. package/dist/feishu/extractors.d.ts +7 -0
  101. package/dist/feishu/extractors.js +215 -0
  102. package/dist/feishu/extractors.js.map +1 -0
  103. package/dist/feishu/long-connection.d.ts +12 -0
  104. package/dist/feishu/long-connection.js +41 -0
  105. package/dist/feishu/long-connection.js.map +1 -0
  106. package/dist/feishu/message-resource.d.ts +14 -0
  107. package/dist/feishu/message-resource.js +309 -0
  108. package/dist/feishu/message-resource.js.map +1 -0
  109. package/dist/feishu/replay.d.ts +37 -0
  110. package/dist/feishu/replay.js +114 -0
  111. package/dist/feishu/replay.js.map +1 -0
  112. package/dist/feishu/task.d.ts +18 -0
  113. package/dist/feishu/task.js +86 -0
  114. package/dist/feishu/task.js.map +1 -0
  115. package/dist/feishu/text.d.ts +23 -0
  116. package/dist/feishu/text.js +155 -0
  117. package/dist/feishu/text.js.map +1 -0
  118. package/dist/feishu/webhook.d.ts +23 -0
  119. package/dist/feishu/webhook.js +130 -0
  120. package/dist/feishu/webhook.js.map +1 -0
  121. package/dist/feishu/wiki.d.ts +52 -0
  122. package/dist/feishu/wiki.js +300 -0
  123. package/dist/feishu/wiki.js.map +1 -0
  124. package/dist/index.d.ts +9 -0
  125. package/dist/index.js +9 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/knowledge/search.d.ts +11 -0
  128. package/dist/knowledge/search.js +83 -0
  129. package/dist/knowledge/search.js.map +1 -0
  130. package/dist/logging.d.ts +3 -0
  131. package/dist/logging.js +40 -0
  132. package/dist/logging.js.map +1 -0
  133. package/dist/mcp/server.d.ts +34 -0
  134. package/dist/mcp/server.js +1196 -0
  135. package/dist/mcp/server.js.map +1 -0
  136. package/dist/memory/embedding-factory.d.ts +6 -0
  137. package/dist/memory/embedding-factory.js +20 -0
  138. package/dist/memory/embedding-factory.js.map +1 -0
  139. package/dist/memory/embeddings.d.ts +40 -0
  140. package/dist/memory/embeddings.js +150 -0
  141. package/dist/memory/embeddings.js.map +1 -0
  142. package/dist/memory/ollama-embeddings.d.ts +63 -0
  143. package/dist/memory/ollama-embeddings.js +215 -0
  144. package/dist/memory/ollama-embeddings.js.map +1 -0
  145. package/dist/memory/retrieve.d.ts +17 -0
  146. package/dist/memory/retrieve.js +29 -0
  147. package/dist/memory/retrieve.js.map +1 -0
  148. package/dist/memory/summarize.d.ts +13 -0
  149. package/dist/memory/summarize.js +58 -0
  150. package/dist/memory/summarize.js.map +1 -0
  151. package/dist/observability/cost.d.ts +12 -0
  152. package/dist/observability/cost.js +22 -0
  153. package/dist/observability/cost.js.map +1 -0
  154. package/dist/observability/dashboard-html.d.ts +5 -0
  155. package/dist/observability/dashboard-html.js +304 -0
  156. package/dist/observability/dashboard-html.js.map +1 -0
  157. package/dist/observability/metrics.d.ts +36 -0
  158. package/dist/observability/metrics.js +230 -0
  159. package/dist/observability/metrics.js.map +1 -0
  160. package/dist/observability/readiness.d.ts +31 -0
  161. package/dist/observability/readiness.js +57 -0
  162. package/dist/observability/readiness.js.map +1 -0
  163. package/dist/observability/server.d.ts +84 -0
  164. package/dist/observability/server.js +181 -0
  165. package/dist/observability/server.js.map +1 -0
  166. package/dist/projects/paths.d.ts +9 -0
  167. package/dist/projects/paths.js +30 -0
  168. package/dist/projects/paths.js.map +1 -0
  169. package/dist/runtime/instance-lock.d.ts +12 -0
  170. package/dist/runtime/instance-lock.js +99 -0
  171. package/dist/runtime/instance-lock.js.map +1 -0
  172. package/dist/runtime/process.d.ts +2 -0
  173. package/dist/runtime/process.js +43 -0
  174. package/dist/runtime/process.js.map +1 -0
  175. package/dist/runtime/shutdown.d.ts +11 -0
  176. package/dist/runtime/shutdown.js +38 -0
  177. package/dist/runtime/shutdown.js.map +1 -0
  178. package/dist/security/access.d.ts +13 -0
  179. package/dist/security/access.js +160 -0
  180. package/dist/security/access.js.map +1 -0
  181. package/dist/service/install.d.ts +19 -0
  182. package/dist/service/install.js +35 -0
  183. package/dist/service/install.js.map +1 -0
  184. package/dist/service/templates.d.ts +22 -0
  185. package/dist/service/templates.js +118 -0
  186. package/dist/service/templates.js.map +1 -0
  187. package/dist/state/audit-log.d.ts +33 -0
  188. package/dist/state/audit-log.js +116 -0
  189. package/dist/state/audit-log.js.map +1 -0
  190. package/dist/state/config-history-store.d.ts +27 -0
  191. package/dist/state/config-history-store.js +65 -0
  192. package/dist/state/config-history-store.js.map +1 -0
  193. package/dist/state/handoff-store.d.ts +20 -0
  194. package/dist/state/handoff-store.js +97 -0
  195. package/dist/state/handoff-store.js.map +1 -0
  196. package/dist/state/idempotency-store.d.ts +19 -0
  197. package/dist/state/idempotency-store.js +84 -0
  198. package/dist/state/idempotency-store.js.map +1 -0
  199. package/dist/state/memory-store.d.ts +137 -0
  200. package/dist/state/memory-store.js +713 -0
  201. package/dist/state/memory-store.js.map +1 -0
  202. package/dist/state/pending-command-store.d.ts +30 -0
  203. package/dist/state/pending-command-store.js +108 -0
  204. package/dist/state/pending-command-store.js.map +1 -0
  205. package/dist/state/run-state-store.d.ts +58 -0
  206. package/dist/state/run-state-store.js +269 -0
  207. package/dist/state/run-state-store.js.map +1 -0
  208. package/dist/state/session-store.d.ts +56 -0
  209. package/dist/state/session-store.js +275 -0
  210. package/dist/state/session-store.js.map +1 -0
  211. package/dist/state/trust-store.d.ts +15 -0
  212. package/dist/state/trust-store.js +53 -0
  213. package/dist/state/trust-store.js.map +1 -0
  214. package/dist/utils/fs.d.ts +4 -0
  215. package/dist/utils/fs.js +26 -0
  216. package/dist/utils/fs.js.map +1 -0
  217. package/dist/utils/json.d.ts +1 -0
  218. package/dist/utils/json.js +9 -0
  219. package/dist/utils/json.js.map +1 -0
  220. package/dist/utils/path.d.ts +3 -0
  221. package/dist/utils/path.js +22 -0
  222. package/dist/utils/path.js.map +1 -0
  223. package/dist/utils/serial-executor.d.ts +5 -0
  224. package/dist/utils/serial-executor.js +12 -0
  225. package/dist/utils/serial-executor.js.map +1 -0
  226. package/package.json +71 -0
  227. package/skills/feique-session/SKILL.md +27 -0
@@ -0,0 +1,713 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { ensureDir } from '../utils/fs.js';
6
+ import { SerialExecutor } from '../utils/serial-executor.js';
7
+ import { LocalEmbeddingProvider, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from '../memory/embeddings.js';
8
+ const MEMORY_SCHEMA_VERSION = 5;
9
+ export class MemoryStore {
10
+ dbPath;
11
+ serial = new SerialExecutor();
12
+ embedder;
13
+ _db = null;
14
+ _schemaReady = false;
15
+ _lastTs = 0;
16
+ constructor(stateDir, embedder) {
17
+ this.dbPath = path.join(stateDir, 'memory.db');
18
+ this.embedder = embedder ?? new LocalEmbeddingProvider();
19
+ }
20
+ /** Return an ISO timestamp that is strictly greater than any previous call. */
21
+ monotonicNow() {
22
+ const now = Date.now();
23
+ this._lastTs = now > this._lastTs ? now : this._lastTs + 1;
24
+ return new Date(this._lastTs).toISOString();
25
+ }
26
+ async upsertThreadSummary(record) {
27
+ return this.serial.run(async () => {
28
+ const now = this.monotonicNow();
29
+ return this.withDb((db) => {
30
+ db.prepare(`
31
+ INSERT INTO thread_summaries (
32
+ conversation_key, project_alias, thread_id, summary, recent_prompt, recent_response_excerpt,
33
+ files_touched_json, open_tasks_json, decisions_json, created_at, updated_at
34
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
35
+ ON CONFLICT(conversation_key, project_alias, thread_id) DO UPDATE SET
36
+ summary = excluded.summary,
37
+ recent_prompt = excluded.recent_prompt,
38
+ recent_response_excerpt = excluded.recent_response_excerpt,
39
+ files_touched_json = excluded.files_touched_json,
40
+ open_tasks_json = excluded.open_tasks_json,
41
+ decisions_json = excluded.decisions_json,
42
+ updated_at = excluded.updated_at
43
+ `).run(record.conversation_key, record.project_alias, record.thread_id, record.summary, record.recent_prompt ?? null, record.recent_response_excerpt ?? null, JSON.stringify(record.files_touched), JSON.stringify(record.open_tasks), JSON.stringify(record.decisions), now, now);
44
+ const row = db.prepare(`
45
+ SELECT conversation_key, project_alias, thread_id, summary, recent_prompt, recent_response_excerpt,
46
+ files_touched_json, open_tasks_json, decisions_json, created_at, updated_at
47
+ FROM thread_summaries
48
+ WHERE conversation_key = ? AND project_alias = ? AND thread_id = ?
49
+ `).get(record.conversation_key, record.project_alias, record.thread_id);
50
+ return mapThreadSummaryRow(row);
51
+ });
52
+ });
53
+ }
54
+ async getThreadSummary(conversationKey, projectAlias, threadId) {
55
+ await this.serial.wait();
56
+ return this.withDb((db) => {
57
+ const row = db.prepare(`
58
+ SELECT conversation_key, project_alias, thread_id, summary, recent_prompt, recent_response_excerpt,
59
+ files_touched_json, open_tasks_json, decisions_json, created_at, updated_at
60
+ FROM thread_summaries
61
+ WHERE conversation_key = ? AND project_alias = ? AND thread_id = ?
62
+ `).get(conversationKey, projectAlias, threadId);
63
+ return row ? mapThreadSummaryRow(row) : null;
64
+ });
65
+ }
66
+ async saveMemory(input) {
67
+ return this.serial.run(async () => {
68
+ const now = this.monotonicNow();
69
+ const record = {
70
+ id: randomUUID(),
71
+ scope: input.scope,
72
+ project_alias: input.project_alias,
73
+ chat_id: input.scope === 'group' ? input.chat_id : undefined,
74
+ title: input.title,
75
+ content: input.content,
76
+ tags: input.tags ?? [],
77
+ source: input.source ?? 'manual',
78
+ pinned: input.pinned ?? false,
79
+ confidence: input.confidence ?? 1,
80
+ created_by: input.created_by,
81
+ created_at: now,
82
+ updated_at: now,
83
+ last_accessed_at: now,
84
+ expires_at: input.expires_at,
85
+ };
86
+ const embeddingText = `${record.title} ${record.content} ${record.tags.join(' ')}`;
87
+ const embeddingJson = serializeEmbedding(await this.embedder.embed(embeddingText));
88
+ return this.withDb((db) => {
89
+ db.prepare(`
90
+ INSERT INTO project_memories (
91
+ id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at, embedding_json
92
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
93
+ `).run(record.id, record.scope, record.project_alias, record.chat_id ?? null, record.title, record.content, JSON.stringify(record.tags), record.source, record.pinned ? 1 : 0, record.confidence, record.created_by ?? null, record.created_at, record.updated_at, record.last_accessed_at ?? null, record.expires_at ?? null, embeddingJson);
94
+ return record;
95
+ });
96
+ });
97
+ }
98
+ async saveProjectMemory(input) {
99
+ return this.saveMemory({ ...input, scope: 'project' });
100
+ }
101
+ async saveGroupMemory(input) {
102
+ return this.saveMemory({ ...input, scope: 'group' });
103
+ }
104
+ async searchMemories(selector, query, limit, filters) {
105
+ await this.serial.wait();
106
+ const touchedAt = this.monotonicNow();
107
+ const queryEmbedding = await this.embedder.embed(query);
108
+ return this.withDb((db) => {
109
+ const { whereClause, params } = buildMemorySelectorWhere(selector, filters);
110
+ // Phase 1: FTS5 keyword search (works for both Chinese and English)
111
+ const ftsQuery = buildFtsQuery(query);
112
+ let ftsRows = [];
113
+ if (ftsQuery) {
114
+ const { whereClause: joinWhere, params: joinParams } = buildMemorySelectorWhere(selector, filters, undefined, 'pm');
115
+ ftsRows = db.prepare(`
116
+ SELECT pm.id, pm.scope, pm.project_alias, pm.chat_id, pm.title, pm.content, pm.tags_json, pm.source, pm.pinned, pm.confidence, pm.created_by, pm.created_at, pm.updated_at, pm.last_accessed_at, pm.expires_at, pm.embedding_json
117
+ FROM memory_fts
118
+ JOIN project_memories pm ON pm.rowid = memory_fts.rowid
119
+ WHERE ${joinWhere}
120
+ AND memory_fts MATCH ?
121
+ ORDER BY pm.pinned DESC, bm25(memory_fts), pm.updated_at DESC
122
+ LIMIT ?
123
+ `).all(...joinParams, ftsQuery, limit * 2);
124
+ }
125
+ // Phase 2: Vector similarity search (semantic matching)
126
+ const allRows = db.prepare(`
127
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at, embedding_json
128
+ FROM project_memories
129
+ WHERE ${whereClause}
130
+ AND embedding_json IS NOT NULL
131
+ ORDER BY updated_at DESC
132
+ LIMIT ?
133
+ `).all(...params, 200);
134
+ const vectorScored = [];
135
+ for (const row of allRows) {
136
+ const embJson = row.embedding_json;
137
+ if (!embJson)
138
+ continue;
139
+ const embedding = deserializeEmbedding(embJson);
140
+ if (!embedding)
141
+ continue;
142
+ const score = cosineSimilarity(queryEmbedding, embedding);
143
+ if (score > 0.35) {
144
+ vectorScored.push({ row, score });
145
+ }
146
+ }
147
+ vectorScored.sort((a, b) => b.score - a.score);
148
+ // Phase 3: Merge and deduplicate
149
+ const seen = new Set();
150
+ const merged = [];
151
+ // FTS results first (keyword relevance)
152
+ for (const row of ftsRows) {
153
+ if (!seen.has(row.id)) {
154
+ seen.add(row.id);
155
+ merged.push(row);
156
+ }
157
+ }
158
+ // Then vector results (semantic relevance)
159
+ for (const { row } of vectorScored) {
160
+ if (!seen.has(row.id)) {
161
+ seen.add(row.id);
162
+ merged.push(row);
163
+ }
164
+ }
165
+ // Fallback: LIKE search if both FTS and vector returned nothing
166
+ if (merged.length === 0) {
167
+ const normalized = `%${query.trim().toLowerCase()}%`;
168
+ const fallbackRows = db.prepare(`
169
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at, embedding_json
170
+ FROM project_memories
171
+ WHERE ${whereClause}
172
+ AND (
173
+ lower(title) LIKE ?
174
+ OR lower(content) LIKE ?
175
+ OR lower(tags_json) LIKE ?
176
+ )
177
+ ORDER BY pinned DESC, updated_at DESC
178
+ LIMIT ?
179
+ `).all(...params, normalized, normalized, normalized, limit);
180
+ return touchRowsAndMap(db, fallbackRows, touchedAt);
181
+ }
182
+ // Pinned first, then limit
183
+ merged.sort((a, b) => (b.pinned ?? 0) - (a.pinned ?? 0));
184
+ const records = touchRowsAndMap(db, merged.slice(0, limit), touchedAt);
185
+ // Post-filter on mapped records: ensure expired/archived records don't leak
186
+ const now = new Date().toISOString();
187
+ return records.filter((r) => !r.archived_at && (!r.expires_at || r.expires_at > now));
188
+ });
189
+ }
190
+ async searchProjectMemories(projectAlias, query, limit, filters) {
191
+ return this.searchMemories({ scope: 'project', project_alias: projectAlias }, query, limit, filters);
192
+ }
193
+ async searchGroupMemories(projectAlias, chatId, query, limit, filters) {
194
+ return this.searchMemories({ scope: 'group', project_alias: projectAlias, chat_id: chatId }, query, limit, filters);
195
+ }
196
+ async getMemory(selector, id) {
197
+ await this.serial.wait();
198
+ return this.withDb((db) => {
199
+ const { whereClause, params } = buildMemorySelectorWhere(selector);
200
+ const row = db.prepare(`
201
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, archived_at, archived_by, archive_reason, expires_at
202
+ FROM project_memories
203
+ WHERE id = ?
204
+ AND ${whereClause}
205
+ `).get(id, ...params);
206
+ return row ? mapMemoryRow(row) : null;
207
+ });
208
+ }
209
+ async getMemoryById(selector, id, options) {
210
+ await this.serial.wait();
211
+ return this.withDb((db) => {
212
+ const { whereClause, params } = buildMemorySelectorWhere(selector, undefined, options);
213
+ const row = db.prepare(`
214
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, archived_at, archived_by, archive_reason, expires_at
215
+ FROM project_memories
216
+ WHERE id = ?
217
+ AND ${whereClause}
218
+ `).get(id, ...params);
219
+ return row ? mapMemoryRow(row) : null;
220
+ });
221
+ }
222
+ async listRecentMemories(selector, limit, filters) {
223
+ await this.serial.wait();
224
+ const touchedAt = this.monotonicNow();
225
+ return this.withDb((db) => {
226
+ const { whereClause, params } = buildMemorySelectorWhere(selector, filters);
227
+ const rows = db.prepare(`
228
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at
229
+ FROM project_memories
230
+ WHERE ${whereClause}
231
+ ORDER BY pinned DESC, updated_at DESC
232
+ LIMIT ?
233
+ `).all(...params, limit);
234
+ return touchRowsAndMap(db, rows, touchedAt);
235
+ });
236
+ }
237
+ async listRecentProjectMemories(projectAlias, limit, filters) {
238
+ return this.listRecentMemories({ scope: 'project', project_alias: projectAlias }, limit, filters);
239
+ }
240
+ async listRecentGroupMemories(projectAlias, chatId, limit, filters) {
241
+ return this.listRecentMemories({ scope: 'group', project_alias: projectAlias, chat_id: chatId }, limit, filters);
242
+ }
243
+ async countMemories(selector) {
244
+ await this.serial.wait();
245
+ return this.withDb((db) => {
246
+ const { whereClause, params } = buildMemorySelectorWhere(selector);
247
+ const row = db.prepare(`
248
+ SELECT COUNT(*) AS count
249
+ FROM project_memories
250
+ WHERE ${whereClause}
251
+ `).get(...params);
252
+ return Number(row.count ?? 0);
253
+ });
254
+ }
255
+ async countProjectMemories(projectAlias) {
256
+ return this.countMemories({ scope: 'project', project_alias: projectAlias });
257
+ }
258
+ async countGroupMemories(projectAlias, chatId) {
259
+ return this.countMemories({ scope: 'group', project_alias: projectAlias, chat_id: chatId });
260
+ }
261
+ async countPinnedMemories(selector) {
262
+ await this.serial.wait();
263
+ return this.withDb((db) => {
264
+ const { whereClause, params } = buildMemorySelectorWhere(selector);
265
+ const row = db.prepare(`
266
+ SELECT COUNT(*) AS count
267
+ FROM project_memories
268
+ WHERE ${whereClause}
269
+ AND pinned = 1
270
+ `).get(...params);
271
+ return Number(row.count ?? 0);
272
+ });
273
+ }
274
+ async countPinnedProjectMemories(projectAlias) {
275
+ return this.countPinnedMemories({ scope: 'project', project_alias: projectAlias });
276
+ }
277
+ async countPinnedGroupMemories(projectAlias, chatId) {
278
+ return this.countPinnedMemories({ scope: 'group', project_alias: projectAlias, chat_id: chatId });
279
+ }
280
+ async getMemoryStats(selector) {
281
+ await this.serial.wait();
282
+ return this.withDb((db) => {
283
+ const now = this.monotonicNow();
284
+ const params = [selector.scope, selector.project_alias];
285
+ const baseClauses = ['scope = ?', 'project_alias = ?'];
286
+ if (selector.scope === 'group') {
287
+ baseClauses.push('chat_id = ?');
288
+ params.push(selector.chat_id ?? null);
289
+ }
290
+ const baseWhere = baseClauses.join(' AND ');
291
+ const row = db.prepare(`
292
+ SELECT
293
+ SUM(CASE WHEN archived_at IS NULL AND (expires_at IS NULL OR expires_at > ?) THEN 1 ELSE 0 END) AS active_count,
294
+ SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS archived_count,
295
+ SUM(CASE WHEN archived_at IS NULL AND expires_at IS NOT NULL AND expires_at <= ? THEN 1 ELSE 0 END) AS expired_count,
296
+ SUM(CASE WHEN archived_at IS NULL AND pinned = 1 AND (expires_at IS NULL OR expires_at > ?) THEN 1 ELSE 0 END) AS pinned_count,
297
+ MAX(updated_at) AS latest_updated_at,
298
+ MAX(last_accessed_at) AS latest_accessed_at,
299
+ MAX(archived_at) AS latest_archived_at
300
+ FROM project_memories
301
+ WHERE ${baseWhere}
302
+ `).get(now, now, now, ...params);
303
+ return {
304
+ active_count: Number(row.active_count ?? 0),
305
+ archived_count: Number(row.archived_count ?? 0),
306
+ expired_count: Number(row.expired_count ?? 0),
307
+ pinned_count: Number(row.pinned_count ?? 0),
308
+ latest_updated_at: row.latest_updated_at ?? undefined,
309
+ latest_accessed_at: row.latest_accessed_at ?? undefined,
310
+ latest_archived_at: row.latest_archived_at ?? undefined,
311
+ };
312
+ });
313
+ }
314
+ async cleanupExpiredMemories(selector) {
315
+ return this.serial.run(async () => {
316
+ return this.withDb((db) => {
317
+ const now = this.monotonicNow();
318
+ const clauses = ['archived_at IS NULL', 'expires_at IS NOT NULL', 'expires_at <= ?'];
319
+ const params = [now];
320
+ if (selector) {
321
+ clauses.push('scope = ?');
322
+ params.push(selector.scope);
323
+ clauses.push('project_alias = ?');
324
+ params.push(selector.project_alias);
325
+ if (selector.scope === 'group') {
326
+ clauses.push('chat_id = ?');
327
+ params.push(selector.chat_id ?? null);
328
+ }
329
+ }
330
+ const result = db.prepare(`
331
+ UPDATE project_memories
332
+ SET archived_at = ?, archived_by = ?, archive_reason = ?, pinned = 0, updated_at = ?
333
+ WHERE ${clauses.join(' AND ')}
334
+ `).run(now, 'system', 'expired', now, ...params);
335
+ return Number(result.changes ?? 0);
336
+ });
337
+ });
338
+ }
339
+ async getOldestPinnedMemory(selector, basis = 'updated_at') {
340
+ await this.serial.wait();
341
+ return this.withDb((db) => {
342
+ const { whereClause, params } = buildMemorySelectorWhere(selector, {});
343
+ const orderBy = basis === 'last_accessed_at'
344
+ ? 'COALESCE(last_accessed_at, updated_at, created_at) ASC, updated_at ASC, rowid ASC'
345
+ : 'updated_at ASC, created_at ASC, rowid ASC';
346
+ const row = db.prepare(`
347
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at
348
+ FROM project_memories
349
+ WHERE ${whereClause}
350
+ AND pinned = 1
351
+ ORDER BY ${orderBy}
352
+ LIMIT 1
353
+ `).get(...params);
354
+ return row ? mapMemoryRow(row) : null;
355
+ });
356
+ }
357
+ async setMemoryPinned(selector, id, pinned) {
358
+ return this.serial.run(async () => {
359
+ const now = this.monotonicNow();
360
+ return this.withDb((db) => {
361
+ const { whereClause, params } = buildMemorySelectorWhere(selector);
362
+ const result = db.prepare(`
363
+ UPDATE project_memories
364
+ SET pinned = ?, updated_at = ?, last_accessed_at = ?
365
+ WHERE id = ?
366
+ AND ${whereClause}
367
+ `).run(pinned ? 1 : 0, now, now, id, ...params);
368
+ if (result.changes === 0) {
369
+ return null;
370
+ }
371
+ const row = db.prepare(`
372
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at
373
+ FROM project_memories
374
+ WHERE id = ?
375
+ AND ${whereClause}
376
+ `).get(id, ...params);
377
+ return row ? mapMemoryRow(row) : null;
378
+ });
379
+ });
380
+ }
381
+ async archiveMemory(selector, id, input) {
382
+ return this.serial.run(async () => {
383
+ const now = this.monotonicNow();
384
+ return this.withDb((db) => {
385
+ const { whereClause, params } = buildMemorySelectorWhere(selector);
386
+ const result = db.prepare(`
387
+ UPDATE project_memories
388
+ SET archived_at = ?, archived_by = ?, archive_reason = ?, pinned = 0, updated_at = ?
389
+ WHERE id = ?
390
+ AND ${whereClause}
391
+ `).run(now, input?.archived_by ?? null, input?.reason ?? 'manual', now, id, ...params);
392
+ if (result.changes === 0) {
393
+ return null;
394
+ }
395
+ const row = db.prepare(`
396
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, archived_at, archived_by, archive_reason, expires_at
397
+ FROM project_memories
398
+ WHERE id = ?
399
+ AND scope = ?
400
+ AND project_alias = ?
401
+ ${selector.scope === 'group' ? 'AND chat_id = ?' : ''}
402
+ `).get(id, selector.scope, selector.project_alias, ...(selector.scope === 'group' ? [selector.chat_id ?? null] : []));
403
+ return row ? mapMemoryRow(row) : null;
404
+ });
405
+ });
406
+ }
407
+ async restoreMemory(selector, id, restoredBy) {
408
+ return this.serial.run(async () => {
409
+ const now = this.monotonicNow();
410
+ return this.withDb((db) => {
411
+ const { whereClause, params } = buildMemorySelectorWhere(selector, undefined, { includeArchived: true, includeExpired: true });
412
+ const result = db.prepare(`
413
+ UPDATE project_memories
414
+ SET archived_at = NULL,
415
+ archived_by = NULL,
416
+ archive_reason = NULL,
417
+ updated_at = ?,
418
+ last_accessed_at = ?,
419
+ expires_at = CASE
420
+ WHEN expires_at IS NOT NULL AND expires_at <= ? THEN NULL
421
+ ELSE expires_at
422
+ END
423
+ WHERE id = ?
424
+ AND archived_at IS NOT NULL
425
+ AND ${whereClause}
426
+ `).run(now, now, now, id, ...params);
427
+ if (result.changes === 0) {
428
+ return null;
429
+ }
430
+ const restored = db.prepare(`
431
+ SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, archived_at, archived_by, archive_reason, expires_at
432
+ FROM project_memories
433
+ WHERE id = ?
434
+ AND ${whereClause}
435
+ `).get(id, ...params);
436
+ if (restoredBy) {
437
+ void restoredBy;
438
+ }
439
+ return restored ? mapMemoryRow(restored) : null;
440
+ });
441
+ });
442
+ }
443
+ async ensureReady() {
444
+ await this.serial.run(async () => {
445
+ await ensureDir(path.dirname(this.dbPath));
446
+ this.getDb();
447
+ });
448
+ }
449
+ close() {
450
+ if (this._db) {
451
+ this._db.close();
452
+ this._db = null;
453
+ this._schemaReady = false;
454
+ }
455
+ }
456
+ getDb() {
457
+ if (!this._db) {
458
+ fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
459
+ this._db = new DatabaseSync(this.dbPath);
460
+ }
461
+ if (!this._schemaReady) {
462
+ initializeSchema(this._db);
463
+ this._schemaReady = true;
464
+ }
465
+ return this._db;
466
+ }
467
+ withDb(callback) {
468
+ return callback(this.getDb());
469
+ }
470
+ }
471
+ function initializeSchema(db) {
472
+ db.exec('PRAGMA journal_mode = WAL;');
473
+ db.exec(`
474
+ CREATE TABLE IF NOT EXISTS thread_summaries (
475
+ conversation_key TEXT NOT NULL,
476
+ project_alias TEXT NOT NULL,
477
+ thread_id TEXT NOT NULL,
478
+ summary TEXT NOT NULL,
479
+ recent_prompt TEXT,
480
+ recent_response_excerpt TEXT,
481
+ files_touched_json TEXT NOT NULL DEFAULT '[]',
482
+ open_tasks_json TEXT NOT NULL DEFAULT '[]',
483
+ decisions_json TEXT NOT NULL DEFAULT '[]',
484
+ created_at TEXT NOT NULL,
485
+ updated_at TEXT NOT NULL,
486
+ PRIMARY KEY (conversation_key, project_alias, thread_id)
487
+ );
488
+
489
+ CREATE TABLE IF NOT EXISTS project_memories (
490
+ id TEXT PRIMARY KEY,
491
+ scope TEXT NOT NULL DEFAULT 'project',
492
+ project_alias TEXT NOT NULL,
493
+ chat_id TEXT,
494
+ title TEXT NOT NULL,
495
+ content TEXT NOT NULL,
496
+ tags_json TEXT NOT NULL DEFAULT '[]',
497
+ source TEXT NOT NULL DEFAULT 'manual',
498
+ pinned INTEGER NOT NULL DEFAULT 0,
499
+ confidence REAL NOT NULL DEFAULT 1,
500
+ created_by TEXT,
501
+ created_at TEXT NOT NULL,
502
+ updated_at TEXT NOT NULL,
503
+ last_accessed_at TEXT,
504
+ archived_at TEXT,
505
+ archived_by TEXT,
506
+ archive_reason TEXT,
507
+ expires_at TEXT
508
+ );
509
+ `);
510
+ const userVersion = getUserVersion(db);
511
+ if (userVersion < MEMORY_SCHEMA_VERSION) {
512
+ migrateProjectMemoriesSchema(db);
513
+ db.exec(`
514
+ DROP TRIGGER IF EXISTS project_memories_ai;
515
+ DROP TRIGGER IF EXISTS project_memories_ad;
516
+ DROP TRIGGER IF EXISTS project_memories_au;
517
+ DROP TABLE IF EXISTS memory_fts;
518
+ `);
519
+ }
520
+ db.exec(`
521
+ CREATE INDEX IF NOT EXISTS idx_project_memories_scope_project_updated
522
+ ON project_memories(scope, project_alias, updated_at DESC);
523
+
524
+ CREATE INDEX IF NOT EXISTS idx_project_memories_scope_project_chat_updated
525
+ ON project_memories(scope, project_alias, chat_id, updated_at DESC);
526
+
527
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
528
+ title,
529
+ content,
530
+ tags_json,
531
+ content='project_memories',
532
+ content_rowid='rowid',
533
+ tokenize='unicode61'
534
+ );
535
+
536
+ CREATE TRIGGER IF NOT EXISTS project_memories_ai AFTER INSERT ON project_memories BEGIN
537
+ INSERT INTO memory_fts(rowid, title, content, tags_json)
538
+ VALUES (new.rowid, new.title, new.content, new.tags_json);
539
+ END;
540
+
541
+ CREATE TRIGGER IF NOT EXISTS project_memories_ad AFTER DELETE ON project_memories BEGIN
542
+ INSERT INTO memory_fts(memory_fts, rowid, title, content, tags_json)
543
+ VALUES ('delete', old.rowid, old.title, old.content, old.tags_json);
544
+ END;
545
+
546
+ CREATE TRIGGER IF NOT EXISTS project_memories_au AFTER UPDATE ON project_memories BEGIN
547
+ INSERT INTO memory_fts(memory_fts, rowid, title, content, tags_json)
548
+ VALUES ('delete', old.rowid, old.title, old.content, old.tags_json);
549
+ INSERT INTO memory_fts(rowid, title, content, tags_json)
550
+ VALUES (new.rowid, new.title, new.content, new.tags_json);
551
+ END;
552
+ `);
553
+ if (userVersion < MEMORY_SCHEMA_VERSION) {
554
+ db.prepare("INSERT INTO memory_fts(memory_fts) VALUES ('rebuild')").run();
555
+ db.exec(`PRAGMA user_version = ${MEMORY_SCHEMA_VERSION};`);
556
+ }
557
+ }
558
+ function migrateProjectMemoriesSchema(db) {
559
+ const columns = db.prepare('PRAGMA table_info(project_memories)').all();
560
+ const names = new Set(columns.map((column) => column.name));
561
+ if (!names.has('scope')) {
562
+ db.exec("ALTER TABLE project_memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'project'");
563
+ }
564
+ if (!names.has('chat_id')) {
565
+ db.exec('ALTER TABLE project_memories ADD COLUMN chat_id TEXT');
566
+ }
567
+ if (!names.has('last_accessed_at')) {
568
+ db.exec('ALTER TABLE project_memories ADD COLUMN last_accessed_at TEXT');
569
+ db.exec('UPDATE project_memories SET last_accessed_at = updated_at WHERE last_accessed_at IS NULL');
570
+ }
571
+ if (!names.has('archived_at')) {
572
+ db.exec('ALTER TABLE project_memories ADD COLUMN archived_at TEXT');
573
+ }
574
+ if (!names.has('archived_by')) {
575
+ db.exec('ALTER TABLE project_memories ADD COLUMN archived_by TEXT');
576
+ }
577
+ if (!names.has('archive_reason')) {
578
+ db.exec('ALTER TABLE project_memories ADD COLUMN archive_reason TEXT');
579
+ }
580
+ if (!names.has('embedding_json')) {
581
+ db.exec('ALTER TABLE project_memories ADD COLUMN embedding_json TEXT');
582
+ }
583
+ }
584
+ function getUserVersion(db) {
585
+ const row = db.prepare('PRAGMA user_version').get();
586
+ return Number(row?.user_version ?? 0);
587
+ }
588
+ function buildMemorySelectorWhere(selector, filters, options, tablePrefix) {
589
+ const p = tablePrefix ? `${tablePrefix}.` : '';
590
+ const params = [selector.scope, selector.project_alias];
591
+ const clauses = [
592
+ `${p}scope = ?`,
593
+ `${p}project_alias = ?`,
594
+ ];
595
+ if (!options?.includeExpired) {
596
+ clauses.push(`(${p}expires_at IS NULL OR ${p}expires_at > ?)`);
597
+ params.push(new Date().toISOString());
598
+ }
599
+ if (!options?.includeArchived) {
600
+ clauses.push(`${p}archived_at IS NULL`);
601
+ }
602
+ if (selector.scope === 'group') {
603
+ clauses.push(`${p}chat_id = ?`);
604
+ params.push(selector.chat_id ?? null);
605
+ }
606
+ if (filters?.source) {
607
+ clauses.push(`${p}source = ?`);
608
+ params.push(filters.source);
609
+ }
610
+ if (filters?.created_by) {
611
+ clauses.push(`${p}created_by = ?`);
612
+ params.push(filters.created_by);
613
+ }
614
+ if (filters?.tag) {
615
+ clauses.push(`lower(${p}tags_json) LIKE ?`);
616
+ params.push(`%${filters.tag.toLowerCase()}%`);
617
+ }
618
+ return {
619
+ whereClause: clauses.join(' AND '),
620
+ params,
621
+ };
622
+ }
623
+ /**
624
+ * Build FTS5 query supporting both English and Chinese text.
625
+ * English: word prefix matching (e.g., "auth*")
626
+ * Chinese: character-level matching (FTS5 unicode61 tokenizes CJK per-character)
627
+ */
628
+ function buildFtsQuery(input) {
629
+ const terms = [];
630
+ // Extract English/numeric tokens → prefix match
631
+ const englishTokens = input.toLowerCase().match(/[a-z][a-z0-9_-]*/g);
632
+ if (englishTokens) {
633
+ for (const token of englishTokens) {
634
+ if (token.length > 1) {
635
+ terms.push(`${token}*`);
636
+ }
637
+ }
638
+ }
639
+ // Extract CJK characters → exact character match
640
+ // FTS5 with unicode61 tokenizes CJK text per-character, so each character is a token
641
+ const cjkChars = input.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g);
642
+ if (cjkChars) {
643
+ for (const char of cjkChars) {
644
+ terms.push(char);
645
+ }
646
+ }
647
+ if (terms.length === 0) {
648
+ return null;
649
+ }
650
+ // Use OR for mixed queries (find docs matching any term)
651
+ // then rely on BM25 scoring to rank by relevance
652
+ return terms.join(' OR ');
653
+ }
654
+ function mapThreadSummaryRow(row) {
655
+ return {
656
+ conversation_key: row.conversation_key,
657
+ project_alias: row.project_alias,
658
+ thread_id: row.thread_id,
659
+ summary: row.summary,
660
+ recent_prompt: row.recent_prompt ?? undefined,
661
+ recent_response_excerpt: row.recent_response_excerpt ?? undefined,
662
+ files_touched: parseJsonArray(row.files_touched_json),
663
+ open_tasks: parseJsonArray(row.open_tasks_json),
664
+ decisions: parseJsonArray(row.decisions_json),
665
+ created_at: row.created_at,
666
+ updated_at: row.updated_at,
667
+ };
668
+ }
669
+ function mapMemoryRow(row) {
670
+ return {
671
+ id: row.id,
672
+ scope: row.scope === 'group' ? 'group' : 'project',
673
+ project_alias: row.project_alias,
674
+ chat_id: row.chat_id ?? undefined,
675
+ title: row.title,
676
+ content: row.content,
677
+ tags: parseJsonArray(row.tags_json),
678
+ source: row.source,
679
+ pinned: row.pinned === 1,
680
+ confidence: row.confidence,
681
+ created_by: row.created_by ?? undefined,
682
+ created_at: row.created_at,
683
+ updated_at: row.updated_at,
684
+ last_accessed_at: row.last_accessed_at ?? undefined,
685
+ archived_at: row.archived_at ?? undefined,
686
+ archived_by: row.archived_by ?? undefined,
687
+ archive_reason: row.archive_reason ?? undefined,
688
+ expires_at: row.expires_at ?? undefined,
689
+ };
690
+ }
691
+ function touchRowsAndMap(db, rows, touchedAt) {
692
+ if (rows.length === 0) {
693
+ return [];
694
+ }
695
+ const ids = rows.map((row) => row.id);
696
+ const placeholders = ids.map(() => '?').join(', ');
697
+ db.prepare(`
698
+ UPDATE project_memories
699
+ SET last_accessed_at = ?
700
+ WHERE id IN (${placeholders})
701
+ `).run(touchedAt, ...ids);
702
+ return rows.map((row) => mapMemoryRow({ ...row, last_accessed_at: touchedAt }));
703
+ }
704
+ function parseJsonArray(value) {
705
+ try {
706
+ const parsed = JSON.parse(value);
707
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : [];
708
+ }
709
+ catch {
710
+ return [];
711
+ }
712
+ }
713
+ //# sourceMappingURL=memory-store.js.map