@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,1004 @@
1
+ /**
2
+ * J.A.R.V.I.S. Daemon
3
+ *
4
+ * Main entry point for the JARVIS daemon process.
5
+ * Initializes database, registers real services (Agent, Observer, WebSocket),
6
+ * starts health monitoring, and handles graceful shutdown.
7
+ */
8
+
9
+ import { mkdirSync, existsSync } from "node:fs";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ import { initDatabase, closeDb } from "../vault/schema.ts";
13
+ import { ServiceRegistry } from "./services.ts";
14
+ import { HealthMonitor } from "./health.ts";
15
+ import { loadConfig } from "../config/loader.ts";
16
+ import { AgentService } from "./agent-service.ts";
17
+ import { ObserverService } from "./observer-service.ts";
18
+ import { WebSocketService } from "./ws-service.ts";
19
+ import { EventReactor } from "./event-reactor.ts";
20
+ import { EventCoalescer } from "./event-coalescer.ts";
21
+ import { CommitmentExecutor } from "./commitment-executor.ts";
22
+ import { checkCommitments, classifyEvent } from "./event-classifier.ts";
23
+ import { createApiRoutes } from "./api-routes.ts";
24
+ import { GoogleAuth } from "../integrations/google-auth.ts";
25
+ import { ResearchQueue } from "./research-queue.ts";
26
+ import { researchQueueTool, setResearchQueueRef } from "../actions/tools/research.ts";
27
+ import { ChannelService } from "./channel-service.ts";
28
+ import { BackgroundAgentService } from "./background-agent-service.ts";
29
+ import { AuthorityEngine } from "../authority/engine.ts";
30
+ import { ApprovalManager } from "../authority/approval.ts";
31
+ import { AuditTrail } from "../authority/audit.ts";
32
+ import { AuthorityLearner } from "../authority/learning.ts";
33
+ import { EmergencyController } from "../authority/emergency.ts";
34
+ import { ApprovalDelivery } from "../authority/approval-delivery.ts";
35
+ import { DeferredExecutor } from "../authority/deferred-executor.ts";
36
+ import { sendDesktopNotification } from "../comms/desktop-notify.ts";
37
+ import { SidecarManager } from "../sidecar/manager.ts";
38
+
39
+ // Constants
40
+ const DEFAULT_PORT = 3142; // JARVIS port
41
+ const DEFAULT_DATA_DIR = path.join(os.homedir(), '.jarvis');
42
+
43
+ export interface DaemonConfig {
44
+ port: number;
45
+ dbPath: string;
46
+ dataDir: string;
47
+ healthCheckInterval?: number; // ms
48
+ noLocalTools?: boolean; // disable local tool execution
49
+ }
50
+
51
+ let shutdownInProgress = false;
52
+ let registry: ServiceRegistry | null = null;
53
+ let healthMonitor: HealthMonitor | null = null;
54
+ let heartbeatTimer: Timer | null = null;
55
+ let commitmentExecutor: CommitmentExecutor | null = null;
56
+ let bgAgent: BackgroundAgentService | null = null;
57
+ let awarenessService: import('../awareness/service.ts').AwarenessService | null = null;
58
+ let goalService: import('../goals/service.ts').GoalService | null = null;
59
+
60
+ /**
61
+ * Parse command line arguments
62
+ */
63
+ function parseArgs(): Partial<DaemonConfig> {
64
+ const args = process.argv.slice(2);
65
+ const config: Partial<DaemonConfig> = {};
66
+
67
+ for (let i = 0; i < args.length; i++) {
68
+ const arg = args[i];
69
+ switch (arg) {
70
+ case '--port':
71
+ config.port = parseInt(args[++i]!, 10);
72
+ break;
73
+ case '--db-path':
74
+ config.dbPath = args[++i]!;
75
+ break;
76
+ case '--data-dir':
77
+ config.dataDir = args[++i]!;
78
+ break;
79
+ case '--health-interval':
80
+ config.healthCheckInterval = parseInt(args[++i]!, 10);
81
+ break;
82
+ case '--no-local-tools':
83
+ (config as any).noLocalTools = true;
84
+ break;
85
+ case '--help':
86
+ case '-h':
87
+ console.log(`
88
+ J.A.R.V.I.S. Daemon
89
+
90
+ Usage:
91
+ bun run src/daemon/index.ts [options]
92
+
93
+ Options:
94
+ --port <number> WebSocket server port (default: ${DEFAULT_PORT})
95
+ --db-path <path> Database file path (default: ~/.jarvis/jarvis.db)
96
+ --data-dir <path> Data directory (default: ~/.jarvis)
97
+ --health-interval <ms> Health check interval in ms (default: 30000)
98
+ --no-local-tools Disable local tool execution (run_command, read_file, etc).
99
+ Tools will only work when routed to a sidecar via target param.
100
+ --help, -h Show this help message
101
+
102
+ Example:
103
+ bun run src/daemon/index.ts --port 3142 --data-dir ~/.jarvis
104
+ `);
105
+ process.exit(0);
106
+ }
107
+ }
108
+
109
+ return config;
110
+ }
111
+
112
+ /**
113
+ * Ensure data directory exists
114
+ */
115
+ function ensureDataDir(dataDir: string): void {
116
+ if (!existsSync(dataDir)) {
117
+ console.log(`[Daemon] Creating data directory: ${dataDir}`);
118
+ mkdirSync(dataDir, { recursive: true });
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Log timestamp helper
124
+ */
125
+ function logWithTimestamp(message: string): void {
126
+ const timestamp = new Date().toISOString();
127
+ console.log(`[${timestamp}] ${message}`);
128
+ }
129
+
130
+ /**
131
+ * Handle graceful shutdown
132
+ */
133
+ async function handleShutdown(signal: string): Promise<void> {
134
+ if (shutdownInProgress) {
135
+ console.log('\n[Daemon] Force shutdown requested, exiting immediately');
136
+ process.exit(1);
137
+ }
138
+
139
+ shutdownInProgress = true;
140
+ console.log(`\n[Daemon] Received ${signal}, shutting down gracefully...`);
141
+
142
+ try {
143
+ // Clear heartbeat timer
144
+ if (heartbeatTimer) {
145
+ clearInterval(heartbeatTimer);
146
+ heartbeatTimer = null;
147
+ }
148
+
149
+ // Stop commitment executor
150
+ if (commitmentExecutor) {
151
+ commitmentExecutor.stop();
152
+ commitmentExecutor = null;
153
+ }
154
+
155
+ // Stop goal service
156
+ if (goalService) {
157
+ await goalService.stop();
158
+ goalService = null;
159
+ }
160
+
161
+ // Stop awareness service
162
+ if (awarenessService) {
163
+ await awarenessService.stop();
164
+ awarenessService = null;
165
+ }
166
+
167
+ // Stop background agent (separate browser)
168
+ if (bgAgent) {
169
+ await bgAgent.stop();
170
+ bgAgent = null;
171
+ }
172
+
173
+ // Stop health monitor
174
+ if (healthMonitor) {
175
+ healthMonitor.stop();
176
+ }
177
+
178
+ // Stop all services (reverse order: websocket -> observers -> agent)
179
+ if (registry) {
180
+ await registry.stopAll();
181
+ }
182
+
183
+ // Close database
184
+ closeDb();
185
+ console.log('[Daemon] Database closed');
186
+
187
+ console.log('[Daemon] Shutdown complete');
188
+ process.exit(0);
189
+ } catch (error) {
190
+ console.error('[Daemon] Error during shutdown:', error);
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Print startup banner
197
+ */
198
+ function printBanner(config: DaemonConfig): void {
199
+ console.log(`
200
+ ██╗ █████╗ ██████╗ ██╗ ██╗██╗███████╗
201
+ ██║██╔══██╗██╔══██╗██║ ██║██║██╔════╝
202
+ ██║███████║██████╔╝██║ ██║██║███████╗
203
+ ██ ██║██╔══██║██╔══██╗╚██╗ ██╔╝██║╚════██║
204
+ ╚█████╔╝██║ ██║██║ ██║ ╚████╔╝ ██║███████║
205
+ ╚════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝╚══════╝
206
+
207
+ Just A Rather Very Intelligent System
208
+ `);
209
+ console.log('[Daemon] Configuration:');
210
+ console.log(` Port: ${config.port}`);
211
+ console.log(` Data Dir: ${config.dataDir}`);
212
+ console.log(` DB Path: ${config.dbPath}`);
213
+ console.log('');
214
+ }
215
+
216
+ /**
217
+ * Start the JARVIS daemon
218
+ */
219
+ export async function startDaemon(userConfig?: Partial<DaemonConfig>): Promise<void> {
220
+ // Load config from YAML (with defaults)
221
+ const jarvisConfig = await loadConfig();
222
+
223
+ // Determine data directory: CLI args > config file > default
224
+ const dataDir = userConfig?.dataDir ?? jarvisConfig.daemon.data_dir ?? DEFAULT_DATA_DIR;
225
+
226
+ // If user specified a custom data dir but no db path, use jarvis.db in that dir
227
+ const dbPath = userConfig?.dbPath ?? jarvisConfig.daemon.db_path ?? path.join(dataDir, 'jarvis.db');
228
+
229
+ // Merge configuration
230
+ const port = userConfig?.port ?? jarvisConfig.daemon.port ?? DEFAULT_PORT;
231
+ const config: DaemonConfig = {
232
+ port,
233
+ dataDir,
234
+ dbPath,
235
+ healthCheckInterval: userConfig?.healthCheckInterval ?? 30000,
236
+ };
237
+
238
+ // If dbPath is relative, make it absolute within dataDir
239
+ if (!path.isAbsolute(config.dbPath)) {
240
+ config.dbPath = path.join(config.dataDir, config.dbPath);
241
+ }
242
+
243
+ printBanner(config);
244
+
245
+ try {
246
+ // 1. Ensure data directory exists
247
+ ensureDataDir(config.dataDir);
248
+
249
+ // 2. Initialize database
250
+ logWithTimestamp(`Initializing database at ${config.dbPath}`);
251
+ initDatabase(config.dbPath);
252
+ logWithTimestamp('Database initialized successfully');
253
+
254
+ // 2b. Load LLM settings from DB + encrypted keychain, merge into config
255
+ const { mergeLLMSettingsIntoConfig } = await import('./llm-settings.ts');
256
+ mergeLLMSettingsIntoConfig(jarvisConfig);
257
+ logWithTimestamp('LLM settings loaded from database');
258
+
259
+ // 3. Create service registry
260
+ registry = new ServiceRegistry();
261
+
262
+ // 4. Create proactive modules
263
+ const heartbeatConfig = jarvisConfig.heartbeat;
264
+ const reactor = new EventReactor();
265
+ const coalescer = new EventCoalescer();
266
+
267
+ // 4b. Create GoogleAuth if configured
268
+ let googleAuth: GoogleAuth | null = null;
269
+ if (jarvisConfig.google?.client_id && jarvisConfig.google?.client_secret) {
270
+ googleAuth = new GoogleAuth(jarvisConfig.google.client_id, jarvisConfig.google.client_secret);
271
+ if (googleAuth.isAuthenticated()) {
272
+ console.log('[Daemon] Google OAuth: authenticated (Gmail + Calendar observers enabled)');
273
+ } else {
274
+ console.log('[Daemon] Google OAuth: credentials found but not authenticated');
275
+ console.log('[Daemon] Run: bun run src/scripts/google-setup.ts to authorize');
276
+ }
277
+ }
278
+
279
+ // 4c. Create research queue
280
+ const researchQueue = new ResearchQueue();
281
+ setResearchQueueRef(researchQueue);
282
+
283
+ // 5. Create real services
284
+ const agentService = new AgentService(jarvisConfig);
285
+ agentService.setResearchQueue(researchQueue);
286
+ const observerService = new ObserverService(reactor, coalescer, googleAuth ?? undefined);
287
+ const wsService = new WebSocketService(config.port, agentService);
288
+
289
+ // 5b. Create channel service for external comms (Telegram, Discord)
290
+ const channelService = new ChannelService(jarvisConfig, agentService);
291
+
292
+ // 5c. Create commitment executor (notify-then-execute)
293
+ const aggressiveness = heartbeatConfig?.aggressiveness ?? 'moderate';
294
+ const executor = new CommitmentExecutor(aggressiveness as any);
295
+
296
+ // 6. Wire reactor callback for WebSocket notifications
297
+ reactor.setReactionCallback((text, priority) => {
298
+ wsService.broadcastNotification(text, priority);
299
+ });
300
+ // Note: reactor.setAgentService + executor.setAgentService wired to bgAgent after startAll (step 10c)
301
+
302
+ // 6b. Wire delegation progress to WebSocket for sub-agent visibility
303
+ agentService.setDelegationProgressCallback((event) => {
304
+ wsService.broadcastSubAgentProgress(event);
305
+ });
306
+
307
+ // 6c. Create sidecar manager
308
+ const sidecarManager = new SidecarManager(jarvisConfig.daemon.data_dir.replace('~', os.homedir()));
309
+ const brainDomain = jarvisConfig.daemon.brain_domain ?? `localhost:${config.port}`;
310
+ sidecarManager.setBrainUrl(brainDomain);
311
+
312
+ // 6d. Wire sidecar manager to WebSocket server for WS routing
313
+ wsService.getServer().setSidecarManager(sidecarManager);
314
+
315
+ // 7. Register services in startup order
316
+ // Agent first (needs DB), Observers second, Channels third, Sidecar, WebSocket last (needs Agent)
317
+ registry.register(agentService);
318
+ registry.register(observerService);
319
+ registry.register(channelService);
320
+ registry.register(sidecarManager);
321
+ registry.register(wsService);
322
+
323
+ // 8. Start health monitor (before services, so API routes can reference it)
324
+ healthMonitor = new HealthMonitor(registry, config.dbPath);
325
+
326
+ // 8b. Wire channel service to WebSocket for cross-channel broadcasts
327
+ wsService.setChannelService(channelService);
328
+
329
+ // 8c. Wire TTS provider if configured
330
+ if (jarvisConfig.tts?.enabled) {
331
+ const { createTTSProvider } = await import('../comms/voice.ts');
332
+ const ttsProvider = createTTSProvider(jarvisConfig.tts);
333
+ if (ttsProvider) {
334
+ wsService.setTTSProvider(ttsProvider);
335
+ console.log(`[Daemon] TTS enabled: ${jarvisConfig.tts.voice ?? 'en-US-AriaNeural'}`);
336
+ }
337
+ }
338
+
339
+ // 8d. Wire STT provider for voice input via dashboard
340
+ if (jarvisConfig.stt) {
341
+ const { createSTTProvider } = await import('../comms/voice.ts');
342
+ const sttProvider = createSTTProvider(jarvisConfig.stt);
343
+ if (sttProvider) {
344
+ wsService.setSTTProvider(sttProvider);
345
+ console.log(`[Daemon] STT for voice input: ${jarvisConfig.stt.provider}`);
346
+ }
347
+ }
348
+
349
+ // 8e. Wire Authority & Autonomy Engine
350
+ const authorityConfig = jarvisConfig.authority ?? { default_level: 3 };
351
+ const authorityEngine = new AuthorityEngine({
352
+ default_level: authorityConfig.default_level,
353
+ governed_categories: (authorityConfig.governed_categories ?? ['send_email', 'send_message', 'make_payment']) as any,
354
+ overrides: (authorityConfig.overrides ?? []) as any,
355
+ context_rules: (authorityConfig.context_rules ?? []) as any,
356
+ learning: authorityConfig.learning ?? { enabled: true, suggest_threshold: 5 },
357
+ emergency_state: authorityConfig.emergency_state ?? 'normal',
358
+ });
359
+ const approvalManager = new ApprovalManager();
360
+ const auditTrail = new AuditTrail();
361
+ const learner = new AuthorityLearner(authorityConfig.learning?.suggest_threshold ?? 5);
362
+ const emergencyController = new EmergencyController();
363
+ const approvalDelivery = new ApprovalDelivery();
364
+ const deferredExecutor = new DeferredExecutor(approvalManager, auditTrail);
365
+ deferredExecutor.setLearner(learner);
366
+
367
+ // Restore emergency state from config
368
+ const savedEmergencyState = authorityConfig.emergency_state ?? 'normal';
369
+ if (savedEmergencyState === 'paused') emergencyController.pause();
370
+ else if (savedEmergencyState === 'killed') emergencyController.kill();
371
+
372
+ // Persist emergency state changes to config.yaml
373
+ emergencyController.setStateChangeCallback(async (state) => {
374
+ wsService.broadcastEmergencyState(state);
375
+ try {
376
+ const { loadConfig: reloadConfig, saveConfig: resaveConfig } = await import('../config/loader.ts');
377
+ const fresh = await reloadConfig();
378
+ if (!fresh.authority) fresh.authority = { default_level: 3 } as any;
379
+ fresh.authority.emergency_state = state;
380
+ await resaveConfig(fresh);
381
+ } catch (err) {
382
+ console.error('[Daemon] Failed to persist emergency state:', err);
383
+ }
384
+ });
385
+
386
+ // Wire authority engine into orchestrator
387
+ const orchestrator = agentService.getOrchestrator();
388
+ orchestrator.setAuthorityEngine(authorityEngine);
389
+ orchestrator.setApprovalManager(approvalManager);
390
+ orchestrator.setAuditTrail(auditTrail);
391
+ orchestrator.setEmergencyController(emergencyController);
392
+
393
+ // Wire approval callback: when orchestrator needs approval, deliver to user
394
+ orchestrator.setApprovalCallback((request) => {
395
+ approvalDelivery.deliver(request).catch(err =>
396
+ console.error('[Daemon] Approval delivery error:', err)
397
+ );
398
+ });
399
+
400
+ // Wire authority engine into agent-service for prompt context
401
+ agentService.setAuthorityEngine(authorityEngine);
402
+
403
+ // Wire deferred executor tool registry (after start, tools are registered)
404
+ // Note: toolRegistry set after startAll() below
405
+
406
+ // Wire channel approval handler
407
+ channelService.setApprovalHandler(async (action, shortId, channel) => {
408
+ const request = approvalManager.findByShortId(shortId);
409
+ if (!request) return `No pending approval found for ID ${shortId}`;
410
+
411
+ if (action === 'approve') {
412
+ const approved = approvalManager.approve(request.id, channel);
413
+ if (!approved) return 'Request already decided';
414
+ const result = await deferredExecutor.executeApproved(request.id);
415
+ const updated = approvalManager.getRequest(request.id);
416
+ if (updated) wsService.broadcastApprovalUpdate(updated);
417
+ return `Approved and executed. Result: ${result.slice(0, 200)}`;
418
+ } else {
419
+ const denied = approvalManager.deny(request.id, channel);
420
+ if (!denied) return 'Request already decided';
421
+ deferredExecutor.recordDenial(denied);
422
+ wsService.broadcastApprovalUpdate(denied);
423
+ return `Denied: ${request.tool_name}`;
424
+ }
425
+ });
426
+
427
+ console.log(`[Daemon] Authority engine initialized (governed: ${authorityEngine.getConfig().governed_categories.join(', ')})`);
428
+
429
+ // 9. Ensure UI is built (auto-build if ui/dist is missing or empty)
430
+ const uiDistDir = path.join(import.meta.dir, '../../ui/dist');
431
+ const uiIndexPath = path.join(uiDistDir, 'index.html');
432
+ if (!existsSync(uiIndexPath)) {
433
+ logWithTimestamp('Dashboard UI not built — building automatically...');
434
+ const buildResult = Bun.spawnSync(['bun', 'run', 'build:ui'], {
435
+ cwd: path.join(import.meta.dir, '../..'),
436
+ stdout: 'pipe',
437
+ stderr: 'pipe',
438
+ env: { ...process.env },
439
+ });
440
+ if (buildResult.exitCode === 0) {
441
+ logWithTimestamp('Dashboard UI built successfully');
442
+ } else {
443
+ const stderr = buildResult.stderr.toString().trim();
444
+ console.warn(`[Daemon] UI build failed (dashboard may not load): ${stderr.slice(0, 200)}`);
445
+ }
446
+ }
447
+
448
+ // 9b. Set up API routes + dashboard static files
449
+ const apiContext: import('./api-routes.ts').ApiContext & Record<string, unknown> = {
450
+ healthMonitor,
451
+ agentService,
452
+ config: jarvisConfig,
453
+ wsService,
454
+ channelService,
455
+ authorityEngine,
456
+ approvalManager,
457
+ auditTrail,
458
+ learner,
459
+ emergencyController,
460
+ deferredExecutor,
461
+ awarenessService: null as any,
462
+ goalService: undefined,
463
+ sidecarManager,
464
+ };
465
+ const apiRoutes = createApiRoutes(apiContext);
466
+ wsService.setApiRoutes(apiRoutes);
467
+
468
+ // Serve dashboard from ui/dist/
469
+ wsService.setStaticDir(uiDistDir);
470
+
471
+ // Serve public assets (wake word models, WASM) from ui/public/
472
+ const uiPublicDir = path.join(import.meta.dir, '../../ui/public');
473
+ wsService.setPublicDir(uiPublicDir);
474
+
475
+ // 9c. Configure auth token if set
476
+ const authToken = jarvisConfig.auth?.token;
477
+ if (authToken) {
478
+ wsService.setAuthToken(authToken);
479
+ console.log('[Daemon] Auth token configured — dashboard routes require ?token= or cookie');
480
+ } else {
481
+ console.warn('[Daemon] No auth token configured — dashboard is open to anyone on the network');
482
+ }
483
+
484
+ // 9b. Apply --no-local-tools flag if set
485
+ if (config.noLocalTools) {
486
+ const { setNoLocalTools } = await import('../actions/tools/builtin.ts');
487
+ setNoLocalTools(true);
488
+ }
489
+
490
+ // 10. Start all services
491
+ await registry.startAll();
492
+
493
+ // 10a-post. Wire authority components that need running services
494
+ const toolRegistry = orchestrator.getToolRegistry();
495
+ if (toolRegistry) {
496
+ deferredExecutor.setToolRegistry(toolRegistry);
497
+ }
498
+ approvalDelivery.setBroadcaster(wsService);
499
+ approvalDelivery.setChannelSender(channelService);
500
+ deferredExecutor.setResultCallback((requestId, request, result) => {
501
+ // Notify via WS and channels that an approved action was executed
502
+ const text = `[EXECUTED] ${request.tool_name}: ${result.slice(0, 200)}`;
503
+ wsService.broadcastNotification(text, 'normal');
504
+ });
505
+
506
+ // 10b. Create and start background agent (needs LLM providers from agentService.start())
507
+ const bgAgentService = new BackgroundAgentService(jarvisConfig, agentService.getLLMManager());
508
+ bgAgentService.setResearchQueue(researchQueue);
509
+ await bgAgentService.start();
510
+ bgAgent = bgAgentService;
511
+ console.log('[Daemon] Background agent started (separate browser for heartbeat/reactions)');
512
+
513
+ // 10c. Wire reactor + executor to background agent (separate browser, no chat contention)
514
+ reactor.setAgentService(bgAgentService);
515
+ executor.setAgentService(bgAgentService);
516
+
517
+ // 10d. Wire executor broadcast (needs wsServer running) and start
518
+ executor.setBroadcast((msg) => wsService.getServer().broadcast(msg));
519
+ wsService.setCommitmentExecutor(executor);
520
+ executor.start();
521
+ commitmentExecutor = executor;
522
+
523
+ // 10e. Create and start Awareness Service (M13)
524
+ if (jarvisConfig.awareness?.enabled !== false) {
525
+ try {
526
+ const { AwarenessService } = await import('../awareness/service.ts');
527
+ const svc = new AwarenessService(
528
+ jarvisConfig,
529
+ agentService.getLLMManager(),
530
+ (event) => {
531
+ // Route awareness events through existing event pipeline
532
+ const classified = classifyEvent({
533
+ type: event.type,
534
+ data: event.data,
535
+ timestamp: event.timestamp,
536
+ });
537
+ if (classified.priority === 'critical' || classified.priority === 'high') {
538
+ reactor.react(classified).catch(err =>
539
+ console.error('[Daemon] Awareness reaction error:', err)
540
+ );
541
+ } else {
542
+ coalescer.addEvent(classified);
543
+ }
544
+ // Broadcast to WebSocket clients
545
+ wsService.broadcastAwarenessEvent(event);
546
+
547
+ // Push suggestions as chat notifications + voice + desktop
548
+ if (event.type === 'suggestion_ready') {
549
+ const title = String(event.data.title ?? '');
550
+ const body = String(event.data.body ?? '');
551
+ const text = `**${title}**\n${body}`;
552
+ console.log(`[Daemon] Awareness suggestion firing: "${title}"`);
553
+
554
+ const hasWsClients = wsService.getServer().getClientCount() > 0;
555
+
556
+ if (hasWsClients) {
557
+ // Primary: deliver via WebSocket + voice
558
+ wsService.broadcastNotification(text, 'urgent');
559
+ sendDesktopNotification(`JARVIS: ${title}`, body, { urgency: 'normal' });
560
+ wsService.broadcastProactiveVoice(body).catch(err =>
561
+ console.error('[Daemon] Awareness TTS error:', err)
562
+ );
563
+ } else {
564
+ // Fallback: no dashboard clients — deliver via external channels + persistent desktop
565
+ console.log('[Daemon] No WS clients — routing suggestion to external channels');
566
+ channelService.broadcastToAll(text).catch(err =>
567
+ console.error('[Daemon] Channel broadcast error:', err)
568
+ );
569
+ sendDesktopNotification(`JARVIS: ${title}`, body, { urgency: 'critical', expireMs: 30000 });
570
+ }
571
+ }
572
+
573
+ // Auto-research errors: silently investigate and deliver solution
574
+ if (event.type === 'error_detected' && bgAgent) {
575
+ const errorText = String(event.data.errorText ?? '');
576
+ const appName = String(event.data.appName ?? '');
577
+ if (errorText.length > 5) {
578
+ console.log(`[Daemon] Auto-researching error: "${errorText.slice(0, 80)}"`);
579
+ bgAgent.handleMessage(
580
+ `The user is seeing this error in ${appName}: "${errorText}". ` +
581
+ `Search the web and vault for a solution. Be concise and actionable. ` +
582
+ `Start your response with the fix, not a question.`,
583
+ 'awareness'
584
+ ).then(solution => {
585
+ if (solution && solution.length > 10) {
586
+ const solutionText = `**Fix for error in ${appName}:**\n${solution.slice(0, 500)}`;
587
+ wsService.broadcastNotification(solutionText, 'urgent');
588
+ sendDesktopNotification(`JARVIS: Fix for ${appName}`, solution.slice(0, 200), { urgency: 'critical', expireMs: 15000 });
589
+ // Strip markdown for TTS — voice should sound natural
590
+ const voiceText = solution
591
+ .replace(/#{1,6}\s*/g, '')
592
+ .replace(/\*{1,2}([^*]+)\*{1,2}/g, '$1')
593
+ .replace(/`([^`]+)`/g, '$1')
594
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
595
+ .replace(/\n{2,}/g, '. ')
596
+ .replace(/\n/g, ' ')
597
+ .replace(/\s{2,}/g, ' ')
598
+ .trim()
599
+ .slice(0, 300);
600
+ console.log(`[Daemon] Speaking error solution (${voiceText.length} chars): "${voiceText.slice(0, 80)}..."`);
601
+ wsService.broadcastProactiveVoice(
602
+ `I found a fix for the error in ${appName}. ${voiceText}`
603
+ ).then(() =>
604
+ console.log('[Daemon] Error solution TTS delivered')
605
+ ).catch(err =>
606
+ console.error('[Daemon] Error solution TTS failed:', err instanceof Error ? err.message : err)
607
+ );
608
+ }
609
+ }).catch(err =>
610
+ console.error('[Daemon] Error auto-research failed:', err instanceof Error ? err.message : err)
611
+ );
612
+ }
613
+ }
614
+
615
+ // Deep-research struggles: for high-confidence code/terminal struggles
616
+ if (event.type === 'struggle_detected' && bgAgent) {
617
+ const appCategory = String(event.data.appCategory ?? 'general');
618
+ const sAppName = String(event.data.appName ?? '');
619
+ const ocrPreview = String(event.data.ocrPreview ?? '');
620
+ const compositeScore = event.data.compositeScore as number;
621
+
622
+ if (compositeScore >= 0.7 && (appCategory === 'code_editor' || appCategory === 'terminal')) {
623
+ console.log(`[Daemon] Deep-researching struggle in ${sAppName} (score: ${compositeScore.toFixed(2)})`);
624
+ bgAgent.handleMessage(
625
+ `The user has been struggling in ${sAppName} (${appCategory}) for several minutes. ` +
626
+ `Here's what's on their screen:\n"${ocrPreview.slice(0, 800)}"\n\n` +
627
+ `Search for solutions to any errors visible. Check documentation for the relevant language/framework. ` +
628
+ `Provide a specific, actionable fix. Start with the solution, not a question.`,
629
+ 'awareness'
630
+ ).then(solution => {
631
+ if (solution && solution.length > 10) {
632
+ const solutionText = `**Help for ${sAppName}:**\n${solution.slice(0, 500)}`;
633
+ wsService.broadcastNotification(solutionText, 'urgent');
634
+ sendDesktopNotification(`JARVIS: Help for ${sAppName}`, solution.slice(0, 200), { urgency: 'critical', expireMs: 15000 });
635
+ const voiceText = solution
636
+ .replace(/#{1,6}\s*/g, '')
637
+ .replace(/\*{1,2}([^*]+)\*{1,2}/g, '$1')
638
+ .replace(/`([^`]+)`/g, '$1')
639
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
640
+ .replace(/\n{2,}/g, '. ')
641
+ .replace(/\n/g, ' ')
642
+ .replace(/\s{2,}/g, ' ')
643
+ .trim()
644
+ .slice(0, 300);
645
+ wsService.broadcastProactiveVoice(
646
+ `I found something that might help with what you're working on in ${sAppName}. ${voiceText}`
647
+ ).catch(err =>
648
+ console.error('[Daemon] Struggle solution TTS failed:', err instanceof Error ? err.message : err)
649
+ );
650
+ }
651
+ }).catch(err =>
652
+ console.error('[Daemon] Struggle auto-research failed:', err instanceof Error ? err.message : err)
653
+ );
654
+ }
655
+ }
656
+
657
+ // M16: Route awareness events to goal auto-detection
658
+ if (goalService && (event.type === 'context_changed' || event.type === 'session_ended')) {
659
+ try {
660
+ const { matchAwarenessToGoals, logAutoDetectedProgress } = require('../goals/awareness-bridge.ts');
661
+ const matches = matchAwarenessToGoals(event.data);
662
+ if (matches.length > 0) {
663
+ logAutoDetectedProgress(matches, event.type);
664
+ }
665
+ } catch (err) {
666
+ // Silently ignore — goal matching is best-effort
667
+ }
668
+ }
669
+ },
670
+ googleAuth
671
+ );
672
+ await svc.start();
673
+ awarenessService = svc;
674
+ apiContext.awarenessService = svc;
675
+ console.log('[Daemon] Awareness service started (event-driven OCR + context tracking)');
676
+
677
+ // Wire sidecar awareness events to awareness service
678
+ sidecarManager.onEvent((sidecarId, event) => {
679
+ if (['screen_capture', 'context_changed', 'idle_detected'].includes(event.event_type)) {
680
+ svc.handleSidecarEvent(sidecarId, event).catch(err =>
681
+ console.error('[Daemon] Awareness sidecar event error:', err instanceof Error ? err.message : err)
682
+ );
683
+ }
684
+ });
685
+
686
+ // Auto-launch overlay widget (non-blocking, best-effort)
687
+ if (jarvisConfig.awareness?.overlay_autolaunch !== false) {
688
+ try {
689
+ const overlayUrl = `http://localhost:${config.port}/overlay`;
690
+ const browsers = ['chromium-browser', 'chromium', 'google-chrome', 'google-chrome-stable'];
691
+ for (const browser of browsers) {
692
+ const which = Bun.spawnSync(['which', browser]);
693
+ if (which.exitCode === 0) {
694
+ Bun.spawn([
695
+ browser,
696
+ `--app=${overlayUrl}`,
697
+ '--window-size=300,320',
698
+ '--window-position=20,20',
699
+ '--no-sandbox',
700
+ '--disable-extensions',
701
+ '--disable-gpu',
702
+ `--user-data-dir=${path.join(config.dataDir, 'browser', 'overlay-profile')}`,
703
+ ], { stdout: 'ignore', stderr: 'ignore' });
704
+ console.log(`[Daemon] Awareness overlay launched (${browser})`);
705
+ break;
706
+ }
707
+ }
708
+ } catch (err) { console.warn('[Daemon] Awareness overlay failed (non-fatal):', err instanceof Error ? err.message : err); }
709
+ }
710
+ } catch (err) {
711
+ console.error('[Daemon] Awareness service failed to start:', err instanceof Error ? err.message : err);
712
+ // Non-fatal — daemon continues without awareness
713
+ }
714
+ }
715
+
716
+ // 10b. Workflow Automation Engine (M14)
717
+ const workflowConfig = jarvisConfig.workflows;
718
+ if (workflowConfig?.enabled !== false) {
719
+ try {
720
+ const { NodeRegistry } = await import('../workflows/nodes/registry.ts');
721
+ const { registerBuiltinNodes } = await import('../workflows/nodes/builtin.ts');
722
+ const { WorkflowEngine } = await import('../workflows/engine.ts');
723
+ const { TriggerManager } = await import('../workflows/triggers/manager.ts');
724
+ const { NLWorkflowBuilder } = await import('../workflows/nl-builder.ts');
725
+ const { WorkflowAutoSuggest } = await import('../workflows/auto-suggest.ts');
726
+
727
+ // Create node registry and register all built-in nodes
728
+ const nodeRegistry = new NodeRegistry();
729
+ registerBuiltinNodes(nodeRegistry);
730
+ console.log(`[Daemon] Node registry: ${nodeRegistry.count()} nodes registered`);
731
+
732
+ // Create and start workflow engine
733
+ const wfToolRegistry = orchestrator.getToolRegistry();
734
+ const workflowEngine = new WorkflowEngine(
735
+ nodeRegistry,
736
+ wfToolRegistry ?? new (await import('../actions/tools/registry.ts')).ToolRegistry(),
737
+ agentService.getLLMManager(),
738
+ );
739
+ workflowEngine.setEventCallback((event) => {
740
+ wsService.broadcastWorkflowEvent(event);
741
+ });
742
+ await workflowEngine.start();
743
+
744
+ // Create and start trigger manager
745
+ const triggerManager = new TriggerManager(workflowEngine);
746
+ await triggerManager.start();
747
+
748
+ // Create NL builder and auto-suggest
749
+ const nlBuilder = new NLWorkflowBuilder(nodeRegistry, agentService.getLLMManager());
750
+ const autoSuggest = new WorkflowAutoSuggest(nodeRegistry, agentService.getLLMManager());
751
+
752
+ // Wire awareness events into auto-suggest
753
+ if (awarenessService) {
754
+ // The awareness service emits events that can feed pattern detection
755
+ console.log('[Daemon] Workflow auto-suggest wired to awareness events');
756
+ }
757
+
758
+ // Register manage_workflow tool so primary agent can create/run workflows from chat
759
+ const { createManageWorkflowTool } = await import('../actions/tools/workflows.ts');
760
+ const manageWorkflowTool = createManageWorkflowTool({ workflowEngine, nlBuilder, triggerManager });
761
+ if (wfToolRegistry) {
762
+ wfToolRegistry.register(manageWorkflowTool);
763
+ console.log('[Daemon] manage_workflow tool registered for chat agent');
764
+ }
765
+
766
+ // Wire into API context
767
+ (apiContext as any).workflowEngine = workflowEngine;
768
+ (apiContext as any).triggerManager = triggerManager;
769
+ (apiContext as any).webhookManager = triggerManager.getWebhookManager();
770
+ (apiContext as any).nodeRegistry = nodeRegistry;
771
+ (apiContext as any).nlBuilder = nlBuilder;
772
+ (apiContext as any).autoSuggest = autoSuggest;
773
+
774
+ console.log('[Daemon] Workflow engine started (engine + triggers + NL builder + auto-suggest)');
775
+ } catch (err) {
776
+ console.error('[Daemon] Workflow engine failed to start:', err instanceof Error ? err.message : err);
777
+ // Non-fatal — daemon continues without workflows
778
+ }
779
+ }
780
+
781
+ // 10f. Goal Service (M16)
782
+ const goalsConfig = jarvisConfig.goals;
783
+ if (goalsConfig?.enabled !== false) {
784
+ try {
785
+ const { GoalService } = await import('../goals/service.ts');
786
+ const goalSvc = new GoalService(goalsConfig ?? {
787
+ enabled: true,
788
+ morning_window: { start: 7, end: 9 },
789
+ evening_window: { start: 20, end: 22 },
790
+ accountability_style: 'drill_sergeant',
791
+ escalation_weeks: { pressure: 1, root_cause: 3, suggest_kill: 4 },
792
+ auto_decompose: true,
793
+ calendar_ownership: false,
794
+ });
795
+ goalSvc.setEventCallback((event) => {
796
+ wsService.broadcastGoalEvent(event);
797
+ });
798
+ await goalSvc.start();
799
+ goalService = goalSvc;
800
+ apiContext.goalService = goalSvc;
801
+
802
+ // Wire workflow bridge for daily rhythm
803
+ try {
804
+ const { generateRhythmWorkflows, registerGoalWorkflows } = await import('../goals/workflow-bridge.ts');
805
+ const effectiveConfig = goalsConfig ?? {
806
+ enabled: true,
807
+ morning_window: { start: 7, end: 9 },
808
+ evening_window: { start: 20, end: 22 },
809
+ accountability_style: 'drill_sergeant' as const,
810
+ escalation_weeks: { pressure: 1, root_cause: 3, suggest_kill: 4 },
811
+ auto_decompose: true,
812
+ calendar_ownership: false,
813
+ };
814
+ const rhythmWorkflows = generateRhythmWorkflows(effectiveConfig);
815
+ if (apiContext.triggerManager) {
816
+ registerGoalWorkflows(rhythmWorkflows, apiContext.triggerManager as any);
817
+ }
818
+ } catch { /* workflow bridge is optional */ }
819
+
820
+ // Register manage_goals tool for chat agent
821
+ try {
822
+ const goalToolRegistry = orchestrator.getToolRegistry();
823
+ if (goalToolRegistry) {
824
+ const { createManageGoalsTool } = await import('../actions/tools/goals.ts');
825
+ const { NLGoalBuilder } = await import('../goals/nl-builder.ts');
826
+ const { GoalEstimator } = await import('../goals/estimator.ts');
827
+ const { DailyRhythm } = await import('../goals/rhythm.ts');
828
+ const { AccountabilityEngine } = await import('../goals/accountability.ts');
829
+ const llm = agentService.getLLMManager();
830
+ const style = goalsConfig?.accountability_style ?? 'drill_sergeant';
831
+ const escWeeks = goalsConfig?.escalation_weeks ?? { pressure: 1, root_cause: 3, suggest_kill: 4 };
832
+ const goalNlBuilder = new NLGoalBuilder(llm);
833
+ const goalEstimator = new GoalEstimator(llm);
834
+ const goalRhythm = new DailyRhythm(llm, style);
835
+ const goalAccountability = new AccountabilityEngine(llm, style, escWeeks);
836
+ const manageGoalsTool = createManageGoalsTool({
837
+ goalService: goalSvc,
838
+ nlBuilder: goalNlBuilder,
839
+ estimator: goalEstimator,
840
+ rhythm: goalRhythm,
841
+ accountability: goalAccountability,
842
+ });
843
+ goalToolRegistry.register(manageGoalsTool);
844
+ console.log('[Daemon] manage_goals tool registered for chat agent');
845
+ }
846
+ } catch (err) {
847
+ console.error('[Daemon] Failed to register manage_goals tool:', err instanceof Error ? err.message : err);
848
+ }
849
+
850
+ console.log('[Daemon] Goal service started (autonomous goal pursuit)');
851
+ } catch (err) {
852
+ console.error('[Daemon] Goal service failed to start:', err instanceof Error ? err.message : err);
853
+ // Non-fatal — daemon continues without goals
854
+ }
855
+ }
856
+
857
+ // 10g. Inject sidecar manager into tool routing layer
858
+ {
859
+ const { setSidecarManagerRef } = await import('../actions/tools/sidecar-route.ts');
860
+ setSidecarManagerRef(sidecarManager);
861
+ console.log('[Daemon] Sidecar routing enabled for run_command, read_file, write_file, list_directory');
862
+ }
863
+
864
+ // 10h. Wire sidecar events into event pipeline (skip awareness events — already handled by awareness service)
865
+ const awarenessEventTypes = ['screen_capture', 'context_changed', 'idle_detected'];
866
+ sidecarManager.onEvent((sidecarId, event) => {
867
+ // Skip events already routed to awareness service to avoid double processing
868
+ if (awarenessService && awarenessEventTypes.includes(event.event_type)) return;
869
+
870
+ const eventType = `sidecar_${event.event_type}`;
871
+ const eventData = {
872
+ sidecar_id: sidecarId,
873
+ ...(typeof event.payload === 'object' && event.payload !== null ? event.payload as Record<string, unknown> : { payload: event.payload }),
874
+ };
875
+ const observerEvent = {
876
+ type: eventType,
877
+ data: eventData,
878
+ timestamp: event.timestamp ?? Date.now(),
879
+ };
880
+
881
+ // Classify and route
882
+ const classified = classifyEvent(observerEvent);
883
+ if (classified.priority === 'critical' || classified.priority === 'high') {
884
+ reactor.react(classified).catch(err =>
885
+ console.error('[Daemon] Sidecar event reaction error:', err)
886
+ );
887
+ } else {
888
+ coalescer.addEvent(classified);
889
+ }
890
+
891
+ // Broadcast to dashboard
892
+ wsService.broadcastSidecarEvent(sidecarId, observerEvent);
893
+ });
894
+
895
+ // 11. Start health monitoring
896
+ healthMonitor.start(config.healthCheckInterval);
897
+
898
+ // 12. Set up heartbeat timer with configurable interval and active hours
899
+ const heartbeatIntervalMs = (heartbeatConfig?.interval_minutes ?? 15) * 60 * 1000;
900
+ const activeHours = heartbeatConfig?.active_hours ?? { start: 8, end: 23 };
901
+
902
+ console.log(`[Daemon] Heartbeat interval: ${heartbeatConfig?.interval_minutes ?? 15} min, active hours: ${activeHours.start}:00-${activeHours.end}:00`);
903
+
904
+ const HEARTBEAT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minute timeout for heartbeat
905
+ let heartbeatBusy = false;
906
+ heartbeatTimer = setInterval(async () => {
907
+ if (heartbeatBusy) {
908
+ console.log('[Daemon] Skipping heartbeat — previous still running');
909
+ return;
910
+ }
911
+ // Check if within active hours
912
+ const currentHour = new Date().getHours();
913
+ if (currentHour < activeHours.start || currentHour >= activeHours.end) {
914
+ console.log(`[Daemon] Outside active hours (${activeHours.start}-${activeHours.end}), skipping heartbeat`);
915
+ return;
916
+ }
917
+
918
+ heartbeatBusy = true;
919
+ console.log('[Daemon] Heartbeat starting...');
920
+ try {
921
+ // Check commitments and route critical/high ones to reactor
922
+ const commitmentEvents = checkCommitments();
923
+ for (const evt of commitmentEvents) {
924
+ if (evt.priority === 'critical' || evt.priority === 'high') {
925
+ reactor.react(evt).catch(err =>
926
+ console.error('[Daemon] Commitment reaction error:', err)
927
+ );
928
+ } else {
929
+ coalescer.addEvent(evt);
930
+ }
931
+ }
932
+
933
+ // Flush coalesced events for heartbeat
934
+ const coalescedSummary = coalescer.flush();
935
+
936
+ // Run heartbeat on BACKGROUND agent with timeout to prevent stuck busy lock
937
+ const heartbeatPromise = bgAgentService.handleHeartbeat(
938
+ coalescedSummary || undefined
939
+ );
940
+ const timeoutPromise = new Promise<null>((resolve) =>
941
+ setTimeout(() => {
942
+ console.error('[Daemon] Heartbeat timed out after 5 minutes');
943
+ resolve(null);
944
+ }, HEARTBEAT_TIMEOUT_MS)
945
+ );
946
+
947
+ const heartbeatResponse = await Promise.race([heartbeatPromise, timeoutPromise]);
948
+
949
+ if (heartbeatResponse) {
950
+ console.log('[Daemon] Heartbeat response:', heartbeatResponse.slice(0, 200));
951
+ wsService.broadcastHeartbeat(heartbeatResponse);
952
+ } else {
953
+ console.log('[Daemon] Heartbeat returned no response (busy or timed out)');
954
+ }
955
+ } catch (err) {
956
+ console.error('[Daemon] Heartbeat error:', err);
957
+ } finally {
958
+ heartbeatBusy = false;
959
+ }
960
+ }, heartbeatIntervalMs);
961
+
962
+ logWithTimestamp(`JARVIS daemon running on port ${config.port}`);
963
+ console.log('');
964
+ console.log('Press Ctrl+C to stop');
965
+ console.log('');
966
+
967
+ // Print initial health status
968
+ console.log(healthMonitor.formatHealth());
969
+ console.log('');
970
+
971
+ } catch (error) {
972
+ console.error('[Daemon] Fatal error during startup:', error);
973
+ process.exit(1);
974
+ }
975
+ }
976
+
977
+ // Register signal handlers
978
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
979
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
980
+
981
+ // Handle uncaught errors
982
+ process.on('uncaughtException', (error) => {
983
+ console.error('[Daemon] Uncaught exception:', error);
984
+ handleShutdown('uncaughtException');
985
+ });
986
+
987
+ process.on('unhandledRejection', (reason) => {
988
+ const msg = reason instanceof Error ? reason.message : String(reason);
989
+
990
+ // Browser timeouts and CDP errors should NOT crash the daemon
991
+ if (msg.includes('Timeout waiting for') || msg.includes('CDP')) {
992
+ console.warn('[Daemon] Non-fatal browser error (ignoring):', msg);
993
+ return;
994
+ }
995
+
996
+ console.error('[Daemon] Unhandled rejection:', reason);
997
+ handleShutdown('unhandledRejection');
998
+ });
999
+
1000
+ // Run as CLI if executed directly
1001
+ if (import.meta.main) {
1002
+ const args = parseArgs();
1003
+ await startDaemon(args);
1004
+ }