@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,528 @@
1
+ /**
2
+ * Awareness Service — Orchestrator
3
+ *
4
+ * Wires together OCREngine, ContextTracker, Intelligence,
5
+ * SuggestionEngine, ContextGraph, and Analytics into a single service.
6
+ * Consumes pushed events from sidecar observers (screen_capture,
7
+ * context_changed, idle_detected) instead of polling.
8
+ */
9
+
10
+ import type { Service, ServiceStatus } from '../daemon/services.ts';
11
+ import type { JarvisConfig, AwarenessConfig } from '../config/types.ts';
12
+ import type { LLMManager } from '../llm/manager.ts';
13
+ import type { AwarenessEvent, LiveContext, DailyReport, Suggestion, SessionSummary, WeeklyReport, BehavioralInsight } from './types.ts';
14
+ import type { SuggestionType, SuggestionRow } from './types.ts';
15
+ import type { SidecarEvent, BinaryDataInline } from '../sidecar/protocol.ts';
16
+
17
+ import { OCREngine } from './ocr-engine.ts';
18
+ import { ContextTracker } from './context-tracker.ts';
19
+ import { AwarenessIntelligence } from './intelligence.ts';
20
+ import { SuggestionEngine } from './suggestion-engine.ts';
21
+ import { ContextGraph } from './context-graph.ts';
22
+ import { BehaviorAnalytics } from './analytics.ts';
23
+ import {
24
+ createCapture,
25
+ getCapturesForSession,
26
+ getSession,
27
+ updateSession,
28
+ updateCaptureRetention,
29
+ deleteCapturesBefore,
30
+ markSuggestionDelivered,
31
+ markSuggestionDismissed,
32
+ markSuggestionActedOn,
33
+ getRecentSuggestions,
34
+ } from '../vault/awareness.ts';
35
+ import { createObservation } from '../vault/observations.ts';
36
+ import { getUpcoming } from '../vault/commitments.ts';
37
+ import { generateId } from '../vault/schema.ts';
38
+ import { mkdirSync, existsSync, unlinkSync, readdirSync, statSync, rmdirSync } from 'node:fs';
39
+ import path from 'node:path';
40
+ import os from 'node:os';
41
+
42
+ let sharp: any = null;
43
+ try {
44
+ sharp = (await import('sharp')).default;
45
+ } catch { /* sharp not available — thumbnails disabled */ }
46
+
47
+ export class AwarenessService implements Service {
48
+ name = 'awareness';
49
+ private _status: ServiceStatus = 'stopped';
50
+
51
+ private config: AwarenessConfig;
52
+ private ocrEngine: OCREngine;
53
+ private contextTracker: ContextTracker;
54
+ private intelligence: AwarenessIntelligence;
55
+ private suggestionEngine: SuggestionEngine;
56
+ private contextGraph: ContextGraph;
57
+ private analytics: BehaviorAnalytics;
58
+ private llm: LLMManager;
59
+ private eventCallback: ((event: AwarenessEvent) => void) | null;
60
+ private enabled: boolean;
61
+ private captureDir: string;
62
+ private cleanupTimer: ReturnType<typeof setInterval> | null = null;
63
+
64
+ constructor(
65
+ jarvisConfig: JarvisConfig,
66
+ llm: LLMManager,
67
+ eventCallback?: (event: AwarenessEvent) => void,
68
+ googleAuth?: { isAuthenticated(): boolean; getAccessToken(): Promise<string> } | null
69
+ ) {
70
+ const cfg = jarvisConfig.awareness!;
71
+ this.config = cfg;
72
+ this.llm = llm;
73
+ this.eventCallback = eventCallback ?? null;
74
+ this.enabled = cfg.enabled;
75
+ this.captureDir = cfg.capture_dir.replace(/^~/, os.homedir());
76
+
77
+ this.ocrEngine = new OCREngine();
78
+ this.contextTracker = new ContextTracker(cfg);
79
+ this.intelligence = new AwarenessIntelligence(
80
+ llm,
81
+ cfg.cloud_vision_enabled ? cfg.cloud_vision_cooldown_ms : Infinity
82
+ );
83
+ this.suggestionEngine = new SuggestionEngine(cfg.suggestion_rate_limit_ms, {
84
+ googleAuth: googleAuth ?? null,
85
+ getUpcomingCommitments: () => getUpcoming(10).map(c => ({
86
+ what: c.what,
87
+ when_due: c.when_due,
88
+ priority: c.priority,
89
+ })),
90
+ });
91
+ this.contextGraph = new ContextGraph();
92
+ this.analytics = new BehaviorAnalytics(llm);
93
+ }
94
+
95
+ async start(): Promise<void> {
96
+ if (!this.enabled) {
97
+ console.log('[Awareness] Disabled by config');
98
+ this._status = 'stopped';
99
+ return;
100
+ }
101
+
102
+ this._status = 'starting';
103
+
104
+ try {
105
+ // 1. Initialize OCR engine
106
+ await this.ocrEngine.initialize();
107
+
108
+ // 2. Ensure capture directory exists
109
+ mkdirSync(this.captureDir, { recursive: true });
110
+
111
+ // 3. Start retention cleanup every 10 minutes
112
+ this.cleanupTimer = setInterval(() => this.cleanupRetention(), 10 * 60 * 1000);
113
+
114
+ this._status = 'running';
115
+ console.log('[Awareness] Service started — listening for sidecar events (OCR + context tracking active)');
116
+ } catch (err) {
117
+ this._status = 'error';
118
+ console.error('[Awareness] Failed to start:', err instanceof Error ? err.message : err);
119
+ throw err;
120
+ }
121
+ }
122
+
123
+ async stop(): Promise<void> {
124
+ this._status = 'stopping';
125
+
126
+ // End current session
127
+ this.contextTracker.endCurrentSession();
128
+
129
+ if (this.cleanupTimer) {
130
+ clearInterval(this.cleanupTimer);
131
+ this.cleanupTimer = null;
132
+ }
133
+
134
+ await this.ocrEngine.shutdown();
135
+
136
+ this._status = 'stopped';
137
+ console.log('[Awareness] Service stopped');
138
+ }
139
+
140
+ status(): ServiceStatus {
141
+ return this._status;
142
+ }
143
+
144
+ // ── Public API ──
145
+
146
+ getLiveContext(): LiveContext {
147
+ return this.analytics.getLiveContext(this.contextTracker, this._status === 'running');
148
+ }
149
+
150
+ getCurrentSession() {
151
+ return this.contextTracker.getCurrentSession();
152
+ }
153
+
154
+ getRecentSuggestionsList(limit?: number, type?: SuggestionType): SuggestionRow[] {
155
+ return getRecentSuggestions(limit, type);
156
+ }
157
+
158
+ dismissSuggestion(id: string): void {
159
+ markSuggestionDismissed(id);
160
+ }
161
+
162
+ actOnSuggestion(id: string): void {
163
+ markSuggestionActedOn(id);
164
+ }
165
+
166
+ async generateReport(date?: string): Promise<DailyReport> {
167
+ return this.analytics.generateDailyReport(date);
168
+ }
169
+
170
+ getSessionHistory(limit?: number): SessionSummary[] {
171
+ return this.analytics.getSessionHistory(limit);
172
+ }
173
+
174
+ async generateWeeklyReport(weekStart?: string): Promise<WeeklyReport> {
175
+ return this.analytics.generateWeeklyReport(weekStart);
176
+ }
177
+
178
+ getBehavioralInsights(days?: number): BehavioralInsight[] {
179
+ return this.analytics.getBehavioralInsights(days);
180
+ }
181
+
182
+ toggle(enabled: boolean): void {
183
+ this.enabled = enabled;
184
+ if (!enabled && this._status === 'running') {
185
+ this.stop().catch(err =>
186
+ console.error('[Awareness] Error stopping:', err)
187
+ );
188
+ } else if (enabled && this._status === 'stopped') {
189
+ this.start().catch(err =>
190
+ console.error('[Awareness] Error starting:', err)
191
+ );
192
+ }
193
+ }
194
+
195
+ isEnabled(): boolean {
196
+ return this.enabled;
197
+ }
198
+
199
+ // ── Sidecar Event Handler ──
200
+
201
+ async handleSidecarEvent(sidecarId: string, event: SidecarEvent): Promise<void> {
202
+ if (this._status !== 'running') return;
203
+
204
+ try {
205
+ switch (event.event_type) {
206
+ case 'screen_capture':
207
+ await this.handleScreenCapture(sidecarId, event);
208
+ break;
209
+ case 'context_changed':
210
+ this.handleContextChanged(sidecarId, event);
211
+ break;
212
+ case 'idle_detected':
213
+ this.handleIdleDetected(sidecarId, event);
214
+ break;
215
+ }
216
+ } catch (err) {
217
+ console.error(`[Awareness] Error handling ${event.event_type} from ${sidecarId}:`, err instanceof Error ? err.message : err);
218
+ }
219
+ }
220
+
221
+ // ── Event Handlers ──
222
+
223
+ private async handleScreenCapture(sidecarId: string, event: SidecarEvent): Promise<void> {
224
+ // Extract image buffer from binary data
225
+ let imageBuffer: Buffer | null = null;
226
+
227
+ if (event.binary) {
228
+ if (event.binary.type === 'inline' && 'data' in event.binary) {
229
+ imageBuffer = Buffer.from((event.binary as BinaryDataInline).data, 'base64');
230
+ } else if (event.binary.type === 'ref') {
231
+ // Binary ref: payload._binary was resolved by SidecarConnection
232
+ const resolved = (event.payload as Record<string, unknown>)._binary as Buffer | undefined;
233
+ if (resolved) {
234
+ imageBuffer = resolved;
235
+ }
236
+ }
237
+ }
238
+
239
+ if (!imageBuffer || imageBuffer.length < 1000) {
240
+ return;
241
+ }
242
+
243
+ const payload = event.payload as Record<string, unknown>;
244
+ const pixelChangePct = (payload.pixel_change_pct as number) ?? 0;
245
+ const captureId = String(payload.capture_id ?? generateId());
246
+ const windowTitle = String(payload.window_title ?? '');
247
+ const appName = String(payload.app_name ?? '');
248
+
249
+ // Update context tracker with window info from sidecar
250
+ if (appName || windowTitle) {
251
+ this.contextTracker.updateWindowInfo(appName, windowTitle);
252
+ }
253
+
254
+ // Save to disk
255
+ const imagePath = await this.saveCapture(imageBuffer, event.timestamp);
256
+ const thumbnailPath = await this.generateThumbnail(imagePath);
257
+
258
+ // Run through existing pipeline
259
+ await this.processCaptureEvent({
260
+ captureId,
261
+ pixelChangePct,
262
+ imagePath,
263
+ thumbnailPath: thumbnailPath ?? undefined,
264
+ imageBuffer,
265
+ windowTitle,
266
+ });
267
+ }
268
+
269
+ private handleContextChanged(_sidecarId: string, event: SidecarEvent): void {
270
+ const payload = event.payload as Record<string, unknown>;
271
+ const toApp = String(payload.to_app ?? '');
272
+ const toWindow = String(payload.to_window ?? '');
273
+
274
+ // Feed context change to tracker (simulates what processCapture does for window changes)
275
+ if (toApp || toWindow) {
276
+ this.contextTracker.updateWindowInfo(toApp, toWindow);
277
+ }
278
+ }
279
+
280
+ private handleIdleDetected(_sidecarId: string, event: SidecarEvent): void {
281
+ const payload = event.payload as Record<string, unknown>;
282
+ const durationMs = (payload.duration_ms as number) ?? 0;
283
+ const appName = String(payload.app_name ?? '');
284
+
285
+ // Feed idle info to context tracker for stuck detection
286
+ this.contextTracker.reportIdle(appName, durationMs);
287
+ }
288
+
289
+ // ── Capture Storage ──
290
+
291
+ private async saveCapture(imageBuffer: Buffer, timestamp: number): Promise<string> {
292
+ const date = new Date(timestamp);
293
+ const dateDir = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
294
+ const fileName = `${String(date.getHours()).padStart(2, '0')}-${String(date.getMinutes()).padStart(2, '0')}-${String(date.getSeconds()).padStart(2, '0')}.png`;
295
+
296
+ const dir = path.join(this.captureDir, dateDir);
297
+ mkdirSync(dir, { recursive: true });
298
+
299
+ const filePath = path.join(dir, fileName);
300
+ await Bun.write(filePath, imageBuffer);
301
+
302
+ return filePath;
303
+ }
304
+
305
+ private async generateThumbnail(fullImagePath: string): Promise<string | null> {
306
+ if (!sharp) return null;
307
+
308
+ const thumbPath = fullImagePath.replace(/\.png$/, '-thumb.jpg');
309
+ try {
310
+ await sharp(fullImagePath).resize(200).jpeg({ quality: 60 }).toFile(thumbPath);
311
+ return thumbPath;
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
316
+
317
+ private cleanupRetention(): void {
318
+ try {
319
+ const now = Date.now();
320
+ const fullCutoff = now - (this.config.retention.full_hours * 60 * 60 * 1000);
321
+ const keyMomentCutoff = now - (this.config.retention.key_moment_hours * 60 * 60 * 1000);
322
+
323
+ let fullDeleted = 0;
324
+ let keyDeleted = 0;
325
+ try {
326
+ fullDeleted = deleteCapturesBefore(fullCutoff, 'full');
327
+ keyDeleted = deleteCapturesBefore(keyMomentCutoff, 'key_moment');
328
+ } catch { /* DB may not be initialized in tests */ }
329
+
330
+ if (!existsSync(this.captureDir)) return;
331
+
332
+ const dateDirs = readdirSync(this.captureDir);
333
+ for (const dateDir of dateDirs) {
334
+ const dirPath = path.join(this.captureDir, dateDir);
335
+ try {
336
+ const stat = statSync(dirPath);
337
+ if (!stat.isDirectory()) continue;
338
+
339
+ const files = readdirSync(dirPath);
340
+ let remaining = files.length;
341
+
342
+ for (const file of files) {
343
+ const filePath = path.join(dirPath, file);
344
+ try {
345
+ const fileStat = statSync(filePath);
346
+ if (fileStat.mtimeMs < keyMomentCutoff) {
347
+ unlinkSync(filePath);
348
+ remaining--;
349
+ }
350
+ } catch { /* file already gone */ }
351
+ }
352
+
353
+ if (remaining === 0) {
354
+ try {
355
+ if (readdirSync(dirPath).length === 0) rmdirSync(dirPath);
356
+ } catch { /* ignore */ }
357
+ }
358
+ } catch { /* skip */ }
359
+ }
360
+
361
+ if (fullDeleted > 0 || keyDeleted > 0) {
362
+ console.log(`[Awareness] Retention cleanup: ${fullDeleted} full, ${keyDeleted} key_moment captures deleted`);
363
+ }
364
+ } catch (err) {
365
+ console.error('[Awareness] Retention cleanup error:', err instanceof Error ? err.message : err);
366
+ }
367
+ }
368
+
369
+ // ── Processing Pipeline ──
370
+
371
+ private async processCaptureEvent(data: {
372
+ captureId: string;
373
+ pixelChangePct: number;
374
+ imagePath: string;
375
+ thumbnailPath?: string;
376
+ imageBuffer: Buffer;
377
+ windowTitle?: string;
378
+ }): Promise<void> {
379
+ try {
380
+ // 1. OCR — extract text from screenshot
381
+ let ocrText = '';
382
+ if (this.ocrEngine.isReady()) {
383
+ const ocr = await this.ocrEngine.extractText(data.imageBuffer);
384
+ ocrText = ocr.text;
385
+ }
386
+
387
+ // 2. Context tracking — detect app changes, stuck states, errors
388
+ // Use window title from capture source (PowerShell/sidecar), fall back to tracker state
389
+ const windowTitle = data.windowTitle || this.contextTracker.getLastWindowTitle();
390
+
391
+ const { context, events } = this.contextTracker.processCapture(
392
+ data.captureId,
393
+ ocrText,
394
+ windowTitle
395
+ );
396
+
397
+ // 3. Entity linking
398
+ this.contextGraph.linkCaptureToEntities(context);
399
+
400
+ // 4. Store capture metadata in DB
401
+ createCapture({
402
+ timestamp: context.timestamp,
403
+ sessionId: context.sessionId,
404
+ imagePath: data.imagePath,
405
+ thumbnailPath: data.thumbnailPath ?? undefined,
406
+ pixelChangePct: data.pixelChangePct,
407
+ ocrText,
408
+ appName: context.appName,
409
+ windowTitle: context.windowTitle,
410
+ url: context.url ?? undefined,
411
+ filePath: context.filePath ?? undefined,
412
+ });
413
+
414
+ // 4b. Promote to key_moment retention if significant events fired
415
+ const keyMomentEventTypes = ['error_detected', 'stuck_detected', 'context_changed'];
416
+ if (events.some(e => keyMomentEventTypes.includes(e.type))) {
417
+ try { updateCaptureRetention(data.captureId, 'key_moment'); } catch { /* best-effort */ }
418
+ }
419
+
420
+ // 5. Store as observation
421
+ try {
422
+ createObservation('screen_capture', {
423
+ captureId: data.captureId,
424
+ appName: context.appName,
425
+ windowTitle: context.windowTitle,
426
+ ocrPreview: ocrText.slice(0, 200),
427
+ });
428
+ } catch { /* observation storage is best-effort */ }
429
+
430
+ // 6. Cloud vision escalation (async, non-blocking)
431
+ let cloudAnalysis: string | undefined;
432
+ if (this.config.cloud_vision_enabled && this.intelligence.shouldEscalateToCloud(context, events)) {
433
+ const base64 = data.imageBuffer.toString('base64');
434
+
435
+ const struggleEvent = events.find(e => e.type === 'struggle_detected');
436
+ if (struggleEvent) {
437
+ cloudAnalysis = await this.intelligence.analyzeStruggle(
438
+ base64,
439
+ context,
440
+ String(struggleEvent.data.appCategory ?? 'general'),
441
+ (struggleEvent.data.signals as Array<{ name: string; score: number; detail: string }>) ?? [],
442
+ String(struggleEvent.data.ocrPreview ?? context.ocrText.slice(0, 500))
443
+ );
444
+ } else if (context.isSignificantChange) {
445
+ cloudAnalysis = await this.intelligence.analyzeDelta(
446
+ base64,
447
+ context,
448
+ this.contextTracker.getPreviousContext()
449
+ );
450
+ } else {
451
+ cloudAnalysis = await this.intelligence.analyzeGeneral(base64, context);
452
+ }
453
+ }
454
+
455
+ // 7. Suggestion evaluation
456
+ const suggestion = await this.suggestionEngine.evaluate(context, events, cloudAnalysis);
457
+ if (suggestion) {
458
+ try { markSuggestionDelivered(suggestion.id, 'websocket'); } catch { /* ignore */ }
459
+
460
+ const suggestionEvent: AwarenessEvent = {
461
+ type: 'suggestion_ready',
462
+ data: {
463
+ id: suggestion.id,
464
+ type: suggestion.type,
465
+ title: suggestion.title,
466
+ body: suggestion.body,
467
+ },
468
+ timestamp: Date.now(),
469
+ };
470
+ events.push(suggestionEvent);
471
+ }
472
+
473
+ // 8. Emit all events
474
+ for (const event of events) {
475
+ this.eventCallback?.(event);
476
+ }
477
+
478
+ // 9. Session topic inference (async, non-blocking)
479
+ const sessionEnd = events.find(e => e.type === 'session_ended');
480
+ if (sessionEnd) {
481
+ this.inferSessionTopic(sessionEnd.data as { sessionId: string; apps: string[] }).catch(err =>
482
+ console.error('[Awareness] Session topic inference failed:', err instanceof Error ? err.message : err)
483
+ );
484
+ }
485
+ } catch (err) {
486
+ console.error('[Awareness] Pipeline error:', err instanceof Error ? err.message : err);
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Asynchronously infer topic and summary for a completed session via LLM.
492
+ */
493
+ private async inferSessionTopic(data: { sessionId: string; apps: string[] }): Promise<void> {
494
+ const { sessionId, apps } = data;
495
+ if (!sessionId) return;
496
+
497
+ try {
498
+ const session = getSession(sessionId);
499
+ if (!session) return;
500
+
501
+ const startedAt = session.started_at;
502
+ const endedAt = session.ended_at ?? Date.now();
503
+ const durationMinutes = Math.round((endedAt - startedAt) / 60000);
504
+
505
+ if (durationMinutes < 2) return;
506
+
507
+ const captures = getCapturesForSession(sessionId);
508
+ const sampleOcrTexts = captures
509
+ .filter(c => c.ocr_text && c.ocr_text.length > 20)
510
+ .slice(0, 5)
511
+ .map(c => c.ocr_text!);
512
+
513
+ if (sampleOcrTexts.length === 0) return;
514
+
515
+ const { topic, summary } = await this.intelligence.summarizeSession(
516
+ apps,
517
+ session.capture_count,
518
+ durationMinutes,
519
+ sampleOcrTexts
520
+ );
521
+
522
+ updateSession(sessionId, { topic, summary });
523
+ console.log(`[Awareness] Session topic: "${topic}" (${durationMinutes}min, ${apps.join(', ')})`);
524
+ } catch (err) {
525
+ console.error('[Awareness] Topic inference error:', err instanceof Error ? err.message : err);
526
+ }
527
+ }
528
+ }