@askexenow/exe-os 0.8.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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/dist/bin/backfill-responses.js +1912 -0
  4. package/dist/bin/backfill-vectors.js +1642 -0
  5. package/dist/bin/cleanup-stale-review-tasks.js +1339 -0
  6. package/dist/bin/cli.js +18800 -0
  7. package/dist/bin/exe-agent.js +1858 -0
  8. package/dist/bin/exe-assign.js +1957 -0
  9. package/dist/bin/exe-boot.js +6460 -0
  10. package/dist/bin/exe-call.js +197 -0
  11. package/dist/bin/exe-cloud.js +850 -0
  12. package/dist/bin/exe-dispatch.js +1146 -0
  13. package/dist/bin/exe-doctor.js +1657 -0
  14. package/dist/bin/exe-export-behaviors.js +1494 -0
  15. package/dist/bin/exe-forget.js +1627 -0
  16. package/dist/bin/exe-gateway.js +7732 -0
  17. package/dist/bin/exe-healthcheck.js +207 -0
  18. package/dist/bin/exe-heartbeat.js +1647 -0
  19. package/dist/bin/exe-kill.js +1479 -0
  20. package/dist/bin/exe-launch-agent.js +1704 -0
  21. package/dist/bin/exe-link.js +192 -0
  22. package/dist/bin/exe-new-employee.js +852 -0
  23. package/dist/bin/exe-pending-messages.js +1446 -0
  24. package/dist/bin/exe-pending-notifications.js +1321 -0
  25. package/dist/bin/exe-pending-reviews.js +1468 -0
  26. package/dist/bin/exe-repo-drift.js +95 -0
  27. package/dist/bin/exe-review.js +1590 -0
  28. package/dist/bin/exe-search.js +2651 -0
  29. package/dist/bin/exe-session-cleanup.js +3173 -0
  30. package/dist/bin/exe-settings.js +354 -0
  31. package/dist/bin/exe-status.js +1532 -0
  32. package/dist/bin/exe-team.js +1324 -0
  33. package/dist/bin/git-sweep.js +2185 -0
  34. package/dist/bin/graph-backfill.js +1968 -0
  35. package/dist/bin/graph-export.js +1604 -0
  36. package/dist/bin/install.js +656 -0
  37. package/dist/bin/list-providers.js +140 -0
  38. package/dist/bin/scan-tasks.js +1820 -0
  39. package/dist/bin/setup.js +951 -0
  40. package/dist/bin/shard-migrate.js +1494 -0
  41. package/dist/bin/update.js +95 -0
  42. package/dist/bin/wiki-sync.js +1514 -0
  43. package/dist/gateway/index.js +8848 -0
  44. package/dist/hooks/bug-report-worker.js +2743 -0
  45. package/dist/hooks/commit-complete.js +2108 -0
  46. package/dist/hooks/error-recall.js +2861 -0
  47. package/dist/hooks/exe-heartbeat-hook.js +232 -0
  48. package/dist/hooks/ingest-worker.js +4793 -0
  49. package/dist/hooks/ingest.js +684 -0
  50. package/dist/hooks/instructions-loaded.js +1880 -0
  51. package/dist/hooks/notification.js +1726 -0
  52. package/dist/hooks/post-compact.js +1751 -0
  53. package/dist/hooks/pre-compact.js +1746 -0
  54. package/dist/hooks/pre-tool-use.js +2191 -0
  55. package/dist/hooks/prompt-ingest-worker.js +2126 -0
  56. package/dist/hooks/prompt-submit.js +4693 -0
  57. package/dist/hooks/response-ingest-worker.js +1936 -0
  58. package/dist/hooks/session-end.js +1752 -0
  59. package/dist/hooks/session-start.js +2795 -0
  60. package/dist/hooks/stop.js +1835 -0
  61. package/dist/hooks/subagent-stop.js +1726 -0
  62. package/dist/hooks/summary-worker.js +2661 -0
  63. package/dist/index.js +11834 -0
  64. package/dist/lib/cloud-sync.js +495 -0
  65. package/dist/lib/config.js +222 -0
  66. package/dist/lib/consolidation.js +476 -0
  67. package/dist/lib/crypto.js +51 -0
  68. package/dist/lib/database.js +730 -0
  69. package/dist/lib/device-registry.js +900 -0
  70. package/dist/lib/embedder.js +632 -0
  71. package/dist/lib/employee-templates.js +543 -0
  72. package/dist/lib/employees.js +177 -0
  73. package/dist/lib/error-detector.js +156 -0
  74. package/dist/lib/exe-daemon-client.js +451 -0
  75. package/dist/lib/exe-daemon.js +8285 -0
  76. package/dist/lib/file-grep.js +199 -0
  77. package/dist/lib/hybrid-search.js +1819 -0
  78. package/dist/lib/identity-templates.js +320 -0
  79. package/dist/lib/identity.js +223 -0
  80. package/dist/lib/keychain.js +145 -0
  81. package/dist/lib/license.js +377 -0
  82. package/dist/lib/messaging.js +1376 -0
  83. package/dist/lib/reminders.js +63 -0
  84. package/dist/lib/schedules.js +1396 -0
  85. package/dist/lib/session-registry.js +52 -0
  86. package/dist/lib/skill-learning.js +477 -0
  87. package/dist/lib/status-brief.js +235 -0
  88. package/dist/lib/store.js +1551 -0
  89. package/dist/lib/task-router.js +62 -0
  90. package/dist/lib/tasks.js +2456 -0
  91. package/dist/lib/tmux-routing.js +2836 -0
  92. package/dist/lib/tmux-status.js +261 -0
  93. package/dist/lib/tmux-transport.js +83 -0
  94. package/dist/lib/transport.js +128 -0
  95. package/dist/lib/ws-auth.js +19 -0
  96. package/dist/lib/ws-client.js +160 -0
  97. package/dist/mcp/server.js +10538 -0
  98. package/dist/mcp/tools/complete-reminder.js +67 -0
  99. package/dist/mcp/tools/create-reminder.js +52 -0
  100. package/dist/mcp/tools/create-task.js +1853 -0
  101. package/dist/mcp/tools/deactivate-behavior.js +263 -0
  102. package/dist/mcp/tools/list-reminders.js +62 -0
  103. package/dist/mcp/tools/list-tasks.js +463 -0
  104. package/dist/mcp/tools/send-message.js +1382 -0
  105. package/dist/mcp/tools/update-task.js +1692 -0
  106. package/dist/runtime/index.js +6809 -0
  107. package/dist/tui/App.js +17479 -0
  108. package/package.json +104 -0
  109. package/src/commands/exe/assign.md +17 -0
  110. package/src/commands/exe/build-adv.md +381 -0
  111. package/src/commands/exe/call.md +133 -0
  112. package/src/commands/exe/cloud.md +17 -0
  113. package/src/commands/exe/employee-heartbeat.md +44 -0
  114. package/src/commands/exe/forget.md +15 -0
  115. package/src/commands/exe/heartbeat.md +92 -0
  116. package/src/commands/exe/intercom.md +81 -0
  117. package/src/commands/exe/kill.md +34 -0
  118. package/src/commands/exe/launch.md +52 -0
  119. package/src/commands/exe/link.md +17 -0
  120. package/src/commands/exe/logs.md +22 -0
  121. package/src/commands/exe/new-employee.md +12 -0
  122. package/src/commands/exe/review.md +14 -0
  123. package/src/commands/exe/schedule.md +108 -0
  124. package/src/commands/exe/search.md +13 -0
  125. package/src/commands/exe/sessions.md +25 -0
  126. package/src/commands/exe/settings.md +13 -0
  127. package/src/commands/exe/setup.md +171 -0
  128. package/src/commands/exe/status.md +15 -0
  129. package/src/commands/exe/team.md +11 -0
  130. package/src/commands/exe/update.md +11 -0
  131. package/src/commands/exe.md +181 -0
