@usejarvis/brain 0.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 (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Vault — Awareness CRUD
3
+ *
4
+ * Database operations for screen_captures, awareness_sessions, and awareness_suggestions tables.
5
+ * Follows the same patterns as observations.ts and commitments.ts.
6
+ */
7
+
8
+ import { getDb, generateId } from './schema.ts';
9
+ import type {
10
+ ScreenCaptureRow,
11
+ SessionRow,
12
+ SuggestionRow,
13
+ SuggestionType,
14
+ AppUsageStat,
15
+ } from '../awareness/types.ts';
16
+
17
+ // ── Screen Captures ──
18
+
19
+ export function createCapture(data: {
20
+ timestamp: number;
21
+ sessionId?: string;
22
+ imagePath?: string;
23
+ thumbnailPath?: string;
24
+ pixelChangePct: number;
25
+ ocrText?: string;
26
+ appName?: string;
27
+ windowTitle?: string;
28
+ url?: string;
29
+ filePath?: string;
30
+ retentionTier?: 'full' | 'key_moment' | 'metadata_only';
31
+ }): ScreenCaptureRow {
32
+ const db = getDb();
33
+ const id = generateId();
34
+ const now = Date.now();
35
+
36
+ db.prepare(`
37
+ INSERT INTO screen_captures
38
+ (id, timestamp, session_id, image_path, thumbnail_path, pixel_change_pct,
39
+ ocr_text, app_name, window_title, url, file_path, retention_tier, created_at)
40
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
41
+ `).run(
42
+ id,
43
+ data.timestamp,
44
+ data.sessionId ?? null,
45
+ data.imagePath ?? null,
46
+ data.thumbnailPath ?? null,
47
+ data.pixelChangePct,
48
+ data.ocrText ?? null,
49
+ data.appName ?? null,
50
+ data.windowTitle ?? null,
51
+ data.url ?? null,
52
+ data.filePath ?? null,
53
+ data.retentionTier ?? 'full',
54
+ now,
55
+ );
56
+
57
+ return {
58
+ id,
59
+ timestamp: data.timestamp,
60
+ session_id: data.sessionId ?? null,
61
+ image_path: data.imagePath ?? null,
62
+ thumbnail_path: data.thumbnailPath ?? null,
63
+ pixel_change_pct: data.pixelChangePct,
64
+ ocr_text: data.ocrText ?? null,
65
+ app_name: data.appName ?? null,
66
+ window_title: data.windowTitle ?? null,
67
+ url: data.url ?? null,
68
+ file_path: data.filePath ?? null,
69
+ retention_tier: data.retentionTier ?? 'full',
70
+ created_at: now,
71
+ };
72
+ }
73
+
74
+ export function getCapture(id: string): ScreenCaptureRow | null {
75
+ const db = getDb();
76
+ return db.prepare('SELECT * FROM screen_captures WHERE id = ?').get(id) as ScreenCaptureRow | null;
77
+ }
78
+
79
+ export function getRecentCaptures(limit: number = 50, appName?: string): ScreenCaptureRow[] {
80
+ const db = getDb();
81
+ if (appName) {
82
+ return db.prepare(
83
+ 'SELECT * FROM screen_captures WHERE app_name = ? ORDER BY timestamp DESC LIMIT ?'
84
+ ).all(appName, limit) as ScreenCaptureRow[];
85
+ }
86
+ return db.prepare(
87
+ 'SELECT * FROM screen_captures ORDER BY timestamp DESC LIMIT ?'
88
+ ).all(limit) as ScreenCaptureRow[];
89
+ }
90
+
91
+ export function getCapturesInRange(startTime: number, endTime: number): ScreenCaptureRow[] {
92
+ const db = getDb();
93
+ return db.prepare(
94
+ 'SELECT * FROM screen_captures WHERE timestamp >= ? AND timestamp <= ? ORDER BY timestamp ASC'
95
+ ).all(startTime, endTime) as ScreenCaptureRow[];
96
+ }
97
+
98
+ export function getAppUsageStats(startTime: number, endTime: number): AppUsageStat[] {
99
+ const db = getDb();
100
+ const rows = db.prepare(`
101
+ SELECT app_name, COUNT(*) as capture_count
102
+ FROM screen_captures
103
+ WHERE timestamp >= ? AND timestamp <= ? AND app_name IS NOT NULL
104
+ GROUP BY app_name
105
+ ORDER BY capture_count DESC
106
+ `).all(startTime, endTime) as Array<{ app_name: string; capture_count: number }>;
107
+
108
+ const totalCaptures = rows.reduce((sum, r) => sum + r.capture_count, 0);
109
+
110
+ return rows.map(r => ({
111
+ app: r.app_name,
112
+ captureCount: r.capture_count,
113
+ minutes: Math.round((r.capture_count * 7) / 60), // ~7s per capture
114
+ percentage: totalCaptures > 0 ? Math.round((r.capture_count / totalCaptures) * 100) : 0,
115
+ }));
116
+ }
117
+
118
+ export function getCaptureCountSince(timestamp: number): number {
119
+ const db = getDb();
120
+ const row = db.prepare(
121
+ 'SELECT COUNT(*) as count FROM screen_captures WHERE timestamp >= ?'
122
+ ).get(timestamp) as { count: number };
123
+ return row.count;
124
+ }
125
+
126
+ export function updateCaptureRetention(id: string, tier: 'full' | 'key_moment' | 'metadata_only'): void {
127
+ const db = getDb();
128
+ db.prepare('UPDATE screen_captures SET retention_tier = ? WHERE id = ?').run(tier, id);
129
+ }
130
+
131
+ export function deleteCapturesBefore(timestamp: number, retentionTier: string): number {
132
+ const db = getDb();
133
+ const result = db.prepare(
134
+ 'DELETE FROM screen_captures WHERE timestamp < ? AND retention_tier = ?'
135
+ ).run(timestamp, retentionTier);
136
+ return result.changes;
137
+ }
138
+
139
+ export function updateCaptureOcrText(id: string, ocrText: string): void {
140
+ const db = getDb();
141
+ db.prepare('UPDATE screen_captures SET ocr_text = ? WHERE id = ?').run(ocrText, id);
142
+ }
143
+
144
+ export function getCapturesForSession(sessionId: string, limit: number = 50): ScreenCaptureRow[] {
145
+ const db = getDb();
146
+ return db.prepare(
147
+ 'SELECT * FROM screen_captures WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?'
148
+ ).all(sessionId, limit) as ScreenCaptureRow[];
149
+ }
150
+
151
+ // ── Awareness Sessions ──
152
+
153
+ export function createSession(data: {
154
+ startedAt: number;
155
+ apps?: string[];
156
+ projectContext?: string;
157
+ }): SessionRow {
158
+ const db = getDb();
159
+ const id = generateId();
160
+ const now = Date.now();
161
+
162
+ db.prepare(`
163
+ INSERT INTO awareness_sessions
164
+ (id, started_at, ended_at, topic, apps, project_context, action_types, entity_links, summary, capture_count, created_at)
165
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
166
+ `).run(
167
+ id,
168
+ data.startedAt,
169
+ null,
170
+ null,
171
+ JSON.stringify(data.apps ?? []),
172
+ data.projectContext ?? null,
173
+ JSON.stringify([]),
174
+ JSON.stringify([]),
175
+ null,
176
+ 0,
177
+ now,
178
+ );
179
+
180
+ return {
181
+ id,
182
+ started_at: data.startedAt,
183
+ ended_at: null,
184
+ topic: null,
185
+ apps: JSON.stringify(data.apps ?? []),
186
+ project_context: data.projectContext ?? null,
187
+ action_types: JSON.stringify([]),
188
+ entity_links: JSON.stringify([]),
189
+ summary: null,
190
+ capture_count: 0,
191
+ created_at: now,
192
+ };
193
+ }
194
+
195
+ export function getSession(id: string): SessionRow | null {
196
+ const db = getDb();
197
+ return db.prepare('SELECT * FROM awareness_sessions WHERE id = ?').get(id) as SessionRow | null;
198
+ }
199
+
200
+ export function updateSession(id: string, updates: Partial<{
201
+ ended_at: number | null;
202
+ topic: string | null;
203
+ apps: string[];
204
+ project_context: string | null;
205
+ action_types: string[];
206
+ entity_links: string[];
207
+ summary: string | null;
208
+ capture_count: number;
209
+ }>): void {
210
+ const db = getDb();
211
+ const setClauses: string[] = [];
212
+ const params: unknown[] = [];
213
+
214
+ if (updates.ended_at !== undefined) { setClauses.push('ended_at = ?'); params.push(updates.ended_at); }
215
+ if (updates.topic !== undefined) { setClauses.push('topic = ?'); params.push(updates.topic); }
216
+ if (updates.apps !== undefined) { setClauses.push('apps = ?'); params.push(JSON.stringify(updates.apps)); }
217
+ if (updates.project_context !== undefined) { setClauses.push('project_context = ?'); params.push(updates.project_context); }
218
+ if (updates.action_types !== undefined) { setClauses.push('action_types = ?'); params.push(JSON.stringify(updates.action_types)); }
219
+ if (updates.entity_links !== undefined) { setClauses.push('entity_links = ?'); params.push(JSON.stringify(updates.entity_links)); }
220
+ if (updates.summary !== undefined) { setClauses.push('summary = ?'); params.push(updates.summary); }
221
+ if (updates.capture_count !== undefined) { setClauses.push('capture_count = ?'); params.push(updates.capture_count); }
222
+
223
+ if (setClauses.length === 0) return;
224
+
225
+ params.push(id);
226
+ db.prepare(`UPDATE awareness_sessions SET ${setClauses.join(', ')} WHERE id = ?`).run(...params as any[]);
227
+ }
228
+
229
+ export function endSession(id: string, summary?: string): void {
230
+ const db = getDb();
231
+ db.prepare(
232
+ 'UPDATE awareness_sessions SET ended_at = ?, summary = ? WHERE id = ?'
233
+ ).run(Date.now(), summary ?? null, id);
234
+ }
235
+
236
+ export function getRecentSessions(limit: number = 20): SessionRow[] {
237
+ const db = getDb();
238
+ return db.prepare(
239
+ 'SELECT * FROM awareness_sessions ORDER BY started_at DESC LIMIT ?'
240
+ ).all(limit) as SessionRow[];
241
+ }
242
+
243
+ export function incrementSessionCaptureCount(id: string): void {
244
+ const db = getDb();
245
+ db.prepare(
246
+ 'UPDATE awareness_sessions SET capture_count = capture_count + 1 WHERE id = ?'
247
+ ).run(id);
248
+ }
249
+
250
+ // ── Awareness Suggestions ──
251
+
252
+ export function createSuggestion(data: {
253
+ type: SuggestionType;
254
+ triggerCaptureId?: string;
255
+ title: string;
256
+ body: string;
257
+ context?: Record<string, unknown>;
258
+ }): SuggestionRow {
259
+ const db = getDb();
260
+ const id = generateId();
261
+ const now = Date.now();
262
+
263
+ db.prepare(`
264
+ INSERT INTO awareness_suggestions
265
+ (id, type, trigger_capture_id, title, body, context, delivered, delivered_at, delivery_channel, dismissed, acted_on, created_at)
266
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
267
+ `).run(
268
+ id,
269
+ data.type,
270
+ data.triggerCaptureId ?? null,
271
+ data.title,
272
+ data.body,
273
+ data.context ? JSON.stringify(data.context) : null,
274
+ 0, null, null, 0, 0,
275
+ now,
276
+ );
277
+
278
+ return {
279
+ id,
280
+ type: data.type,
281
+ trigger_capture_id: data.triggerCaptureId ?? null,
282
+ title: data.title,
283
+ body: data.body,
284
+ context: data.context ? JSON.stringify(data.context) : null,
285
+ delivered: 0,
286
+ delivered_at: null,
287
+ delivery_channel: null,
288
+ dismissed: 0,
289
+ acted_on: 0,
290
+ created_at: now,
291
+ };
292
+ }
293
+
294
+ export function markSuggestionDelivered(id: string, channel: string): void {
295
+ const db = getDb();
296
+ db.prepare(
297
+ 'UPDATE awareness_suggestions SET delivered = 1, delivered_at = ?, delivery_channel = ? WHERE id = ?'
298
+ ).run(Date.now(), channel, id);
299
+ }
300
+
301
+ export function markSuggestionDismissed(id: string): void {
302
+ const db = getDb();
303
+ db.prepare('UPDATE awareness_suggestions SET dismissed = 1 WHERE id = ?').run(id);
304
+ }
305
+
306
+ export function markSuggestionActedOn(id: string): void {
307
+ const db = getDb();
308
+ db.prepare('UPDATE awareness_suggestions SET acted_on = 1 WHERE id = ?').run(id);
309
+ }
310
+
311
+ export function getRecentSuggestions(limit: number = 20, type?: SuggestionType): SuggestionRow[] {
312
+ const db = getDb();
313
+ if (type) {
314
+ return db.prepare(
315
+ 'SELECT * FROM awareness_suggestions WHERE type = ? ORDER BY created_at DESC LIMIT ?'
316
+ ).all(type, limit) as SuggestionRow[];
317
+ }
318
+ return db.prepare(
319
+ 'SELECT * FROM awareness_suggestions ORDER BY created_at DESC LIMIT ?'
320
+ ).all(limit) as SuggestionRow[];
321
+ }
322
+
323
+ export function getSuggestionCountSince(timestamp: number): number {
324
+ const db = getDb();
325
+ const row = db.prepare(
326
+ 'SELECT COUNT(*) as count FROM awareness_suggestions WHERE created_at >= ?'
327
+ ).get(timestamp) as { count: number };
328
+ return row.count;
329
+ }
330
+
331
+ export function getSuggestionStats(startTime: number, endTime: number): { total: number; actedOn: number } {
332
+ const db = getDb();
333
+ const row = db.prepare(`
334
+ SELECT
335
+ COUNT(*) as total,
336
+ SUM(CASE WHEN acted_on = 1 THEN 1 ELSE 0 END) as acted_on
337
+ FROM awareness_suggestions
338
+ WHERE created_at >= ? AND created_at <= ?
339
+ `).get(startTime, endTime) as { total: number; acted_on: number };
340
+ return { total: row.total, actedOn: row.acted_on ?? 0 };
341
+ }
@@ -0,0 +1,299 @@
1
+ import { getDb, generateId } from './schema.ts';
2
+
3
+ export type CommitmentPriority = 'low' | 'normal' | 'high' | 'critical';
4
+ export type CommitmentStatus = 'pending' | 'active' | 'completed' | 'failed' | 'escalated';
5
+
6
+ export type RetryPolicy = {
7
+ max_retries: number;
8
+ interval_ms: number;
9
+ escalate_after: number;
10
+ };
11
+
12
+ export type Commitment = {
13
+ id: string;
14
+ what: string;
15
+ when_due: number | null;
16
+ context: string | null;
17
+ priority: CommitmentPriority;
18
+ status: CommitmentStatus;
19
+ retry_policy: RetryPolicy | null;
20
+ created_from: string | null;
21
+ assigned_to: string | null;
22
+ created_at: number;
23
+ completed_at: number | null;
24
+ result: string | null;
25
+ sort_order: number;
26
+ };
27
+
28
+ type CommitmentRow = {
29
+ id: string;
30
+ what: string;
31
+ when_due: number | null;
32
+ context: string | null;
33
+ priority: CommitmentPriority;
34
+ status: CommitmentStatus;
35
+ retry_policy: string | null;
36
+ created_from: string | null;
37
+ assigned_to: string | null;
38
+ created_at: number;
39
+ completed_at: number | null;
40
+ result: string | null;
41
+ sort_order: number;
42
+ };
43
+
44
+ /**
45
+ * Parse commitment row from database, deserializing JSON fields
46
+ */
47
+ function parseCommitment(row: CommitmentRow): Commitment {
48
+ return {
49
+ ...row,
50
+ retry_policy: row.retry_policy ? JSON.parse(row.retry_policy) : null,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Create a new commitment
56
+ */
57
+ export function createCommitment(
58
+ what: string,
59
+ opts?: {
60
+ when_due?: number;
61
+ context?: string;
62
+ priority?: CommitmentPriority;
63
+ retry_policy?: RetryPolicy;
64
+ created_from?: string;
65
+ assigned_to?: string;
66
+ }
67
+ ): Commitment {
68
+ const db = getDb();
69
+ const id = generateId();
70
+ const now = Date.now();
71
+ const priority = opts?.priority ?? 'normal';
72
+
73
+ const stmt = db.prepare(
74
+ 'INSERT INTO commitments (id, what, when_due, context, priority, status, retry_policy, created_from, assigned_to, created_at, completed_at, result) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
75
+ );
76
+
77
+ stmt.run(
78
+ id,
79
+ what,
80
+ opts?.when_due ?? null,
81
+ opts?.context ?? null,
82
+ priority,
83
+ 'pending',
84
+ opts?.retry_policy ? JSON.stringify(opts.retry_policy) : null,
85
+ opts?.created_from ?? null,
86
+ opts?.assigned_to ?? null,
87
+ now,
88
+ null,
89
+ null
90
+ );
91
+
92
+ return {
93
+ id,
94
+ what,
95
+ when_due: opts?.when_due ?? null,
96
+ context: opts?.context ?? null,
97
+ priority,
98
+ status: 'pending',
99
+ retry_policy: opts?.retry_policy ?? null,
100
+ created_from: opts?.created_from ?? null,
101
+ assigned_to: opts?.assigned_to ?? null,
102
+ created_at: now,
103
+ completed_at: null,
104
+ result: null,
105
+ sort_order: 0,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Get a commitment by ID
111
+ */
112
+ export function getCommitment(id: string): Commitment | null {
113
+ const db = getDb();
114
+ const stmt = db.prepare('SELECT * FROM commitments WHERE id = ?');
115
+ const row = stmt.get(id) as CommitmentRow | null;
116
+
117
+ if (!row) return null;
118
+
119
+ return parseCommitment(row);
120
+ }
121
+
122
+ /**
123
+ * Find commitments matching query criteria
124
+ */
125
+ export function findCommitments(query: {
126
+ status?: CommitmentStatus;
127
+ priority?: CommitmentPriority;
128
+ assigned_to?: string;
129
+ overdue?: boolean;
130
+ }): Commitment[] {
131
+ const db = getDb();
132
+ const conditions: string[] = [];
133
+ const params: unknown[] = [];
134
+
135
+ if (query.status) {
136
+ conditions.push('status = ?');
137
+ params.push(query.status);
138
+ }
139
+
140
+ if (query.priority) {
141
+ conditions.push('priority = ?');
142
+ params.push(query.priority);
143
+ }
144
+
145
+ if (query.assigned_to) {
146
+ conditions.push('assigned_to = ?');
147
+ params.push(query.assigned_to);
148
+ }
149
+
150
+ if (query.overdue) {
151
+ conditions.push('when_due IS NOT NULL AND when_due <= ?');
152
+ params.push(Date.now());
153
+ conditions.push("status IN ('pending', 'active')");
154
+ }
155
+
156
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
157
+ const stmt = db.prepare(`SELECT * FROM commitments ${where} ORDER BY sort_order ASC, created_at DESC`);
158
+ const rows = stmt.all(...params as any[]) as CommitmentRow[];
159
+
160
+ return rows.map(parseCommitment);
161
+ }
162
+
163
+ /**
164
+ * Get upcoming commitments, ordered by due date
165
+ */
166
+ export function getUpcoming(limit: number = 10): Commitment[] {
167
+ const db = getDb();
168
+ const stmt = db.prepare(
169
+ "SELECT * FROM commitments WHERE status IN ('pending', 'active') AND when_due IS NOT NULL ORDER BY when_due ASC LIMIT ?"
170
+ );
171
+ const rows = stmt.all(limit) as CommitmentRow[];
172
+
173
+ return rows.map(parseCommitment);
174
+ }
175
+
176
+ /**
177
+ * Mark a commitment as completed
178
+ */
179
+ export function completeCommitment(id: string, result?: string): Commitment | null {
180
+ const db = getDb();
181
+ const commitment = getCommitment(id);
182
+ if (!commitment) return null;
183
+
184
+ const stmt = db.prepare(
185
+ 'UPDATE commitments SET status = ?, completed_at = ?, result = ? WHERE id = ?'
186
+ );
187
+ stmt.run('completed', Date.now(), result ?? null, id);
188
+
189
+ return getCommitment(id);
190
+ }
191
+
192
+ /**
193
+ * Mark a commitment as failed
194
+ */
195
+ export function failCommitment(id: string, reason?: string): Commitment | null {
196
+ const db = getDb();
197
+ const commitment = getCommitment(id);
198
+ if (!commitment) return null;
199
+
200
+ const stmt = db.prepare(
201
+ 'UPDATE commitments SET status = ?, completed_at = ?, result = ? WHERE id = ?'
202
+ );
203
+ stmt.run('failed', Date.now(), reason ?? null, id);
204
+
205
+ return getCommitment(id);
206
+ }
207
+
208
+ /**
209
+ * Escalate a commitment
210
+ */
211
+ export function escalateCommitment(id: string): Commitment | null {
212
+ const db = getDb();
213
+ const commitment = getCommitment(id);
214
+ if (!commitment) return null;
215
+
216
+ const stmt = db.prepare('UPDATE commitments SET status = ? WHERE id = ?');
217
+ stmt.run('escalated', id);
218
+
219
+ return getCommitment(id);
220
+ }
221
+
222
+ /**
223
+ * Get commitments that are currently due
224
+ */
225
+ export function getDueCommitments(): Commitment[] {
226
+ const db = getDb();
227
+ const now = Date.now();
228
+ const stmt = db.prepare(
229
+ "SELECT * FROM commitments WHERE when_due IS NOT NULL AND when_due <= ? AND status IN ('pending', 'active') ORDER BY when_due ASC"
230
+ );
231
+ const rows = stmt.all(now) as CommitmentRow[];
232
+
233
+ return rows.map(parseCommitment);
234
+ }
235
+
236
+ /**
237
+ * Update a commitment's status to any valid status.
238
+ * Sets completed_at for terminal states (completed, failed).
239
+ */
240
+ export function updateCommitmentStatus(
241
+ id: string,
242
+ status: CommitmentStatus,
243
+ result?: string
244
+ ): Commitment | null {
245
+ const db = getDb();
246
+ const commitment = getCommitment(id);
247
+ if (!commitment) return null;
248
+
249
+ const isTerminal = status === 'completed' || status === 'failed';
250
+ const completedAt = isTerminal ? Date.now() : null;
251
+
252
+ db.prepare(
253
+ 'UPDATE commitments SET status = ?, completed_at = ?, result = ? WHERE id = ?'
254
+ ).run(status, completedAt, result ?? null, id);
255
+
256
+ return getCommitment(id);
257
+ }
258
+
259
+ /**
260
+ * Update a commitment's assigned_to field.
261
+ */
262
+ export function updateCommitmentAssignee(id: string, assignedTo: string): Commitment | null {
263
+ const db = getDb();
264
+ const commitment = getCommitment(id);
265
+ if (!commitment) return null;
266
+
267
+ db.prepare('UPDATE commitments SET assigned_to = ? WHERE id = ?').run(assignedTo, id);
268
+ return getCommitment(id);
269
+ }
270
+
271
+ /**
272
+ * Update a commitment's due date.
273
+ */
274
+ export function updateCommitmentDue(id: string, when_due: number | null): Commitment | null {
275
+ const db = getDb();
276
+ const commitment = getCommitment(id);
277
+ if (!commitment) return null;
278
+
279
+ db.prepare('UPDATE commitments SET when_due = ? WHERE id = ?').run(when_due, id);
280
+ return getCommitment(id);
281
+ }
282
+
283
+ /**
284
+ * Bulk update sort order for commitments (used by kanban drag & drop).
285
+ */
286
+ export function reorderCommitments(
287
+ items: { id: string; sort_order: number }[]
288
+ ): void {
289
+ const db = getDb();
290
+ const stmt = db.prepare('UPDATE commitments SET sort_order = ? WHERE id = ?');
291
+
292
+ const transaction = db.transaction(() => {
293
+ for (const item of items) {
294
+ stmt.run(item.sort_order, item.id);
295
+ }
296
+ });
297
+
298
+ transaction();
299
+ }