@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.
- package/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- 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
|
+
}
|