@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,214 @@
1
+ /**
2
+ * Event Reactor — Immediate Response Engine
3
+ *
4
+ * Handles critical/high priority events by sending them to the agent
5
+ * as synthetic messages with full tool access. Includes cooldown and
6
+ * deduplication to prevent reaction storms.
7
+ */
8
+
9
+ import type { ClassifiedEvent } from './event-classifier.ts';
10
+ import type { IAgentService } from './agent-service-interface.ts';
11
+
12
+ export type ReactorConfig = {
13
+ /** Max reactions per event type within the cooldown window */
14
+ maxPerType: number;
15
+ /** Cooldown window per event type in ms (default: 60s) */
16
+ typeCooldownMs: number;
17
+ /** Global max reactions within the global window */
18
+ globalMax: number;
19
+ /** Global cooldown window in ms (default: 10 min) */
20
+ globalWindowMs: number;
21
+ };
22
+
23
+ const DEFAULT_CONFIG: ReactorConfig = {
24
+ maxPerType: 5,
25
+ typeCooldownMs: 10_000,
26
+ globalMax: 15,
27
+ globalWindowMs: 10 * 60_000,
28
+ };
29
+
30
+ type ReactionRecord = {
31
+ eventHash: string;
32
+ eventType: string;
33
+ timestamp: number;
34
+ };
35
+
36
+ export type ReactionCallback = (text: string, priority: 'urgent' | 'normal') => void;
37
+
38
+ export class EventReactor {
39
+ private agentService: IAgentService | null = null;
40
+ private config: ReactorConfig;
41
+ private reactionLog: ReactionRecord[] = [];
42
+ private seenHashes = new Set<string>();
43
+ private onReaction: ReactionCallback | null = null;
44
+ private queue: ClassifiedEvent[] = [];
45
+ private processing = false;
46
+
47
+ constructor(config?: Partial<ReactorConfig>) {
48
+ this.config = { ...DEFAULT_CONFIG, ...config };
49
+ }
50
+
51
+ /**
52
+ * Wire the reactor to the agent service (called during daemon startup).
53
+ */
54
+ setAgentService(agent: IAgentService): void {
55
+ this.agentService = agent;
56
+ }
57
+
58
+ /**
59
+ * Set callback for when a reaction is produced (e.g., broadcast via WebSocket).
60
+ */
61
+ setReactionCallback(cb: ReactionCallback): void {
62
+ this.onReaction = cb;
63
+ }
64
+
65
+ /**
66
+ * Attempt to react to a classified event.
67
+ * Returns true if reaction was triggered, false if throttled/deduped.
68
+ */
69
+ async react(classified: ClassifiedEvent): Promise<boolean> {
70
+ if (!this.agentService) {
71
+ console.warn('[EventReactor] No agent service configured, skipping reaction');
72
+ return false;
73
+ }
74
+
75
+ const hash = this.hashEvent(classified);
76
+
77
+ // Deduplication: don't react to the exact same event twice
78
+ if (this.seenHashes.has(hash)) {
79
+ return false;
80
+ }
81
+
82
+ // Cooldown: check per-type rate limit
83
+ if (!this.canReactForType(classified.event.type)) {
84
+ console.log(`[EventReactor] Cooldown active for type: ${classified.event.type}`);
85
+ return false;
86
+ }
87
+
88
+ // Cooldown: check global rate limit
89
+ if (!this.canReactGlobally()) {
90
+ console.log('[EventReactor] Global reaction limit reached');
91
+ return false;
92
+ }
93
+
94
+ // If already processing, queue for later
95
+ if (this.processing) {
96
+ console.log(`[EventReactor] Queuing event (${this.queue.length + 1} in queue): ${classified.reason}`);
97
+ this.queue.push(classified);
98
+ return true; // Will be processed later
99
+ }
100
+
101
+ await this.processEvent(classified, hash);
102
+ return true;
103
+ }
104
+
105
+ // --- Private helpers ---
106
+
107
+ private async processEvent(classified: ClassifiedEvent, hash: string): Promise<void> {
108
+ this.processing = true;
109
+
110
+ try {
111
+ const prompt = this.buildReactionPrompt(classified);
112
+ console.log(`[EventReactor] Reacting to ${classified.priority} event: ${classified.reason}`);
113
+
114
+ const response = await this.agentService!.handleMessage(prompt, 'system');
115
+
116
+ // Record the reaction
117
+ this.recordReaction(hash, classified.event.type);
118
+
119
+ // Broadcast via callback
120
+ const priority = classified.priority === 'critical' ? 'urgent' : 'normal';
121
+ if (this.onReaction && response) {
122
+ this.onReaction(response, priority);
123
+ }
124
+ } catch (err) {
125
+ console.error('[EventReactor] Reaction failed:', err);
126
+ } finally {
127
+ this.processing = false;
128
+
129
+ // Drain the queue
130
+ await this.processQueue();
131
+ }
132
+ }
133
+
134
+ private async processQueue(): Promise<void> {
135
+ while (this.queue.length > 0 && !this.processing) {
136
+ const next = this.queue.shift()!;
137
+ const hash = this.hashEvent(next);
138
+
139
+ // Re-check dedup and cooldowns before processing queued event
140
+ if (this.seenHashes.has(hash)) continue;
141
+ if (!this.canReactForType(next.event.type)) continue;
142
+ if (!this.canReactGlobally()) break;
143
+
144
+ await this.processEvent(next, hash);
145
+ }
146
+ }
147
+
148
+ private buildReactionPrompt(classified: ClassifiedEvent): string {
149
+ const { event, priority, reason } = classified;
150
+ const dataStr = JSON.stringify(event.data, null, 2);
151
+
152
+ return [
153
+ `[PROACTIVE — ${priority.toUpperCase()}]`,
154
+ '',
155
+ reason,
156
+ '',
157
+ `Event type: ${event.type}`,
158
+ `Event data: ${dataStr}`,
159
+ '',
160
+ 'Take appropriate action. You have full access to your tools (browser, terminal, files).',
161
+ 'If this requires user attention, explain clearly what happened and what you did or recommend.',
162
+ 'If you can handle it autonomously, do so and report what you did.',
163
+ ].join('\n');
164
+ }
165
+
166
+ private hashEvent(classified: ClassifiedEvent): string {
167
+ // Simple hash: type + stringified data (first 500 chars to avoid huge hashes)
168
+ const key = `${classified.event.type}:${JSON.stringify(classified.event.data).slice(0, 500)}`;
169
+ let hash = 0;
170
+ for (let i = 0; i < key.length; i++) {
171
+ const char = key.charCodeAt(i);
172
+ hash = ((hash << 5) - hash) + char;
173
+ hash |= 0; // Convert to 32-bit integer
174
+ }
175
+ return hash.toString(36);
176
+ }
177
+
178
+ private canReactForType(eventType: string): boolean {
179
+ const now = Date.now();
180
+ const cutoff = now - this.config.typeCooldownMs;
181
+
182
+ const recentForType = this.reactionLog.filter(
183
+ r => r.eventType === eventType && r.timestamp > cutoff
184
+ );
185
+
186
+ return recentForType.length < this.config.maxPerType;
187
+ }
188
+
189
+ private canReactGlobally(): boolean {
190
+ const now = Date.now();
191
+ const cutoff = now - this.config.globalWindowMs;
192
+
193
+ const recentGlobal = this.reactionLog.filter(r => r.timestamp > cutoff);
194
+
195
+ return recentGlobal.length < this.config.globalMax;
196
+ }
197
+
198
+ private recordReaction(hash: string, eventType: string): void {
199
+ const now = Date.now();
200
+
201
+ this.reactionLog.push({ eventHash: hash, eventType, timestamp: now });
202
+ this.seenHashes.add(hash);
203
+
204
+ // Prune old records (keep last hour)
205
+ const oneHourAgo = now - 60 * 60_000;
206
+ this.reactionLog = this.reactionLog.filter(r => r.timestamp > oneHourAgo);
207
+
208
+ // Prune seen hashes (keep max 1000)
209
+ if (this.seenHashes.size > 1000) {
210
+ const arr = Array.from(this.seenHashes);
211
+ this.seenHashes = new Set(arr.slice(arr.length - 500));
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Health Monitor
3
+ *
4
+ * Monitors daemon health, service status, memory usage, and database connectivity.
5
+ * Runs periodic health checks and logs warnings when issues are detected.
6
+ */
7
+
8
+ import { existsSync, statSync } from "node:fs";
9
+ import { ServiceRegistry, type ServiceStatus } from "./services.ts";
10
+ import { getDb } from "../vault/schema.ts";
11
+
12
+ export type HealthStatus = {
13
+ uptime: number; // seconds
14
+ services: Record<string, ServiceStatus>;
15
+ memory: { heapUsed: number; heapTotal: number; rss: number };
16
+ database: { connected: boolean; size: number }; // DB file size in bytes
17
+ startedAt: number; // epoch ms
18
+ };
19
+
20
+ export class HealthMonitor {
21
+ private startTime: number;
22
+ private registry: ServiceRegistry;
23
+ private checkInterval: Timer | null = null;
24
+ private dbPath: string;
25
+
26
+ constructor(registry: ServiceRegistry, dbPath: string) {
27
+ this.startTime = Date.now();
28
+ this.registry = registry;
29
+ this.dbPath = dbPath;
30
+ }
31
+
32
+ /**
33
+ * Start periodic health checks
34
+ * @param intervalMs - Check interval in milliseconds (default: 30 seconds)
35
+ */
36
+ start(intervalMs: number = 30000): void {
37
+ if (this.checkInterval) {
38
+ console.log('[HealthMonitor] Already running');
39
+ return;
40
+ }
41
+
42
+ console.log(`[HealthMonitor] Starting health checks (every ${intervalMs}ms)`);
43
+ this.checkInterval = setInterval(() => {
44
+ this.check();
45
+ }, intervalMs);
46
+
47
+ // Run initial check
48
+ this.check();
49
+ }
50
+
51
+ /**
52
+ * Stop periodic health checks
53
+ */
54
+ stop(): void {
55
+ if (this.checkInterval) {
56
+ clearInterval(this.checkInterval);
57
+ this.checkInterval = null;
58
+ console.log('[HealthMonitor] Stopped');
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get current health status
64
+ */
65
+ getHealth(): HealthStatus {
66
+ const now = Date.now();
67
+ const uptime = Math.floor((now - this.startTime) / 1000);
68
+ const services = this.registry.getStatus();
69
+ const memory = this.getMemoryStats();
70
+ const database = this.getDatabaseStats();
71
+
72
+ return {
73
+ uptime,
74
+ services,
75
+ memory,
76
+ database,
77
+ startedAt: this.startTime,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Perform health check and log warnings
83
+ */
84
+ private check(): void {
85
+ const health = this.getHealth();
86
+
87
+ // Check service health
88
+ const unhealthyServices = Object.entries(health.services)
89
+ .filter(([_, status]) => status === 'error' || status === 'stopping');
90
+
91
+ if (unhealthyServices.length > 0) {
92
+ console.warn('[HealthMonitor] ⚠ Unhealthy services detected:');
93
+ for (const [name, status] of unhealthyServices) {
94
+ const info = this.registry.getServiceInfo(name);
95
+ const error = info?.error ? ` - ${info.error}` : '';
96
+ console.warn(` - ${name}: ${status}${error}`);
97
+ }
98
+ }
99
+
100
+ // Check database connectivity
101
+ if (!health.database.connected) {
102
+ console.warn('[HealthMonitor] ⚠ Database not connected');
103
+ }
104
+
105
+ // Check memory usage (warn if > 500MB)
106
+ const memoryMB = health.memory.rss / (1024 * 1024);
107
+ if (memoryMB > 500) {
108
+ console.warn(
109
+ `[HealthMonitor] ⚠ High memory usage: ${memoryMB.toFixed(2)}MB RSS`
110
+ );
111
+ }
112
+
113
+ // Log uptime milestone (every hour)
114
+ if (health.uptime > 0 && health.uptime % 3600 === 0) {
115
+ const hours = health.uptime / 3600;
116
+ console.log(`[HealthMonitor] Uptime: ${hours} hour${hours > 1 ? 's' : ''}`);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Get memory statistics
122
+ */
123
+ private getMemoryStats(): { heapUsed: number; heapTotal: number; rss: number } {
124
+ const usage = process.memoryUsage();
125
+ return {
126
+ heapUsed: usage.heapUsed,
127
+ heapTotal: usage.heapTotal,
128
+ rss: usage.rss,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Get database statistics
134
+ */
135
+ private getDatabaseStats(): { connected: boolean; size: number } {
136
+ let connected = false;
137
+ let size = 0;
138
+
139
+ try {
140
+ const db = getDb();
141
+ // Simple query to check if DB is responsive
142
+ db.query("SELECT 1").get();
143
+ connected = true;
144
+
145
+ // Get DB file size if it's not in-memory
146
+ if (this.dbPath !== ':memory:' && existsSync(this.dbPath)) {
147
+ const stats = statSync(this.dbPath);
148
+ size = stats.size;
149
+ }
150
+ } catch (error) {
151
+ // DB not connected or not initialized
152
+ connected = false;
153
+ }
154
+
155
+ return { connected, size };
156
+ }
157
+
158
+ /**
159
+ * Format health status as human-readable string
160
+ */
161
+ formatHealth(): string {
162
+ const health = this.getHealth();
163
+ const lines: string[] = [];
164
+
165
+ lines.push('=== JARVIS Health Status ===');
166
+ lines.push(`Uptime: ${this.formatUptime(health.uptime)}`);
167
+ lines.push(`Started: ${new Date(health.startedAt).toISOString()}`);
168
+ lines.push('');
169
+
170
+ lines.push('Services:');
171
+ for (const [name, status] of Object.entries(health.services)) {
172
+ const icon = status === 'running' ? '✓' : status === 'error' ? '✗' : '○';
173
+ lines.push(` ${icon} ${name}: ${status}`);
174
+ }
175
+ lines.push('');
176
+
177
+ lines.push('Memory:');
178
+ lines.push(` Heap: ${this.formatBytes(health.memory.heapUsed)} / ${this.formatBytes(health.memory.heapTotal)}`);
179
+ lines.push(` RSS: ${this.formatBytes(health.memory.rss)}`);
180
+ lines.push('');
181
+
182
+ lines.push('Database:');
183
+ lines.push(` Connected: ${health.database.connected ? 'Yes' : 'No'}`);
184
+ lines.push(` Size: ${this.formatBytes(health.database.size)}`);
185
+
186
+ return lines.join('\n');
187
+ }
188
+
189
+ /**
190
+ * Format uptime in human-readable format
191
+ */
192
+ private formatUptime(seconds: number): string {
193
+ const days = Math.floor(seconds / 86400);
194
+ const hours = Math.floor((seconds % 86400) / 3600);
195
+ const minutes = Math.floor((seconds % 3600) / 60);
196
+ const secs = seconds % 60;
197
+
198
+ const parts: string[] = [];
199
+ if (days > 0) parts.push(`${days}d`);
200
+ if (hours > 0) parts.push(`${hours}h`);
201
+ if (minutes > 0) parts.push(`${minutes}m`);
202
+ if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
203
+
204
+ return parts.join(' ');
205
+ }
206
+
207
+ /**
208
+ * Format bytes in human-readable format
209
+ */
210
+ private formatBytes(bytes: number): string {
211
+ if (bytes === 0) return '0 B';
212
+
213
+ const k = 1024;
214
+ const sizes = ['B', 'KB', 'MB', 'GB'];
215
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
216
+ const size = bytes / Math.pow(k, i);
217
+
218
+ return `${size.toFixed(2)} ${sizes[i]}`;
219
+ }
220
+ }