@shykaruu/jarvis-brain 0.4.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 (330) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +428 -0
  3. package/bin/jarvis.ts +449 -0
  4. package/package.json +79 -0
  5. package/roles/activity-observer.yaml +60 -0
  6. package/roles/ceo-founder.yaml +144 -0
  7. package/roles/chief-of-staff.yaml +158 -0
  8. package/roles/dev-lead.yaml +182 -0
  9. package/roles/executive-assistant.yaml +77 -0
  10. package/roles/marketing-director.yaml +168 -0
  11. package/roles/personal-assistant.yaml +266 -0
  12. package/roles/research-specialist.yaml +60 -0
  13. package/roles/specialists/content-writer.yaml +53 -0
  14. package/roles/specialists/customer-support.yaml +57 -0
  15. package/roles/specialists/data-analyst.yaml +57 -0
  16. package/roles/specialists/financial-analyst.yaml +56 -0
  17. package/roles/specialists/hr-specialist.yaml +55 -0
  18. package/roles/specialists/legal-advisor.yaml +58 -0
  19. package/roles/specialists/marketing-strategist.yaml +56 -0
  20. package/roles/specialists/project-coordinator.yaml +55 -0
  21. package/roles/specialists/research-analyst.yaml +58 -0
  22. package/roles/specialists/software-engineer.yaml +57 -0
  23. package/roles/specialists/system-administrator.yaml +57 -0
  24. package/roles/system-admin.yaml +76 -0
  25. package/scripts/ensure-bun.cjs +16 -0
  26. package/src/actions/README.md +421 -0
  27. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  28. package/src/actions/app-control/desktop-controller.ts +438 -0
  29. package/src/actions/app-control/interface.ts +64 -0
  30. package/src/actions/app-control/linux.ts +273 -0
  31. package/src/actions/app-control/macos.ts +54 -0
  32. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  33. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  34. package/src/actions/app-control/windows.ts +44 -0
  35. package/src/actions/browser/cdp.ts +138 -0
  36. package/src/actions/browser/chrome-launcher.ts +261 -0
  37. package/src/actions/browser/session.ts +506 -0
  38. package/src/actions/browser/stealth.ts +49 -0
  39. package/src/actions/index.ts +20 -0
  40. package/src/actions/terminal/executor.ts +157 -0
  41. package/src/actions/terminal/wsl-bridge.ts +126 -0
  42. package/src/actions/test.ts +93 -0
  43. package/src/actions/tools/agents.ts +363 -0
  44. package/src/actions/tools/builtin.ts +950 -0
  45. package/src/actions/tools/commitments.ts +192 -0
  46. package/src/actions/tools/content.ts +217 -0
  47. package/src/actions/tools/delegate.ts +147 -0
  48. package/src/actions/tools/desktop.test.ts +55 -0
  49. package/src/actions/tools/desktop.ts +305 -0
  50. package/src/actions/tools/documents.ts +169 -0
  51. package/src/actions/tools/goals.ts +376 -0
  52. package/src/actions/tools/local-tools-guard.ts +31 -0
  53. package/src/actions/tools/registry.ts +173 -0
  54. package/src/actions/tools/research.ts +111 -0
  55. package/src/actions/tools/sidecar-list.ts +57 -0
  56. package/src/actions/tools/sidecar-route.ts +105 -0
  57. package/src/actions/tools/workflows.ts +216 -0
  58. package/src/agents/agent.ts +132 -0
  59. package/src/agents/delegation.ts +107 -0
  60. package/src/agents/hierarchy.ts +113 -0
  61. package/src/agents/index.ts +19 -0
  62. package/src/agents/messaging.ts +125 -0
  63. package/src/agents/orchestrator.ts +592 -0
  64. package/src/agents/role-discovery.ts +61 -0
  65. package/src/agents/sub-agent-runner.ts +309 -0
  66. package/src/agents/task-manager.ts +151 -0
  67. package/src/authority/approval-delivery.ts +59 -0
  68. package/src/authority/approval.ts +196 -0
  69. package/src/authority/audit.ts +158 -0
  70. package/src/authority/authority.test.ts +519 -0
  71. package/src/authority/deferred-executor.ts +103 -0
  72. package/src/authority/emergency.ts +66 -0
  73. package/src/authority/engine.ts +301 -0
  74. package/src/authority/index.ts +12 -0
  75. package/src/authority/learning.ts +111 -0
  76. package/src/authority/tool-action-map.ts +74 -0
  77. package/src/awareness/analytics.ts +466 -0
  78. package/src/awareness/awareness.test.ts +332 -0
  79. package/src/awareness/capture-engine.ts +305 -0
  80. package/src/awareness/context-graph.ts +130 -0
  81. package/src/awareness/context-tracker.ts +349 -0
  82. package/src/awareness/index.ts +25 -0
  83. package/src/awareness/intelligence.ts +321 -0
  84. package/src/awareness/ocr-engine.ts +88 -0
  85. package/src/awareness/service.ts +528 -0
  86. package/src/awareness/struggle-detector.ts +342 -0
  87. package/src/awareness/suggestion-engine.ts +476 -0
  88. package/src/awareness/types.ts +201 -0
  89. package/src/cli/autostart.ts +417 -0
  90. package/src/cli/deps.ts +449 -0
  91. package/src/cli/doctor.ts +238 -0
  92. package/src/cli/helpers.ts +401 -0
  93. package/src/cli/onboard.ts +827 -0
  94. package/src/cli/uninstall.test.ts +37 -0
  95. package/src/cli/uninstall.ts +202 -0
  96. package/src/comms/README.md +329 -0
  97. package/src/comms/auth-error.html +48 -0
  98. package/src/comms/channels/discord.ts +228 -0
  99. package/src/comms/channels/signal.ts +56 -0
  100. package/src/comms/channels/telegram.ts +316 -0
  101. package/src/comms/channels/whatsapp.ts +60 -0
  102. package/src/comms/channels.test.ts +173 -0
  103. package/src/comms/dashboard-auth.ts +75 -0
  104. package/src/comms/desktop-notify.ts +114 -0
  105. package/src/comms/example.ts +129 -0
  106. package/src/comms/index.ts +129 -0
  107. package/src/comms/streaming.ts +149 -0
  108. package/src/comms/voice.test.ts +504 -0
  109. package/src/comms/voice.ts +341 -0
  110. package/src/comms/websocket.test.ts +409 -0
  111. package/src/comms/websocket.ts +669 -0
  112. package/src/config/README.md +389 -0
  113. package/src/config/index.ts +6 -0
  114. package/src/config/loader.test.ts +183 -0
  115. package/src/config/loader.ts +148 -0
  116. package/src/config/types.ts +293 -0
  117. package/src/daemon/README.md +232 -0
  118. package/src/daemon/agent-service-interface.ts +9 -0
  119. package/src/daemon/agent-service.ts +667 -0
  120. package/src/daemon/api-routes.ts +3067 -0
  121. package/src/daemon/background-agent-service.ts +396 -0
  122. package/src/daemon/background-agent.test.ts +78 -0
  123. package/src/daemon/channel-service.ts +201 -0
  124. package/src/daemon/commitment-executor.ts +297 -0
  125. package/src/daemon/dashboard-auth.test.ts +170 -0
  126. package/src/daemon/event-classifier.ts +239 -0
  127. package/src/daemon/event-coalescer.ts +123 -0
  128. package/src/daemon/event-reactor.ts +214 -0
  129. package/src/daemon/flock.c +7 -0
  130. package/src/daemon/health.ts +220 -0
  131. package/src/daemon/index.ts +1070 -0
  132. package/src/daemon/llm-settings.test.ts +78 -0
  133. package/src/daemon/llm-settings.ts +450 -0
  134. package/src/daemon/observer-service.ts +150 -0
  135. package/src/daemon/pid.test.ts +283 -0
  136. package/src/daemon/pid.ts +224 -0
  137. package/src/daemon/research-queue.ts +155 -0
  138. package/src/daemon/services.ts +175 -0
  139. package/src/daemon/ws-service.ts +926 -0
  140. package/src/global.d.ts +4 -0
  141. package/src/goals/accountability.ts +240 -0
  142. package/src/goals/awareness-bridge.ts +185 -0
  143. package/src/goals/estimator.ts +185 -0
  144. package/src/goals/events.ts +28 -0
  145. package/src/goals/goals.test.ts +400 -0
  146. package/src/goals/integration.test.ts +329 -0
  147. package/src/goals/nl-builder.test.ts +220 -0
  148. package/src/goals/nl-builder.ts +256 -0
  149. package/src/goals/rhythm.test.ts +177 -0
  150. package/src/goals/rhythm.ts +275 -0
  151. package/src/goals/service.test.ts +135 -0
  152. package/src/goals/service.ts +407 -0
  153. package/src/goals/types.ts +106 -0
  154. package/src/goals/workflow-bridge.ts +96 -0
  155. package/src/integrations/google-api.ts +134 -0
  156. package/src/integrations/google-auth.ts +175 -0
  157. package/src/llm/README.md +291 -0
  158. package/src/llm/anthropic.ts +400 -0
  159. package/src/llm/gemini.ts +380 -0
  160. package/src/llm/groq.ts +406 -0
  161. package/src/llm/history.ts +147 -0
  162. package/src/llm/index.ts +21 -0
  163. package/src/llm/manager.ts +226 -0
  164. package/src/llm/ollama.ts +316 -0
  165. package/src/llm/openai.ts +411 -0
  166. package/src/llm/openrouter.ts +390 -0
  167. package/src/llm/provider.test.ts +487 -0
  168. package/src/llm/provider.ts +61 -0
  169. package/src/llm/test.ts +88 -0
  170. package/src/observers/README.md +278 -0
  171. package/src/observers/calendar.ts +113 -0
  172. package/src/observers/clipboard.ts +136 -0
  173. package/src/observers/email.ts +109 -0
  174. package/src/observers/example.ts +58 -0
  175. package/src/observers/file-watcher.ts +124 -0
  176. package/src/observers/index.ts +159 -0
  177. package/src/observers/notifications.ts +197 -0
  178. package/src/observers/observers.test.ts +203 -0
  179. package/src/observers/processes.ts +225 -0
  180. package/src/personality/README.md +61 -0
  181. package/src/personality/adapter.ts +196 -0
  182. package/src/personality/index.ts +20 -0
  183. package/src/personality/learner.ts +209 -0
  184. package/src/personality/model.ts +132 -0
  185. package/src/personality/personality.test.ts +236 -0
  186. package/src/roles/README.md +252 -0
  187. package/src/roles/authority.ts +120 -0
  188. package/src/roles/example-usage.ts +198 -0
  189. package/src/roles/index.ts +42 -0
  190. package/src/roles/loader.ts +143 -0
  191. package/src/roles/prompt-builder.ts +218 -0
  192. package/src/roles/test-multi.ts +102 -0
  193. package/src/roles/test-role.yaml +77 -0
  194. package/src/roles/test-utils.ts +93 -0
  195. package/src/roles/test.ts +106 -0
  196. package/src/roles/tool-guide.ts +195 -0
  197. package/src/roles/types.ts +36 -0
  198. package/src/roles/utils.ts +200 -0
  199. package/src/scripts/google-setup.ts +168 -0
  200. package/src/sidecar/connection.ts +179 -0
  201. package/src/sidecar/index.ts +6 -0
  202. package/src/sidecar/manager.ts +542 -0
  203. package/src/sidecar/protocol.ts +85 -0
  204. package/src/sidecar/rpc.ts +161 -0
  205. package/src/sidecar/scheduler.ts +136 -0
  206. package/src/sidecar/types.ts +112 -0
  207. package/src/sidecar/validator.ts +144 -0
  208. package/src/sites/builder-tools.ts +215 -0
  209. package/src/sites/dev-server-manager.ts +286 -0
  210. package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
  211. package/src/sites/fixtures/security-test-site/Makefile +15 -0
  212. package/src/sites/fixtures/security-test-site/README.md +18 -0
  213. package/src/sites/fixtures/security-test-site/index.html +12 -0
  214. package/src/sites/fixtures/security-test-site/index.ts +16 -0
  215. package/src/sites/fixtures/security-test-site/package.json +13 -0
  216. package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
  217. package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
  218. package/src/sites/git-manager.ts +240 -0
  219. package/src/sites/github-manager.ts +355 -0
  220. package/src/sites/index.ts +25 -0
  221. package/src/sites/project-manager.ts +389 -0
  222. package/src/sites/proxy.ts +133 -0
  223. package/src/sites/service.ts +136 -0
  224. package/src/sites/templates.ts +169 -0
  225. package/src/sites/types.ts +89 -0
  226. package/src/user/profile-followup.test.ts +84 -0
  227. package/src/user/profile-followup.ts +185 -0
  228. package/src/user/profile.ts +224 -0
  229. package/src/vault/README.md +110 -0
  230. package/src/vault/awareness.ts +341 -0
  231. package/src/vault/commitments.ts +299 -0
  232. package/src/vault/content-pipeline.ts +270 -0
  233. package/src/vault/conversations.ts +173 -0
  234. package/src/vault/dashboard-sessions.ts +44 -0
  235. package/src/vault/documents.ts +130 -0
  236. package/src/vault/entities.ts +185 -0
  237. package/src/vault/extractor.test.ts +356 -0
  238. package/src/vault/extractor.ts +345 -0
  239. package/src/vault/facts.ts +190 -0
  240. package/src/vault/goals.ts +477 -0
  241. package/src/vault/index.ts +87 -0
  242. package/src/vault/keychain.ts +99 -0
  243. package/src/vault/observations.ts +115 -0
  244. package/src/vault/relationships.ts +178 -0
  245. package/src/vault/retrieval.test.ts +139 -0
  246. package/src/vault/retrieval.ts +258 -0
  247. package/src/vault/schema.ts +709 -0
  248. package/src/vault/settings.ts +38 -0
  249. package/src/vault/user-profile.test.ts +113 -0
  250. package/src/vault/user-profile.ts +176 -0
  251. package/src/vault/vectors.ts +92 -0
  252. package/src/vault/webapp-template-seeds.ts +116 -0
  253. package/src/vault/webapp-templates.ts +244 -0
  254. package/src/vault/workflows.ts +403 -0
  255. package/src/workflows/auto-suggest.ts +290 -0
  256. package/src/workflows/engine.ts +366 -0
  257. package/src/workflows/events.ts +24 -0
  258. package/src/workflows/executor.ts +207 -0
  259. package/src/workflows/nl-builder.ts +198 -0
  260. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  261. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  262. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  263. package/src/workflows/nodes/actions/discord.ts +77 -0
  264. package/src/workflows/nodes/actions/file-write.ts +73 -0
  265. package/src/workflows/nodes/actions/gmail.ts +69 -0
  266. package/src/workflows/nodes/actions/http-request.ts +117 -0
  267. package/src/workflows/nodes/actions/notification.ts +85 -0
  268. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  269. package/src/workflows/nodes/actions/send-message.ts +82 -0
  270. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  271. package/src/workflows/nodes/actions/telegram.ts +60 -0
  272. package/src/workflows/nodes/builtin.ts +119 -0
  273. package/src/workflows/nodes/error/error-handler.ts +37 -0
  274. package/src/workflows/nodes/error/fallback.ts +47 -0
  275. package/src/workflows/nodes/error/retry.ts +82 -0
  276. package/src/workflows/nodes/logic/delay.ts +42 -0
  277. package/src/workflows/nodes/logic/if-else.ts +41 -0
  278. package/src/workflows/nodes/logic/loop.ts +90 -0
  279. package/src/workflows/nodes/logic/merge.ts +38 -0
  280. package/src/workflows/nodes/logic/race.ts +40 -0
  281. package/src/workflows/nodes/logic/switch.ts +59 -0
  282. package/src/workflows/nodes/logic/template-render.ts +53 -0
  283. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  284. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  285. package/src/workflows/nodes/registry.ts +99 -0
  286. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  287. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  288. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  289. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  290. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  291. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  292. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  293. package/src/workflows/nodes/triggers/cron.ts +40 -0
  294. package/src/workflows/nodes/triggers/email.ts +40 -0
  295. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  296. package/src/workflows/nodes/triggers/git.ts +46 -0
  297. package/src/workflows/nodes/triggers/manual.ts +23 -0
  298. package/src/workflows/nodes/triggers/poll.ts +81 -0
  299. package/src/workflows/nodes/triggers/process.ts +44 -0
  300. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  301. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  302. package/src/workflows/safe-eval.ts +139 -0
  303. package/src/workflows/template.ts +118 -0
  304. package/src/workflows/triggers/cron.ts +311 -0
  305. package/src/workflows/triggers/manager.ts +285 -0
  306. package/src/workflows/triggers/observer-bridge.ts +172 -0
  307. package/src/workflows/triggers/poller.ts +201 -0
  308. package/src/workflows/triggers/screen-condition.ts +218 -0
  309. package/src/workflows/triggers/triggers.test.ts +740 -0
  310. package/src/workflows/triggers/webhook.ts +191 -0
  311. package/src/workflows/types.ts +133 -0
  312. package/src/workflows/variables.ts +72 -0
  313. package/src/workflows/workflows.test.ts +383 -0
  314. package/src/workflows/yaml.ts +104 -0
  315. package/ui/dist/index-3gr23jt9.js +112614 -0
  316. package/ui/dist/index-9vmj8127.css +14239 -0
  317. package/ui/dist/index-hy9pc1gm.js +112873 -0
  318. package/ui/dist/index-j2ep5d1w.js +112374 -0
  319. package/ui/dist/index-jt00vjqs.js +112858 -0
  320. package/ui/dist/index-k9ymx5qb.js +112374 -0
  321. package/ui/dist/index.html +16 -0
  322. package/ui/public/audio/pcm-capture-processor.js +11 -0
  323. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  324. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  325. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  326. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  327. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  328. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  329. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  330. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,78 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
