@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,316 @@
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 { GeminiProvider } from '../llm/gemini.ts';
14
+ import { OllamaProvider } from '../llm/ollama.ts';
15
+ import type { LLMProvider } from '../llm/provider.ts';
16
+ import type { LLMManager } from '../llm/manager.ts';
17
+
18
+ // Keychain key names
19
+ const KEY_ANTHROPIC = 'llm.anthropic.api_key';
20
+ const KEY_OPENAI = 'llm.openai.api_key';
21
+ const KEY_GEMINI = 'llm.gemini.api_key';
22
+
23
+ // DB setting keys
24
+ const SETTING_PRIMARY = 'llm.primary';
25
+ const SETTING_FALLBACK = 'llm.fallback';
26
+ const SETTING_ANTHROPIC_MODEL = 'llm.anthropic.model';
27
+ const SETTING_OPENAI_MODEL = 'llm.openai.model';
28
+ const SETTING_GEMINI_MODEL = 'llm.gemini.model';
29
+ const SETTING_OLLAMA_MODEL = 'llm.ollama.model';
30
+ const SETTING_OLLAMA_BASE_URL = 'llm.ollama.base_url';
31
+
32
+ export type LLMSettingsResponse = {
33
+ primary: string;
34
+ fallback: string[];
35
+ anthropic: { model: string; has_api_key: boolean } | null;
36
+ openai: { model: string; has_api_key: boolean } | null;
37
+ gemini: { model: string; has_api_key: boolean } | null;
38
+ ollama: { base_url: string; model: string } | null;
39
+ };
40
+
41
+ /**
42
+ * Read LLM settings from DB + keychain and return a dashboard-safe response.
43
+ * Falls back to in-memory config values for anything not yet saved to DB.
44
+ */
45
+ export function getLLMSettings(config: JarvisConfig): LLMSettingsResponse {
46
+ const primary = getSetting(SETTING_PRIMARY) ?? config.llm.primary;
47
+ const fallbackRaw = getSetting(SETTING_FALLBACK);
48
+ const fallback = fallbackRaw ? JSON.parse(fallbackRaw) : config.llm.fallback;
49
+
50
+ const anthropicModel = getSetting(SETTING_ANTHROPIC_MODEL) ?? config.llm.anthropic?.model ?? 'claude-sonnet-4-6';
51
+ const openaiModel = getSetting(SETTING_OPENAI_MODEL) ?? config.llm.openai?.model ?? 'gpt-5.4';
52
+ const geminiModel = getSetting(SETTING_GEMINI_MODEL) ?? config.llm.gemini?.model ?? 'gemini-3-flash-preview';
53
+ const ollamaModel = getSetting(SETTING_OLLAMA_MODEL) ?? config.llm.ollama?.model ?? 'llama3';
54
+ const ollamaBaseUrl = getSetting(SETTING_OLLAMA_BASE_URL) ?? config.llm.ollama?.base_url ?? 'http://localhost:11434';
55
+
56
+ const hasAnthropicKey = hasSecret(KEY_ANTHROPIC) || !!config.llm.anthropic?.api_key;
57
+ const hasOpenaiKey = hasSecret(KEY_OPENAI) || !!config.llm.openai?.api_key;
58
+ const hasGeminiKey = hasSecret(KEY_GEMINI) || !!config.llm.gemini?.api_key;
59
+
60
+ return {
61
+ primary,
62
+ fallback,
63
+ anthropic: { model: anthropicModel, has_api_key: hasAnthropicKey },
64
+ openai: { model: openaiModel, has_api_key: hasOpenaiKey },
65
+ gemini: { model: geminiModel, has_api_key: hasGeminiKey },
66
+ ollama: { base_url: ollamaBaseUrl, model: ollamaModel },
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Save LLM settings to DB + keychain and update the in-memory config.
72
+ */
73
+ export function saveLLMSettings(
74
+ config: JarvisConfig,
75
+ body: {
76
+ primary?: string;
77
+ fallback?: string[];
78
+ anthropic?: { api_key?: string; model?: string };
79
+ openai?: { api_key?: string; model?: string };
80
+ gemini?: { api_key?: string; model?: string };
81
+ ollama?: { base_url?: string; model?: string };
82
+ },
83
+ ): void {
84
+ // Save non-secret settings to DB
85
+ if (body.primary) {
86
+ setSetting(SETTING_PRIMARY, body.primary);
87
+ config.llm.primary = body.primary;
88
+ }
89
+ if (body.fallback) {
90
+ setSetting(SETTING_FALLBACK, JSON.stringify(body.fallback));
91
+ config.llm.fallback = body.fallback;
92
+ }
93
+
94
+ // Anthropic
95
+ if (body.anthropic) {
96
+ if (body.anthropic.model) {
97
+ setSetting(SETTING_ANTHROPIC_MODEL, body.anthropic.model);
98
+ }
99
+ if (body.anthropic.api_key) {
100
+ setSecret(KEY_ANTHROPIC, body.anthropic.api_key);
101
+ }
102
+ config.llm.anthropic = {
103
+ ...config.llm.anthropic,
104
+ model: body.anthropic.model ?? config.llm.anthropic?.model,
105
+ api_key: body.anthropic.api_key ?? getAnthropicApiKey(config) ?? '',
106
+ };
107
+ }
108
+
109
+ // OpenAI
110
+ if (body.openai) {
111
+ if (body.openai.model) {
112
+ setSetting(SETTING_OPENAI_MODEL, body.openai.model);
113
+ }
114
+ if (body.openai.api_key) {
115
+ setSecret(KEY_OPENAI, body.openai.api_key);
116
+ }
117
+ config.llm.openai = {
118
+ ...config.llm.openai,
119
+ model: body.openai.model ?? config.llm.openai?.model,
120
+ api_key: body.openai.api_key ?? getOpenAIApiKey(config) ?? '',
121
+ };
122
+ }
123
+
124
+ // Gemini
125
+ if (body.gemini) {
126
+ if (body.gemini.model) {
127
+ setSetting(SETTING_GEMINI_MODEL, body.gemini.model);
128
+ }
129
+ if (body.gemini.api_key) {
130
+ setSecret(KEY_GEMINI, body.gemini.api_key);
131
+ }
132
+ config.llm.gemini = {
133
+ ...config.llm.gemini,
134
+ model: body.gemini.model ?? config.llm.gemini?.model,
135
+ api_key: body.gemini.api_key ?? getGeminiApiKey(config) ?? '',
136
+ };
137
+ }
138
+
139
+ // Ollama
140
+ if (body.ollama) {
141
+ if (body.ollama.model) {
142
+ setSetting(SETTING_OLLAMA_MODEL, body.ollama.model);
143
+ }
144
+ if (body.ollama.base_url) {
145
+ setSetting(SETTING_OLLAMA_BASE_URL, body.ollama.base_url);
146
+ }
147
+ config.llm.ollama = {
148
+ ...config.llm.ollama,
149
+ model: body.ollama.model ?? config.llm.ollama?.model,
150
+ base_url: body.ollama.base_url ?? config.llm.ollama?.base_url,
151
+ };
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Resolve the Anthropic API key: keychain > config.yaml > env var.
157
+ */
158
+ function getAnthropicApiKey(config: JarvisConfig): string | null {
159
+ return getSecret(KEY_ANTHROPIC) ?? config.llm.anthropic?.api_key ?? null;
160
+ }
161
+
162
+ /**
163
+ * Resolve the OpenAI API key: keychain > config.yaml > env var.
164
+ */
165
+ function getOpenAIApiKey(config: JarvisConfig): string | null {
166
+ return getSecret(KEY_OPENAI) ?? config.llm.openai?.api_key ?? null;
167
+ }
168
+
169
+ /**
170
+ * Resolve the Gemini API key: keychain > config.yaml > env var.
171
+ */
172
+ function getGeminiApiKey(config: JarvisConfig): string | null {
173
+ return getSecret(KEY_GEMINI) ?? config.llm.gemini?.api_key ?? null;
174
+ }
175
+
176
+ /**
177
+ * Merge DB/keychain LLM settings into config at startup.
178
+ * Env vars (already applied by loadConfig) take priority over DB values.
179
+ */
180
+ export function mergeLLMSettingsIntoConfig(config: JarvisConfig): void {
181
+ // Only override from DB if env vars are NOT set
182
+ const dbPrimary = getSetting(SETTING_PRIMARY);
183
+ if (dbPrimary) config.llm.primary = dbPrimary;
184
+
185
+ const dbFallback = getSetting(SETTING_FALLBACK);
186
+ if (dbFallback) config.llm.fallback = JSON.parse(dbFallback);
187
+
188
+ // Anthropic
189
+ const dbAnthropicModel = getSetting(SETTING_ANTHROPIC_MODEL);
190
+ const keychainAnthropicKey = getSecret(KEY_ANTHROPIC);
191
+ if (dbAnthropicModel || keychainAnthropicKey) {
192
+ config.llm.anthropic = {
193
+ ...config.llm.anthropic,
194
+ api_key: (!process.env.JARVIS_API_KEY && keychainAnthropicKey)
195
+ ? keychainAnthropicKey
196
+ : (config.llm.anthropic?.api_key ?? ''),
197
+ model: dbAnthropicModel ?? config.llm.anthropic?.model,
198
+ };
199
+ }
200
+
201
+ // OpenAI
202
+ const dbOpenaiModel = getSetting(SETTING_OPENAI_MODEL);
203
+ const keychainOpenaiKey = getSecret(KEY_OPENAI);
204
+ if (dbOpenaiModel || keychainOpenaiKey) {
205
+ config.llm.openai = {
206
+ ...config.llm.openai,
207
+ api_key: (!process.env.JARVIS_OPENAI_KEY && keychainOpenaiKey)
208
+ ? keychainOpenaiKey
209
+ : (config.llm.openai?.api_key ?? ''),
210
+ model: dbOpenaiModel ?? config.llm.openai?.model,
211
+ };
212
+ }
213
+
214
+ // Gemini
215
+ const dbGeminiModel = getSetting(SETTING_GEMINI_MODEL);
216
+ const keychainGeminiKey = getSecret(KEY_GEMINI);
217
+ if (dbGeminiModel || keychainGeminiKey) {
218
+ config.llm.gemini = {
219
+ ...config.llm.gemini,
220
+ api_key: (!process.env.JARVIS_GEMINI_KEY && keychainGeminiKey)
221
+ ? keychainGeminiKey
222
+ : (config.llm.gemini?.api_key ?? ''),
223
+ model: dbGeminiModel ?? config.llm.gemini?.model,
224
+ };
225
+ }
226
+
227
+ // Ollama
228
+ const dbOllamaModel = getSetting(SETTING_OLLAMA_MODEL);
229
+ const dbOllamaUrl = getSetting(SETTING_OLLAMA_BASE_URL);
230
+ if (dbOllamaModel || dbOllamaUrl) {
231
+ config.llm.ollama = {
232
+ ...config.llm.ollama,
233
+ model: dbOllamaModel ?? config.llm.ollama?.model,
234
+ base_url: (!process.env.JARVIS_OLLAMA_URL && dbOllamaUrl)
235
+ ? dbOllamaUrl
236
+ : (config.llm.ollama?.base_url ?? 'http://localhost:11434'),
237
+ };
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Build fresh LLM provider instances from the current config and hot-reload them
243
+ * into the shared LLMManager (atomic swap, safe for in-flight requests).
244
+ */
245
+ export function hotReloadLLMProviders(config: JarvisConfig, llmManager: LLMManager): void {
246
+ const { llm } = config;
247
+ const providers: LLMProvider[] = [];
248
+
249
+ if (llm.anthropic?.api_key) {
250
+ providers.push(new AnthropicProvider(llm.anthropic.api_key, llm.anthropic.model));
251
+ console.log('[LLM] Hot-reloaded Anthropic provider');
252
+ }
253
+ if (llm.openai?.api_key) {
254
+ providers.push(new OpenAIProvider(llm.openai.api_key, llm.openai.model));
255
+ console.log('[LLM] Hot-reloaded OpenAI provider');
256
+ }
257
+ if (llm.gemini?.api_key) {
258
+ providers.push(new GeminiProvider(llm.gemini.api_key, llm.gemini.model));
259
+ console.log('[LLM] Hot-reloaded Gemini provider');
260
+ }
261
+ if (llm.ollama) {
262
+ providers.push(new OllamaProvider(llm.ollama.base_url, llm.ollama.model));
263
+ console.log('[LLM] Hot-reloaded Ollama provider');
264
+ }
265
+
266
+ const fallback = llm.fallback.filter(n => providers.some(p => p.name === n));
267
+ llmManager.replaceProviders(providers, llm.primary, fallback);
268
+ console.log(`[LLM] Providers active: ${providers.map(p => p.name).join(', ') || 'none'} (primary: ${llm.primary})`);
269
+ }
270
+
271
+ /**
272
+ * Test an LLM provider connection. Uses provided credentials if given,
273
+ * otherwise falls back to stored keys (keychain > config).
274
+ */
275
+ export async function testLLMProvider(
276
+ opts: {
277
+ provider: string;
278
+ api_key?: string;
279
+ model?: string;
280
+ base_url?: string;
281
+ },
282
+ config: JarvisConfig,
283
+ ): Promise<{ ok: boolean; model?: string; error?: string }> {
284
+ try {
285
+ let instance: LLMProvider;
286
+
287
+ if (opts.provider === 'anthropic') {
288
+ const key = opts.api_key || getSecret(KEY_ANTHROPIC) || config.llm.anthropic?.api_key;
289
+ if (!key) return { ok: false, error: 'API key required' };
290
+ instance = new AnthropicProvider(key, opts.model ?? config.llm.anthropic?.model);
291
+ } else if (opts.provider === 'openai') {
292
+ const key = opts.api_key || getSecret(KEY_OPENAI) || config.llm.openai?.api_key;
293
+ if (!key) return { ok: false, error: 'API key required' };
294
+ instance = new OpenAIProvider(key, opts.model ?? config.llm.openai?.model);
295
+ } else if (opts.provider === 'gemini') {
296
+ const key = opts.api_key || config.llm.gemini?.api_key;
297
+ if (!key) return { ok: false, error: 'API key required' };
298
+ instance = new GeminiProvider(key, opts.model ?? config.llm.gemini?.model);
299
+ } else if (opts.provider === 'ollama') {
300
+ instance = new OllamaProvider(
301
+ opts.base_url ?? config.llm.ollama?.base_url,
302
+ opts.model ?? config.llm.ollama?.model,
303
+ );
304
+ } else {
305
+ return { ok: false, error: `Unknown provider: ${opts.provider}` };
306
+ }
307
+
308
+ const resp = await instance.chat(
309
+ [{ role: 'user', content: 'Say OK' }],
310
+ { max_tokens: 5 },
311
+ );
312
+ return { ok: true, model: resp.model };
313
+ } catch (err) {
314
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
315
+ }
316
+ }
@@ -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
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * PID File Manager for J.A.R.V.I.S. Daemon
3
+ *
4
+ * Manages the daemon PID file at ~/.jarvis/jarvis.pid
5
+ * for start/stop/status lifecycle commands.
6
+ */
7
+
8
+ import { join } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
11
+
12
+ const JARVIS_DIR = join(homedir(), '.jarvis');
13
+ const LOG_DIR = join(JARVIS_DIR, 'logs');
14
+ const PID_PATH = join(JARVIS_DIR, 'jarvis.pid');
15
+ const LOG_PATH = join(LOG_DIR, 'jarvis.log');
16
+
17
+ /**
18
+ * Write the current daemon PID to the PID file.
19
+ */
20
+ export function writePid(pid: number): void {
21
+ try {
22
+ mkdirSync(JARVIS_DIR, { recursive: true }); // idempotent, no race
23
+ writeFileSync(PID_PATH, String(pid), 'utf-8');
24
+ } catch (err) {
25
+ console.error(`[PID] Failed to write PID file: ${err}`);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Read the PID from the PID file. Returns null if no PID file exists.
31
+ */
32
+ export function readPid(): number | null {
33
+ if (!existsSync(PID_PATH)) return null;
34
+ try {
35
+ const content = readFileSync(PID_PATH, 'utf-8').trim();
36
+ const pid = parseInt(content, 10);
37
+ if (isNaN(pid) || pid <= 0) return null;
38
+ return pid;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Clear (delete) the PID file.
46
+ */
47
+ export function clearPid(): void {
48
+ try {
49
+ if (existsSync(PID_PATH)) {
50
+ unlinkSync(PID_PATH);
51
+ }
52
+ } catch (err) {
53
+ // Log but don't crash — file may already be gone or permissions issue
54
+ console.warn(`[PID] Could not remove PID file: ${err}`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Check if a daemon process is currently running.
60
+ * Returns the PID if running, null otherwise.
61
+ * Also cleans up stale PID files.
62
+ */
63
+ export function isRunning(): number | null {
64
+ const pid = readPid();
65
+ if (pid === null) return null;
66
+
67
+ try {
68
+ // signal 0 doesn't kill the process — just checks if it exists
69
+ process.kill(pid, 0);
70
+ return pid;
71
+ } catch {
72
+ // Process doesn't exist — stale PID file
73
+ clearPid();
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get the PID file path (for display purposes).
80
+ */
81
+ export function getPidPath(): string {
82
+ return PID_PATH;
83
+ }
84
+
85
+ /**
86
+ * Get the log file path. Creates the log directory if needed.
87
+ */
88
+ export function getLogPath(): string {
89
+ mkdirSync(LOG_DIR, { recursive: true }); // idempotent
90
+ return LOG_PATH;
91
+ }
92
+
93
+ /**
94
+ * Get the log directory path.
95
+ */
96
+ export function getLogDir(): string {
97
+ return LOG_DIR;
98
+ }