@agentdeck/bridge 0.1.0 → 0.2.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 +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-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 +1305 -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 +22 -13
package/dist/index.js
CHANGED
|
@@ -1,30 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AgentDeck Bridge — session entry point.
|
|
3
|
+
*
|
|
4
|
+
* Exports `startSession()` called by cli.ts.
|
|
5
|
+
* Uses BridgeCore for shared infrastructure.
|
|
6
|
+
*/
|
|
7
|
+
import { BridgeCore } from './bridge-core.js';
|
|
8
|
+
import { initApme } from './apme/index.js';
|
|
9
|
+
import { readLastTurn as readClaudeTranscriptLastTurn } from './apme/claude-transcript-reader.js';
|
|
10
|
+
import { claudeHookToSpans } from './apme/adapters/claude-hook.js';
|
|
11
|
+
import { claudePtyParserEventToSpans, claudePtyResponseToSpan } from './apme/adapters/claude-pty.js';
|
|
12
|
+
import { timelineEntryToSpans } from './apme/adapters/timeline.js';
|
|
13
|
+
import { spanNameForKind } from '@agentdeck/shared';
|
|
14
|
+
import { randomUUID } from 'crypto';
|
|
6
15
|
import { VoiceManager } from './voice.js';
|
|
7
16
|
import { checkDependencies } from './check-deps.js';
|
|
8
|
-
import { enableDebugLog, debug } from './logger.js';
|
|
17
|
+
import { enableDebugLog, log, debug, setPtyMode } from './logger.js';
|
|
9
18
|
import { EventJournal } from './event-journal.js';
|
|
10
19
|
import { PtyRingBuffer } from './pty-ringbuffer.js';
|
|
11
20
|
import { createDiagDump } from './diag-analyzer.js';
|
|
12
|
-
import { createAdapter } from './adapters/index.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
21
|
+
import { createAdapter, ClaudeCodeAdapter, CodexCliAdapter, OpenCodeAdapter } from './adapters/index.js';
|
|
22
|
+
import { MonitorAdapter } from './adapters/monitor.js';
|
|
23
|
+
import { UtilityProxy } from './utility-proxy.js';
|
|
24
|
+
import { BridgeLogStream } from './log-stream.js';
|
|
25
|
+
import { extractTopicHint, summarizeResponse } from './timeline-summarizer.js';
|
|
26
|
+
import { cleanDetailText, cleanRawText } from '@agentdeck/shared';
|
|
27
|
+
import { VoiceAssistantManager } from './voice-assistant.js';
|
|
28
|
+
import { TerminalStatus } from './terminal-status.js';
|
|
29
|
+
import { readFileSync } from 'fs';
|
|
30
|
+
import { resolve, dirname } from 'path';
|
|
18
31
|
import { fileURLToPath } from 'url';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
32
|
+
import { execSync } from 'child_process';
|
|
33
|
+
import { listActive as listActiveSessions, findAvailablePort, detectTmuxSession, findDaemonPortAsync, } from './session-registry.js';
|
|
34
|
+
import { resolveProjectName } from './utils/project-name.js';
|
|
35
|
+
import { DaemonWsClient } from './daemon-ws-client.js';
|
|
36
|
+
import { getLanIp } from '@agentdeck/shared';
|
|
37
|
+
import { initModules, stopModules, createDefaultModules, } from './modules/index.js';
|
|
38
|
+
import { onESP32Message, sendWifiProvisionToAll, } from './esp32-serial.js';
|
|
39
|
+
import { loadWifiConfig } from './wifi-config.js';
|
|
40
|
+
import { getAdbDeviceCount } from './adb-reverse.js';
|
|
41
|
+
import { esp32ConnectionCount, getESP32Ports } from './esp32-serial.js';
|
|
42
|
+
import { getPixooDeviceDetails, getLastFrame, renderPreviewFrame } from './pixoo/pixoo-bridge.js';
|
|
43
|
+
import { BRIDGE_WS_PORT, State, } from './types.js';
|
|
25
44
|
function loadTemplates() {
|
|
26
45
|
try {
|
|
27
|
-
// Try multiple locations: relative to bridge, project root, etc.
|
|
28
46
|
const candidates = [
|
|
29
47
|
resolve(dirname(fileURLToPath(import.meta.url)), '../../config/prompt-templates.json'),
|
|
30
48
|
resolve(process.cwd(), 'config/prompt-templates.json'),
|
|
@@ -33,181 +51,76 @@ function loadTemplates() {
|
|
|
33
51
|
try {
|
|
34
52
|
const data = JSON.parse(readFileSync(p, 'utf-8'));
|
|
35
53
|
if (Array.isArray(data?.templates)) {
|
|
36
|
-
debug('
|
|
54
|
+
debug('agentdeck', `Loaded ${data.templates.length} templates from ${p}`);
|
|
37
55
|
return data.templates;
|
|
38
56
|
}
|
|
39
57
|
}
|
|
40
|
-
catch {
|
|
41
|
-
// try next
|
|
42
|
-
}
|
|
58
|
+
catch { /* try next */ }
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
|
-
catch {
|
|
46
|
-
// ignore
|
|
47
|
-
}
|
|
61
|
+
catch { /* ignore */ }
|
|
48
62
|
return [];
|
|
49
63
|
}
|
|
50
64
|
const promptTemplates = loadTemplates();
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.option('-c, --command <cmd>', 'Command to spawn', 'claude')
|
|
66
|
-
.option('-a, --agent <type>', 'Agent type (claude-code|openclaw)', 'claude-code')
|
|
67
|
-
.option('-g, --gateway <url>', 'OpenClaw gateway WebSocket URL')
|
|
68
|
-
.option('-d, --debug', 'Enable debug logging to /tmp/sdc-debug.log')
|
|
69
|
-
.action(async (opts) => {
|
|
70
|
-
if (opts.debug) {
|
|
71
|
-
enableDebugLog();
|
|
72
|
-
log('[sdc] Debug logging enabled → /tmp/sdc-debug.log');
|
|
73
|
-
}
|
|
74
|
-
const port = parseInt(opts.port, 10);
|
|
75
|
-
const agentType = opts.agent;
|
|
76
|
-
await startBridge(port, opts.command, agentType, opts.gateway);
|
|
77
|
-
});
|
|
78
|
-
program
|
|
79
|
-
.command('attach')
|
|
80
|
-
.description('Attach to an existing bridge session')
|
|
81
|
-
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
82
|
-
.action(async (opts) => {
|
|
83
|
-
const port = parseInt(opts.port, 10);
|
|
84
|
-
log(`Attaching to bridge on port ${port}...`);
|
|
85
|
-
log('Attach mode not yet implemented');
|
|
86
|
-
process.exit(1);
|
|
87
|
-
});
|
|
88
|
-
program
|
|
89
|
-
.command('status')
|
|
90
|
-
.description('Show bridge and session status')
|
|
91
|
-
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
92
|
-
.action(async (opts) => {
|
|
93
|
-
const port = parseInt(opts.port, 10);
|
|
94
|
-
try {
|
|
95
|
-
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
96
|
-
const data = await res.json();
|
|
97
|
-
log(`Bridge status: ${JSON.stringify(data, null, 2)}`);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
log('Bridge is not running');
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
program
|
|
105
|
-
.command('stop')
|
|
106
|
-
.description('Stop the bridge and session')
|
|
107
|
-
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
108
|
-
.action(async (opts) => {
|
|
109
|
-
const port = parseInt(opts.port, 10);
|
|
110
|
-
try {
|
|
111
|
-
await fetch(`http://127.0.0.1:${port}/hooks/shutdown`, { method: 'POST' });
|
|
112
|
-
log('Shutdown signal sent');
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
log('Bridge is not running');
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
program
|
|
119
|
-
.command('qr')
|
|
120
|
-
.description('Show pairing URL and QR code for remote clients')
|
|
121
|
-
.option('-p, --port <port>', 'Bridge server port (auto-detects from running sessions)')
|
|
122
|
-
.action(async (opts) => {
|
|
123
|
-
const { getOrCreateToken, getWsUrl } = await import('./auth.js');
|
|
124
|
-
const { listActive } = await import('./session-registry.js');
|
|
125
|
-
// Determine port: explicit flag > running session > default
|
|
126
|
-
let port;
|
|
127
|
-
if (opts.port) {
|
|
128
|
-
port = parseInt(opts.port, 10);
|
|
65
|
+
// log(), logError(), debug() imported from logger.ts
|
|
66
|
+
// log() is suppressed after setPtyMode(true) — startup messages still shown
|
|
67
|
+
/** Extract tool input for timeline display */
|
|
68
|
+
function formatToolInputForTimeline(toolName, input) {
|
|
69
|
+
if (!input)
|
|
70
|
+
return null;
|
|
71
|
+
const keyMap = {
|
|
72
|
+
Bash: 'command', Read: 'file_path', Write: 'file_path', Edit: 'file_path',
|
|
73
|
+
Glob: 'pattern', Grep: 'pattern', WebFetch: 'url', WebSearch: 'query', Task: 'prompt',
|
|
74
|
+
};
|
|
75
|
+
const key = keyMap[toolName];
|
|
76
|
+
if (key && typeof input[key] === 'string') {
|
|
77
|
+
const line = input[key].split('\n')[0];
|
|
78
|
+
return line.length > 120 ? line.slice(0, 119) + '\u2026' : line;
|
|
129
79
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (sessions.length > 1) {
|
|
135
|
-
log(`Multiple sessions running. Using port ${port} (${sessions[0].projectName}).`);
|
|
136
|
-
log(`Specify --port to target a different session.`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
port = BRIDGE_WS_PORT;
|
|
80
|
+
for (const v of Object.values(input)) {
|
|
81
|
+
if (typeof v === 'string' && v.length > 0 && v.length < 200) {
|
|
82
|
+
const line = v.split('\n')[0];
|
|
83
|
+
return line.length > 120 ? line.slice(0, 119) + '\u2026' : line;
|
|
141
84
|
}
|
|
142
85
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
log(
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// qrcode not available in bridge — URL is sufficient
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// ===== startSession =====
|
|
89
|
+
export async function startSession(opts) {
|
|
90
|
+
const agentType = opts.agentType;
|
|
91
|
+
if (opts.debug) {
|
|
92
|
+
enableDebugLog();
|
|
93
|
+
log('Debug logging enabled');
|
|
154
94
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
160
|
-
.option('-a, --analyze', 'Run AI analysis on the dump')
|
|
161
|
-
.option('-t, --tail <lines>', 'Number of journal entries to include', '200')
|
|
162
|
-
.action(async (opts) => {
|
|
163
|
-
const port = parseInt(opts.port, 10);
|
|
164
|
-
const tail = parseInt(opts.tail, 10);
|
|
165
|
-
try {
|
|
166
|
-
const res = await fetch(`http://127.0.0.1:${port}/diag?tail=${tail}`);
|
|
167
|
-
if (!res.ok) {
|
|
168
|
-
log(`Diag endpoint error: ${res.status} ${res.statusText}`);
|
|
95
|
+
// Dependency check (skip for monitor — no PTY needed)
|
|
96
|
+
if (agentType !== 'monitor') {
|
|
97
|
+
const deps = checkDependencies(agentType);
|
|
98
|
+
if (!deps.ok)
|
|
169
99
|
process.exit(1);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
log(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
log('AI analysis failed (is `claude` CLI available?)');
|
|
100
|
+
for (const w of deps.warnings)
|
|
101
|
+
log(`WARNING: ${w}`);
|
|
102
|
+
// Version compatibility check (Claude Code only — uses npm registry + compatibleClaudeCode range)
|
|
103
|
+
if (agentType === 'claude-code' && !opts.noUpdateCheck) {
|
|
104
|
+
const { checkVersionCompatibility } = await import('./version-check.js');
|
|
105
|
+
const versionResult = await checkVersionCompatibility({
|
|
106
|
+
skipCheck: false,
|
|
107
|
+
claudeCodeVersion: deps.agentVersion,
|
|
108
|
+
});
|
|
109
|
+
for (const w of versionResult.warnings)
|
|
110
|
+
log(`WARNING: ${w}`);
|
|
111
|
+
if (versionResult.restartNeeded) {
|
|
112
|
+
log('AgentDeck updated. Please restart.');
|
|
113
|
+
process.exit(0);
|
|
185
114
|
}
|
|
186
115
|
}
|
|
187
116
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
async function startBridge(port, command, agentType, gatewayUrl) {
|
|
195
|
-
const deps = checkDependencies();
|
|
196
|
-
if (!deps.ok) {
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
for (const w of deps.warnings) {
|
|
200
|
-
log(`[sdc] WARNING: ${w}`);
|
|
117
|
+
// Multi-session port allocation — session bridges reserve 9120 for the daemon
|
|
118
|
+
const requestedPort = opts.port ?? BRIDGE_WS_PORT;
|
|
119
|
+
let port = requestedPort === BRIDGE_WS_PORT ? await findAvailablePort({ reserveDaemon: true }) : requestedPort;
|
|
120
|
+
// Auto-migrate hooks (Claude Code mode only)
|
|
121
|
+
if (agentType === 'claude-code') {
|
|
122
|
+
migrateHooksIfNeeded();
|
|
201
123
|
}
|
|
202
|
-
// Multi-session: find available port if default is taken
|
|
203
|
-
const actualPort = port === BRIDGE_WS_PORT ? await findAvailablePort() : port;
|
|
204
|
-
if (actualPort !== port) {
|
|
205
|
-
log(`[sdc] Port ${port} in use, using ${actualPort}`);
|
|
206
|
-
}
|
|
207
|
-
port = actualPort;
|
|
208
|
-
// Auto-migrate old-format hooks (hardcoded port → env var)
|
|
209
|
-
migrateHooksIfNeeded();
|
|
210
|
-
const sessionId = randomUUID();
|
|
211
124
|
const tmuxSession = detectTmuxSession();
|
|
212
125
|
const parentTty = (() => {
|
|
213
126
|
try {
|
|
@@ -217,580 +130,1410 @@ async function startBridge(port, command, agentType, gatewayUrl) {
|
|
|
217
130
|
return undefined;
|
|
218
131
|
}
|
|
219
132
|
})();
|
|
220
|
-
const projectName =
|
|
221
|
-
// Warn if same project
|
|
133
|
+
const projectName = resolveProjectName();
|
|
134
|
+
// Warn if same project already has a non-daemon session running
|
|
222
135
|
const existingSessions = listActiveSessions();
|
|
223
|
-
const sameProject = existingSessions.filter((s) => s.projectName === projectName);
|
|
136
|
+
const sameProject = existingSessions.filter((s) => s.projectName === projectName && s.agentType !== 'daemon');
|
|
224
137
|
if (sameProject.length > 0) {
|
|
225
138
|
const ports = sameProject.map((s) => s.port).join(', ');
|
|
226
|
-
log(
|
|
139
|
+
log(`\u26A0 Session "${projectName}" already running on port ${ports}. Starting new session on port ${port}.`);
|
|
140
|
+
}
|
|
141
|
+
log(`Starting AgentDeck bridge on port ${port} (agent: ${agentType})...`);
|
|
142
|
+
// ===== Create adapter =====
|
|
143
|
+
const adapter = createAdapter(agentType, opts.gatewayUrl);
|
|
144
|
+
// ===== Start adapter (creates HTTP server, spawns process) =====
|
|
145
|
+
try {
|
|
146
|
+
await adapter.start({ port, command: opts.command, gatewayUrl: opts.gatewayUrl });
|
|
147
|
+
log(`Adapter started: ${adapter.capabilities.displayName}`);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
log(`Failed to start adapter: ${err}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
// Suppress stderr logging once PTY is active (PTY adapters share terminal)
|
|
154
|
+
// Non-PTY adapters (Monitor, OpenClaw) keep logs visible
|
|
155
|
+
if (adapter.capabilities.hasTerminal) {
|
|
156
|
+
setPtyMode(true);
|
|
157
|
+
}
|
|
158
|
+
// ===== BridgeCore =====
|
|
159
|
+
const core = new BridgeCore({
|
|
160
|
+
port,
|
|
161
|
+
projectName,
|
|
162
|
+
httpServer: adapter.getHttpServer(),
|
|
163
|
+
});
|
|
164
|
+
// ===== APME (Agent Performance Monitoring & Evaluation) =====
|
|
165
|
+
// Optional: degrades to no-op if better-sqlite3 isn't installed.
|
|
166
|
+
const apme = await initApme();
|
|
167
|
+
if (apme) {
|
|
168
|
+
core.setApme(apme, process.cwd());
|
|
169
|
+
log('APME enabled — runs will be logged to ~/.agentdeck/apme.sqlite');
|
|
170
|
+
// Wire APME eval results → timeline entries (session bridge mode).
|
|
171
|
+
// Daemon has its own richer wiring; this covers standalone session bridges.
|
|
172
|
+
apme.runner.onResult(({ runId, turnId }) => {
|
|
173
|
+
if (turnId) {
|
|
174
|
+
// Turn-level eval completed
|
|
175
|
+
const run = apme.store.getRun(runId);
|
|
176
|
+
if (!run)
|
|
177
|
+
return;
|
|
178
|
+
const turnEvals = apme.store.listEvalsForTurn(turnId);
|
|
179
|
+
const overall = turnEvals.find(e => e.metric === 'overall');
|
|
180
|
+
if (!overall)
|
|
181
|
+
return;
|
|
182
|
+
const pct = Math.round(overall.score * 100);
|
|
183
|
+
const axes = turnEvals.filter(e => e.metric !== 'overall');
|
|
184
|
+
const axisStr = axes.map(e => `${e.metric}=${Math.round(e.score * 100)}%`).join(' ');
|
|
185
|
+
core.bridgeTimeline.addEntry({
|
|
186
|
+
ts: Date.now(), type: 'eval_result',
|
|
187
|
+
raw: `★ turn ${pct}% [${run.taskCategory ?? '?'}]`,
|
|
188
|
+
detail: axisStr || `Turn eval · ${run.taskPrompt?.slice(0, 80) ?? ''}`,
|
|
189
|
+
agentType: run.agentType,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
227
193
|
}
|
|
228
|
-
|
|
229
|
-
// API usage data (fetched from Anthropic, not from PTY)
|
|
230
|
-
let cachedApiUsage = null;
|
|
231
|
-
let lastApiFetchTime = 0;
|
|
232
|
-
// Model catalog (OpenClaw: from CLI)
|
|
233
|
-
let cachedModelCatalog = null;
|
|
234
|
-
// 1. Initialize components
|
|
235
|
-
const adapter = createAdapter(agentType, gatewayUrl);
|
|
236
|
-
const usageTracker = new UsageTracker();
|
|
237
|
-
const stateMachine = new StateMachine(usageTracker);
|
|
194
|
+
// ===== Session-specific components =====
|
|
238
195
|
const voiceManager = new VoiceManager();
|
|
196
|
+
const utilityProxy = new UtilityProxy();
|
|
239
197
|
const journal = new EventJournal();
|
|
240
198
|
const ptyRingBuffer = new PtyRingBuffer();
|
|
241
|
-
|
|
199
|
+
let cachedSlotMap = null;
|
|
200
|
+
let previousBridgeState = State.IDLE;
|
|
201
|
+
// Timeline log stream (OpenClaw/Claude Code)
|
|
202
|
+
const bridgeLogStream = agentType === 'openclaw' ? new BridgeLogStream() : null;
|
|
203
|
+
// Voice server (non-blocking)
|
|
242
204
|
voiceManager.connectToServer().catch((err) => {
|
|
243
|
-
debug('
|
|
205
|
+
debug('agentdeck', `whisper-server connection failed: ${err}`);
|
|
244
206
|
});
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
207
|
+
// Voice assistant (wake word → STT → LLM → TTS)
|
|
208
|
+
let voiceAssistant = null;
|
|
209
|
+
if (opts.wakeWord) {
|
|
210
|
+
voiceAssistant = new VoiceAssistantManager({
|
|
211
|
+
sendPrompt: (text) => {
|
|
212
|
+
if (adapter.handleCommand({ type: 'send_prompt', text })) {
|
|
213
|
+
core.stateMachine.handleUserAction('send_prompt');
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// Fallback for PTY-based adapters
|
|
217
|
+
adapter.writeInput(text);
|
|
218
|
+
setTimeout(() => adapter.writeInput('\r'), 50);
|
|
219
|
+
core.stateMachine.handleUserAction('send_prompt');
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
transcribeFile: (filePath) => voiceManager.transcribeFile(filePath),
|
|
223
|
+
isPttRecording: () => voiceManager.isRecording(),
|
|
224
|
+
});
|
|
225
|
+
// Wire state broadcasts
|
|
226
|
+
voiceAssistant.on('state_change', (info) => {
|
|
227
|
+
// Broadcast dedicated event (for plugin FORWARDED_EVENTS)
|
|
228
|
+
core.broadcast({
|
|
229
|
+
type: 'voice_assistant_state',
|
|
230
|
+
state: info.state,
|
|
231
|
+
deviceId: 'mac-builtin',
|
|
232
|
+
text: info.text,
|
|
233
|
+
responseText: info.responseText,
|
|
234
|
+
});
|
|
235
|
+
// Piggyback on state_update so all clients get it automatically
|
|
236
|
+
core.updateVoiceAssistantState(info.state, info.text, info.responseText);
|
|
237
|
+
});
|
|
238
|
+
voiceAssistant.on('wake_word_detected', (info) => {
|
|
239
|
+
core.broadcast({
|
|
240
|
+
type: 'wake_word_detected',
|
|
241
|
+
deviceId: info.deviceId,
|
|
242
|
+
timestamp: info.timestamp,
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
// Start (non-blocking)
|
|
246
|
+
voiceAssistant.start().then((ok) => {
|
|
247
|
+
if (ok)
|
|
248
|
+
log('Wake word voice assistant active ("오픈클로")');
|
|
249
|
+
else
|
|
250
|
+
log('Wake word not available (missing model or access key)');
|
|
251
|
+
}).catch((err) => {
|
|
252
|
+
log(`Wake word start failed: ${err}`);
|
|
253
|
+
});
|
|
253
254
|
}
|
|
254
|
-
//
|
|
255
|
-
const mdnsCleanup = advertiseBridge(port, projectName, agentType);
|
|
256
|
-
// 2c. Auth token initialization + QR URL for pairing
|
|
257
|
-
getOrCreateToken(); // Ensure token exists on disk
|
|
258
|
-
const wsUrl = getWsUrl(port);
|
|
259
|
-
log(`[sdc] Auth token ready. Pairing URL: ${wsUrl}`);
|
|
260
|
-
// 2d. SSE broadcasting helper (only for ClaudeCode adapter which has HookServer)
|
|
255
|
+
// SSE broadcast (ClaudeCodeAdapter / MonitorAdapter have HookServer)
|
|
261
256
|
let hookServer = null;
|
|
262
257
|
if (adapter instanceof ClaudeCodeAdapter) {
|
|
263
|
-
hookServer = adapter.
|
|
258
|
+
hookServer = adapter.getClaudeHookServer();
|
|
259
|
+
hookServer.setMeta({ agentType, projectName });
|
|
260
|
+
hookServer.setVoiceManager(voiceManager);
|
|
261
|
+
hookServer.onApiUsage(() => ({ usage: core.cachedApiUsage, fetchedAt: core.lastApiFetchTime }));
|
|
262
|
+
hookServer.pairingToken = core.authToken;
|
|
263
|
+
}
|
|
264
|
+
else if (adapter instanceof CodexCliAdapter) {
|
|
265
|
+
hookServer = adapter.getCodexHookServer();
|
|
266
|
+
hookServer.setMeta({ agentType, projectName });
|
|
267
|
+
hookServer.setVoiceManager(voiceManager);
|
|
268
|
+
hookServer.pairingToken = core.authToken;
|
|
269
|
+
}
|
|
270
|
+
else if (adapter instanceof OpenCodeAdapter) {
|
|
271
|
+
hookServer = adapter.getHookServer();
|
|
264
272
|
hookServer.setMeta({ agentType, projectName });
|
|
273
|
+
hookServer.setVoiceManager(voiceManager);
|
|
274
|
+
hookServer.pairingToken = core.authToken;
|
|
275
|
+
}
|
|
276
|
+
else if (adapter instanceof MonitorAdapter) {
|
|
277
|
+
hookServer = adapter.getHookServer();
|
|
278
|
+
hookServer.setMeta({ agentType, projectName });
|
|
279
|
+
hookServer.setVoiceManager(voiceManager);
|
|
280
|
+
hookServer.onApiUsage(() => ({ usage: core.cachedApiUsage, fetchedAt: core.lastApiFetchTime }));
|
|
281
|
+
hookServer.pairingToken = core.authToken;
|
|
265
282
|
}
|
|
266
283
|
const broadcastSse = (event) => hookServer?.broadcastSse(event);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
284
|
+
core.setSseBroadcast(broadcastSse);
|
|
285
|
+
// ===== Display monitor =====
|
|
286
|
+
core.wireDisplayMonitor();
|
|
287
|
+
// ===== Terminal status (tab title + user vars) =====
|
|
288
|
+
const postit = opts.postit !== false && adapter.capabilities.hasTerminal
|
|
289
|
+
? new TerminalStatus(process.stdout)
|
|
290
|
+
: null;
|
|
291
|
+
// Register process handlers early (before module init) so uncaughtException
|
|
292
|
+
// handler is active before mDNS module starts — suppresses "already in use" errors
|
|
293
|
+
core.registerProcessHandlers('agentdeck');
|
|
294
|
+
// Override unhandledRejection to not shutdown (index.ts original behavior)
|
|
295
|
+
process.removeAllListeners('unhandledRejection');
|
|
296
|
+
process.on('unhandledRejection', (reason) => {
|
|
297
|
+
debug('Bridge', `Unhandled rejection: ${reason}`);
|
|
298
|
+
debug('Bridge', `Unhandled rejection stack: ${reason instanceof Error ? reason.stack : reason}`);
|
|
299
|
+
// Don't shutdown — non-fatal
|
|
300
|
+
});
|
|
301
|
+
// ===== Device modules =====
|
|
302
|
+
const moduleConfigs = opts.modules ?? {
|
|
303
|
+
mdns: false, // daemon-only — session bridges never advertise mDNS
|
|
304
|
+
adb: 'auto',
|
|
305
|
+
serial: false, // daemon-only — session bridges never talk to ESP32
|
|
306
|
+
pixoo: false, // daemon-only — session bridges never talk to Pixoo
|
|
307
|
+
};
|
|
308
|
+
const deviceModules = createDefaultModules(agentType);
|
|
309
|
+
// Set up ESP32 state provider before module init
|
|
310
|
+
let lastStateEvent = null;
|
|
311
|
+
const serialModule = deviceModules.find(m => m.name === 'serial');
|
|
312
|
+
if (serialModule) {
|
|
313
|
+
serialModule.setStateProvider(() => lastStateEvent);
|
|
314
|
+
serialModule.setUsageProvider(() => core.buildUsage());
|
|
315
|
+
serialModule.setInitialStateProvider(() => {
|
|
316
|
+
const events = [];
|
|
317
|
+
if (lastStateEvent)
|
|
318
|
+
events.push(lastStateEvent);
|
|
319
|
+
events.push(core.buildUsage());
|
|
320
|
+
events.push({ type: 'display_state', displayOn: core.displayMonitor.isDisplayOn() });
|
|
321
|
+
core.broadcastSessionsList().catch(() => { });
|
|
322
|
+
return events;
|
|
323
|
+
});
|
|
324
|
+
// Include ESP32 serial connections in client count for polling guards
|
|
325
|
+
core.setExternalClientCountProvider(() => esp32ConnectionCount());
|
|
326
|
+
}
|
|
327
|
+
const startedModules = await initModules(deviceModules, moduleConfigs, {
|
|
328
|
+
port,
|
|
329
|
+
authToken: core.authToken,
|
|
330
|
+
projectName,
|
|
331
|
+
wsServer: core.wsServer,
|
|
332
|
+
broadcastSse,
|
|
333
|
+
});
|
|
334
|
+
// WiFi auto-provisioning for ESP32
|
|
335
|
+
const wifiConfig = loadWifiConfig();
|
|
336
|
+
if (wifiConfig?.autoProvision) {
|
|
337
|
+
const lanIp = getLanIp();
|
|
338
|
+
onESP32Message((portPath, msg) => {
|
|
339
|
+
if (msg.type === 'device_info' && !msg.wifiConnected) {
|
|
340
|
+
sendWifiProvisionToAll({
|
|
341
|
+
type: 'wifi_provision',
|
|
342
|
+
ssid: wifiConfig.ssid,
|
|
343
|
+
password: wifiConfig.password,
|
|
344
|
+
bridgeIp: lanIp,
|
|
345
|
+
bridgePort: port,
|
|
346
|
+
authToken: core.authToken,
|
|
347
|
+
});
|
|
348
|
+
log(`WiFi provision sent to ESP32 on ${portPath}`);
|
|
349
|
+
}
|
|
350
|
+
else if (msg.type === 'wifi_provision_ack') {
|
|
351
|
+
log(msg.success ? `ESP32 WiFi connected: ${msg.ip} \u2713` : `ESP32 WiFi failed: ${msg.error || 'unknown'}`);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
log(`WebSocket server ready on port ${port}`);
|
|
356
|
+
log(`Auth token ready. Pairing URL: ${core.wsUrl}`);
|
|
357
|
+
// Device info getter for GET /devices
|
|
358
|
+
hookServer?.setDeviceInfoGetter(() => ({
|
|
359
|
+
devices: [
|
|
360
|
+
{ type: 'websocket', count: core.wsServer.getClientCount() },
|
|
361
|
+
{ type: 'esp32', count: esp32ConnectionCount(), ports: getESP32Ports() },
|
|
362
|
+
{ type: 'pixoo', details: getPixooDeviceDetails() },
|
|
363
|
+
{ type: 'adb', count: getAdbDeviceCount() },
|
|
364
|
+
],
|
|
365
|
+
}));
|
|
366
|
+
// Pixoo live preview frame getter for GET /pixoo/frame
|
|
367
|
+
hookServer?.setPixooFrameGetter(() => getLastFrame() ?? renderPreviewFrame());
|
|
368
|
+
// ===== Diagnostics =====
|
|
369
|
+
adapter.onDiag((tail) => createDiagDump(core.stateMachine, core.wsServer, journal, ptyRingBuffer, tail));
|
|
273
370
|
adapter.onRawData((data) => {
|
|
274
371
|
ptyRingBuffer.push(data);
|
|
275
372
|
const preview = data.replace(/[\x00-\x1f\x1b]/g, '').slice(0, 200);
|
|
276
373
|
journal.write('pty_chunk', 'pty', { size: data.length, preview });
|
|
277
374
|
});
|
|
278
|
-
//
|
|
375
|
+
// Voice errors
|
|
279
376
|
voiceManager.on('error', (err) => {
|
|
280
|
-
debug('
|
|
281
|
-
wsServer.broadcast({ type: 'voice_state', state: 'error', error: err.message });
|
|
377
|
+
debug('agentdeck', `Voice error: ${err.message}`);
|
|
378
|
+
core.wsServer.broadcast({ type: 'voice_state', state: 'error', error: err.message });
|
|
282
379
|
});
|
|
283
|
-
//
|
|
380
|
+
// ===== Timeline wiring =====
|
|
381
|
+
core.wireTimeline(bridgeLogStream ?? undefined);
|
|
382
|
+
// Claude Code hook events → timeline entries
|
|
383
|
+
if (agentType === 'claude-code') {
|
|
384
|
+
wireClaudeCodeTimeline(adapter, core, journal, ptyRingBuffer);
|
|
385
|
+
}
|
|
386
|
+
// APME wiring for non-Claude-Code agents (OpenCode, Codex, OpenClaw)
|
|
387
|
+
if (apme && agentType !== 'claude-code' && agentType !== 'monitor') {
|
|
388
|
+
wireAgentApme(adapter, agentType, apme, core, ptyRingBuffer);
|
|
389
|
+
}
|
|
390
|
+
// ===== Wire adapter events → StateMachine + journal =====
|
|
391
|
+
// APME: PTY response fallback — captures response from terminal output when
|
|
392
|
+
// the Stop hook doesn't fire (unreliable in Claude Code v2.1+).
|
|
393
|
+
// Three capture paths handle the hook/PTY race condition:
|
|
394
|
+
// Path A: idle fires, turn exists → apply directly
|
|
395
|
+
// Path B: idle fires before hook (fast responses) → buffer, apply on hook arrival
|
|
396
|
+
// Path C: UserPromptSubmit closes prev turn → apply PTY text to closed turn
|
|
397
|
+
let pendingPtyResponse = null;
|
|
284
398
|
adapter.on('event', (evt) => {
|
|
285
399
|
switch (evt.source) {
|
|
286
400
|
case 'hook':
|
|
287
401
|
journal.write('hook', 'hook', { event: evt.event, data: evt.data });
|
|
402
|
+
// APME ingestion via the shared TelemetrySpan envelope. The Claude
|
|
403
|
+
// hook adapter handles `/clear` (emits a `task_boundary` span) so
|
|
404
|
+
// there's no separate splitRun branch here. Path B/C below preserve
|
|
405
|
+
// the existing PTY-vs-hook race coordination — that's about timing,
|
|
406
|
+
// not ingestion semantics, so it stays in this file.
|
|
407
|
+
if (apme) {
|
|
408
|
+
const ctx = makeApmeAdapterCtx(apme, core.sessionId, agentType);
|
|
409
|
+
for (const span of claudeHookToSpans(ctx, evt.event, evt.data ?? {})) {
|
|
410
|
+
apme.collector.ingestSpan(core.sessionId, span);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (evt.event === 'UserPromptSubmit' && apme) {
|
|
414
|
+
// Path B: apply buffered response to the NEW turn (idle fired before hook)
|
|
415
|
+
if (pendingPtyResponse) {
|
|
416
|
+
debug('APME', `hook:UPS Path B: applying pending (${pendingPtyResponse.length} chars)`);
|
|
417
|
+
const ctx = makeApmeAdapterCtx(apme, core.sessionId, agentType);
|
|
418
|
+
const span = claudePtyResponseToSpan(ctx, pendingPtyResponse);
|
|
419
|
+
if (span)
|
|
420
|
+
apme.collector.ingestSpan(core.sessionId, span);
|
|
421
|
+
pendingPtyResponse = null;
|
|
422
|
+
}
|
|
423
|
+
// Path C: prev turn was just closed by ingestHook — apply pending response
|
|
424
|
+
if (pendingPtyResponse) {
|
|
425
|
+
debug('APME', `hook:UPS Path C: applying pending to closed turn (${pendingPtyResponse.length} chars)`);
|
|
426
|
+
const ctx = makeApmeAdapterCtx(apme, core.sessionId, agentType);
|
|
427
|
+
const span = claudePtyResponseToSpan(ctx, pendingPtyResponse, { fallbackToLastClosed: true });
|
|
428
|
+
if (span)
|
|
429
|
+
apme.collector.ingestSpan(core.sessionId, span);
|
|
430
|
+
pendingPtyResponse = null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (evt.event === 'Stop')
|
|
434
|
+
pendingPtyResponse = null; // Stop has cleaner response
|
|
288
435
|
if (evt.event === 'shutdown') {
|
|
289
436
|
shutdown();
|
|
290
437
|
return;
|
|
291
438
|
}
|
|
292
|
-
stateMachine.handleHookEvent(evt.event, evt.data);
|
|
439
|
+
core.stateMachine.handleHookEvent(evt.event, evt.data);
|
|
293
440
|
break;
|
|
294
441
|
case 'parser':
|
|
295
442
|
journal.write('parser_emit', 'pty', { event: evt.event, ...evt.data });
|
|
296
|
-
stateMachine.handleParserEvent(evt.event, evt.data);
|
|
443
|
+
core.stateMachine.handleParserEvent(evt.event, evt.data);
|
|
444
|
+
// APME ingestion fallback: PTY parser sees tool use + state changes
|
|
445
|
+
// even when Claude Code hooks are flaky (memory: feedback_apme_stop_hook).
|
|
446
|
+
if (apme && evt.event) {
|
|
447
|
+
const ctx = makeApmeAdapterCtx(apme, core.sessionId, agentType);
|
|
448
|
+
for (const span of claudePtyParserEventToSpans(ctx, evt.event, evt.data ?? {})) {
|
|
449
|
+
apme.collector.ingestSpan(core.sessionId, span);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// APME: Path A — spinner_stop fires when Claude finishes responding.
|
|
453
|
+
// Delay 500ms to let response render in PTY, then extract text after ⏺ marker.
|
|
454
|
+
if (apme && evt.event === 'spinner_stop') {
|
|
455
|
+
const sid = core.sessionId;
|
|
456
|
+
setTimeout(async () => {
|
|
457
|
+
if (!apme)
|
|
458
|
+
return;
|
|
459
|
+
const tail = ptyRingBuffer.getTail(5000);
|
|
460
|
+
// Claude's response starts with ⏺ — extract content after last ⏺ marker.
|
|
461
|
+
// Take only meaningful lines (stop at spinner chars ✢✻⏸, prompt ❯, or separator ──).
|
|
462
|
+
const marker = tail.lastIndexOf('⏺');
|
|
463
|
+
let response = '';
|
|
464
|
+
if (marker >= 0) {
|
|
465
|
+
const afterMarker = tail.slice(marker + 1).trim();
|
|
466
|
+
const lines = afterMarker.split('\n');
|
|
467
|
+
const clean = [];
|
|
468
|
+
for (const line of lines) {
|
|
469
|
+
const trimmed = line.trim();
|
|
470
|
+
if (!trimmed)
|
|
471
|
+
continue;
|
|
472
|
+
// Stop at UI artifacts: spinner chars (✢✳✶✻✽), plan mode (⏸), prompt (❯), separator (─)
|
|
473
|
+
if (/^[✢✳✶✻✽⏸⏵❯─>]/.test(trimmed))
|
|
474
|
+
break;
|
|
475
|
+
if (/planmode|plan\s*mode|shift\+tab|accept\s*edits/i.test(trimmed))
|
|
476
|
+
break;
|
|
477
|
+
// Status text: "Whirring…", "Finagling…", token count, shortcuts hint
|
|
478
|
+
if (/^\S+…(\s|$)/.test(trimmed) && /tokens|shortcuts|\d+[ms]\s/i.test(trimmed))
|
|
479
|
+
break;
|
|
480
|
+
if (/\?\s*for\s*shortcuts/.test(trimmed))
|
|
481
|
+
break;
|
|
482
|
+
clean.push(trimmed);
|
|
483
|
+
}
|
|
484
|
+
response = clean.join('\n').trim();
|
|
485
|
+
}
|
|
486
|
+
debug('APME', `spinner_stop+500ms: tailLen=${tail.length} marker=${marker} respLen=${response.length}`);
|
|
487
|
+
if (response.length > 2) {
|
|
488
|
+
const turnId = apme.collector.getActiveTurnId(sid);
|
|
489
|
+
if (turnId) {
|
|
490
|
+
const ctx = makeApmeAdapterCtx(apme, sid, agentType);
|
|
491
|
+
const span = claudePtyResponseToSpan(ctx, response);
|
|
492
|
+
if (span)
|
|
493
|
+
apme.collector.ingestSpan(sid, span);
|
|
494
|
+
pendingPtyResponse = null;
|
|
495
|
+
await classifyAndEnqueueTurn(apme, sid);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
pendingPtyResponse = response;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}, 500);
|
|
502
|
+
}
|
|
297
503
|
break;
|
|
298
504
|
case 'metadata':
|
|
299
505
|
switch (evt.event) {
|
|
300
|
-
case 'cursor_update':
|
|
301
|
-
|
|
302
|
-
stateMachine.updateCursorIndex(idx, 'pty');
|
|
506
|
+
case 'cursor_update':
|
|
507
|
+
core.stateMachine.updateCursorIndex(evt.data?.cursorIndex ?? 0, 'pty');
|
|
303
508
|
break;
|
|
304
|
-
}
|
|
305
509
|
case 'usage_info':
|
|
306
|
-
usageTracker.setUsageInfo(evt.data);
|
|
307
|
-
|
|
308
|
-
|
|
510
|
+
core.usageTracker.setUsageInfo(evt.data);
|
|
511
|
+
core.broadcastUsage();
|
|
512
|
+
apme?.collector.updateUsage(core.sessionId, core.usageTracker.getSnapshot());
|
|
309
513
|
break;
|
|
310
514
|
case 'user_prompt': {
|
|
311
515
|
const text = evt.data?.text;
|
|
312
|
-
if (text)
|
|
313
|
-
|
|
516
|
+
if (text)
|
|
517
|
+
core.broadcast({ type: 'user_prompt', text });
|
|
518
|
+
// APME: user_prompt is the PTY-detected prompt — use it to open a turn
|
|
519
|
+
// and capture the prompt text (fallback for when hooks don't fire).
|
|
520
|
+
if (apme && text) {
|
|
521
|
+
apme.collector.ingestHook(core.sessionId, 'UserPromptSubmit', { message: { content: text } });
|
|
314
522
|
}
|
|
315
523
|
break;
|
|
316
524
|
}
|
|
317
525
|
case 'model_catalog': {
|
|
318
526
|
const models = evt.data?.models;
|
|
319
527
|
if (models) {
|
|
320
|
-
cachedModelCatalog = models;
|
|
321
|
-
debug('
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
const stateEvt = {
|
|
325
|
-
type: 'state_update',
|
|
326
|
-
state: snap.state,
|
|
327
|
-
permissionMode: snap.permissionMode,
|
|
528
|
+
core.cachedModelCatalog = models;
|
|
529
|
+
debug('agentdeck', `Model catalog updated: ${models.length} models`);
|
|
530
|
+
const snap = core.stateMachine.getSnapshot();
|
|
531
|
+
const stateEvent = core.buildStateEvent({
|
|
328
532
|
agentType: adapter.capabilities.type,
|
|
329
|
-
|
|
330
|
-
};
|
|
331
|
-
|
|
533
|
+
snapshot: snap,
|
|
534
|
+
});
|
|
535
|
+
core.broadcast(stateEvent);
|
|
536
|
+
lastStateEvent = stateEvent;
|
|
537
|
+
core.broadcastUsage();
|
|
332
538
|
}
|
|
333
539
|
break;
|
|
334
540
|
}
|
|
335
541
|
}
|
|
336
542
|
break;
|
|
337
543
|
case 'activity':
|
|
338
|
-
stateMachine.onPtyActivity();
|
|
544
|
+
core.stateMachine.onPtyActivity();
|
|
339
545
|
break;
|
|
340
546
|
case 'connection': {
|
|
341
547
|
const connEvt = { type: 'connection', status: evt.status };
|
|
342
|
-
|
|
343
|
-
|
|
548
|
+
core.broadcast(connEvt);
|
|
549
|
+
if (evt.status === 'connected' && bridgeLogStream)
|
|
550
|
+
bridgeLogStream.start();
|
|
551
|
+
else if (evt.status === 'disconnected' && bridgeLogStream)
|
|
552
|
+
bridgeLogStream.stop();
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
case 'timeline': {
|
|
556
|
+
if (evt.upsert) {
|
|
557
|
+
core.bridgeTimeline.upsertEntry(evt.entry);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
core.bridgeTimeline.addEntry(evt.entry);
|
|
561
|
+
}
|
|
562
|
+
if (evt.entry.type === 'tool_request' && bridgeLogStream) {
|
|
563
|
+
bridgeLogStream.trackToolRequest(evt.entry.raw);
|
|
564
|
+
}
|
|
344
565
|
break;
|
|
345
566
|
}
|
|
346
567
|
}
|
|
347
568
|
});
|
|
348
|
-
//
|
|
349
|
-
adapter.on('exit', (
|
|
569
|
+
// Adapter exit — always shutdown (shutdownInProgress guard prevents double-shutdown)
|
|
570
|
+
adapter.on('exit', (code, signal) => {
|
|
571
|
+
log(`Agent process exited (code=${code}, signal=${signal})`);
|
|
350
572
|
shutdown();
|
|
351
573
|
});
|
|
352
|
-
//
|
|
353
|
-
|
|
574
|
+
// Default idle button config
|
|
575
|
+
const DEFAULT_IDLE_BUTTONS = [
|
|
576
|
+
{ label: 'GO ON', action: 'continue' },
|
|
577
|
+
{ label: 'REVIEW', action: '/review' },
|
|
578
|
+
{ label: 'COMMIT', action: '/commit' },
|
|
579
|
+
{ label: 'CLEAR', action: '/clear' },
|
|
580
|
+
];
|
|
581
|
+
// ===== Internal WS: push state to daemon =====
|
|
582
|
+
// Port-drift resilient: portProvider is re-invoked on every (re)connect,
|
|
583
|
+
// so the client follows daemon restarts onto fallback ports and the case
|
|
584
|
+
// where the session bridge starts before the daemon is up.
|
|
585
|
+
//
|
|
586
|
+
// Async provider: on cold start the registry may still be empty (session
|
|
587
|
+
// bridge raced the daemon) or the active daemon may be writing its
|
|
588
|
+
// daemon.json into an unreadable dir (App Store group container vs CLI
|
|
589
|
+
// `~/.agentdeck` split). `findDaemonPortAsync` probes 9120-9139 /health
|
|
590
|
+
// as a last resort so the session still attaches instead of staying
|
|
591
|
+
// orphaned.
|
|
592
|
+
const daemonPortProvider = async () => {
|
|
593
|
+
const resolved = await findDaemonPortAsync();
|
|
594
|
+
if (!resolved)
|
|
595
|
+
return null;
|
|
596
|
+
return resolved.port !== core.port ? resolved.port : null;
|
|
597
|
+
};
|
|
598
|
+
const daemonWsClient = new DaemonWsClient(core.sessionId, core.port, agentType, core.projectName, daemonPortProvider);
|
|
599
|
+
daemonWsClient.connect(null);
|
|
600
|
+
core.onShutdown(() => { daemonWsClient.close(); });
|
|
601
|
+
// ===== State changed → broadcast =====
|
|
602
|
+
core.stateMachine.on('state_changed', (snapshot) => {
|
|
603
|
+
postit?.update(snapshot);
|
|
604
|
+
// Voice assistant: reset timeout on any activity during processing
|
|
605
|
+
if (snapshot.state === State.PROCESSING && voiceAssistant?.getState() === 'processing') {
|
|
606
|
+
voiceAssistant.resetResponseTimeout();
|
|
607
|
+
}
|
|
608
|
+
// PROCESSING→IDLE: fetch fresh usage + voice assistant TTS
|
|
609
|
+
const wasActive = previousBridgeState === State.PROCESSING;
|
|
610
|
+
previousBridgeState = snapshot.state;
|
|
611
|
+
if (wasActive && snapshot.state === State.IDLE) {
|
|
612
|
+
if (core.wsServer.getClientCount() > 0 && Date.now() - core.lastApiFetchTime > 10_000) {
|
|
613
|
+
core.fetchAndUpdateUsage().catch(() => { });
|
|
614
|
+
}
|
|
615
|
+
// Voice assistant: if processing a wake-word prompt, speak the response
|
|
616
|
+
if (voiceAssistant?.getState() === 'processing') {
|
|
617
|
+
const lastEntry = core.bridgeTimeline.getLastEntry('chat_end');
|
|
618
|
+
const responseText = lastEntry?.detail;
|
|
619
|
+
if (responseText) {
|
|
620
|
+
voiceAssistant.handleResponse(responseText).catch((err) => {
|
|
621
|
+
debug('agentdeck', `Voice assistant TTS error: ${err}`);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// No response text available, return to idle
|
|
626
|
+
voiceAssistant.handleResponse('완료했습니다.').catch(() => { });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
hookServer?.setMeta({
|
|
631
|
+
state: snapshot.state,
|
|
632
|
+
modelName: snapshot.modelName ?? undefined,
|
|
633
|
+
effortLevel: snapshot.effortLevel ?? undefined,
|
|
634
|
+
});
|
|
635
|
+
apme?.collector.updateModel(core.sessionId, snapshot.modelName ?? null);
|
|
354
636
|
journal.write('state_change', 'internal', { state: snapshot.state, permissionMode: snapshot.permissionMode, suggestedPrompt: snapshot.suggestedPrompt });
|
|
355
|
-
|
|
356
|
-
|
|
637
|
+
const stateEvent = core.buildStateEvent({
|
|
638
|
+
agentType: adapter.capabilities.type,
|
|
639
|
+
snapshot,
|
|
640
|
+
});
|
|
641
|
+
core.broadcast(stateEvent);
|
|
642
|
+
lastStateEvent = stateEvent;
|
|
643
|
+
core.maybeBroadcastSessionsList();
|
|
644
|
+
// Backward-compat prompt_options
|
|
357
645
|
if (snapshot.options.length > 0) {
|
|
358
|
-
promptType = 'multi_select';
|
|
646
|
+
let promptType = 'multi_select';
|
|
359
647
|
if (snapshot.state === State.AWAITING_PERMISSION) {
|
|
360
648
|
promptType = snapshot.options.length > 2 ? 'yes_no_always' : 'yes_no';
|
|
361
649
|
}
|
|
362
650
|
else if (snapshot.state === State.AWAITING_DIFF) {
|
|
363
651
|
promptType = 'diff_review';
|
|
364
652
|
}
|
|
365
|
-
|
|
366
|
-
// Include options atomically in state_update to avoid race conditions
|
|
367
|
-
// Note: agentCapabilities sent only on client connect (static), not on every broadcast
|
|
368
|
-
const stateEvent = {
|
|
369
|
-
type: 'state_update',
|
|
370
|
-
state: snapshot.state,
|
|
371
|
-
permissionMode: snapshot.permissionMode,
|
|
372
|
-
agentType: adapter.capabilities.type,
|
|
373
|
-
currentTool: snapshot.currentTool ?? undefined,
|
|
374
|
-
toolInput: snapshot.toolInput ?? undefined,
|
|
375
|
-
toolProgress: snapshot.toolProgress ?? undefined,
|
|
376
|
-
projectName: snapshot.projectName ?? undefined,
|
|
377
|
-
modelName: snapshot.modelName ?? undefined,
|
|
378
|
-
billingType: snapshot.billingType,
|
|
379
|
-
options: snapshot.options.length > 0 ? snapshot.options : undefined,
|
|
380
|
-
promptType,
|
|
381
|
-
question: snapshot.question ?? undefined,
|
|
382
|
-
navigable: snapshot.navigable || undefined,
|
|
383
|
-
cursorIndex: (snapshot.state === State.AWAITING_OPTION ||
|
|
384
|
-
snapshot.state === State.AWAITING_PERMISSION ||
|
|
385
|
-
snapshot.state === State.AWAITING_DIFF)
|
|
386
|
-
? snapshot.cursorIndex : undefined,
|
|
387
|
-
suggestedPrompt: snapshot.suggestedPrompt ?? undefined,
|
|
388
|
-
modelCatalog: cachedModelCatalog ?? undefined,
|
|
389
|
-
remoteUrl: snapshot.remoteUrl ?? undefined,
|
|
390
|
-
pairingUrl: wsUrl,
|
|
391
|
-
};
|
|
392
|
-
wsServer.broadcast(stateEvent);
|
|
393
|
-
broadcastSse(stateEvent);
|
|
394
|
-
// Also send separate prompt_options for backward compatibility
|
|
395
|
-
if (snapshot.options.length > 0) {
|
|
396
|
-
const promptEvent = {
|
|
653
|
+
core.broadcast({
|
|
397
654
|
type: 'prompt_options',
|
|
398
|
-
promptType
|
|
655
|
+
promptType,
|
|
399
656
|
question: snapshot.question ?? undefined,
|
|
400
657
|
options: snapshot.options,
|
|
401
|
-
};
|
|
402
|
-
wsServer.broadcast(promptEvent);
|
|
658
|
+
});
|
|
403
659
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
660
|
+
core.broadcastUsage();
|
|
661
|
+
// Push state to daemon via internal WS
|
|
662
|
+
daemonWsClient.pushState(snapshot.state, snapshot.modelName ?? undefined, snapshot.effortLevel ?? undefined);
|
|
663
|
+
// Encoder + button state
|
|
664
|
+
const encEvt = computeEncoderState();
|
|
665
|
+
core.broadcast(encEvt);
|
|
666
|
+
const btnEvt = computeButtonState();
|
|
667
|
+
core.broadcast(btnEvt);
|
|
407
668
|
});
|
|
408
|
-
//
|
|
409
|
-
wsServer.onCommand((cmd) => {
|
|
410
|
-
debug('
|
|
411
|
-
//
|
|
412
|
-
// ClaudeCode: switch_mode, interrupt, escape, respond
|
|
413
|
-
// OpenClaw: also select_option, navigate_option, send_prompt (via RPC)
|
|
669
|
+
// ===== Commands from WS clients =====
|
|
670
|
+
core.wsServer.onCommand((cmd) => {
|
|
671
|
+
debug('agentdeck', `pluginCmd: ${cmd.type}`);
|
|
672
|
+
// Adapter-owned commands
|
|
414
673
|
if (adapter.handleCommand(cmd)) {
|
|
415
|
-
// Adapter handled the transport side; update StateMachine as needed
|
|
416
674
|
switch (cmd.type) {
|
|
417
675
|
case 'respond':
|
|
418
|
-
stateMachine.handleUserAction('respond');
|
|
676
|
+
core.stateMachine.handleUserAction('respond');
|
|
419
677
|
break;
|
|
420
678
|
case 'interrupt':
|
|
421
|
-
stateMachine.handleUserAction('interrupt');
|
|
679
|
+
core.stateMachine.handleUserAction('interrupt');
|
|
422
680
|
break;
|
|
423
681
|
case 'escape':
|
|
424
|
-
stateMachine.handleUserAction('interrupt');
|
|
682
|
+
core.stateMachine.handleUserAction('interrupt');
|
|
425
683
|
break;
|
|
426
684
|
case 'select_option':
|
|
427
|
-
stateMachine.handleUserAction('select_option');
|
|
685
|
+
core.stateMachine.handleUserAction('select_option');
|
|
428
686
|
break;
|
|
429
687
|
case 'send_prompt':
|
|
430
|
-
stateMachine.handleUserAction('send_prompt');
|
|
688
|
+
core.stateMachine.handleUserAction('send_prompt');
|
|
431
689
|
break;
|
|
432
690
|
}
|
|
433
691
|
return;
|
|
434
692
|
}
|
|
435
|
-
//
|
|
693
|
+
// Bridge-coordinated commands
|
|
436
694
|
switch (cmd.type) {
|
|
437
695
|
case 'select_option': {
|
|
438
|
-
const snapshot = stateMachine.getSnapshot();
|
|
439
|
-
debug('sdc', `select_option: idx=${cmd.index} navigable=${snapshot.navigable} cursor=${snapshot.cursorIndex}`);
|
|
696
|
+
const snapshot = core.stateMachine.getSnapshot();
|
|
440
697
|
if (snapshot.navigable) {
|
|
441
|
-
// Arrow-key mode: navigate to desired option then press Enter
|
|
442
698
|
const delta = cmd.index - snapshot.cursorIndex;
|
|
443
699
|
if (delta !== 0) {
|
|
444
700
|
const arrow = delta > 0 ? '\x1b[B' : '\x1b[A';
|
|
445
|
-
|
|
446
|
-
debug('sdc', `select_option: navigating ${steps} steps ${delta > 0 ? 'down' : 'up'}`);
|
|
447
|
-
adapter.writeInput(arrow.repeat(steps));
|
|
701
|
+
adapter.writeInput(arrow.repeat(Math.abs(delta)));
|
|
448
702
|
}
|
|
449
|
-
|
|
450
|
-
const delay = 50 + Math.abs(delta) * 20;
|
|
451
|
-
setTimeout(() => {
|
|
452
|
-
adapter.writeInput('\r');
|
|
453
|
-
}, delay);
|
|
703
|
+
setTimeout(() => adapter.writeInput('\r'), 50 + Math.abs(delta) * 20);
|
|
454
704
|
}
|
|
455
705
|
else {
|
|
456
|
-
// Number input mode: type the 1-based index
|
|
457
706
|
adapter.writeInput(String(cmd.index + 1) + '\r');
|
|
458
707
|
}
|
|
459
|
-
stateMachine.handleUserAction('select_option');
|
|
708
|
+
core.stateMachine.handleUserAction('select_option');
|
|
460
709
|
break;
|
|
461
710
|
}
|
|
462
711
|
case 'navigate_option': {
|
|
463
|
-
const total = stateMachine.getOptionsCount();
|
|
464
|
-
const cur = stateMachine.getCursorIndex();
|
|
712
|
+
const total = core.stateMachine.getOptionsCount();
|
|
713
|
+
const cur = core.stateMachine.getCursorIndex();
|
|
465
714
|
const newIdx = total > 0
|
|
466
|
-
? (cmd.direction === 'up'
|
|
467
|
-
? Math.max(cur - 1, 0)
|
|
468
|
-
: Math.min(cur + 1, total - 1))
|
|
715
|
+
? (cmd.direction === 'up' ? Math.max(cur - 1, 0) : Math.min(cur + 1, total - 1))
|
|
469
716
|
: cur;
|
|
470
|
-
stateMachine.updateCursorIndex(newIdx, 'optimistic');
|
|
471
|
-
debug('sdc', `navigate_option: ${cmd.direction} cursor=${cur}->${newIdx}`);
|
|
717
|
+
core.stateMachine.updateCursorIndex(newIdx, 'optimistic');
|
|
472
718
|
adapter.prepareForNavigation?.();
|
|
473
719
|
adapter.writeInput(cmd.direction === 'up' ? '\x1b[A' : '\x1b[B');
|
|
474
|
-
// Don't call handleUserAction — cursor movement is not a selection
|
|
475
720
|
break;
|
|
476
721
|
}
|
|
477
722
|
case 'send_prompt': {
|
|
478
723
|
let text = cmd.text;
|
|
479
|
-
// Expand template references
|
|
480
724
|
const templateMatch = text.match(/^__template:(\d+)$/);
|
|
481
725
|
if (templateMatch) {
|
|
482
726
|
const idx = parseInt(templateMatch[1], 10);
|
|
483
727
|
if (idx >= 0 && idx < promptTemplates.length) {
|
|
484
728
|
text = promptTemplates[idx].prompt;
|
|
485
|
-
debug('sdc', `Template ${idx} → "${text.slice(0, 50)}"`);
|
|
486
729
|
}
|
|
487
|
-
else
|
|
488
|
-
debug('sdc', `Template ${idx} out of range (${promptTemplates.length} available)`);
|
|
730
|
+
else
|
|
489
731
|
break;
|
|
490
|
-
}
|
|
491
732
|
}
|
|
492
733
|
if (text) {
|
|
493
734
|
adapter.writeInput(text);
|
|
494
735
|
setTimeout(() => adapter.writeInput('\r'), 50);
|
|
495
|
-
stateMachine.handleUserAction('send_prompt');
|
|
736
|
+
core.stateMachine.handleUserAction('send_prompt');
|
|
496
737
|
}
|
|
497
738
|
break;
|
|
498
739
|
}
|
|
740
|
+
case 'utility':
|
|
741
|
+
utilityProxy.handleCommand(cmd);
|
|
742
|
+
core.broadcast(computeEncoderState());
|
|
743
|
+
break;
|
|
499
744
|
case 'voice':
|
|
500
|
-
handleVoiceCommand(cmd.action, voiceManager,
|
|
745
|
+
handleVoiceCommand(cmd.action, voiceManager, core);
|
|
501
746
|
break;
|
|
502
|
-
case 'query_usage':
|
|
503
|
-
|
|
504
|
-
debug('sdc', 'Fetching usage from API (on demand)');
|
|
505
|
-
fetchUsageFromApi().then((apiUsage) => {
|
|
506
|
-
if (apiUsage) {
|
|
507
|
-
cachedApiUsage = apiUsage;
|
|
508
|
-
lastApiFetchTime = Date.now();
|
|
509
|
-
if (apiUsage.inferredBillingType) {
|
|
510
|
-
stateMachine.inferBillingType(apiUsage.inferredBillingType);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
const snapshot = stateMachine.getSnapshot();
|
|
514
|
-
wsServer.broadcast(buildUsageEvent(snapshot, cachedApiUsage));
|
|
515
|
-
});
|
|
747
|
+
case 'query_usage':
|
|
748
|
+
core.fetchAndUpdateUsage().catch(() => { });
|
|
516
749
|
break;
|
|
517
|
-
}
|
|
518
750
|
}
|
|
519
751
|
});
|
|
520
|
-
//
|
|
521
|
-
wsServer.
|
|
522
|
-
|
|
523
|
-
|
|
752
|
+
// Deck slot map relay
|
|
753
|
+
core.wsServer.onRawMessage((msg, sender) => {
|
|
754
|
+
if (msg.type === 'deck_slot_map') {
|
|
755
|
+
cachedSlotMap = msg;
|
|
756
|
+
core.wsServer.broadcastExcept(cachedSlotMap, sender);
|
|
757
|
+
broadcastSse(cachedSlotMap);
|
|
758
|
+
core.broadcast(computeButtonState());
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
return false;
|
|
524
762
|
});
|
|
525
|
-
//
|
|
526
|
-
|
|
763
|
+
// WS connect/disconnect journal
|
|
764
|
+
core.wsServer.onClientDisconnect(() => {
|
|
765
|
+
journal.write('ws_event', 'ws', { action: 'disconnect', clients: core.wsServer.getClientCount() });
|
|
766
|
+
hookServer?.setMeta({ clientCount: core.wsServer.getClientCount() });
|
|
767
|
+
});
|
|
768
|
+
// Kick initial state
|
|
527
769
|
if (adapter.isAlive()) {
|
|
528
|
-
stateMachine.handleHookEvent('SessionStart', {});
|
|
770
|
+
core.stateMachine.handleHookEvent('SessionStart', {});
|
|
529
771
|
}
|
|
530
|
-
// Register
|
|
531
|
-
registerSession({
|
|
532
|
-
id: sessionId,
|
|
533
|
-
port,
|
|
534
|
-
pid: process.pid,
|
|
535
|
-
projectName: adapter.getProjectName() || projectName,
|
|
772
|
+
// Register session
|
|
773
|
+
core.registerSession(agentType, {
|
|
536
774
|
tmuxSession,
|
|
537
775
|
parentTty,
|
|
538
776
|
tty: adapter.getTtyPath(),
|
|
539
|
-
startedAt: new Date().toISOString(),
|
|
540
777
|
});
|
|
541
|
-
//
|
|
542
|
-
wsServer.onClientConnect((ws) => {
|
|
543
|
-
journal.write('ws_event', 'ws', { action: 'connect', clients: wsServer.getClientCount() });
|
|
544
|
-
hookServer?.setMeta({ clientCount: wsServer.getClientCount() });
|
|
545
|
-
const snapshot = stateMachine.getSnapshot();
|
|
546
|
-
//
|
|
547
|
-
let
|
|
778
|
+
// ===== Client connect =====
|
|
779
|
+
core.wsServer.onClientConnect((ws) => {
|
|
780
|
+
journal.write('ws_event', 'ws', { action: 'connect', clients: core.wsServer.getClientCount() });
|
|
781
|
+
hookServer?.setMeta({ clientCount: core.wsServer.getClientCount() });
|
|
782
|
+
const snapshot = core.stateMachine.getSnapshot();
|
|
783
|
+
// Restore suggested prompt on reconnect
|
|
784
|
+
let reconnectSuggestion = snapshot.suggestedPrompt;
|
|
785
|
+
if (!reconnectSuggestion && snapshot.state === State.IDLE) {
|
|
786
|
+
reconnectSuggestion = core.stateMachine.getLastValidSuggestedPrompt();
|
|
787
|
+
}
|
|
788
|
+
// Build state event with capabilities for initial connect
|
|
789
|
+
const stateEvent = core.buildStateEvent({
|
|
790
|
+
agentType: adapter.capabilities.type,
|
|
791
|
+
agentCapabilities: adapter.capabilities,
|
|
792
|
+
snapshot,
|
|
793
|
+
});
|
|
794
|
+
// Override suggestedPrompt with reconnect value
|
|
795
|
+
if (reconnectSuggestion) {
|
|
796
|
+
stateEvent.suggestedPrompt = reconnectSuggestion;
|
|
797
|
+
}
|
|
798
|
+
// Extra events for initial state
|
|
799
|
+
const extraEvents = [];
|
|
800
|
+
// Backward-compat prompt_options
|
|
548
801
|
if (snapshot.options.length > 0) {
|
|
549
|
-
initPromptType = 'multi_select';
|
|
802
|
+
let initPromptType = 'multi_select';
|
|
550
803
|
if (snapshot.state === State.AWAITING_PERMISSION) {
|
|
551
804
|
initPromptType = snapshot.options.length > 2 ? 'yes_no_always' : 'yes_no';
|
|
552
805
|
}
|
|
553
806
|
else if (snapshot.state === State.AWAITING_DIFF) {
|
|
554
807
|
initPromptType = 'diff_review';
|
|
555
808
|
}
|
|
556
|
-
|
|
557
|
-
// Restore last valid suggestion on reconnect when IDLE (current suggestedPrompt may already be null)
|
|
558
|
-
let reconnectSuggestion = snapshot.suggestedPrompt;
|
|
559
|
-
if (!reconnectSuggestion && snapshot.state === State.IDLE) {
|
|
560
|
-
reconnectSuggestion = stateMachine.getLastValidSuggestedPrompt();
|
|
561
|
-
if (reconnectSuggestion) {
|
|
562
|
-
debug('sdc', `Restoring lastValidSuggestedPrompt on reconnect: "${reconnectSuggestion.slice(0, 40)}"`);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
const stateEvent = {
|
|
566
|
-
type: 'state_update',
|
|
567
|
-
state: snapshot.state,
|
|
568
|
-
permissionMode: snapshot.permissionMode,
|
|
569
|
-
agentType: adapter.capabilities.type,
|
|
570
|
-
agentCapabilities: adapter.capabilities,
|
|
571
|
-
currentTool: snapshot.currentTool ?? undefined,
|
|
572
|
-
toolInput: snapshot.toolInput ?? undefined,
|
|
573
|
-
toolProgress: snapshot.toolProgress ?? undefined,
|
|
574
|
-
projectName: snapshot.projectName ?? undefined,
|
|
575
|
-
modelName: snapshot.modelName ?? undefined,
|
|
576
|
-
billingType: snapshot.billingType,
|
|
577
|
-
options: snapshot.options.length > 0 ? snapshot.options : undefined,
|
|
578
|
-
promptType: initPromptType,
|
|
579
|
-
question: snapshot.question ?? undefined,
|
|
580
|
-
navigable: snapshot.navigable || undefined,
|
|
581
|
-
cursorIndex: (snapshot.state === State.AWAITING_OPTION ||
|
|
582
|
-
snapshot.state === State.AWAITING_PERMISSION ||
|
|
583
|
-
snapshot.state === State.AWAITING_DIFF)
|
|
584
|
-
? snapshot.cursorIndex : undefined,
|
|
585
|
-
suggestedPrompt: reconnectSuggestion ?? undefined,
|
|
586
|
-
modelCatalog: cachedModelCatalog ?? undefined,
|
|
587
|
-
pairingUrl: wsUrl,
|
|
588
|
-
};
|
|
589
|
-
wsServer.sendTo(ws, stateEvent);
|
|
590
|
-
// Also send separate prompt_options for backward compatibility
|
|
591
|
-
if (snapshot.options.length > 0) {
|
|
592
|
-
wsServer.sendTo(ws, {
|
|
809
|
+
extraEvents.push({
|
|
593
810
|
type: 'prompt_options',
|
|
594
811
|
promptType: initPromptType,
|
|
595
812
|
question: snapshot.question ?? undefined,
|
|
596
813
|
options: snapshot.options,
|
|
597
814
|
});
|
|
598
815
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
fetchUsageFromApi().then((apiUsage) => {
|
|
612
|
-
if (apiUsage) {
|
|
613
|
-
cachedApiUsage = apiUsage;
|
|
614
|
-
lastApiFetchTime = Date.now();
|
|
615
|
-
if (apiUsage.inferredBillingType) {
|
|
616
|
-
stateMachine.inferBillingType(apiUsage.inferredBillingType);
|
|
617
|
-
}
|
|
618
|
-
const snap2 = stateMachine.getSnapshot();
|
|
619
|
-
wsServer.broadcast(buildUsageEvent(snap2, cachedApiUsage));
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
}
|
|
816
|
+
// Encoder + button state
|
|
817
|
+
extraEvents.push(computeEncoderState());
|
|
818
|
+
extraEvents.push(computeButtonState());
|
|
819
|
+
// Slot map
|
|
820
|
+
if (cachedSlotMap)
|
|
821
|
+
extraEvents.push(cachedSlotMap);
|
|
822
|
+
core.sendInitialState(ws, {
|
|
823
|
+
agentType: adapter.capabilities.type,
|
|
824
|
+
agentCapabilities: adapter.capabilities,
|
|
825
|
+
isAlive: adapter.isAlive(),
|
|
826
|
+
extraEvents,
|
|
827
|
+
});
|
|
623
828
|
});
|
|
624
|
-
//
|
|
829
|
+
// ===== Terminal attachment =====
|
|
625
830
|
if (adapter.capabilities.hasTerminal) {
|
|
626
|
-
if (process.stdin.isTTY)
|
|
831
|
+
if (process.stdin.isTTY)
|
|
627
832
|
process.stdin.setRawMode(true);
|
|
628
|
-
}
|
|
629
833
|
process.stdin.resume();
|
|
630
834
|
adapter.attachTerminal(process.stdin, process.stdout);
|
|
631
835
|
}
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
836
|
+
// ===== Polling =====
|
|
837
|
+
core.startUsageTick();
|
|
838
|
+
core.startApiUsagePolling(90_000);
|
|
839
|
+
core.startOllamaProbe();
|
|
840
|
+
core.startMlxProbe();
|
|
841
|
+
core.startAntigravityProbe();
|
|
842
|
+
core.startGatewayProbe(800);
|
|
843
|
+
core.startGatewayHealthCheck();
|
|
844
|
+
core.startSessionsListPolling();
|
|
845
|
+
// Inject virtual OpenClaw session when Gateway is detected (same as daemon-server.ts)
|
|
846
|
+
core.setSessionsEnricher((sessions) => {
|
|
847
|
+
if (!core.cachedGatewayAvailable)
|
|
848
|
+
return sessions;
|
|
849
|
+
if (sessions.some(s => s.agentType === 'openclaw'))
|
|
850
|
+
return sessions;
|
|
851
|
+
return [...sessions, {
|
|
852
|
+
id: 'openclaw-gateway',
|
|
853
|
+
port: 18789,
|
|
854
|
+
projectName: 'OpenClaw',
|
|
855
|
+
agentType: 'openclaw',
|
|
856
|
+
alive: true,
|
|
857
|
+
}];
|
|
858
|
+
});
|
|
859
|
+
// ===== Encoder state computation =====
|
|
860
|
+
function computeEncoderState() {
|
|
861
|
+
const snapshot = core.stateMachine.getSnapshot();
|
|
862
|
+
const isInteractive = snapshot.state === State.AWAITING_OPTION ||
|
|
863
|
+
snapshot.state === State.AWAITING_PERMISSION ||
|
|
864
|
+
snapshot.state === State.AWAITING_DIFF;
|
|
865
|
+
const utilState = utilityProxy.getState();
|
|
866
|
+
const e1 = {
|
|
867
|
+
slot: 0, encoderType: 'utility',
|
|
868
|
+
header: utilState.mode.toUpperCase(),
|
|
869
|
+
value: utilState.value, icon: utilState.icon,
|
|
870
|
+
accentColor: '#3b82f6', progress: utilState.level,
|
|
871
|
+
};
|
|
872
|
+
const e2 = {
|
|
873
|
+
slot: 1, encoderType: 'action',
|
|
874
|
+
header: 'ACTION', accentColor: '#f59e0b',
|
|
875
|
+
};
|
|
876
|
+
if (isInteractive && snapshot.options.length > 0) {
|
|
877
|
+
const opt = snapshot.options[snapshot.cursorIndex];
|
|
878
|
+
e2.header = 'OPTION';
|
|
879
|
+
e2.value = opt?.label ?? '';
|
|
880
|
+
e2.counter = `${snapshot.cursorIndex + 1}/${snapshot.options.length}`;
|
|
881
|
+
e2.detail = opt?.label;
|
|
882
|
+
}
|
|
883
|
+
else if (snapshot.suggestedPrompt) {
|
|
884
|
+
e2.header = 'PROMPT';
|
|
885
|
+
e2.value = snapshot.suggestedPrompt;
|
|
886
|
+
}
|
|
887
|
+
const pct5 = core.cachedApiUsage?.fiveHourPercent ?? null;
|
|
888
|
+
const pct7 = core.cachedApiUsage?.sevenDayPercent ?? null;
|
|
889
|
+
const worstPct = Math.max(pct5 ?? 0, pct7 ?? 0);
|
|
890
|
+
const usageColor = worstPct > 80 ? '#ef4444' : worstPct > 50 ? '#eab308' : '#22c55e';
|
|
891
|
+
const e3 = {
|
|
892
|
+
slot: 2, encoderType: 'usage',
|
|
893
|
+
header: 'USAGE',
|
|
894
|
+
value: pct5 != null ? `${Math.round(pct5)}% / ${Math.round(pct7 ?? 0)}%` : '\u2014',
|
|
895
|
+
accentColor: usageColor,
|
|
896
|
+
progress: worstPct / 100,
|
|
897
|
+
};
|
|
898
|
+
const isRec = voiceManager.isRecording();
|
|
899
|
+
const e4 = {
|
|
900
|
+
slot: 3, encoderType: 'voice',
|
|
901
|
+
header: 'VOICE', value: isRec ? 'Recording...' : 'Ready',
|
|
902
|
+
icon: isRec ? '\uD83D\uDD34' : '\uD83C\uDFA4',
|
|
903
|
+
accentColor: isRec ? '#ef4444' : '#8b5cf6',
|
|
904
|
+
voiceState: isRec ? 'recording' : 'idle',
|
|
905
|
+
};
|
|
906
|
+
return {
|
|
907
|
+
type: 'encoder_state',
|
|
908
|
+
encoders: [e1, e2, e3, e4],
|
|
909
|
+
takeoverActive: isInteractive && snapshot.options.length > 0,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
// ===== Button state computation =====
|
|
913
|
+
function computeButtonState() {
|
|
914
|
+
const snapshot = core.stateMachine.getSnapshot();
|
|
915
|
+
const st = snapshot.state;
|
|
916
|
+
const mode = snapshot.permissionMode;
|
|
917
|
+
const options = snapshot.options;
|
|
918
|
+
const isInteractive = st === State.AWAITING_OPTION ||
|
|
919
|
+
st === State.AWAITING_PERMISSION ||
|
|
920
|
+
st === State.AWAITING_DIFF;
|
|
921
|
+
const DIM = {
|
|
922
|
+
slot: 0, title: '', bgColor: '#1a1a1a', textColor: '#444444',
|
|
923
|
+
enabled: false, dim: true,
|
|
924
|
+
};
|
|
925
|
+
function getIdleButtons() {
|
|
926
|
+
if (!cachedSlotMap)
|
|
927
|
+
return DEFAULT_IDLE_BUTTONS;
|
|
928
|
+
const responseSlots = cachedSlotMap.buttons
|
|
929
|
+
.filter(s => s.actionType === 'response-button')
|
|
930
|
+
.sort((a, b) => a.slot - b.slot);
|
|
931
|
+
if (responseSlots.length === 0)
|
|
932
|
+
return DEFAULT_IDLE_BUTTONS;
|
|
933
|
+
return responseSlots.map((s, i) => ({
|
|
934
|
+
label: s.settings?.label ?? DEFAULT_IDLE_BUTTONS[i]?.label ?? '',
|
|
935
|
+
action: s.settings?.action ?? DEFAULT_IDLE_BUTTONS[i]?.action ?? '',
|
|
936
|
+
}));
|
|
937
|
+
}
|
|
938
|
+
function colorForOption(opt) {
|
|
939
|
+
const shortcut = (opt.shortcut ?? '').toLowerCase();
|
|
940
|
+
const lower = opt.label.toLowerCase();
|
|
941
|
+
if (lower.startsWith('always'))
|
|
942
|
+
return { bg: '#1e40af', text: '#ffffff' };
|
|
943
|
+
if (/don[''\u2019]t\s+ask\s+again/i.test(lower))
|
|
944
|
+
return { bg: '#1e40af', text: '#ffffff' };
|
|
945
|
+
if (/allow\s+all\s+sessions/i.test(lower))
|
|
946
|
+
return { bg: '#1e40af', text: '#ffffff' };
|
|
947
|
+
if (shortcut === 'n' || shortcut === 'd' || lower.startsWith('no') || lower.startsWith('deny'))
|
|
948
|
+
return { bg: '#991b1b', text: '#ffffff' };
|
|
949
|
+
if (shortcut === 'y' || shortcut === 'a')
|
|
950
|
+
return { bg: '#166534', text: '#ffffff' };
|
|
951
|
+
if (opt.recommended)
|
|
952
|
+
return { bg: '#1e4d2b', text: '#86efac' };
|
|
953
|
+
return { bg: '#1e3a5f', text: '#93c5fd' };
|
|
954
|
+
}
|
|
955
|
+
function uppercaseShort(label) {
|
|
956
|
+
return label.length <= 12 ? label.toUpperCase() : label;
|
|
957
|
+
}
|
|
958
|
+
const buttons = [];
|
|
959
|
+
// Slot 0: Mode
|
|
960
|
+
const modeColors = {
|
|
961
|
+
default: '#2a2a2a', plan: '#7c3aed', acceptEdits: '#2563eb',
|
|
962
|
+
dontAsk: '#0e7490', bypassPermissions: '#991b1b',
|
|
963
|
+
};
|
|
964
|
+
const modeLabels = {
|
|
965
|
+
default: 'DEFAULT', plan: 'PLAN', acceptEdits: 'ACCEPT',
|
|
966
|
+
dontAsk: "DON'T ASK", bypassPermissions: 'BYPASS',
|
|
967
|
+
};
|
|
968
|
+
const modeEnabled = st === State.IDLE;
|
|
969
|
+
buttons.push({
|
|
970
|
+
slot: 0,
|
|
971
|
+
title: modeLabels[mode] ?? 'DEFAULT',
|
|
972
|
+
subtitle: 'Mode',
|
|
973
|
+
bgColor: modeEnabled ? (modeColors[mode] ?? '#2a2a2a') : '#1a1a1a',
|
|
974
|
+
textColor: modeEnabled ? '#ffffff' : '#444444',
|
|
975
|
+
enabled: modeEnabled,
|
|
976
|
+
action: modeEnabled ? 'switch_mode' : undefined,
|
|
977
|
+
dim: !modeEnabled,
|
|
978
|
+
});
|
|
979
|
+
// Slot 1: Session/Status
|
|
980
|
+
if (st === State.IDLE) {
|
|
981
|
+
// Skip model-agnostic neutral levels — "medium" (legacy default) and "default"
|
|
982
|
+
// (Claude Code 2.1+ name for the per-model default). Show everything else
|
|
983
|
+
// including max/xhigh/high/low/fast as an informational suffix.
|
|
984
|
+
const effortSuffix = snapshot.effortLevel
|
|
985
|
+
&& snapshot.effortLevel !== 'medium'
|
|
986
|
+
&& snapshot.effortLevel !== 'default'
|
|
987
|
+
? snapshot.effortLevel : null;
|
|
988
|
+
const sessionSubtitle = effortSuffix && snapshot.modelName
|
|
989
|
+
? `${snapshot.modelName} \u00B7 ${effortSuffix}` : snapshot.modelName ?? undefined;
|
|
990
|
+
buttons.push({
|
|
991
|
+
slot: 1, title: snapshot.projectName ?? '\u2014',
|
|
992
|
+
subtitle: sessionSubtitle, bgColor: '#1e293b', textColor: '#ffffff', enabled: false,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
else if (st === State.PROCESSING) {
|
|
996
|
+
buttons.push({
|
|
997
|
+
slot: 1, title: snapshot.currentTool ?? '...',
|
|
998
|
+
subtitle: snapshot.toolProgress ?? undefined,
|
|
999
|
+
bgColor: '#1e3a5f', textColor: '#93c5fd', enabled: false,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
else if (st === State.AWAITING_PERMISSION || st === State.AWAITING_DIFF) {
|
|
1003
|
+
buttons.push({ slot: 1, title: 'PERMIT?', bgColor: '#b45309', textColor: '#ffffff', enabled: false });
|
|
1004
|
+
}
|
|
1005
|
+
else if (st === State.AWAITING_OPTION) {
|
|
1006
|
+
buttons.push({ slot: 1, title: 'SELECT', bgColor: '#b45309', textColor: '#ffffff', enabled: false });
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
buttons.push({ ...DIM, slot: 1 });
|
|
1010
|
+
}
|
|
1011
|
+
// Slot 2: Usage
|
|
1012
|
+
const pct = core.cachedApiUsage?.fiveHourPercent ?? null;
|
|
1013
|
+
const usageText = pct != null ? `${Math.round(pct)}%` : '\u2014';
|
|
1014
|
+
const usageBg = pct == null ? '#1e293b' : pct >= 90 ? '#991b1b' : pct >= 70 ? '#92400e' : '#166534';
|
|
1015
|
+
buttons.push({ slot: 2, title: usageText, subtitle: '5h', bgColor: usageBg, textColor: '#ffffff', enabled: false });
|
|
1016
|
+
// Slots 3-6: Response buttons
|
|
1017
|
+
if (st === State.IDLE) {
|
|
1018
|
+
const idleBtns = getIdleButtons();
|
|
1019
|
+
for (let i = 0; i < 4; i++) {
|
|
1020
|
+
const btn = idleBtns[i];
|
|
1021
|
+
if (btn) {
|
|
1022
|
+
const isGoOn = btn.action === 'continue' || btn.label.toLowerCase() === 'go on';
|
|
1023
|
+
buttons.push({
|
|
1024
|
+
slot: 3 + i, title: btn.label,
|
|
1025
|
+
bgColor: isGoOn ? '#1e3a2f' : '#1e293b',
|
|
1026
|
+
textColor: isGoOn ? '#22c55e' : '#ffffff',
|
|
1027
|
+
enabled: true, action: `command:${btn.action === 'continue' ? 'go on' : btn.action}`,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
buttons.push({ ...DIM, slot: 3 + i });
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
else if (st === State.PROCESSING) {
|
|
1036
|
+
for (let i = 0; i < 4; i++)
|
|
1037
|
+
buttons.push({ ...DIM, slot: 3 + i });
|
|
1038
|
+
}
|
|
1039
|
+
else if (isInteractive) {
|
|
1040
|
+
if (options.length === 0 && st === State.AWAITING_PERMISSION) {
|
|
1041
|
+
buttons.push({ slot: 3, title: 'YES', bgColor: '#166534', textColor: '#ffffff', enabled: true, action: 'respond:y' });
|
|
1042
|
+
buttons.push({ slot: 4, title: 'NO', bgColor: '#991b1b', textColor: '#ffffff', enabled: true, action: 'respond:n' });
|
|
1043
|
+
buttons.push({ slot: 5, title: 'ALWAYS', bgColor: '#1e40af', textColor: '#ffffff', enabled: true, action: 'respond:a' });
|
|
1044
|
+
buttons.push({ ...DIM, slot: 6 });
|
|
1045
|
+
}
|
|
1046
|
+
else if (options.length === 0 && st === State.AWAITING_DIFF) {
|
|
1047
|
+
buttons.push({ slot: 3, title: 'APPLY', bgColor: '#166534', textColor: '#ffffff', enabled: true, action: 'respond:a' });
|
|
1048
|
+
buttons.push({ slot: 4, title: 'DENY', bgColor: '#991b1b', textColor: '#ffffff', enabled: true, action: 'respond:d' });
|
|
1049
|
+
buttons.push({ slot: 5, title: 'VIEW', bgColor: '#1e3a5f', textColor: '#93c5fd', enabled: true, action: 'respond:v' });
|
|
1050
|
+
buttons.push({ ...DIM, slot: 6 });
|
|
1051
|
+
}
|
|
1052
|
+
else if (options.length <= 4) {
|
|
1053
|
+
for (let i = 0; i < 4; i++) {
|
|
1054
|
+
if (i < options.length) {
|
|
1055
|
+
const opt = options[i];
|
|
1056
|
+
const colors = (st === State.AWAITING_PERMISSION || st === State.AWAITING_DIFF)
|
|
1057
|
+
? colorForOption(opt) : opt.recommended ? { bg: '#1e4d2b', text: '#86efac' } : { bg: '#1e3a5f', text: '#93c5fd' };
|
|
1058
|
+
const action = snapshot.navigable
|
|
1059
|
+
? `select_option:${opt.index}` : `respond:${opt.shortcut || opt.label.charAt(0).toLowerCase()}`;
|
|
1060
|
+
buttons.push({
|
|
1061
|
+
slot: 3 + i, title: uppercaseShort(opt.label),
|
|
1062
|
+
bgColor: colors.bg, textColor: colors.text,
|
|
1063
|
+
enabled: true, badge: opt.recommended ? '\u2605' : opt.selected ? '\u2713' : undefined,
|
|
1064
|
+
action,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
buttons.push({ ...DIM, slot: 3 + i });
|
|
648
1069
|
}
|
|
649
|
-
// Broadcast updated usage so clients see fresh rate-limit data
|
|
650
|
-
const snapshot = stateMachine.getSnapshot();
|
|
651
|
-
wsServer.broadcast(buildUsageEvent(snapshot, cachedApiUsage));
|
|
652
1070
|
}
|
|
653
|
-
}
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
for (let i = 0; i < 3; i++) {
|
|
1074
|
+
const opt = options[i];
|
|
1075
|
+
const colors = (st === State.AWAITING_PERMISSION || st === State.AWAITING_DIFF)
|
|
1076
|
+
? colorForOption(opt) : opt.recommended ? { bg: '#1e4d2b', text: '#86efac' } : { bg: '#1e3a5f', text: '#93c5fd' };
|
|
1077
|
+
const action = snapshot.navigable
|
|
1078
|
+
? `select_option:${opt.index}` : `respond:${opt.shortcut || opt.label.charAt(0).toLowerCase()}`;
|
|
1079
|
+
buttons.push({
|
|
1080
|
+
slot: 3 + i, title: uppercaseShort(opt.label),
|
|
1081
|
+
bgColor: colors.bg, textColor: colors.text, enabled: true, action,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
buttons.push({ slot: 6, title: 'MORE \u25BC', bgColor: '#334155', textColor: '#94a3b8', enabled: true, action: 'expand_options' });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
for (let i = 0; i < 4; i++)
|
|
1089
|
+
buttons.push({ ...DIM, slot: 3 + i });
|
|
654
1090
|
}
|
|
655
|
-
|
|
656
|
-
|
|
1091
|
+
// Slot 7: Stop/ESC
|
|
1092
|
+
if (st === State.PROCESSING) {
|
|
1093
|
+
buttons.push({ slot: 7, title: 'STOP', bgColor: '#cc0000', textColor: '#ffffff', enabled: true, action: 'interrupt' });
|
|
1094
|
+
}
|
|
1095
|
+
else if (isInteractive) {
|
|
1096
|
+
buttons.push({ slot: 7, title: 'ESC', bgColor: '#b45309', textColor: '#ffffff', enabled: true, action: 'escape' });
|
|
1097
|
+
}
|
|
1098
|
+
else if (st === State.IDLE) {
|
|
1099
|
+
buttons.push({ slot: 7, title: 'ESC', bgColor: '#3d2607', textColor: '#ffb347', enabled: true, action: 'escape', dim: false });
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
buttons.push({ ...DIM, slot: 7 });
|
|
1103
|
+
}
|
|
1104
|
+
return { type: 'button_state', buttons };
|
|
1105
|
+
}
|
|
1106
|
+
// ===== Shutdown =====
|
|
657
1107
|
let shutdownInProgress = false;
|
|
658
1108
|
function shutdown() {
|
|
659
1109
|
if (shutdownInProgress)
|
|
660
1110
|
return;
|
|
661
1111
|
shutdownInProgress = true;
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (process.stdin.isTTY) {
|
|
1112
|
+
// Hard failsafe — exit no matter what after 3s
|
|
1113
|
+
setTimeout(() => process.exit(0), 3000).unref();
|
|
1114
|
+
log('Shutting down...');
|
|
1115
|
+
if (process.stdin.isTTY)
|
|
667
1116
|
process.stdin.setRawMode(false);
|
|
668
|
-
|
|
669
|
-
|
|
1117
|
+
process.stdin.pause();
|
|
1118
|
+
process.stdin.removeAllListeners();
|
|
1119
|
+
postit?.cleanup();
|
|
1120
|
+
utilityProxy.cleanup();
|
|
1121
|
+
voiceAssistant?.stop();
|
|
670
1122
|
voiceManager.disconnectFromServer();
|
|
1123
|
+
bridgeLogStream?.stop();
|
|
671
1124
|
journal.close();
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1125
|
+
core.onShutdown(async () => {
|
|
1126
|
+
await Promise.all([
|
|
1127
|
+
adapter.shutdown(),
|
|
1128
|
+
stopModules(startedModules).catch(() => { }),
|
|
1129
|
+
]);
|
|
675
1130
|
process.exit(0);
|
|
676
1131
|
});
|
|
677
|
-
|
|
678
|
-
setTimeout(() => {
|
|
679
|
-
process.exit(1);
|
|
680
|
-
}, 3000);
|
|
1132
|
+
core.shutdown();
|
|
681
1133
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1134
|
+
}
|
|
1135
|
+
// Mid-session classify + turn-level eval enqueue.
|
|
1136
|
+
// Shared between Claude spinner_stop, OpenClaw/OpenCode chat_response, and Codex spinner_stop
|
|
1137
|
+
// so all agents get the same turns.task_category stamping and non-code turn_judge evals.
|
|
1138
|
+
async function classifyAndEnqueueTurn(apme, sid) {
|
|
1139
|
+
const turnId = apme.collector.getActiveTurnId(sid);
|
|
1140
|
+
const runId = apme.collector.getRunId(sid);
|
|
1141
|
+
if (!turnId || !runId)
|
|
1142
|
+
return;
|
|
1143
|
+
const run = apme.store.getRun(runId);
|
|
1144
|
+
if (!run)
|
|
1145
|
+
return;
|
|
1146
|
+
let category = run.taskCategory ?? null;
|
|
1147
|
+
if (!category) {
|
|
1148
|
+
try {
|
|
1149
|
+
const { classifyRun } = await import('./apme/classifier.js');
|
|
1150
|
+
const { category: c, signals } = classifyRun(apme.store, run.id);
|
|
1151
|
+
if (c && c !== 'unknown') {
|
|
1152
|
+
category = c;
|
|
1153
|
+
apme.store.updateRun(run.id, {
|
|
1154
|
+
taskCategory: c,
|
|
1155
|
+
taskSignals: JSON.stringify(signals),
|
|
1156
|
+
taskCategorySource: 'rule',
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
catch (err) {
|
|
1161
|
+
debug('APME', `mid-session classify failed: ${String(err)}`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (category) {
|
|
1165
|
+
try {
|
|
1166
|
+
apme.store.updateTurn(turnId, { taskCategory: category });
|
|
1167
|
+
}
|
|
1168
|
+
catch { /* ignore */ }
|
|
1169
|
+
}
|
|
1170
|
+
const NON_CODE = new Set(['conversation', 'planning', 'research', 'review']);
|
|
1171
|
+
if (category && NON_CODE.has(category)) {
|
|
1172
|
+
apme.runner.enqueueTurn({ runId: run.id, turnId, category });
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// ===== Non-Claude agent APME wiring =====
|
|
1176
|
+
// For OpenCode, Codex, OpenClaw: intercepts adapter timeline events to create
|
|
1177
|
+
// APME turns with prompts and responses. Claude Code uses hooks instead.
|
|
1178
|
+
function wireAgentApme(adapter, agentType, apme, core, ptyRingBuffer) {
|
|
1179
|
+
const sid = core.sessionId;
|
|
1180
|
+
adapter.on('event', (evt) => {
|
|
1181
|
+
// ── Timeline events (OpenCode / OpenClaw) ──
|
|
1182
|
+
// Both adapters emit { source: 'timeline', entry: TimelineEntry }.
|
|
1183
|
+
// The timeline adapter normalizes them to AgentDeck telemetry spans.
|
|
1184
|
+
if (evt.source === 'timeline' && evt.entry) {
|
|
1185
|
+
const entry = evt.entry;
|
|
1186
|
+
const ctx = makeApmeAdapterCtx(apme, sid, agentType);
|
|
1187
|
+
for (const span of timelineEntryToSpans(ctx, entry)) {
|
|
1188
|
+
apme.collector.ingestSpan(sid, span);
|
|
1189
|
+
}
|
|
1190
|
+
// Mid-session classification still runs once we've captured a response.
|
|
1191
|
+
if (entry.type === 'chat_response') {
|
|
1192
|
+
void classifyAndEnqueueTurn(apme, sid);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// ── Codex: PTY parser fallback ──
|
|
1196
|
+
if (evt.source === 'parser' && agentType === 'codex-cli') {
|
|
1197
|
+
const ctx = makeApmeAdapterCtx(apme, sid, agentType);
|
|
1198
|
+
if (evt.event === 'user_prompt') {
|
|
1199
|
+
const text = evt.data?.text;
|
|
1200
|
+
if (text) {
|
|
1201
|
+
// PTY-derived prompt = synthetic turn_start span. We construct it
|
|
1202
|
+
// inline rather than reusing claudeHookToSpans because the source
|
|
1203
|
+
// is a parser event, not a hook payload.
|
|
1204
|
+
const span = {
|
|
1205
|
+
traceId: ctx.traceId,
|
|
1206
|
+
spanId: randomUUID(),
|
|
1207
|
+
parentSpanId: ctx.activeTurnId,
|
|
1208
|
+
name: spanNameForKind('turn_start'),
|
|
1209
|
+
kind: 'turn_start',
|
|
1210
|
+
ts: Date.now(),
|
|
1211
|
+
attributes: {
|
|
1212
|
+
'agentdeck.agent_type': ctx.agentType,
|
|
1213
|
+
'agentdeck.prompt_text': text,
|
|
1214
|
+
...(ctx.cwd ? { 'agentdeck.cwd': ctx.cwd } : {}),
|
|
1215
|
+
},
|
|
1216
|
+
};
|
|
1217
|
+
apme.collector.ingestSpan(sid, span);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (evt.event === 'spinner_stop') {
|
|
1221
|
+
setTimeout(() => {
|
|
1222
|
+
const tail = ptyRingBuffer.getTail(5000);
|
|
1223
|
+
const lines = tail.split('\n').map(l => l.trim()).filter(Boolean);
|
|
1224
|
+
// Filter out spinner/UI artifacts from tail (Codex PTY output)
|
|
1225
|
+
const clean = lines.filter(l => !/^[✢✳✶✻✽⏸⏵❯─>]/.test(l) &&
|
|
1226
|
+
!/planmode|plan\s*mode|shift\+tab|accept\s*edits/i.test(l) &&
|
|
1227
|
+
!/\?\s*for\s*shortcuts/.test(l));
|
|
1228
|
+
const response = clean.slice(-5).join('\n');
|
|
1229
|
+
if (response.length > 2) {
|
|
1230
|
+
const span = claudePtyResponseToSpan(ctx, response);
|
|
1231
|
+
if (span)
|
|
1232
|
+
apme.collector.ingestSpan(sid, span);
|
|
1233
|
+
void classifyAndEnqueueTurn(apme, sid);
|
|
1234
|
+
}
|
|
1235
|
+
}, 500);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
691
1238
|
});
|
|
692
1239
|
}
|
|
693
|
-
|
|
1240
|
+
/** Build the AdapterContext used by every APME ingestion adapter. The
|
|
1241
|
+
* trace id mirrors the active run id so future OTLP exports tie spans
|
|
1242
|
+
* emitted in the same session together. `cwd` falls back to `process.cwd()`
|
|
1243
|
+
* because the bridge is started from the project directory. */
|
|
1244
|
+
function makeApmeAdapterCtx(apme, sid, agentType) {
|
|
694
1245
|
return {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
estimatedCostUsd: snapshot.estimatedCostUsd ?? undefined,
|
|
701
|
-
sessionPercent: snapshot.sessionPercent ?? undefined,
|
|
702
|
-
costSpent: snapshot.costSpent ?? undefined,
|
|
703
|
-
costLimit: snapshot.costLimit ?? undefined,
|
|
704
|
-
resetTime: snapshot.resetTime ?? undefined,
|
|
705
|
-
resetDate: snapshot.resetDate ?? undefined,
|
|
706
|
-
fiveHourPercent: apiUsage?.fiveHourPercent ?? undefined,
|
|
707
|
-
fiveHourResetsAt: apiUsage?.fiveHourResetsAt ?? undefined,
|
|
708
|
-
sevenDayPercent: apiUsage?.sevenDayPercent ?? undefined,
|
|
709
|
-
sevenDayResetsAt: apiUsage?.sevenDayResetsAt ?? undefined,
|
|
710
|
-
extraUsageEnabled: apiUsage?.extraUsageEnabled ?? undefined,
|
|
711
|
-
extraUsageMonthlyLimit: apiUsage?.extraUsageMonthlyLimit ?? undefined,
|
|
712
|
-
extraUsageUsedCredits: apiUsage?.extraUsageUsedCredits ?? undefined,
|
|
713
|
-
extraUsageUtilization: apiUsage?.extraUsageUtilization ?? undefined,
|
|
1246
|
+
sessionId: sid,
|
|
1247
|
+
agentType,
|
|
1248
|
+
traceId: apme.collector.getRunId(sid) ?? sid,
|
|
1249
|
+
cwd: process.cwd(),
|
|
1250
|
+
activeTurnId: apme.collector.getActiveTurnId(sid) ?? undefined,
|
|
714
1251
|
};
|
|
715
1252
|
}
|
|
716
|
-
|
|
1253
|
+
// ===== Claude Code timeline wiring =====
|
|
1254
|
+
function wireClaudeCodeTimeline(adapter, core, journal, ptyRingBuffer) {
|
|
1255
|
+
let ccChatStart = null;
|
|
1256
|
+
let ccPendingChatStart = false;
|
|
1257
|
+
let ccPendingChatStartTimer = null;
|
|
1258
|
+
let ccLastPromptText = null;
|
|
1259
|
+
let ccLastHookToolTs = 0; // Change 1: track last hook-based tool_request
|
|
1260
|
+
let ccPendingCompletion = false; // Change 2: track pending chat_end for PTY fallback
|
|
1261
|
+
let ccPendingCompletionTimer = null;
|
|
1262
|
+
const emitChatStart = (text) => {
|
|
1263
|
+
if (ccPendingChatStartTimer) {
|
|
1264
|
+
clearTimeout(ccPendingChatStartTimer);
|
|
1265
|
+
ccPendingChatStartTimer = null;
|
|
1266
|
+
}
|
|
1267
|
+
ccPendingChatStart = false;
|
|
1268
|
+
ccLastPromptText = text || null;
|
|
1269
|
+
const snippet = text.length > 500 ? text.slice(0, 497) + '...' : text;
|
|
1270
|
+
const detail = text.length > 100 ? (text.length > 1000 ? text.slice(0, 1000) + '...' : text) : undefined;
|
|
1271
|
+
// Change 4: better fallback text — include project name
|
|
1272
|
+
const fallbackRaw = (() => {
|
|
1273
|
+
const proj = core.stateMachine.getSnapshot().projectName;
|
|
1274
|
+
return proj ? `Prompt \u00B7 ${proj}` : 'Prompt sent';
|
|
1275
|
+
})();
|
|
1276
|
+
core.bridgeTimeline.addEntry({
|
|
1277
|
+
ts: ccChatStart ?? Date.now(), type: 'chat_start',
|
|
1278
|
+
raw: snippet || fallbackRaw,
|
|
1279
|
+
...(detail ? { detail } : {}),
|
|
1280
|
+
agentType: 'claude-code',
|
|
1281
|
+
});
|
|
1282
|
+
};
|
|
1283
|
+
/** Extract response text from PTY ringbuffer after ⏺ marker (same pattern as APME) */
|
|
1284
|
+
const extractPtyResponse = () => {
|
|
1285
|
+
const tail = ptyRingBuffer.getTail(5000);
|
|
1286
|
+
const marker = tail.lastIndexOf('⏺');
|
|
1287
|
+
if (marker < 0)
|
|
1288
|
+
return '';
|
|
1289
|
+
const afterMarker = tail.slice(marker + 1).trim();
|
|
1290
|
+
const lines = afterMarker.split('\n');
|
|
1291
|
+
const clean = [];
|
|
1292
|
+
for (const line of lines) {
|
|
1293
|
+
const trimmed = line.trim();
|
|
1294
|
+
if (!trimmed)
|
|
1295
|
+
continue;
|
|
1296
|
+
if (/^[✢✳✶✻✽⏸⏵❯─>]/.test(trimmed))
|
|
1297
|
+
break;
|
|
1298
|
+
if (/planmode|plan\s*mode|shift\+tab|accept\s*edits/i.test(trimmed))
|
|
1299
|
+
break;
|
|
1300
|
+
if (/^\S+…(\s|$)/.test(trimmed) && /tokens|shortcuts|\d+[ms]\s/i.test(trimmed))
|
|
1301
|
+
break;
|
|
1302
|
+
if (/\?\s*for\s*shortcuts/.test(trimmed))
|
|
1303
|
+
break;
|
|
1304
|
+
clean.push(trimmed);
|
|
1305
|
+
}
|
|
1306
|
+
return clean.join('\n').trim();
|
|
1307
|
+
};
|
|
1308
|
+
/** Emit chat_response + chat_end from response text (shared by Stop hook and PTY fallback) */
|
|
1309
|
+
const emitCompletion = (responseText, duration, toolSummary) => {
|
|
1310
|
+
const now = Date.now();
|
|
1311
|
+
// Change 5: emit chat_response if meaningful response text exists
|
|
1312
|
+
if (responseText.length > 20) {
|
|
1313
|
+
const respRaw = responseText.length > 200 ? responseText.slice(0, 197) + '...' : responseText;
|
|
1314
|
+
core.bridgeTimeline.addEntry({
|
|
1315
|
+
ts: now - 1, type: 'chat_response',
|
|
1316
|
+
raw: cleanRawText(respRaw),
|
|
1317
|
+
detail: cleanDetailText(responseText.slice(0, 3000)) || undefined,
|
|
1318
|
+
agentType: 'claude-code',
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
const topicHint = responseText ? extractTopicHint(responseText) : null;
|
|
1322
|
+
const promptTopic = ccLastPromptText ? extractTopicHint(ccLastPromptText) : null;
|
|
1323
|
+
const completedLabel = topicHint || promptTopic || 'Completed';
|
|
1324
|
+
let summary = duration != null ? `${completedLabel} \u00B7 ${duration}s` : completedLabel;
|
|
1325
|
+
if (toolSummary)
|
|
1326
|
+
summary += ` \u00B7 ${toolSummary}`;
|
|
1327
|
+
const chatEndDetail = responseText
|
|
1328
|
+
? (() => { const c = cleanDetailText(responseText); return c ? (c.length > 1000 ? c.slice(0, 1000) + '...' : c) : undefined; })()
|
|
1329
|
+
: ccLastPromptText
|
|
1330
|
+
? `Prompt: ${ccLastPromptText.length > 200 ? ccLastPromptText.slice(0, 200) + '...' : ccLastPromptText}`
|
|
1331
|
+
: undefined;
|
|
1332
|
+
const chatEndTs = now;
|
|
1333
|
+
ccChatStart = null;
|
|
1334
|
+
ccLastPromptText = null;
|
|
1335
|
+
core.bridgeTimeline.addEntry({
|
|
1336
|
+
ts: chatEndTs, type: 'chat_end', raw: summary,
|
|
1337
|
+
...(chatEndDetail ? { detail: chatEndDetail } : {}),
|
|
1338
|
+
agentType: 'claude-code',
|
|
1339
|
+
});
|
|
1340
|
+
// Async LLM summarization — fire-and-forget, upsert chat_end when ready
|
|
1341
|
+
if (responseText && responseText.length > 30) {
|
|
1342
|
+
const savedDuration = duration;
|
|
1343
|
+
const savedToolSummary = toolSummary;
|
|
1344
|
+
const savedDetail = chatEndDetail;
|
|
1345
|
+
summarizeResponse(responseText).then((llmSummary) => {
|
|
1346
|
+
if (llmSummary) {
|
|
1347
|
+
const enrichedParts = [llmSummary];
|
|
1348
|
+
if (savedDuration != null)
|
|
1349
|
+
enrichedParts.push(`${savedDuration}s`);
|
|
1350
|
+
if (savedToolSummary)
|
|
1351
|
+
enrichedParts.push(savedToolSummary);
|
|
1352
|
+
debug('timeline', `CC LLM summary: ${llmSummary}`);
|
|
1353
|
+
core.bridgeTimeline.upsertEntry({
|
|
1354
|
+
ts: chatEndTs, type: 'chat_end',
|
|
1355
|
+
raw: enrichedParts.join(' \u00B7 '),
|
|
1356
|
+
...(savedDetail ? { detail: savedDetail } : {}),
|
|
1357
|
+
agentType: 'claude-code',
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}).catch(() => { });
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
// Change 4: late upsert — user_prompt metadata arriving after 500ms timer
|
|
1364
|
+
adapter.on('event', (evt) => {
|
|
1365
|
+
if (evt.source === 'metadata' && evt.event === 'user_prompt') {
|
|
1366
|
+
const text = evt.data?.text || '';
|
|
1367
|
+
if (ccPendingChatStart) {
|
|
1368
|
+
emitChatStart(text);
|
|
1369
|
+
}
|
|
1370
|
+
else if (ccChatStart && Date.now() - ccChatStart < 5000 && text) {
|
|
1371
|
+
// Late arrival — upsert chat_start with actual prompt text
|
|
1372
|
+
const snippet = text.length > 500 ? text.slice(0, 497) + '...' : text;
|
|
1373
|
+
core.bridgeTimeline.upsertEntry({
|
|
1374
|
+
ts: ccChatStart, type: 'chat_start',
|
|
1375
|
+
raw: snippet, agentType: 'claude-code',
|
|
1376
|
+
});
|
|
1377
|
+
ccLastPromptText = text;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
// Change 1: PTY tool_action → tool_exec timeline entry (fills gap when hooks don't fire)
|
|
1382
|
+
adapter.on('event', (evt) => {
|
|
1383
|
+
if (evt.source !== 'parser' || evt.event !== 'tool_action')
|
|
1384
|
+
return;
|
|
1385
|
+
const now = Date.now();
|
|
1386
|
+
// Skip if hook already emitted tool_request within 2s (avoid duplicates)
|
|
1387
|
+
if (now - ccLastHookToolTs < 2000)
|
|
1388
|
+
return;
|
|
1389
|
+
const toolName = evt.data?.toolName || 'tool';
|
|
1390
|
+
const toolArgs = evt.data?.toolArgs || '';
|
|
1391
|
+
const raw = toolArgs ? `${toolName} ${toolArgs}` : toolName;
|
|
1392
|
+
core.bridgeTimeline.addEntry({
|
|
1393
|
+
ts: now, type: 'tool_exec',
|
|
1394
|
+
raw: raw.length > 500 ? raw.slice(0, 497) + '...' : raw,
|
|
1395
|
+
agentType: 'claude-code',
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
// Change 2: PTY fallback chat_end from spinner_stop/idle (when Stop hook doesn't fire)
|
|
1399
|
+
adapter.on('event', (evt) => {
|
|
1400
|
+
if (evt.source !== 'parser')
|
|
1401
|
+
return;
|
|
1402
|
+
if ((evt.event === 'spinner_stop' || evt.event === 'idle') && ccPendingCompletion) {
|
|
1403
|
+
// Wait 1.5s — Stop hook may still arrive
|
|
1404
|
+
if (ccPendingCompletionTimer)
|
|
1405
|
+
clearTimeout(ccPendingCompletionTimer);
|
|
1406
|
+
ccPendingCompletionTimer = setTimeout(() => {
|
|
1407
|
+
ccPendingCompletionTimer = null;
|
|
1408
|
+
if (!ccPendingCompletion)
|
|
1409
|
+
return; // Stop hook already handled it
|
|
1410
|
+
ccPendingCompletion = false;
|
|
1411
|
+
if (ccPendingChatStart)
|
|
1412
|
+
emitChatStart('');
|
|
1413
|
+
const now = Date.now();
|
|
1414
|
+
const duration = ccChatStart ? Math.round((now - ccChatStart) / 1000) : null;
|
|
1415
|
+
const toolSummary = core.usageTracker.getToolSummary();
|
|
1416
|
+
// Extract response from PTY ringbuffer
|
|
1417
|
+
const responseText = extractPtyResponse();
|
|
1418
|
+
debug('timeline', `CC PTY fallback chat_end: respLen=${responseText.length} duration=${duration}`);
|
|
1419
|
+
emitCompletion(responseText, duration, toolSummary);
|
|
1420
|
+
}, 1500);
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
adapter.on('event', (evt) => {
|
|
1424
|
+
if (evt.source !== 'hook')
|
|
1425
|
+
return;
|
|
1426
|
+
const now = Date.now();
|
|
1427
|
+
switch (evt.event) {
|
|
1428
|
+
case 'UserPromptSubmit': {
|
|
1429
|
+
ccChatStart = now;
|
|
1430
|
+
core.usageTracker.resetToolCounts();
|
|
1431
|
+
ccPendingChatStart = true;
|
|
1432
|
+
ccPendingCompletion = true; // Change 2: arm PTY fallback
|
|
1433
|
+
// Claude Code v2.1+ sends { message: { content: "..." } }, not { prompt: "..." }
|
|
1434
|
+
const msg = evt.data?.message;
|
|
1435
|
+
const hookText = evt.data?.prompt
|
|
1436
|
+
|| evt.data?.text
|
|
1437
|
+
|| (typeof msg === 'object' && msg !== null ? msg.content : undefined)
|
|
1438
|
+
|| (typeof msg === 'string' ? msg : '')
|
|
1439
|
+
|| '';
|
|
1440
|
+
ccPendingChatStartTimer = setTimeout(() => {
|
|
1441
|
+
if (ccPendingChatStart)
|
|
1442
|
+
emitChatStart(hookText);
|
|
1443
|
+
}, 500);
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
case 'PreToolUse': {
|
|
1447
|
+
ccLastHookToolTs = now; // Change 1: mark hook tool time for dedup
|
|
1448
|
+
const toolName = evt.data?.tool_name || 'tool';
|
|
1449
|
+
const formatted = formatToolInputForTimeline(toolName, evt.data?.tool_input);
|
|
1450
|
+
core.bridgeTimeline.addEntry({
|
|
1451
|
+
ts: now, type: 'tool_request',
|
|
1452
|
+
raw: formatted ? `${toolName} ${formatted}` : toolName,
|
|
1453
|
+
agentType: 'claude-code',
|
|
1454
|
+
});
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
case 'PostToolUse': {
|
|
1458
|
+
// Change 3: include tool name instead of generic "Approved"
|
|
1459
|
+
const resolvedTool = evt.data?.tool_name || 'tool';
|
|
1460
|
+
core.bridgeTimeline.addEntry({ ts: now, type: 'tool_resolved', raw: `${resolvedTool} approved`, agentType: 'claude-code' });
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
case 'Stop': {
|
|
1464
|
+
// Race guard: if ccPendingCompletion is already false, the PTY fallback
|
|
1465
|
+
// timer fired first (Stop hook arrived >1.5s after spinner_stop) and
|
|
1466
|
+
// already emitted chat_response/chat_end. Skipping here prevents the
|
|
1467
|
+
// duplicate-line bug visible on the dashboard timeline.
|
|
1468
|
+
const wasPending = ccPendingCompletion;
|
|
1469
|
+
ccPendingCompletion = false; // Change 2: disarm PTY fallback — hook handled it
|
|
1470
|
+
if (ccPendingCompletionTimer) {
|
|
1471
|
+
clearTimeout(ccPendingCompletionTimer);
|
|
1472
|
+
ccPendingCompletionTimer = null;
|
|
1473
|
+
}
|
|
1474
|
+
if (!wasPending) {
|
|
1475
|
+
debug('timeline', 'CC Stop hook arrived after PTY fallback emit — skipping duplicate');
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
if (ccPendingChatStart)
|
|
1479
|
+
emitChatStart('');
|
|
1480
|
+
const duration = ccChatStart ? Math.round((now - ccChatStart) / 1000) : null;
|
|
1481
|
+
const toolSummary = core.usageTracker.getToolSummary();
|
|
1482
|
+
// Response capture priority (CLI bridge only — the App Store Swift
|
|
1483
|
+
// daemon is sandboxed off the projects/ path and relies on
|
|
1484
|
+
// Task 2's response_kind heuristic instead):
|
|
1485
|
+
// 1. transcript_path JSONL — authoritative; Claude Code writes
|
|
1486
|
+
// every user/assistant/tool block here, and the Stop hook
|
|
1487
|
+
// payload points at it.
|
|
1488
|
+
// 2. last_assistant_message — ~18% reliable field on the hook
|
|
1489
|
+
// payload; honoured when the transcript isn't reachable.
|
|
1490
|
+
// 3. PTY ringbuffer — last-resort fallback already handled by
|
|
1491
|
+
// the existing spinner_stop pathway.
|
|
1492
|
+
const transcriptPath = evt.data?.transcript_path || '';
|
|
1493
|
+
let lastAssistantMsg = evt.data?.last_assistant_message || '';
|
|
1494
|
+
if (transcriptPath) {
|
|
1495
|
+
try {
|
|
1496
|
+
const excerpt = readClaudeTranscriptLastTurn(transcriptPath);
|
|
1497
|
+
if (excerpt?.hasAssistantText) {
|
|
1498
|
+
lastAssistantMsg = excerpt.assistantText;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
catch (err) {
|
|
1502
|
+
debug('agentdeck', `transcript read skipped: ${String(err)}`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
// APME: store Claude's response on the current turn
|
|
1506
|
+
const apmeRef = core.getApme();
|
|
1507
|
+
if (apmeRef && lastAssistantMsg) {
|
|
1508
|
+
apmeRef.collector.setTurnResponse(core.sessionId, lastAssistantMsg);
|
|
1509
|
+
}
|
|
1510
|
+
emitCompletion(lastAssistantMsg, duration, toolSummary);
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
// ===== Voice command handler =====
|
|
1517
|
+
function handleVoiceCommand(action, voiceManager, core) {
|
|
717
1518
|
switch (action) {
|
|
718
1519
|
case 'start':
|
|
719
1520
|
voiceManager.startRecording();
|
|
720
|
-
wsServer.broadcast({ type: 'voice_state', state: 'recording' });
|
|
1521
|
+
core.wsServer.broadcast({ type: 'voice_state', state: 'recording' });
|
|
721
1522
|
break;
|
|
722
1523
|
case 'stop':
|
|
723
|
-
wsServer.broadcast({ type: 'voice_state', state: 'transcribing' });
|
|
1524
|
+
core.wsServer.broadcast({ type: 'voice_state', state: 'transcribing' });
|
|
724
1525
|
voiceManager.stopRecording().then((text) => {
|
|
725
|
-
|
|
726
|
-
// Don't auto-send — plugin shows review UI; user confirms via send_prompt
|
|
727
|
-
wsServer.broadcast({ type: 'voice_state', state: 'idle', text: text || '' });
|
|
1526
|
+
core.wsServer.broadcast({ type: 'voice_state', state: 'idle', text: text || '' });
|
|
728
1527
|
}).catch((err) => {
|
|
729
|
-
|
|
730
|
-
wsServer.broadcast({ type: 'voice_state', state: 'error', error: String(err) });
|
|
1528
|
+
core.wsServer.broadcast({ type: 'voice_state', state: 'error', error: String(err) });
|
|
731
1529
|
});
|
|
732
1530
|
break;
|
|
733
1531
|
case 'cancel':
|
|
734
1532
|
voiceManager.cancel();
|
|
735
|
-
wsServer.broadcast({ type: 'voice_state', state: 'idle' });
|
|
1533
|
+
core.wsServer.broadcast({ type: 'voice_state', state: 'idle' });
|
|
736
1534
|
break;
|
|
737
1535
|
}
|
|
738
1536
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
* 1. Hardcoded localhost:9120 → $AGENTDECK_PORT env var
|
|
742
|
-
* 2. Old flat format → new matcher-group format (Claude Code v2.1+)
|
|
743
|
-
* Old: { type: "command", command: "curl ..." }
|
|
744
|
-
* New: { matcher: "", hooks: [{ type: "command", command: "curl ..." }] }
|
|
745
|
-
*/
|
|
746
|
-
function migrateHooksIfNeeded() {
|
|
747
|
-
const settingsPath = join(homedir(), '.claude', 'settings.local.json');
|
|
748
|
-
try {
|
|
749
|
-
if (!existsSync(settingsPath))
|
|
750
|
-
return;
|
|
751
|
-
const raw = readFileSync(settingsPath, 'utf-8');
|
|
752
|
-
if (!raw.includes('AGENTDECK_PORT') && !raw.includes('localhost:9120'))
|
|
753
|
-
return;
|
|
754
|
-
const settings = JSON.parse(raw);
|
|
755
|
-
if (!settings.hooks)
|
|
756
|
-
return;
|
|
757
|
-
let migrated = false;
|
|
758
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
759
|
-
const hooks = settings.hooks[event];
|
|
760
|
-
if (!Array.isArray(hooks))
|
|
761
|
-
continue;
|
|
762
|
-
for (let i = 0; i < hooks.length; i++) {
|
|
763
|
-
const hook = hooks[i];
|
|
764
|
-
// Migration 1: hardcoded port → env var
|
|
765
|
-
if (hook.command?.includes('localhost:9120') && !hook.command?.includes('AGENTDECK_PORT')) {
|
|
766
|
-
hook.command = hook.command.replace(/localhost:9120/g, 'localhost:${AGENTDECK_PORT:-9120}');
|
|
767
|
-
migrated = true;
|
|
768
|
-
}
|
|
769
|
-
// Migration 2: flat format → matcher-group format
|
|
770
|
-
// Detect flat format: has "type" + "command" at top level, no "hooks" array
|
|
771
|
-
if (hook.type === 'command' && hook.command?.includes('AGENTDECK_PORT') && !hook.hooks) {
|
|
772
|
-
const handler = { type: hook.type, command: hook.command };
|
|
773
|
-
hooks[i] = { matcher: '', hooks: [handler] };
|
|
774
|
-
migrated = true;
|
|
775
|
-
}
|
|
776
|
-
// Also migrate matcher-group entries with hardcoded port inside
|
|
777
|
-
if (Array.isArray(hook.hooks)) {
|
|
778
|
-
for (const inner of hook.hooks) {
|
|
779
|
-
if (inner.command?.includes('localhost:9120') && !inner.command?.includes('AGENTDECK_PORT')) {
|
|
780
|
-
inner.command = inner.command.replace(/localhost:9120/g, 'localhost:${AGENTDECK_PORT:-9120}');
|
|
781
|
-
migrated = true;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
if (migrated) {
|
|
788
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
789
|
-
log('[sdc] Auto-migrated hooks to v2.1 matcher-group format');
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
catch (err) {
|
|
793
|
-
debug('sdc', `Hook migration check failed: ${err}`);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
1537
|
+
// ===== Hook migration =====
|
|
1538
|
+
import { migrateHooksIfNeeded } from '@agentdeck/hooks';
|
|
796
1539
|
//# sourceMappingURL=index.js.map
|