@agentdeck/bridge 0.1.0 → 0.2.1
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 +21 -0
- package/dist/adapters/claude-code.d.ts +11 -27
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +29 -123
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/codex-cli.d.ts +21 -0
- package/dist/adapters/codex-cli.d.ts.map +1 -0
- package/dist/adapters/codex-cli.js +53 -0
- package/dist/adapters/codex-cli.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +13 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/monitor.d.ts +34 -0
- package/dist/adapters/monitor.d.ts.map +1 -0
- package/dist/adapters/monitor.js +86 -0
- package/dist/adapters/monitor.js.map +1 -0
- package/dist/adapters/openclaw.d.ts +36 -2
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +329 -59
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/adapters/opencode-adapter.d.ts +48 -0
- package/dist/adapters/opencode-adapter.d.ts.map +1 -0
- package/dist/adapters/opencode-adapter.js +345 -0
- package/dist/adapters/opencode-adapter.js.map +1 -0
- package/dist/adapters/pty-adapter.d.ts +56 -0
- package/dist/adapters/pty-adapter.d.ts.map +1 -0
- package/dist/adapters/pty-adapter.js +143 -0
- package/dist/adapters/pty-adapter.js.map +1 -0
- package/dist/adb-reverse.d.ts +25 -0
- package/dist/adb-reverse.d.ts.map +1 -0
- package/dist/adb-reverse.js +129 -0
- package/dist/adb-reverse.js.map +1 -0
- package/dist/antigravity-local.d.ts +7 -0
- package/dist/antigravity-local.d.ts.map +1 -0
- package/dist/antigravity-local.js +153 -0
- package/dist/antigravity-local.js.map +1 -0
- package/dist/apme/adapters/claude-hook.d.ts +15 -0
- package/dist/apme/adapters/claude-hook.d.ts.map +1 -0
- package/dist/apme/adapters/claude-hook.js +85 -0
- package/dist/apme/adapters/claude-hook.js.map +1 -0
- package/dist/apme/adapters/claude-pty.d.ts +28 -0
- package/dist/apme/adapters/claude-pty.d.ts.map +1 -0
- package/dist/apme/adapters/claude-pty.js +88 -0
- package/dist/apme/adapters/claude-pty.js.map +1 -0
- package/dist/apme/adapters/timeline.d.ts +16 -0
- package/dist/apme/adapters/timeline.d.ts.map +1 -0
- package/dist/apme/adapters/timeline.js +66 -0
- package/dist/apme/adapters/timeline.js.map +1 -0
- package/dist/apme/classifier.d.ts +53 -0
- package/dist/apme/classifier.d.ts.map +1 -0
- package/dist/apme/classifier.js +266 -0
- package/dist/apme/classifier.js.map +1 -0
- package/dist/apme/claude-transcript-reader.d.ts +38 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -0
- package/dist/apme/claude-transcript-reader.js +127 -0
- package/dist/apme/claude-transcript-reader.js.map +1 -0
- package/dist/apme/collector.d.ts +108 -0
- package/dist/apme/collector.d.ts.map +1 -0
- package/dist/apme/collector.js +659 -0
- package/dist/apme/collector.js.map +1 -0
- package/dist/apme/dashboard-html.d.ts +9 -0
- package/dist/apme/dashboard-html.d.ts.map +1 -0
- package/dist/apme/dashboard-html.js +416 -0
- package/dist/apme/dashboard-html.js.map +1 -0
- package/dist/apme/http.d.ts +19 -0
- package/dist/apme/http.d.ts.map +1 -0
- package/dist/apme/http.js +210 -0
- package/dist/apme/http.js.map +1 -0
- package/dist/apme/hw-sampler.d.ts +30 -0
- package/dist/apme/hw-sampler.d.ts.map +1 -0
- package/dist/apme/hw-sampler.js +106 -0
- package/dist/apme/hw-sampler.js.map +1 -0
- package/dist/apme/index.d.ts +43 -0
- package/dist/apme/index.d.ts.map +1 -0
- package/dist/apme/index.js +75 -0
- package/dist/apme/index.js.map +1 -0
- package/dist/apme/outcome.d.ts +48 -0
- package/dist/apme/outcome.d.ts.map +1 -0
- package/dist/apme/outcome.js +232 -0
- package/dist/apme/outcome.js.map +1 -0
- package/dist/apme/recommend.d.ts +32 -0
- package/dist/apme/recommend.d.ts.map +1 -0
- package/dist/apme/recommend.js +41 -0
- package/dist/apme/recommend.js.map +1 -0
- package/dist/apme/runner.d.ts +94 -0
- package/dist/apme/runner.d.ts.map +1 -0
- package/dist/apme/runner.js +812 -0
- package/dist/apme/runner.js.map +1 -0
- package/dist/apme/settings.d.ts +65 -0
- package/dist/apme/settings.d.ts.map +1 -0
- package/dist/apme/settings.js +97 -0
- package/dist/apme/settings.js.map +1 -0
- package/dist/apme/store.d.ts +99 -0
- package/dist/apme/store.d.ts.map +1 -0
- package/dist/apme/store.js +911 -0
- package/dist/apme/store.js.map +1 -0
- package/dist/apme/tuner.d.ts +64 -0
- package/dist/apme/tuner.d.ts.map +1 -0
- package/dist/apme/tuner.js +297 -0
- package/dist/apme/tuner.js.map +1 -0
- package/dist/apme/types.d.ts +11 -0
- package/dist/apme/types.d.ts.map +1 -0
- package/dist/apme/types.js +10 -0
- package/dist/apme/types.js.map +1 -0
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +15 -5
- package/dist/auth.js.map +1 -1
- package/dist/bridge-core.d.ts +162 -0
- package/dist/bridge-core.d.ts.map +1 -0
- package/dist/bridge-core.js +627 -0
- package/dist/bridge-core.js.map +1 -0
- package/dist/check-deps.d.ts +3 -1
- package/dist/check-deps.d.ts.map +1 -1
- package/dist/check-deps.js +34 -13
- package/dist/check-deps.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1235 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-auth.d.ts +10 -0
- package/dist/codex-auth.d.ts.map +1 -0
- package/dist/codex-auth.js +63 -0
- package/dist/codex-auth.js.map +1 -0
- package/dist/codex-output-parser.d.ts +21 -0
- package/dist/codex-output-parser.d.ts.map +1 -0
- package/dist/codex-output-parser.js +253 -0
- package/dist/codex-output-parser.js.map +1 -0
- package/dist/d200h/hid-protocol.d.ts +62 -0
- package/dist/d200h/hid-protocol.d.ts.map +1 -0
- package/dist/d200h/hid-protocol.js +124 -0
- package/dist/d200h/hid-protocol.js.map +1 -0
- package/dist/d200h/image-renderer.d.ts +36 -0
- package/dist/d200h/image-renderer.d.ts.map +1 -0
- package/dist/d200h/image-renderer.js +487 -0
- package/dist/d200h/image-renderer.js.map +1 -0
- package/dist/daemon-server.d.ts +19 -0
- package/dist/daemon-server.d.ts.map +1 -0
- package/dist/daemon-server.js +1168 -0
- package/dist/daemon-server.js.map +1 -0
- package/dist/daemon-ws-client.d.ts +82 -0
- package/dist/daemon-ws-client.d.ts.map +1 -0
- package/dist/daemon-ws-client.js +179 -0
- package/dist/daemon-ws-client.js.map +1 -0
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +56 -0
- package/dist/daemon.js.map +1 -0
- package/dist/display-monitor.d.ts +25 -0
- package/dist/display-monitor.d.ts.map +1 -0
- package/dist/display-monitor.js +143 -0
- package/dist/display-monitor.js.map +1 -0
- package/dist/esp32-serial.d.ts +112 -0
- package/dist/esp32-serial.d.ts.map +1 -0
- package/dist/esp32-serial.js +502 -0
- package/dist/esp32-serial.js.map +1 -0
- package/dist/gateway-probe.d.ts +12 -0
- package/dist/gateway-probe.d.ts.map +1 -0
- package/dist/gateway-probe.js +39 -0
- package/dist/gateway-probe.js.map +1 -0
- package/dist/hook-migration.d.ts +2 -0
- package/dist/hook-migration.d.ts.map +1 -0
- package/dist/hook-migration.js +104 -0
- package/dist/hook-migration.js.map +1 -0
- package/dist/hook-server.d.ts +29 -0
- package/dist/hook-server.d.ts.map +1 -1
- package/dist/hook-server.js +366 -8
- package/dist/hook-server.js.map +1 -1
- package/dist/index.d.ts +20 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1304 -562
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +26 -0
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +111 -0
- package/dist/log-stream.js.map +1 -0
- package/dist/logger.d.ts +9 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +36 -5
- package/dist/logger.js.map +1 -1
- package/dist/mdns.d.ts +15 -1
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +143 -28
- package/dist/mdns.js.map +1 -1
- package/dist/mlx-probe.d.ts +14 -0
- package/dist/mlx-probe.d.ts.map +1 -0
- package/dist/mlx-probe.js +44 -0
- package/dist/mlx-probe.js.map +1 -0
- package/dist/model-catalog.d.ts +5 -3
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +45 -29
- package/dist/model-catalog.js.map +1 -1
- package/dist/modules/adb-module.d.ts +14 -0
- package/dist/modules/adb-module.d.ts.map +1 -0
- package/dist/modules/adb-module.js +36 -0
- package/dist/modules/adb-module.js.map +1 -0
- package/dist/modules/d200h-module.d.ts +43 -0
- package/dist/modules/d200h-module.d.ts.map +1 -0
- package/dist/modules/d200h-module.js +360 -0
- package/dist/modules/d200h-module.js.map +1 -0
- package/dist/modules/d200h-renderer.d.ts +7 -0
- package/dist/modules/d200h-renderer.d.ts.map +1 -0
- package/dist/modules/d200h-renderer.js +321 -0
- package/dist/modules/d200h-renderer.js.map +1 -0
- package/dist/modules/index.d.ts +23 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +65 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/mdns-module.d.ts +12 -0
- package/dist/modules/mdns-module.d.ts.map +1 -0
- package/dist/modules/mdns-module.js +21 -0
- package/dist/modules/mdns-module.js.map +1 -0
- package/dist/modules/pixoo-module.d.ts +8 -0
- package/dist/modules/pixoo-module.d.ts.map +1 -0
- package/dist/modules/pixoo-module.js +24 -0
- package/dist/modules/pixoo-module.js.map +1 -0
- package/dist/modules/serial-module.d.ts +18 -0
- package/dist/modules/serial-module.d.ts.map +1 -0
- package/dist/modules/serial-module.js +47 -0
- package/dist/modules/serial-module.js.map +1 -0
- package/dist/modules/types.d.ts +39 -0
- package/dist/modules/types.d.ts.map +1 -0
- package/dist/modules/types.js +2 -0
- package/dist/modules/types.js.map +1 -0
- package/dist/ollama-probe.d.ts +13 -0
- package/dist/ollama-probe.d.ts.map +1 -0
- package/dist/ollama-probe.js +46 -0
- package/dist/ollama-probe.js.map +1 -0
- package/dist/opencode-client.d.ts +126 -0
- package/dist/opencode-client.d.ts.map +1 -0
- package/dist/opencode-client.js +181 -0
- package/dist/opencode-client.js.map +1 -0
- package/dist/output-parser.d.ts +3 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +29 -3
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +38 -0
- package/dist/passive-observer.d.ts.map +1 -0
- package/dist/passive-observer.js +632 -0
- package/dist/passive-observer.js.map +1 -0
- package/dist/pixoo/pixoo-bridge.d.ts +50 -0
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -0
- package/dist/pixoo/pixoo-bridge.js +280 -0
- package/dist/pixoo/pixoo-bridge.js.map +1 -0
- package/dist/pixoo/pixoo-camera.d.ts +80 -0
- package/dist/pixoo/pixoo-camera.d.ts.map +1 -0
- package/dist/pixoo/pixoo-camera.js +271 -0
- package/dist/pixoo/pixoo-camera.js.map +1 -0
- package/dist/pixoo/pixoo-client.d.ts +67 -0
- package/dist/pixoo/pixoo-client.d.ts.map +1 -0
- package/dist/pixoo/pixoo-client.js +335 -0
- package/dist/pixoo/pixoo-client.js.map +1 -0
- package/dist/pixoo/pixoo-font.d.ts +24 -0
- package/dist/pixoo/pixoo-font.d.ts.map +1 -0
- package/dist/pixoo/pixoo-font.js +121 -0
- package/dist/pixoo/pixoo-font.js.map +1 -0
- package/dist/pixoo/pixoo-renderer.d.ts +32 -0
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -0
- package/dist/pixoo/pixoo-renderer.js +696 -0
- package/dist/pixoo/pixoo-renderer.js.map +1 -0
- package/dist/pixoo/pixoo-settings.d.ts +10 -0
- package/dist/pixoo/pixoo-settings.d.ts.map +1 -0
- package/dist/pixoo/pixoo-settings.js +47 -0
- package/dist/pixoo/pixoo-settings.js.map +1 -0
- package/dist/pixoo/pixoo-sprites.d.ts +187 -0
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -0
- package/dist/pixoo/pixoo-sprites.js +926 -0
- package/dist/pixoo/pixoo-sprites.js.map +1 -0
- package/dist/pty-manager.d.ts +1 -1
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +36 -15
- package/dist/pty-manager.js.map +1 -1
- package/dist/session-aggregator.d.ts +41 -0
- package/dist/session-aggregator.d.ts.map +1 -0
- package/dist/session-aggregator.js +76 -0
- package/dist/session-aggregator.js.map +1 -0
- package/dist/session-focus-relay.d.ts +35 -0
- package/dist/session-focus-relay.d.ts.map +1 -0
- package/dist/session-focus-relay.js +127 -0
- package/dist/session-focus-relay.js.map +1 -0
- package/dist/session-registry.d.ts +88 -1
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +222 -15
- package/dist/session-registry.js.map +1 -1
- package/dist/session-timeline-relay.d.ts +27 -0
- package/dist/session-timeline-relay.d.ts.map +1 -0
- package/dist/session-timeline-relay.js +124 -0
- package/dist/session-timeline-relay.js.map +1 -0
- package/dist/state-machine.d.ts +1 -0
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +14 -0
- package/dist/state-machine.js.map +1 -1
- package/dist/terminal-status.d.ts +28 -0
- package/dist/terminal-status.d.ts.map +1 -0
- package/dist/terminal-status.js +152 -0
- package/dist/terminal-status.js.map +1 -0
- package/dist/timeline-store.d.ts +22 -0
- package/dist/timeline-store.d.ts.map +1 -0
- package/dist/timeline-store.js +79 -0
- package/dist/timeline-store.js.map +1 -0
- package/dist/timeline-summarizer.d.ts +14 -0
- package/dist/timeline-summarizer.d.ts.map +1 -0
- package/dist/timeline-summarizer.js +154 -0
- package/dist/timeline-summarizer.js.map +1 -0
- package/dist/tts.d.ts +25 -0
- package/dist/tts.d.ts.map +1 -0
- package/dist/tts.js +99 -0
- package/dist/tts.js.map +1 -0
- package/dist/tui/ansi.d.ts +92 -0
- package/dist/tui/ansi.d.ts.map +1 -0
- package/dist/tui/ansi.js +181 -0
- package/dist/tui/ansi.js.map +1 -0
- package/dist/tui/dashboard.d.ts +35 -0
- package/dist/tui/dashboard.d.ts.map +1 -0
- package/dist/tui/dashboard.js +436 -0
- package/dist/tui/dashboard.js.map +1 -0
- package/dist/tui/gauge.d.ts +15 -0
- package/dist/tui/gauge.d.ts.map +1 -0
- package/dist/tui/gauge.js +82 -0
- package/dist/tui/gauge.js.map +1 -0
- package/dist/tui/renderer.d.ts +15 -0
- package/dist/tui/renderer.d.ts.map +1 -0
- package/dist/tui/renderer.js +758 -0
- package/dist/tui/renderer.js.map +1 -0
- package/dist/tui/screen.d.ts +27 -0
- package/dist/tui/screen.d.ts.map +1 -0
- package/dist/tui/screen.js +109 -0
- package/dist/tui/screen.js.map +1 -0
- package/dist/tui/terrarium.d.ts +96 -0
- package/dist/tui/terrarium.d.ts.map +1 -0
- package/dist/tui/terrarium.js +912 -0
- package/dist/tui/terrarium.js.map +1 -0
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -2
- package/dist/types.js.map +1 -1
- package/dist/usage-api.d.ts +9 -2
- package/dist/usage-api.d.ts.map +1 -1
- package/dist/usage-api.js +224 -42
- package/dist/usage-api.js.map +1 -1
- package/dist/usage-event.d.ts +12 -0
- package/dist/usage-event.d.ts.map +1 -0
- package/dist/usage-event.js +78 -0
- package/dist/usage-event.js.map +1 -0
- package/dist/usage-tracker.d.ts +4 -0
- package/dist/usage-tracker.d.ts.map +1 -1
- package/dist/usage-tracker.js +17 -0
- package/dist/usage-tracker.js.map +1 -1
- package/dist/utility-proxy.d.ts +30 -0
- package/dist/utility-proxy.d.ts.map +1 -0
- package/dist/utility-proxy.js +217 -0
- package/dist/utility-proxy.js.map +1 -0
- package/dist/utils/project-name.d.ts +22 -0
- package/dist/utils/project-name.d.ts.map +1 -0
- package/dist/utils/project-name.js +72 -0
- package/dist/utils/project-name.js.map +1 -0
- package/dist/version-check.d.ts +26 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +225 -0
- package/dist/version-check.js.map +1 -0
- package/dist/voice-assistant.d.ts +63 -0
- package/dist/voice-assistant.d.ts.map +1 -0
- package/dist/voice-assistant.js +406 -0
- package/dist/voice-assistant.js.map +1 -0
- package/dist/voice.d.ts +2 -0
- package/dist/voice.d.ts.map +1 -1
- package/dist/voice.js +26 -1
- package/dist/voice.js.map +1 -1
- package/dist/wake-word.d.ts +35 -0
- package/dist/wake-word.d.ts.map +1 -0
- package/dist/wake-word.js +225 -0
- package/dist/wake-word.js.map +1 -0
- package/dist/whisper-server-manager.d.ts +1 -1
- package/dist/whisper-server-manager.js +1 -1
- package/dist/wifi-config.d.ts +24 -0
- package/dist/wifi-config.d.ts.map +1 -0
- package/dist/wifi-config.js +84 -0
- package/dist/wifi-config.js.map +1 -0
- package/dist/ws-server.d.ts +12 -0
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +81 -3
- package/dist/ws-server.js.map +1 -1
- package/package.json +21 -13
|
@@ -0,0 +1,1168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentDeck Daemon — lightweight monitoring server.
|
|
3
|
+
*
|
|
4
|
+
* No PTY, no voice, no utility. Provides:
|
|
5
|
+
* - WS server for display clients
|
|
6
|
+
* - mDNS advertisement
|
|
7
|
+
* - OpenClaw Gateway proxy
|
|
8
|
+
* - Usage relay (sibling HTTP → WS → direct API)
|
|
9
|
+
* - Pixoo + ADB + Serial device modules
|
|
10
|
+
*
|
|
11
|
+
* Exports `startDaemon()` called by cli.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { createServer } from 'http';
|
|
14
|
+
import WebSocket from 'ws';
|
|
15
|
+
import { BridgeCore } from './bridge-core.js';
|
|
16
|
+
import { OpenClawAdapter } from './adapters/openclaw.js';
|
|
17
|
+
import { BridgeLogStream } from './log-stream.js';
|
|
18
|
+
import { PassiveSessionObserver } from './passive-observer.js';
|
|
19
|
+
import { SessionTimelineRelay } from './session-timeline-relay.js';
|
|
20
|
+
import { SessionFocusRelay } from './session-focus-relay.js';
|
|
21
|
+
import { updatePushState } from './session-aggregator.js';
|
|
22
|
+
import { VoiceManager } from './voice.js';
|
|
23
|
+
import { VoiceAssistantManager } from './voice-assistant.js';
|
|
24
|
+
import { listActive as listActiveSessions, findAvailablePort, findExistingDaemon, DAEMON_DEFAULT_PORT, probeDaemonHealth, writeDaemonInfo, removeDaemonInfo, readDaemonInfo, } from './session-registry.js';
|
|
25
|
+
import { fetchUsageFromApi, hasOAuthToken, resetConsecutiveFailures } from './usage-api.js';
|
|
26
|
+
import { isLocalConnection, validateToken } from './auth.js';
|
|
27
|
+
import { getLastFrame, renderPreviewFrame, onFrameRendered, offFrameRendered } from './pixoo/pixoo-bridge.js';
|
|
28
|
+
import { handlePixooWake } from './pixoo/pixoo-client.js';
|
|
29
|
+
import { triggerMdnsRecovery } from './mdns.js';
|
|
30
|
+
import { rgbToBmp, pixooLiveHtml } from './hook-server.js';
|
|
31
|
+
import { enableDebugLog, debug } from './logger.js';
|
|
32
|
+
import { initApme } from './apme/index.js';
|
|
33
|
+
import { handleApmeRequest } from './apme/http.js';
|
|
34
|
+
import { initModules, stopModules, createDefaultModules, } from './modules/index.js';
|
|
35
|
+
import { esp32ConnectionCount, getESP32DeviceInfo, onESP32Message, sendWifiProvisionToAll, handleESP32Wake } from './esp32-serial.js';
|
|
36
|
+
import { loadWifiConfig } from './wifi-config.js';
|
|
37
|
+
import { getConnectedAdbDevices, hasAdb } from './adb-reverse.js';
|
|
38
|
+
import { getPixooDeviceDetails, pixooDeviceCount } from './pixoo/pixoo-bridge.js';
|
|
39
|
+
import { getLanIp } from '@agentdeck/shared';
|
|
40
|
+
import { readFileSync } from 'fs';
|
|
41
|
+
import { join } from 'path';
|
|
42
|
+
import { homedir } from 'os';
|
|
43
|
+
import { OPENCLAW_CAPABILITIES, State, } from './types.js';
|
|
44
|
+
function loadDaemonSettings() {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(join(homedir(), '.agentdeck', 'settings.json'), 'utf-8'));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function log(msg) {
|
|
53
|
+
process.stderr.write(msg + '\n');
|
|
54
|
+
}
|
|
55
|
+
async function fetchUsageViaHttp(siblings) {
|
|
56
|
+
for (const sibling of siblings) {
|
|
57
|
+
try {
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
60
|
+
const res = await fetch(`http://127.0.0.1:${sibling.port}/usage`, { signal: controller.signal });
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
if (!res.ok)
|
|
63
|
+
continue;
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
if (!data.usage)
|
|
66
|
+
continue;
|
|
67
|
+
const age = Date.now() - data.fetchedAt;
|
|
68
|
+
if (age > 5 * 60 * 1000)
|
|
69
|
+
continue;
|
|
70
|
+
return { usage: data.usage, fetchedAt: data.fetchedAt };
|
|
71
|
+
}
|
|
72
|
+
catch { /* try next */ }
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
async function fetchUsageViaWs(siblings) {
|
|
77
|
+
for (const sibling of siblings) {
|
|
78
|
+
try {
|
|
79
|
+
const usage = await new Promise((resolve, reject) => {
|
|
80
|
+
const ws = new WebSocket(`ws://127.0.0.1:${sibling.port}`);
|
|
81
|
+
const timer = setTimeout(() => { ws.close(); reject(new Error('timeout')); }, 3000);
|
|
82
|
+
ws.on('message', (raw) => {
|
|
83
|
+
try {
|
|
84
|
+
const evt = JSON.parse(raw.toString());
|
|
85
|
+
if (evt.type === 'usage_update' && evt.fiveHourPercent != null) {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
ws.close();
|
|
88
|
+
resolve({
|
|
89
|
+
fiveHourPercent: evt.fiveHourPercent ?? null,
|
|
90
|
+
fiveHourResetsAt: evt.fiveHourResetsAt ?? null,
|
|
91
|
+
sevenDayPercent: evt.sevenDayPercent ?? null,
|
|
92
|
+
sevenDayResetsAt: evt.sevenDayResetsAt ?? null,
|
|
93
|
+
extraUsageEnabled: evt.extraUsageEnabled ?? false,
|
|
94
|
+
extraUsageMonthlyLimit: evt.extraUsageMonthlyLimit ?? null,
|
|
95
|
+
extraUsageUsedCredits: evt.extraUsageUsedCredits ?? null,
|
|
96
|
+
extraUsageUtilization: evt.extraUsageUtilization ?? null,
|
|
97
|
+
inferredBillingType: null,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch { /* ignore */ }
|
|
102
|
+
});
|
|
103
|
+
ws.on('error', () => { clearTimeout(timer); reject(new Error('ws error')); });
|
|
104
|
+
ws.on('close', () => { clearTimeout(timer); reject(new Error('ws closed')); });
|
|
105
|
+
});
|
|
106
|
+
if (usage)
|
|
107
|
+
return usage;
|
|
108
|
+
}
|
|
109
|
+
catch { /* try next */ }
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
async function fetchUsageRelayed(selfPort) {
|
|
114
|
+
const sessions = listActiveSessions();
|
|
115
|
+
const siblings = sessions.filter(s => s.port !== selfPort && s.agentType !== 'daemon');
|
|
116
|
+
if (siblings.length > 0) {
|
|
117
|
+
const httpResult = await fetchUsageViaHttp(siblings);
|
|
118
|
+
if (httpResult)
|
|
119
|
+
return httpResult.usage;
|
|
120
|
+
const wsResult = await fetchUsageViaWs(siblings);
|
|
121
|
+
if (wsResult)
|
|
122
|
+
return wsResult;
|
|
123
|
+
debug('daemon', 'Siblings exist but relay failed — skipping direct API');
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
debug('daemon', 'No siblings, using direct API');
|
|
127
|
+
return fetchUsageFromApi();
|
|
128
|
+
}
|
|
129
|
+
function buildNodeModuleHealth(startedModules) {
|
|
130
|
+
const started = new Set(startedModules.map((m) => m.name));
|
|
131
|
+
const modules = {};
|
|
132
|
+
if (started.has('adb')) {
|
|
133
|
+
const adbAvailable = hasAdb();
|
|
134
|
+
const devices = adbAvailable ? getConnectedAdbDevices() : [];
|
|
135
|
+
modules.adb = {
|
|
136
|
+
available: adbAvailable,
|
|
137
|
+
devices,
|
|
138
|
+
classifiedDevices: [],
|
|
139
|
+
reverseReadyCount: devices.length,
|
|
140
|
+
lastError: adbAvailable ? null : 'adb not found',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const d200h = startedModules.find((m) => m.name === 'd200h');
|
|
144
|
+
if (d200h?.statusSnapshot) {
|
|
145
|
+
modules.d200h = d200h.statusSnapshot();
|
|
146
|
+
}
|
|
147
|
+
if (started.has('pixoo') || pixooDeviceCount() > 0) {
|
|
148
|
+
const details = getPixooDeviceDetails();
|
|
149
|
+
modules.pixoo = {
|
|
150
|
+
configuredDeviceCount: pixooDeviceCount(),
|
|
151
|
+
deviceIps: details.map((d) => d.ip),
|
|
152
|
+
hasFrame: true,
|
|
153
|
+
displayDimmed: false,
|
|
154
|
+
devices: details.map((d) => ({
|
|
155
|
+
ip: d.ip,
|
|
156
|
+
name: d.name,
|
|
157
|
+
online: !d.backedOff,
|
|
158
|
+
failures: d.failures,
|
|
159
|
+
backedOff: d.backedOff,
|
|
160
|
+
})),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (started.has('serial')) {
|
|
164
|
+
const connections = getESP32DeviceInfo().map((info) => ({
|
|
165
|
+
port: info.port,
|
|
166
|
+
connected: true,
|
|
167
|
+
deviceInfo: {
|
|
168
|
+
board: info.board,
|
|
169
|
+
version: info.version,
|
|
170
|
+
wifiConfigured: info.wifiConfigured,
|
|
171
|
+
wifiConnected: info.wifiConnected,
|
|
172
|
+
},
|
|
173
|
+
}));
|
|
174
|
+
modules.serial = {
|
|
175
|
+
connectedPorts: connections.map((c) => c.port),
|
|
176
|
+
connections,
|
|
177
|
+
lastError: null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return modules;
|
|
181
|
+
}
|
|
182
|
+
// ===== startDaemon =====
|
|
183
|
+
export async function startDaemon(opts) {
|
|
184
|
+
if (opts.debug) {
|
|
185
|
+
enableDebugLog('/tmp/agentdeck-debug.log');
|
|
186
|
+
log('[agentdeck] Debug logging enabled');
|
|
187
|
+
}
|
|
188
|
+
// CLI --wake-word flag OR settings.json wakeWord: true
|
|
189
|
+
const settings = loadDaemonSettings();
|
|
190
|
+
const wakeWordEnabled = opts.wakeWord || settings.wakeWord === true;
|
|
191
|
+
// ===== Singleton guard + port allocation =====
|
|
192
|
+
// 1. Check daemon.json and sessions.json for existing daemon
|
|
193
|
+
const existingInfo = readDaemonInfo();
|
|
194
|
+
if (existingInfo) {
|
|
195
|
+
log(`[agentdeck] Daemon already running on port ${existingInfo.port} (PID ${existingInfo.pid}).`);
|
|
196
|
+
process.exit(0);
|
|
197
|
+
}
|
|
198
|
+
const existingSession = findExistingDaemon();
|
|
199
|
+
if (existingSession) {
|
|
200
|
+
log(`[agentdeck] Daemon already running on port ${existingSession.port} (PID ${existingSession.pid}).`);
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
// 2. Determine port — try default first, fallback if occupied by non-daemon
|
|
204
|
+
const requestedPort = opts.port ?? DAEMON_DEFAULT_PORT;
|
|
205
|
+
let port = requestedPort;
|
|
206
|
+
// If using default port, check if it's available
|
|
207
|
+
if (requestedPort === DAEMON_DEFAULT_PORT) {
|
|
208
|
+
const health = await probeDaemonHealth(requestedPort);
|
|
209
|
+
if (health) {
|
|
210
|
+
if (health.mode === 'daemon') {
|
|
211
|
+
// Daemon alive but not in our registry — race condition or stale state
|
|
212
|
+
log(`[agentdeck] Daemon already running on port ${requestedPort} (detected via /health).`);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
// Port occupied by non-daemon (e.g. session bridge) — find alternative
|
|
216
|
+
log(`[agentdeck] Port ${requestedPort} occupied (${health.mode ?? 'unknown'}), finding alternative...`);
|
|
217
|
+
port = await findAvailablePort();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
log(`[agentdeck] Starting daemon on port ${port}...`);
|
|
221
|
+
// ===== APME (lazy — may be null if better-sqlite3 isn't installed) =====
|
|
222
|
+
let apme = null;
|
|
223
|
+
// Declare early — HTTP /health handler references this in its closure.
|
|
224
|
+
// Must be declared before the HTTP server so it's initialized (not in TDZ)
|
|
225
|
+
// when the first /health request arrives.
|
|
226
|
+
let gatewayAdapter = null;
|
|
227
|
+
let gatewayConnecting = false;
|
|
228
|
+
let moduleHealthProvider = () => ({});
|
|
229
|
+
// ===== HTTP server =====
|
|
230
|
+
const httpServer = createServer((req, res) => {
|
|
231
|
+
// APME routes: auth-gated (task prompts, project paths, hook payloads are sensitive).
|
|
232
|
+
if ((req.url ?? '').startsWith('/apme')) {
|
|
233
|
+
const ip = req.socket.remoteAddress ?? '';
|
|
234
|
+
if (!isLocalConnection(ip)) {
|
|
235
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
236
|
+
const token = url.searchParams.get('token') ?? '';
|
|
237
|
+
if (!validateToken(token)) {
|
|
238
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
239
|
+
res.end(JSON.stringify({ error: 'Unauthorized — token required for APME routes' }));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
void handleApmeRequest(req, res, apme).catch((err) => {
|
|
244
|
+
try {
|
|
245
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
246
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
247
|
+
}
|
|
248
|
+
catch { /* ignore */ }
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const pathname = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`).pathname;
|
|
253
|
+
// Health check is public (no auth) — used by iOS/Android for pairing token discovery
|
|
254
|
+
if (req.method === 'GET' && pathname === '/health') {
|
|
255
|
+
const snap = core.stateMachine.getSnapshot();
|
|
256
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
257
|
+
res.end(JSON.stringify({
|
|
258
|
+
status: 'ok', mode: 'daemon', state: snap.state,
|
|
259
|
+
gateway: gatewayAdapter?.isAlive() ? 'connected' : 'disconnected',
|
|
260
|
+
uptime: process.uptime(), port,
|
|
261
|
+
pairingToken: core.authToken,
|
|
262
|
+
modules: moduleHealthProvider(),
|
|
263
|
+
}));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (req.method === 'GET' && pathname === '/status') {
|
|
267
|
+
const snap = core.stateMachine.getSnapshot();
|
|
268
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
269
|
+
res.end(JSON.stringify({
|
|
270
|
+
status: 'ok',
|
|
271
|
+
state: snap.state,
|
|
272
|
+
daemon: { port, pid: process.pid },
|
|
273
|
+
gateway: {
|
|
274
|
+
available: core.cachedGatewayAvailable,
|
|
275
|
+
connected: core.cachedGatewayConnected,
|
|
276
|
+
hasError: core.cachedGatewayHasError,
|
|
277
|
+
},
|
|
278
|
+
clients: core.wsServer.getClientCount(),
|
|
279
|
+
modules: moduleHealthProvider(),
|
|
280
|
+
}));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (req.method === 'GET' && pathname === '/devices') {
|
|
284
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
285
|
+
res.end(JSON.stringify({ devices: [], modules: moduleHealthProvider() }));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (req.method === 'GET' && pathname === '/pixoo/stream') {
|
|
289
|
+
res.writeHead(200, {
|
|
290
|
+
'Content-Type': 'text/event-stream',
|
|
291
|
+
'Cache-Control': 'no-cache',
|
|
292
|
+
'Connection': 'keep-alive',
|
|
293
|
+
'Access-Control-Allow-Origin': '*',
|
|
294
|
+
});
|
|
295
|
+
res.on('error', () => { }); // Prevent unhandled stream error on client disconnect
|
|
296
|
+
const listener = (frame) => {
|
|
297
|
+
const bmp = rgbToBmp(frame, 64, 64);
|
|
298
|
+
const b64 = bmp.toString('base64');
|
|
299
|
+
try {
|
|
300
|
+
res.write(`event: frame\ndata: ${b64}\n\n`);
|
|
301
|
+
}
|
|
302
|
+
catch { /* client gone */ }
|
|
303
|
+
};
|
|
304
|
+
onFrameRendered(listener);
|
|
305
|
+
// Send current frame immediately
|
|
306
|
+
const current = getLastFrame() ?? renderPreviewFrame();
|
|
307
|
+
listener(current);
|
|
308
|
+
// Heartbeat
|
|
309
|
+
const heartbeat = setInterval(() => {
|
|
310
|
+
try {
|
|
311
|
+
res.write(':heartbeat\n\n');
|
|
312
|
+
}
|
|
313
|
+
catch { /* */ }
|
|
314
|
+
}, 30_000);
|
|
315
|
+
req.on('close', () => {
|
|
316
|
+
offFrameRendered(listener);
|
|
317
|
+
clearInterval(heartbeat);
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (req.method === 'GET' && pathname === '/pixoo/frame') {
|
|
322
|
+
const rgb = getLastFrame() ?? renderPreviewFrame();
|
|
323
|
+
const bmp = rgbToBmp(rgb, 64, 64);
|
|
324
|
+
res.writeHead(200, {
|
|
325
|
+
'Content-Type': 'image/bmp',
|
|
326
|
+
'Cache-Control': 'no-store',
|
|
327
|
+
'Access-Control-Allow-Origin': '*',
|
|
328
|
+
});
|
|
329
|
+
res.end(bmp);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (req.method === 'GET' && pathname === '/pixoo') {
|
|
333
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
334
|
+
res.end(pixooLiveHtml({ projectName: 'AgentDeck' }));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (req.method === 'GET' && pathname === '/sse') {
|
|
338
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
339
|
+
res.on('error', () => { }); // Prevent unhandled stream error on client disconnect
|
|
340
|
+
res.write(`event: connected\ndata: {}\n\n`);
|
|
341
|
+
req.on('close', () => { });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Hook endpoint — receives Claude Code hook POSTs at /hooks/:eventName.
|
|
345
|
+
// Routes through APME collector the same way session bridge's hook-server does.
|
|
346
|
+
if (req.method === 'POST' && pathname.startsWith('/hooks/')) {
|
|
347
|
+
const eventName = pathname.slice('/hooks/'.length);
|
|
348
|
+
let body = '';
|
|
349
|
+
req.on('data', (c) => { body += c; if (body.length > 1_000_000)
|
|
350
|
+
req.destroy(); });
|
|
351
|
+
req.on('end', () => {
|
|
352
|
+
let json = {};
|
|
353
|
+
try {
|
|
354
|
+
json = body ? JSON.parse(body) : {};
|
|
355
|
+
}
|
|
356
|
+
catch { /* ignore */ }
|
|
357
|
+
// Map PascalCase event names to snake_case for state machine + APME
|
|
358
|
+
const eventMap = {
|
|
359
|
+
SessionStart: 'session_start', SessionEnd: 'session_end',
|
|
360
|
+
PreToolUse: 'tool_start', PostToolUse: 'tool_end',
|
|
361
|
+
Stop: 'stop', UserPromptSubmit: 'user_prompt_submit',
|
|
362
|
+
Notification: 'notification',
|
|
363
|
+
};
|
|
364
|
+
const mapped = eventMap[eventName] ?? eventName;
|
|
365
|
+
// State machine
|
|
366
|
+
if (mapped === 'session_start')
|
|
367
|
+
core.stateMachine.handleHookEvent('SessionStart', json);
|
|
368
|
+
else if (mapped === 'session_end')
|
|
369
|
+
core.stateMachine.handleHookEvent('SessionEnd', json);
|
|
370
|
+
else if (mapped === 'user_prompt_submit')
|
|
371
|
+
core.stateMachine.handleHookEvent('UserPromptSubmit', json);
|
|
372
|
+
else if (mapped === 'stop')
|
|
373
|
+
core.stateMachine.handleHookEvent('Stop', json);
|
|
374
|
+
else if (mapped === 'tool_start') {
|
|
375
|
+
core.stateMachine.handleHookEvent('PreToolUse', json);
|
|
376
|
+
}
|
|
377
|
+
else if (mapped === 'tool_end') {
|
|
378
|
+
core.stateMachine.handleHookEvent('PostToolUse', json);
|
|
379
|
+
}
|
|
380
|
+
// APME collector
|
|
381
|
+
if (apme) {
|
|
382
|
+
// Use a stable "hook session" for the daemon — hooks from direct `claude` runs
|
|
383
|
+
// don't have AGENTDECK_PORT, so they all come here.
|
|
384
|
+
const hookSessionId = 'daemon-hook';
|
|
385
|
+
if (mapped === 'session_start') {
|
|
386
|
+
// Extract prompt source from message.content (Claude v2.1+) or prompt field
|
|
387
|
+
apme.collector.openRun({
|
|
388
|
+
sessionId: hookSessionId,
|
|
389
|
+
agentType: 'claude-code',
|
|
390
|
+
projectName: json.project_name ?? undefined,
|
|
391
|
+
projectPath: json.project_path ?? undefined,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
apme.collector.ingestHook(hookSessionId, mapped, json);
|
|
395
|
+
if (mapped === 'session_end') {
|
|
396
|
+
apme.collector.closeRun(hookSessionId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ received: true }));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (req.method === 'POST' && pathname === '/shutdown') {
|
|
405
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
406
|
+
res.end(JSON.stringify({ status: 'shutting_down' }));
|
|
407
|
+
core.shutdown();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
411
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
412
|
+
});
|
|
413
|
+
// Catch HTTP-level client errors (malformed requests, abrupt disconnects during upgrade)
|
|
414
|
+
httpServer.on('clientError', (err, socket) => {
|
|
415
|
+
debug('daemon', `HTTP client error: ${err.message}`);
|
|
416
|
+
if (!socket.destroyed)
|
|
417
|
+
socket.destroy();
|
|
418
|
+
});
|
|
419
|
+
await new Promise((resolve, reject) => {
|
|
420
|
+
httpServer.on('error', (err) => {
|
|
421
|
+
if (err.code === 'EADDRINUSE') {
|
|
422
|
+
// Port was grabbed between our check and bind — find alternative
|
|
423
|
+
reject(new Error(`EADDRINUSE:${port}`));
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
reject(err);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
httpServer.listen(port, '0.0.0.0', () => resolve());
|
|
430
|
+
}).catch(async (err) => {
|
|
431
|
+
// Handle race condition: port became unavailable after probe
|
|
432
|
+
if (err.message.startsWith('EADDRINUSE:') && port === requestedPort) {
|
|
433
|
+
port = await findAvailablePort();
|
|
434
|
+
log(`[agentdeck] Port ${requestedPort} grabbed, retrying on ${port}...`);
|
|
435
|
+
await new Promise((resolve, reject) => {
|
|
436
|
+
httpServer.on('error', (e) => reject(e));
|
|
437
|
+
httpServer.listen(port, '0.0.0.0', () => resolve());
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
// Write daemon.json for client discovery (must be after successful bind)
|
|
445
|
+
writeDaemonInfo({ port, pid: process.pid, startedAt: new Date().toISOString() });
|
|
446
|
+
// ===== BridgeCore =====
|
|
447
|
+
const core = new BridgeCore({
|
|
448
|
+
port,
|
|
449
|
+
projectName: 'AgentDeck',
|
|
450
|
+
httpServer,
|
|
451
|
+
});
|
|
452
|
+
// Timeline
|
|
453
|
+
const bridgeLogStream = new BridgeLogStream();
|
|
454
|
+
core.wireTimeline(bridgeLogStream);
|
|
455
|
+
core.wireDisplayMonitor();
|
|
456
|
+
// System wake recovery — re-publish mDNS, reconnect devices, refresh usage
|
|
457
|
+
core.onSystemWake(() => {
|
|
458
|
+
log('[daemon] System wake detected — recovering devices');
|
|
459
|
+
triggerMdnsRecovery();
|
|
460
|
+
handleESP32Wake();
|
|
461
|
+
handlePixooWake();
|
|
462
|
+
// Reset backoff from pre-sleep failures and fetch fresh usage after network stabilizes
|
|
463
|
+
resetConsecutiveFailures();
|
|
464
|
+
setTimeout(() => {
|
|
465
|
+
fetchUsageRelayed(port).then((usage) => {
|
|
466
|
+
if (usage)
|
|
467
|
+
core.updateApiUsage(usage);
|
|
468
|
+
else {
|
|
469
|
+
core.oauthConnected = hasOAuthToken();
|
|
470
|
+
if (core.cachedApiUsage)
|
|
471
|
+
core.apiUsageStale = true;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}, 4000);
|
|
475
|
+
});
|
|
476
|
+
// Subscribe to sibling session bridges' timelines + modelCatalog relay
|
|
477
|
+
const timelineRelay = new SessionTimelineRelay(port, core.bridgeTimeline);
|
|
478
|
+
timelineRelay.setOnModelCatalog((models) => {
|
|
479
|
+
// Merge modelCatalog from sibling Claude Code sessions (daemon doesn't run PTY).
|
|
480
|
+
// Gateway may also set catalog — merge both, dedup by key.
|
|
481
|
+
const existing = core.cachedModelCatalog ?? [];
|
|
482
|
+
const existingKeys = new Set(existing.map(m => m.key));
|
|
483
|
+
const merged = [...existing];
|
|
484
|
+
for (const m of models) {
|
|
485
|
+
if (!existingKeys.has(m.key)) {
|
|
486
|
+
merged.push(m);
|
|
487
|
+
existingKeys.add(m.key);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (merged.length !== existing.length) {
|
|
491
|
+
core.cachedModelCatalog = merged;
|
|
492
|
+
debug('daemon', `Model catalog merged from sibling: ${merged.length} models total`);
|
|
493
|
+
const snap = core.stateMachine.getSnapshot();
|
|
494
|
+
const stateEvent = core.buildStateEvent({
|
|
495
|
+
agentType: gatewayAdapter?.isAlive() ? 'openclaw' : 'daemon',
|
|
496
|
+
agentCapabilities: gatewayAdapter?.isAlive() ? OPENCLAW_CAPABILITIES : undefined,
|
|
497
|
+
snapshot: snap,
|
|
498
|
+
});
|
|
499
|
+
lastStateEvent = stateEvent;
|
|
500
|
+
core.broadcast(stateEvent);
|
|
501
|
+
core.broadcastUsage();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
timelineRelay.start();
|
|
505
|
+
// Session focus relay — allows SD plugin to interact with a specific session via daemon
|
|
506
|
+
const focusRelay = new SessionFocusRelay();
|
|
507
|
+
focusRelay.setEventHandler((evt) => {
|
|
508
|
+
if (evt.type === 'state_update') {
|
|
509
|
+
// Merge daemon metadata into the session's state_update
|
|
510
|
+
const merged = {
|
|
511
|
+
...evt,
|
|
512
|
+
sessionId: focusRelay.getFocusedSessionId(),
|
|
513
|
+
modelCatalog: evt.modelCatalog ?? core.cachedModelCatalog,
|
|
514
|
+
gatewayAvailable: core.cachedGatewayAvailable,
|
|
515
|
+
gatewayConnected: core.cachedGatewayConnected,
|
|
516
|
+
ollamaStatus: core.cachedOllamaStatus,
|
|
517
|
+
gatewayHasError: evt.gatewayHasError ?? core.cachedGatewayHasError,
|
|
518
|
+
moduleHealth: moduleHealthProvider(),
|
|
519
|
+
};
|
|
520
|
+
lastStateEvent = merged;
|
|
521
|
+
core.wsServer.broadcast(merged);
|
|
522
|
+
}
|
|
523
|
+
else if (evt.type === 'usage_update') {
|
|
524
|
+
// Sync daemon cache with relay's already-adjusted values (prevents oscillation)
|
|
525
|
+
const u = evt;
|
|
526
|
+
if (core.cachedApiUsage && u.fiveHourPercent != null) {
|
|
527
|
+
core.cachedApiUsage.fiveHourPercent = u.fiveHourPercent;
|
|
528
|
+
core.cachedApiUsage.fiveHourResetsAt = u.fiveHourResetsAt ?? null;
|
|
529
|
+
core.cachedApiUsage.sevenDayPercent = u.sevenDayPercent ?? core.cachedApiUsage.sevenDayPercent;
|
|
530
|
+
core.cachedApiUsage.sevenDayResetsAt = u.sevenDayResetsAt ?? core.cachedApiUsage.sevenDayResetsAt ?? null;
|
|
531
|
+
core.apiUsagePreAdjusted = true;
|
|
532
|
+
}
|
|
533
|
+
core.wsServer.broadcast(evt);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// prompt_options — relay as-is
|
|
537
|
+
core.wsServer.broadcast(evt);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
// mDNS + device modules
|
|
541
|
+
const deviceModules = createDefaultModules('daemon');
|
|
542
|
+
const startedModules = await initModules(deviceModules, { mdns: true, adb: 'auto', serial: 'auto', pixoo: 'auto', d200h: 'auto' }, { port, authToken: core.authToken, projectName: 'AgentDeck', wsServer: core.wsServer });
|
|
543
|
+
moduleHealthProvider = () => buildNodeModuleHealth(startedModules);
|
|
544
|
+
core.setModuleHealthProvider(moduleHealthProvider);
|
|
545
|
+
// Serial module state provider (heartbeat needs cached state)
|
|
546
|
+
let lastStateEvent = null;
|
|
547
|
+
const serialModule = startedModules.find(m => m.name === 'serial');
|
|
548
|
+
if (serialModule) {
|
|
549
|
+
serialModule.setStateProvider(() => lastStateEvent);
|
|
550
|
+
serialModule.setUsageProvider(() => core.buildUsage());
|
|
551
|
+
// Send full state (state + usage + sessions) when new ESP32 device connects
|
|
552
|
+
serialModule.setInitialStateProvider(() => {
|
|
553
|
+
const events = [];
|
|
554
|
+
if (lastStateEvent)
|
|
555
|
+
events.push(lastStateEvent);
|
|
556
|
+
events.push(core.buildUsage());
|
|
557
|
+
events.push({ type: 'display_state', displayOn: core.displayMonitor.isDisplayOn() });
|
|
558
|
+
// Sessions list (async enrichment runs synchronously from cache here)
|
|
559
|
+
core.broadcastSessionsList().catch(() => { });
|
|
560
|
+
return events;
|
|
561
|
+
});
|
|
562
|
+
// Include ESP32 serial connections in client count for polling guards
|
|
563
|
+
core.setExternalClientCountProvider(() => esp32ConnectionCount());
|
|
564
|
+
// WiFi auto-provisioning for ESP32 (enables independent WiFi operation)
|
|
565
|
+
const wifiConfig = loadWifiConfig();
|
|
566
|
+
if (wifiConfig?.autoProvision) {
|
|
567
|
+
const lanIp = getLanIp();
|
|
568
|
+
onESP32Message((portPath, msg) => {
|
|
569
|
+
if (msg.type === 'device_info' && !msg.wifiConnected) {
|
|
570
|
+
sendWifiProvisionToAll({
|
|
571
|
+
type: 'wifi_provision',
|
|
572
|
+
ssid: wifiConfig.ssid,
|
|
573
|
+
password: wifiConfig.password,
|
|
574
|
+
bridgeIp: lanIp,
|
|
575
|
+
bridgePort: port,
|
|
576
|
+
authToken: core.authToken,
|
|
577
|
+
});
|
|
578
|
+
log(`[agentdeck] WiFi provision sent to ESP32 on ${portPath}`);
|
|
579
|
+
}
|
|
580
|
+
else if (msg.type === 'wifi_provision_ack') {
|
|
581
|
+
log(msg.success ? `[agentdeck] ESP32 WiFi connected: ${msg.ip} ✓` : `[agentdeck] ESP32 WiFi failed: ${msg.error || 'unknown'}`);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
log(`[agentdeck] WebSocket server ready on port ${port}`);
|
|
587
|
+
log(`[agentdeck] Pairing URL: ${core.wsUrl}`);
|
|
588
|
+
// Initialize APME store + collector so the daemon can serve /apme/* HTTP
|
|
589
|
+
// routes. `setApme` on core is gated against the `daemon` meta-session so
|
|
590
|
+
// register/deregister won't open a bogus run. Session bridges opening their
|
|
591
|
+
// own connection to the same sqlite file is safe under WAL mode.
|
|
592
|
+
apme = await initApme();
|
|
593
|
+
if (apme) {
|
|
594
|
+
core.setApme(apme);
|
|
595
|
+
log('[agentdeck] APME enabled — /apme/* routes active');
|
|
596
|
+
}
|
|
597
|
+
// Register session
|
|
598
|
+
core.registerSession('daemon');
|
|
599
|
+
const passiveSessionObserver = new PassiveSessionObserver();
|
|
600
|
+
// ===== Gateway adapter lifecycle =====
|
|
601
|
+
// (gatewayAdapter + gatewayConnecting declared earlier, before HTTP server)
|
|
602
|
+
// Inject OpenClaw virtual session only after Gateway authentication succeeds.
|
|
603
|
+
// Reachability alone is a topology signal, not proof that commands can route.
|
|
604
|
+
core.setSessionsEnricher((sessions) => {
|
|
605
|
+
const enrichedSessions = [...sessions, ...passiveSessionObserver.collect(sessions)];
|
|
606
|
+
const adapterAlive = gatewayAdapter?.isAlive() ?? false;
|
|
607
|
+
if (!adapterAlive && !core.cachedGatewayConnected)
|
|
608
|
+
return enrichedSessions;
|
|
609
|
+
if (enrichedSessions.some(s => s.agentType === 'openclaw'))
|
|
610
|
+
return enrichedSessions;
|
|
611
|
+
const snap = core.stateMachine.getSnapshot();
|
|
612
|
+
return [...enrichedSessions, {
|
|
613
|
+
id: 'openclaw-gateway',
|
|
614
|
+
port: 18789,
|
|
615
|
+
projectName: adapterAlive ? (snap.projectName ?? 'OpenClaw') : 'OpenClaw',
|
|
616
|
+
agentType: 'openclaw',
|
|
617
|
+
alive: true,
|
|
618
|
+
state: adapterAlive ? snap.state : 'idle',
|
|
619
|
+
modelName: adapterAlive ? (snap.modelName ?? undefined) : undefined,
|
|
620
|
+
controlMode: 'managed',
|
|
621
|
+
}];
|
|
622
|
+
});
|
|
623
|
+
function connectGatewayAdapter() {
|
|
624
|
+
if (gatewayAdapter || gatewayConnecting)
|
|
625
|
+
return;
|
|
626
|
+
gatewayConnecting = true;
|
|
627
|
+
log('[agentdeck] OpenClaw Gateway detected, connecting...');
|
|
628
|
+
const adapter = new OpenClawAdapter({ autoReconnect: false });
|
|
629
|
+
adapter.on('event', (evt) => {
|
|
630
|
+
switch (evt.source) {
|
|
631
|
+
case 'hook':
|
|
632
|
+
if (evt.event === 'SessionStart')
|
|
633
|
+
core.stateMachine.handleHookEvent('SessionStart', {});
|
|
634
|
+
else if (evt.event === 'SessionEnd')
|
|
635
|
+
core.stateMachine.handleHookEvent('SessionEnd', {});
|
|
636
|
+
break;
|
|
637
|
+
case 'parser':
|
|
638
|
+
core.stateMachine.handleParserEvent(evt.event, evt.data);
|
|
639
|
+
break;
|
|
640
|
+
case 'metadata':
|
|
641
|
+
if (evt.event === 'model_catalog') {
|
|
642
|
+
const models = evt.data?.models;
|
|
643
|
+
if (models) {
|
|
644
|
+
core.cachedModelCatalog = models;
|
|
645
|
+
const snap = core.stateMachine.getSnapshot();
|
|
646
|
+
const stateEvent = core.buildStateEvent({
|
|
647
|
+
agentType: 'openclaw',
|
|
648
|
+
agentCapabilities: OPENCLAW_CAPABILITIES,
|
|
649
|
+
snapshot: snap,
|
|
650
|
+
});
|
|
651
|
+
lastStateEvent = stateEvent;
|
|
652
|
+
core.broadcast(stateEvent);
|
|
653
|
+
core.broadcastUsage();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else if (evt.event === 'gateway_health') {
|
|
657
|
+
// Use real-time health event from Gateway WS instead of polling `openclaw doctor`
|
|
658
|
+
const hasError = !evt.data?.ok;
|
|
659
|
+
const changed = hasError !== core.cachedGatewayHasError;
|
|
660
|
+
core.cachedGatewayHasError = hasError;
|
|
661
|
+
if (changed) {
|
|
662
|
+
core.stateMachine.emit('state_changed', core.stateMachine.getSnapshot());
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
break;
|
|
666
|
+
case 'activity':
|
|
667
|
+
core.stateMachine.onPtyActivity();
|
|
668
|
+
break;
|
|
669
|
+
case 'timeline':
|
|
670
|
+
if (evt.entry) {
|
|
671
|
+
if (evt.upsert)
|
|
672
|
+
core.bridgeTimeline.upsertEntry(evt.entry);
|
|
673
|
+
else
|
|
674
|
+
core.bridgeTimeline.addEntry(evt.entry);
|
|
675
|
+
if (evt.entry.type === 'tool_request')
|
|
676
|
+
bridgeLogStream.trackToolRequest(evt.entry.raw);
|
|
677
|
+
}
|
|
678
|
+
break;
|
|
679
|
+
case 'connection': {
|
|
680
|
+
// Do NOT forward gateway adapter connection events as bridge connection
|
|
681
|
+
// events — WS clients would interpret them as their own bridge disconnect
|
|
682
|
+
// and show "disconnected" UI. Gateway status is conveyed via state_update
|
|
683
|
+
// (agentType/gatewayAvailable) and sessions_list.
|
|
684
|
+
if (evt.status === 'connected') {
|
|
685
|
+
core.cachedGatewayAvailable = true;
|
|
686
|
+
core.cachedGatewayConnected = true;
|
|
687
|
+
bridgeLogStream.start();
|
|
688
|
+
log('[agentdeck] OpenClaw Gateway connected');
|
|
689
|
+
if (core.stateMachine.getSnapshot().state === 'disconnected') {
|
|
690
|
+
core.stateMachine.handleHookEvent('SessionStart', {});
|
|
691
|
+
}
|
|
692
|
+
// Force full state broadcast
|
|
693
|
+
const snap = core.stateMachine.getSnapshot();
|
|
694
|
+
const gwStateEvent = core.buildStateEvent({
|
|
695
|
+
agentType: 'openclaw',
|
|
696
|
+
agentCapabilities: OPENCLAW_CAPABILITIES,
|
|
697
|
+
snapshot: snap,
|
|
698
|
+
});
|
|
699
|
+
lastStateEvent = gwStateEvent;
|
|
700
|
+
core.wsServer.broadcast(gwStateEvent);
|
|
701
|
+
core.broadcastUsage();
|
|
702
|
+
core.broadcastSessionsList().catch(() => { });
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
core.cachedGatewayConnected = false;
|
|
706
|
+
bridgeLogStream.stop();
|
|
707
|
+
log('[agentdeck] OpenClaw Gateway disconnected');
|
|
708
|
+
core.broadcastSessionsList().catch(() => { });
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
adapter.on('exit', () => disconnectGatewayAdapter());
|
|
715
|
+
adapter.start({ port, externalServer: httpServer }).then(() => {
|
|
716
|
+
gatewayAdapter = adapter;
|
|
717
|
+
gatewayConnecting = false;
|
|
718
|
+
}).catch((err) => {
|
|
719
|
+
log(`[agentdeck] Failed to connect to Gateway: ${err}`);
|
|
720
|
+
gatewayConnecting = false;
|
|
721
|
+
core.cachedGatewayConnected = false;
|
|
722
|
+
core.stateMachine.emit('state_changed', core.stateMachine.getSnapshot());
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function disconnectGatewayAdapter() {
|
|
726
|
+
if (!gatewayAdapter)
|
|
727
|
+
return;
|
|
728
|
+
log('[agentdeck] OpenClaw Gateway lost, cleaning up...');
|
|
729
|
+
const wasAlive = gatewayAdapter.isAlive();
|
|
730
|
+
gatewayAdapter.shutdown().catch(() => { });
|
|
731
|
+
gatewayAdapter = null;
|
|
732
|
+
core.cachedGatewayConnected = false;
|
|
733
|
+
core.cachedModelCatalog = null;
|
|
734
|
+
if (wasAlive)
|
|
735
|
+
core.stateMachine.handleHookEvent('SessionEnd', {});
|
|
736
|
+
// Do NOT broadcast connection:disconnected — that would make WS clients
|
|
737
|
+
// think they lost their bridge connection. State change to 'daemon' agentType
|
|
738
|
+
// and updated sessions_list convey the gateway loss.
|
|
739
|
+
core.broadcastSessionsList().catch(() => { });
|
|
740
|
+
}
|
|
741
|
+
// ===== Voice assistant (wake word) =====
|
|
742
|
+
let voiceAssistant = null;
|
|
743
|
+
let voiceManager = null;
|
|
744
|
+
let previousDaemonState = State.IDLE;
|
|
745
|
+
if (wakeWordEnabled) {
|
|
746
|
+
voiceManager = new VoiceManager();
|
|
747
|
+
voiceManager.connectToServer().catch((err) => {
|
|
748
|
+
debug('daemon', `whisper-server connection failed: ${err}`);
|
|
749
|
+
});
|
|
750
|
+
voiceAssistant = new VoiceAssistantManager({
|
|
751
|
+
sendPrompt: (text) => {
|
|
752
|
+
if (gatewayAdapter?.isAlive() && gatewayAdapter.handleCommand({ type: 'send_prompt', text })) {
|
|
753
|
+
core.stateMachine.handleUserAction('send_prompt');
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
debug('daemon', 'Wake word prompt but no active adapter');
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
transcribeFile: (filePath) => voiceManager.transcribeFile(filePath),
|
|
760
|
+
});
|
|
761
|
+
voiceAssistant.on('state_change', (info) => {
|
|
762
|
+
// Broadcast dedicated event (for plugin FORWARDED_EVENTS)
|
|
763
|
+
core.broadcast({
|
|
764
|
+
type: 'voice_assistant_state',
|
|
765
|
+
state: info.state,
|
|
766
|
+
deviceId: 'mac-builtin',
|
|
767
|
+
text: info.text,
|
|
768
|
+
responseText: info.responseText,
|
|
769
|
+
});
|
|
770
|
+
// Piggyback on state_update so all clients (Android/Apple/TUI) get it automatically
|
|
771
|
+
core.updateVoiceAssistantState(info.state, info.text, info.responseText);
|
|
772
|
+
});
|
|
773
|
+
voiceAssistant.on('wake_word_detected', (info) => {
|
|
774
|
+
core.broadcast({
|
|
775
|
+
type: 'wake_word_detected',
|
|
776
|
+
deviceId: info.deviceId,
|
|
777
|
+
timestamp: info.timestamp,
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
voiceAssistant.start().then((ok) => {
|
|
781
|
+
if (ok)
|
|
782
|
+
log('[agentdeck] Wake word voice assistant active ("오픈클로")');
|
|
783
|
+
else
|
|
784
|
+
log('[agentdeck] Wake word not available (missing model or access key)');
|
|
785
|
+
}).catch((err) => {
|
|
786
|
+
log(`[agentdeck] Wake word start failed: ${err}`);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
// ===== State changed → broadcast =====
|
|
790
|
+
core.stateMachine.on('state_changed', (snapshot) => {
|
|
791
|
+
const gwAlive = gatewayAdapter?.isAlive() ?? false;
|
|
792
|
+
const stateEvent = core.buildStateEvent({
|
|
793
|
+
agentType: gwAlive ? 'openclaw' : 'daemon',
|
|
794
|
+
agentCapabilities: gwAlive ? OPENCLAW_CAPABILITIES : undefined,
|
|
795
|
+
snapshot,
|
|
796
|
+
});
|
|
797
|
+
lastStateEvent = stateEvent;
|
|
798
|
+
core.wsServer.broadcast(stateEvent);
|
|
799
|
+
core.maybeBroadcastSessionsList();
|
|
800
|
+
core.broadcastUsage();
|
|
801
|
+
// Voice assistant: reset timeout on any activity during processing
|
|
802
|
+
if (snapshot.state === State.PROCESSING && voiceAssistant?.getState() === 'processing') {
|
|
803
|
+
voiceAssistant.resetResponseTimeout();
|
|
804
|
+
}
|
|
805
|
+
// Voice assistant: PROCESSING→IDLE triggers TTS response
|
|
806
|
+
const wasActive = previousDaemonState === State.PROCESSING;
|
|
807
|
+
previousDaemonState = snapshot.state;
|
|
808
|
+
if (wasActive && snapshot.state === State.IDLE && voiceAssistant?.getState() === 'processing') {
|
|
809
|
+
const lastEntry = core.bridgeTimeline.getLastEntry('chat_end');
|
|
810
|
+
const responseText = lastEntry?.detail ?? lastEntry?.raw ?? '';
|
|
811
|
+
voiceAssistant.handleResponse(responseText || '완료했습니다.').catch((err) => {
|
|
812
|
+
debug('daemon', `Voice assistant TTS error: ${err}`);
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
// ===== Commands from WS clients =====
|
|
817
|
+
// ===== Internal WS: session push channel =====
|
|
818
|
+
core.wsServer.onRawMessage((msg, sender) => {
|
|
819
|
+
if (msg.type === 'session_push_register') {
|
|
820
|
+
const { sessionId, port: sessionPort, agentType: at, projectName: pn } = msg;
|
|
821
|
+
debug('daemon', `session_push_register: ${sessionId} port=${sessionPort} agent=${at}`);
|
|
822
|
+
// Acknowledge registration
|
|
823
|
+
try {
|
|
824
|
+
sender.send(JSON.stringify({ type: 'session_push_ack', sessionId }));
|
|
825
|
+
}
|
|
826
|
+
catch { /* client disconnecting */ }
|
|
827
|
+
return true; // consumed
|
|
828
|
+
}
|
|
829
|
+
if (msg.type === 'session_push_state') {
|
|
830
|
+
const { sessionId, state, modelName, effortLevel } = msg;
|
|
831
|
+
if (sessionId && state) {
|
|
832
|
+
updatePushState(sessionId, state, modelName, effortLevel);
|
|
833
|
+
// Trigger sessions list broadcast so clients get fresh state
|
|
834
|
+
core.maybeBroadcastSessionsList();
|
|
835
|
+
}
|
|
836
|
+
return true; // consumed
|
|
837
|
+
}
|
|
838
|
+
return false; // not consumed — pass to command handler
|
|
839
|
+
});
|
|
840
|
+
core.wsServer.onCommand((cmd) => {
|
|
841
|
+
debug('daemon', `cmd: ${cmd.type}`);
|
|
842
|
+
if (gatewayAdapter?.isAlive() && gatewayAdapter.handleCommand(cmd)) {
|
|
843
|
+
switch (cmd.type) {
|
|
844
|
+
case 'respond':
|
|
845
|
+
core.stateMachine.handleUserAction('respond');
|
|
846
|
+
break;
|
|
847
|
+
case 'interrupt':
|
|
848
|
+
core.stateMachine.handleUserAction('interrupt');
|
|
849
|
+
break;
|
|
850
|
+
case 'escape':
|
|
851
|
+
core.stateMachine.handleUserAction('interrupt');
|
|
852
|
+
break;
|
|
853
|
+
case 'select_option':
|
|
854
|
+
core.stateMachine.handleUserAction('select_option');
|
|
855
|
+
break;
|
|
856
|
+
case 'send_prompt':
|
|
857
|
+
core.stateMachine.handleUserAction('send_prompt');
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (cmd.type === 'switch_agent') {
|
|
863
|
+
focusRelay.unfocus(); // Clear session focus on agent switch
|
|
864
|
+
const target = cmd.agent;
|
|
865
|
+
if (target === 'openclaw' && gatewayAdapter?.isAlive()) {
|
|
866
|
+
// Force broadcast OpenClaw state to all clients
|
|
867
|
+
const snap = core.stateMachine.getSnapshot();
|
|
868
|
+
const gwStateEvent = core.buildStateEvent({
|
|
869
|
+
agentType: 'openclaw',
|
|
870
|
+
agentCapabilities: OPENCLAW_CAPABILITIES,
|
|
871
|
+
snapshot: snap,
|
|
872
|
+
});
|
|
873
|
+
lastStateEvent = gwStateEvent;
|
|
874
|
+
core.wsServer.broadcast(gwStateEvent);
|
|
875
|
+
}
|
|
876
|
+
else if (target === 'claude-code') {
|
|
877
|
+
// Broadcast daemon/claude-code state — clients reconnect to session bridges independently
|
|
878
|
+
const snap = core.stateMachine.getSnapshot();
|
|
879
|
+
const stateEvent = core.buildStateEvent({
|
|
880
|
+
agentType: 'daemon',
|
|
881
|
+
snapshot: snap,
|
|
882
|
+
});
|
|
883
|
+
lastStateEvent = stateEvent;
|
|
884
|
+
core.wsServer.broadcast(stateEvent);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (cmd.type === 'focus_session') {
|
|
889
|
+
const sessionId = cmd.sessionId;
|
|
890
|
+
if (!sessionId)
|
|
891
|
+
return;
|
|
892
|
+
focusRelay.focus(sessionId);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
// Session-scoped command: forward inner command to a specific session's bridge
|
|
896
|
+
if (cmd.type === 'session_command') {
|
|
897
|
+
const { sessionId, command } = cmd;
|
|
898
|
+
if (!sessionId || !command)
|
|
899
|
+
return;
|
|
900
|
+
const sessions = listActiveSessions();
|
|
901
|
+
const target = sessions.find(s => s.id === sessionId);
|
|
902
|
+
if (!target) {
|
|
903
|
+
debug('daemon', `session_command: session ${sessionId} not found`);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
// Focus the session first, then route the command
|
|
907
|
+
focusRelay.focus(sessionId);
|
|
908
|
+
// Small delay to let focus take effect, then route
|
|
909
|
+
setTimeout(() => focusRelay.routeCommand(command), 100);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
// Route interactive commands to focused session (if any)
|
|
913
|
+
if (focusRelay.getFocusedSessionId() && focusRelay.routeCommand(cmd)) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (cmd.type === 'query_usage') {
|
|
917
|
+
fetchUsageRelayed(port).then((usage) => {
|
|
918
|
+
if (usage)
|
|
919
|
+
core.updateApiUsage(usage);
|
|
920
|
+
else if (core.cachedApiUsage)
|
|
921
|
+
core.apiUsageStale = true;
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
// ===== Client connect =====
|
|
926
|
+
core.wsServer.onClientConnect((ws) => {
|
|
927
|
+
const gwAlive = gatewayAdapter?.isAlive() ?? false;
|
|
928
|
+
core.sendInitialState(ws, {
|
|
929
|
+
agentType: gwAlive ? 'openclaw' : 'daemon',
|
|
930
|
+
agentCapabilities: gwAlive ? OPENCLAW_CAPABILITIES : undefined,
|
|
931
|
+
isAlive: true, // WS client IS connected to daemon — gateway status conveyed via state_update
|
|
932
|
+
});
|
|
933
|
+
// Fetch usage on connect if stale
|
|
934
|
+
const cacheAge = Date.now() - core.lastApiFetchTime;
|
|
935
|
+
if (!core.cachedApiUsage || (core.lastApiFetchTime > 0 && cacheAge > 5 * 60 * 1000)) {
|
|
936
|
+
fetchUsageRelayed(port).then((usage) => {
|
|
937
|
+
if (usage)
|
|
938
|
+
core.updateApiUsage(usage);
|
|
939
|
+
else {
|
|
940
|
+
core.oauthConnected = hasOAuthToken();
|
|
941
|
+
if (core.cachedApiUsage)
|
|
942
|
+
core.apiUsageStale = true;
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
// ===== Probes & polling =====
|
|
948
|
+
core.startOllamaProbe();
|
|
949
|
+
core.startMlxProbe();
|
|
950
|
+
core.startAntigravityProbe();
|
|
951
|
+
core.startGatewayProbe(5000, () => connectGatewayAdapter(), () => { if (gatewayAdapter && !gatewayAdapter.isAlive())
|
|
952
|
+
disconnectGatewayAdapter(); });
|
|
953
|
+
core.startGatewayHealthCheck();
|
|
954
|
+
core.startUsageTick();
|
|
955
|
+
core.startApiUsagePolling(60_000, () => fetchUsageRelayed(port));
|
|
956
|
+
core.startSessionsListPolling();
|
|
957
|
+
// APME: periodically pick up runs that session bridges closed but couldn't
|
|
958
|
+
// eval (session exits within 2s of shutdown). Daemon is long-lived, so it
|
|
959
|
+
// can run the full deterministic + judge pipeline without time pressure.
|
|
960
|
+
if (apme) {
|
|
961
|
+
const { evaluateOutcome } = await import('./apme/outcome.js');
|
|
962
|
+
const { classifyRunSmart } = await import('./apme/classifier.js');
|
|
963
|
+
const { aggregateOverall } = await import('./apme/http.js');
|
|
964
|
+
// Broadcast eval results to all WS clients + timeline when runner completes
|
|
965
|
+
apme.runner.onResult(({ runId, turnId }) => {
|
|
966
|
+
// Turn-level eval: broadcast and add timeline entry with turn score
|
|
967
|
+
if (turnId) {
|
|
968
|
+
const run = apme.store.getRun(runId);
|
|
969
|
+
if (!run)
|
|
970
|
+
return;
|
|
971
|
+
const turnEvals = apme.store.listEvalsForTurn(turnId);
|
|
972
|
+
const overall = turnEvals.find(e => e.metric === 'overall');
|
|
973
|
+
if (!overall)
|
|
974
|
+
return;
|
|
975
|
+
// Persist turn-level outcome + composite so downstream analytics
|
|
976
|
+
// (category scorecard, recommender) can aggregate per-turn scores.
|
|
977
|
+
try {
|
|
978
|
+
apme.store.updateTurn(turnId, {
|
|
979
|
+
outcome: 'committed',
|
|
980
|
+
compositeScore: overall.score,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
catch { /* ignore */ }
|
|
984
|
+
const pct = Math.round(overall.score * 100);
|
|
985
|
+
// WS broadcast — reuse apme_eval event for turn eval so dashboards pick it up
|
|
986
|
+
const turnEvalEvent = {
|
|
987
|
+
type: 'apme_eval',
|
|
988
|
+
run: {
|
|
989
|
+
runId: run.id, sessionId: run.sessionId, agentType: run.agentType, startedAt: run.startedAt,
|
|
990
|
+
modelId: run.modelId ?? undefined, projectName: run.projectName ?? undefined,
|
|
991
|
+
taskPrompt: run.taskPrompt ?? undefined, taskCategory: run.taskCategory ?? undefined,
|
|
992
|
+
outcome: 'committed',
|
|
993
|
+
compositeScore: overall.score,
|
|
994
|
+
overallScore: overall.score,
|
|
995
|
+
evals: turnEvals.map(e => ({
|
|
996
|
+
layer: e.layer, metric: e.metric, score: e.score,
|
|
997
|
+
judgeModel: e.judgeModel ?? undefined, createdAt: e.createdAt,
|
|
998
|
+
})),
|
|
999
|
+
},
|
|
1000
|
+
};
|
|
1001
|
+
core.broadcast(turnEvalEvent);
|
|
1002
|
+
// Change 8: include axis scores + judge reasoning in turn eval detail
|
|
1003
|
+
const turnAxes = turnEvals.filter(e => e.metric !== 'overall');
|
|
1004
|
+
const turnAxisStr = turnAxes.map(e => `${e.metric}=${Math.round(e.score * 100)}%`).join(' ');
|
|
1005
|
+
let turnDetail = turnAxisStr
|
|
1006
|
+
? `${turnAxisStr}\n${run.taskPrompt?.slice(0, 80) ?? ''}`
|
|
1007
|
+
: `Turn eval · ${run.taskPrompt?.slice(0, 80) ?? ''}`;
|
|
1008
|
+
if (overall.raw) {
|
|
1009
|
+
try {
|
|
1010
|
+
const parsed = JSON.parse(overall.raw);
|
|
1011
|
+
const done = parsed.done?.slice(0, 2).join(', ') || '';
|
|
1012
|
+
const missed = parsed.missed?.slice(0, 2).join(', ') || '';
|
|
1013
|
+
if (done)
|
|
1014
|
+
turnDetail += `\n✓ ${done}`;
|
|
1015
|
+
if (missed)
|
|
1016
|
+
turnDetail += `\n✗ ${missed}`;
|
|
1017
|
+
}
|
|
1018
|
+
catch { /* ignore */ }
|
|
1019
|
+
}
|
|
1020
|
+
core.bridgeTimeline.addEntry({
|
|
1021
|
+
ts: Date.now(), type: 'eval_result',
|
|
1022
|
+
raw: `★ turn ${pct}% [${run.taskCategory ?? '?'}]`,
|
|
1023
|
+
detail: turnDetail,
|
|
1024
|
+
agentType: run.agentType,
|
|
1025
|
+
});
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const run = apme.store.getRun(runId);
|
|
1029
|
+
if (!run)
|
|
1030
|
+
return;
|
|
1031
|
+
const evals = apme.store.listEvalsForRun(runId);
|
|
1032
|
+
const overallScore = aggregateOverall(evals);
|
|
1033
|
+
// WS broadcast: apme_eval event (type already in protocol.ts)
|
|
1034
|
+
const evalEvent = {
|
|
1035
|
+
type: 'apme_eval',
|
|
1036
|
+
run: {
|
|
1037
|
+
runId: run.id, sessionId: run.sessionId, agentType: run.agentType, startedAt: run.startedAt,
|
|
1038
|
+
modelId: run.modelId ?? undefined, projectName: run.projectName ?? undefined,
|
|
1039
|
+
taskPrompt: run.taskPrompt ?? undefined, taskCategory: run.taskCategory ?? undefined,
|
|
1040
|
+
outcome: run.outcome ?? undefined,
|
|
1041
|
+
compositeScore: run.compositeScore ?? undefined,
|
|
1042
|
+
overallScore: overallScore ?? undefined,
|
|
1043
|
+
evals: evals.map(e => ({
|
|
1044
|
+
layer: e.layer, metric: e.metric, score: e.score,
|
|
1045
|
+
judgeModel: e.judgeModel ?? undefined, createdAt: e.createdAt,
|
|
1046
|
+
})),
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1049
|
+
core.broadcast(evalEvent);
|
|
1050
|
+
// Change 9: separate deterministic layer timeline entry (lint/build/test)
|
|
1051
|
+
const detEvals = evals.filter(e => e.layer === 'deterministic');
|
|
1052
|
+
if (detEvals.length > 0) {
|
|
1053
|
+
const detResults = detEvals.map(e => `${e.metric}=${e.score ? '✓' : '✗'}`).join(' ');
|
|
1054
|
+
core.bridgeTimeline.addEntry({
|
|
1055
|
+
ts: Date.now(), type: 'eval_result',
|
|
1056
|
+
raw: `⚡ ${detResults}`,
|
|
1057
|
+
detail: `Deterministic eval · ${run.projectName ?? ''}`,
|
|
1058
|
+
agentType: run.agentType,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
// Change 7: enriched run-level eval_result with axis scores + deterministic summary
|
|
1062
|
+
const pct = Math.round((overallScore ?? run.compositeScore ?? 0) * 100);
|
|
1063
|
+
const judgeEvals = evals.filter(e => e.layer === 'llm_judge' && e.metric !== 'overall');
|
|
1064
|
+
const axisStr = judgeEvals.map(e => `${e.metric}=${Math.round(e.score * 100)}%`).join(' ');
|
|
1065
|
+
const detStr = detEvals.length > 0
|
|
1066
|
+
? detEvals.map(e => `${e.metric}=${e.score ? '✓' : '✗'}`).join(' ') + ' · '
|
|
1067
|
+
: '';
|
|
1068
|
+
const enrichedDetail = axisStr
|
|
1069
|
+
? `${detStr}${axisStr}\n${run.projectName ?? ''} · ${run.taskPrompt?.slice(0, 100) ?? ''}`
|
|
1070
|
+
: `${run.projectName ?? ''} · ${run.taskPrompt?.slice(0, 100) ?? ''}`;
|
|
1071
|
+
core.bridgeTimeline.addEntry({
|
|
1072
|
+
ts: Date.now(),
|
|
1073
|
+
type: 'eval_result',
|
|
1074
|
+
raw: `★ [${run.taskCategory ?? '?'}] ${pct}% · ${run.outcome ?? 'pending'}`,
|
|
1075
|
+
detail: enrichedDetail,
|
|
1076
|
+
agentType: run.agentType,
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
const apmeEvalTimer = setInterval(() => {
|
|
1080
|
+
// 1. Enqueue unevaluated runs for deterministic + judge
|
|
1081
|
+
const pending = apme.store.listUnevaluatedRuns(5);
|
|
1082
|
+
for (const run of pending) {
|
|
1083
|
+
apme.runner.enqueue({ runId: run.id, projectPath: run.projectPath ?? undefined });
|
|
1084
|
+
}
|
|
1085
|
+
// 2. Run outcome detection + composite scoring on recently closed runs
|
|
1086
|
+
// that don't have an outcome yet.
|
|
1087
|
+
const closedRuns = apme.store.listRuns({ limit: 20 });
|
|
1088
|
+
for (const run of closedRuns) {
|
|
1089
|
+
if (run.endedAt && !run.outcome) {
|
|
1090
|
+
// Wait at least 10s after close before judging outcome
|
|
1091
|
+
const elapsed = Date.now() - run.endedAt;
|
|
1092
|
+
if (elapsed > 10_000) {
|
|
1093
|
+
evaluateOutcome(apme.store, run.id);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
// 3. Classify unclassified runs (fire-and-forget from session bridge may
|
|
1098
|
+
// have been killed by process exit — daemon retries here).
|
|
1099
|
+
const unclassified = apme.store.listUnclassifiedRuns(5);
|
|
1100
|
+
for (const run of unclassified) {
|
|
1101
|
+
void classifyRunSmart(apme.store, run.id).then(({ signals, category, source }) => {
|
|
1102
|
+
apme.store.updateRun(run.id, {
|
|
1103
|
+
taskSignals: JSON.stringify(signals),
|
|
1104
|
+
taskCategory: category,
|
|
1105
|
+
taskCategorySource: source,
|
|
1106
|
+
});
|
|
1107
|
+
}).catch(() => { });
|
|
1108
|
+
}
|
|
1109
|
+
// 3b. Backfill turn outcome/composite for turns with captured response.
|
|
1110
|
+
// Turn-level judge (turn_judge) only fires for non-code categories,
|
|
1111
|
+
// so code-category turns never get outcome/composite otherwise.
|
|
1112
|
+
// Heuristic: response captured = 'committed', composite from overall
|
|
1113
|
+
// turn_judge score if present, otherwise null (not inflated).
|
|
1114
|
+
const needOutcome = apme.store.listTurnsNeedingOutcome(20);
|
|
1115
|
+
for (const t of needOutcome) {
|
|
1116
|
+
const evs = apme.store.listEvalsForTurn(t.id);
|
|
1117
|
+
const overall = evs.find(e => e.layer === 'turn_judge' && e.metric === 'overall');
|
|
1118
|
+
try {
|
|
1119
|
+
apme.store.updateTurn(t.id, {
|
|
1120
|
+
outcome: 'committed',
|
|
1121
|
+
...(overall ? { compositeScore: overall.score } : {}),
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
catch { /* ignore */ }
|
|
1125
|
+
}
|
|
1126
|
+
// 4. Clean up orphaned runs — session bridges that crashed without graceful
|
|
1127
|
+
// shutdown leave runs with no ended_at, no turns, no prompt. Tag as _empty
|
|
1128
|
+
// so the dashboard filters them out.
|
|
1129
|
+
const orphans = apme.store.listOrphanedRuns(1800); // 30 min stale threshold
|
|
1130
|
+
for (const id of orphans) {
|
|
1131
|
+
apme.store.updateRun(id, { endedAt: Date.now(), taskCategory: '_empty' });
|
|
1132
|
+
}
|
|
1133
|
+
}, 30_000); // every 30s
|
|
1134
|
+
core.addInterval(apmeEvalTimer);
|
|
1135
|
+
}
|
|
1136
|
+
// Initial usage fetch (delayed 10s)
|
|
1137
|
+
core.addTimeout(setTimeout(() => {
|
|
1138
|
+
fetchUsageRelayed(port).then((usage) => {
|
|
1139
|
+
if (usage)
|
|
1140
|
+
core.updateApiUsage(usage);
|
|
1141
|
+
else {
|
|
1142
|
+
core.oauthConnected = hasOAuthToken();
|
|
1143
|
+
if (core.cachedApiUsage)
|
|
1144
|
+
core.apiUsageStale = true;
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
}, 10_000));
|
|
1148
|
+
// ===== Shutdown =====
|
|
1149
|
+
core.onShutdown(async () => {
|
|
1150
|
+
removeDaemonInfo();
|
|
1151
|
+
focusRelay.stop();
|
|
1152
|
+
timelineRelay.stop();
|
|
1153
|
+
voiceAssistant?.stop();
|
|
1154
|
+
voiceManager?.disconnectFromServer();
|
|
1155
|
+
bridgeLogStream.stop();
|
|
1156
|
+
await Promise.all([
|
|
1157
|
+
gatewayAdapter ? gatewayAdapter.shutdown().catch(() => { }) : Promise.resolve(),
|
|
1158
|
+
stopModules(startedModules)
|
|
1159
|
+
]);
|
|
1160
|
+
gatewayAdapter = null;
|
|
1161
|
+
httpServer.close(() => process.exit(0));
|
|
1162
|
+
// Force exit if httpServer.close() hangs on CLOSE_WAIT connections
|
|
1163
|
+
setTimeout(() => process.exit(0), 5000).unref();
|
|
1164
|
+
});
|
|
1165
|
+
core.registerProcessHandlers('agentdeck');
|
|
1166
|
+
log(`[agentdeck] Daemon running. Gateway probe active.`);
|
|
1167
|
+
}
|
|
1168
|
+
//# sourceMappingURL=daemon-server.js.map
|