@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,476 @@
1
+ /**
2
+ * Suggestion Engine — Proactive Help
3
+ *
4
+ * Evaluates screen context and events to generate actionable suggestions.
5
+ * Rate-limited and deduped to avoid being annoying.
6
+ * Supports: error, stuck, automation, knowledge, schedule, break, general.
7
+ */
8
+
9
+ import type { ScreenContext, AwarenessEvent, Suggestion, SuggestionType } from './types.ts';
10
+ import { createSuggestion, getSuggestionCountSince, getCaptureCountSince } from '../vault/awareness.ts';
11
+ import { searchEntitiesByName } from '../vault/entities.ts';
12
+ import { findFacts } from '../vault/facts.ts';
13
+
14
+ const MAX_DEDUP_HASHES = 50;
15
+
16
+ export type ScheduleDeps = {
17
+ googleAuth?: { isAuthenticated(): boolean; getAccessToken(): Promise<string> } | null;
18
+ getUpcomingCommitments?: () => Array<{ what: string; when_due: number | null; priority: string }>;
19
+ };
20
+
21
+ // Per-type rate limits — errors fire fast, proactive suggestions less often
22
+ const TYPE_RATE_LIMITS: Record<string, number> = {
23
+ error: 15_000, // 15s — errors are urgent
24
+ struggle: 90_000, // 90s — behavioral, stays true a while
25
+ stuck: 60_000, // 60s — persistent state
26
+ automation: 120_000, // 2min — proactive, shouldn't nag
27
+ knowledge: 120_000, // 2min
28
+ schedule: 300_000, // 5min — calendar events don't change fast
29
+ break: 600_000, // 10min — break reminders shouldn't nag
30
+ general: 60_000, // 1min — cloud insight
31
+ };
32
+
33
+ export class SuggestionEngine {
34
+ private defaultRateLimitMs: number;
35
+ private lastSuggestionByType = new Map<string, number>();
36
+ private recentHashes: Set<string> = new Set();
37
+ private hashQueue: string[] = [];
38
+
39
+ // Gap 3: automation detection state
40
+ private actionHistory: Array<{ appName: string; windowTitle: string; timestamp: number }> = [];
41
+
42
+ // Gap 4: knowledge dedup
43
+ private lastKnowledgeEntityId = '';
44
+
45
+ // Gap 5: schedule check throttle
46
+ private scheduleDeps: ScheduleDeps | null;
47
+ private lastScheduleCheckAt = 0;
48
+ private lastScheduleEventId = '';
49
+
50
+ constructor(rateLimitMs: number = 60000, scheduleDeps?: ScheduleDeps) {
51
+ this.defaultRateLimitMs = rateLimitMs;
52
+ this.scheduleDeps = scheduleDeps ?? null;
53
+ }
54
+
55
+ /**
56
+ * Evaluate context and events for a potential suggestion.
57
+ * Returns null if no suggestion or rate-limited.
58
+ */
59
+ async evaluate(
60
+ context: ScreenContext,
61
+ events: AwarenessEvent[],
62
+ cloudAnalysis?: string
63
+ ): Promise<Suggestion | null> {
64
+ const now = Date.now();
65
+
66
+ // Try each suggestion type in priority order
67
+ // Each candidate is checked against its own per-type rate limit
68
+ const candidates: (Suggestion | null)[] = [
69
+ this.checkError(context, events),
70
+ this.checkStruggle(context, events, cloudAnalysis),
71
+ this.checkStuck(context, events),
72
+ this.checkAutomation(context, events),
73
+ this.checkKnowledge(context, events),
74
+ null, // placeholder for async schedule
75
+ this.checkBreak(context),
76
+ this.checkCloudInsight(context, cloudAnalysis),
77
+ ];
78
+
79
+ // Async schedule check (only if nothing higher-priority passed)
80
+ if (candidates.every(c => c === null)) {
81
+ (candidates as (Suggestion | null)[])[5] = await this.checkSchedule(context);
82
+ }
83
+
84
+ // Find the first non-null candidate that passes its per-type rate limit
85
+ let suggestion: Suggestion | null = null;
86
+ for (const candidate of candidates) {
87
+ if (!candidate) continue;
88
+ const typeLimit = TYPE_RATE_LIMITS[candidate.type] ?? this.defaultRateLimitMs;
89
+ const lastFired = this.lastSuggestionByType.get(candidate.type) ?? 0;
90
+ if (now - lastFired < typeLimit) continue; // rate-limited for this type
91
+ suggestion = candidate;
92
+ break;
93
+ }
94
+
95
+ if (!suggestion) return null;
96
+
97
+ // Dedup check
98
+ const hash = this.hashSuggestion(suggestion);
99
+ if (this.recentHashes.has(hash)) return null;
100
+
101
+ // Store and track
102
+ this.addHash(hash);
103
+ this.lastSuggestionByType.set(suggestion.type, now);
104
+
105
+ // Persist to DB
106
+ const row = createSuggestion({
107
+ type: suggestion.type,
108
+ triggerCaptureId: context.captureId,
109
+ title: suggestion.title,
110
+ body: suggestion.body,
111
+ context: suggestion.context ?? undefined,
112
+ });
113
+
114
+ return {
115
+ ...suggestion,
116
+ id: row.id,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Check for error-related suggestions.
122
+ */
123
+ private checkError(context: ScreenContext, events: AwarenessEvent[]): Suggestion | null {
124
+ const errorEvent = events.find(e => e.type === 'error_detected');
125
+ if (!errorEvent) return null;
126
+
127
+ const errorText = String(errorEvent.data.errorText ?? '');
128
+ const errorContext = String(errorEvent.data.errorContext ?? '');
129
+
130
+ return {
131
+ id: '', // set by DB
132
+ type: 'error',
133
+ title: `Error detected in ${context.appName}`,
134
+ body: `I spotted "${errorText.slice(0, 80)}". Researching a fix now...`,
135
+ triggerCaptureId: context.captureId,
136
+ context: { errorText, errorContext, appName: context.appName },
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Check for struggle-state suggestions with deep analysis.
142
+ */
143
+ private checkStruggle(
144
+ context: ScreenContext,
145
+ events: AwarenessEvent[],
146
+ cloudAnalysis?: string
147
+ ): Suggestion | null {
148
+ const struggleEvent = events.find(e => e.type === 'struggle_detected');
149
+ if (!struggleEvent) return null;
150
+
151
+ const appCategory = String(struggleEvent.data.appCategory ?? 'general');
152
+ const compositeScore = struggleEvent.data.compositeScore as number;
153
+ const signals = struggleEvent.data.signals as Array<{ name: string; score: number; detail: string }>;
154
+ const durationMs = struggleEvent.data.durationMs as number;
155
+ const minutes = Math.round(durationMs / 60000);
156
+
157
+ // Use cloud analysis as body if available, otherwise contextual fallback
158
+ const body = cloudAnalysis && cloudAnalysis.length > 20
159
+ ? cloudAnalysis.slice(0, 500)
160
+ : this.buildStruggleFallback(appCategory, context.appName, minutes, signals);
161
+
162
+ const titleMap: Record<string, string> = {
163
+ code_editor: 'I think I see the issue in your code',
164
+ terminal: 'I noticed the command keeps failing',
165
+ browser: 'Need help with this page?',
166
+ creative_app: `Let me help you find that in ${context.appName}`,
167
+ puzzle_game: 'I might have a hint for this puzzle',
168
+ general: 'You seem stuck — let me take a look',
169
+ };
170
+
171
+ return {
172
+ id: '',
173
+ type: 'struggle',
174
+ title: titleMap[appCategory] ?? titleMap.general!,
175
+ body,
176
+ triggerCaptureId: context.captureId,
177
+ context: {
178
+ appName: context.appName,
179
+ windowTitle: context.windowTitle,
180
+ appCategory,
181
+ compositeScore,
182
+ durationMs,
183
+ signals: signals.map(s => s.name),
184
+ source: cloudAnalysis ? 'cloud_vision' : 'behavioral',
185
+ },
186
+ };
187
+ }
188
+
189
+ private buildStruggleFallback(
190
+ appCategory: string,
191
+ appName: string,
192
+ minutes: number,
193
+ signals: Array<{ name: string; score: number; detail: string }>
194
+ ): string {
195
+ const topSignal = signals.sort((a, b) => b.score - a.score)[0]!;
196
+
197
+ const messages: Record<string, string> = {
198
+ code_editor: `I've been watching you edit this code for ${minutes}+ minutes and it looks like you might be stuck. Let me analyze your code for issues...`,
199
+ terminal: `You've been running into the same issue in the terminal for a while. Let me look into what's going wrong...`,
200
+ browser: `You've been on this page for a while without finding what you need. Want me to help navigate?`,
201
+ creative_app: `You seem to be looking for something in ${appName}. Let me help you find it...`,
202
+ puzzle_game: `Stuck on this puzzle? Let me analyze the board and suggest a move...`,
203
+ general: `You've been working on this for ${minutes}+ minutes with a lot of back-and-forth. Want me to help figure out what's blocking you?`,
204
+ };
205
+
206
+ return messages[appCategory] ?? messages.general!;
207
+ }
208
+
209
+ /**
210
+ * Check for stuck-state suggestions.
211
+ */
212
+ private checkStuck(context: ScreenContext, events: AwarenessEvent[]): Suggestion | null {
213
+ const stuckEvent = events.find(e => e.type === 'stuck_detected');
214
+ if (!stuckEvent) return null;
215
+
216
+ const durationMs = stuckEvent.data.durationMs as number;
217
+ const minutes = Math.round(durationMs / 60000);
218
+
219
+ return {
220
+ id: '',
221
+ type: 'stuck',
222
+ title: `Stuck on ${context.appName}?`,
223
+ body: `You've been on the same screen for ${minutes}+ minutes. Want me to help with what you're working on?`,
224
+ triggerCaptureId: context.captureId,
225
+ context: { appName: context.appName, windowTitle: context.windowTitle, durationMs },
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Detect repetitive app-switching patterns (automation opportunities).
231
+ * Tracks action history and looks for A→B→A→B patterns (3+ repeats in 5 min).
232
+ */
233
+ private checkAutomation(context: ScreenContext, events: AwarenessEvent[]): Suggestion | null {
234
+ // Track action history
235
+ this.actionHistory.push({
236
+ appName: context.appName,
237
+ windowTitle: context.windowTitle,
238
+ timestamp: context.timestamp,
239
+ });
240
+
241
+ // Trim to last 5 minutes
242
+ const fiveMinAgo = Date.now() - 5 * 60 * 1000;
243
+ this.actionHistory = this.actionHistory.filter(a => a.timestamp > fiveMinAgo);
244
+
245
+ // Need a context change and sufficient history
246
+ if (!events.some(e => e.type === 'context_changed')) return null;
247
+ if (this.actionHistory.length < 6) return null;
248
+
249
+ // Count app-pair transitions
250
+ const transitions = new Map<string, number>();
251
+ for (let i = 1; i < this.actionHistory.length; i++) {
252
+ const from = this.actionHistory[i - 1]!.appName;
253
+ const to = this.actionHistory[i]!.appName;
254
+ if (from !== to) {
255
+ const key = `${from}→${to}`;
256
+ transitions.set(key, (transitions.get(key) ?? 0) + 1);
257
+ }
258
+ }
259
+
260
+ // Find most repeated transition
261
+ let maxTransition = '';
262
+ let maxCount = 0;
263
+ for (const [key, count] of transitions) {
264
+ if (count > maxCount) {
265
+ maxTransition = key;
266
+ maxCount = count;
267
+ }
268
+ }
269
+
270
+ if (maxCount >= 3) {
271
+ const [fromApp, toApp] = maxTransition.split('→');
272
+ return {
273
+ id: '',
274
+ type: 'automation',
275
+ title: `Repetitive pattern: ${fromApp} ↔ ${toApp}`,
276
+ body: `You've switched between ${fromApp} and ${toApp} ${maxCount} times recently. Want me to create a workflow to automate this?`,
277
+ triggerCaptureId: context.captureId,
278
+ context: { fromApp, toApp, count: maxCount, pattern: 'app_switch' },
279
+ };
280
+ }
281
+
282
+ return null;
283
+ }
284
+
285
+ /**
286
+ * Surface relevant vault knowledge when context changes to a recognized entity/project.
287
+ */
288
+ private checkKnowledge(context: ScreenContext, events: AwarenessEvent[]): Suggestion | null {
289
+ if (!events.some(e => e.type === 'context_changed')) return null;
290
+
291
+ try {
292
+ // Extract meaningful words from window title
293
+ const words = (context.windowTitle || '')
294
+ .split(/\s+[-–—|/\\]\s+|\s+/)
295
+ .filter(w => w.length >= 3)
296
+ .slice(0, 5);
297
+
298
+ for (const word of words) {
299
+ const entities = searchEntitiesByName(word);
300
+ const relevant = entities.filter(e =>
301
+ e.type === 'project' || e.type === 'concept' || e.type === 'person'
302
+ );
303
+
304
+ for (const entity of relevant.slice(0, 3)) {
305
+ if (entity.id === this.lastKnowledgeEntityId) continue;
306
+
307
+ const facts = findFacts({ subject_id: entity.id });
308
+ if (facts.length === 0) continue;
309
+
310
+ this.lastKnowledgeEntityId = entity.id;
311
+
312
+ const factSummary = facts
313
+ .slice(0, 3)
314
+ .map(f => `${f.predicate}: ${f.object}`)
315
+ .join('; ');
316
+
317
+ return {
318
+ id: '',
319
+ type: 'knowledge',
320
+ title: `Relevant info: ${entity.name}`,
321
+ body: `I noticed you're working with ${entity.name}. Here's what I know: ${factSummary}`,
322
+ triggerCaptureId: context.captureId,
323
+ context: { entityId: entity.id, entityName: entity.name, entityType: entity.type, factCount: facts.length },
324
+ };
325
+ }
326
+ }
327
+ } catch { /* vault query failure — non-fatal */ }
328
+
329
+ return null;
330
+ }
331
+
332
+ /**
333
+ * Check upcoming calendar events and vault commitments.
334
+ * Self-throttled to check every 5 minutes.
335
+ */
336
+ private async checkSchedule(context: ScreenContext): Promise<Suggestion | null> {
337
+ const now = Date.now();
338
+ if (now - this.lastScheduleCheckAt < 5 * 60 * 1000) return null;
339
+ this.lastScheduleCheckAt = now;
340
+
341
+ if (!this.scheduleDeps) return null;
342
+
343
+ try {
344
+ // 1. Check vault commitments
345
+ if (this.scheduleDeps.getUpcomingCommitments) {
346
+ const commitments = this.scheduleDeps.getUpcomingCommitments();
347
+ for (const c of commitments) {
348
+ if (!c.when_due) continue;
349
+ const minutesUntilDue = (c.when_due - now) / 60_000;
350
+ if (minutesUntilDue > 0 && minutesUntilDue <= 15) {
351
+ return {
352
+ id: '',
353
+ type: 'schedule',
354
+ title: 'Commitment due soon',
355
+ body: `"${c.what}" is due in ${Math.round(minutesUntilDue)} minutes.`,
356
+ triggerCaptureId: context.captureId,
357
+ context: { commitment: c.what, minutesUntilDue: Math.round(minutesUntilDue), priority: c.priority },
358
+ };
359
+ }
360
+ }
361
+ }
362
+
363
+ // 2. Check Google Calendar
364
+ if (this.scheduleDeps.googleAuth?.isAuthenticated()) {
365
+ const { listUpcomingEvents } = await import('../integrations/google-api.ts');
366
+ const token = await this.scheduleDeps.googleAuth.getAccessToken();
367
+ const nowIso = new Date().toISOString();
368
+ const in20Min = new Date(now + 20 * 60_000).toISOString();
369
+
370
+ const events = await listUpcomingEvents(token, 'primary', nowIso, in20Min, 5);
371
+
372
+ for (const event of events) {
373
+ if (event.id === this.lastScheduleEventId) continue;
374
+
375
+ const startTime = new Date(event.start).getTime();
376
+ const minutesUntil = (startTime - now) / 60_000;
377
+
378
+ if (minutesUntil > 0 && minutesUntil <= 15) {
379
+ this.lastScheduleEventId = event.id;
380
+
381
+ const attendeeInfo = event.attendees.length > 0
382
+ ? ` with ${event.attendees.slice(0, 3).join(', ')}`
383
+ : '';
384
+
385
+ return {
386
+ id: '',
387
+ type: 'schedule',
388
+ title: `Upcoming: ${event.summary}`,
389
+ body: `"${event.summary}"${attendeeInfo} starts in ${Math.round(minutesUntil)} minutes.${event.location ? ` Location: ${event.location}` : ''}`,
390
+ triggerCaptureId: context.captureId,
391
+ context: { calendarEventId: event.id, summary: event.summary, minutesUntil: Math.round(minutesUntil) },
392
+ };
393
+ }
394
+ }
395
+ }
396
+ } catch (err) {
397
+ // Calendar may not be configured — this is normal
398
+ if (err instanceof Error && !err.message.includes('403')) {
399
+ console.error('[SuggestionEngine] Schedule check error:', err.message);
400
+ }
401
+ }
402
+
403
+ return null;
404
+ }
405
+
406
+ /**
407
+ * Check if user needs a break (>90 min continuous activity).
408
+ */
409
+ private checkBreak(context: ScreenContext): Suggestion | null {
410
+ try {
411
+ const ninetyMinAgo = Date.now() - 90 * 60 * 1000;
412
+
413
+ // Don't suggest a break if we already suggested one recently
414
+ const recentBreakSuggestions = getSuggestionCountSince(ninetyMinAgo);
415
+ if (recentBreakSuggestions > 2) return null;
416
+
417
+ // Check if user has been continuously active for 90+ minutes
418
+ // At ~7s per capture, 90 min = ~770 captures
419
+ const captureCount = getCaptureCountSince(ninetyMinAgo);
420
+ if (captureCount < 700) return null;
421
+
422
+ return {
423
+ id: '',
424
+ type: 'break',
425
+ title: 'Time for a break?',
426
+ body: `You've been working for over 90 minutes straight. A short break can boost focus and creativity.`,
427
+ triggerCaptureId: context.captureId,
428
+ context: { captureCount, minutesActive: Math.round((captureCount * 7) / 60) },
429
+ };
430
+ } catch { /* ignore */ }
431
+
432
+ return null;
433
+ }
434
+
435
+ /**
436
+ * Generate suggestion from cloud analysis insight.
437
+ */
438
+ private checkCloudInsight(context: ScreenContext, cloudAnalysis?: string): Suggestion | null {
439
+ if (!cloudAnalysis || cloudAnalysis.length < 20) return null;
440
+
441
+ // Only suggest if the analysis contains actionable content
442
+ const actionablePatterns = /\b(suggest|try|consider|could|should|might want|tip|hint|recommendation)\b/i;
443
+ if (!actionablePatterns.test(cloudAnalysis)) return null;
444
+
445
+ return {
446
+ id: '',
447
+ type: 'general',
448
+ title: `Insight for ${context.appName}`,
449
+ body: cloudAnalysis.slice(0, 300),
450
+ triggerCaptureId: context.captureId,
451
+ context: { appName: context.appName, source: 'cloud_vision' },
452
+ };
453
+ }
454
+
455
+ /**
456
+ * Simple hash for dedup.
457
+ */
458
+ private hashSuggestion(suggestion: Omit<Suggestion, 'id'>): string {
459
+ // Include body snippet in hash so different errors with same title aren't deduplicated
460
+ const bodySnippet = (suggestion.body || '').slice(0, 80);
461
+ return `${suggestion.type}:${suggestion.title}:${bodySnippet}`;
462
+ }
463
+
464
+ /**
465
+ * Track hash with FIFO eviction.
466
+ */
467
+ private addHash(hash: string): void {
468
+ this.recentHashes.add(hash);
469
+ this.hashQueue.push(hash);
470
+
471
+ if (this.hashQueue.length > MAX_DEDUP_HASHES) {
472
+ const oldest = this.hashQueue.shift()!;
473
+ this.recentHashes.delete(oldest);
474
+ }
475
+ }
476
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Awareness Engine — Shared Types
3
+ *
4
+ * Type definitions for the continuous screen awareness system (M13).
5
+ */
6
+
7
+ // ── Capture Layer ──
8
+
9
+ export type CaptureFrame = {
10
+ id: string;
11
+ timestamp: number;
12
+ imageBuffer: Buffer;
13
+ pixelChangePct: number;
14
+ };
15
+
16
+ export type OCRResult = {
17
+ text: string;
18
+ confidence: number;
19
+ durationMs: number;
20
+ };
21
+
22
+ // ── Context ──
23
+
24
+ export type ScreenContext = {
25
+ captureId: string;
26
+ timestamp: number;
27
+ appName: string;
28
+ windowTitle: string;
29
+ url: string | null;
30
+ filePath: string | null;
31
+ ocrText: string;
32
+ sessionId: string;
33
+ isSignificantChange: boolean;
34
+ };
35
+
36
+ // ── Events ──
37
+
38
+ export type AwarenessEventType =
39
+ | 'context_changed'
40
+ | 'error_detected'
41
+ | 'stuck_detected'
42
+ | 'struggle_detected'
43
+ | 'session_started'
44
+ | 'session_ended'
45
+ | 'suggestion_ready';
46
+
47
+ export type AwarenessEvent = {
48
+ type: AwarenessEventType;
49
+ data: Record<string, unknown>;
50
+ timestamp: number;
51
+ };
52
+
53
+ // ── Suggestions ──
54
+
55
+ export type SuggestionType =
56
+ | 'error'
57
+ | 'stuck'
58
+ | 'struggle'
59
+ | 'automation'
60
+ | 'knowledge'
61
+ | 'schedule'
62
+ | 'break'
63
+ | 'general';
64
+
65
+ export type Suggestion = {
66
+ id: string;
67
+ type: SuggestionType;
68
+ title: string;
69
+ body: string;
70
+ triggerCaptureId: string;
71
+ context?: Record<string, unknown>;
72
+ };
73
+
74
+ // ── Sessions ──
75
+
76
+ export type SessionSummary = {
77
+ id: string;
78
+ startedAt: number;
79
+ endedAt: number | null;
80
+ topic: string | null;
81
+ apps: string[];
82
+ projectContext: string | null;
83
+ captureCount: number;
84
+ summary: string | null;
85
+ };
86
+
87
+ // ── Analytics ──
88
+
89
+ export type AppUsageStat = {
90
+ app: string;
91
+ minutes: number;
92
+ percentage: number;
93
+ captureCount: number;
94
+ };
95
+
96
+ export type DailyReport = {
97
+ date: string;
98
+ totalActiveMinutes: number;
99
+ appBreakdown: AppUsageStat[];
100
+ sessionCount: number;
101
+ sessions: Array<{ topic: string | null; durationMinutes: number; apps: string[] }>;
102
+ focusScore: number; // 0-100
103
+ contextSwitches: number;
104
+ longestFocusMinutes: number;
105
+ suggestions: { total: number; actedOn: number };
106
+ aiTakeaways: string[];
107
+ };
108
+
109
+ export type LiveContext = {
110
+ currentApp: string | null;
111
+ currentWindow: string | null;
112
+ currentSession: { id: string; topic: string | null; durationMs: number } | null;
113
+ recentApps: string[];
114
+ capturesLastHour: number;
115
+ suggestionsToday: number;
116
+ isRunning: boolean;
117
+ };
118
+
119
+ // ── Weekly Report & Insights ──
120
+
121
+ export type WeeklyReport = {
122
+ weekStart: string; // YYYY-MM-DD (Monday)
123
+ weekEnd: string; // YYYY-MM-DD (Sunday)
124
+ totalActiveMinutes: number;
125
+ avgDailyMinutes: number;
126
+ avgFocusScore: number;
127
+ topApps: AppUsageStat[];
128
+ dailyBreakdown: Array<{
129
+ date: string;
130
+ activeMinutes: number;
131
+ focusScore: number;
132
+ contextSwitches: number;
133
+ sessionCount: number;
134
+ }>;
135
+ trends: {
136
+ activeTime: 'up' | 'down' | 'stable';
137
+ focusScore: 'up' | 'down' | 'stable';
138
+ contextSwitches: 'up' | 'down' | 'stable';
139
+ };
140
+ aiInsights: string[];
141
+ };
142
+
143
+ export type BehavioralInsight = {
144
+ id: string;
145
+ type: 'active_time' | 'focus' | 'top_app' | 'pattern' | 'general';
146
+ title: string;
147
+ body: string;
148
+ metric?: {
149
+ name: string;
150
+ current: number;
151
+ previous: number;
152
+ unit: string;
153
+ };
154
+ };
155
+
156
+ // ── Database Row Types ──
157
+
158
+ export type ScreenCaptureRow = {
159
+ id: string;
160
+ timestamp: number;
161
+ session_id: string | null;
162
+ image_path: string | null;
163
+ thumbnail_path: string | null;
164
+ pixel_change_pct: number;
165
+ ocr_text: string | null;
166
+ app_name: string | null;
167
+ window_title: string | null;
168
+ url: string | null;
169
+ file_path: string | null;
170
+ retention_tier: 'full' | 'key_moment' | 'metadata_only';
171
+ created_at: number;
172
+ };
173
+
174
+ export type SessionRow = {
175
+ id: string;
176
+ started_at: number;
177
+ ended_at: number | null;
178
+ topic: string | null;
179
+ apps: string; // JSON array
180
+ project_context: string | null;
181
+ action_types: string; // JSON array
182
+ entity_links: string; // JSON array
183
+ summary: string | null;
184
+ capture_count: number;
185
+ created_at: number;
186
+ };
187
+
188
+ export type SuggestionRow = {
189
+ id: string;
190
+ type: SuggestionType;
191
+ trigger_capture_id: string | null;
192
+ title: string;
193
+ body: string;
194
+ context: string | null; // JSON
195
+ delivered: number;
196
+ delivered_at: number | null;
197
+ delivery_channel: string | null;
198
+ dismissed: number;
199
+ acted_on: number;
200
+ created_at: number;
201
+ };