@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/cli.js
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { execSync, spawn } from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { BRIDGE_WS_PORT } from './types.js';
|
|
9
|
+
function log(msg) {
|
|
10
|
+
process.stderr.write(msg + '\n');
|
|
11
|
+
}
|
|
12
|
+
// ===== LaunchAgent plist =====
|
|
13
|
+
const PLIST_LABEL = 'dev.agentdeck.daemon';
|
|
14
|
+
const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
|
|
15
|
+
function getAgentdeckBin() {
|
|
16
|
+
try {
|
|
17
|
+
return execSync('which agentdeck', { encoding: 'utf-8' }).trim();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
const distDir = new URL('.', import.meta.url).pathname;
|
|
21
|
+
return join(distDir, 'cli.js');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function buildPlist() {
|
|
25
|
+
const bin = getAgentdeckBin();
|
|
26
|
+
const logDir = join(homedir(), '.agentdeck');
|
|
27
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
28
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
29
|
+
<plist version="1.0">
|
|
30
|
+
<dict>
|
|
31
|
+
<key>Label</key>
|
|
32
|
+
<string>${PLIST_LABEL}</string>
|
|
33
|
+
<key>ProgramArguments</key>
|
|
34
|
+
<array>
|
|
35
|
+
<string>${bin}</string>
|
|
36
|
+
<string>daemon</string>
|
|
37
|
+
<string>start</string>
|
|
38
|
+
<string>--foreground</string>
|
|
39
|
+
</array>
|
|
40
|
+
<key>RunAtLoad</key>
|
|
41
|
+
<true/>
|
|
42
|
+
<key>KeepAlive</key>
|
|
43
|
+
<dict>
|
|
44
|
+
<key>SuccessfulExit</key>
|
|
45
|
+
<false/>
|
|
46
|
+
</dict>
|
|
47
|
+
<key>StandardOutPath</key>
|
|
48
|
+
<string>${logDir}/daemon-stdout.log</string>
|
|
49
|
+
<key>StandardErrorPath</key>
|
|
50
|
+
<string>${logDir}/daemon-stderr.log</string>
|
|
51
|
+
<key>EnvironmentVariables</key>
|
|
52
|
+
<dict>
|
|
53
|
+
<key>PATH</key>
|
|
54
|
+
<string>${process.env.PATH}</string>
|
|
55
|
+
</dict>
|
|
56
|
+
</dict>
|
|
57
|
+
</plist>`;
|
|
58
|
+
}
|
|
59
|
+
// ===== Helpers =====
|
|
60
|
+
async function stopDaemon(port) {
|
|
61
|
+
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
62
|
+
const info = readDaemonInfo();
|
|
63
|
+
const targetPort = info?.httpPort ?? info?.port ?? findDaemonPort() ?? port;
|
|
64
|
+
try {
|
|
65
|
+
await fetch(`http://127.0.0.1:${targetPort}/shutdown`, { method: 'POST' });
|
|
66
|
+
log('Shutdown signal sent');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
log('Daemon is not running');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ===== Program =====
|
|
73
|
+
const program = new Command();
|
|
74
|
+
program
|
|
75
|
+
.name('agentdeck')
|
|
76
|
+
.description('AgentDeck — Physical Controller for AI Coding Agents')
|
|
77
|
+
.version('0.1.0');
|
|
78
|
+
// ===== Agent session commands =====
|
|
79
|
+
program
|
|
80
|
+
.command('claude')
|
|
81
|
+
.description('Start Claude Code session (PTY + bridge)')
|
|
82
|
+
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
83
|
+
.option('-c, --command <cmd>', 'Command to spawn', 'claude')
|
|
84
|
+
.option('-d, --debug', 'Enable debug logging')
|
|
85
|
+
.option('--no-update-check', 'Skip version check and auto-update')
|
|
86
|
+
.option('--local', 'Disable all device modules (WS only)')
|
|
87
|
+
// --no-mdns removed: session bridges never advertise mDNS (daemon only)
|
|
88
|
+
.option('--no-adb', 'Disable ADB reverse setup')
|
|
89
|
+
.option('--no-postit', 'Disable terminal tab title updates')
|
|
90
|
+
.option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
|
|
91
|
+
.action(async (opts) => {
|
|
92
|
+
const { startSession } = await import('./index.js');
|
|
93
|
+
await startSession({
|
|
94
|
+
agentType: 'claude-code',
|
|
95
|
+
port: parseInt(opts.port, 10),
|
|
96
|
+
command: opts.command,
|
|
97
|
+
debug: opts.debug,
|
|
98
|
+
noUpdateCheck: opts.updateCheck === false,
|
|
99
|
+
postit: opts.postit !== false,
|
|
100
|
+
wakeWord: !!opts.wakeWord,
|
|
101
|
+
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, d200h: false } : {
|
|
102
|
+
mdns: false, // daemon-only — session bridges never advertise mDNS
|
|
103
|
+
adb: opts.adb !== false ? 'auto' : false,
|
|
104
|
+
serial: false, // daemon-only — session bridges never talk to ESP32
|
|
105
|
+
pixoo: false, // daemon-only — session bridges never talk to Pixoo
|
|
106
|
+
d200h: false, // daemon-only — session bridges never talk to D200H
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
program
|
|
111
|
+
.command('codex')
|
|
112
|
+
.description('Start Codex CLI session (PTY + bridge)')
|
|
113
|
+
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
114
|
+
.option('-c, --command <cmd>', 'Command to spawn', 'codex')
|
|
115
|
+
.option('-d, --debug', 'Enable debug logging')
|
|
116
|
+
.option('--local', 'Disable all device modules (WS only)')
|
|
117
|
+
.option('--no-adb', 'Disable ADB reverse setup')
|
|
118
|
+
.option('--no-postit', 'Disable terminal tab title updates')
|
|
119
|
+
.action(async (opts) => {
|
|
120
|
+
const { startSession } = await import('./index.js');
|
|
121
|
+
await startSession({
|
|
122
|
+
agentType: 'codex-cli',
|
|
123
|
+
port: parseInt(opts.port, 10),
|
|
124
|
+
command: opts.command,
|
|
125
|
+
debug: opts.debug,
|
|
126
|
+
postit: opts.postit !== false,
|
|
127
|
+
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, d200h: false } : {
|
|
128
|
+
mdns: false, // daemon-only
|
|
129
|
+
adb: opts.adb !== false ? 'auto' : false,
|
|
130
|
+
serial: false, // daemon-only
|
|
131
|
+
pixoo: false, // daemon-only
|
|
132
|
+
d200h: false, // daemon-only
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
program
|
|
137
|
+
.command('opencode')
|
|
138
|
+
.description('Start OpenCode session (PTY + SSE bridge)')
|
|
139
|
+
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
140
|
+
.option('-c, --command <cmd>', 'Command to spawn', 'opencode')
|
|
141
|
+
.option('-d, --debug', 'Enable debug logging')
|
|
142
|
+
.option('--local', 'Disable all device modules (WS only)')
|
|
143
|
+
.option('--no-adb', 'Disable ADB reverse setup')
|
|
144
|
+
.option('--no-postit', 'Disable terminal tab title updates')
|
|
145
|
+
.action(async (opts) => {
|
|
146
|
+
const { startSession } = await import('./index.js');
|
|
147
|
+
await startSession({
|
|
148
|
+
agentType: 'opencode',
|
|
149
|
+
port: parseInt(opts.port, 10),
|
|
150
|
+
command: opts.command,
|
|
151
|
+
debug: opts.debug,
|
|
152
|
+
postit: opts.postit !== false,
|
|
153
|
+
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, d200h: false } : {
|
|
154
|
+
mdns: false,
|
|
155
|
+
adb: opts.adb !== false ? 'auto' : false,
|
|
156
|
+
serial: false,
|
|
157
|
+
pixoo: false,
|
|
158
|
+
d200h: false,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
program
|
|
163
|
+
.command('monitor')
|
|
164
|
+
.description('Start hook-only bridge (no PTY — run claude separately)')
|
|
165
|
+
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
166
|
+
.option('-d, --debug', 'Enable debug logging')
|
|
167
|
+
.option('--local', 'Disable all device modules')
|
|
168
|
+
.action(async (opts) => {
|
|
169
|
+
const { startSession } = await import('./index.js');
|
|
170
|
+
const port = parseInt(opts.port, 10);
|
|
171
|
+
log(`\nMonitor mode: hook server on port ${port}`);
|
|
172
|
+
log(`Run in another terminal: AGENTDECK_PORT=${port} claude\n`);
|
|
173
|
+
await startSession({
|
|
174
|
+
agentType: 'monitor',
|
|
175
|
+
port,
|
|
176
|
+
debug: opts.debug,
|
|
177
|
+
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, d200h: false } : undefined,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ===== Daemon commands =====
|
|
181
|
+
const daemon = program.command('daemon').description('Manage monitoring daemon');
|
|
182
|
+
daemon
|
|
183
|
+
.command('start')
|
|
184
|
+
.description('Start monitoring daemon (WS + mDNS + Gateway proxy)')
|
|
185
|
+
.option('-p, --port <port>', 'Server port', String(BRIDGE_WS_PORT))
|
|
186
|
+
.option('-d, --debug', 'Enable debug logging')
|
|
187
|
+
.option('-f, --foreground', 'Run in foreground (default: background fork)')
|
|
188
|
+
.option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
|
|
189
|
+
.action(async (opts) => {
|
|
190
|
+
const { findExistingDaemon, readDaemonInfo } = await import('./session-registry.js');
|
|
191
|
+
const daemonInfo = readDaemonInfo();
|
|
192
|
+
if (daemonInfo) {
|
|
193
|
+
log(`Daemon already running on port ${daemonInfo.port} (PID ${daemonInfo.pid}). Use 'agentdeck daemon stop' first.`);
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
const existing = findExistingDaemon();
|
|
197
|
+
if (existing) {
|
|
198
|
+
log(`Daemon already running on port ${existing.port} (PID ${existing.pid}). Use 'agentdeck daemon stop' first.`);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
// Background fork unless --foreground
|
|
202
|
+
if (!opts.foreground) {
|
|
203
|
+
const { openSync } = await import('fs');
|
|
204
|
+
const logDir = join(homedir(), '.agentdeck');
|
|
205
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
206
|
+
const args = [scriptPath, 'daemon', 'start', '--foreground'];
|
|
207
|
+
if (opts.port !== String(BRIDGE_WS_PORT))
|
|
208
|
+
args.push('-p', opts.port);
|
|
209
|
+
if (opts.debug)
|
|
210
|
+
args.push('-d');
|
|
211
|
+
if (opts.wakeWord)
|
|
212
|
+
args.push('--wake-word');
|
|
213
|
+
// Use log files instead of 'ignore' — preserves device access (mic, etc.)
|
|
214
|
+
const out = openSync(join(logDir, 'daemon-stdout.log'), 'w');
|
|
215
|
+
const err = openSync(join(logDir, 'daemon-stderr.log'), 'w');
|
|
216
|
+
const child = spawn(process.execPath, args, {
|
|
217
|
+
detached: true,
|
|
218
|
+
stdio: ['ignore', out, err],
|
|
219
|
+
});
|
|
220
|
+
child.unref();
|
|
221
|
+
log(`Daemon started (PID ${child.pid})`);
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
const { startDaemon } = await import('./daemon-server.js');
|
|
225
|
+
await startDaemon({
|
|
226
|
+
port: parseInt(opts.port, 10),
|
|
227
|
+
debug: opts.debug,
|
|
228
|
+
wakeWord: !!opts.wakeWord,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
daemon
|
|
232
|
+
.command('stop')
|
|
233
|
+
.description('Stop the daemon')
|
|
234
|
+
.option('-p, --port <port>', 'Server port', String(BRIDGE_WS_PORT))
|
|
235
|
+
.action(async (opts) => {
|
|
236
|
+
await stopDaemon(parseInt(opts.port, 10));
|
|
237
|
+
});
|
|
238
|
+
daemon
|
|
239
|
+
.command('restart')
|
|
240
|
+
.description('Stop and restart the daemon')
|
|
241
|
+
.option('-p, --port <port>', 'Server port', String(BRIDGE_WS_PORT))
|
|
242
|
+
.option('-d, --debug', 'Enable debug logging')
|
|
243
|
+
.action(async (opts) => {
|
|
244
|
+
await stopDaemon(parseInt(opts.port, 10));
|
|
245
|
+
// Wait for port release + session cleanup
|
|
246
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
247
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
248
|
+
const args = [scriptPath, 'daemon', 'start', '--foreground'];
|
|
249
|
+
if (opts.port !== String(BRIDGE_WS_PORT))
|
|
250
|
+
args.push('-p', opts.port);
|
|
251
|
+
if (opts.debug)
|
|
252
|
+
args.push('-d');
|
|
253
|
+
const child = spawn(process.execPath, args, {
|
|
254
|
+
detached: true,
|
|
255
|
+
stdio: 'ignore',
|
|
256
|
+
});
|
|
257
|
+
child.unref();
|
|
258
|
+
log(`Daemon restarted (PID ${child.pid})`);
|
|
259
|
+
process.exit(0);
|
|
260
|
+
});
|
|
261
|
+
daemon
|
|
262
|
+
.command('status')
|
|
263
|
+
.description('Show daemon status')
|
|
264
|
+
.option('-p, --port <port>', 'Server port', String(BRIDGE_WS_PORT))
|
|
265
|
+
.action(async (opts) => {
|
|
266
|
+
const port = parseInt(opts.port, 10);
|
|
267
|
+
const { listActive, readDaemonInfo } = await import('./session-registry.js');
|
|
268
|
+
const info = readDaemonInfo();
|
|
269
|
+
const sessions = listActive();
|
|
270
|
+
const d = sessions.find(s => s.agentType === 'daemon');
|
|
271
|
+
const targetPort = info?.httpPort ?? info?.port ?? d?.port ?? port;
|
|
272
|
+
try {
|
|
273
|
+
const res = await fetch(`http://127.0.0.1:${targetPort}/health`);
|
|
274
|
+
const data = await res.json();
|
|
275
|
+
log(`Daemon status (port ${targetPort}): ${JSON.stringify(data, null, 2)}`);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
log('Daemon is not running');
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
daemon
|
|
283
|
+
.command('install')
|
|
284
|
+
.description('Install macOS LaunchAgent for auto-start')
|
|
285
|
+
.action(() => {
|
|
286
|
+
if (process.platform !== 'darwin') {
|
|
287
|
+
log('LaunchAgent is macOS-only');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
const plist = buildPlist();
|
|
291
|
+
writeFileSync(PLIST_PATH, plist, 'utf-8');
|
|
292
|
+
log(`Wrote ${PLIST_PATH}`);
|
|
293
|
+
try {
|
|
294
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`);
|
|
295
|
+
}
|
|
296
|
+
catch { }
|
|
297
|
+
execSync(`launchctl load "${PLIST_PATH}"`);
|
|
298
|
+
log('LaunchAgent loaded. Daemon will auto-start on login.');
|
|
299
|
+
});
|
|
300
|
+
daemon
|
|
301
|
+
.command('uninstall')
|
|
302
|
+
.description('Remove macOS LaunchAgent')
|
|
303
|
+
.action(() => {
|
|
304
|
+
if (!existsSync(PLIST_PATH)) {
|
|
305
|
+
log('LaunchAgent not installed');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`);
|
|
310
|
+
}
|
|
311
|
+
catch { }
|
|
312
|
+
unlinkSync(PLIST_PATH);
|
|
313
|
+
log('LaunchAgent removed.');
|
|
314
|
+
});
|
|
315
|
+
// ===== Session management =====
|
|
316
|
+
program
|
|
317
|
+
.command('status')
|
|
318
|
+
.description('Show all sessions and daemon status')
|
|
319
|
+
.action(async () => {
|
|
320
|
+
const { listActive } = await import('./session-registry.js');
|
|
321
|
+
const sessions = listActive();
|
|
322
|
+
if (sessions.length === 0) {
|
|
323
|
+
log('No active sessions.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
log(`${sessions.length} active session(s):\n`);
|
|
327
|
+
for (const s of sessions) {
|
|
328
|
+
const type = s.agentType ?? 'unknown';
|
|
329
|
+
const age = Math.round((Date.now() - new Date(s.startedAt).getTime()) / 1000);
|
|
330
|
+
log(` [${type}] :${s.port} — ${s.projectName} (PID ${s.pid}, ${age}s)`);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
program
|
|
334
|
+
.command('stop')
|
|
335
|
+
.description('Stop a session or all sessions')
|
|
336
|
+
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
337
|
+
.option('-a, --all', 'Stop all sessions')
|
|
338
|
+
.action(async (opts) => {
|
|
339
|
+
const port = parseInt(opts.port, 10);
|
|
340
|
+
if (opts.all) {
|
|
341
|
+
const { listActive } = await import('./session-registry.js');
|
|
342
|
+
const sessions = listActive();
|
|
343
|
+
for (const s of sessions) {
|
|
344
|
+
try {
|
|
345
|
+
const url = s.agentType === 'daemon'
|
|
346
|
+
? `http://127.0.0.1:${s.port}/shutdown`
|
|
347
|
+
: `http://127.0.0.1:${s.port}/hooks/shutdown`;
|
|
348
|
+
await fetch(url, { method: 'POST' });
|
|
349
|
+
log(`Sent stop to :${s.port} (${s.projectName})`);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
log(`Failed to reach :${s.port}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
try {
|
|
358
|
+
await fetch(`http://127.0.0.1:${port}/hooks/shutdown`, { method: 'POST' });
|
|
359
|
+
log('Shutdown signal sent');
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
log('Session is not running');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
// ===== Device commands =====
|
|
367
|
+
program
|
|
368
|
+
.command('devices')
|
|
369
|
+
.description('Show connected devices (WebSocket, ESP32, Pixoo, ADB)')
|
|
370
|
+
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
371
|
+
.action(async (opts) => {
|
|
372
|
+
const port = parseInt(opts.port, 10);
|
|
373
|
+
try {
|
|
374
|
+
const res = await fetch(`http://127.0.0.1:${port}/devices`, {
|
|
375
|
+
signal: AbortSignal.timeout(2000),
|
|
376
|
+
});
|
|
377
|
+
const data = await res.json();
|
|
378
|
+
const lines = ['Connected devices:'];
|
|
379
|
+
let total = 0;
|
|
380
|
+
for (const d of data.devices) {
|
|
381
|
+
if (d.type === 'websocket' && d.count) {
|
|
382
|
+
lines.push(` WebSocket ${d.count} client${d.count !== 1 ? 's' : ''}`);
|
|
383
|
+
total += d.count;
|
|
384
|
+
}
|
|
385
|
+
else if ((d.type === 'esp32' || d.type === 'esp32_serial') && (d.count || d.connections)) {
|
|
386
|
+
// Node daemon: { count, ports }, Swift daemon: { connections: [{ port, connected, deviceInfo }] }
|
|
387
|
+
const count = d.count ?? d.connections?.filter((c) => c.connected).length ?? 0;
|
|
388
|
+
const ports = d.ports ?? d.connections?.map((c) => c.port) ?? [];
|
|
389
|
+
if (count > 0) {
|
|
390
|
+
const portInfo = ports.length ? ` (${ports.join(', ')})` : '';
|
|
391
|
+
lines.push(` ESP32 ${count} serial${portInfo}`);
|
|
392
|
+
total += count;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else if (d.type === 'pixoo') {
|
|
396
|
+
// Node daemon: { details: [...] }, Swift daemon: { deviceIps: [...] }
|
|
397
|
+
if (d.details) {
|
|
398
|
+
for (const px of d.details) {
|
|
399
|
+
if (px.backedOff) {
|
|
400
|
+
const mins = Math.ceil(px.nextProbeMs / 60_000);
|
|
401
|
+
lines.push(` Pixoo64 ${px.ip} (${px.name}) \u26A0 backed off (next probe ${mins}m)`);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
const ago = px.lastPushAgo >= 0 ? `${Math.round(px.lastPushAgo / 1000)}s ago` : 'no push yet';
|
|
405
|
+
lines.push(` Pixoo64 ${px.ip} (${px.name}) \u2713 ${ago}`);
|
|
406
|
+
}
|
|
407
|
+
total++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if (d.deviceIps) {
|
|
411
|
+
for (const ip of d.deviceIps) {
|
|
412
|
+
const err = d.lastPushError ? ` \u26A0 ${d.lastPushError}` : ' \u2713';
|
|
413
|
+
lines.push(` Pixoo64 ${ip}${err}`);
|
|
414
|
+
total++;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else if (d.type === 'adb') {
|
|
419
|
+
const count = d.count ?? d.devices?.length ?? 0;
|
|
420
|
+
if (count > 0) {
|
|
421
|
+
lines.push(` ADB ${count} USB device${count !== 1 ? 's' : ''}`);
|
|
422
|
+
total += count;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (d.type === 'd200h' && d.connected) {
|
|
426
|
+
const writeInfo = d.writeOK != null ? ` (writes: ${d.writeOK} ok, ${d.writeFail} fail)` : '';
|
|
427
|
+
lines.push(` D200H HID connected${writeInfo}`);
|
|
428
|
+
total++;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (total === 0) {
|
|
432
|
+
log('No devices connected.');
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
lines.push('');
|
|
436
|
+
lines.push(`Total: ${total} device connection${total !== 1 ? 's' : ''}`);
|
|
437
|
+
log(lines.join('\n'));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
const { loadPixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
442
|
+
const pixoo = loadPixooDevices();
|
|
443
|
+
if (pixoo.length > 0) {
|
|
444
|
+
log('Bridge is not running.\nConfigured Pixoo devices:');
|
|
445
|
+
for (const d of pixoo) {
|
|
446
|
+
log(` ${d.ip} (${d.name || 'Pixoo64'})`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
log('Bridge is not running.');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
program
|
|
455
|
+
.command('qr')
|
|
456
|
+
.description('Show pairing URL and QR code')
|
|
457
|
+
.option('-p, --port <port>', 'Bridge server port (auto-detects from running sessions)')
|
|
458
|
+
.action(async (opts) => {
|
|
459
|
+
const { getOrCreateToken, getWsUrl } = await import('./auth.js');
|
|
460
|
+
const { listActive } = await import('./session-registry.js');
|
|
461
|
+
let port;
|
|
462
|
+
if (opts.port) {
|
|
463
|
+
port = parseInt(opts.port, 10);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
const sessions = listActive();
|
|
467
|
+
if (sessions.length > 0) {
|
|
468
|
+
port = sessions[0].port;
|
|
469
|
+
if (sessions.length > 1) {
|
|
470
|
+
log(`Multiple sessions running. Using port ${port} (${sessions[0].projectName}).`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
port = BRIDGE_WS_PORT;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
getOrCreateToken();
|
|
478
|
+
const url = getWsUrl(port);
|
|
479
|
+
log(`\nPairing URL:\n ${url}\n`);
|
|
480
|
+
try {
|
|
481
|
+
const { default: QRCode } = await import('qrcode');
|
|
482
|
+
const text = await QRCode.toString(url, { type: 'terminal', small: true });
|
|
483
|
+
log(text);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// qrcode not available
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
program
|
|
490
|
+
.command('diag')
|
|
491
|
+
.description('Generate diagnostic dump')
|
|
492
|
+
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
493
|
+
.option('-a, --analyze', 'Run AI analysis on the dump')
|
|
494
|
+
.option('-t, --tail <lines>', 'Number of journal entries', '200')
|
|
495
|
+
.action(async (opts) => {
|
|
496
|
+
const port = parseInt(opts.port, 10);
|
|
497
|
+
const tail = parseInt(opts.tail, 10);
|
|
498
|
+
try {
|
|
499
|
+
const res = await fetch(`http://127.0.0.1:${port}/diag?tail=${tail}`);
|
|
500
|
+
if (!res.ok) {
|
|
501
|
+
log(`Diag endpoint error: ${res.status} ${res.statusText}`);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
const dump = await res.json();
|
|
505
|
+
const { saveDiagDump, analyzeDump } = await import('./diag-analyzer.js');
|
|
506
|
+
const dumpPath = saveDiagDump(dump);
|
|
507
|
+
log(`Diagnostic dump saved: ${dumpPath}`);
|
|
508
|
+
if (opts.analyze) {
|
|
509
|
+
log('Running AI analysis...');
|
|
510
|
+
const analysis = await analyzeDump(dumpPath);
|
|
511
|
+
if (analysis) {
|
|
512
|
+
log('\n--- AI Analysis ---\n');
|
|
513
|
+
log(analysis);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
log('AI analysis failed (is `claude` CLI available?)');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
log('Bridge is not running. Cannot generate live diagnostic dump.');
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
// ===== Pixoo commands =====
|
|
526
|
+
const pixoo = program.command('pixoo').description('Manage Pixoo64 LED matrix devices');
|
|
527
|
+
pixoo
|
|
528
|
+
.command('scan')
|
|
529
|
+
.description('Discover Pixoo devices on LAN')
|
|
530
|
+
.action(async () => {
|
|
531
|
+
const { discoverDevices, getDeviceConfig } = await import('./pixoo/pixoo-client.js');
|
|
532
|
+
const { loadPixooDevices, savePixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
533
|
+
log('Scanning for Pixoo devices...');
|
|
534
|
+
const found = await discoverDevices();
|
|
535
|
+
if (found.length === 0) {
|
|
536
|
+
log('No devices found.');
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const verified = [];
|
|
540
|
+
for (const d of found) {
|
|
541
|
+
const config = await getDeviceConfig(d.ip);
|
|
542
|
+
verified.push({ ...d, ok: config !== null });
|
|
543
|
+
}
|
|
544
|
+
log(`\nFound ${verified.length} device(s):`);
|
|
545
|
+
for (const d of verified) {
|
|
546
|
+
log(` ${d.name} (${d.ip}) ${d.ok ? '\u2713 reachable' : '\u2717 unreachable'}`);
|
|
547
|
+
}
|
|
548
|
+
const reachable = verified.filter(d => d.ok);
|
|
549
|
+
if (reachable.length === 0) {
|
|
550
|
+
log('\nNo reachable devices to save.');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const readline = await import('readline');
|
|
554
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
555
|
+
const answer = await new Promise(resolve => {
|
|
556
|
+
rl.question(`\nSave ${reachable.length} device(s)? (Y/n) `, resolve);
|
|
557
|
+
});
|
|
558
|
+
rl.close();
|
|
559
|
+
if (answer.toLowerCase() === 'n') {
|
|
560
|
+
log('Cancelled.');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const existing = loadPixooDevices();
|
|
564
|
+
const existingIps = new Set(existing.map(d => d.ip));
|
|
565
|
+
const newDevices = reachable
|
|
566
|
+
.filter(d => !existingIps.has(d.ip))
|
|
567
|
+
.map(d => ({ ip: d.ip, name: d.name }));
|
|
568
|
+
if (newDevices.length === 0) {
|
|
569
|
+
log('All discovered devices already in settings.');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
savePixooDevices([...existing, ...newDevices]);
|
|
573
|
+
log(`Added ${newDevices.length} device(s) to ~/.agentdeck/settings.json`);
|
|
574
|
+
});
|
|
575
|
+
pixoo
|
|
576
|
+
.command('add <ip>')
|
|
577
|
+
.description('Manually add a Pixoo device')
|
|
578
|
+
.option('-n, --name <name>', 'Device name', 'Pixoo64')
|
|
579
|
+
.option('-b, --brightness <value>', 'Brightness 0-100')
|
|
580
|
+
.action(async (ip, opts) => {
|
|
581
|
+
const { getDeviceConfig } = await import('./pixoo/pixoo-client.js');
|
|
582
|
+
const { addDevice } = await import('./pixoo/pixoo-settings.js');
|
|
583
|
+
log(`Testing connection to ${ip}...`);
|
|
584
|
+
const config = await getDeviceConfig(ip);
|
|
585
|
+
log(config ? 'Device reachable \u2713' : `Cannot reach ${ip}. Adding anyway.`);
|
|
586
|
+
const device = { ip, name: opts.name };
|
|
587
|
+
if (opts.brightness)
|
|
588
|
+
device.brightness = Math.max(0, Math.min(100, parseInt(opts.brightness, 10)));
|
|
589
|
+
if (addDevice(device)) {
|
|
590
|
+
log(`Added ${device.name} (${ip}) to settings.`);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
log(`Device ${ip} already exists.`);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
pixoo
|
|
597
|
+
.command('list')
|
|
598
|
+
.description('List configured Pixoo devices')
|
|
599
|
+
.action(async () => {
|
|
600
|
+
const { loadPixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
601
|
+
const devices = loadPixooDevices();
|
|
602
|
+
if (devices.length === 0) {
|
|
603
|
+
log('No Pixoo devices configured. Run `agentdeck pixoo scan`.');
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
log(`${devices.length} device(s):`);
|
|
607
|
+
for (const d of devices) {
|
|
608
|
+
const parts = [d.name || 'Pixoo', `(${d.ip})`];
|
|
609
|
+
if (d.brightness !== undefined)
|
|
610
|
+
parts.push(`brightness=${d.brightness}`);
|
|
611
|
+
log(` ${parts.join(' ')}`);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
pixoo
|
|
615
|
+
.command('remove <ip>')
|
|
616
|
+
.description('Remove a Pixoo device')
|
|
617
|
+
.action(async (ip) => {
|
|
618
|
+
const { removeDevice } = await import('./pixoo/pixoo-settings.js');
|
|
619
|
+
if (removeDevice(ip)) {
|
|
620
|
+
log(`Removed ${ip}.`);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
log(`Device ${ip} not found.`);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
pixoo
|
|
627
|
+
.command('test [ip]')
|
|
628
|
+
.description('Send a test frame to a Pixoo device')
|
|
629
|
+
.action(async (ip) => {
|
|
630
|
+
const { loadPixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
631
|
+
const { pushFrame, setBrightness } = await import('./pixoo/pixoo-client.js');
|
|
632
|
+
let targetIp = ip;
|
|
633
|
+
if (!targetIp) {
|
|
634
|
+
const devices = loadPixooDevices();
|
|
635
|
+
if (devices.length === 0) {
|
|
636
|
+
log('No device specified and none configured.');
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
targetIp = devices[0].ip;
|
|
640
|
+
log(`Using first configured device: ${targetIp}`);
|
|
641
|
+
}
|
|
642
|
+
const buf = new Uint8Array(64 * 64 * 3);
|
|
643
|
+
for (let y = 0; y < 64; y++) {
|
|
644
|
+
for (let x = 0; x < 64; x++) {
|
|
645
|
+
const i = (y * 64 + x) * 3;
|
|
646
|
+
const hue = ((x + y) * 4) % 256;
|
|
647
|
+
const region = Math.floor(hue / 43);
|
|
648
|
+
const remainder = (hue - region * 43) * 6;
|
|
649
|
+
const q = 255 - remainder;
|
|
650
|
+
const t = remainder;
|
|
651
|
+
if (region === 0) {
|
|
652
|
+
buf[i] = 255;
|
|
653
|
+
buf[i + 1] = t;
|
|
654
|
+
buf[i + 2] = 0;
|
|
655
|
+
}
|
|
656
|
+
else if (region === 1) {
|
|
657
|
+
buf[i] = q;
|
|
658
|
+
buf[i + 1] = 255;
|
|
659
|
+
buf[i + 2] = 0;
|
|
660
|
+
}
|
|
661
|
+
else if (region === 2) {
|
|
662
|
+
buf[i] = 0;
|
|
663
|
+
buf[i + 1] = 255;
|
|
664
|
+
buf[i + 2] = t;
|
|
665
|
+
}
|
|
666
|
+
else if (region === 3) {
|
|
667
|
+
buf[i] = 0;
|
|
668
|
+
buf[i + 1] = q;
|
|
669
|
+
buf[i + 2] = 255;
|
|
670
|
+
}
|
|
671
|
+
else if (region === 4) {
|
|
672
|
+
buf[i] = t;
|
|
673
|
+
buf[i + 1] = 0;
|
|
674
|
+
buf[i + 2] = 255;
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
buf[i] = 255;
|
|
678
|
+
buf[i + 1] = 0;
|
|
679
|
+
buf[i + 2] = q;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
await setBrightness(targetIp, 60);
|
|
684
|
+
const ok = await pushFrame(targetIp, buf);
|
|
685
|
+
log(ok ? `Test frame sent to ${targetIp} \u2713` : `Failed to send to ${targetIp}`);
|
|
686
|
+
});
|
|
687
|
+
// ===== WiFi Setup =====
|
|
688
|
+
program
|
|
689
|
+
.command('wifi-setup')
|
|
690
|
+
.description('Configure WiFi for ESP32 independent operation (USB-free)')
|
|
691
|
+
.option('--ssid <ssid>', 'WiFi network name (auto-detected from macOS if omitted)')
|
|
692
|
+
.option('--password <password>', 'WiFi password (auto-fetched from Keychain if omitted)')
|
|
693
|
+
.action(async (opts) => {
|
|
694
|
+
const { detectCurrentSSID, getKeychainPassword, saveWifiConfig } = await import('./wifi-config.js');
|
|
695
|
+
let ssid = opts.ssid;
|
|
696
|
+
let password = opts.password;
|
|
697
|
+
if (!ssid) {
|
|
698
|
+
const detected = detectCurrentSSID();
|
|
699
|
+
if (detected) {
|
|
700
|
+
log(`Detected WiFi: ${detected}`);
|
|
701
|
+
ssid = detected;
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
log('Could not detect WiFi network. Use --ssid <name>');
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (!password) {
|
|
709
|
+
log(`Looking up Keychain password for "${ssid}"...`);
|
|
710
|
+
const keychainPw = getKeychainPassword(ssid);
|
|
711
|
+
if (keychainPw) {
|
|
712
|
+
log('Found password in Keychain \u2713');
|
|
713
|
+
password = keychainPw;
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
log('Password not found in Keychain. Use --password <pw>');
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
saveWifiConfig({ ssid, password, autoProvision: true });
|
|
721
|
+
log(`WiFi config saved: SSID="${ssid}" \u2192 ~/.agentdeck/wifi-config.json`);
|
|
722
|
+
log('Daemon will auto-provision ESP32 devices on next restart.');
|
|
723
|
+
});
|
|
724
|
+
// ===== TUI Dashboard =====
|
|
725
|
+
program
|
|
726
|
+
.command('dashboard')
|
|
727
|
+
.alias('dash')
|
|
728
|
+
.description('TUI monitoring dashboard with terrarium animation')
|
|
729
|
+
.option('-p, --port <port>', 'Bridge port (auto-discover if omitted)')
|
|
730
|
+
.option('-s, --session <id>', 'Specific session ID')
|
|
731
|
+
.action(async (opts) => {
|
|
732
|
+
const { startDashboard } = await import('./tui/dashboard.js');
|
|
733
|
+
await startDashboard(opts);
|
|
734
|
+
});
|
|
735
|
+
// ===== Backward compat: `agentdeck start` → `agentdeck daemon start` =====
|
|
736
|
+
program
|
|
737
|
+
.command('start', { hidden: true })
|
|
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
|
+
});
|
|
755
|
+
// ===== Default command (no args) =====
|
|
756
|
+
// If no command specified, show status or help
|
|
757
|
+
program.action(async () => {
|
|
758
|
+
const { listActive } = await import('./session-registry.js');
|
|
759
|
+
const sessions = listActive();
|
|
760
|
+
if (sessions.length === 0) {
|
|
761
|
+
program.help();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
log('AgentDeck — Active sessions:\n');
|
|
765
|
+
for (const s of sessions) {
|
|
766
|
+
const type = s.agentType ?? 'unknown';
|
|
767
|
+
const age = Math.round((Date.now() - new Date(s.startedAt).getTime()) / 1000);
|
|
768
|
+
log(` [${type}] :${s.port} — ${s.projectName} (PID ${s.pid}, ${age}s)`);
|
|
769
|
+
}
|
|
770
|
+
log('\nRun `agentdeck --help` for all commands.');
|
|
771
|
+
});
|
|
772
|
+
// ===== APME commands =====
|
|
773
|
+
const apme = program.command('apme').description('Agent Performance Monitoring & Evaluation');
|
|
774
|
+
apme
|
|
775
|
+
.command('runs')
|
|
776
|
+
.description('List recent evaluated runs')
|
|
777
|
+
.option('-n, --limit <n>', 'Number of runs', '20')
|
|
778
|
+
.option('-a, --agent <type>', 'Filter by agent type (claude-code, openclaw, ...)')
|
|
779
|
+
.option('-m, --model <id>', 'Filter by model id')
|
|
780
|
+
.action(async (opts) => {
|
|
781
|
+
const { initApme } = await import('./apme/index.js');
|
|
782
|
+
const apme = await initApme();
|
|
783
|
+
if (!apme) {
|
|
784
|
+
log('APME not available (better-sqlite3 missing)');
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
const runs = apme.store.listRuns({
|
|
788
|
+
limit: parseInt(opts.limit, 10) || 20,
|
|
789
|
+
agentType: opts.agent,
|
|
790
|
+
modelId: opts.model,
|
|
791
|
+
});
|
|
792
|
+
if (runs.length === 0) {
|
|
793
|
+
log('No runs found.');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
log(`\n ${'ID'.padEnd(10)} ${'Category'.padEnd(14)} ${'Agent'.padEnd(14)} ${'Model'.padEnd(20)} ${'Project'.padEnd(14)} ${'Cost'.padEnd(8)} ${'Score'.padEnd(7)} Dur`);
|
|
797
|
+
log(` ${'─'.repeat(10)} ${'─'.repeat(14)} ${'─'.repeat(14)} ${'─'.repeat(20)} ${'─'.repeat(14)} ${'─'.repeat(8)} ${'─'.repeat(7)} ${'─'.repeat(5)}`);
|
|
798
|
+
for (const r of runs) {
|
|
799
|
+
const evals = apme.store.listEvalsForRun(r.id);
|
|
800
|
+
const overall = evals.find(e => e.layer === 'llm_judge' && e.metric === 'overall');
|
|
801
|
+
const det = evals.filter(e => e.layer === 'deterministic');
|
|
802
|
+
const score = overall
|
|
803
|
+
? `${(overall.score * 100).toFixed(0)}%`
|
|
804
|
+
: det.length > 0
|
|
805
|
+
? `${det.filter(e => e.score === 1).length}/${det.length}`
|
|
806
|
+
: '—';
|
|
807
|
+
const dur = r.endedAt && r.startedAt
|
|
808
|
+
? `${Math.round((r.endedAt - r.startedAt) / 1000)}s`
|
|
809
|
+
: '—';
|
|
810
|
+
const cost = r.costUsd != null ? `$${r.costUsd.toFixed(3)}` : '—';
|
|
811
|
+
const cat = r.taskCategory ?? '—';
|
|
812
|
+
log(` ${r.id.slice(0, 10)} ${cat.padEnd(14)} ${(r.agentType ?? '—').padEnd(14)} ${(r.modelId ?? '—').slice(0, 20).padEnd(20)} ${(r.projectName ?? '—').slice(0, 14).padEnd(14)} ${cost.padEnd(8)} ${score.padEnd(7)} ${dur}`);
|
|
813
|
+
}
|
|
814
|
+
log(`\n ${runs.length} run(s) shown.`);
|
|
815
|
+
});
|
|
816
|
+
apme
|
|
817
|
+
.command('run <id>')
|
|
818
|
+
.description('Show details for a specific run')
|
|
819
|
+
.action(async (id) => {
|
|
820
|
+
const { initApme, evaluateOutcome } = await import('./apme/index.js');
|
|
821
|
+
const apme = await initApme();
|
|
822
|
+
if (!apme) {
|
|
823
|
+
log('APME not available');
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
// Support partial id match
|
|
827
|
+
let run = apme.store.getRun(id);
|
|
828
|
+
if (!run) {
|
|
829
|
+
const all = apme.store.listRuns({ limit: 500 });
|
|
830
|
+
const match = all.find(r => r.id.startsWith(id));
|
|
831
|
+
if (match)
|
|
832
|
+
run = match;
|
|
833
|
+
}
|
|
834
|
+
if (!run) {
|
|
835
|
+
log(`Run ${id} not found.`);
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
const durSec = run.endedAt && run.startedAt ? Math.round((run.endedAt - run.startedAt) / 1000) : 0;
|
|
839
|
+
const durStr = durSec >= 60 ? `${Math.floor(durSec / 60)}m ${durSec % 60}s` : `${durSec}s`;
|
|
840
|
+
const tokIn = run.inputTokens ?? 0;
|
|
841
|
+
const tokOut = run.outputTokens ?? 0;
|
|
842
|
+
const costStr = run.costUsd != null ? `$${run.costUsd.toFixed(3)}` : '—';
|
|
843
|
+
const catStr = run.taskCategory ?? 'unknown';
|
|
844
|
+
log(`\n Run: ${run.id.slice(0, 10)} (${run.agentType} / ${run.modelId ?? '—'} / ${run.projectName ?? '—'})`);
|
|
845
|
+
if (run.taskPrompt)
|
|
846
|
+
log(` Task: "${run.taskPrompt.slice(0, 300)}"`);
|
|
847
|
+
log(` Duration: ${durStr} │ Tokens: ${tokIn.toLocaleString()} in / ${tokOut.toLocaleString()} out │ Cost: ${costStr}`);
|
|
848
|
+
log(` Category: ${catStr}`);
|
|
849
|
+
// ── Outcome ──
|
|
850
|
+
let outcomeData = run.outcome ? { outcome: run.outcome, confidence: run.outcomeConfidence ?? '—' } : null;
|
|
851
|
+
if (!outcomeData && run.endedAt) {
|
|
852
|
+
// Compute now if not yet evaluated
|
|
853
|
+
const result = evaluateOutcome(apme.store, run.id);
|
|
854
|
+
if (result) {
|
|
855
|
+
outcomeData = { outcome: result.outcome.outcome, confidence: result.outcome.confidence };
|
|
856
|
+
run = apme.store.getRun(run.id) ?? run; // refresh
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (outcomeData) {
|
|
860
|
+
log(`\n ── Outcome ${'─'.repeat(40)} confidence: ${String(outcomeData.confidence).toUpperCase()}`);
|
|
861
|
+
log(` ${outcomeData.outcome}`);
|
|
862
|
+
}
|
|
863
|
+
// ── LLM Judge ──
|
|
864
|
+
const evals = apme.store.listEvalsForRun(run.id);
|
|
865
|
+
const judgeEvals = evals.filter(e => e.layer === 'llm_judge');
|
|
866
|
+
if (judgeEvals.length > 0) {
|
|
867
|
+
const overall = judgeEvals.find(e => e.metric === 'overall');
|
|
868
|
+
log(`\n ── Task Completion (LLM Judge) ${'─'.repeat(24)} score: ${overall ? (overall.score * 100).toFixed(0) + '%' : '—'}`);
|
|
869
|
+
for (const e of judgeEvals.filter(e => e.metric !== 'overall')) {
|
|
870
|
+
log(` ${e.metric.padEnd(20)} ${(e.score * 100).toFixed(0)}%`);
|
|
871
|
+
}
|
|
872
|
+
if (overall?.raw) {
|
|
873
|
+
try {
|
|
874
|
+
const raw = JSON.parse(overall.raw);
|
|
875
|
+
if (raw.done?.length) {
|
|
876
|
+
for (const item of raw.done)
|
|
877
|
+
log(` ✓ ${item}`);
|
|
878
|
+
}
|
|
879
|
+
if (raw.missed?.length) {
|
|
880
|
+
for (const item of raw.missed)
|
|
881
|
+
log(` ✗ ${item}`);
|
|
882
|
+
}
|
|
883
|
+
if (raw.reasoning)
|
|
884
|
+
log(` Reasoning: "${raw.reasoning.slice(0, 300)}"`);
|
|
885
|
+
}
|
|
886
|
+
catch { /* ignore */ }
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// ── Deterministic ──
|
|
890
|
+
const detEvals = evals.filter(e => e.layer === 'deterministic');
|
|
891
|
+
if (detEvals.length > 0) {
|
|
892
|
+
log(`\n ── Deterministic ${'─'.repeat(36)}`);
|
|
893
|
+
for (const e of detEvals) {
|
|
894
|
+
const icon = e.score === 1 ? '✓' : '✗';
|
|
895
|
+
let cmdInfo = '';
|
|
896
|
+
if (e.raw) {
|
|
897
|
+
try {
|
|
898
|
+
const r = JSON.parse(e.raw);
|
|
899
|
+
cmdInfo = ` (${r.command ?? '—'}, exit=${r.exitCode ?? '—'}, ${r.durationMs ? Math.round(r.durationMs / 1000) + 's' : '—'})`;
|
|
900
|
+
}
|
|
901
|
+
catch { /* ignore */ }
|
|
902
|
+
}
|
|
903
|
+
log(` ${icon} ${e.metric}${cmdInfo}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// ── Efficiency ──
|
|
907
|
+
if (run.efficiencyJson) {
|
|
908
|
+
try {
|
|
909
|
+
const eff = JSON.parse(run.efficiencyJson);
|
|
910
|
+
log(`\n ── Efficiency ${'─'.repeat(40)}`);
|
|
911
|
+
if (eff.diffLines != null)
|
|
912
|
+
log(` ${eff.diffLines} lines changed`);
|
|
913
|
+
if (eff.tokensPerChange != null)
|
|
914
|
+
log(` ${eff.tokensPerChange} tokens/changed line`);
|
|
915
|
+
if (eff.costPerChange != null)
|
|
916
|
+
log(` $${eff.costPerChange.toFixed(4)}/changed line`);
|
|
917
|
+
if (eff.toolEfficiency != null)
|
|
918
|
+
log(` ${eff.toolEfficiency} lines/tool call`);
|
|
919
|
+
if (eff.timeToCompleteSec != null)
|
|
920
|
+
log(` ${eff.timeToCompleteSec}s total`);
|
|
921
|
+
}
|
|
922
|
+
catch { /* ignore */ }
|
|
923
|
+
}
|
|
924
|
+
// ── Composite Score ──
|
|
925
|
+
if (run.compositeScore != null) {
|
|
926
|
+
log(`\n ── Composite Score ${'─'.repeat(34)} ${(run.compositeScore * 100).toFixed(0)}%`);
|
|
927
|
+
// Show weight breakdown
|
|
928
|
+
const oc = outcomeData ? `outcome(${outcomeData.outcome})` : 'outcome(—)';
|
|
929
|
+
const jd = judgeEvals.find(e => e.metric === 'overall');
|
|
930
|
+
const jdStr = jd ? `judge(${(jd.score * 100).toFixed(0)}%)` : 'judge(—)';
|
|
931
|
+
const vibe = apme.store.latestVibeForRun(run.id);
|
|
932
|
+
const vibeStr = vibe ? `vibe(${vibe.verdict})` : 'vibe(—)';
|
|
933
|
+
log(` ${oc} × 0.4 + ${jdStr} × 0.3 + efficiency × 0.2 + ${vibeStr} × 0.1`);
|
|
934
|
+
}
|
|
935
|
+
// ── Turns ──
|
|
936
|
+
const turns = apme.store.listTurns(run.id);
|
|
937
|
+
if (turns.length > 0) {
|
|
938
|
+
log(`\n ── Turns (${turns.length}) ${'─'.repeat(38)}`);
|
|
939
|
+
for (const t of turns) {
|
|
940
|
+
const idx = t.turn_index;
|
|
941
|
+
const prompt = t.prompt;
|
|
942
|
+
const tc = t.tool_calls ?? 0;
|
|
943
|
+
const fm = t.files_modified ?? 0;
|
|
944
|
+
const fc = t.files_created ?? 0;
|
|
945
|
+
const dur = t.ended_at && t.started_at
|
|
946
|
+
? `${Math.round((t.ended_at - t.started_at) / 1000)}s`
|
|
947
|
+
: 'open';
|
|
948
|
+
const gitChanged = t.git_before && t.git_after && t.git_before !== t.git_after ? ' +commit' : '';
|
|
949
|
+
const turnEvals = apme.store.listEvalsForTurn(t.id);
|
|
950
|
+
const turnOverall = turnEvals.find(e => e.metric === 'overall');
|
|
951
|
+
const scoreStr = turnOverall ? ` score=${(turnOverall.score * 100).toFixed(0)}%` : '';
|
|
952
|
+
const resp = t.response;
|
|
953
|
+
log(` [${idx}] ${prompt ? '"' + prompt.slice(0, 80) + (prompt.length > 80 ? '...' : '') + '"' : '(no prompt)'}`);
|
|
954
|
+
if (resp)
|
|
955
|
+
log(` → ${resp.slice(0, 200)}${resp.length > 200 ? '...' : ''}`);
|
|
956
|
+
log(` ${tc} tools, ${fm} edits, ${fc} creates, ${dur}${gitChanged}${scoreStr}`);
|
|
957
|
+
// Show judge reasoning if available for this turn
|
|
958
|
+
if (turnOverall?.raw) {
|
|
959
|
+
try {
|
|
960
|
+
const raw = JSON.parse(turnOverall.raw);
|
|
961
|
+
if (raw.done?.length)
|
|
962
|
+
for (const item of raw.done)
|
|
963
|
+
log(` ✓ ${item}`);
|
|
964
|
+
if (raw.missed?.length)
|
|
965
|
+
for (const item of raw.missed)
|
|
966
|
+
log(` ✗ ${item}`);
|
|
967
|
+
}
|
|
968
|
+
catch { /* ignore */ }
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// ── Steps summary ──
|
|
973
|
+
const steps = apme.store.listSteps(run.id);
|
|
974
|
+
if (steps.length > 0) {
|
|
975
|
+
log(`\n ── Steps (${steps.length}) ${'─'.repeat(38)}`);
|
|
976
|
+
for (const s of steps.slice(-10)) {
|
|
977
|
+
const tool = s.toolName ? ` [${s.toolName}]` : '';
|
|
978
|
+
log(` ${new Date(s.ts).toISOString().slice(11, 19)} ${s.kind}${tool}`);
|
|
979
|
+
}
|
|
980
|
+
if (steps.length > 10)
|
|
981
|
+
log(` ... ${steps.length - 10} more`);
|
|
982
|
+
}
|
|
983
|
+
const vibe = apme.store.latestVibeForRun(run.id);
|
|
984
|
+
if (vibe)
|
|
985
|
+
log(`\n Vibe: ${vibe.verdict}${vibe.note ? ` — ${vibe.note}` : ''}`);
|
|
986
|
+
});
|
|
987
|
+
apme
|
|
988
|
+
.command('scorecard')
|
|
989
|
+
.description('Model performance scorecard')
|
|
990
|
+
.action(async () => {
|
|
991
|
+
const { initApme } = await import('./apme/index.js');
|
|
992
|
+
const apme = await initApme();
|
|
993
|
+
if (!apme) {
|
|
994
|
+
log('APME not available');
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
const cards = apme.store.scorecard();
|
|
998
|
+
if (cards.length === 0) {
|
|
999
|
+
log('No scorecard data yet.');
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
log(`\n ${'Model'.padEnd(26)} ${'Agent'.padEnd(14)} ${'Runs'.padEnd(6)} ${'Score'.padEnd(8)} ${'Tests'.padEnd(8)} ${'Cost'.padEnd(10)} $/Quality`);
|
|
1003
|
+
log(` ${'─'.repeat(26)} ${'─'.repeat(14)} ${'─'.repeat(6)} ${'─'.repeat(8)} ${'─'.repeat(8)} ${'─'.repeat(10)} ${'─'.repeat(9)}`);
|
|
1004
|
+
for (const c of cards) {
|
|
1005
|
+
const score = c.avgOverall != null ? `${(c.avgOverall * 100).toFixed(0)}%` : '—';
|
|
1006
|
+
const tests = c.avgTestsPass != null ? `${(c.avgTestsPass * 100).toFixed(0)}%` : '—';
|
|
1007
|
+
const cost = c.totalCost != null ? `$${c.totalCost.toFixed(2)}` : '—';
|
|
1008
|
+
const cpq = c.costPerQuality != null ? `$${c.costPerQuality.toFixed(2)}` : '—';
|
|
1009
|
+
log(` ${c.modelId.slice(0, 26).padEnd(26)} ${c.agentType.padEnd(14)} ${String(c.runs).padEnd(6)} ${score.padEnd(8)} ${tests.padEnd(8)} ${cost.padEnd(10)} ${cpq}`);
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
apme
|
|
1013
|
+
.command('judge')
|
|
1014
|
+
.description('Run evaluation on unevaluated runs')
|
|
1015
|
+
.option('--all', 'Judge all unevaluated runs (default: up to 10)')
|
|
1016
|
+
.action(async (opts) => {
|
|
1017
|
+
const { initApme } = await import('./apme/index.js');
|
|
1018
|
+
const apme = await initApme();
|
|
1019
|
+
if (!apme) {
|
|
1020
|
+
log('APME not available');
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
const limit = opts.all ? 100 : 10;
|
|
1024
|
+
const pending = apme.store.listUnevaluatedRuns(limit);
|
|
1025
|
+
if (pending.length === 0) {
|
|
1026
|
+
log('All runs already evaluated.');
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
log(`Evaluating ${pending.length} run(s)...`);
|
|
1030
|
+
let completed = 0;
|
|
1031
|
+
apme.runner.onResult((r) => {
|
|
1032
|
+
completed++;
|
|
1033
|
+
const tag = [r.layer1Ran && 'L1', r.layer2Ran && 'L2'].filter(Boolean).join('+') || 'skip';
|
|
1034
|
+
log(` [${completed}/${pending.length}] ${r.runId.slice(0, 10)} → ${tag}${r.overall != null ? ` overall=${(r.overall * 100).toFixed(0)}%` : ''}`);
|
|
1035
|
+
});
|
|
1036
|
+
for (const run of pending) {
|
|
1037
|
+
apme.runner.enqueue({ runId: run.id, projectPath: run.projectPath ?? undefined });
|
|
1038
|
+
}
|
|
1039
|
+
await apme.runner.drain();
|
|
1040
|
+
log(`Done. ${completed} run(s) evaluated.`);
|
|
1041
|
+
});
|
|
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
|
+
apme
|
|
1062
|
+
.command('vibe <runId> <verdict>')
|
|
1063
|
+
.description('Submit vibe feedback (approve|reject|neutral)')
|
|
1064
|
+
.option('-n, --note <text>', 'Optional note')
|
|
1065
|
+
.action(async (runId, verdict, opts) => {
|
|
1066
|
+
const { initApme } = await import('./apme/index.js');
|
|
1067
|
+
const apme = await initApme();
|
|
1068
|
+
if (!apme) {
|
|
1069
|
+
log('APME not available');
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
if (!['approve', 'reject', 'neutral'].includes(verdict)) {
|
|
1073
|
+
log('Verdict must be approve, reject, or neutral.');
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
// Support partial id
|
|
1077
|
+
let run = apme.store.getRun(runId);
|
|
1078
|
+
if (!run) {
|
|
1079
|
+
const all = apme.store.listRuns({ limit: 500 });
|
|
1080
|
+
const match = all.find(r => r.id.startsWith(runId));
|
|
1081
|
+
if (match)
|
|
1082
|
+
run = match;
|
|
1083
|
+
}
|
|
1084
|
+
if (!run) {
|
|
1085
|
+
log(`Run ${runId} not found.`);
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
apme.store.insertVibe({
|
|
1089
|
+
runId: run.id,
|
|
1090
|
+
verdict: verdict,
|
|
1091
|
+
note: opts.note ?? null,
|
|
1092
|
+
ts: Date.now(),
|
|
1093
|
+
});
|
|
1094
|
+
log(`Vibe ${verdict} recorded for run ${run.id.slice(0, 10)}.`);
|
|
1095
|
+
});
|
|
1096
|
+
apme
|
|
1097
|
+
.command('export')
|
|
1098
|
+
.description('Export runs + evals as JSONL')
|
|
1099
|
+
.option('-n, --limit <n>', 'Number of runs', '100')
|
|
1100
|
+
.option('-o, --output <file>', 'Output file (default: stdout)')
|
|
1101
|
+
.action(async (opts) => {
|
|
1102
|
+
const { initApme } = await import('./apme/index.js');
|
|
1103
|
+
const { writeFileSync: wfs } = await import('fs');
|
|
1104
|
+
const apme = await initApme();
|
|
1105
|
+
if (!apme) {
|
|
1106
|
+
log('APME not available');
|
|
1107
|
+
process.exit(1);
|
|
1108
|
+
}
|
|
1109
|
+
const runs = apme.store.listRuns({ limit: parseInt(opts.limit, 10) || 100 });
|
|
1110
|
+
const lines = [];
|
|
1111
|
+
for (const r of runs) {
|
|
1112
|
+
const evals = apme.store.listEvalsForRun(r.id);
|
|
1113
|
+
const vibe = apme.store.latestVibeForRun(r.id);
|
|
1114
|
+
lines.push(JSON.stringify({ ...r, evals, vibe }));
|
|
1115
|
+
}
|
|
1116
|
+
if (opts.output) {
|
|
1117
|
+
wfs(opts.output, lines.join('\n') + '\n', 'utf-8');
|
|
1118
|
+
log(`Exported ${runs.length} run(s) to ${opts.output}`);
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
for (const line of lines)
|
|
1122
|
+
process.stdout.write(line + '\n');
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
apme
|
|
1126
|
+
.command('rubric')
|
|
1127
|
+
.description('Show current judge rubric')
|
|
1128
|
+
.action(async () => {
|
|
1129
|
+
const { initApme } = await import('./apme/index.js');
|
|
1130
|
+
const apme = await initApme();
|
|
1131
|
+
if (!apme) {
|
|
1132
|
+
log('APME not available');
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
const rubric = apme.store.getCurrentRubric('general');
|
|
1136
|
+
if (!rubric) {
|
|
1137
|
+
log('No rubric seeded.');
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
log(`\n Rubric v${rubric.version} (purpose: ${rubric.purpose})`);
|
|
1141
|
+
log(` Created: ${new Date(rubric.createdAt).toISOString()}`);
|
|
1142
|
+
if (rubric.parentVer)
|
|
1143
|
+
log(` Parent: v${rubric.parentVer}`);
|
|
1144
|
+
if (rubric.notes)
|
|
1145
|
+
log(` Notes: ${rubric.notes}`);
|
|
1146
|
+
log(`\n Weights: ${rubric.weights}`);
|
|
1147
|
+
log(`\n Prompt:\n${rubric.prompt.split('\n').map(l => ' ' + l).join('\n')}`);
|
|
1148
|
+
});
|
|
1149
|
+
apme
|
|
1150
|
+
.command('tag <runId> <category>')
|
|
1151
|
+
.description('Override task category for a run (user label)')
|
|
1152
|
+
.action(async (runId, category) => {
|
|
1153
|
+
const { initApme, TASK_CATEGORIES } = await import('./apme/index.js');
|
|
1154
|
+
const apme = await initApme();
|
|
1155
|
+
if (!apme) {
|
|
1156
|
+
log('APME not available');
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
// Support partial id
|
|
1160
|
+
let run = apme.store.getRun(runId);
|
|
1161
|
+
if (!run) {
|
|
1162
|
+
const all = apme.store.listRuns({ limit: 500 });
|
|
1163
|
+
const match = all.find(r => r.id.startsWith(runId));
|
|
1164
|
+
if (match)
|
|
1165
|
+
run = match;
|
|
1166
|
+
}
|
|
1167
|
+
if (!run) {
|
|
1168
|
+
log(`Run ${runId} not found.`);
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
if (!TASK_CATEGORIES.includes(category) && category !== 'unknown') {
|
|
1172
|
+
log(`Known categories: ${TASK_CATEGORIES.join(', ')}`);
|
|
1173
|
+
log(`You can also use any custom string.`);
|
|
1174
|
+
}
|
|
1175
|
+
apme.store.updateRun(run.id, {
|
|
1176
|
+
taskCategory: category,
|
|
1177
|
+
taskCategorySource: 'user',
|
|
1178
|
+
});
|
|
1179
|
+
log(`Tagged run ${run.id.slice(0, 10)} as "${category}" (source: user).`);
|
|
1180
|
+
});
|
|
1181
|
+
apme
|
|
1182
|
+
.command('reclassify')
|
|
1183
|
+
.description('Re-classify all runs using the current rule-based classifier')
|
|
1184
|
+
.option('--force', 'Overwrite user-tagged runs too')
|
|
1185
|
+
.action(async (opts) => {
|
|
1186
|
+
const { initApme, classifyRun } = await import('./apme/index.js');
|
|
1187
|
+
const apme = await initApme();
|
|
1188
|
+
if (!apme) {
|
|
1189
|
+
log('APME not available');
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
const runs = apme.store.listRuns({ limit: 5000 });
|
|
1193
|
+
let updated = 0;
|
|
1194
|
+
let skipped = 0;
|
|
1195
|
+
for (const r of runs) {
|
|
1196
|
+
if (!opts.force && r.taskCategorySource === 'user') {
|
|
1197
|
+
skipped++;
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const { signals, category } = classifyRun(apme.store, r.id);
|
|
1201
|
+
apme.store.updateRun(r.id, {
|
|
1202
|
+
taskSignals: JSON.stringify(signals),
|
|
1203
|
+
taskCategory: category,
|
|
1204
|
+
taskCategorySource: 'auto',
|
|
1205
|
+
});
|
|
1206
|
+
updated++;
|
|
1207
|
+
}
|
|
1208
|
+
log(`Reclassified ${updated} run(s).${skipped > 0 ? ` Skipped ${skipped} user-tagged (use --force to overwrite).` : ''}`);
|
|
1209
|
+
});
|
|
1210
|
+
apme
|
|
1211
|
+
.command('categories')
|
|
1212
|
+
.description('Show task category scorecard (model × category performance)')
|
|
1213
|
+
.action(async () => {
|
|
1214
|
+
const { initApme } = await import('./apme/index.js');
|
|
1215
|
+
const apme = await initApme();
|
|
1216
|
+
if (!apme) {
|
|
1217
|
+
log('APME not available');
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
const cards = apme.store.categoryScorecard();
|
|
1221
|
+
if (cards.length === 0) {
|
|
1222
|
+
log('No category data yet.');
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
log(`\n ${'Category'.padEnd(16)} ${'Model'.padEnd(22)} ${'Runs'.padEnd(6)} ${'Score'.padEnd(8)} ${'Tests'.padEnd(8)} Cost`);
|
|
1226
|
+
log(` ${'─'.repeat(16)} ${'─'.repeat(22)} ${'─'.repeat(6)} ${'─'.repeat(8)} ${'─'.repeat(8)} ${'─'.repeat(8)}`);
|
|
1227
|
+
for (const c of cards) {
|
|
1228
|
+
const score = c.avgOverall != null ? `${(c.avgOverall * 100).toFixed(0)}%` : '—';
|
|
1229
|
+
const tests = c.avgTestsPass != null ? `${(c.avgTestsPass * 100).toFixed(0)}%` : '—';
|
|
1230
|
+
const cost = c.totalCost != null ? `$${c.totalCost.toFixed(2)}` : '—';
|
|
1231
|
+
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
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
program.parse();
|
|
1235
|
+
//# sourceMappingURL=cli.js.map
|