@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.
- package/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- 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
|
+
}
|