@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,130 @@
1
+ /**
2
+ * Context Graph — Entity Linking
3
+ *
4
+ * Links screen captures to vault entities by matching OCR text and
5
+ * window titles against known entity names. Creates new 'tool' entities
6
+ * for unseen applications.
7
+ */
8
+
9
+ import type { ScreenContext } from './types.ts';
10
+ import { searchEntitiesByName, createEntity } from '../vault/entities.ts';
11
+ import { getSession, updateSession } from '../vault/awareness.ts';
12
+
13
+ // Cache of known app names to avoid redundant entity creation
14
+ const knownApps = new Set<string>();
15
+
16
+ export class ContextGraph {
17
+ /**
18
+ * Link a capture to vault entities.
19
+ * Searches entity names against OCR text + window title.
20
+ * Returns array of matched entity IDs.
21
+ */
22
+ linkCaptureToEntities(context: ScreenContext): string[] {
23
+ const linkedIds: string[] = [];
24
+
25
+ try {
26
+ // 1. Ensure the app itself is a known entity
27
+ if (context.appName && context.appName !== 'Unknown') {
28
+ this.ensureAppEntity(context.appName);
29
+ }
30
+
31
+ // 2. Search OCR text and window title for known entity names
32
+ const searchText = `${context.windowTitle} ${context.ocrText.slice(0, 1000)}`;
33
+ const matchedEntities = this.findEntitiesInText(searchText);
34
+ linkedIds.push(...matchedEntities);
35
+
36
+ // 3. Update session entity links
37
+ if (context.sessionId && linkedIds.length > 0) {
38
+ this.updateSessionLinks(context.sessionId, linkedIds);
39
+ }
40
+ } catch (err) {
41
+ console.error('[ContextGraph] Entity linking error:', err instanceof Error ? err.message : err);
42
+ }
43
+
44
+ return linkedIds;
45
+ }
46
+
47
+ /**
48
+ * Ensure an app has an entity in the vault.
49
+ */
50
+ private ensureAppEntity(appName: string): void {
51
+ if (knownApps.has(appName)) return;
52
+
53
+ try {
54
+ const existing = searchEntitiesByName(appName);
55
+ if (existing.length === 0) {
56
+ createEntity('tool', appName, { source: 'awareness_auto' }, 'awareness');
57
+ }
58
+ knownApps.add(appName);
59
+ } catch { /* ignore — entity creation is best-effort */ }
60
+ }
61
+
62
+ /**
63
+ * Find vault entities mentioned in text.
64
+ * Uses a simple word-boundary search against entity names.
65
+ */
66
+ private findEntitiesInText(text: string): string[] {
67
+ const ids: string[] = [];
68
+ const textLower = text.toLowerCase();
69
+
70
+ try {
71
+ // Search for entities with names >= 3 chars that appear in the text
72
+ // We search for common entity types that would appear in screen context
73
+ const words = this.extractSignificantWords(textLower);
74
+
75
+ for (const word of words) {
76
+ if (word.length < 3) continue;
77
+
78
+ const matches = searchEntitiesByName(word);
79
+ for (const entity of matches) {
80
+ // Exact word match (not substring of longer word)
81
+ const nameLower = entity.name.toLowerCase();
82
+ if (textLower.includes(nameLower) && !ids.includes(entity.id)) {
83
+ ids.push(entity.id);
84
+ }
85
+ }
86
+
87
+ // Limit to avoid too many DB queries
88
+ if (ids.length >= 10) break;
89
+ }
90
+ } catch { /* ignore */ }
91
+
92
+ return ids;
93
+ }
94
+
95
+ /**
96
+ * Extract significant words for entity matching (skip common words).
97
+ */
98
+ private extractSignificantWords(text: string): string[] {
99
+ const stopWords = new Set([
100
+ 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can',
101
+ 'had', 'her', 'was', 'one', 'our', 'out', 'has', 'have', 'from',
102
+ 'this', 'that', 'with', 'they', 'been', 'said', 'each', 'which',
103
+ 'their', 'will', 'other', 'about', 'more', 'some', 'than', 'them',
104
+ 'would', 'make', 'like', 'just', 'over', 'such', 'into', 'also',
105
+ 'file', 'new', 'open', 'save', 'close', 'edit', 'view', 'help',
106
+ 'true', 'false', 'null', 'undefined', 'error', 'warning',
107
+ ]);
108
+
109
+ const words = text.match(/[a-z]{3,}/g) ?? [];
110
+ const unique = [...new Set(words)];
111
+ return unique.filter(w => !stopWords.has(w)).slice(0, 20);
112
+ }
113
+
114
+ /**
115
+ * Update session's entity_links with newly found links.
116
+ */
117
+ private updateSessionLinks(sessionId: string, entityIds: string[]): void {
118
+ try {
119
+ const session = getSession(sessionId);
120
+ if (!session) return;
121
+
122
+ const existing: string[] = JSON.parse(session.entity_links || '[]');
123
+ const merged = [...new Set([...existing, ...entityIds])];
124
+
125
+ if (merged.length !== existing.length) {
126
+ updateSession(sessionId, { entity_links: merged });
127
+ }
128
+ } catch { /* ignore */ }
129
+ }
130
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Context Tracker — Screen Context Analysis
3
+ *
4
+ * Maintains current screen context, detects app changes, stuck states,
5
+ * error patterns, and manages activity sessions.
6
+ */
7
+
8
+ import type { AwarenessConfig } from '../config/types.ts';
9
+ import type { ScreenContext, AwarenessEvent } from './types.ts';
10
+ import { createSession, endSession, incrementSessionCaptureCount, updateSession } from '../vault/awareness.ts';
11
+ import { generateId } from '../vault/schema.ts';
12
+ import { StruggleDetector } from './struggle-detector.ts';
13
+
14
+ // Strong error indicators — always trigger (rare in normal output)
15
+ const STRONG_ERROR_PATTERN = /\b(traceback|segfault|SIGSEGV|SIGABRT|panic|undefined is not|cannot read prop|stack overflow|out of memory|unhandled rejection|uncaught exception|TypeError:|ReferenceError:|SyntaxError:|RangeError:|ModuleNotFoundError|ImportError|FileNotFoundError|PermissionError|ConnectionRefusedError|ECONNREFUSED|ENOTFOUND|EPERM|Build failed|Compilation error|npm ERR!)\b/i;
16
+
17
+ // Weaker indicators — only trigger when accompanied by stack trace context
18
+ const WEAK_ERROR_PATTERN = /\b(error|exception|failed|fatal|crash|denied|refused|timeout|ENOENT|EACCES|ECONNREFUSED)\b/i;
19
+
20
+ // Context that confirms a weak error is real (stack traces, error codes, build output, HTTP errors, etc.)
21
+ const ERROR_CONFIRM_PATTERN = /(?:at .+:\d+|line \d+|\.ts:\d+|\.js:\d+|\.py:\d+|\.go:\d+|throw |exit code|status [45]\d{2}|Traceback|^\s+\^|command not found|No such file|permission denied|cannot find module|Module not found|Compilation failed|Build failed|ERR!|npm ERR|error\[\w+\]|✗|✘|FAIL|FAILED|returned non-zero|Process exited)/mi;
22
+
23
+ // JARVIS own log lines — filter these out from error detection
24
+ const JARVIS_LOG_PREFIX = /\[(CaptureEngine|ObserverManager|file-watcher|clipboard|processes|notifications|email|calendar|Daemon|ServiceRegistry|AgentService|WSService|OCREngine|Awareness|DesktopController|Executor|HealthMonitor|ChannelManager|TelegramAdapter|BackgroundAgent|WebSocketServer|EventReactor|Orchestrator|ChannelService|ObserverService)\]/;
25
+
26
+ // URL pattern in text
27
+ const URL_PATTERN = /https?:\/\/[^\s<>"{}|\\^`[\]]+/;
28
+
29
+ // File path patterns
30
+ const FILE_PATH_PATTERN = /(?:\/[\w.-]+){2,}(?:\.\w+)?|[A-Z]:\\[\w\\.-]+/;
31
+
32
+ export class ContextTracker {
33
+ private config: AwarenessConfig;
34
+ private currentContext: ScreenContext | null = null;
35
+ private previousContext: ScreenContext | null = null;
36
+ private currentSessionId: string | null = null;
37
+ private currentSessionApps: Set<string> = new Set();
38
+ private sameWindowSince: number = 0;
39
+ private lastOcrTextHash: string = '';
40
+ private lastActivityTimestamp: number = 0;
41
+ private lastErrorText: string = '';
42
+ private lastErrorTimestamp: number = 0;
43
+ private pendingWindowInfo: { appName: string; windowTitle: string } | null = null;
44
+ private struggleDetector: StruggleDetector;
45
+
46
+ constructor(config: AwarenessConfig) {
47
+ this.config = config;
48
+ this.struggleDetector = new StruggleDetector({
49
+ graceMs: config.struggle_grace_ms ?? 120_000,
50
+ cooldownMs: config.struggle_cooldown_ms ?? 180_000,
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Process a new screen capture. Returns the context and any detected events.
56
+ */
57
+ processCapture(captureId: string, ocrText: string, rawWindowTitle?: string): {
58
+ context: ScreenContext;
59
+ events: AwarenessEvent[];
60
+ } {
61
+ const now = Date.now();
62
+ const events: AwarenessEvent[] = [];
63
+
64
+ // Use pending window info from sidecar context_changed if no rawWindowTitle provided
65
+ const effectiveWindowTitle = rawWindowTitle || this.pendingWindowInfo?.windowTitle;
66
+ if (this.pendingWindowInfo && !rawWindowTitle) {
67
+ this.pendingWindowInfo = null; // consumed
68
+ }
69
+
70
+ // Parse app name and details from window title or OCR
71
+ const { appName, windowTitle, url, filePath } = this.parseWindowInfo(ocrText, effectiveWindowTitle);
72
+
73
+ // Detect context change
74
+ const isAppChange = this.currentContext !== null &&
75
+ (this.currentContext.appName !== appName || this.currentContext.windowTitle !== windowTitle);
76
+
77
+ const isSignificantChange = isAppChange || this.currentContext === null;
78
+
79
+ // Session management
80
+ const idleGap = this.lastActivityTimestamp > 0 ? (now - this.lastActivityTimestamp) : 0;
81
+ const isIdleReturn = idleGap > 5 * 60 * 1000; // 5 min idle
82
+
83
+ if (isAppChange || isIdleReturn || !this.currentSessionId) {
84
+ // End previous session if it exists
85
+ if (this.currentSessionId && isAppChange) {
86
+ this.endCurrentSession();
87
+ events.push({
88
+ type: 'session_ended',
89
+ data: { sessionId: this.currentSessionId, apps: Array.from(this.currentSessionApps) },
90
+ timestamp: now,
91
+ });
92
+ }
93
+
94
+ // Start new session
95
+ const session = createSession({ startedAt: now, apps: [appName] });
96
+ this.currentSessionId = session.id;
97
+ this.currentSessionApps = new Set([appName]);
98
+ events.push({
99
+ type: 'session_started',
100
+ data: { sessionId: session.id, appName },
101
+ timestamp: now,
102
+ });
103
+ }
104
+
105
+ // Track app in current session
106
+ if (this.currentSessionId && appName) {
107
+ this.currentSessionApps.add(appName);
108
+ incrementSessionCaptureCount(this.currentSessionId);
109
+ }
110
+
111
+ // Context change event
112
+ if (isAppChange) {
113
+ events.push({
114
+ type: 'context_changed',
115
+ data: {
116
+ fromApp: this.currentContext?.appName ?? 'unknown',
117
+ toApp: appName,
118
+ fromWindow: this.currentContext?.windowTitle ?? '',
119
+ toWindow: windowTitle,
120
+ },
121
+ timestamp: now,
122
+ });
123
+ this.sameWindowSince = now;
124
+ this.lastOcrTextHash = '';
125
+ this.struggleDetector.reset();
126
+ }
127
+
128
+ // Track same-window duration for stuck detection
129
+ if (!isAppChange && this.currentContext) {
130
+ // Same window — check if stuck
131
+ if (this.sameWindowSince === 0) {
132
+ this.sameWindowSince = now;
133
+ }
134
+
135
+ const sameWindowDuration = now - this.sameWindowSince;
136
+ const ocrHash = simpleHash(ocrText);
137
+ const textUnchanged = ocrHash === this.lastOcrTextHash;
138
+
139
+ if (sameWindowDuration > this.config.stuck_threshold_ms && textUnchanged) {
140
+ events.push({
141
+ type: 'stuck_detected',
142
+ data: {
143
+ windowTitle,
144
+ appName,
145
+ durationMs: sameWindowDuration,
146
+ ocrPreview: ocrText.slice(0, 200),
147
+ },
148
+ timestamp: now,
149
+ });
150
+ }
151
+
152
+ this.lastOcrTextHash = ocrHash;
153
+ } else {
154
+ this.sameWindowSince = now;
155
+ this.lastOcrTextHash = simpleHash(ocrText);
156
+ }
157
+
158
+ // Struggle detection (behavioral analysis beyond simple stuck)
159
+ const struggleResult = this.struggleDetector.evaluate(ocrText, appName, windowTitle, now);
160
+ if (struggleResult) {
161
+ events.push({
162
+ type: 'struggle_detected',
163
+ data: {
164
+ appName,
165
+ windowTitle,
166
+ appCategory: struggleResult.appCategory,
167
+ compositeScore: struggleResult.compositeScore,
168
+ signals: struggleResult.signals,
169
+ durationMs: struggleResult.durationMs,
170
+ ocrPreview: ocrText.slice(0, 500),
171
+ },
172
+ timestamp: now,
173
+ });
174
+ }
175
+
176
+ // Error detection in OCR text
177
+ // First, filter out lines that are JARVIS's own log output
178
+ const filteredOcrText = ocrText
179
+ .split('\n')
180
+ .filter(line => !JARVIS_LOG_PREFIX.test(line))
181
+ .join('\n');
182
+
183
+ const strongMatch = filteredOcrText.match(STRONG_ERROR_PATTERN);
184
+ const weakMatch = !strongMatch ? filteredOcrText.match(WEAK_ERROR_PATTERN) : null;
185
+ // Weak errors only fire if there's confirming context (stack trace, etc.)
186
+ const errorMatch = strongMatch ?? (weakMatch && ERROR_CONFIRM_PATTERN.test(filteredOcrText) ? weakMatch : null);
187
+
188
+ if (errorMatch) {
189
+ // Cooldown: don't re-fire same error text within 30 seconds
190
+ const sameError = errorMatch[0].toLowerCase() === this.lastErrorText.toLowerCase();
191
+ const cooldownExpired = (now - this.lastErrorTimestamp) > 30_000;
192
+
193
+ if (!sameError || cooldownExpired) {
194
+ const idx = filteredOcrText.indexOf(errorMatch[0]);
195
+ const errorContext = filteredOcrText.slice(Math.max(0, idx - 100), idx + 200);
196
+
197
+ events.push({
198
+ type: 'error_detected',
199
+ data: {
200
+ errorText: errorMatch[0],
201
+ errorContext: errorContext.trim(),
202
+ appName,
203
+ windowTitle,
204
+ },
205
+ timestamp: now,
206
+ });
207
+
208
+ this.lastErrorText = errorMatch[0];
209
+ this.lastErrorTimestamp = now;
210
+ }
211
+ }
212
+
213
+ // Build context object
214
+ const context: ScreenContext = {
215
+ captureId,
216
+ timestamp: now,
217
+ appName,
218
+ windowTitle,
219
+ url,
220
+ filePath,
221
+ ocrText,
222
+ sessionId: this.currentSessionId!,
223
+ isSignificantChange,
224
+ };
225
+
226
+ // Update state
227
+ this.previousContext = this.currentContext;
228
+ this.currentContext = context;
229
+ this.lastActivityTimestamp = now;
230
+
231
+ return { context, events };
232
+ }
233
+
234
+ getCurrentContext(): ScreenContext | null {
235
+ return this.currentContext;
236
+ }
237
+
238
+ getPreviousContext(): ScreenContext | null {
239
+ return this.previousContext;
240
+ }
241
+
242
+ getCurrentSession(): { id: string; topic: string | null; startedAt: number } | null {
243
+ if (!this.currentSessionId) return null;
244
+ return {
245
+ id: this.currentSessionId,
246
+ topic: null, // Topic set later by intelligence layer
247
+ startedAt: this.sameWindowSince || Date.now(),
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Get the last known window title (set by updateWindowInfo from sidecar events).
253
+ */
254
+ getLastWindowTitle(): string | undefined {
255
+ return this.currentContext?.windowTitle ?? undefined;
256
+ }
257
+
258
+ /**
259
+ * Update cached window info from a sidecar context_changed event.
260
+ * Called externally when the sidecar pushes a window change.
261
+ */
262
+ updateWindowInfo(appName: string, windowTitle: string): void {
263
+ if (!this.currentContext) {
264
+ // No capture processed yet — store as pending info for the first processCapture call
265
+ this.pendingWindowInfo = { appName, windowTitle };
266
+ return;
267
+ }
268
+ // Directly update current context's window info
269
+ this.currentContext = {
270
+ ...this.currentContext,
271
+ appName: appName || this.currentContext.appName,
272
+ windowTitle: windowTitle || this.currentContext.windowTitle,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Report idle/stuck state from sidecar idle_detected event.
278
+ */
279
+ reportIdle(appName: string, durationMs: number): void {
280
+ // The struggle detector already handles stuck detection from processCapture,
281
+ // but this provides an external signal from the sidecar's window observer.
282
+ // We can use it to supplement the stuck detection threshold.
283
+ if (this.currentContext && durationMs > 0) {
284
+ this.sameWindowSince = Date.now() - durationMs;
285
+ }
286
+ }
287
+
288
+ endCurrentSession(): void {
289
+ if (this.currentSessionId) {
290
+ try {
291
+ // Update session apps before ending
292
+ updateSession(this.currentSessionId, {
293
+ apps: Array.from(this.currentSessionApps),
294
+ });
295
+ endSession(this.currentSessionId);
296
+ } catch { /* session may not exist in test environments */ }
297
+ this.currentSessionId = null;
298
+ this.currentSessionApps.clear();
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Parse window title and OCR text to extract app name, URL, file path.
304
+ */
305
+ private parseWindowInfo(ocrText: string, rawWindowTitle?: string): {
306
+ appName: string;
307
+ windowTitle: string;
308
+ url: string | null;
309
+ filePath: string | null;
310
+ } {
311
+ const windowTitle = rawWindowTitle || '';
312
+
313
+ // Extract app name from window title (typically "Content - AppName" or "AppName - Content")
314
+ let appName = 'Unknown';
315
+ if (windowTitle) {
316
+ const parts = windowTitle.split(/\s[-–—]\s/);
317
+ if (parts.length >= 2) {
318
+ // Last part is usually the app name
319
+ appName = parts[parts.length - 1]!.trim();
320
+ } else {
321
+ appName = windowTitle.trim();
322
+ }
323
+ }
324
+
325
+ // Extract URL from OCR text or window title
326
+ const urlMatch = (windowTitle + ' ' + ocrText).match(URL_PATTERN);
327
+ const url = urlMatch ? urlMatch[0] : null;
328
+
329
+ // Extract file path from window title
330
+ const fileMatch = windowTitle.match(FILE_PATH_PATTERN);
331
+ const filePath = fileMatch ? fileMatch[0] : null;
332
+
333
+ return { appName, windowTitle, url, filePath };
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Simple string hash for quick comparison (not cryptographic).
339
+ */
340
+ function simpleHash(str: string): string {
341
+ let hash = 0;
342
+ const sample = str.slice(0, 2000); // Only hash first 2000 chars
343
+ for (let i = 0; i < sample.length; i++) {
344
+ const char = sample.charCodeAt(i);
345
+ hash = ((hash << 5) - hash) + char;
346
+ hash |= 0; // Convert to 32bit integer
347
+ }
348
+ return hash.toString(36);
349
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Awareness Engine — Public API
3
+ */
4
+
5
+ export { AwarenessService } from './service.ts';
6
+ export { OCREngine } from './ocr-engine.ts';
7
+ export { ContextTracker } from './context-tracker.ts';
8
+ export { AwarenessIntelligence } from './intelligence.ts';
9
+ export { SuggestionEngine } from './suggestion-engine.ts';
10
+ export { ContextGraph } from './context-graph.ts';
11
+ export { BehaviorAnalytics } from './analytics.ts';
12
+
13
+ export type {
14
+ CaptureFrame,
15
+ OCRResult,
16
+ ScreenContext,
17
+ AwarenessEvent,
18
+ AwarenessEventType,
19
+ SuggestionType,
20
+ Suggestion,
21
+ SessionSummary,
22
+ AppUsageStat,
23
+ DailyReport,
24
+ LiveContext,
25
+ } from './types.ts';