@agentdeck/bridge 0.2.1 → 0.2.3
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/assets/fonts/IBMPlexSans-Bold.ttf +0 -0
- package/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Bold.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/assets/fonts/LICENSES.md +22 -0
- package/dist/adapters/claude-code.d.ts +6 -0
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +8 -0
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/openclaw.d.ts +24 -0
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +291 -35
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/adapters/opencode-adapter.d.ts +59 -0
- package/dist/adapters/opencode-adapter.d.ts.map +1 -1
- package/dist/adapters/opencode-adapter.js +297 -35
- package/dist/adapters/opencode-adapter.js.map +1 -1
- package/dist/adb-reverse.d.ts +1 -1
- package/dist/adb-reverse.js +1 -1
- package/dist/antigravity-local.d.ts +1 -0
- package/dist/antigravity-local.d.ts.map +1 -1
- package/dist/antigravity-local.js +33 -0
- package/dist/antigravity-local.js.map +1 -1
- package/dist/apme/adapters/claude-hook.d.ts.map +1 -1
- package/dist/apme/adapters/claude-hook.js +5 -0
- package/dist/apme/adapters/claude-hook.js.map +1 -1
- package/dist/apme/adapters/codex-hook.d.ts +17 -0
- package/dist/apme/adapters/codex-hook.d.ts.map +1 -0
- package/dist/apme/adapters/codex-hook.js +95 -0
- package/dist/apme/adapters/codex-hook.js.map +1 -0
- package/dist/apme/adapters/codex-turn-manager.d.ts +86 -0
- package/dist/apme/adapters/codex-turn-manager.d.ts.map +1 -0
- package/dist/apme/adapters/codex-turn-manager.js +496 -0
- package/dist/apme/adapters/codex-turn-manager.js.map +1 -0
- package/dist/apme/adapters/openclaw-hook.d.ts +99 -0
- package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -0
- package/dist/apme/adapters/openclaw-hook.js +234 -0
- package/dist/apme/adapters/openclaw-hook.js.map +1 -0
- package/dist/apme/adapters/opencode-hook.d.ts +56 -0
- package/dist/apme/adapters/opencode-hook.d.ts.map +1 -0
- package/dist/apme/adapters/opencode-hook.js +182 -0
- package/dist/apme/adapters/opencode-hook.js.map +1 -0
- package/dist/apme/adapters/timeline.d.ts.map +1 -1
- package/dist/apme/adapters/timeline.js +6 -1
- package/dist/apme/adapters/timeline.js.map +1 -1
- package/dist/apme/antigravity-transcript.d.ts +18 -0
- package/dist/apme/antigravity-transcript.d.ts.map +1 -0
- package/dist/apme/antigravity-transcript.js +94 -0
- package/dist/apme/antigravity-transcript.js.map +1 -0
- package/dist/apme/classify-turn.d.ts +13 -0
- package/dist/apme/classify-turn.d.ts.map +1 -0
- package/dist/apme/classify-turn.js +49 -0
- package/dist/apme/classify-turn.js.map +1 -0
- package/dist/apme/claude-transcript-reader.d.ts +10 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
- package/dist/apme/claude-transcript-reader.js +35 -0
- package/dist/apme/claude-transcript-reader.js.map +1 -1
- package/dist/apme/collector.d.ts +129 -6
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +470 -23
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +87 -11
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +43 -6
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/index.d.ts +26 -5
- package/dist/apme/index.d.ts.map +1 -1
- package/dist/apme/index.js +135 -9
- package/dist/apme/index.js.map +1 -1
- package/dist/apme/pareto.d.ts +39 -0
- package/dist/apme/pareto.d.ts.map +1 -0
- package/dist/apme/pareto.js +59 -0
- package/dist/apme/pareto.js.map +1 -0
- package/dist/apme/recommend.d.ts +3 -0
- package/dist/apme/recommend.d.ts.map +1 -1
- package/dist/apme/recommend.js +43 -1
- package/dist/apme/recommend.js.map +1 -1
- package/dist/apme/runner.d.ts +92 -4
- package/dist/apme/runner.d.ts.map +1 -1
- package/dist/apme/runner.js +586 -70
- package/dist/apme/runner.js.map +1 -1
- package/dist/apme/sample-to-timeline.d.ts +26 -0
- package/dist/apme/sample-to-timeline.d.ts.map +1 -0
- package/dist/apme/sample-to-timeline.js +95 -0
- package/dist/apme/sample-to-timeline.js.map +1 -0
- package/dist/apme/scorers/index.d.ts +47 -0
- package/dist/apme/scorers/index.d.ts.map +1 -0
- package/dist/apme/scorers/index.js +96 -0
- package/dist/apme/scorers/index.js.map +1 -0
- package/dist/apme/settings.d.ts +14 -10
- package/dist/apme/settings.d.ts.map +1 -1
- package/dist/apme/settings.js +34 -8
- package/dist/apme/settings.js.map +1 -1
- package/dist/apme/store.d.ts +33 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +334 -25
- package/dist/apme/store.js.map +1 -1
- package/dist/awaiting-overlay.d.ts +99 -0
- package/dist/awaiting-overlay.d.ts.map +1 -0
- package/dist/awaiting-overlay.js +148 -0
- package/dist/awaiting-overlay.js.map +1 -0
- package/dist/ble-sync-spawn.d.ts +63 -0
- package/dist/ble-sync-spawn.d.ts.map +1 -0
- package/dist/ble-sync-spawn.js +196 -0
- package/dist/ble-sync-spawn.js.map +1 -0
- package/dist/bridge-core.d.ts +24 -1
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +157 -24
- package/dist/bridge-core.js.map +1 -1
- package/dist/broadcast.d.ts +3 -0
- package/dist/broadcast.d.ts.map +1 -0
- package/dist/broadcast.js +122 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/check-deps.d.ts.map +1 -1
- package/dist/check-deps.js +25 -9
- package/dist/check-deps.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1025 -94
- package/dist/cli.js.map +1 -1
- package/dist/codex-auth.d.ts +21 -0
- package/dist/codex-auth.d.ts.map +1 -1
- package/dist/codex-auth.js +52 -1
- package/dist/codex-auth.js.map +1 -1
- package/dist/codex-output-parser.d.ts +8 -0
- package/dist/codex-output-parser.d.ts.map +1 -1
- package/dist/codex-output-parser.js +110 -25
- package/dist/codex-output-parser.js.map +1 -1
- package/dist/codex-rate-limits.d.ts +16 -0
- package/dist/codex-rate-limits.d.ts.map +1 -0
- package/dist/codex-rate-limits.js +197 -0
- package/dist/codex-rate-limits.js.map +1 -0
- package/dist/codex-rollout-response.d.ts +31 -0
- package/dist/codex-rollout-response.d.ts.map +1 -0
- package/dist/codex-rollout-response.js +135 -0
- package/dist/codex-rollout-response.js.map +1 -0
- package/dist/daemon-server.d.ts +85 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +1627 -159
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +15 -6
- package/dist/daemon.js.map +1 -1
- package/dist/display-dim.d.ts +9 -0
- package/dist/display-dim.d.ts.map +1 -0
- package/dist/display-dim.js +53 -0
- package/dist/display-dim.js.map +1 -0
- package/dist/display-monitor.d.ts +15 -2
- package/dist/display-monitor.d.ts.map +1 -1
- package/dist/display-monitor.js +104 -26
- package/dist/display-monitor.js.map +1 -1
- package/dist/esp32-serial.d.ts +160 -1
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +950 -153
- package/dist/esp32-serial.js.map +1 -1
- package/dist/fallback-task-timeline.d.ts +42 -0
- package/dist/fallback-task-timeline.d.ts.map +1 -0
- package/dist/fallback-task-timeline.js +112 -0
- package/dist/fallback-task-timeline.js.map +1 -0
- package/dist/foundation-models-helper.d.ts +11 -0
- package/dist/foundation-models-helper.d.ts.map +1 -0
- package/dist/foundation-models-helper.js +241 -0
- package/dist/foundation-models-helper.js.map +1 -0
- package/dist/hook-migration.js +1 -1
- package/dist/hook-migration.js.map +1 -1
- package/dist/hook-server.d.ts +2 -3
- package/dist/hook-server.d.ts.map +1 -1
- package/dist/hook-server.js +22 -10
- package/dist/hook-server.js.map +1 -1
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +33 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.js +164 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-discover.d.ts +18 -0
- package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-discover.js +85 -0
- package/dist/idotmatrix/idotmatrix-discover.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-settings.d.ts +20 -0
- package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-settings.js +57 -0
- package/dist/idotmatrix/idotmatrix-settings.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +215 -179
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +15 -16
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +18 -96
- package/dist/log-stream.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +12 -3
- package/dist/logger.js.map +1 -1
- package/dist/mdns.d.ts +17 -0
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +52 -18
- package/dist/mdns.js.map +1 -1
- package/dist/modules/adb-module.d.ts +1 -1
- package/dist/modules/adb-module.d.ts.map +1 -1
- package/dist/modules/adb-module.js +4 -3
- package/dist/modules/adb-module.js.map +1 -1
- package/dist/modules/broadcast-module.d.ts +17 -0
- package/dist/modules/broadcast-module.d.ts.map +1 -0
- package/dist/modules/broadcast-module.js +26 -0
- package/dist/modules/broadcast-module.js.map +1 -0
- package/dist/modules/idotmatrix-module.d.ts +9 -0
- package/dist/modules/idotmatrix-module.d.ts.map +1 -0
- package/dist/modules/idotmatrix-module.js +46 -0
- package/dist/modules/idotmatrix-module.js.map +1 -0
- package/dist/modules/index.d.ts +3 -1
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +9 -3
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/pixoo-module.d.ts.map +1 -1
- package/dist/modules/pixoo-module.js +14 -4
- package/dist/modules/pixoo-module.js.map +1 -1
- package/dist/modules/serial-module.d.ts +4 -0
- package/dist/modules/serial-module.d.ts.map +1 -1
- package/dist/modules/serial-module.js +14 -12
- package/dist/modules/serial-module.js.map +1 -1
- package/dist/modules/timebox-module.d.ts +9 -0
- package/dist/modules/timebox-module.d.ts.map +1 -0
- package/dist/modules/timebox-module.js +46 -0
- package/dist/modules/timebox-module.js.map +1 -0
- package/dist/modules/types.d.ts +4 -2
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/openclaw-session.d.ts +18 -0
- package/dist/openclaw-session.d.ts.map +1 -0
- package/dist/openclaw-session.js +34 -0
- package/dist/openclaw-session.js.map +1 -0
- package/dist/output-parser.d.ts +30 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +125 -9
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +26 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +258 -49
- package/dist/passive-observer.js.map +1 -1
- package/dist/permission-resolver.d.ts +47 -0
- package/dist/permission-resolver.d.ts.map +1 -0
- package/dist/permission-resolver.js +103 -0
- package/dist/permission-resolver.js.map +1 -0
- package/dist/pixoo/micro-glyphs.d.ts +49 -0
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -0
- package/dist/pixoo/micro-glyphs.js +276 -0
- package/dist/pixoo/micro-glyphs.js.map +1 -0
- package/dist/pixoo/pixoo-bridge.d.ts +2 -2
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
- package/dist/pixoo/pixoo-bridge.js +93 -25
- package/dist/pixoo/pixoo-bridge.js.map +1 -1
- package/dist/pixoo/pixoo-camera.d.ts +19 -3
- package/dist/pixoo/pixoo-camera.d.ts.map +1 -1
- package/dist/pixoo/pixoo-camera.js +62 -27
- package/dist/pixoo/pixoo-camera.js.map +1 -1
- package/dist/pixoo/pixoo-client.d.ts +13 -3
- package/dist/pixoo/pixoo-client.d.ts.map +1 -1
- package/dist/pixoo/pixoo-client.js +51 -22
- package/dist/pixoo/pixoo-client.js.map +1 -1
- package/dist/pixoo/pixoo-discover.d.ts +37 -0
- package/dist/pixoo/pixoo-discover.d.ts.map +1 -0
- package/dist/pixoo/pixoo-discover.js +105 -0
- package/dist/pixoo/pixoo-discover.js.map +1 -0
- package/dist/pixoo/pixoo-renderer.d.ts +8 -3
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +189 -60
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pixoo/pixoo-settings.d.ts +6 -0
- package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
- package/dist/pixoo/pixoo-settings.js +9 -0
- package/dist/pixoo/pixoo-settings.js.map +1 -1
- package/dist/pixoo/pixoo-sprites.d.ts +60 -11
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
- package/dist/pixoo/pixoo-sprites.js +512 -209
- package/dist/pixoo/pixoo-sprites.js.map +1 -1
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +8 -2
- package/dist/pty-manager.js.map +1 -1
- package/dist/session-activity.d.ts +14 -0
- package/dist/session-activity.d.ts.map +1 -0
- package/dist/session-activity.js +160 -0
- package/dist/session-activity.js.map +1 -0
- package/dist/session-aggregator.d.ts +17 -0
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js +48 -9
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-registry.d.ts +71 -3
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +137 -8
- package/dist/session-registry.js.map +1 -1
- package/dist/session-timeline-relay.d.ts.map +1 -1
- package/dist/session-timeline-relay.js +13 -4
- package/dist/session-timeline-relay.js.map +1 -1
- package/dist/session-transcript-timeline.d.ts +49 -0
- package/dist/session-transcript-timeline.d.ts.map +1 -0
- package/dist/session-transcript-timeline.js +276 -0
- package/dist/session-transcript-timeline.js.map +1 -0
- package/dist/state-machine.d.ts +6 -1
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +86 -28
- package/dist/state-machine.js.map +1 -1
- package/dist/timebox/timebox-daemon-sync.d.ts +19 -0
- package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -0
- package/dist/timebox/timebox-daemon-sync.js +135 -0
- package/dist/timebox/timebox-daemon-sync.js.map +1 -0
- package/dist/timebox/timebox-discover.d.ts +19 -0
- package/dist/timebox/timebox-discover.d.ts.map +1 -0
- package/dist/timebox/timebox-discover.js +87 -0
- package/dist/timebox/timebox-discover.js.map +1 -0
- package/dist/timebox/timebox-settings.d.ts +32 -0
- package/dist/timebox/timebox-settings.d.ts.map +1 -0
- package/dist/timebox/timebox-settings.js +80 -0
- package/dist/timebox/timebox-settings.js.map +1 -0
- package/dist/timeline-store.d.ts +63 -3
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +292 -14
- package/dist/timeline-store.js.map +1 -1
- package/dist/timeline-summarizer.d.ts.map +1 -1
- package/dist/timeline-summarizer.js +29 -5
- package/dist/timeline-summarizer.js.map +1 -1
- package/dist/tui/ansi.d.ts +0 -4
- package/dist/tui/ansi.d.ts.map +1 -1
- package/dist/tui/ansi.js +14 -7
- package/dist/tui/ansi.js.map +1 -1
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/tui/dashboard.d.ts.map +1 -1
- package/dist/tui/dashboard.js +24 -7
- package/dist/tui/dashboard.js.map +1 -1
- package/dist/tui/renderer.d.ts +36 -0
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +303 -119
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/terrarium.d.ts.map +1 -1
- package/dist/tui/terrarium.js +37 -49
- package/dist/tui/terrarium.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/dist/usage-api.d.ts.map +1 -1
- package/dist/usage-api.js +7 -1
- package/dist/usage-api.js.map +1 -1
- package/dist/usage-event.d.ts +10 -3
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +74 -12
- package/dist/usage-event.js.map +1 -1
- package/dist/utility-proxy.d.ts.map +1 -1
- package/dist/utility-proxy.js +8 -0
- package/dist/utility-proxy.js.map +1 -1
- package/dist/utils/project-name.d.ts +3 -0
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +44 -2
- package/dist/utils/project-name.js.map +1 -1
- package/dist/windows-service.d.ts +26 -0
- package/dist/windows-service.d.ts.map +1 -0
- package/dist/windows-service.js +145 -0
- package/dist/windows-service.js.map +1 -0
- package/dist/ws-server.d.ts +22 -1
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +89 -4
- package/dist/ws-server.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +112 -0
- package/package.json +9 -3
- package/scripts/build-fm-helper.mjs +26 -0
- package/LICENSE +0 -21
- package/dist/apme/tuner.d.ts +0 -64
- package/dist/apme/tuner.d.ts.map +0 -1
- package/dist/apme/tuner.js +0 -297
- package/dist/apme/tuner.js.map +0 -1
- package/dist/d200h/hid-protocol.d.ts +0 -62
- package/dist/d200h/hid-protocol.d.ts.map +0 -1
- package/dist/d200h/hid-protocol.js +0 -124
- package/dist/d200h/hid-protocol.js.map +0 -1
- package/dist/d200h/image-renderer.d.ts +0 -36
- package/dist/d200h/image-renderer.d.ts.map +0 -1
- package/dist/d200h/image-renderer.js +0 -487
- package/dist/d200h/image-renderer.js.map +0 -1
- package/dist/modules/d200h-module.d.ts +0 -43
- package/dist/modules/d200h-module.d.ts.map +0 -1
- package/dist/modules/d200h-module.js +0 -360
- package/dist/modules/d200h-module.js.map +0 -1
- package/dist/modules/d200h-renderer.d.ts +0 -7
- package/dist/modules/d200h-renderer.d.ts.map +0 -1
- package/dist/modules/d200h-renderer.js +0 -321
- package/dist/modules/d200h-renderer.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,63 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
3
|
+
import { writeFileSync, unlinkSync, existsSync, realpathSync } from 'fs';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
-
import { join } from 'path';
|
|
6
|
-
import { execSync, spawn } from 'child_process';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { execFileSync, execSync, spawn } from 'child_process';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import { request } from 'http';
|
|
8
10
|
import { BRIDGE_WS_PORT } from './types.js';
|
|
11
|
+
import { TASK_NAME, installWindowsTask, taskExists, runWindowsTask, endWindowsTask, deleteWindowsTask, } from './windows-service.js';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const packageJson = require('../package.json');
|
|
9
14
|
function log(msg) {
|
|
10
15
|
process.stderr.write(msg + '\n');
|
|
11
16
|
}
|
|
17
|
+
function formatBytes(value) {
|
|
18
|
+
const bytes = typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
19
|
+
if (bytes >= 1024 * 1024)
|
|
20
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
21
|
+
if (bytes >= 1024)
|
|
22
|
+
return `${Math.round(bytes / 1024)}KB`;
|
|
23
|
+
return `${bytes}B`;
|
|
24
|
+
}
|
|
25
|
+
function postJsonWithTimeout(urlString, body, timeoutMs) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const payload = JSON.stringify(body);
|
|
28
|
+
const url = new URL(urlString);
|
|
29
|
+
const req = request({
|
|
30
|
+
hostname: url.hostname,
|
|
31
|
+
port: url.port,
|
|
32
|
+
path: `${url.pathname}${url.search}`,
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
37
|
+
},
|
|
38
|
+
}, (res) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
res.on('data', (chunk) => {
|
|
41
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
42
|
+
});
|
|
43
|
+
res.on('end', () => {
|
|
44
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
45
|
+
try {
|
|
46
|
+
resolve({ statusCode: res.statusCode ?? 0, body: JSON.parse(text) });
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
reject(new Error(`Invalid JSON response: ${err instanceof Error ? err.message : String(err)}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
req.setTimeout(timeoutMs, () => {
|
|
54
|
+
req.destroy(new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
55
|
+
});
|
|
56
|
+
req.on('error', reject);
|
|
57
|
+
req.write(payload);
|
|
58
|
+
req.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
12
61
|
// ===== LaunchAgent plist =====
|
|
13
62
|
const PLIST_LABEL = 'dev.agentdeck.daemon';
|
|
14
63
|
const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
|
|
@@ -62,19 +111,91 @@ async function stopDaemon(port) {
|
|
|
62
111
|
const info = readDaemonInfo();
|
|
63
112
|
const targetPort = info?.httpPort ?? info?.port ?? findDaemonPort() ?? port;
|
|
64
113
|
try {
|
|
65
|
-
await fetch(`http://127.0.0.1:${targetPort}/shutdown`, {
|
|
114
|
+
await fetch(`http://127.0.0.1:${targetPort}/shutdown`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
signal: AbortSignal.timeout(2000),
|
|
117
|
+
});
|
|
66
118
|
log('Shutdown signal sent');
|
|
67
119
|
}
|
|
68
120
|
catch {
|
|
69
121
|
log('Daemon is not running');
|
|
70
122
|
}
|
|
71
123
|
}
|
|
124
|
+
async function isDaemonPort(port) {
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
127
|
+
signal: AbortSignal.timeout(1000),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return false;
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
return data.mode === 'daemon' || data.agentType === 'daemon';
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function resolveTimeboxSyncPaths() {
|
|
139
|
+
const distPath = dirname(fileURLToPath(import.meta.url));
|
|
140
|
+
const projectRoot = join(distPath, '..', '..');
|
|
141
|
+
const timeboxDir = join(projectRoot, 'bridge', 'src', 'timebox');
|
|
142
|
+
return {
|
|
143
|
+
python: join(projectRoot, '.venv', 'bin', 'python'),
|
|
144
|
+
bleScript: join(timeboxDir, 'sync_ble.py'),
|
|
145
|
+
scanScript: join(timeboxDir, 'scan_ble.py'),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function projectRootPath() {
|
|
149
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
150
|
+
}
|
|
151
|
+
const ESP32_OTA_BOARDS = [
|
|
152
|
+
{ board: 'inkdeck', env: 'inkdeck', aliases: [] },
|
|
153
|
+
{ board: 'ulanzi_tc001', env: 'led8x32', aliases: ['led8x32'] },
|
|
154
|
+
{ board: 'ttgo_t_display', env: 'ttgo', aliases: ['ttgo'] },
|
|
155
|
+
{ board: 'round_amoled', env: 'amoled', aliases: ['amoled'] },
|
|
156
|
+
{ board: 'ips_35', env: 'ips35', aliases: ['ips35'] },
|
|
157
|
+
{ board: '86box', env: 'box_86', aliases: ['box_86', 'box_40'] },
|
|
158
|
+
{ board: 'ips_10', env: 'ips10', aliases: ['ips10', 'ips_101'] },
|
|
159
|
+
];
|
|
160
|
+
// alias/board/env-key → { env, board }. Built from the SSOT above.
|
|
161
|
+
export const ESP32_OTA_BY_TARGET = {};
|
|
162
|
+
for (const b of ESP32_OTA_BOARDS) {
|
|
163
|
+
for (const key of [b.board, ...b.aliases]) {
|
|
164
|
+
ESP32_OTA_BY_TARGET[key] = { env: b.env, board: b.board };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve an esp32-ota `target` (alias, canonical board string, or an IP) to the
|
|
169
|
+
* canonical `device_info.board` string the daemon matches against. Unknown
|
|
170
|
+
* targets (e.g. a raw IP or one-off board) pass through unchanged so IP-based
|
|
171
|
+
* and future targeting still works. Exported for testing.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveEsp32OtaDaemonTarget(target) {
|
|
174
|
+
return ESP32_OTA_BY_TARGET[target]?.board ?? target;
|
|
175
|
+
}
|
|
176
|
+
function resolveEsp32FirmwarePath(target, envOpt, firmwareOpt) {
|
|
177
|
+
if (firmwareOpt)
|
|
178
|
+
return realpathSync(firmwareOpt);
|
|
179
|
+
const env = envOpt || ESP32_OTA_BY_TARGET[target]?.env || target;
|
|
180
|
+
const path = join(projectRootPath(), 'esp32', '.pio', 'build', env, 'firmware.bin');
|
|
181
|
+
if (!existsSync(path)) {
|
|
182
|
+
throw new Error(`Firmware not found: ${path}. Build it first with: /opt/homebrew/bin/pio run -e ${env}`);
|
|
183
|
+
}
|
|
184
|
+
return path;
|
|
185
|
+
}
|
|
186
|
+
function platformioBin() {
|
|
187
|
+
if (process.env.PLATFORMIO)
|
|
188
|
+
return process.env.PLATFORMIO;
|
|
189
|
+
if (existsSync('/opt/homebrew/bin/pio'))
|
|
190
|
+
return '/opt/homebrew/bin/pio';
|
|
191
|
+
return 'pio';
|
|
192
|
+
}
|
|
72
193
|
// ===== Program =====
|
|
73
194
|
const program = new Command();
|
|
74
195
|
program
|
|
75
196
|
.name('agentdeck')
|
|
76
197
|
.description('AgentDeck — Physical Controller for AI Coding Agents')
|
|
77
|
-
.version(
|
|
198
|
+
.version(packageJson.version);
|
|
78
199
|
// ===== Agent session commands =====
|
|
79
200
|
program
|
|
80
201
|
.command('claude')
|
|
@@ -98,12 +219,12 @@ program
|
|
|
98
219
|
noUpdateCheck: opts.updateCheck === false,
|
|
99
220
|
postit: opts.postit !== false,
|
|
100
221
|
wakeWord: !!opts.wakeWord,
|
|
101
|
-
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false,
|
|
222
|
+
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
|
|
102
223
|
mdns: false, // daemon-only — session bridges never advertise mDNS
|
|
103
224
|
adb: opts.adb !== false ? 'auto' : false,
|
|
104
225
|
serial: false, // daemon-only — session bridges never talk to ESP32
|
|
105
226
|
pixoo: false, // daemon-only — session bridges never talk to Pixoo
|
|
106
|
-
|
|
227
|
+
timebox: false, // daemon-only — session bridges never talk to Timebox
|
|
107
228
|
},
|
|
108
229
|
});
|
|
109
230
|
});
|
|
@@ -116,7 +237,32 @@ program
|
|
|
116
237
|
.option('--local', 'Disable all device modules (WS only)')
|
|
117
238
|
.option('--no-adb', 'Disable ADB reverse setup')
|
|
118
239
|
.option('--no-postit', 'Disable terminal tab title updates')
|
|
240
|
+
.option('--no-codex-hooks', 'Skip ~/.codex/config.toml hook install')
|
|
119
241
|
.action(async (opts) => {
|
|
242
|
+
// Install Codex lifecycle hooks before starting the session so the
|
|
243
|
+
// first prompt's UserPromptSubmit / Stop events reach the daemon.
|
|
244
|
+
// Idempotent: re-running with the same daemon port is a no-op.
|
|
245
|
+
if (opts.codexHooks !== false) {
|
|
246
|
+
try {
|
|
247
|
+
const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
248
|
+
const result = installCodexHooksIfNeeded();
|
|
249
|
+
if (result.installed) {
|
|
250
|
+
log('Codex lifecycle hooks ready in ~/.codex/config.toml');
|
|
251
|
+
if (result.warning)
|
|
252
|
+
log(`Codex hooks degraded: ${result.warning}`);
|
|
253
|
+
}
|
|
254
|
+
else if (result.reason) {
|
|
255
|
+
log(`Codex hooks skipped: ${result.reason}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
log(`Codex hooks unavailable: ${String(err)}`);
|
|
260
|
+
// PTY parser fallback still works without lifecycle hooks.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
log('Codex hooks skipped: --no-codex-hooks');
|
|
265
|
+
}
|
|
120
266
|
const { startSession } = await import('./index.js');
|
|
121
267
|
await startSession({
|
|
122
268
|
agentType: 'codex-cli',
|
|
@@ -124,12 +270,12 @@ program
|
|
|
124
270
|
command: opts.command,
|
|
125
271
|
debug: opts.debug,
|
|
126
272
|
postit: opts.postit !== false,
|
|
127
|
-
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false,
|
|
273
|
+
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
|
|
128
274
|
mdns: false, // daemon-only
|
|
129
275
|
adb: opts.adb !== false ? 'auto' : false,
|
|
130
276
|
serial: false, // daemon-only
|
|
131
277
|
pixoo: false, // daemon-only
|
|
132
|
-
|
|
278
|
+
timebox: false, // daemon-only
|
|
133
279
|
},
|
|
134
280
|
});
|
|
135
281
|
});
|
|
@@ -142,7 +288,31 @@ program
|
|
|
142
288
|
.option('--local', 'Disable all device modules (WS only)')
|
|
143
289
|
.option('--no-adb', 'Disable ADB reverse setup')
|
|
144
290
|
.option('--no-postit', 'Disable terminal tab title updates')
|
|
291
|
+
.option('--no-opencode-hooks', 'Skip OpenCode observer plugin install')
|
|
145
292
|
.action(async (opts) => {
|
|
293
|
+
// Install the OpenCode observer plugin so standalone `opencode` runs
|
|
294
|
+
// (outside this managed session) also reach the daemon timeline. The
|
|
295
|
+
// plugin self-disables inside managed PTYs via AGENTDECK_PORT, so this
|
|
296
|
+
// session is unaffected. Idempotent content-compare write.
|
|
297
|
+
if (opts.opencodeHooks !== false) {
|
|
298
|
+
try {
|
|
299
|
+
const { installOpenCodeHooksIfNeeded, opencodePluginPath } = await import('@agentdeck/hooks');
|
|
300
|
+
const result = installOpenCodeHooksIfNeeded();
|
|
301
|
+
if (result.installed) {
|
|
302
|
+
log(`OpenCode observer plugin ready at ${opencodePluginPath()}`);
|
|
303
|
+
}
|
|
304
|
+
else if (result.reason) {
|
|
305
|
+
log(`OpenCode observer plugin skipped: ${result.reason}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
log(`OpenCode observer plugin unavailable: ${String(err)}`);
|
|
310
|
+
// Managed SSE bridge works without the plugin.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
log('OpenCode observer plugin skipped: --no-opencode-hooks');
|
|
315
|
+
}
|
|
146
316
|
const { startSession } = await import('./index.js');
|
|
147
317
|
await startSession({
|
|
148
318
|
agentType: 'opencode',
|
|
@@ -150,12 +320,12 @@ program
|
|
|
150
320
|
command: opts.command,
|
|
151
321
|
debug: opts.debug,
|
|
152
322
|
postit: opts.postit !== false,
|
|
153
|
-
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false,
|
|
323
|
+
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
|
|
154
324
|
mdns: false,
|
|
155
325
|
adb: opts.adb !== false ? 'auto' : false,
|
|
156
326
|
serial: false,
|
|
157
327
|
pixoo: false,
|
|
158
|
-
|
|
328
|
+
timebox: false,
|
|
159
329
|
},
|
|
160
330
|
});
|
|
161
331
|
});
|
|
@@ -174,9 +344,15 @@ program
|
|
|
174
344
|
agentType: 'monitor',
|
|
175
345
|
port,
|
|
176
346
|
debug: opts.debug,
|
|
177
|
-
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false,
|
|
347
|
+
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : undefined,
|
|
178
348
|
});
|
|
179
349
|
});
|
|
350
|
+
// The worktree-compare feature ("cockpit") was extracted into its own
|
|
351
|
+
// standalone tool, Worktree Cockpit (`wtcp`):
|
|
352
|
+
// https://github.com/puritysb/worktree-cockpit . It no longer ships with
|
|
353
|
+
// AgentDeck. AgentDeck still exposes
|
|
354
|
+
// the on-device Apple Intelligence helper via the daemon `/generate` endpoint,
|
|
355
|
+
// which wtcp can optionally use for branch naming.
|
|
180
356
|
// ===== Daemon commands =====
|
|
181
357
|
const daemon = program.command('daemon').description('Manage monitoring daemon');
|
|
182
358
|
daemon
|
|
@@ -187,20 +363,65 @@ daemon
|
|
|
187
363
|
.option('-f, --foreground', 'Run in foreground (default: background fork)')
|
|
188
364
|
.option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
|
|
189
365
|
.action(async (opts) => {
|
|
190
|
-
const { findExistingDaemon, readDaemonInfo } = await import('./session-registry.js');
|
|
366
|
+
const { findExistingDaemon, probeDaemonHealth, readDaemonInfo, removeDaemonInfo, removeDaemonSession, requestDaemonStandDown, requestDaemonShutdown, waitForDaemonExit } = await import('./session-registry.js');
|
|
367
|
+
// Reverse two-tier upgrade path: the macOS app may already own the canonical
|
|
368
|
+
// port with its in-process Swift daemon (Tier 1 — limited: no ADB devices,
|
|
369
|
+
// no subscription usage, …). A plain "already running → exit" would strand
|
|
370
|
+
// the user on Tier 1. Instead, if the incumbent identifies as a Swift
|
|
371
|
+
// daemon, ask it to STAND DOWN (demote to client, app keeps running) so this
|
|
372
|
+
// CLI daemon can take over with the full feature set, then wait for the port
|
|
373
|
+
// to clear before binding. A real Node daemon on the port still means
|
|
374
|
+
// "already running".
|
|
375
|
+
const targetPort = opts.port ? parseInt(String(opts.port), 10) : BRIDGE_WS_PORT;
|
|
376
|
+
const incumbent = await probeDaemonHealth(targetPort);
|
|
377
|
+
if (incumbent?.mode === 'daemon') {
|
|
378
|
+
if (incumbent.isSwift) {
|
|
379
|
+
log(`AgentDeck app's in-process daemon holds port ${targetPort} — requesting stand-down to take over with the full CLI feature set…`);
|
|
380
|
+
// Prefer /stand-down (clean demote: the app stays running as a client).
|
|
381
|
+
// Fall back to /shutdown for older app builds that predate the endpoint.
|
|
382
|
+
let acked = await requestDaemonStandDown(targetPort);
|
|
383
|
+
if (!acked) {
|
|
384
|
+
await requestDaemonShutdown(targetPort);
|
|
385
|
+
acked = true; // shutdown is best-effort (no ack body); rely on the exit wait
|
|
386
|
+
}
|
|
387
|
+
if (acked && await waitForDaemonExit(targetPort, 12000)) {
|
|
388
|
+
log(`App daemon yielded port ${targetPort}. Starting CLI daemon…`);
|
|
389
|
+
// fall through — port is clear, proceed to bind below
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
log(`The AgentDeck app did not yield port ${targetPort} in time. Quit the app and retry 'agentdeck daemon start', or start on a different port with -p.`);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
log(`Daemon already running on port ${targetPort}. Use 'agentdeck daemon stop' first.`);
|
|
398
|
+
process.exit(0);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
191
401
|
const daemonInfo = readDaemonInfo();
|
|
192
402
|
if (daemonInfo) {
|
|
193
|
-
|
|
194
|
-
|
|
403
|
+
const probePort = daemonInfo.httpPort ?? daemonInfo.port;
|
|
404
|
+
const health = await probeDaemonHealth(probePort);
|
|
405
|
+
if (health?.mode === 'daemon') {
|
|
406
|
+
log(`Daemon already running on port ${daemonInfo.port} (PID ${daemonInfo.pid}). Use 'agentdeck daemon stop' first.`);
|
|
407
|
+
process.exit(0);
|
|
408
|
+
}
|
|
409
|
+
log(`Ignoring stale daemon entry on port ${daemonInfo.port} (PID ${daemonInfo.pid}; /health did not respond).`);
|
|
410
|
+
removeDaemonInfo();
|
|
195
411
|
}
|
|
196
412
|
const existing = findExistingDaemon();
|
|
197
413
|
if (existing) {
|
|
198
|
-
|
|
199
|
-
|
|
414
|
+
const health = await probeDaemonHealth(existing.port);
|
|
415
|
+
if (health?.mode === 'daemon') {
|
|
416
|
+
log(`Daemon already running on port ${existing.port} (PID ${existing.pid}). Use 'agentdeck daemon stop' first.`);
|
|
417
|
+
process.exit(0);
|
|
418
|
+
}
|
|
419
|
+
log(`Ignoring stale daemon session on port ${existing.port} (PID ${existing.pid}; /health did not respond).`);
|
|
420
|
+
removeDaemonSession(existing);
|
|
200
421
|
}
|
|
201
422
|
// Background fork unless --foreground
|
|
202
423
|
if (!opts.foreground) {
|
|
203
|
-
const { openSync } = await import('fs');
|
|
424
|
+
const { openSync, statSync, renameSync } = await import('fs');
|
|
204
425
|
const logDir = join(homedir(), '.agentdeck');
|
|
205
426
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
206
427
|
const args = [scriptPath, 'daemon', 'start', '--foreground'];
|
|
@@ -211,8 +432,22 @@ daemon
|
|
|
211
432
|
if (opts.wakeWord)
|
|
212
433
|
args.push('--wake-word');
|
|
213
434
|
// Use log files instead of 'ignore' — preserves device access (mic, etc.)
|
|
214
|
-
|
|
215
|
-
|
|
435
|
+
// Append (never truncate) so multi-day history survives restarts —
|
|
436
|
+
// overnight device incidents can only be correlated against logs that
|
|
437
|
+
// are still there in the morning. Rotate once past 5MB instead.
|
|
438
|
+
const openDaemonLog = (name) => {
|
|
439
|
+
const path = join(logDir, name);
|
|
440
|
+
try {
|
|
441
|
+
if (statSync(path).size > 5 * 1024 * 1024)
|
|
442
|
+
renameSync(path, `${path}.1`);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
/* first run — no log yet */
|
|
446
|
+
}
|
|
447
|
+
return openSync(path, 'a');
|
|
448
|
+
};
|
|
449
|
+
const out = openDaemonLog('daemon-stdout.log');
|
|
450
|
+
const err = openDaemonLog('daemon-stderr.log');
|
|
216
451
|
const child = spawn(process.execPath, args, {
|
|
217
452
|
detached: true,
|
|
218
453
|
stdio: ['ignore', out, err],
|
|
@@ -264,7 +499,7 @@ daemon
|
|
|
264
499
|
.option('-p, --port <port>', 'Server port', String(BRIDGE_WS_PORT))
|
|
265
500
|
.action(async (opts) => {
|
|
266
501
|
const port = parseInt(opts.port, 10);
|
|
267
|
-
const { listActive, readDaemonInfo } = await import('./session-registry.js');
|
|
502
|
+
const { listActive, readDaemonInfo, removeDaemonInfo, removeDaemonSession } = await import('./session-registry.js');
|
|
268
503
|
const info = readDaemonInfo();
|
|
269
504
|
const sessions = listActive();
|
|
270
505
|
const d = sessions.find(s => s.agentType === 'daemon');
|
|
@@ -275,14 +510,70 @@ daemon
|
|
|
275
510
|
log(`Daemon status (port ${targetPort}): ${JSON.stringify(data, null, 2)}`);
|
|
276
511
|
}
|
|
277
512
|
catch {
|
|
513
|
+
if (info)
|
|
514
|
+
removeDaemonInfo();
|
|
515
|
+
if (d)
|
|
516
|
+
removeDaemonSession(d);
|
|
278
517
|
log('Daemon is not running');
|
|
279
518
|
process.exit(1);
|
|
280
519
|
}
|
|
281
520
|
});
|
|
282
521
|
daemon
|
|
283
522
|
.command('install')
|
|
284
|
-
.description('Install
|
|
285
|
-
.action(() => {
|
|
523
|
+
.description('Install daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows)')
|
|
524
|
+
.action(async () => {
|
|
525
|
+
if (process.platform === 'win32') {
|
|
526
|
+
try {
|
|
527
|
+
installWindowsTask();
|
|
528
|
+
log(`Scheduled task '${TASK_NAME}' registered. Daemon will auto-start on logon.`);
|
|
529
|
+
}
|
|
530
|
+
catch (e) {
|
|
531
|
+
const detail = e.stderr?.toString().trim() || e.message;
|
|
532
|
+
log('Failed to register the AgentDeck scheduled task.');
|
|
533
|
+
if (detail)
|
|
534
|
+
log(detail);
|
|
535
|
+
log('You can still run the daemon manually with: agentdeck daemon start');
|
|
536
|
+
log('(or add a shortcut to shell:startup to autostart it yourself).');
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
// Start it now so the user does not have to log out/in (the singleton
|
|
540
|
+
// guard makes a double-start a safe no-op).
|
|
541
|
+
try {
|
|
542
|
+
runWindowsTask();
|
|
543
|
+
log('Daemon started.');
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
log('Task registered; immediate start failed — it will start on next logon.');
|
|
547
|
+
}
|
|
548
|
+
// Install Codex lifecycle hooks for parity with the macOS install path.
|
|
549
|
+
try {
|
|
550
|
+
const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
551
|
+
const result = installCodexHooksIfNeeded();
|
|
552
|
+
if (result.installed) {
|
|
553
|
+
log('Codex lifecycle hooks installed in ~/.codex/config.toml');
|
|
554
|
+
if (result.warning)
|
|
555
|
+
log(`Codex hooks degraded: ${result.warning}`);
|
|
556
|
+
}
|
|
557
|
+
else if (result.reason) {
|
|
558
|
+
log(`Codex hooks skipped: ${result.reason}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch { /* hooks package not built yet — task install still succeeds */ }
|
|
562
|
+
// OpenCode observer plugin — standalone `opencode` runs report to the
|
|
563
|
+
// daemon timeline the same way direct `codex` runs do.
|
|
564
|
+
try {
|
|
565
|
+
const { installOpenCodeHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
566
|
+
const result = installOpenCodeHooksIfNeeded();
|
|
567
|
+
if (result.installed) {
|
|
568
|
+
log('OpenCode observer plugin installed.');
|
|
569
|
+
}
|
|
570
|
+
else if (result.reason) {
|
|
571
|
+
log(`OpenCode observer plugin skipped: ${result.reason}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch { /* hooks package not built yet — task install still succeeds */ }
|
|
575
|
+
process.exit(0);
|
|
576
|
+
}
|
|
286
577
|
if (process.platform !== 'darwin') {
|
|
287
578
|
log('LaunchAgent is macOS-only');
|
|
288
579
|
process.exit(1);
|
|
@@ -296,11 +587,61 @@ daemon
|
|
|
296
587
|
catch { }
|
|
297
588
|
execSync(`launchctl load "${PLIST_PATH}"`);
|
|
298
589
|
log('LaunchAgent loaded. Daemon will auto-start on login.');
|
|
590
|
+
// Install Codex lifecycle hooks parallel to the LaunchAgent install
|
|
591
|
+
// so the daemon hub gets codex_* events as soon as Codex CLI runs.
|
|
592
|
+
try {
|
|
593
|
+
const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
594
|
+
const result = installCodexHooksIfNeeded();
|
|
595
|
+
if (result.installed) {
|
|
596
|
+
log('Codex lifecycle hooks installed in ~/.codex/config.toml');
|
|
597
|
+
}
|
|
598
|
+
else if (result.reason) {
|
|
599
|
+
log(`Codex hooks skipped: ${result.reason}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch { /* hooks package not built yet — daemon install still succeeds */ }
|
|
603
|
+
// OpenCode observer plugin — standalone `opencode` runs report to the
|
|
604
|
+
// daemon hub as opencode_* events, same pipeline as codex_*.
|
|
605
|
+
try {
|
|
606
|
+
const { installOpenCodeHooksIfNeeded, opencodePluginPath } = await import('@agentdeck/hooks');
|
|
607
|
+
const result = installOpenCodeHooksIfNeeded();
|
|
608
|
+
if (result.installed) {
|
|
609
|
+
log(`OpenCode observer plugin installed at ${opencodePluginPath()}`);
|
|
610
|
+
}
|
|
611
|
+
else if (result.reason) {
|
|
612
|
+
log(`OpenCode observer plugin skipped: ${result.reason}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch { /* hooks package not built yet — daemon install still succeeds */ }
|
|
299
616
|
});
|
|
300
617
|
daemon
|
|
301
618
|
.command('uninstall')
|
|
302
|
-
.description('Remove macOS
|
|
303
|
-
.action(() => {
|
|
619
|
+
.description('Remove daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows)')
|
|
620
|
+
.action(async () => {
|
|
621
|
+
if (process.platform === 'win32') {
|
|
622
|
+
if (!taskExists()) {
|
|
623
|
+
log(`Scheduled task '${TASK_NAME}' is not installed.`);
|
|
624
|
+
process.exit(0);
|
|
625
|
+
}
|
|
626
|
+
// Graceful daemon shutdown first, then stop + delete the task.
|
|
627
|
+
await stopDaemon(BRIDGE_WS_PORT);
|
|
628
|
+
try {
|
|
629
|
+
endWindowsTask();
|
|
630
|
+
}
|
|
631
|
+
catch { /* not running */ }
|
|
632
|
+
try {
|
|
633
|
+
deleteWindowsTask();
|
|
634
|
+
log(`Scheduled task '${TASK_NAME}' removed.`);
|
|
635
|
+
}
|
|
636
|
+
catch (e) {
|
|
637
|
+
const detail = e.stderr?.toString().trim() || e.message;
|
|
638
|
+
log('Failed to remove the scheduled task.');
|
|
639
|
+
if (detail)
|
|
640
|
+
log(detail);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
process.exit(0);
|
|
644
|
+
}
|
|
304
645
|
if (!existsSync(PLIST_PATH)) {
|
|
305
646
|
log('LaunchAgent not installed');
|
|
306
647
|
return;
|
|
@@ -355,8 +696,15 @@ program
|
|
|
355
696
|
}
|
|
356
697
|
else {
|
|
357
698
|
try {
|
|
358
|
-
await
|
|
359
|
-
|
|
699
|
+
const daemonPort = await isDaemonPort(port);
|
|
700
|
+
const url = daemonPort
|
|
701
|
+
? `http://127.0.0.1:${port}/shutdown`
|
|
702
|
+
: `http://127.0.0.1:${port}/hooks/shutdown`;
|
|
703
|
+
await fetch(url, {
|
|
704
|
+
method: 'POST',
|
|
705
|
+
signal: AbortSignal.timeout(2000),
|
|
706
|
+
});
|
|
707
|
+
log(daemonPort ? 'Daemon shutdown signal sent' : 'Shutdown signal sent');
|
|
360
708
|
}
|
|
361
709
|
catch {
|
|
362
710
|
log('Session is not running');
|
|
@@ -366,10 +714,14 @@ program
|
|
|
366
714
|
// ===== Device commands =====
|
|
367
715
|
program
|
|
368
716
|
.command('devices')
|
|
369
|
-
.description('Show connected devices (WebSocket, ESP32, Pixoo, ADB)')
|
|
370
|
-
.option('-p, --port <port>', 'Bridge server port'
|
|
717
|
+
.description('Show connected devices (WebSocket, ESP32, Pixoo, Timebox, ADB)')
|
|
718
|
+
.option('-p, --port <port>', 'Bridge server port')
|
|
371
719
|
.action(async (opts) => {
|
|
372
|
-
const
|
|
720
|
+
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
721
|
+
const info = readDaemonInfo();
|
|
722
|
+
const port = opts.port != null
|
|
723
|
+
? parseInt(opts.port, 10)
|
|
724
|
+
: (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
|
|
373
725
|
try {
|
|
374
726
|
const res = await fetch(`http://127.0.0.1:${port}/devices`, {
|
|
375
727
|
signal: AbortSignal.timeout(2000),
|
|
@@ -383,15 +735,51 @@ program
|
|
|
383
735
|
total += d.count;
|
|
384
736
|
}
|
|
385
737
|
else if ((d.type === 'esp32' || d.type === 'esp32_serial') && (d.count || d.connections)) {
|
|
386
|
-
// Node daemon: { count, ports }, Swift daemon: { connections: [{ port, connected, deviceInfo }] }
|
|
738
|
+
// Node daemon: { count, ports, devices }, Swift daemon: { connections: [{ port, connected, deviceInfo }] }
|
|
387
739
|
const count = d.count ?? d.connections?.filter((c) => c.connected).length ?? 0;
|
|
388
740
|
const ports = d.ports ?? d.connections?.map((c) => c.port) ?? [];
|
|
741
|
+
// Per-device build identity, so a stale flash is distinguishable from the latest.
|
|
742
|
+
const devices = d.devices ?? d.connections?.filter((c) => c.connected).map((c) => ({ port: c.port, ...c.deviceInfo })) ?? [];
|
|
389
743
|
if (count > 0) {
|
|
390
|
-
|
|
391
|
-
|
|
744
|
+
if (devices.length) {
|
|
745
|
+
lines.push(` ESP32 ${count} serial`);
|
|
746
|
+
for (const dev of devices) {
|
|
747
|
+
const ver = dev.version ? ` v${dev.version}` : '';
|
|
748
|
+
const hash = dev.buildHash ? ` (${dev.buildHash})` : '';
|
|
749
|
+
const board = dev.board ?? 'esp32';
|
|
750
|
+
const ota = dev.otaSupported === true
|
|
751
|
+
? ` OTA ${formatBytes(dev.otaSlotSize)}`
|
|
752
|
+
: dev.otaReason ? ` OTA no:${dev.otaReason}` : '';
|
|
753
|
+
lines.push(` ${board}${ver}${hash}${ota} @ ${dev.port}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
const portInfo = ports.length ? ` (${ports.join(', ')})` : '';
|
|
758
|
+
lines.push(` ESP32 ${count} serial${portInfo}`);
|
|
759
|
+
}
|
|
392
760
|
total += count;
|
|
393
761
|
}
|
|
394
762
|
}
|
|
763
|
+
else if (d.type === 'esp32-wifi') {
|
|
764
|
+
const devices = (d.devices ?? []);
|
|
765
|
+
if (devices.length) {
|
|
766
|
+
lines.push(` ESP32 WiFi ${devices.length} client${devices.length !== 1 ? 's' : ''}`);
|
|
767
|
+
for (const dev of devices) {
|
|
768
|
+
const ver = dev.version ? ` v${dev.version}` : '';
|
|
769
|
+
const hash = dev.buildHash ? ` (${dev.buildHash})` : '';
|
|
770
|
+
const board = dev.board ?? 'esp32';
|
|
771
|
+
const ota = dev.otaSupported === true
|
|
772
|
+
? ` OTA ${formatBytes(dev.otaSlotSize)}`
|
|
773
|
+
: dev.otaReason ? ` OTA no:${dev.otaReason}` : '';
|
|
774
|
+
const stale = dev.stale ? ' stale' : '';
|
|
775
|
+
// Single-path: a board also live on USB serial is driven over
|
|
776
|
+
// serial; its WiFi link is a hot standby (no duplicate traffic).
|
|
777
|
+
const transport = dev.serialActive ? ' [serial-active · wifi standby]' : '';
|
|
778
|
+
lines.push(` ${board}${ver}${hash}${ota}${stale}${transport} @ ${dev.ip ?? 'wifi'}`);
|
|
779
|
+
}
|
|
780
|
+
total += devices.length;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
395
783
|
else if (d.type === 'pixoo') {
|
|
396
784
|
// Node daemon: { details: [...] }, Swift daemon: { deviceIps: [...] }
|
|
397
785
|
if (d.details) {
|
|
@@ -415,6 +803,43 @@ program
|
|
|
415
803
|
}
|
|
416
804
|
}
|
|
417
805
|
}
|
|
806
|
+
else if (d.type === 'timebox') {
|
|
807
|
+
// Node daemon: { devices: [{address, name, brightness}] }, Swift
|
|
808
|
+
// daemon: { configuredDeviceCount, connected, deviceName, statusReason }.
|
|
809
|
+
if (Array.isArray(d.devices)) {
|
|
810
|
+
for (const tb of d.devices) {
|
|
811
|
+
const brightnessInfo = tb.brightness !== undefined ? ` brightness=${tb.brightness}%` : '';
|
|
812
|
+
lines.push(` Timebox ${tb.address} (${tb.name || 'Timebox Mini Light'})${brightnessInfo}`);
|
|
813
|
+
total++;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
else if ((d.configuredDeviceCount ?? 0) > 0) {
|
|
817
|
+
const status = d.connected ? '✓' : `⚠ ${d.statusReason ?? 'not connected'}`;
|
|
818
|
+
lines.push(` Timebox ${d.deviceName ?? 'Timebox Mini'} ${status}`);
|
|
819
|
+
total++;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
else if (d.type === 'idotmatrix') {
|
|
823
|
+
// Node daemon: { devices: [{address, name}] }, Swift daemon:
|
|
824
|
+
// { configuredDeviceCount, connected, deviceName, statusReason }.
|
|
825
|
+
if (Array.isArray(d.devices)) {
|
|
826
|
+
for (const dm of d.devices) {
|
|
827
|
+
lines.push(` iDotMatrix ${dm.address} (${dm.name || 'iDotMatrix'})`);
|
|
828
|
+
total++;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else if ((d.configuredDeviceCount ?? 0) > 0) {
|
|
832
|
+
const status = d.connected ? '✓' : `⚠ ${d.statusReason ?? 'not connected'}`;
|
|
833
|
+
lines.push(` iDotMatrix ${d.deviceName ?? 'iDotMatrix'} ${status}`);
|
|
834
|
+
total++;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else if (d.type === 'tui') {
|
|
838
|
+
for (const t of (d.devices ?? [])) {
|
|
839
|
+
lines.push(` TUI ${t.name ?? 'terminal'} (agentdeck dashboard)`);
|
|
840
|
+
total++;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
418
843
|
else if (d.type === 'adb') {
|
|
419
844
|
const count = d.count ?? d.devices?.length ?? 0;
|
|
420
845
|
if (count > 0) {
|
|
@@ -423,8 +848,7 @@ program
|
|
|
423
848
|
}
|
|
424
849
|
}
|
|
425
850
|
else if (d.type === 'd200h' && d.connected) {
|
|
426
|
-
|
|
427
|
-
lines.push(` D200H HID connected${writeInfo}`);
|
|
851
|
+
lines.push(` D200H Ulanzi Studio connected (via WebSocket)`);
|
|
428
852
|
total++;
|
|
429
853
|
}
|
|
430
854
|
}
|
|
@@ -439,11 +863,17 @@ program
|
|
|
439
863
|
}
|
|
440
864
|
catch {
|
|
441
865
|
const { loadPixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
866
|
+
const { loadTimeboxDevices } = await import('./timebox/timebox-settings.js');
|
|
442
867
|
const pixoo = loadPixooDevices();
|
|
443
|
-
|
|
444
|
-
|
|
868
|
+
const timebox = loadTimeboxDevices();
|
|
869
|
+
if (pixoo.length > 0 || timebox.length > 0) {
|
|
870
|
+
log('Bridge is not running.\nConfigured devices:');
|
|
445
871
|
for (const d of pixoo) {
|
|
446
|
-
log(` ${d.ip} (${d.name || 'Pixoo64'})`);
|
|
872
|
+
log(` Pixoo64 ${d.ip} (${d.name || 'Pixoo64'})`);
|
|
873
|
+
}
|
|
874
|
+
for (const d of timebox) {
|
|
875
|
+
const brightnessInfo = d.brightness !== undefined ? ` brightness=${d.brightness}%` : '';
|
|
876
|
+
log(` Timebox ${d.address} (${d.name || 'Timebox Mini Light'})${brightnessInfo}`);
|
|
447
877
|
}
|
|
448
878
|
}
|
|
449
879
|
else {
|
|
@@ -451,6 +881,39 @@ program
|
|
|
451
881
|
}
|
|
452
882
|
}
|
|
453
883
|
});
|
|
884
|
+
program
|
|
885
|
+
.command('esp32-ota <target>')
|
|
886
|
+
.description('Upload built ESP32 firmware to a WiFi-connected AgentDeck ESP32')
|
|
887
|
+
.option('-p, --port <port>', 'Daemon port')
|
|
888
|
+
.option('-e, --env <env>', 'PlatformIO environment for default firmware path')
|
|
889
|
+
.option('-f, --firmware <path>', 'Firmware .bin path')
|
|
890
|
+
.option('--build', 'Build the PlatformIO environment before upload')
|
|
891
|
+
.action(async (target, opts) => {
|
|
892
|
+
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
893
|
+
const info = readDaemonInfo();
|
|
894
|
+
const port = opts.port != null
|
|
895
|
+
? parseInt(opts.port, 10)
|
|
896
|
+
: (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
|
|
897
|
+
const env = opts.env || ESP32_OTA_BY_TARGET[target]?.env || target;
|
|
898
|
+
if (opts.build) {
|
|
899
|
+
const pio = platformioBin();
|
|
900
|
+
log(`Building ${env} with ${pio}...`);
|
|
901
|
+
execFileSync(pio, ['run', '-e', env], {
|
|
902
|
+
cwd: join(projectRootPath(), 'esp32'),
|
|
903
|
+
stdio: 'inherit',
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
// Send the canonical board string, not the raw alias — the daemon matches
|
|
907
|
+
// `target` against device_info.board, so `ttgo` must become `ttgo_t_display`.
|
|
908
|
+
const daemonTarget = resolveEsp32OtaDaemonTarget(target);
|
|
909
|
+
const firmwarePath = resolveEsp32FirmwarePath(target, opts.env, opts.firmware);
|
|
910
|
+
log(`Uploading ${firmwarePath} to ${daemonTarget} via daemon :${port}...`);
|
|
911
|
+
const { statusCode, body } = await postJsonWithTimeout(`http://127.0.0.1:${port}/esp32/ota`, { target: daemonTarget, firmwarePath }, 15 * 60_000);
|
|
912
|
+
if (statusCode < 200 || statusCode >= 300 || body.ok === false) {
|
|
913
|
+
throw new Error(String(body.error ?? `HTTP ${statusCode}`));
|
|
914
|
+
}
|
|
915
|
+
log(`OTA complete: ${body.board ?? target} ${formatBytes(body.bytes)} in ${body.chunks} chunks`);
|
|
916
|
+
});
|
|
454
917
|
program
|
|
455
918
|
.command('qr')
|
|
456
919
|
.description('Show pairing URL and QR code')
|
|
@@ -493,7 +956,9 @@ program
|
|
|
493
956
|
.option('-a, --analyze', 'Run AI analysis on the dump')
|
|
494
957
|
.option('-t, --tail <lines>', 'Number of journal entries', '200')
|
|
495
958
|
.action(async (opts) => {
|
|
496
|
-
const
|
|
959
|
+
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
960
|
+
const info = readDaemonInfo();
|
|
961
|
+
const port = info?.httpPort ?? info?.port ?? findDaemonPort() ?? parseInt(opts.port, 10);
|
|
497
962
|
const tail = parseInt(opts.tail, 10);
|
|
498
963
|
try {
|
|
499
964
|
const res = await fetch(`http://127.0.0.1:${port}/diag?tail=${tail}`);
|
|
@@ -522,6 +987,390 @@ program
|
|
|
522
987
|
process.exit(1);
|
|
523
988
|
}
|
|
524
989
|
});
|
|
990
|
+
// ===== Task control =====
|
|
991
|
+
//
|
|
992
|
+
// User-driven APME task boundary signal. Closes the currently-active task on
|
|
993
|
+
// the daemon's APME collector via POST /task/close — same path the macOS
|
|
994
|
+
// detail-pane "Mark task complete" button uses. Without this command, the
|
|
995
|
+
// only ways to declare task completion are TodoWrite all-completed (Claude /
|
|
996
|
+
// Codex / OpenCode) or /clear (also splits the run); OpenClaw users had no
|
|
997
|
+
// manual gesture at all before idle_gap landed.
|
|
998
|
+
const task = program.command('task').description('Manage APME task boundaries');
|
|
999
|
+
async function postTaskClose(opts) {
|
|
1000
|
+
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
1001
|
+
const info = readDaemonInfo();
|
|
1002
|
+
const port = info?.httpPort ?? info?.port ?? findDaemonPort();
|
|
1003
|
+
if (!port) {
|
|
1004
|
+
log('Daemon not running. Start it with `agentdeck daemon start`.');
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
}
|
|
1007
|
+
const body = JSON.stringify({
|
|
1008
|
+
signal: opts.signal,
|
|
1009
|
+
...(opts.outcome ? { outcome: opts.outcome } : {}),
|
|
1010
|
+
...(opts.sessionId ? { sessionId: opts.sessionId } : {}),
|
|
1011
|
+
});
|
|
1012
|
+
try {
|
|
1013
|
+
const res = await fetch(`http://127.0.0.1:${port}/task/close`, {
|
|
1014
|
+
method: 'POST',
|
|
1015
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1016
|
+
body,
|
|
1017
|
+
});
|
|
1018
|
+
const json = await res.json().catch(() => ({}));
|
|
1019
|
+
if (!res.ok) {
|
|
1020
|
+
log(`Failed (${res.status}): ${JSON.stringify(json)}`);
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
if (json.closed) {
|
|
1024
|
+
log(`Task closed (signal=${opts.signal}${opts.outcome ? `, outcome=${opts.outcome}` : ''})`);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
log('No active task to close.');
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
catch (err) {
|
|
1031
|
+
log(`Request failed: ${String(err)}`);
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
task
|
|
1036
|
+
.command('done')
|
|
1037
|
+
.description('Mark the active task as complete (manual boundary signal)')
|
|
1038
|
+
.option('-o, --outcome <v>', 'Outcome class: success | fail | partial | abandoned', 'success')
|
|
1039
|
+
.option('-s, --session <id>', 'Target session id (defaults to active OpenClaw session)')
|
|
1040
|
+
.action(async (opts) => {
|
|
1041
|
+
await postTaskClose({ signal: 'manual', outcome: opts.outcome, sessionId: opts.session });
|
|
1042
|
+
});
|
|
1043
|
+
task
|
|
1044
|
+
.command('cancel')
|
|
1045
|
+
.description('Mark the active task as abandoned (shorthand for `task done --outcome abandoned`)')
|
|
1046
|
+
.option('-s, --session <id>', 'Target session id (defaults to active OpenClaw session)')
|
|
1047
|
+
.action(async (opts) => {
|
|
1048
|
+
await postTaskClose({ signal: 'manual', outcome: 'abandoned', sessionId: opts.session });
|
|
1049
|
+
});
|
|
1050
|
+
// ===== iDotMatrix commands =====
|
|
1051
|
+
const idotmatrix = program.command('idotmatrix').description('Manage iDotMatrix BLE pixel display devices');
|
|
1052
|
+
idotmatrix
|
|
1053
|
+
.command('scan')
|
|
1054
|
+
.description('Discover iDotMatrix devices via BLE')
|
|
1055
|
+
.action(async () => {
|
|
1056
|
+
const { dirname } = await import('path');
|
|
1057
|
+
const { fileURLToPath } = await import('url');
|
|
1058
|
+
const { spawn } = await import('child_process');
|
|
1059
|
+
const distPath = dirname(fileURLToPath(import.meta.url));
|
|
1060
|
+
const projectRoot = join(distPath, '..', '..');
|
|
1061
|
+
const venvPython = join(projectRoot, '.venv', 'bin', 'python');
|
|
1062
|
+
const scanScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'scan.py');
|
|
1063
|
+
log('Scanning for BLE devices (5 seconds)...');
|
|
1064
|
+
const py = spawn(venvPython, [scanScript]);
|
|
1065
|
+
let stdoutData = '';
|
|
1066
|
+
py.stdout.on('data', (data) => {
|
|
1067
|
+
stdoutData += data.toString();
|
|
1068
|
+
});
|
|
1069
|
+
py.on('close', (code) => {
|
|
1070
|
+
if (code !== 0) {
|
|
1071
|
+
log('Failed to run scan script.');
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
try {
|
|
1075
|
+
const devices = JSON.parse(stdoutData.trim());
|
|
1076
|
+
if (devices.error) {
|
|
1077
|
+
log(`Scan error: ${devices.error}`);
|
|
1078
|
+
process.exit(1);
|
|
1079
|
+
}
|
|
1080
|
+
if (!Array.isArray(devices) || devices.length === 0) {
|
|
1081
|
+
log('No BLE devices found.');
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
log('\nFound devices:');
|
|
1085
|
+
for (const d of devices) {
|
|
1086
|
+
const prefix = d.is_idotmatrix ? '★ [iDotMatrix] ' : ' ';
|
|
1087
|
+
log(`${prefix}${d.name} (${d.address})`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
catch (e) {
|
|
1091
|
+
log(`Failed to parse scan output: ${stdoutData}`);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
});
|
|
1095
|
+
idotmatrix
|
|
1096
|
+
.command('add <address>')
|
|
1097
|
+
.description('Add an iDotMatrix device address')
|
|
1098
|
+
.option('-n, --name <name>', 'Device name', 'iDotMatrix')
|
|
1099
|
+
.option('-b, --brightness <value>', 'Brightness 5-100', '100')
|
|
1100
|
+
.action(async (address, opts) => {
|
|
1101
|
+
const { addIDotMatrixDevice } = await import('./idotmatrix/idotmatrix-settings.js');
|
|
1102
|
+
const brightness = parseInt(opts.brightness, 10);
|
|
1103
|
+
if (addIDotMatrixDevice({ address, name: opts.name, brightness: isNaN(brightness) ? 100 : brightness })) {
|
|
1104
|
+
log(`Added ${opts.name} (${address}) with brightness ${brightness}% to settings.`);
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
log(`Device ${address} already exists.`);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
idotmatrix
|
|
1111
|
+
.command('list')
|
|
1112
|
+
.description('List configured iDotMatrix devices')
|
|
1113
|
+
.action(async () => {
|
|
1114
|
+
const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
|
|
1115
|
+
const devices = loadIDotMatrixDevices();
|
|
1116
|
+
if (devices.length === 0) {
|
|
1117
|
+
log('No iDotMatrix devices configured. Run `agentdeck idotmatrix scan` or `add`.');
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
log(`${devices.length} device(s) configured:`);
|
|
1121
|
+
for (const d of devices) {
|
|
1122
|
+
const brightnessInfo = d.brightness !== undefined ? ` (brightness=${d.brightness}%)` : '';
|
|
1123
|
+
log(` ${d.name || 'iDotMatrix'} (${d.address})${brightnessInfo}`);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
idotmatrix
|
|
1127
|
+
.command('remove <address>')
|
|
1128
|
+
.description('Remove an iDotMatrix device')
|
|
1129
|
+
.action(async (address) => {
|
|
1130
|
+
const { removeIDotMatrixDevice } = await import('./idotmatrix/idotmatrix-settings.js');
|
|
1131
|
+
if (removeIDotMatrixDevice(address)) {
|
|
1132
|
+
log(`Removed device ${address}.`);
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
log(`Device ${address} not found.`);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
idotmatrix
|
|
1139
|
+
.command('brightness <value>')
|
|
1140
|
+
.description('Set brightness of the iDotMatrix device (5-100)')
|
|
1141
|
+
.option('-a, --address <address>', 'BLE Address (defaults to first configured device)')
|
|
1142
|
+
.action(async (valueStr, opts) => {
|
|
1143
|
+
const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
|
|
1144
|
+
const { dirname } = await import('path');
|
|
1145
|
+
const { fileURLToPath } = await import('url');
|
|
1146
|
+
const { spawn } = await import('child_process');
|
|
1147
|
+
const value = parseInt(valueStr, 10);
|
|
1148
|
+
if (isNaN(value) || value < 5 || value > 100) {
|
|
1149
|
+
log('Brightness value must be between 5 and 100.');
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
let targetAddress = opts.address;
|
|
1153
|
+
if (!targetAddress) {
|
|
1154
|
+
const devices = loadIDotMatrixDevices();
|
|
1155
|
+
if (devices.length === 0) {
|
|
1156
|
+
log('No device specified and none configured. Use `agentdeck idotmatrix add <address>`.');
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
targetAddress = devices[0].address;
|
|
1160
|
+
}
|
|
1161
|
+
const distPath = dirname(fileURLToPath(import.meta.url));
|
|
1162
|
+
const projectRoot = join(distPath, '..', '..');
|
|
1163
|
+
const venvPython = join(projectRoot, '.venv', 'bin', 'python');
|
|
1164
|
+
const brightnessScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'brightness.py');
|
|
1165
|
+
log(`Setting brightness of ${targetAddress} to ${value}%...`);
|
|
1166
|
+
const py = spawn(venvPython, [brightnessScript, '-a', targetAddress, '-b', String(value)], {
|
|
1167
|
+
stdio: 'inherit'
|
|
1168
|
+
});
|
|
1169
|
+
py.on('close', (code) => {
|
|
1170
|
+
if (code === 0) {
|
|
1171
|
+
log('Brightness updated successfully.');
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
log(`Failed to update brightness, process exited with code ${code}`);
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
idotmatrix
|
|
1179
|
+
.command('sync [address]')
|
|
1180
|
+
.description('Sync AgentDeck dashboard frames to iDotMatrix BLE device')
|
|
1181
|
+
.option('-p, --port <port>', 'Bridge server port')
|
|
1182
|
+
.option('-b, --brightness <value>', 'Override brightness (5-100)')
|
|
1183
|
+
.option('--boost <value>', 'Software brightness boost factor (default: 1.6)')
|
|
1184
|
+
.action(async (addressOpt, opts) => {
|
|
1185
|
+
const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
|
|
1186
|
+
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
1187
|
+
const { dirname } = await import('path');
|
|
1188
|
+
const { fileURLToPath } = await import('url');
|
|
1189
|
+
const { spawn } = await import('child_process');
|
|
1190
|
+
let targetAddress = addressOpt;
|
|
1191
|
+
let defaultBrightness = 100;
|
|
1192
|
+
const devices = loadIDotMatrixDevices();
|
|
1193
|
+
if (!targetAddress) {
|
|
1194
|
+
if (devices.length === 0) {
|
|
1195
|
+
log('No device specified and none configured. Use `agentdeck idotmatrix add <address>`.');
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
targetAddress = devices[0].address;
|
|
1199
|
+
defaultBrightness = devices[0].brightness ?? 100;
|
|
1200
|
+
log(`Using first configured device: ${targetAddress}`);
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
const matched = devices.find(d => d.address.toLowerCase() === targetAddress.toLowerCase());
|
|
1204
|
+
if (matched) {
|
|
1205
|
+
defaultBrightness = matched.brightness ?? 100;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const brightness = opts.brightness ? parseInt(opts.brightness, 10) : defaultBrightness;
|
|
1209
|
+
const boost = opts.boost || '1.6';
|
|
1210
|
+
const info = readDaemonInfo();
|
|
1211
|
+
const port = opts.port != null
|
|
1212
|
+
? parseInt(opts.port, 10)
|
|
1213
|
+
: (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
|
|
1214
|
+
const url = `http://127.0.0.1:${port}`;
|
|
1215
|
+
const distPath = dirname(fileURLToPath(import.meta.url));
|
|
1216
|
+
const projectRoot = join(distPath, '..', '..');
|
|
1217
|
+
const venvPython = join(projectRoot, '.venv', 'bin', 'python');
|
|
1218
|
+
const syncScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'sync.py');
|
|
1219
|
+
log(`Starting BLE sync client linking to bridge at ${url} (brightness ${brightness}%, boost ${boost}x)...`);
|
|
1220
|
+
const py = spawn(venvPython, [syncScript, '-a', targetAddress, '-u', url, '-b', String(brightness), '--boost', String(boost)], {
|
|
1221
|
+
stdio: 'inherit'
|
|
1222
|
+
});
|
|
1223
|
+
py.on('close', (code) => {
|
|
1224
|
+
log(`Sync process exited with code ${code}`);
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
// ===== Divoom Timebox Mini Light commands =====
|
|
1228
|
+
const timebox = program.command('timebox').description('Manage Divoom Timebox Mini devices (Bluetooth LE)');
|
|
1229
|
+
timebox
|
|
1230
|
+
.command('scan')
|
|
1231
|
+
.description('Discover BLE TimeBox-mini-light peripherals')
|
|
1232
|
+
.action(async () => {
|
|
1233
|
+
const paths = resolveTimeboxSyncPaths();
|
|
1234
|
+
log('Scanning for BLE devices (5 seconds)...');
|
|
1235
|
+
const py = spawn(paths.python, [paths.scanScript]);
|
|
1236
|
+
let stdoutData = '';
|
|
1237
|
+
py.stdout.on('data', (data) => {
|
|
1238
|
+
stdoutData += data.toString();
|
|
1239
|
+
});
|
|
1240
|
+
py.on('close', (code) => {
|
|
1241
|
+
if (code !== 0) {
|
|
1242
|
+
log('BLE scan failed (is bleak installed in .venv?).');
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
try {
|
|
1246
|
+
const devices = JSON.parse(stdoutData.trim());
|
|
1247
|
+
if (devices.error) {
|
|
1248
|
+
log(`BLE scan error: ${devices.error}`);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (!Array.isArray(devices) || devices.length === 0) {
|
|
1252
|
+
log('No BLE devices found.');
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
log('\nFound BLE devices:');
|
|
1256
|
+
for (const d of devices) {
|
|
1257
|
+
const prefix = d.is_timebox ? '★ [TimeBox-mini-light] ' : ' ';
|
|
1258
|
+
const rssi = typeof d.rssi === 'number' ? ` ${d.rssi}dBm` : '';
|
|
1259
|
+
log(`${prefix}${d.name} (${d.address})${rssi}`);
|
|
1260
|
+
}
|
|
1261
|
+
log('\nAdd the starred BLE device with: agentdeck timebox add <address>');
|
|
1262
|
+
}
|
|
1263
|
+
catch {
|
|
1264
|
+
log(`Failed to parse BLE scan output: ${stdoutData}`);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
timebox
|
|
1269
|
+
.command('add <address>')
|
|
1270
|
+
.description('Add a Timebox device by its BLE address')
|
|
1271
|
+
.option('-n, --name <name>', 'Device name', 'Timebox Mini')
|
|
1272
|
+
.option('-b, --brightness <value>', 'Brightness 0-100', '80')
|
|
1273
|
+
.action(async (address, opts) => {
|
|
1274
|
+
const { addTimeboxDevice } = await import('./timebox/timebox-settings.js');
|
|
1275
|
+
const parsed = parseInt(opts.brightness, 10);
|
|
1276
|
+
const brightness = Number.isFinite(parsed) ? Math.max(0, Math.min(100, parsed)) : 80;
|
|
1277
|
+
if (addTimeboxDevice({ address, name: opts.name, brightness })) {
|
|
1278
|
+
log(`Added ${opts.name} (BLE ${address}) with brightness ${brightness}% to settings.`);
|
|
1279
|
+
log('Restart the daemon to start automatic Timebox sync.');
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
log(`Device ${address} already exists (or has no valid address).`);
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
timebox
|
|
1286
|
+
.command('list')
|
|
1287
|
+
.description('List configured Timebox devices')
|
|
1288
|
+
.action(async () => {
|
|
1289
|
+
const { loadTimeboxDevices, deviceId } = await import('./timebox/timebox-settings.js');
|
|
1290
|
+
const devices = loadTimeboxDevices();
|
|
1291
|
+
if (devices.length === 0) {
|
|
1292
|
+
log('No Timebox devices configured. Run `agentdeck timebox scan`, then `agentdeck timebox add <address>`.');
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
log(`${devices.length} device(s):`);
|
|
1296
|
+
for (const d of devices) {
|
|
1297
|
+
const brightnessInfo = d.brightness !== undefined ? ` brightness=${d.brightness}%` : '';
|
|
1298
|
+
log(` [BLE] ${d.name || 'Timebox Mini'} (${deviceId(d)})${brightnessInfo}`);
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
timebox
|
|
1302
|
+
.command('remove <address>')
|
|
1303
|
+
.description('Remove a Timebox device by BLE address')
|
|
1304
|
+
.action(async (target) => {
|
|
1305
|
+
const { removeTimeboxDevice } = await import('./timebox/timebox-settings.js');
|
|
1306
|
+
if (removeTimeboxDevice(target)) {
|
|
1307
|
+
log(`Removed ${target}.`);
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
log(`Device ${target} not found.`);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
/**
|
|
1314
|
+
* Spawn the BLE sync writer for a one-shot test or a continuous sync,
|
|
1315
|
+
* resolving the target device from CLI args / configured devices.
|
|
1316
|
+
*/
|
|
1317
|
+
async function runTimeboxSync(targetOpt, opts, once) {
|
|
1318
|
+
const { loadTimeboxDevices, deviceId } = await import('./timebox/timebox-settings.js');
|
|
1319
|
+
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
1320
|
+
const devices = loadTimeboxDevices();
|
|
1321
|
+
let device = targetOpt
|
|
1322
|
+
? devices.find((d) => deviceId(d).toLowerCase() === targetOpt.toLowerCase())
|
|
1323
|
+
: devices[0];
|
|
1324
|
+
// Allow an ad-hoc BLE address that isn't in settings.
|
|
1325
|
+
if (!device && targetOpt) {
|
|
1326
|
+
device = { address: targetOpt };
|
|
1327
|
+
}
|
|
1328
|
+
if (!device) {
|
|
1329
|
+
log('No device specified and none configured. Use `agentdeck timebox scan` and `agentdeck timebox add <address>`.');
|
|
1330
|
+
process.exit(1);
|
|
1331
|
+
}
|
|
1332
|
+
const id = deviceId(device);
|
|
1333
|
+
if (!targetOpt)
|
|
1334
|
+
log(`Using first configured device: ${id} (BLE)`);
|
|
1335
|
+
const defaultBrightness = device.brightness ?? 80;
|
|
1336
|
+
const parsedBrightness = opts.brightness ? parseInt(opts.brightness, 10) : defaultBrightness;
|
|
1337
|
+
const brightness = Number.isFinite(parsedBrightness) ? Math.max(0, Math.min(100, parsedBrightness)) : defaultBrightness;
|
|
1338
|
+
const info = readDaemonInfo();
|
|
1339
|
+
const bridgePort = opts.bridgePort != null
|
|
1340
|
+
? parseInt(opts.bridgePort, 10)
|
|
1341
|
+
: (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
|
|
1342
|
+
const url = `http://127.0.0.1:${bridgePort}`;
|
|
1343
|
+
const paths = resolveTimeboxSyncPaths();
|
|
1344
|
+
const args = [paths.bleScript, '--address', id, '--url', url, '--brightness', String(brightness), ...(once ? ['--once'] : [])];
|
|
1345
|
+
log(`${once ? 'Sending one Timebox frame' : 'Starting Timebox sync'} via BLE ${id} from ${url} (brightness ${brightness}%)...`);
|
|
1346
|
+
const py = spawn(paths.python, args, { stdio: 'inherit' });
|
|
1347
|
+
py.on('close', (code) => {
|
|
1348
|
+
if (once) {
|
|
1349
|
+
if (code === 0) {
|
|
1350
|
+
log('Timebox test frame sent.');
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
log(`Timebox test exited with code ${code}`);
|
|
1354
|
+
process.exit(code ?? 1);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
log(`Timebox sync process exited with code ${code}`);
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
timebox
|
|
1363
|
+
.command('test [target]')
|
|
1364
|
+
.description('Send one AgentDeck frame to a Timebox Mini (BLE)')
|
|
1365
|
+
.option('-p, --bridge-port <port>', 'Bridge server port')
|
|
1366
|
+
.option('-b, --brightness <value>', 'Override brightness 0-100')
|
|
1367
|
+
.action((targetOpt, opts) => runTimeboxSync(targetOpt, opts, true));
|
|
1368
|
+
timebox
|
|
1369
|
+
.command('sync [target]')
|
|
1370
|
+
.description('Sync AgentDeck dashboard frames to a Timebox Mini (BLE)')
|
|
1371
|
+
.option('-p, --bridge-port <port>', 'Bridge server port')
|
|
1372
|
+
.option('-b, --brightness <value>', 'Override brightness 0-100')
|
|
1373
|
+
.action((targetOpt, opts) => runTimeboxSync(targetOpt, opts, false));
|
|
525
1374
|
// ===== Pixoo commands =====
|
|
526
1375
|
const pixoo = program.command('pixoo').description('Manage Pixoo64 LED matrix devices');
|
|
527
1376
|
pixoo
|
|
@@ -732,29 +1581,11 @@ program
|
|
|
732
1581
|
const { startDashboard } = await import('./tui/dashboard.js');
|
|
733
1582
|
await startDashboard(opts);
|
|
734
1583
|
});
|
|
735
|
-
//
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
.description('(legacy) Start daemon in foreground — use `agentdeck daemon start`')
|
|
739
|
-
.option('-p, --port <port>', 'Server port', String(BRIDGE_WS_PORT))
|
|
740
|
-
.option('-d, --debug', 'Enable debug logging')
|
|
741
|
-
.action(async (opts) => {
|
|
742
|
-
// Legacy: runs foreground (LaunchAgent plist compatibility)
|
|
743
|
-
const { findExistingDaemon } = await import('./session-registry.js');
|
|
744
|
-
const existing = findExistingDaemon();
|
|
745
|
-
if (existing) {
|
|
746
|
-
log(`Daemon already running on port ${existing.port} (PID ${existing.pid}).`);
|
|
747
|
-
process.exit(0);
|
|
748
|
-
}
|
|
749
|
-
const { startDaemon } = await import('./daemon-server.js');
|
|
750
|
-
await startDaemon({
|
|
751
|
-
port: parseInt(opts.port, 10),
|
|
752
|
-
debug: opts.debug,
|
|
753
|
-
});
|
|
754
|
-
});
|
|
1584
|
+
// Note: there is no top-level `agentdeck start`. The daemon is started via
|
|
1585
|
+
// `agentdeck daemon start` (what the LaunchAgent/install paths already use —
|
|
1586
|
+
// see cli.ts ~368/413).
|
|
755
1587
|
// ===== Default command (no args) =====
|
|
756
|
-
|
|
757
|
-
program.action(async () => {
|
|
1588
|
+
async function showDefaultStatusOrHelp() {
|
|
758
1589
|
const { listActive } = await import('./session-registry.js');
|
|
759
1590
|
const sessions = listActive();
|
|
760
1591
|
if (sessions.length === 0) {
|
|
@@ -768,7 +1599,7 @@ program.action(async () => {
|
|
|
768
1599
|
log(` [${type}] :${s.port} — ${s.projectName} (PID ${s.pid}, ${age}s)`);
|
|
769
1600
|
}
|
|
770
1601
|
log('\nRun `agentdeck --help` for all commands.');
|
|
771
|
-
}
|
|
1602
|
+
}
|
|
772
1603
|
// ===== APME commands =====
|
|
773
1604
|
const apme = program.command('apme').description('Agent Performance Monitoring & Evaluation');
|
|
774
1605
|
apme
|
|
@@ -1039,25 +1870,6 @@ apme
|
|
|
1039
1870
|
await apme.runner.drain();
|
|
1040
1871
|
log(`Done. ${completed} run(s) evaluated.`);
|
|
1041
1872
|
});
|
|
1042
|
-
apme
|
|
1043
|
-
.command('tune')
|
|
1044
|
-
.description('Manually trigger rubric auto-tuning')
|
|
1045
|
-
.action(async () => {
|
|
1046
|
-
const { initApme } = await import('./apme/index.js');
|
|
1047
|
-
const apme = await initApme();
|
|
1048
|
-
if (!apme) {
|
|
1049
|
-
log('APME not available');
|
|
1050
|
-
process.exit(1);
|
|
1051
|
-
}
|
|
1052
|
-
log('Running rubric tuner...');
|
|
1053
|
-
const outcome = await apme.tuner.tune();
|
|
1054
|
-
if (outcome.accepted) {
|
|
1055
|
-
log(`Accepted rubric v${outcome.newVersion} (correlation ${outcome.baselineCorrelation?.toFixed(2) ?? '?'} → ${outcome.proposedCorrelation?.toFixed(2) ?? '?'})`);
|
|
1056
|
-
}
|
|
1057
|
-
else {
|
|
1058
|
-
log(`Rejected: ${outcome.reason}`);
|
|
1059
|
-
}
|
|
1060
|
-
});
|
|
1061
1873
|
apme
|
|
1062
1874
|
.command('vibe <runId> <verdict>')
|
|
1063
1875
|
.description('Submit vibe feedback (approve|reject|neutral)')
|
|
@@ -1095,9 +1907,12 @@ apme
|
|
|
1095
1907
|
});
|
|
1096
1908
|
apme
|
|
1097
1909
|
.command('export')
|
|
1098
|
-
.description('Export runs
|
|
1099
|
-
.option('-n, --limit <n>', '
|
|
1910
|
+
.description('Export runs (default) or tasks as JSONL/CSV — dataset for offline analysis or meta-eval')
|
|
1911
|
+
.option('-n, --limit <n>', 'Maximum rows', '100')
|
|
1100
1912
|
.option('-o, --output <file>', 'Output file (default: stdout)')
|
|
1913
|
+
.option('-b, --by <unit>', 'Row unit: run | task', 'run')
|
|
1914
|
+
.option('-f, --format <fmt>', 'Format: jsonl | csv', 'jsonl')
|
|
1915
|
+
.option('--closed-only', 'Tasks only: skip in-progress tasks (no ended_at)', false)
|
|
1101
1916
|
.action(async (opts) => {
|
|
1102
1917
|
const { initApme } = await import('./apme/index.js');
|
|
1103
1918
|
const { writeFileSync: wfs } = await import('fs');
|
|
@@ -1106,22 +1921,117 @@ apme
|
|
|
1106
1921
|
log('APME not available');
|
|
1107
1922
|
process.exit(1);
|
|
1108
1923
|
}
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1924
|
+
const limit = parseInt(opts.limit, 10) || 100;
|
|
1925
|
+
const by = opts.by === 'task' ? 'task' : 'run';
|
|
1926
|
+
const format = opts.format === 'csv' ? 'csv' : 'jsonl';
|
|
1927
|
+
let rows;
|
|
1928
|
+
if (by === 'task') {
|
|
1929
|
+
// Task-unit dataset: one row per task (group of turns between
|
|
1930
|
+
// boundary signals). This is the dataset format that supports
|
|
1931
|
+
// meaningful agent evaluation per the user requirement —
|
|
1932
|
+
// "의미 있는 단위의 세션을 데이터셋으로 저장".
|
|
1933
|
+
const tasks = apme.store.listAllTasks({ limit, closedOnly: !!opts.closedOnly });
|
|
1934
|
+
rows = tasks.map((t) => {
|
|
1935
|
+
const turns = apme.store.listTurnsForTask(t.id);
|
|
1936
|
+
const evals = apme.store.listEvalsForTask(t.id);
|
|
1937
|
+
const run = apme.store.getRun(t.runId);
|
|
1938
|
+
const vibe = apme.store.latestVibeForRun(t.runId);
|
|
1939
|
+
const axes = {};
|
|
1940
|
+
for (const e of evals) {
|
|
1941
|
+
if (e.layer === 'task_judge')
|
|
1942
|
+
axes[e.metric] = e.score;
|
|
1943
|
+
}
|
|
1944
|
+
const overallEval = evals.find((e) => e.layer === 'task_judge' && e.metric === 'overall');
|
|
1945
|
+
let reasoning = null;
|
|
1946
|
+
let done = null;
|
|
1947
|
+
let missed = null;
|
|
1948
|
+
if (overallEval?.raw) {
|
|
1949
|
+
try {
|
|
1950
|
+
const parsed = JSON.parse(overallEval.raw);
|
|
1951
|
+
reasoning = parsed.reasoning ?? null;
|
|
1952
|
+
done = parsed.done ?? null;
|
|
1953
|
+
missed = parsed.missed ?? null;
|
|
1954
|
+
}
|
|
1955
|
+
catch { /* ignore parse failure */ }
|
|
1956
|
+
}
|
|
1957
|
+
const toolCalls = turns.reduce((n, tu) => n + (tu.tool_calls ?? 0), 0);
|
|
1958
|
+
const filesModified = turns.reduce((n, tu) => n + (tu.files_modified ?? 0), 0);
|
|
1959
|
+
const filesCreated = turns.reduce((n, tu) => n + (tu.files_created ?? 0), 0);
|
|
1960
|
+
const firstPrompt = turns[0]?.prompt ?? null;
|
|
1961
|
+
const lastResponse = turns[turns.length - 1]?.response ?? null;
|
|
1962
|
+
return {
|
|
1963
|
+
taskId: t.id,
|
|
1964
|
+
runId: t.runId,
|
|
1965
|
+
taskIndex: t.taskIndex,
|
|
1966
|
+
agentType: run?.agentType ?? null,
|
|
1967
|
+
modelId: run?.modelId ?? null,
|
|
1968
|
+
projectName: run?.projectName ?? null,
|
|
1969
|
+
taskCategory: t.taskCategory ?? run?.taskCategory ?? null,
|
|
1970
|
+
boundarySignal: t.boundarySignal,
|
|
1971
|
+
startedAt: t.startedAt,
|
|
1972
|
+
endedAt: t.endedAt,
|
|
1973
|
+
durationSec: t.endedAt ? Math.round((t.endedAt - t.startedAt) / 1000) : null,
|
|
1974
|
+
turnCount: turns.length,
|
|
1975
|
+
firstTurnIndex: t.firstTurnIndex,
|
|
1976
|
+
lastTurnIndex: t.lastTurnIndex,
|
|
1977
|
+
toolCalls,
|
|
1978
|
+
filesModified,
|
|
1979
|
+
filesCreated,
|
|
1980
|
+
firstPrompt: firstPrompt ? firstPrompt.slice(0, 500) : null,
|
|
1981
|
+
lastResponse: lastResponse ? lastResponse.slice(0, 500) : null,
|
|
1982
|
+
summary: t.summary ?? null,
|
|
1983
|
+
composite: t.compositeScore ?? null,
|
|
1984
|
+
outcome: t.outcome ?? null,
|
|
1985
|
+
axes,
|
|
1986
|
+
overall: axes.overall ?? null,
|
|
1987
|
+
reasoning,
|
|
1988
|
+
done,
|
|
1989
|
+
missed,
|
|
1990
|
+
vibeVerdict: vibe?.verdict ?? null,
|
|
1991
|
+
vibeNote: vibe?.note ?? null,
|
|
1992
|
+
};
|
|
1993
|
+
});
|
|
1115
1994
|
}
|
|
1995
|
+
else {
|
|
1996
|
+
const runRows = apme.store.listRuns({ limit });
|
|
1997
|
+
rows = runRows.map((r) => {
|
|
1998
|
+
const evals = apme.store.listEvalsForRun(r.id);
|
|
1999
|
+
const vibe = apme.store.latestVibeForRun(r.id);
|
|
2000
|
+
return { ...r, evals, vibe };
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
const output = format === 'csv' ? rowsToCsv(rows) : rowsToJsonl(rows);
|
|
1116
2004
|
if (opts.output) {
|
|
1117
|
-
wfs(opts.output,
|
|
1118
|
-
log(`Exported ${
|
|
2005
|
+
wfs(opts.output, output, 'utf-8');
|
|
2006
|
+
log(`Exported ${rows.length} ${by}(s) to ${opts.output} (${format})`);
|
|
1119
2007
|
}
|
|
1120
2008
|
else {
|
|
1121
|
-
|
|
1122
|
-
process.stdout.write(line + '\n');
|
|
2009
|
+
process.stdout.write(output);
|
|
1123
2010
|
}
|
|
1124
2011
|
});
|
|
2012
|
+
function rowsToJsonl(rows) {
|
|
2013
|
+
return rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : '');
|
|
2014
|
+
}
|
|
2015
|
+
function rowsToCsv(rows) {
|
|
2016
|
+
if (rows.length === 0)
|
|
2017
|
+
return '';
|
|
2018
|
+
const headerSet = new Set();
|
|
2019
|
+
for (const r of rows)
|
|
2020
|
+
for (const k of Object.keys(r))
|
|
2021
|
+
headerSet.add(k);
|
|
2022
|
+
const headers = Array.from(headerSet);
|
|
2023
|
+
const escape = (v) => {
|
|
2024
|
+
if (v === null || v === undefined)
|
|
2025
|
+
return '';
|
|
2026
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
2027
|
+
return /[,"\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
2028
|
+
};
|
|
2029
|
+
const lines = [
|
|
2030
|
+
headers.join(','),
|
|
2031
|
+
...rows.map((r) => headers.map((h) => escape(r[h])).join(',')),
|
|
2032
|
+
];
|
|
2033
|
+
return lines.join('\n') + '\n';
|
|
2034
|
+
}
|
|
1125
2035
|
apme
|
|
1126
2036
|
.command('rubric')
|
|
1127
2037
|
.description('Show current judge rubric')
|
|
@@ -1231,5 +2141,26 @@ apme
|
|
|
1231
2141
|
log(` ${c.taskCategory.padEnd(16)} ${c.modelId.slice(0, 22).padEnd(22)} ${String(c.runs).padEnd(6)} ${score.padEnd(8)} ${tests.padEnd(8)} ${cost}`);
|
|
1232
2142
|
}
|
|
1233
2143
|
});
|
|
1234
|
-
|
|
2144
|
+
function isMainModule() {
|
|
2145
|
+
if (!process.argv[1])
|
|
2146
|
+
return false;
|
|
2147
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
2148
|
+
try {
|
|
2149
|
+
return realpathSync(thisFile) === realpathSync(process.argv[1]);
|
|
2150
|
+
}
|
|
2151
|
+
catch {
|
|
2152
|
+
return thisFile === process.argv[1];
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
export async function runCli(argv = process.argv) {
|
|
2156
|
+
if (argv.slice(2).length === 0) {
|
|
2157
|
+
await showDefaultStatusOrHelp();
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
program.parse(argv);
|
|
2161
|
+
}
|
|
2162
|
+
export { program };
|
|
2163
|
+
if (isMainModule()) {
|
|
2164
|
+
void runCli();
|
|
2165
|
+
}
|
|
1235
2166
|
//# sourceMappingURL=cli.js.map
|