@agentdeck/bridge 1.0.4 → 1.0.6
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/fm-helper/agentdeck-fm-helper +0 -0
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +3 -0
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adb-reverse.d.ts.map +1 -1
- package/dist/adb-reverse.js +4 -2
- package/dist/adb-reverse.js.map +1 -1
- package/dist/apme/collector.js +2 -2
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/outcome.js +2 -2
- package/dist/apme/outcome.js.map +1 -1
- package/dist/apme/runner.js +3 -3
- package/dist/apme/runner.js.map +1 -1
- package/dist/ble-sync-spawn.js +1 -1
- package/dist/ble-sync-spawn.js.map +1 -1
- package/dist/calendar.d.ts +82 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +324 -0
- package/dist/calendar.js.map +1 -0
- package/dist/card-feed.d.ts +63 -5
- package/dist/card-feed.d.ts.map +1 -1
- package/dist/card-feed.js +218 -14
- package/dist/card-feed.js.map +1 -1
- package/dist/card-modules.d.ts +4 -0
- package/dist/card-modules.d.ts.map +1 -1
- package/dist/card-modules.js +5 -3
- package/dist/card-modules.js.map +1 -1
- package/dist/check-deps.js +4 -4
- package/dist/check-deps.js.map +1 -1
- package/dist/cli.d.ts +35 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +269 -9
- package/dist/cli.js.map +1 -1
- package/dist/codex-otel.d.ts +89 -0
- package/dist/codex-otel.d.ts.map +1 -0
- package/dist/codex-otel.js +487 -0
- package/dist/codex-otel.js.map +1 -0
- package/dist/codex-rate-limits.d.ts.map +1 -1
- package/dist/codex-rate-limits.js +15 -3
- package/dist/codex-rate-limits.js.map +1 -1
- package/dist/daemon-server.d.ts +4 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +880 -63
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon-ws-client.d.ts +108 -24
- package/dist/daemon-ws-client.d.ts.map +1 -1
- package/dist/daemon-ws-client.js +175 -39
- package/dist/daemon-ws-client.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/device-voice-reply.d.ts +1 -10
- package/dist/device-voice-reply.d.ts.map +1 -1
- package/dist/device-voice-reply.js +19 -36
- package/dist/device-voice-reply.js.map +1 -1
- package/dist/device-voice.d.ts +37 -0
- package/dist/device-voice.d.ts.map +1 -1
- package/dist/device-voice.js +130 -1
- package/dist/device-voice.js.map +1 -1
- package/dist/diag-analyzer.d.ts.map +1 -1
- package/dist/diag-analyzer.js +1 -0
- package/dist/diag-analyzer.js.map +1 -1
- package/dist/display-monitor.d.ts.map +1 -1
- package/dist/display-monitor.js +1 -0
- package/dist/display-monitor.js.map +1 -1
- package/dist/esp32-serial.d.ts +4 -0
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +6 -3
- package/dist/esp32-serial.js.map +1 -1
- package/dist/foundation-models-helper.d.ts +23 -0
- package/dist/foundation-models-helper.d.ts.map +1 -1
- package/dist/foundation-models-helper.js +90 -21
- package/dist/foundation-models-helper.js.map +1 -1
- package/dist/gateway-probe.js +1 -1
- package/dist/gateway-probe.js.map +1 -1
- package/dist/glance-frame.d.ts +64 -0
- package/dist/glance-frame.d.ts.map +1 -0
- package/dist/glance-frame.js +364 -0
- package/dist/glance-frame.js.map +1 -0
- package/dist/hook-codex-sessions.d.ts +40 -0
- package/dist/hook-codex-sessions.d.ts.map +1 -0
- package/dist/hook-codex-sessions.js +174 -0
- package/dist/hook-codex-sessions.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -1
- package/dist/idotmatrix/idotmatrix-discover.js +7 -4
- package/dist/idotmatrix/idotmatrix-discover.js.map +1 -1
- package/dist/idotmatrix/idotmatrix-settings.d.ts +15 -0
- package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -1
- package/dist/idotmatrix/idotmatrix-settings.js +21 -0
- package/dist/idotmatrix/idotmatrix-settings.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -7
- package/dist/index.js.map +1 -1
- package/dist/linux-service.d.ts +52 -0
- package/dist/linux-service.d.ts.map +1 -0
- package/dist/linux-service.js +206 -0
- package/dist/linux-service.js.map +1 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -1
- package/dist/mdns-discover.d.ts +19 -0
- package/dist/mdns-discover.d.ts.map +1 -0
- package/dist/mdns-discover.js +109 -0
- package/dist/mdns-discover.js.map +1 -0
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +1 -0
- package/dist/model-catalog.js.map +1 -1
- package/dist/modules/adb-module.js +1 -1
- package/dist/modules/adb-module.js.map +1 -1
- package/dist/openclaw-session.d.ts +6 -0
- package/dist/openclaw-session.d.ts.map +1 -1
- package/dist/openclaw-session.js +7 -1
- package/dist/openclaw-session.js.map +1 -1
- package/dist/output-parser.d.ts +9 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +67 -16
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +57 -8
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +210 -74
- package/dist/passive-observer.js.map +1 -1
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
- package/dist/pixoo/pixoo-bridge.js +47 -6
- package/dist/pixoo/pixoo-bridge.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts +2 -1
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +137 -8
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pocket-autonomy.d.ts +78 -0
- package/dist/pocket-autonomy.d.ts.map +1 -0
- package/dist/pocket-autonomy.js +529 -0
- package/dist/pocket-autonomy.js.map +1 -0
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +3 -2
- package/dist/pty-manager.js.map +1 -1
- package/dist/python-ble-runtime.d.ts +2 -0
- package/dist/python-ble-runtime.d.ts.map +1 -1
- package/dist/python-ble-runtime.js +2 -1
- package/dist/python-ble-runtime.js.map +1 -1
- package/dist/remote-sessions.d.ts +64 -0
- package/dist/remote-sessions.d.ts.map +1 -0
- package/dist/remote-sessions.js +81 -0
- package/dist/remote-sessions.js.map +1 -0
- package/dist/review-runner.js +2 -2
- package/dist/review-runner.js.map +1 -1
- package/dist/session-aggregator.d.ts +2 -0
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js +6 -1
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-focus-relay.d.ts +47 -1
- package/dist/session-focus-relay.d.ts.map +1 -1
- package/dist/session-focus-relay.js +130 -15
- package/dist/session-focus-relay.js.map +1 -1
- package/dist/session-push-channel.d.ts +31 -0
- package/dist/session-push-channel.d.ts.map +1 -0
- package/dist/session-push-channel.js +94 -0
- package/dist/session-push-channel.js.map +1 -0
- package/dist/session-registry.d.ts +126 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +180 -3
- package/dist/session-registry.js.map +1 -1
- package/dist/subagent-timeline.d.ts +34 -0
- package/dist/subagent-timeline.d.ts.map +1 -0
- package/dist/subagent-timeline.js +156 -0
- package/dist/subagent-timeline.js.map +1 -0
- package/dist/timebox/timebox-discover.js +1 -1
- package/dist/timebox/timebox-discover.js.map +1 -1
- package/dist/tui/gauge.d.ts +4 -2
- package/dist/tui/gauge.d.ts.map +1 -1
- package/dist/tui/gauge.js +8 -4
- package/dist/tui/gauge.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +28 -0
- package/dist/tui/renderer.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/usage-api.d.ts +43 -0
- package/dist/usage-api.d.ts.map +1 -1
- package/dist/usage-api.js +114 -9
- package/dist/usage-api.js.map +1 -1
- package/dist/usage-event.d.ts +19 -0
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +35 -0
- package/dist/usage-event.js.map +1 -1
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +1 -0
- package/dist/utils/project-name.js.map +1 -1
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +3 -2
- package/dist/version-check.js.map +1 -1
- package/dist/voice-assistant.d.ts.map +1 -1
- package/dist/voice-assistant.js +2 -1
- package/dist/voice-assistant.js.map +1 -1
- package/dist/voice.d.ts.map +1 -1
- package/dist/voice.js +1 -0
- package/dist/voice.js.map +1 -1
- package/dist/weather.d.ts +72 -0
- package/dist/weather.d.ts.map +1 -0
- package/dist/weather.js +206 -0
- package/dist/weather.js.map +1 -0
- package/dist/windows-service.js +5 -5
- package/dist/windows-service.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +200 -0
- package/fm-helper/Info.plist +27 -0
- package/package.json +15 -13
- package/scripts/build-fm-helper.mjs +35 -3
- package/src/idotmatrix/identity_generated.py +50 -0
- package/src/idotmatrix/scan.py +33 -13
package/dist/windows-service.js
CHANGED
|
@@ -111,7 +111,7 @@ export function installWindowsTask() {
|
|
|
111
111
|
writeFileSync(tmpFile, '' + xml, 'utf16le');
|
|
112
112
|
try {
|
|
113
113
|
// /F overwrites an existing task (idempotent; mirrors macOS unload-before-load).
|
|
114
|
-
execSync(`schtasks /Create /TN "${TASK_NAME}" /XML "${tmpFile}" /F`, { stdio: 'pipe' });
|
|
114
|
+
execSync(`schtasks /Create /TN "${TASK_NAME}" /XML "${tmpFile}" /F`, { stdio: 'pipe', windowsHide: true });
|
|
115
115
|
}
|
|
116
116
|
finally {
|
|
117
117
|
try {
|
|
@@ -123,7 +123,7 @@ export function installWindowsTask() {
|
|
|
123
123
|
/** True if the AgentDeckDaemon scheduled task is registered. */
|
|
124
124
|
export function taskExists() {
|
|
125
125
|
try {
|
|
126
|
-
execSync(`schtasks /Query /TN "${TASK_NAME}"`, { stdio: 'pipe' });
|
|
126
|
+
execSync(`schtasks /Query /TN "${TASK_NAME}"`, { stdio: 'pipe', windowsHide: true });
|
|
127
127
|
return true;
|
|
128
128
|
}
|
|
129
129
|
catch {
|
|
@@ -132,14 +132,14 @@ export function taskExists() {
|
|
|
132
132
|
}
|
|
133
133
|
/** Start the registered task immediately (so no logout is required). */
|
|
134
134
|
export function runWindowsTask() {
|
|
135
|
-
execSync(`schtasks /Run /TN "${TASK_NAME}"`, { stdio: 'pipe' });
|
|
135
|
+
execSync(`schtasks /Run /TN "${TASK_NAME}"`, { stdio: 'pipe', windowsHide: true });
|
|
136
136
|
}
|
|
137
137
|
/** Stop a running task instance (best-effort; ignores "not running"). */
|
|
138
138
|
export function endWindowsTask() {
|
|
139
|
-
execSync(`schtasks /End /TN "${TASK_NAME}"`, { stdio: 'pipe' });
|
|
139
|
+
execSync(`schtasks /End /TN "${TASK_NAME}"`, { stdio: 'pipe', windowsHide: true });
|
|
140
140
|
}
|
|
141
141
|
/** Delete the registered task (/F suppresses the confirm prompt). */
|
|
142
142
|
export function deleteWindowsTask() {
|
|
143
|
-
execSync(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'pipe' });
|
|
143
|
+
execSync(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'pipe', windowsHide: true });
|
|
144
144
|
}
|
|
145
145
|
//# sourceMappingURL=windows-service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"windows-service.js","sourceRoot":"","sources":["../src/windows-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,CAAC,MAAM,SAAS,GAAG,iBAAiB,CAAC;AAE3C,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,OAAO,CAAC;SACL,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,+EAA+E;AAC/E,+EAA+E;AAC/E,8EAA8E;AAC9E,yDAAyD;AACzD,MAAM,UAAU,mBAAmB;IACjC,MAAM,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACxD,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACtC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACzD,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAuD;IAC3F,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,mBAAmB,EAAE,EAAE,GAAG,IAAI,EAAE,CAAC;IAC9D,MAAM,IAAI,GAAG,IAAI,EAAE,IAAI,IAAI,kBAAkB,EAAE,CAAC;IAChD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAChC,6EAA6E;IAC7E,6EAA6E;IAC7E,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,KAAK,6BAA6B,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAChC,+EAA+E;IAC/E,2EAA2E;IAC3E,0DAA0D;IAC1D,OAAO;;;;aAII,SAAS;;;;;gBAKN,OAAO;;;;;gBAKP,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6BN,OAAO;mBACL,IAAI;0BACG,SAAS,CAAC,UAAU,CAAC;;;QAGvC,CAAC;AACT,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,GAAG,GAAG,qBAAqB,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,kBAAkB,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAClF,yEAAyE;IACzE,aAAa,CAAC,OAAO,EAAE,GAAG,GAAG,GAAG,EAAE,SAAS,CAAC,CAAC;IAC7C,IAAI,CAAC;QACH,iFAAiF;QACjF,QAAQ,CAAC,yBAAyB,SAAS,WAAW,OAAO,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"windows-service.js","sourceRoot":"","sources":["../src/windows-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,6 +19,8 @@ struct HelperRequest: Decodable {
|
|
|
19
19
|
let text: String?
|
|
20
20
|
let voice: String?
|
|
21
21
|
let rate: Double?
|
|
22
|
+
/// `record`: hard cap on capture length; the reply arrives at stop or here.
|
|
23
|
+
let maxMs: Int?
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
@main
|
|
@@ -60,6 +62,20 @@ struct AgentDeckFMHelper {
|
|
|
60
62
|
return
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
if request.type == "record" {
|
|
66
|
+
// Recording spans an unknown PTT hold, and the stop arrives as a
|
|
67
|
+
// later stdin line — so this must not block the read loop.
|
|
68
|
+
Task.detached { await handleRecord(request) }
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if request.type == "record_stop" || request.type == "record_cancel" {
|
|
73
|
+
let stopped = activeRecording.finish(
|
|
74
|
+
cancelled: request.type == "record_cancel", reason: "stopped")
|
|
75
|
+
write(["id": request.id, "stopped": stopped])
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
guard let prompt = request.prompt, !prompt.isEmpty else {
|
|
64
80
|
write(["id": request.id, "error": "bad_request", "reason": "missing prompt"])
|
|
65
81
|
return
|
|
@@ -165,6 +181,111 @@ struct AgentDeckFMHelper {
|
|
|
165
181
|
}
|
|
166
182
|
}
|
|
167
183
|
|
|
184
|
+
// MARK: - Microphone capture
|
|
185
|
+
|
|
186
|
+
private static let activeRecording = RecordingBox()
|
|
187
|
+
|
|
188
|
+
/// Capture the host microphone into a 16 kHz mono PCM16 WAV. This is the
|
|
189
|
+
/// piece that retired the old Stream Deck Voice dial: capture used to mean
|
|
190
|
+
/// borrowing iTerm2's mic grant via AppleScript plus Homebrew `sox`. Here
|
|
191
|
+
/// the daemon's own bundled helper records, so the TCC grant belongs to
|
|
192
|
+
/// this process tree and there is nothing to install.
|
|
193
|
+
///
|
|
194
|
+
/// The reply is deferred: it is written when `record_stop`/`record_cancel`
|
|
195
|
+
/// arrives on stdin or `maxMs` elapses, whichever comes first.
|
|
196
|
+
private static func handleRecord(_ request: HelperRequest) async {
|
|
197
|
+
guard let outPath = request.wav, !outPath.isEmpty else {
|
|
198
|
+
write(["id": request.id, "error": "bad_request", "reason": "missing wav output path"])
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
let granted = await ensureMicAuthorization()
|
|
202
|
+
guard granted else {
|
|
203
|
+
write([
|
|
204
|
+
"id": request.id,
|
|
205
|
+
"error": "unauthorized",
|
|
206
|
+
"reason": "microphone access not granted — grant it in System Settings › Privacy & Security › Microphone",
|
|
207
|
+
])
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
let sampleRate = 16000.0
|
|
211
|
+
guard let outFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
|
|
212
|
+
sampleRate: sampleRate,
|
|
213
|
+
channels: 1,
|
|
214
|
+
interleaved: true) else {
|
|
215
|
+
write(["id": request.id, "error": "record_failed", "reason": "output format unavailable"])
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let engine = AVAudioEngine()
|
|
220
|
+
let input = engine.inputNode
|
|
221
|
+
let inFormat = input.outputFormat(forBus: 0)
|
|
222
|
+
guard inFormat.sampleRate > 0, inFormat.channelCount > 0 else {
|
|
223
|
+
write(["id": request.id, "error": "record_failed", "reason": "no audio input device"])
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
let sink = PCMSink()
|
|
227
|
+
guard activeRecording.begin(requestId: request.id, engine: engine, sink: sink) else {
|
|
228
|
+
write(["id": request.id, "error": "busy", "reason": "a recording is already in progress"])
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
input.installTap(onBus: 0, bufferSize: 4096, format: inFormat) { buffer, _ in
|
|
232
|
+
sink.append(buffer, to: outFormat)
|
|
233
|
+
}
|
|
234
|
+
do {
|
|
235
|
+
try engine.start()
|
|
236
|
+
} catch {
|
|
237
|
+
activeRecording.clear()
|
|
238
|
+
input.removeTap(onBus: 0)
|
|
239
|
+
write(["id": request.id, "error": "record_failed", "reason": String(describing: error)])
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let maxMs = max(1000, min(request.maxMs ?? 30_000, 120_000))
|
|
244
|
+
let outcome = await activeRecording.wait(maxMs: maxMs)
|
|
245
|
+
engine.stop()
|
|
246
|
+
input.removeTap(onBus: 0)
|
|
247
|
+
|
|
248
|
+
if outcome.cancelled {
|
|
249
|
+
write(["id": request.id, "cancelled": true])
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
if let failure = sink.failure {
|
|
253
|
+
write(["id": request.id, "error": "record_failed", "reason": failure])
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
let pcm = sink.pcm
|
|
257
|
+
guard pcm.count >= 2 else {
|
|
258
|
+
write(["id": request.id, "error": "record_failed", "reason": "no audio captured"])
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
do {
|
|
262
|
+
try wavData(pcm: pcm, sampleRate: Int(sampleRate)).write(to: URL(fileURLWithPath: outPath))
|
|
263
|
+
} catch {
|
|
264
|
+
write(["id": request.id, "error": "record_failed", "reason": String(describing: error)])
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
write([
|
|
268
|
+
"id": request.id,
|
|
269
|
+
"wav": outPath,
|
|
270
|
+
"sampleRate": Int(sampleRate),
|
|
271
|
+
"samples": pcm.count / 2,
|
|
272
|
+
"durationMs": Int(Double(pcm.count / 2) / sampleRate * 1000.0),
|
|
273
|
+
"stopReason": outcome.reason,
|
|
274
|
+
])
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private static func ensureMicAuthorization() async -> Bool {
|
|
278
|
+
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
|
279
|
+
case .authorized: return true
|
|
280
|
+
case .notDetermined:
|
|
281
|
+
return await withCheckedContinuation { continuation in
|
|
282
|
+
let box = ResumeOnce(continuation)
|
|
283
|
+
AVCaptureDevice.requestAccess(for: .audio) { box.resume($0) }
|
|
284
|
+
}
|
|
285
|
+
default: return false
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
168
289
|
// MARK: - Text to speech
|
|
169
290
|
|
|
170
291
|
/// Speak a reply through the host's audio output. Kept in the same helper
|
|
@@ -312,13 +433,92 @@ struct AgentDeckFMHelper {
|
|
|
312
433
|
#endif
|
|
313
434
|
}
|
|
314
435
|
|
|
436
|
+
/// stdout is shared by the serial request loop and the detached record
|
|
437
|
+
/// task; the lock keeps two replies from interleaving inside one line.
|
|
438
|
+
private static let writeLock = NSLock()
|
|
439
|
+
|
|
315
440
|
private static func write(_ object: [String: Any]) {
|
|
316
441
|
guard JSONSerialization.isValidJSONObject(object),
|
|
317
442
|
let data = try? JSONSerialization.data(withJSONObject: object),
|
|
318
443
|
let text = String(data: data, encoding: .utf8) else {
|
|
319
444
|
return
|
|
320
445
|
}
|
|
446
|
+
writeLock.lock()
|
|
321
447
|
FileHandle.standardOutput.write(Data((text + "\n").utf8))
|
|
448
|
+
writeLock.unlock()
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// State for the single in-flight microphone capture. One at a time by design:
|
|
453
|
+
/// the daemon models push-to-talk, and two overlapping engine taps on the same
|
|
454
|
+
/// input device produce garbage anyway.
|
|
455
|
+
private final class RecordingBox: @unchecked Sendable {
|
|
456
|
+
struct Outcome { let cancelled: Bool; let reason: String }
|
|
457
|
+
|
|
458
|
+
private let lock = NSLock()
|
|
459
|
+
private var requestId: Int?
|
|
460
|
+
private var engine: AVAudioEngine?
|
|
461
|
+
private var sink: PCMSink?
|
|
462
|
+
private var continuation: ResumeOnce<Outcome>?
|
|
463
|
+
/// Bumped per recording so a stale max-duration timer from a finished
|
|
464
|
+
/// capture cannot terminate the one that started after it.
|
|
465
|
+
private var generation = 0
|
|
466
|
+
|
|
467
|
+
func begin(requestId: Int, engine: AVAudioEngine, sink: PCMSink) -> Bool {
|
|
468
|
+
lock.lock(); defer { lock.unlock() }
|
|
469
|
+
guard self.requestId == nil else { return false }
|
|
470
|
+
self.requestId = requestId
|
|
471
|
+
self.engine = engine
|
|
472
|
+
self.sink = sink
|
|
473
|
+
generation += 1
|
|
474
|
+
return true
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func wait(maxMs: Int) async -> Outcome {
|
|
478
|
+
let gen = currentGeneration()
|
|
479
|
+
let outcome: Outcome = await withCheckedContinuation { cont in
|
|
480
|
+
arm(ResumeOnce(cont))
|
|
481
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(maxMs)) { [weak self] in
|
|
482
|
+
guard let self, self.isLive(generation: gen) else { return }
|
|
483
|
+
_ = self.finish(cancelled: false, reason: "max_duration")
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return outcome
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private func currentGeneration() -> Int {
|
|
490
|
+
lock.lock(); defer { lock.unlock() }
|
|
491
|
+
return generation
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private func arm(_ box: ResumeOnce<Outcome>) {
|
|
495
|
+
lock.lock(); defer { lock.unlock() }
|
|
496
|
+
continuation = box
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private func isLive(generation gen: Int) -> Bool {
|
|
500
|
+
lock.lock(); defer { lock.unlock() }
|
|
501
|
+
return generation == gen && requestId != nil
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// Returns false when no recording is active — the stop raced a max-length
|
|
505
|
+
/// finish or arrived with nothing running.
|
|
506
|
+
func finish(cancelled: Bool, reason: String) -> Bool {
|
|
507
|
+
lock.lock()
|
|
508
|
+
let cont = continuation
|
|
509
|
+
let active = requestId != nil
|
|
510
|
+
continuation = nil
|
|
511
|
+
requestId = nil
|
|
512
|
+
engine = nil
|
|
513
|
+
sink = nil
|
|
514
|
+
lock.unlock()
|
|
515
|
+
guard active, let cont else { return false }
|
|
516
|
+
cont.resume(Outcome(cancelled: cancelled, reason: cancelled ? "cancelled" : reason))
|
|
517
|
+
return true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
func clear() {
|
|
521
|
+
_ = finish(cancelled: true, reason: "cleared")
|
|
322
522
|
}
|
|
323
523
|
}
|
|
324
524
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<!--
|
|
4
|
+
Embedded (via `-sectcreate __TEXT __info_plist`) into the single-file
|
|
5
|
+
agentdeck-fm-helper binary, because TCC refuses to *prompt* for a
|
|
6
|
+
privacy-sensitive service unless the requesting code carries a usage
|
|
7
|
+
description — and "refuses" means SIGABRT, not an error return. Without this
|
|
8
|
+
the helper dies the first time it touches speech recognition or the mic on any
|
|
9
|
+
machine whose grant is not already on record, which is exactly what happened
|
|
10
|
+
once a rebuild changed the binary's cdhash and invalidated the old grant.
|
|
11
|
+
|
|
12
|
+
The identifier must match the `codesign --identifier` in
|
|
13
|
+
scripts/build-fm-helper.mjs: TCC keys an ad-hoc signed binary's grant on that
|
|
14
|
+
identifier, and a mismatch reads as a different app.
|
|
15
|
+
-->
|
|
16
|
+
<plist version="1.0">
|
|
17
|
+
<dict>
|
|
18
|
+
<key>CFBundleIdentifier</key>
|
|
19
|
+
<string>bound.serendipity.agentdeck.fm-helper</string>
|
|
20
|
+
<key>CFBundleName</key>
|
|
21
|
+
<string>AgentDeck Voice Helper</string>
|
|
22
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
23
|
+
<string>AgentDeck records a short push-to-talk clip so your spoken instruction can be transcribed on this Mac and sent to your coding agent.</string>
|
|
24
|
+
<key>NSSpeechRecognitionUsageDescription</key>
|
|
25
|
+
<string>AgentDeck transcribes your push-to-talk clip with Apple's on-device recognizer so your spoken instruction can be sent to your coding agent. The audio never leaves this Mac.</string>
|
|
26
|
+
</dict>
|
|
27
|
+
</plist>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentdeck/bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,16 +24,26 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"agentdeck": "./dist/cli.js"
|
|
26
26
|
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"build:fm-helper": "node scripts/build-fm-helper.mjs",
|
|
30
|
+
"prepack": "node scripts/build-fm-helper.mjs",
|
|
31
|
+
"dev": "tsc --watch",
|
|
32
|
+
"clean": "rm -rf dist",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"start": "node dist/cli.js claude"
|
|
35
|
+
},
|
|
27
36
|
"dependencies": {
|
|
37
|
+
"@agentdeck/hooks": "workspace:*",
|
|
38
|
+
"@agentdeck/shared": "workspace:*",
|
|
28
39
|
"@anthropic-ai/sdk": "^0.111.0",
|
|
29
40
|
"bonjour-service": "^1.3.0",
|
|
30
41
|
"commander": "^14.0.3",
|
|
31
42
|
"express": "^5.2.1",
|
|
32
43
|
"qrcode": "^1.5.4",
|
|
44
|
+
"sharp": "^0.34.5",
|
|
33
45
|
"strip-ansi": "^7.2.0",
|
|
34
|
-
"ws": "^8.20.0"
|
|
35
|
-
"@agentdeck/hooks": "1.0.4",
|
|
36
|
-
"@agentdeck/shared": "1.0.4"
|
|
46
|
+
"ws": "^8.20.0"
|
|
37
47
|
},
|
|
38
48
|
"optionalDependencies": {
|
|
39
49
|
"@picovoice/porcupine-node": "^4.0.2",
|
|
@@ -49,13 +59,5 @@
|
|
|
49
59
|
"@types/qrcode": "^1.5.6",
|
|
50
60
|
"@types/ws": "^8.18.1",
|
|
51
61
|
"typescript": "^6.0.3"
|
|
52
|
-
},
|
|
53
|
-
"scripts": {
|
|
54
|
-
"build": "tsc",
|
|
55
|
-
"build:fm-helper": "node scripts/build-fm-helper.mjs",
|
|
56
|
-
"dev": "tsc --watch",
|
|
57
|
-
"clean": "rm -rf dist",
|
|
58
|
-
"typecheck": "tsc --noEmit",
|
|
59
|
-
"start": "node dist/cli.js claude"
|
|
60
62
|
}
|
|
61
|
-
}
|
|
63
|
+
}
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { chmodSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
7
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
8
|
const source = join(root, 'fm-helper', 'AgentDeckFMHelper.swift');
|
|
9
|
-
const
|
|
9
|
+
const plist = join(root, 'fm-helper', 'Info.plist');
|
|
10
|
+
const defaultOut = join(root, 'assets', 'fm-helper', 'agentdeck-fm-helper');
|
|
10
11
|
const arch = process.arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
11
12
|
const target = `${arch}-apple-macos26.0`;
|
|
13
|
+
/** Must match CFBundleIdentifier in fm-helper/Info.plist — see the note there. */
|
|
14
|
+
const signingIdentifier = 'bound.serendipity.agentdeck.fm-helper';
|
|
15
|
+
|
|
16
|
+
// The daemon's source-compiled fallback (foundation-models-helper.ts) runs this
|
|
17
|
+
// same script with --out so the plist embedding and the signature can't drift
|
|
18
|
+
// between the packaged binary and a locally rebuilt one.
|
|
19
|
+
const outFlag = process.argv.indexOf('--out');
|
|
20
|
+
const out = outFlag >= 0 ? process.argv[outFlag + 1] : defaultOut;
|
|
21
|
+
if (outFlag >= 0 && !out) {
|
|
22
|
+
console.error('[fm-helper] --out requires a path');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
if (process.platform !== 'darwin') {
|
|
14
27
|
console.log('[fm-helper] skipped: darwin only');
|
|
@@ -21,6 +34,25 @@ const swiftc = execFileSync('/usr/bin/xcrun', ['--find', 'swiftc'], { encoding:
|
|
|
21
34
|
// stdlib can't be found ("unable to load standard library for target ..."). Pass
|
|
22
35
|
// the SDK path explicitly so the build is reproducible from the npm lifecycle.
|
|
23
36
|
const sdk = execFileSync('/usr/bin/xcrun', ['--sdk', 'macosx', '--show-sdk-path'], { encoding: 'utf8' }).trim();
|
|
24
|
-
|
|
37
|
+
const args = ['-parse-as-library', '-sdk', sdk, '-target', target, source, '-o', out];
|
|
38
|
+
// A single-file tool has no bundle, so its usage descriptions have to live in an
|
|
39
|
+
// __info_plist section: TCC aborts (SIGABRT, not an error return) the moment it
|
|
40
|
+
// needs to prompt for the mic or speech recognition without one.
|
|
41
|
+
if (existsSync(plist)) {
|
|
42
|
+
args.push('-Xlinker', '-sectcreate', '-Xlinker', '__TEXT', '-Xlinker', '__info_plist', '-Xlinker', plist);
|
|
43
|
+
} else {
|
|
44
|
+
console.warn(`[fm-helper] WARNING: ${plist} missing — voice capture will crash under TCC`);
|
|
45
|
+
}
|
|
46
|
+
execFileSync(swiftc, args, { stdio: 'inherit' });
|
|
25
47
|
chmodSync(out, 0o755);
|
|
48
|
+
// Ad-hoc signing gives TCC a stable identity to hang the grant on. It is not
|
|
49
|
+
// about trust: an unsigned binary's grant is keyed on the raw cdhash alone, so
|
|
50
|
+
// the prompt cannot name the helper and the user sees a nameless request.
|
|
51
|
+
try {
|
|
52
|
+
execFileSync('/usr/bin/codesign', [
|
|
53
|
+
'--force', '--sign', '-', '--identifier', signingIdentifier, out,
|
|
54
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.warn(`[fm-helper] WARNING: ad-hoc codesign failed: ${String(err).slice(0, 160)}`);
|
|
57
|
+
}
|
|
26
58
|
console.log(`[fm-helper] built ${out}`);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# GENERATED FILE — DO NOT EDIT.
|
|
2
|
+
# Source of truth: shared/src/idotmatrix-identity.ts
|
|
3
|
+
# Regenerate: pnpm generate-idotmatrix-identity (drift gated by shared/src/__tests__/idotmatrix-identity.test.ts)
|
|
4
|
+
"""Brand-independent identity of an iDotMatrix-protocol BLE panel.
|
|
5
|
+
|
|
6
|
+
The same hardware advertises as 'IDM-...' (iDotMatrix) or 'iPixel-...', so a
|
|
7
|
+
scan filters on the advertised service first and only falls back to the name
|
|
8
|
+
families.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
IDOTMATRIX_SERVICE_UUID = "000000fa-0000-1000-8000-00805f9b34fb"
|
|
12
|
+
IDOTMATRIX_WRITE_CHARACTERISTIC_UUID = "0000fa02-0000-1000-8000-00805f9b34fb"
|
|
13
|
+
IDOTMATRIX_NAME_PREFIXES = ["IDM-", "IPIXEL"]
|
|
14
|
+
|
|
15
|
+
_HEX = set("0123456789abcdef")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_uuid(value):
|
|
19
|
+
"""Expand a 16-bit ('fa02') or 32-bit ('000000fa') UUID to Bluetooth-base form.
|
|
20
|
+
|
|
21
|
+
Short forms are left-padded first: 'fa' and '00fa' are the same service.
|
|
22
|
+
"""
|
|
23
|
+
s = (value or "").strip().lower()
|
|
24
|
+
if not s or not set(s) <= _HEX:
|
|
25
|
+
return s
|
|
26
|
+
if len(s) <= 4:
|
|
27
|
+
return "0000" + s.rjust(4, "0") + "-0000-1000-8000-00805f9b34fb"
|
|
28
|
+
if len(s) <= 8:
|
|
29
|
+
return s.rjust(8, "0") + "-0000-1000-8000-00805f9b34fb"
|
|
30
|
+
return s
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def matches_name(name, extra_prefixes=()):
|
|
34
|
+
"""Whether an advertised local name belongs to a known panel family."""
|
|
35
|
+
n = (name or "").strip().upper()
|
|
36
|
+
if not n:
|
|
37
|
+
return False
|
|
38
|
+
for prefix in list(IDOTMATRIX_NAME_PREFIXES) + list(extra_prefixes or ()):
|
|
39
|
+
p = (prefix or "").strip().upper()
|
|
40
|
+
if p and n.startswith(p):
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def matches_advertisement(name, service_uuids, extra_prefixes=()):
|
|
46
|
+
"""The discovery predicate: service UUID first, name families second."""
|
|
47
|
+
for uuid in service_uuids or ():
|
|
48
|
+
if normalize_uuid(uuid) == IDOTMATRIX_SERVICE_UUID:
|
|
49
|
+
return True
|
|
50
|
+
return matches_name(name, extra_prefixes)
|
package/src/idotmatrix/scan.py
CHANGED
|
@@ -1,35 +1,55 @@
|
|
|
1
|
+
"""BLE scan for iDotMatrix-protocol panels (terminal-managed daemon + CLI).
|
|
2
|
+
|
|
3
|
+
Reports every peripheral it sees, flagging the ones that speak the iDotMatrix
|
|
4
|
+
protocol. Identification is brand-independent — advertised service UUID first,
|
|
5
|
+
known name families second — because the same hardware ships as 'IDM-...'
|
|
6
|
+
(iDotMatrix) and 'iPixel-...'. The rule lives in identity_generated.py, mirrored
|
|
7
|
+
from shared/src/idotmatrix-identity.ts; the Swift CoreBluetooth scanner in the
|
|
8
|
+
App Store daemon applies the identical predicate.
|
|
9
|
+
|
|
10
|
+
Extra name prefixes may be passed as argv (the daemon forwards
|
|
11
|
+
`idotmatrixNamePrefixes` from settings.json) so a further rebrand is a config
|
|
12
|
+
change rather than a release.
|
|
13
|
+
"""
|
|
14
|
+
|
|
1
15
|
import asyncio
|
|
2
16
|
import json
|
|
3
17
|
import sys
|
|
4
18
|
from bleak import BleakScanner
|
|
5
19
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
from identity_generated import matches_advertisement
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def scan(extra_prefixes):
|
|
24
|
+
# Scan for 5 seconds. return_adv gives the advertisement payload, which is
|
|
25
|
+
# where the service UUIDs live — peripheral.name alone cannot identify a
|
|
26
|
+
# panel that advertises under an unknown brand name.
|
|
27
|
+
discovered = await BleakScanner.discover(timeout=5.0, return_adv=True)
|
|
28
|
+
|
|
10
29
|
results = []
|
|
11
|
-
for
|
|
12
|
-
name =
|
|
13
|
-
|
|
14
|
-
is_idm = name.startswith("IDM-")
|
|
30
|
+
for device, adv in discovered.values():
|
|
31
|
+
name = adv.local_name or device.name or "Unknown"
|
|
32
|
+
service_uuids = list(adv.service_uuids or ())
|
|
15
33
|
results.append({
|
|
16
34
|
"name": name,
|
|
17
|
-
"address":
|
|
18
|
-
"is_idotmatrix":
|
|
35
|
+
"address": device.address,
|
|
36
|
+
"is_idotmatrix": matches_advertisement(name, service_uuids, extra_prefixes),
|
|
19
37
|
})
|
|
20
|
-
|
|
38
|
+
|
|
21
39
|
# Sort so that iDotMatrix devices are on top
|
|
22
40
|
results.sort(key=lambda x: x["is_idotmatrix"], reverse=True)
|
|
23
|
-
|
|
41
|
+
|
|
24
42
|
# Print as JSON so the calling Node.js CLI can easily parse it
|
|
25
43
|
print(json.dumps(results))
|
|
26
44
|
|
|
45
|
+
|
|
27
46
|
def main():
|
|
28
47
|
try:
|
|
29
|
-
asyncio.run(scan())
|
|
48
|
+
asyncio.run(scan(sys.argv[1:]))
|
|
30
49
|
except Exception as e:
|
|
31
50
|
print(json.dumps({"error": str(e)}))
|
|
32
51
|
sys.exit(1)
|
|
33
52
|
|
|
53
|
+
|
|
34
54
|
if __name__ == "__main__":
|
|
35
55
|
main()
|