@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,466 @@
1
+ /**
2
+ * Behavior Analytics — Daily Reports & Usage Stats
3
+ *
4
+ * Aggregates screen capture data into daily productivity reports,
5
+ * app usage breakdowns, focus scores, and session histories.
6
+ */
7
+
8
+ import type { LLMManager } from '../llm/manager.ts';
9
+ import type { DailyReport, LiveContext, SessionSummary, AppUsageStat, WeeklyReport, BehavioralInsight } from './types.ts';
10
+ import { generateId } from '../vault/schema.ts';
11
+ import {
12
+ getCapturesInRange,
13
+ getAppUsageStats,
14
+ getRecentSessions,
15
+ getCaptureCountSince,
16
+ } from '../vault/awareness.ts';
17
+ import { getSuggestionStats, getSuggestionCountSince } from '../vault/awareness.ts';
18
+ import type { ContextTracker } from './context-tracker.ts';
19
+
20
+ export class BehaviorAnalytics {
21
+ private llm: LLMManager;
22
+
23
+ constructor(llm: LLMManager) {
24
+ this.llm = llm;
25
+ }
26
+
27
+ /**
28
+ * Generate a daily productivity report.
29
+ * @param date — Date string 'YYYY-MM-DD' or undefined for today
30
+ */
31
+ async generateDailyReport(date?: string): Promise<DailyReport> {
32
+ const targetDate = date || new Date().toISOString().split('T')[0];
33
+ const dayStart = new Date(targetDate + 'T00:00:00').getTime();
34
+ const dayEnd = new Date(targetDate + 'T23:59:59.999').getTime();
35
+
36
+ // Get captures and compute stats
37
+ const captures = getCapturesInRange(dayStart, dayEnd);
38
+ const appBreakdown = getAppUsageStats(dayStart, dayEnd);
39
+ const suggestionStats = getSuggestionStats(dayStart, dayEnd);
40
+
41
+ // Get sessions for the day
42
+ const allSessions = getRecentSessions(100);
43
+ const daySessions = allSessions.filter(s => s.started_at >= dayStart && s.started_at <= dayEnd);
44
+
45
+ // Compute focus metrics
46
+ const contextSwitches = captures.filter((c, i) =>
47
+ i > 0 && c.app_name !== captures[i - 1]!.app_name
48
+ ).length;
49
+
50
+ const totalActiveMinutes = Math.round((captures.length * 7) / 60); // ~7s per capture
51
+
52
+ // Focus score: fewer context switches per hour = higher focus
53
+ const activeHours = Math.max(totalActiveMinutes / 60, 0.1);
54
+ const switchesPerHour = contextSwitches / activeHours;
55
+ // Score: 100 for 0 switches/hr, ~50 for 10, ~20 for 30+
56
+ const focusScore = Math.max(0, Math.min(100, Math.round(100 * Math.exp(-switchesPerHour / 15))));
57
+
58
+ // Longest continuous focus (same app streak)
59
+ let longestStreak = 0;
60
+ let currentStreak = 1;
61
+ for (let i = 1; i < captures.length; i++) {
62
+ if (captures[i]!.app_name === captures[i - 1]!.app_name) {
63
+ currentStreak++;
64
+ } else {
65
+ longestStreak = Math.max(longestStreak, currentStreak);
66
+ currentStreak = 1;
67
+ }
68
+ }
69
+ longestStreak = Math.max(longestStreak, currentStreak);
70
+ const longestFocusMinutes = Math.round((longestStreak * 7) / 60);
71
+
72
+ // Build session summaries
73
+ const sessions = daySessions.map(s => {
74
+ const apps = JSON.parse(s.apps || '[]') as string[];
75
+ const durationMs = (s.ended_at || Date.now()) - s.started_at;
76
+ return {
77
+ topic: s.topic,
78
+ durationMinutes: Math.round(durationMs / 60000),
79
+ apps,
80
+ };
81
+ });
82
+
83
+ // Generate AI takeaways
84
+ const aiTakeaways = await this.generateTakeaways(
85
+ totalActiveMinutes,
86
+ appBreakdown,
87
+ contextSwitches,
88
+ focusScore,
89
+ sessions
90
+ );
91
+
92
+ return {
93
+ date: targetDate!,
94
+ totalActiveMinutes,
95
+ appBreakdown,
96
+ sessionCount: daySessions.length,
97
+ sessions,
98
+ focusScore,
99
+ contextSwitches,
100
+ longestFocusMinutes,
101
+ suggestions: suggestionStats,
102
+ aiTakeaways,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Get app usage stats for a time range.
108
+ */
109
+ getAppUsage(startTime: number, endTime: number): AppUsageStat[] {
110
+ return getAppUsageStats(startTime, endTime);
111
+ }
112
+
113
+ /**
114
+ * Get recent session history.
115
+ */
116
+ getSessionHistory(limit: number = 20): SessionSummary[] {
117
+ const rows = getRecentSessions(limit);
118
+ return rows.map(r => ({
119
+ id: r.id,
120
+ startedAt: r.started_at,
121
+ endedAt: r.ended_at,
122
+ topic: r.topic,
123
+ apps: JSON.parse(r.apps || '[]') as string[],
124
+ projectContext: r.project_context,
125
+ captureCount: r.capture_count,
126
+ summary: r.summary,
127
+ }));
128
+ }
129
+
130
+ /**
131
+ * Get live context snapshot.
132
+ */
133
+ getLiveContext(tracker: ContextTracker, isRunning: boolean): LiveContext {
134
+ const ctx = tracker.getCurrentContext();
135
+ const session = tracker.getCurrentSession();
136
+
137
+ // Recent unique apps from last 10 minutes
138
+ const tenMinAgo = Date.now() - 10 * 60 * 1000;
139
+ let recentApps: string[] = [];
140
+ try {
141
+ const stats = getAppUsageStats(tenMinAgo, Date.now());
142
+ recentApps = stats.map(s => s.app);
143
+ } catch { /* ignore */ }
144
+
145
+ // Counts
146
+ let capturesLastHour = 0;
147
+ let suggestionsToday = 0;
148
+ try {
149
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
150
+ capturesLastHour = getCaptureCountSince(oneHourAgo);
151
+
152
+ const todayStart = new Date();
153
+ todayStart.setHours(0, 0, 0, 0);
154
+ suggestionsToday = getSuggestionCountSince(todayStart.getTime());
155
+ } catch { /* ignore */ }
156
+
157
+ return {
158
+ currentApp: ctx?.appName ?? null,
159
+ currentWindow: ctx?.windowTitle ?? null,
160
+ currentSession: session ? {
161
+ id: session.id,
162
+ topic: session.topic,
163
+ durationMs: Date.now() - session.startedAt,
164
+ } : null,
165
+ recentApps,
166
+ capturesLastHour,
167
+ suggestionsToday,
168
+ isRunning,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Generate a weekly productivity report with trends.
174
+ * @param weekStart — Monday date 'YYYY-MM-DD', or undefined for current week
175
+ */
176
+ async generateWeeklyReport(weekStart?: string): Promise<WeeklyReport> {
177
+ const monday = weekStart ? new Date(weekStart + 'T00:00:00') : this.getMondayOfWeek(new Date());
178
+ const sunday = new Date(monday);
179
+ sunday.setDate(sunday.getDate() + 6);
180
+
181
+ const weekStartStr = monday.toISOString().split('T')[0];
182
+ const weekEndStr = sunday.toISOString().split('T')[0];
183
+
184
+ // Previous week for trend comparison
185
+ const prevMonday = new Date(monday);
186
+ prevMonday.setDate(prevMonday.getDate() - 7);
187
+
188
+ // Build daily breakdown
189
+ const dailyBreakdown: WeeklyReport['dailyBreakdown'] = [];
190
+ let totalMinutes = 0;
191
+ let totalFocus = 0;
192
+ let totalSwitches = 0;
193
+ let totalSessions = 0;
194
+ let daysWithData = 0;
195
+
196
+ for (let d = 0; d < 7; d++) {
197
+ const day = new Date(monday);
198
+ day.setDate(day.getDate() + d);
199
+ const dateStr = day.toISOString().split('T')[0];
200
+ const dayStart = new Date(dateStr + 'T00:00:00').getTime();
201
+ const dayEnd = new Date(dateStr + 'T23:59:59.999').getTime();
202
+
203
+ const captures = getCapturesInRange(dayStart, dayEnd);
204
+ const activeMinutes = Math.round((captures.length * 7) / 60);
205
+
206
+ const contextSwitches = captures.filter((c, i) =>
207
+ i > 0 && c.app_name !== captures[i - 1]!.app_name
208
+ ).length;
209
+
210
+ const activeHours = Math.max(activeMinutes / 60, 0.1);
211
+ const switchesPerHour = contextSwitches / activeHours;
212
+ const focusScore = captures.length > 0
213
+ ? Math.max(0, Math.min(100, Math.round(100 * Math.exp(-switchesPerHour / 15))))
214
+ : 0;
215
+
216
+ const allSessions = getRecentSessions(100);
217
+ const sessionCount = allSessions.filter(s => s.started_at >= dayStart && s.started_at <= dayEnd).length;
218
+
219
+ dailyBreakdown.push({ date: dateStr!, activeMinutes, focusScore, contextSwitches, sessionCount });
220
+ totalMinutes += activeMinutes;
221
+ totalFocus += focusScore;
222
+ totalSwitches += contextSwitches;
223
+ totalSessions += sessionCount;
224
+ if (activeMinutes > 0) daysWithData++;
225
+ }
226
+
227
+ const avgDailyMinutes = daysWithData > 0 ? Math.round(totalMinutes / daysWithData) : 0;
228
+ const avgFocusScore = daysWithData > 0 ? Math.round(totalFocus / daysWithData) : 0;
229
+
230
+ // Get aggregated top apps for the week
231
+ const weekStartMs = monday.getTime();
232
+ const weekEndMs = sunday.getTime() + 24 * 60 * 60 * 1000 - 1;
233
+ const topApps = getAppUsageStats(weekStartMs, weekEndMs);
234
+
235
+ // Compare with previous week for trends
236
+ const prevWeekStartMs = prevMonday.getTime();
237
+ const prevWeekEndMs = weekStartMs - 1;
238
+ let prevTotalMinutes = 0;
239
+ let prevTotalFocus = 0;
240
+ let prevTotalSwitches = 0;
241
+ let prevDaysWithData = 0;
242
+
243
+ for (let d = 0; d < 7; d++) {
244
+ const day = new Date(prevMonday);
245
+ day.setDate(day.getDate() + d);
246
+ const dateStr = day.toISOString().split('T')[0];
247
+ const dayStart = new Date(dateStr + 'T00:00:00').getTime();
248
+ const dayEnd = new Date(dateStr + 'T23:59:59.999').getTime();
249
+ const captures = getCapturesInRange(dayStart, dayEnd);
250
+ const mins = Math.round((captures.length * 7) / 60);
251
+ const switches = captures.filter((c, i) => i > 0 && c.app_name !== captures[i - 1]!.app_name).length;
252
+ const hrs = Math.max(mins / 60, 0.1);
253
+ const focus = captures.length > 0
254
+ ? Math.max(0, Math.min(100, Math.round(100 * Math.exp(-(switches / hrs) / 15))))
255
+ : 0;
256
+ prevTotalMinutes += mins;
257
+ prevTotalFocus += focus;
258
+ prevTotalSwitches += switches;
259
+ if (mins > 0) prevDaysWithData++;
260
+ }
261
+
262
+ const prevAvgMinutes = prevDaysWithData > 0 ? prevTotalMinutes / prevDaysWithData : 0;
263
+ const prevAvgFocus = prevDaysWithData > 0 ? prevTotalFocus / prevDaysWithData : 0;
264
+ const prevAvgSwitches = prevDaysWithData > 0 ? prevTotalSwitches / prevDaysWithData : 0;
265
+ const currAvgSwitches = daysWithData > 0 ? totalSwitches / daysWithData : 0;
266
+
267
+ const trendOf = (curr: number, prev: number): 'up' | 'down' | 'stable' => {
268
+ if (prev === 0) return curr > 0 ? 'up' : 'stable';
269
+ const change = (curr - prev) / prev;
270
+ if (change > 0.1) return 'up';
271
+ if (change < -0.1) return 'down';
272
+ return 'stable';
273
+ };
274
+
275
+ const trends: WeeklyReport['trends'] = {
276
+ activeTime: trendOf(avgDailyMinutes, prevAvgMinutes),
277
+ focusScore: trendOf(avgFocusScore, prevAvgFocus),
278
+ contextSwitches: trendOf(currAvgSwitches, prevAvgSwitches),
279
+ };
280
+
281
+ // Generate AI weekly insights
282
+ const aiInsights = await this.generateWeeklyInsights(
283
+ totalMinutes, avgDailyMinutes, avgFocusScore, topApps, trends, dailyBreakdown
284
+ );
285
+
286
+ return {
287
+ weekStart: weekStartStr!,
288
+ weekEnd: weekEndStr!,
289
+ totalActiveMinutes: totalMinutes,
290
+ avgDailyMinutes,
291
+ avgFocusScore,
292
+ topApps,
293
+ dailyBreakdown,
294
+ trends,
295
+ aiInsights,
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Get behavioral insights comparing recent activity to previous period.
301
+ */
302
+ getBehavioralInsights(days: number = 7): BehavioralInsight[] {
303
+ const insights: BehavioralInsight[] = [];
304
+ const now = Date.now();
305
+ const periodMs = days * 24 * 60 * 60 * 1000;
306
+ const currentStart = now - periodMs;
307
+ const prevStart = currentStart - periodMs;
308
+
309
+ // Current period stats
310
+ const currentCaptures = getCapturesInRange(currentStart, now);
311
+ const prevCaptures = getCapturesInRange(prevStart, currentStart);
312
+
313
+ const currentMinutes = Math.round((currentCaptures.length * 7) / 60);
314
+ const prevMinutes = Math.round((prevCaptures.length * 7) / 60);
315
+
316
+ // Active time comparison
317
+ if (currentMinutes > 0 || prevMinutes > 0) {
318
+ const delta = currentMinutes - prevMinutes;
319
+ const direction = delta > 0 ? 'more' : delta < 0 ? 'less' : 'the same';
320
+ insights.push({
321
+ id: generateId(),
322
+ type: 'active_time',
323
+ title: 'Active Time',
324
+ body: `You were active for ${currentMinutes} minutes over the last ${days} days — ${Math.abs(delta)} minutes ${direction} than the previous period.`,
325
+ metric: { name: 'Active Minutes', current: currentMinutes, previous: prevMinutes, unit: 'min' },
326
+ });
327
+ }
328
+
329
+ // Focus comparison
330
+ const computeFocus = (captures: Array<{ app_name: string | null }>) => {
331
+ if (captures.length === 0) return 0;
332
+ const switches = captures.filter((c, i) => i > 0 && c.app_name !== captures[i - 1]!.app_name).length;
333
+ const hours = Math.max((captures.length * 7) / 3600, 0.1);
334
+ return Math.max(0, Math.min(100, Math.round(100 * Math.exp(-(switches / hours) / 15))));
335
+ };
336
+
337
+ const currentFocus = computeFocus(currentCaptures);
338
+ const prevFocus = computeFocus(prevCaptures);
339
+
340
+ if (currentCaptures.length > 0) {
341
+ insights.push({
342
+ id: generateId(),
343
+ type: 'focus',
344
+ title: 'Focus Score',
345
+ body: currentFocus >= prevFocus
346
+ ? `Focus score is ${currentFocus}/100 — ${currentFocus > prevFocus ? 'improved' : 'holding steady'} from ${prevFocus}/100.`
347
+ : `Focus score dropped to ${currentFocus}/100 from ${prevFocus}/100. Consider reducing context switches.`,
348
+ metric: { name: 'Focus Score', current: currentFocus, previous: prevFocus, unit: '/100' },
349
+ });
350
+ }
351
+
352
+ // Top app identification
353
+ const currentApps = getAppUsageStats(currentStart, now);
354
+ if (currentApps.length > 0) {
355
+ const topApp = currentApps[0]!;
356
+ insights.push({
357
+ id: generateId(),
358
+ type: 'top_app',
359
+ title: `Top App: ${topApp.app}`,
360
+ body: `${topApp.app} dominated with ${topApp.minutes} minutes (${topApp.percentage}% of active time).`,
361
+ });
362
+ }
363
+
364
+ return insights;
365
+ }
366
+
367
+ private getMondayOfWeek(date: Date): Date {
368
+ const d = new Date(date);
369
+ const day = d.getDay();
370
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
371
+ d.setDate(diff);
372
+ d.setHours(0, 0, 0, 0);
373
+ return d;
374
+ }
375
+
376
+ private async generateWeeklyInsights(
377
+ totalMinutes: number,
378
+ avgDailyMinutes: number,
379
+ avgFocusScore: number,
380
+ topApps: AppUsageStat[],
381
+ trends: WeeklyReport['trends'],
382
+ dailyBreakdown: WeeklyReport['dailyBreakdown']
383
+ ): Promise<string[]> {
384
+ if (totalMinutes < 10) return ['Not enough data for weekly insights.'];
385
+
386
+ const topAppsStr = topApps.slice(0, 5).map(a => `${a.app}: ${a.minutes}min`).join(', ');
387
+ const trendStr = `Active time: ${trends.activeTime}, Focus: ${trends.focusScore}, Switches: ${trends.contextSwitches}`;
388
+ const bestDay = [...dailyBreakdown].sort((a, b) => b.focusScore - a.focusScore)[0];
389
+
390
+ try {
391
+ const response = await this.llm.chat(
392
+ [{
393
+ role: 'user',
394
+ content: `Analyze this weekly productivity data and give 3-4 brief insights:
395
+
396
+ Total active time: ${totalMinutes} minutes (avg ${avgDailyMinutes}/day)
397
+ Average focus score: ${avgFocusScore}/100
398
+ Top apps: ${topAppsStr}
399
+ Trends vs last week: ${trendStr}
400
+ Best focus day: ${bestDay?.date} (${bestDay?.focusScore}/100)
401
+
402
+ Give actionable insights as a JSON array of strings.`,
403
+ }],
404
+ { max_tokens: 300 }
405
+ );
406
+
407
+ try {
408
+ const match = response.content.match(/\[[\s\S]*\]/);
409
+ if (match) return JSON.parse(match[0]) as string[];
410
+ } catch { /* parse failure */ }
411
+
412
+ return [response.content.slice(0, 200)];
413
+ } catch (err) {
414
+ console.error('[Analytics] Weekly insight generation failed:', err instanceof Error ? err.message : err);
415
+ return [`${totalMinutes} minutes active this week. Avg focus: ${avgFocusScore}/100.`];
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Generate AI takeaways from daily stats.
421
+ */
422
+ private async generateTakeaways(
423
+ totalMinutes: number,
424
+ appBreakdown: AppUsageStat[],
425
+ contextSwitches: number,
426
+ focusScore: number,
427
+ sessions: Array<{ topic: string | null; durationMinutes: number; apps: string[] }>
428
+ ): Promise<string[]> {
429
+ if (totalMinutes < 5) return ['Not enough activity data for insights.'];
430
+
431
+ const topApps = appBreakdown.slice(0, 5).map(a => `${a.app}: ${a.minutes}min (${a.percentage}%)`).join(', ');
432
+ const sessionSummary = sessions.slice(0, 5).map(s =>
433
+ `${s.topic || 'Unnamed'}: ${s.durationMinutes}min in ${s.apps.join(', ')}`
434
+ ).join('; ');
435
+
436
+ try {
437
+ const response = await this.llm.chat(
438
+ [{
439
+ role: 'user',
440
+ content: `Analyze this daily productivity data and give 3-5 brief takeaways:
441
+
442
+ Active time: ${totalMinutes} minutes
443
+ Focus score: ${focusScore}/100
444
+ Context switches: ${contextSwitches}
445
+ Top apps: ${topApps}
446
+ Sessions: ${sessionSummary}
447
+
448
+ Give actionable insights as a JSON array of strings. Example: ["You spent 40% of time in VS Code — focused coding session!", "High context switching after 3pm — consider blocking distracting apps"]`,
449
+ }],
450
+ { max_tokens: 300 }
451
+ );
452
+
453
+ try {
454
+ const match = response.content.match(/\[[\s\S]*\]/);
455
+ if (match) {
456
+ return JSON.parse(match[0]) as string[];
457
+ }
458
+ } catch { /* parse failure */ }
459
+
460
+ return [response.content.slice(0, 200)];
461
+ } catch (err) {
462
+ console.error('[Analytics] Takeaway generation failed:', err instanceof Error ? err.message : err);
463
+ return [`Active for ${totalMinutes} minutes. Focus score: ${focusScore}/100.`];
464
+ }
465
+ }
466
+ }