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