@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,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
|
+
};
|