2
+ import { testLLMProvider } from './llm-settings.ts';
3
+ import { DEFAULT_CONFIG, type JarvisConfig } from '../config/types.ts';
4
+
5
+ function makeConfig(): JarvisConfig {
6
+ return structuredClone(DEFAULT_CONFIG);
7
+ }
8
+
9
+ describe('testLLMProvider', () => {
10
+ const originalFetch = globalThis.fetch;
11
+
12
+ beforeEach(() => {
13
+ globalThis.fetch = mock(async (_url: string) => {
14
+ return new Response(JSON.stringify({
15
+ id: 'cmpl_test',
16
+ object: 'chat.completion',
17
+ created: Date.now(),
18
+ model: 'gpt-test',
19
+ choices: [
20
+ {
21
+ index: 0,
22
+ message: { role: 'assistant', content: 'OK' },
23
+ finish_reason: 'stop',
24
+ },
25
+ ],
26
+ usage: {
27
+ prompt_tokens: 1,
28
+ completion_tokens: 1,
29
+ total_tokens: 2,
30
+ },
31
+ }), {
32
+ status: 200,
33
+ headers: { 'Content-Type': 'application/json' },
34
+ });
35
+ }) as unknown as typeof fetch;
36
+ });
37
+
38
+ afterEach(() => {
39
+ globalThis.fetch = originalFetch;
40
+ });
41
+
42
+ test('uses the official OpenAI endpoint when no base URL is configured', async () => {
43
+ const config = makeConfig();
44
+ const result = await testLLMProvider(
45
+ {
46
+ provider: 'openai',
47
+ api_key: 'sk-test-key',
48
+ model: 'gpt-5.4',
49
+ },
50
+ config,
51
+ );
52
+
53
+ expect(result.ok).toBe(true);
54
+
55
+ const fetchMock = globalThis.fetch as unknown as ReturnType<typeof mock>;
56
+ expect(fetchMock).toHaveBeenCalledTimes(1);
57
+ expect(fetchMock.mock.calls[0]?.[0]).toBe('https://api.openai.com/v1/chat/completions');
58
+ });
59
+
60
+ test('uses custom compatible base URLs without assuming an OpenAI key prefix', async () => {
61
+ const config = makeConfig();
62
+ const result = await testLLMProvider(
63
+ {
64
+ provider: 'openai',
65
+ api_key: 'nvapi-custom-key',
66
+ model: 'gpt-5.4',
67
+ base_url: 'https://gateway.example.com/openai',
68
+ },
69
+ config,
70
+ );
71
+
72
+ expect(result.ok).toBe(true);
73
+
74
+ const fetchMock = globalThis.fetch as unknown as ReturnType<typeof mock>;
75
+ expect(fetchMock).toHaveBeenCalledTimes(1);
76
+ expect(fetchMock.mock.calls[0]?.[0]).toBe('https://gateway.example.com/openai/chat/completions');
77
+ });
78
+ });
@@ -0,0 +1,450 @@
1
+ /**
2
+ * LLM Settings — Bridge between DB settings, encrypted keychain, and in-memory config.
3
+ *
4
+ * Non-secret settings (provider, model, fallback) are stored in the SQLite `settings` table.
5
+ * API keys are stored in the encrypted secrets file via the keychain module.
6
+ */
7
+
8
+ import { getSetting, setSetting, getSettingsByPrefix } from '../vault/settings.ts';
9
+ import { getSecret, setSecret, deleteSecret, hasSecret } from '../vault/keychain.ts';
10
+ import type { JarvisConfig } from '../config/types.ts';
11
+ import { AnthropicProvider } from '../llm/anthropic.ts';
12
+ import { OpenAIProvider } from '../llm/openai.ts';
13
+ import { GroqProvider } from '../llm/groq.ts';
14
+ import { GeminiProvider } from '../llm/gemini.ts';
15
+ import { OllamaProvider } from '../llm/ollama.ts';
16
+ import { OpenRouterProvider } from '../llm/openrouter.ts';
17
+ import type { LLMProvider } from '../llm/provider.ts';
18
+ import type { LLMManager } from '../llm/manager.ts';
19
+
20
+ // Keychain key names
21
+ const KEY_ANTHROPIC = 'llm.anthropic.api_key';
22
+ const KEY_OPENAI = 'llm.openai.api_key';
23
+ const KEY_GROQ = 'llm.groq.api_key';
24
+ const KEY_GEMINI = 'llm.gemini.api_key';
25
+ const KEY_OPENROUTER = 'llm.openrouter.api_key';
26
+
27
+ // DB setting keys
28
+ const SETTING_PRIMARY = 'llm.primary';
29
+ const SETTING_FALLBACK = 'llm.fallback';
30
+ const SETTING_ANTHROPIC_MODEL = 'llm.anthropic.model';
31
+ const SETTING_OPENAI_MODEL = 'llm.openai.model';
32
+ const SETTING_OPENAI_BASE_URL = 'llm.openai.base_url';
33
+ const SETTING_GROQ_MODEL = 'llm.groq.model';
34
+ const SETTING_GEMINI_MODEL = 'llm.gemini.model';
35
+ const SETTING_OLLAMA_MODEL = 'llm.ollama.model';
36
+ const SETTING_OLLAMA_BASE_URL = 'llm.ollama.base_url';
37
+ const SETTING_OPENROUTER_MODEL = 'llm.openrouter.model';
38
+
39
+ export type LLMSettingsResponse = {
40
+ primary: string;
41
+ fallback: string[];
42
+ anthropic: { model: string; has_api_key: boolean } | null;
43
+ openai: { model: string; has_api_key: boolean; base_url?: string } | null;
44
+ groq: { model: string; has_api_key: boolean } | null;
45
+ gemini: { model: string; has_api_key: boolean } | null;
46
+ ollama: { base_url: string; model: string } | null;
47
+ openrouter: { model: string; has_api_key: boolean } | null;
48
+ };
49
+
50
+ /**
51
+ * Read LLM settings from DB + keychain and return a dashboard-safe response.
52
+ * Falls back to in-memory config values for anything not yet saved to DB.
53
+ */
54
+ export function getLLMSettings(config: JarvisConfig): LLMSettingsResponse {
55
+ const primary = getSetting(SETTING_PRIMARY) ?? config.llm.primary;
56
+ const fallbackRaw = getSetting(SETTING_FALLBACK);
57
+ const fallback = fallbackRaw ? JSON.parse(fallbackRaw) : config.llm.fallback;
58
+
59
+ const anthropicModel = getSetting(SETTING_ANTHROPIC_MODEL) ?? config.llm.anthropic?.model ?? 'claude-sonnet-4-6';
60
+ const openaiModel = getSetting(SETTING_OPENAI_MODEL) ?? config.llm.openai?.model ?? 'gpt-5.4';
61
+ const openaiBaseUrl = getSetting(SETTING_OPENAI_BASE_URL) ?? config.llm.openai?.base_url;
62
+ const groqModel = getSetting(SETTING_GROQ_MODEL) ?? config.llm.groq?.model ?? 'llama-3.3-70b-versatile';
63
+ const geminiModel = getSetting(SETTING_GEMINI_MODEL) ?? config.llm.gemini?.model ?? 'gemini-3-flash-preview';
64
+ const ollamaModel = getSetting(SETTING_OLLAMA_MODEL) ?? config.llm.ollama?.model ?? 'llama3';
65
+ const ollamaBaseUrl = getSetting(SETTING_OLLAMA_BASE_URL) ?? config.llm.ollama?.base_url ?? 'http://localhost:11434';
66
+
67
+ const openrouterModel = getSetting(SETTING_OPENROUTER_MODEL) ?? config.llm.openrouter?.model ?? 'anthropic/claude-sonnet-4';
68
+
69
+ const hasAnthropicKey = hasSecret(KEY_ANTHROPIC) || !!config.llm.anthropic?.api_key;
70
+ const hasOpenaiKey = hasSecret(KEY_OPENAI) || !!config.llm.openai?.api_key;
71
+ const hasGroqKey = hasSecret(KEY_GROQ) || !!config.llm.groq?.api_key;
72
+ const hasGeminiKey = hasSecret(KEY_GEMINI) || !!config.llm.gemini?.api_key;
73
+ const hasOpenrouterKey = hasSecret(KEY_OPENROUTER) || !!config.llm.openrouter?.api_key;
74
+
75
+ return {
76
+ primary,
77
+ fallback,
78
+ anthropic: { model: anthropicModel, has_api_key: hasAnthropicKey },
79
+ openai: { model: openaiModel, has_api_key: hasOpenaiKey, ...(openaiBaseUrl ? { base_url: openaiBaseUrl } : {}) },
80
+ groq: { model: groqModel, has_api_key: hasGroqKey },
81
+ gemini: { model: geminiModel, has_api_key: hasGeminiKey },
82
+ ollama: { base_url: ollamaBaseUrl, model: ollamaModel },
83
+ openrouter: { model: openrouterModel, has_api_key: hasOpenrouterKey },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Save LLM settings to DB + keychain and update the in-memory config.
89
+ */
90
+ export function saveLLMSettings(
91
+ config: JarvisConfig,
92
+ body: {
93
+ primary?: string;
94
+ fallback?: string[];
95
+ anthropic?: { api_key?: string; model?: string };
96
+ openai?: { api_key?: string; model?: string; base_url?: string };
97
+ groq?: { api_key?: string; model?: string };
98
+ gemini?: { api_key?: string; model?: string };
99
+ ollama?: { base_url?: string; model?: string };
100
+ openrouter?: { api_key?: string; model?: string };
101
+ },
102
+ ): void {
103
+ // Save non-secret settings to DB
104
+ if (body.primary) {
105
+ setSetting(SETTING_PRIMARY, body.primary);
106
+ config.llm.primary = body.primary;
107
+ }
108
+ if (body.fallback) {
109
+ setSetting(SETTING_FALLBACK, JSON.stringify(body.fallback));
110
+ config.llm.fallback = body.fallback;
111
+ }
112
+
113
+ // Anthropic
114
+ if (body.anthropic) {
115
+ if (body.anthropic.model) {
116
+ setSetting(SETTING_ANTHROPIC_MODEL, body.anthropic.model);
117
+ }
118
+ if (body.anthropic.api_key) {
119
+ setSecret(KEY_ANTHROPIC, body.anthropic.api_key);
120
+ }
121
+ config.llm.anthropic = {
122
+ ...config.llm.anthropic,
123
+ model: body.anthropic.model ?? config.llm.anthropic?.model,
124
+ api_key: body.anthropic.api_key ?? getAnthropicApiKey(config) ?? '',
125
+ };
126
+ }
127
+
128
+ // OpenAI
129
+ if (body.openai) {
130
+ if (body.openai.base_url !== undefined) {
131
+ const trimmedBaseUrl = body.openai.base_url.trim();
132
+ if (trimmedBaseUrl) {
133
+ try {
134
+ new URL(trimmedBaseUrl);
135
+ } catch {
136
+ throw new Error(`Invalid OpenAI base URL: ${trimmedBaseUrl}`);
137
+ }
138
+ }
139
+ }
140
+ if (body.openai.model) {
141
+ setSetting(SETTING_OPENAI_MODEL, body.openai.model);
142
+ }
143
+ if (body.openai.base_url !== undefined) {
144
+ const trimmedBaseUrl = body.openai.base_url.trim();
145
+ if (trimmedBaseUrl) {
146
+ setSetting(SETTING_OPENAI_BASE_URL, trimmedBaseUrl);
147
+ } else {
148
+ setSetting(SETTING_OPENAI_BASE_URL, '');
149
+ }
150
+ }
151
+ if (body.openai.api_key) {
152
+ setSecret(KEY_OPENAI, body.openai.api_key);
153
+ }
154
+ config.llm.openai = {
155
+ ...config.llm.openai,
156
+ model: body.openai.model ?? config.llm.openai?.model,
157
+ base_url: body.openai.base_url !== undefined
158
+ ? (body.openai.base_url.trim() || undefined)
159
+ : config.llm.openai?.base_url,
160
+ api_key: body.openai.api_key ?? getOpenAIApiKey(config) ?? '',
161
+ };
162
+ }
163
+
164
+ // Groq
165
+ if (body.groq) {
166
+ if (body.groq.model) {
167
+ setSetting(SETTING_GROQ_MODEL, body.groq.model);
168
+ }
169
+ if (body.groq.api_key) {
170
+ setSecret(KEY_GROQ, body.groq.api_key);
171
+ }
172
+ config.llm.groq = {
173
+ ...config.llm.groq,
174
+ model: body.groq.model ?? config.llm.groq?.model,
175
+ api_key: body.groq.api_key ?? getGroqApiKey(config) ?? '',
176
+ };
177
+ }
178
+
179
+ // Gemini
180
+ if (body.gemini) {
181
+ if (body.gemini.model) {
182
+ setSetting(SETTING_GEMINI_MODEL, body.gemini.model);
183
+ }
184
+ if (body.gemini.api_key) {
185
+ setSecret(KEY_GEMINI, body.gemini.api_key);
186
+ }
187
+ config.llm.gemini = {
188
+ ...config.llm.gemini,
189
+ model: body.gemini.model ?? config.llm.gemini?.model,
190
+ api_key: body.gemini.api_key ?? getGeminiApiKey(config) ?? '',
191
+ };
192
+ }
193
+
194
+ // Ollama
195
+ if (body.ollama) {
196
+ if (body.ollama.model) {
197
+ setSetting(SETTING_OLLAMA_MODEL, body.ollama.model);
198
+ }
199
+ if (body.ollama.base_url) {
200
+ setSetting(SETTING_OLLAMA_BASE_URL, body.ollama.base_url);
201
+ }
202
+ config.llm.ollama = {
203
+ ...config.llm.ollama,
204
+ model: body.ollama.model ?? config.llm.ollama?.model,
205
+ base_url: body.ollama.base_url ?? config.llm.ollama?.base_url,
206
+ };
207
+ }
208
+
209
+ // OpenRouter
210
+ if (body.openrouter) {
211
+ if (body.openrouter.model) {
212
+ setSetting(SETTING_OPENROUTER_MODEL, body.openrouter.model);
213
+ }
214
+ if (body.openrouter.api_key) {
215
+ setSecret(KEY_OPENROUTER, body.openrouter.api_key);
216
+ }
217
+ config.llm.openrouter = {
218
+ ...config.llm.openrouter,
219
+ model: body.openrouter.model ?? config.llm.openrouter?.model,
220
+ api_key: body.openrouter.api_key ?? getOpenRouterApiKey(config) ?? '',
221
+ };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Resolve the Anthropic API key: keychain > config.yaml > env var.
227
+ */
228
+ function getAnthropicApiKey(config: JarvisConfig): string | null {
229
+ return getSecret(KEY_ANTHROPIC) ?? config.llm.anthropic?.api_key ?? null;
230
+ }
231
+
232
+ /**
233
+ * Resolve the OpenAI API key: keychain > config.yaml > env var.
234
+ */
235
+ function getOpenAIApiKey(config: JarvisConfig): string | null {
236
+ return getSecret(KEY_OPENAI) ?? config.llm.openai?.api_key ?? null;
237
+ }
238
+
239
+ /**
240
+ * Resolve the Groq API key: keychain > config.yaml > env var.
241
+ */
242
+ function getGroqApiKey(config: JarvisConfig): string | null {
243
+ return getSecret(KEY_GROQ) ?? config.llm.groq?.api_key ?? null;
244
+ }
245
+
246
+ /**
247
+ * Resolve the Gemini API key: keychain > config.yaml > env var.
248
+ */
249
+ function getGeminiApiKey(config: JarvisConfig): string | null {
250
+ return getSecret(KEY_GEMINI) ?? config.llm.gemini?.api_key ?? null;
251
+ }
252
+
253
+ /**
254
+ * Resolve the OpenRouter API key: keychain > config.yaml > env var.
255
+ */
256
+ function getOpenRouterApiKey(config: JarvisConfig): string | null {
257
+ return getSecret(KEY_OPENROUTER) ?? config.llm.openrouter?.api_key ?? null;
258
+ }
259
+
260
+ /**
261
+ * Merge DB/keychain LLM settings into config at startup.
262
+ * Env vars (already applied by loadConfig) take priority over DB values.
263
+ */
264
+ export function mergeLLMSettingsIntoConfig(config: JarvisConfig): void {
265
+ // Only override from DB if env vars are NOT set
266
+ const dbPrimary = getSetting(SETTING_PRIMARY);
267
+ if (dbPrimary) config.llm.primary = dbPrimary;
268
+
269
+ const dbFallback = getSetting(SETTING_FALLBACK);
270
+ if (dbFallback) config.llm.fallback = JSON.parse(dbFallback);
271
+
272
+ // Anthropic
273
+ const dbAnthropicModel = getSetting(SETTING_ANTHROPIC_MODEL);
274
+ const keychainAnthropicKey = getSecret(KEY_ANTHROPIC);
275
+ if (dbAnthropicModel || keychainAnthropicKey) {
276
+ config.llm.anthropic = {
277
+ ...config.llm.anthropic,
278
+ api_key: (!process.env.JARVIS_API_KEY && keychainAnthropicKey)
279
+ ? keychainAnthropicKey
280
+ : (config.llm.anthropic?.api_key ?? ''),
281
+ model: dbAnthropicModel ?? config.llm.anthropic?.model,
282
+ };
283
+ }
284
+
285
+ // OpenAI
286
+ const dbOpenaiModel = getSetting(SETTING_OPENAI_MODEL);
287
+ const dbOpenaiBaseUrl = getSetting(SETTING_OPENAI_BASE_URL);
288
+ const keychainOpenaiKey = getSecret(KEY_OPENAI);
289
+ if (dbOpenaiModel || dbOpenaiBaseUrl !== null || keychainOpenaiKey) {
290
+ config.llm.openai = {
291
+ ...config.llm.openai,
292
+ api_key: (!process.env.JARVIS_OPENAI_KEY && keychainOpenaiKey)
293
+ ? keychainOpenaiKey
294
+ : (config.llm.openai?.api_key ?? ''),
295
+ model: dbOpenaiModel ?? config.llm.openai?.model,
296
+ base_url: dbOpenaiBaseUrl !== null
297
+ ? (dbOpenaiBaseUrl || undefined)
298
+ : config.llm.openai?.base_url,
299
+ };
300
+ }
301
+
302
+ // Groq
303
+ const dbGroqModel = getSetting(SETTING_GROQ_MODEL);
304
+ const keychainGroqKey = getSecret(KEY_GROQ);
305
+ if (dbGroqModel || keychainGroqKey) {
306
+ config.llm.groq = {
307
+ ...config.llm.groq,
308
+ api_key: (!process.env.JARVIS_GROQ_KEY && keychainGroqKey)
309
+ ? keychainGroqKey
310
+ : (config.llm.groq?.api_key ?? ''),
311
+ model: dbGroqModel ?? config.llm.groq?.model,
312
+ };
313
+ }
314
+
315
+ // Gemini
316
+ const dbGeminiModel = getSetting(SETTING_GEMINI_MODEL);
317
+ const keychainGeminiKey = getSecret(KEY_GEMINI);
318
+ if (dbGeminiModel || keychainGeminiKey) {
319
+ config.llm.gemini = {
320
+ ...config.llm.gemini,
321
+ api_key: (!process.env.JARVIS_GEMINI_KEY && keychainGeminiKey)
322
+ ? keychainGeminiKey
323
+ : (config.llm.gemini?.api_key ?? ''),
324
+ model: dbGeminiModel ?? config.llm.gemini?.model,
325
+ };
326
+ }
327
+
328
+ // Ollama
329
+ const dbOllamaModel = getSetting(SETTING_OLLAMA_MODEL);
330
+ const dbOllamaUrl = getSetting(SETTING_OLLAMA_BASE_URL);
331
+ if (dbOllamaModel || dbOllamaUrl) {
332
+ config.llm.ollama = {
333
+ ...config.llm.ollama,
334
+ model: dbOllamaModel ?? config.llm.ollama?.model,
335
+ base_url: (!process.env.JARVIS_OLLAMA_URL && dbOllamaUrl)
336
+ ? dbOllamaUrl
337
+ : (config.llm.ollama?.base_url ?? 'http://localhost:11434'),
338
+ };
339
+ }
340
+
341
+ // OpenRouter
342
+ const dbOpenrouterModel = getSetting(SETTING_OPENROUTER_MODEL);
343
+ const keychainOpenrouterKey = getSecret(KEY_OPENROUTER);
344
+ if (dbOpenrouterModel || keychainOpenrouterKey) {
345
+ config.llm.openrouter = {
346
+ ...config.llm.openrouter,
347
+ api_key: (!process.env.JARVIS_OPENROUTER_KEY && keychainOpenrouterKey)
348
+ ? keychainOpenrouterKey
349
+ : (config.llm.openrouter?.api_key ?? ''),
350
+ model: dbOpenrouterModel ?? config.llm.openrouter?.model,
351
+ };
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Build fresh LLM provider instances from the current config and hot-reload them
357
+ * into the shared LLMManager (atomic swap, safe for in-flight requests).
358
+ */
359
+ export function hotReloadLLMProviders(config: JarvisConfig, llmManager: LLMManager): void {
360
+ const { llm } = config;
361
+ const providers: LLMProvider[] = [];
362
+
363
+ if (llm.anthropic?.api_key) {
364
+ providers.push(new AnthropicProvider(llm.anthropic.api_key, llm.anthropic.model));
365
+ console.log('[LLM] Hot-reloaded Anthropic provider');
366
+ }
367
+ if (llm.openai?.api_key) {
368
+ providers.push(new OpenAIProvider(llm.openai.api_key, llm.openai.model, llm.openai.base_url));
369
+ console.log('[LLM] Hot-reloaded OpenAI provider');
370
+ }
371
+ if (llm.groq?.api_key) {
372
+ providers.push(new GroqProvider(llm.groq.api_key, llm.groq.model));
373
+ console.log('[LLM] Hot-reloaded Groq provider');
374
+ }
375
+ if (llm.gemini?.api_key) {
376
+ providers.push(new GeminiProvider(llm.gemini.api_key, llm.gemini.model));
377
+ console.log('[LLM] Hot-reloaded Gemini provider');
378
+ }
379
+ if (llm.openrouter?.api_key) {
380
+ providers.push(new OpenRouterProvider(llm.openrouter.api_key, llm.openrouter.model));
381
+ console.log('[LLM] Hot-reloaded OpenRouter provider');
382
+ }
383
+ if (llm.ollama) {
384
+ providers.push(new OllamaProvider(llm.ollama.base_url, llm.ollama.model));
385
+ console.log('[LLM] Hot-reloaded Ollama provider');
386
+ }
387
+
388
+ const fallback = llm.fallback.filter(n => providers.some(p => p.name === n));
389
+ llmManager.replaceProviders(providers, llm.primary, fallback);
390
+ console.log(`[LLM] Providers active: ${providers.map(p => p.name).join(', ') || 'none'} (primary: ${llm.primary})`);
391
+ }
392
+
393
+ /**
394
+ * Test an LLM provider connection. Uses provided credentials if given,
395
+ * otherwise falls back to stored keys (keychain > config).
396
+ */
397
+ export async function testLLMProvider(
398
+ opts: {
399
+ provider: string;
400
+ api_key?: string;
401
+ model?: string;
402
+ base_url?: string;
403
+ },
404
+ config: JarvisConfig,
405
+ ): Promise<{ ok: boolean; model?: string; error?: string }> {
406
+ try {
407
+ let instance: LLMProvider;
408
+
409
+ if (opts.provider === 'anthropic') {
410
+ const key = opts.api_key || getSecret(KEY_ANTHROPIC) || config.llm.anthropic?.api_key;
411
+ if (!key) return { ok: false, error: 'API key required' };
412
+ instance = new AnthropicProvider(key, opts.model ?? config.llm.anthropic?.model);
413
+ } else if (opts.provider === 'openai') {
414
+ const key = opts.api_key || getSecret(KEY_OPENAI) || config.llm.openai?.api_key;
415
+ if (!key) return { ok: false, error: 'API key required' };
416
+ instance = new OpenAIProvider(
417
+ key,
418
+ opts.model ?? config.llm.openai?.model,
419
+ opts.base_url ?? config.llm.openai?.base_url,
420
+ );
421
+ } else if (opts.provider === 'groq') {
422
+ const key = opts.api_key || getSecret(KEY_GROQ) || config.llm.groq?.api_key;
423
+ if (!key) return { ok: false, error: 'API key required' };
424
+ instance = new GroqProvider(key, opts.model ?? config.llm.groq?.model);
425
+ } else if (opts.provider === 'gemini') {
426
+ const key = opts.api_key || config.llm.gemini?.api_key;
427
+ if (!key) return { ok: false, error: 'API key required' };
428
+ instance = new GeminiProvider(key, opts.model ?? config.llm.gemini?.model);
429
+ } else if (opts.provider === 'openrouter') {
430
+ const key = opts.api_key || getSecret(KEY_OPENROUTER) || config.llm.openrouter?.api_key;
431
+ if (!key) return { ok: false, error: 'API key required' };
432
+ instance = new OpenRouterProvider(key, opts.model ?? config.llm.openrouter?.model);
433
+ } else if (opts.provider === 'ollama') {
434
+ instance = new OllamaProvider(
435
+ opts.base_url ?? config.llm.ollama?.base_url,
436
+ opts.model ?? config.llm.ollama?.model,
437
+ );
438
+ } else {
439
+ return { ok: false, error: `Unknown provider: ${opts.provider}` };
440
+ }
441
+
442
+ const resp = await instance.chat(
443
+ [{ role: 'user', content: 'Say OK' }],
444
+ { max_tokens: 5 },
445
+ );
446
+ return { ok: true, model: resp.model };
447
+ } catch (err) {
448
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
449
+ }
450
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Observer Service — The Eyes
3
+ *
4
+ * Wraps ObserverManager. Registers system observers (file watcher,
5
+ * clipboard monitor, process monitor, email, calendar, notifications)
6
+ * and routes events to the vault.
7
+ * Also classifies events and routes them to the EventReactor (immediate)
8
+ * or EventCoalescer (batched for heartbeat).
9
+ */
10
+
11
+ import type { Service, ServiceStatus } from './services.ts';
12
+ import type { ObserverEvent } from '../observers/index.ts';
13
+ import type { ObservationType } from '../vault/observations.ts';
14
+ import type { EventReactor } from './event-reactor.ts';
15
+ import type { EventCoalescer } from './event-coalescer.ts';
16
+ import type { GoogleAuth } from '../integrations/google-auth.ts';
17
+
18
+ import { homedir } from 'node:os';
19
+ import {
20
+ ObserverManager,
21
+ FileWatcher,
22
+ ClipboardMonitor,
23
+ ProcessMonitor,
24
+ } from '../observers/index.ts';
25
+ import { EmailSync } from '../observers/email.ts';
26
+ import { CalendarSync } from '../observers/calendar.ts';
27
+ import { NotificationListener } from '../observers/notifications.ts';
28
+ import { createObservation } from '../vault/observations.ts';
29
+ import { classifyEvent } from './event-classifier.ts';
30
+
31
+ /**
32
+ * Map observer event types to vault observation types.
33
+ */
34
+ function mapEventType(eventType: string): ObservationType {
35
+ switch (eventType) {
36
+ case 'file_change':
37
+ return 'file_change';
38
+ case 'clipboard':
39
+ return 'clipboard';
40
+ case 'process_started':
41
+ case 'process_stopped':
42
+ return 'process';
43
+ case 'notification':
44
+ return 'notification';
45
+ case 'calendar':
46
+ return 'calendar';
47
+ case 'email':
48
+ return 'email';
49
+ case 'browser':
50
+ return 'browser';
51
+ case 'screen_capture':
52
+ case 'context_changed':
53
+ case 'error_detected':
54
+ case 'stuck_detected':
55
+ case 'session_started':
56
+ case 'session_ended':
57
+ case 'suggestion_ready':
58
+ return 'screen_capture';
59
+ default:
60
+ return 'app_activity';
61
+ }
62
+ }
63
+
64
+ export class ObserverService implements Service {
65
+ name = 'observers';
66
+ private _status: ServiceStatus = 'stopped';
67
+ private manager: ObserverManager;
68
+ private reactor: EventReactor | null;
69
+ private coalescer: EventCoalescer | null;
70
+ private googleAuth: GoogleAuth | null;
71
+
72
+ constructor(reactor?: EventReactor, coalescer?: EventCoalescer, googleAuth?: GoogleAuth) {
73
+ this.manager = new ObserverManager();
74
+ this.reactor = reactor ?? null;
75
+ this.coalescer = coalescer ?? null;
76
+ this.googleAuth = googleAuth ?? null;
77
+ }
78
+
79
+ async start(): Promise<void> {
80
+ this._status = 'starting';
81
+
82
+ try {
83
+ // Register core observers
84
+ this.manager.register(new FileWatcher([homedir()]));
85
+ this.manager.register(new ClipboardMonitor());
86
+ this.manager.register(new ProcessMonitor());
87
+
88
+ // Register D-Bus notification observer (Linux/WSL2)
89
+ this.manager.register(new NotificationListener());
90
+
91
+ // Register Gmail observer (if Google auth available)
92
+ this.manager.register(new EmailSync(this.googleAuth ?? undefined));
93
+
94
+ // Register Calendar observer (if Google auth available)
95
+ this.manager.register(new CalendarSync(this.googleAuth ?? undefined));
96
+
97
+ // Set event handler: store in vault + classify + route
98
+ this.manager.setEventHandler((event: ObserverEvent) => {
99
+ // 1. Always store in vault
100
+ try {
101
+ const obsType = mapEventType(event.type);
102
+ createObservation(obsType, event.data);
103
+ } catch (err) {
104
+ console.error('[ObserverService] Error storing observation:', err);
105
+ }
106
+
107
+ // 2. Classify and route
108
+ try {
109
+ const classified = classifyEvent(event);
110
+
111
+ if (classified.priority === 'critical' || classified.priority === 'high') {
112
+ // Route to reactor for immediate handling
113
+ if (this.reactor) {
114
+ this.reactor.react(classified).catch(err =>
115
+ console.error('[ObserverService] Reactor error:', err)
116
+ );
117
+ }
118
+ } else {
119
+ // Route to coalescer for batched delivery at heartbeat
120
+ if (this.coalescer) {
121
+ this.coalescer.addEvent(classified);
122
+ }
123
+ }
124
+ } catch (err) {
125
+ console.error('[ObserverService] Error classifying event:', err);
126
+ }
127
+ });
128
+
129
+ // Start all observers (individual failures don't crash the service)
130
+ await this.manager.startAll();
131
+
132
+ this._status = 'running';
133
+ console.log('[ObserverService] Started');
134
+ } catch (error) {
135
+ this._status = 'error';
136
+ throw error;
137
+ }
138
+ }
139
+
140
+ async stop(): Promise<void> {
141
+ this._status = 'stopping';
142
+ await this.manager.stopAll();
143
+ this._status = 'stopped';
144
+ console.log('[ObserverService] Stopped');
145
+ }
146
+
147
+ status(): ServiceStatus {
148
+ return this._status;
149
+ }
150
+ }