@@ -0,0 +1,476 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __esm = (fn, res) => function __init() {
3
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
+ };
5
+
6
+ // src/lib/config.ts
7
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
8
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
9
+ import path2 from "path";
10
+ import os from "os";
11
+ function resolveDataDir() {
12
+ if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
13
+ if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
14
+ const newDir = path2.join(os.homedir(), ".exe-os");
15
+ const legacyDir = path2.join(os.homedir(), ".exe-mem");
16
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
17
+ try {
18
+ renameSync(legacyDir, newDir);
19
+ process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
20
+ `);
21
+ } catch {
22
+ return legacyDir;
23
+ }
24
+ }
25
+ return newDir;
26
+ }
27
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG;
28
+ var init_config = __esm({
29
+ "src/lib/config.ts"() {
30
+ "use strict";
31
+ EXE_AI_DIR = resolveDataDir();
32
+ DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
33
+ MODELS_DIR = path2.join(EXE_AI_DIR, "models");
34
+ CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
35
+ LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
36
+ CURRENT_CONFIG_VERSION = 1;
37
+ DEFAULT_CONFIG = {
38
+ config_version: CURRENT_CONFIG_VERSION,
39
+ dbPath: DB_PATH,
40
+ modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
41
+ embeddingDim: 1024,
42
+ batchSize: 20,
43
+ flushIntervalMs: 1e4,
44
+ autoIngestion: true,
45
+ autoRetrieval: true,
46
+ searchMode: "hybrid",
47
+ hookSearchMode: "hybrid",
48
+ fileGrepEnabled: true,
49
+ splashEffect: true,
50
+ consolidationEnabled: true,
51
+ consolidationIntervalMs: 6 * 60 * 60 * 1e3,
52
+ consolidationModel: "claude-haiku-4-5-20251001",
53
+ consolidationMaxCallsPerRun: 20,
54
+ selfQueryRouter: true,
55
+ selfQueryModel: "claude-haiku-4-5-20251001",
56
+ rerankerEnabled: true,
57
+ scalingRoadmap: {
58
+ rerankerAutoTrigger: {
59
+ enabled: true,
60
+ broadQueryMinCardinality: 5e4,
61
+ fetchTopK: 150,
62
+ returnTopK: 5
63
+ }
64
+ },
65
+ graphRagEnabled: true,
66
+ wikiEnabled: false,
67
+ wikiUrl: "",
68
+ wikiApiKey: "",
69
+ wikiSyncIntervalMs: 30 * 60 * 1e3,
70
+ wikiWorkspaceMapping: {
71
+ exe: "Executive",
72
+ yoshi: "Engineering",
73
+ mari: "Marketing",
74
+ tom: "Engineering",
75
+ sasha: "Production"
76
+ },
77
+ wikiAutoUpdate: true,
78
+ wikiAutoUpdateThreshold: 0.5,
79
+ wikiAutoUpdateCreateNew: true,
80
+ skillLearning: true,
81
+ skillThreshold: 3,
82
+ skillModel: "claude-haiku-4-5-20251001",
83
+ exeHeartbeat: {
84
+ enabled: true,
85
+ intervalSeconds: 60,
86
+ staleInProgressThresholdHours: 2
87
+ },
88
+ sessionLifecycle: {
89
+ idleKillEnabled: true,
90
+ idleKillTicksRequired: 3,
91
+ idleKillIntercomAckWindowMs: 1e4,
92
+ maxAutoInstances: 10
93
+ }
94
+ };
95
+ }
96
+ });
97
+
98
+ // src/lib/consolidation.ts
99
+ import { randomUUID } from "crypto";
100
+
101
+ // src/lib/database.ts
102
+ import { createClient } from "@libsql/client";
103
+
104
+ // src/lib/keychain.ts
105
+ import { readFile, writeFile, unlink, mkdir, chmod } from "fs/promises";
106
+ import { existsSync } from "fs";
107
+ import path from "path";
108
+ import crypto from "crypto";
109
+
110
+ // src/lib/store.ts
111
+ init_config();
112
+ function vectorToBlob(vector) {
113
+ const f32 = vector instanceof Float32Array ? vector : new Float32Array(vector);
114
+ return JSON.stringify(Array.from(f32));
115
+ }
116
+
117
+ // src/lib/consolidation.ts
118
+ async function selectUnconsolidated(client, limit = 200) {
119
+ const result = await client.execute({
120
+ sql: `SELECT id, agent_id, project_name, tool_name, raw_text, timestamp
121
+ FROM memories
122
+ WHERE consolidated = 0
123
+ AND timestamp >= datetime('now', '-7 days')
124
+ ORDER BY timestamp DESC
125
+ LIMIT ?`,
126
+ args: [limit]
127
+ });
128
+ return result.rows.map((row) => ({
129
+ id: row.id,
130
+ agent_id: row.agent_id,
131
+ project_name: row.project_name,
132
+ tool_name: row.tool_name,
133
+ raw_text: row.raw_text,
134
+ timestamp: row.timestamp
135
+ }));
136
+ }
137
+ function groupMemories(memories) {
138
+ const byProject = /* @__PURE__ */ new Map();
139
+ for (const mem of memories) {
140
+ const key = `${mem.agent_id}::${mem.project_name}`;
141
+ const list = byProject.get(key) ?? [];
142
+ list.push(mem);
143
+ byProject.set(key, list);
144
+ }
145
+ const clusters = [];
146
+ for (const [key, projectMemories] of byProject) {
147
+ const [agentId, projectName] = key.split("::");
148
+ const byDay = /* @__PURE__ */ new Map();
149
+ for (const mem of projectMemories) {
150
+ const day = mem.timestamp.slice(0, 10);
151
+ const list = byDay.get(day) ?? [];
152
+ list.push(mem);
153
+ byDay.set(day, list);
154
+ }
155
+ for (const [day, dayMemories] of byDay) {
156
+ if (dayMemories.length <= 15) {
157
+ clusters.push({
158
+ agentId,
159
+ projectName,
160
+ dateRange: day,
161
+ memories: dayMemories
162
+ });
163
+ } else {
164
+ for (let i = 0; i < dayMemories.length; i += 12) {
165
+ const chunk = dayMemories.slice(i, i + 12);
166
+ clusters.push({
167
+ agentId,
168
+ projectName,
169
+ dateRange: day,
170
+ memories: chunk
171
+ });
172
+ }
173
+ }
174
+ }
175
+ }
176
+ return clusters;
177
+ }
178
+ function buildConsolidationPrompt(cluster) {
179
+ const snippets = cluster.memories.map((m, i) => {
180
+ const text = m.raw_text.length > 200 ? m.raw_text.slice(0, 200) + "..." : m.raw_text;
181
+ return `${i + 1}. [${m.tool_name}] ${text}`;
182
+ }).join("\n");
183
+ return `You are reviewing a set of work memories from an AI coding agent.
184
+ These are raw tool call records from ${cluster.dateRange}.
185
+
186
+ Agent: ${cluster.agentId} | Project: ${cluster.projectName} | Date: ${cluster.dateRange}
187
+
188
+ MEMORIES:
189
+ ${snippets}
190
+
191
+ Extract EXACTLY THREE types of insights:
192
+
193
+ 1. KEY DECISIONS \u2014 choices that were made and why (at most 3)
194
+ 2. RECURRING PATTERNS \u2014 actions or approaches that repeated (at most 3)
195
+ 3. OPEN QUESTIONS \u2014 things that seem unresolved or risky (at most 2)
196
+
197
+ Format each as a single sentence. Be specific \u2014 include file names,
198
+ function names, and concrete details. Skip if no insight for a category.`;
199
+ }
200
+ async function consolidateCluster(cluster, model) {
201
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
202
+ const client = new Anthropic();
203
+ const prompt = buildConsolidationPrompt(cluster);
204
+ const response = await client.messages.create({
205
+ model,
206
+ max_tokens: 300,
207
+ messages: [{ role: "user", content: prompt }]
208
+ });
209
+ const textBlock = response.content.find((b) => b.type === "text");
210
+ return textBlock?.text ?? "";
211
+ }
212
+ async function storeConsolidation(client, cluster, synthesisText, embedFn) {
213
+ const consolidatedId = randomUUID();
214
+ const now = (/* @__PURE__ */ new Date()).toISOString();
215
+ const rawText = `CONSOLIDATION [${cluster.dateRange}, ${cluster.projectName}]:
216
+
217
+ ${synthesisText}`;
218
+ let vector = null;
219
+ if (embedFn) {
220
+ try {
221
+ vector = await embedFn(rawText);
222
+ } catch {
223
+ }
224
+ }
225
+ const insertSql = vector ? `INSERT INTO memories
226
+ (id, agent_id, agent_role, session_id, timestamp,
227
+ tool_name, project_name, has_error, raw_text, vector, version, consolidated)
228
+ VALUES (?, ?, 'consolidation', 'daemon-consolidation', ?, 'consolidation', ?, 0, ?, vector32(?), 0, 1)` : `INSERT INTO memories
229
+ (id, agent_id, agent_role, session_id, timestamp,
230
+ tool_name, project_name, has_error, raw_text, vector, version, consolidated)
231
+ VALUES (?, ?, 'consolidation', 'daemon-consolidation', ?, 'consolidation', ?, 0, ?, NULL, 0, 1)`;
232
+ const insertArgs = vector ? [consolidatedId, cluster.agentId, now, cluster.projectName, rawText, vectorToBlob(vector)] : [consolidatedId, cluster.agentId, now, cluster.projectName, rawText];
233
+ await client.execute({ sql: insertSql, args: insertArgs });
234
+ const sourceIds = cluster.memories.map((m) => m.id);
235
+ const linkStmts = sourceIds.map((sourceId) => ({
236
+ sql: `INSERT INTO consolidations (id, consolidated_memory_id, source_memory_id, created_at)
237
+ VALUES (?, ?, ?, ?)`,
238
+ args: [randomUUID(), consolidatedId, sourceId, now]
239
+ }));
240
+ const placeholders = sourceIds.map(() => "?").join(",");
241
+ const markStmt = {
242
+ sql: `UPDATE memories SET consolidated = 1 WHERE id IN (${placeholders})`,
243
+ args: sourceIds
244
+ };
245
+ await client.batch([...linkStmts, markStmt], "write");
246
+ return { consolidatedMemoryId: consolidatedId, sourceIds, rawText };
247
+ }
248
+ var WIKI_FETCH_TIMEOUT_MS = 1e4;
249
+ async function pushToWiki(consolidation, config) {
250
+ if (!config.wikiEnabled || !config.wikiAutoUpdate) {
251
+ return { updated: false };
252
+ }
253
+ const apiUrl = config.wikiUrl || process.env.EXE_WIKI_API_URL;
254
+ const apiKey = config.wikiApiKey || process.env.EXE_WIKI_API_KEY;
255
+ if (!apiUrl || !apiKey) {
256
+ return { updated: false };
257
+ }
258
+ const workspace = config.wikiWorkspaceMapping[consolidation.projectName] ?? consolidation.projectName;
259
+ try {
260
+ const listRes = await fetch(
261
+ `${apiUrl}/v1/workspace/${encodeURIComponent(workspace)}/documents`,
262
+ {
263
+ headers: { Authorization: `Bearer ${apiKey}` },
264
+ signal: AbortSignal.timeout(WIKI_FETCH_TIMEOUT_MS)
265
+ }
266
+ );
267
+ if (!listRes.ok) {
268
+ return { updated: false, error: `Workspace "${workspace}" not accessible (${listRes.status})` };
269
+ }
270
+ const listJson = await listRes.json();
271
+ const docs = listJson.documents ?? listJson.localFiles?.items ?? [];
272
+ const contentLines = consolidation.rawText.split("\n").filter((l) => l.trim() && !l.startsWith("CONSOLIDATION") && !l.match(/^[A-Z\s]+:$/)).join(" ");
273
+ const keywords = contentLines.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3);
274
+ let bestMatch = null;
275
+ for (const doc of docs) {
276
+ if (!doc.id || !doc.title) continue;
277
+ const titleWords = doc.title.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3);
278
+ if (titleWords.length === 0) continue;
279
+ const matchCount = titleWords.filter(
280
+ (tw) => keywords.some((k) => k.includes(tw) || tw.includes(k))
281
+ ).length;
282
+ const score = matchCount / titleWords.length;
283
+ if (score > (bestMatch?.score ?? 0)) {
284
+ bestMatch = { id: doc.id, title: doc.title, score };
285
+ }
286
+ }
287
+ if (bestMatch && bestMatch.score >= config.wikiAutoUpdateThreshold) {
288
+ const updateRes = await fetch(`${apiUrl}/v1/document/raw-text`, {
289
+ method: "POST",
290
+ headers: {
291
+ "Content-Type": "application/json",
292
+ Authorization: `Bearer ${apiKey}`
293
+ },
294
+ body: JSON.stringify({
295
+ textContent: consolidation.rawText,
296
+ metadata: { title: bestMatch.title, appendTo: bestMatch.id },
297
+ workspaceSlugs: [workspace]
298
+ }),
299
+ signal: AbortSignal.timeout(WIKI_FETCH_TIMEOUT_MS)
300
+ });
301
+ if (updateRes.ok) {
302
+ process.stderr.write(
303
+ `[consolidation] Wiki auto-updated: ${workspace}/${bestMatch.title}
304
+ `
305
+ );
306
+ return { updated: true, action: "updated", page: bestMatch.title };
307
+ }
308
+ return { updated: false, error: `Update failed (${updateRes.status})` };
309
+ }
310
+ if (config.wikiAutoUpdateCreateNew) {
311
+ const title = `Consolidated Insights \u2014 ${consolidation.projectName} (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)})`;
312
+ const createRes = await fetch(`${apiUrl}/v1/document/raw-text`, {
313
+ method: "POST",
314
+ headers: {
315
+ "Content-Type": "application/json",
316
+ Authorization: `Bearer ${apiKey}`
317
+ },
318
+ body: JSON.stringify({
319
+ textContent: consolidation.rawText,
320
+ metadata: { title },
321
+ workspaceSlugs: [workspace]
322
+ }),
323
+ signal: AbortSignal.timeout(WIKI_FETCH_TIMEOUT_MS)
324
+ });
325
+ if (createRes.ok) {
326
+ process.stderr.write(
327
+ `[consolidation] Wiki page created: ${workspace}/${title}
328
+ `
329
+ );
330
+ return { updated: true, action: "created", page: title };
331
+ }
332
+ return { updated: false, error: `Create failed (${createRes.status})` };
333
+ }
334
+ return { updated: false };
335
+ } catch (err) {
336
+ const msg = err instanceof Error ? err.message : String(err);
337
+ process.stderr.write(`[consolidation] Wiki push failed: ${msg}
338
+ `);
339
+ return { updated: false, error: msg };
340
+ }
341
+ }
342
+ async function runConsolidation(client, options) {
343
+ const memories = await selectUnconsolidated(client);
344
+ if (memories.length < 20) {
345
+ return { clustersProcessed: 0, memoriesConsolidated: 0 };
346
+ }
347
+ const clusters = groupMemories(memories);
348
+ let clustersProcessed = 0;
349
+ let memoriesConsolidated = 0;
350
+ for (const cluster of clusters) {
351
+ if (clustersProcessed >= options.maxCalls) break;
352
+ if (cluster.memories.length < 3) continue;
353
+ try {
354
+ const isExe = cluster.agentId === "exe";
355
+ if (isExe) {
356
+ const synthesis = await consolidateCluster(cluster, options.model);
357
+ if (!synthesis.trim()) continue;
358
+ const result = await storeConsolidation(client, cluster, synthesis, options.embedFn);
359
+ if (options.wikiConfig) {
360
+ await pushToWiki(
361
+ { ...result, projectName: cluster.projectName },
362
+ options.wikiConfig
363
+ ).catch((err) => {
364
+ process.stderr.write(
365
+ `[consolidation] Wiki push error (non-fatal): ${err instanceof Error ? err.message : String(err)}
366
+ `
367
+ );
368
+ });
369
+ }
370
+ const sourceIds = result.sourceIds;
371
+ if (sourceIds.length > 0) {
372
+ const placeholders = sourceIds.map(() => "?").join(",");
373
+ await client.execute({
374
+ sql: `UPDATE memories SET status = 'archived' WHERE id IN (${placeholders})`,
375
+ args: sourceIds
376
+ });
377
+ }
378
+ } else {
379
+ const dedupCount = await dedupCluster(client, cluster, options.embedFn);
380
+ memoriesConsolidated += dedupCount;
381
+ if (dedupCount === 0) continue;
382
+ }
383
+ clustersProcessed++;
384
+ memoriesConsolidated += isExe ? cluster.memories.length : 0;
385
+ } catch (err) {
386
+ process.stderr.write(
387
+ `[consolidation] Cluster failed (${cluster.projectName}/${cluster.dateRange}): ${err instanceof Error ? err.message : String(err)}
388
+ `
389
+ );
390
+ }
391
+ }
392
+ return { clustersProcessed, memoriesConsolidated };
393
+ }
394
+ async function dedupCluster(client, cluster, embedFn) {
395
+ if (!embedFn || cluster.memories.length < 2) return 0;
396
+ const vectors = [];
397
+ for (const mem of cluster.memories) {
398
+ try {
399
+ const v = await embedFn(mem.raw_text.slice(0, 500));
400
+ vectors.push({ id: mem.id, vector: v });
401
+ } catch {
402
+ }
403
+ }
404
+ if (vectors.length < 2) return 0;
405
+ const toArchive = /* @__PURE__ */ new Set();
406
+ for (let i = 0; i < vectors.length; i++) {
407
+ if (toArchive.has(vectors[i].id)) continue;
408
+ for (let j = i + 1; j < vectors.length; j++) {
409
+ if (toArchive.has(vectors[j].id)) continue;
410
+ const sim = cosineSimilarity(vectors[i].vector, vectors[j].vector);
411
+ if (sim > 0.95) {
412
+ toArchive.add(vectors[j].id);
413
+ }
414
+ }
415
+ }
416
+ if (toArchive.size === 0) return 0;
417
+ const ids = [...toArchive];
418
+ const placeholders = ids.map(() => "?").join(",");
419
+ await client.execute({
420
+ sql: `UPDATE memories SET status = 'archived', consolidated = 1 WHERE id IN (${placeholders})`,
421
+ args: ids
422
+ });
423
+ const survivors = vectors.filter((v) => !toArchive.has(v.id)).map((v) => v.id);
424
+ if (survivors.length > 0) {
425
+ const survivorPlaceholders = survivors.map(() => "?").join(",");
426
+ await client.execute({
427
+ sql: `UPDATE memories SET confidence = MIN(1.0, COALESCE(confidence, 0.7) + 0.1) WHERE id IN (${survivorPlaceholders})`,
428
+ args: survivors
429
+ });
430
+ }
431
+ return ids.length;
432
+ }
433
+ function cosineSimilarity(a, b) {
434
+ let dot = 0, normA = 0, normB = 0;
435
+ for (let i = 0; i < a.length; i++) {
436
+ dot += a[i] * b[i];
437
+ normA += a[i] * a[i];
438
+ normB += b[i] * b[i];
439
+ }
440
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
441
+ return denom === 0 ? 0 : dot / denom;
442
+ }
443
+ async function isUserIdle(client, idleMinutes = 30) {
444
+ const result = await client.execute({
445
+ sql: `SELECT MAX(timestamp) as last_activity
446
+ FROM memories
447
+ WHERE tool_name != 'consolidation'
448
+ AND timestamp >= datetime('now', '-1 day')`,
449
+ args: []
450
+ });
451
+ const lastActivity = result.rows[0]?.last_activity;
452
+ if (!lastActivity) return true;
453
+ const lastMs = new Date(lastActivity).getTime();
454
+ const now = Date.now();
455
+ return now - lastMs >= idleMinutes * 60 * 1e3;
456
+ }
457
+ async function countUnconsolidated(client) {
458
+ const result = await client.execute({
459
+ sql: `SELECT COUNT(*) as cnt FROM memories
460
+ WHERE consolidated = 0
461
+ AND timestamp >= datetime('now', '-7 days')`,
462
+ args: []
463
+ });
464
+ return Number(result.rows[0]?.cnt ?? 0);
465
+ }
466
+ export {
467
+ buildConsolidationPrompt,
468
+ consolidateCluster,
469
+ countUnconsolidated,
470
+ groupMemories,
471
+ isUserIdle,
472
+ pushToWiki,
473
+ runConsolidation,
474
+ selectUnconsolidated,
475
+ storeConsolidation
476
+ };
@@ -0,0 +1,51 @@
1
+ // src/lib/crypto.ts
2
+ import crypto from "crypto";
3
+ var ALGORITHM = "aes-256-gcm";
4
+ var IV_LENGTH = 12;
5
+ var TAG_LENGTH = 16;
6
+ var SYNC_HKDF_INFO = "exe-mem-sync-v2";
7
+ var _syncKey = null;
8
+ function initSyncCrypto(masterKey) {
9
+ if (masterKey.length !== 32) {
10
+ throw new Error(`Master key must be 32 bytes, got ${masterKey.length}`);
11
+ }
12
+ _syncKey = Buffer.from(
13
+ crypto.hkdfSync("sha256", masterKey, "", SYNC_HKDF_INFO, 32)
14
+ );
15
+ }
16
+ function isSyncCryptoInitialized() {
17
+ return _syncKey !== null;
18
+ }
19
+ function requireSyncKey() {
20
+ if (!_syncKey) {
21
+ throw new Error("Sync crypto not initialized. Call initSyncCrypto(masterKey) first.");
22
+ }
23
+ return _syncKey;
24
+ }
25
+ function encryptSyncBlob(data) {
26
+ const key = requireSyncKey();
27
+ const iv = crypto.randomBytes(IV_LENGTH);
28
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
29
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
30
+ const tag = cipher.getAuthTag();
31
+ return Buffer.concat([iv, encrypted, tag]).toString("base64");
32
+ }
33
+ function decryptSyncBlob(ciphertext) {
34
+ const key = requireSyncKey();
35
+ const combined = Buffer.from(ciphertext, "base64");
36
+ if (combined.length < IV_LENGTH + TAG_LENGTH) {
37
+ throw new Error("Sync blob too short to contain IV + tag");
38
+ }
39
+ const iv = combined.subarray(0, IV_LENGTH);
40
+ const tag = combined.subarray(combined.length - TAG_LENGTH);
41
+ const encrypted = combined.subarray(IV_LENGTH, combined.length - TAG_LENGTH);
42
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
43
+ decipher.setAuthTag(tag);
44
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]);
45
+ }
46
+ export {
47
+ decryptSyncBlob,
48
+ encryptSyncBlob,
49
+ initSyncCrypto,
50
+ isSyncCryptoInitialized
51
+ };