@agentdeck/bridge 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/dist/apme/collector.d.ts.map +1 -1
  3. package/dist/apme/collector.js +5 -2
  4. package/dist/apme/collector.js.map +1 -1
  5. package/dist/apme/runner.d.ts +11 -0
  6. package/dist/apme/runner.d.ts.map +1 -1
  7. package/dist/apme/runner.js +60 -4
  8. package/dist/apme/runner.js.map +1 -1
  9. package/dist/apme/settings.d.ts +10 -0
  10. package/dist/apme/settings.d.ts.map +1 -1
  11. package/dist/apme/settings.js +14 -0
  12. package/dist/apme/settings.js.map +1 -1
  13. package/dist/awaiting-overlay.d.ts +44 -10
  14. package/dist/awaiting-overlay.d.ts.map +1 -1
  15. package/dist/awaiting-overlay.js +96 -28
  16. package/dist/awaiting-overlay.js.map +1 -1
  17. package/dist/bridge-core.d.ts +10 -0
  18. package/dist/bridge-core.d.ts.map +1 -1
  19. package/dist/bridge-core.js +47 -12
  20. package/dist/bridge-core.js.map +1 -1
  21. package/dist/card-feed.d.ts +107 -0
  22. package/dist/card-feed.d.ts.map +1 -0
  23. package/dist/card-feed.js +277 -0
  24. package/dist/card-feed.js.map +1 -0
  25. package/dist/card-modules.d.ts +87 -0
  26. package/dist/card-modules.d.ts.map +1 -0
  27. package/dist/card-modules.js +179 -0
  28. package/dist/card-modules.js.map +1 -0
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +93 -0
  31. package/dist/cli.js.map +1 -1
  32. package/dist/daemon-server.d.ts.map +1 -1
  33. package/dist/daemon-server.js +1025 -50
  34. package/dist/daemon-server.js.map +1 -1
  35. package/dist/daemon-ws-client.d.ts +2 -1
  36. package/dist/daemon-ws-client.d.ts.map +1 -1
  37. package/dist/daemon-ws-client.js +2 -1
  38. package/dist/daemon-ws-client.js.map +1 -1
  39. package/dist/daemon.js +1 -1
  40. package/dist/device-photo.d.ts +86 -0
  41. package/dist/device-photo.d.ts.map +1 -0
  42. package/dist/device-photo.js +200 -0
  43. package/dist/device-photo.js.map +1 -0
  44. package/dist/device-voice-reply.d.ts +124 -0
  45. package/dist/device-voice-reply.d.ts.map +1 -0
  46. package/dist/device-voice-reply.js +236 -0
  47. package/dist/device-voice-reply.js.map +1 -0
  48. package/dist/device-voice.d.ts +56 -0
  49. package/dist/device-voice.d.ts.map +1 -0
  50. package/dist/device-voice.js +134 -0
  51. package/dist/device-voice.js.map +1 -0
  52. package/dist/esp32-serial.d.ts +40 -0
  53. package/dist/esp32-serial.d.ts.map +1 -1
  54. package/dist/esp32-serial.js +109 -13
  55. package/dist/esp32-serial.js.map +1 -1
  56. package/dist/foundation-models-helper.d.ts +30 -0
  57. package/dist/foundation-models-helper.d.ts.map +1 -1
  58. package/dist/foundation-models-helper.js +45 -0
  59. package/dist/foundation-models-helper.js.map +1 -1
  60. package/dist/index.js +2 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/mdns.d.ts +11 -0
  63. package/dist/mdns.d.ts.map +1 -1
  64. package/dist/mdns.js +13 -0
  65. package/dist/mdns.js.map +1 -1
  66. package/dist/observed-inject.d.ts +119 -0
  67. package/dist/observed-inject.d.ts.map +1 -0
  68. package/dist/observed-inject.js +405 -0
  69. package/dist/observed-inject.js.map +1 -0
  70. package/dist/passive-observer.d.ts +14 -0
  71. package/dist/passive-observer.d.ts.map +1 -1
  72. package/dist/passive-observer.js +35 -5
  73. package/dist/passive-observer.js.map +1 -1
  74. package/dist/peripheral-mapping.d.ts +65 -0
  75. package/dist/peripheral-mapping.d.ts.map +1 -0
  76. package/dist/peripheral-mapping.js +115 -0
  77. package/dist/peripheral-mapping.js.map +1 -0
  78. package/dist/session-aggregator.d.ts +4 -1
  79. package/dist/session-aggregator.d.ts.map +1 -1
  80. package/dist/session-aggregator.js +5 -5
  81. package/dist/session-aggregator.js.map +1 -1
  82. package/dist/session-id-resolve.d.ts +17 -0
  83. package/dist/session-id-resolve.d.ts.map +1 -0
  84. package/dist/session-id-resolve.js +35 -0
  85. package/dist/session-id-resolve.js.map +1 -0
  86. package/dist/session-registry.d.ts +12 -0
  87. package/dist/session-registry.d.ts.map +1 -1
  88. package/dist/session-registry.js +31 -0
  89. package/dist/session-registry.js.map +1 -1
  90. package/dist/session-transcript-timeline.d.ts +7 -0
  91. package/dist/session-transcript-timeline.d.ts.map +1 -1
  92. package/dist/session-transcript-timeline.js +16 -6
  93. package/dist/session-transcript-timeline.js.map +1 -1
  94. package/dist/timeline-store.js +2 -2
  95. package/dist/timeline-store.js.map +1 -1
  96. package/dist/usage-event.d.ts.map +1 -1
  97. package/dist/usage-event.js +14 -7
  98. package/dist/usage-event.js.map +1 -1
  99. package/dist/voice.d.ts +13 -10
  100. package/dist/voice.d.ts.map +1 -1
  101. package/dist/voice.js +26 -272
  102. package/dist/voice.js.map +1 -1
  103. package/dist/ws-server.d.ts +14 -0
  104. package/dist/ws-server.d.ts.map +1 -1
  105. package/dist/ws-server.js +66 -1
  106. package/dist/ws-server.js.map +1 -1
  107. package/fm-helper/AgentDeckFMHelper.swift +303 -0
  108. package/package.json +3 -3
  109. package/dist/d200h/hid-protocol.d.ts +0 -62
  110. package/dist/d200h/hid-protocol.d.ts.map +0 -1
  111. package/dist/d200h/hid-protocol.js +0 -124
  112. package/dist/d200h/hid-protocol.js.map +0 -1
  113. package/dist/d200h/image-renderer.d.ts +0 -24
  114. package/dist/d200h/image-renderer.d.ts.map +0 -1
  115. package/dist/d200h/image-renderer.js +0 -321
  116. package/dist/d200h/image-renderer.js.map +0 -1
  117. package/dist/hook-migration.d.ts +0 -2
  118. package/dist/hook-migration.d.ts.map +0 -1
  119. package/dist/hook-migration.js +0 -104
  120. package/dist/hook-migration.js.map +0 -1
  121. package/dist/modules/d200h-module.d.ts +0 -65
  122. package/dist/modules/d200h-module.d.ts.map +0 -1
  123. package/dist/modules/d200h-module.js +0 -495
  124. package/dist/modules/d200h-module.js.map +0 -1
  125. package/dist/modules/d200h-renderer.d.ts +0 -7
  126. package/dist/modules/d200h-renderer.d.ts.map +0 -1
  127. package/dist/modules/d200h-renderer.js +0 -321
  128. package/dist/modules/d200h-renderer.js.map +0 -1
  129. package/dist/modules/trmnl-module.d.ts +0 -38
  130. package/dist/modules/trmnl-module.d.ts.map +0 -1
  131. package/dist/modules/trmnl-module.js +0 -111
  132. package/dist/modules/trmnl-module.js.map +0 -1
  133. package/dist/trmnl/byos-server.d.ts +0 -39
  134. package/dist/trmnl/byos-server.d.ts.map +0 -1
  135. package/dist/trmnl/byos-server.js +0 -301
  136. package/dist/trmnl/byos-server.js.map +0 -1
  137. package/dist/trmnl/frame-cache.d.ts +0 -37
  138. package/dist/trmnl/frame-cache.d.ts.map +0 -1
  139. package/dist/trmnl/frame-cache.js +0 -157
  140. package/dist/trmnl/frame-cache.js.map +0 -1
  141. package/dist/trmnl/image-renderer.d.ts +0 -27
  142. package/dist/trmnl/image-renderer.d.ts.map +0 -1
  143. package/dist/trmnl/image-renderer.js +0 -235
  144. package/dist/trmnl/image-renderer.js.map +0 -1
  145. package/dist/trmnl/trmnl-settings.d.ts +0 -96
  146. package/dist/trmnl/trmnl-settings.d.ts.map +0 -1
  147. package/dist/trmnl/trmnl-settings.js +0 -177
  148. package/dist/trmnl/trmnl-settings.js.map +0 -1
  149. package/dist/trmnl/trmnl-telemetry.d.ts +0 -47
  150. package/dist/trmnl/trmnl-telemetry.d.ts.map +0 -1
  151. package/dist/trmnl/trmnl-telemetry.js +0 -56
  152. package/dist/trmnl/trmnl-telemetry.js.map +0 -1
  153. package/dist/whisper-server-manager.d.ts +0 -19
  154. package/dist/whisper-server-manager.d.ts.map +0 -1
  155. package/dist/whisper-server-manager.js +0 -171
  156. package/dist/whisper-server-manager.js.map +0 -1
@@ -1,4 +1,6 @@
1
1
  import Foundation
2
+ import AVFoundation
3
+ import Speech
2
4
  #if canImport(FoundationModels)
3
5
  import FoundationModels
4
6
  #endif
@@ -9,6 +11,14 @@ struct HelperRequest: Decodable {
9
11
  let prompt: String?
10
12
  let instructions: String?
11
13
  let temperature: Double?
14
+ /// `transcribe`: absolute path to a WAV file the daemon already wrote.
15
+ let wav: String?
16
+ /// BCP-47 locale hint, e.g. "ko-KR". Falls back to the current locale then en-US.
17
+ let locale: String?
18
+ /// `speak`: text to synthesize, with optional voice/rate overrides.
19
+ let text: String?
20
+ let voice: String?
21
+ let rate: Double?
12
22
  }
13
23
 
14
24
  @main
@@ -35,6 +45,21 @@ struct AgentDeckFMHelper {
35
45
  return
36
46
  }
37
47
 
48
+ if request.type == "transcribe" {
49
+ await handleTranscribe(request)
50
+ return
51
+ }
52
+
53
+ if request.type == "speak" {
54
+ await handleSpeak(request)
55
+ return
56
+ }
57
+
58
+ if request.type == "synthesize" {
59
+ await handleSynthesize(request)
60
+ return
61
+ }
62
+
38
63
  guard let prompt = request.prompt, !prompt.isEmpty else {
39
64
  write(["id": request.id, "error": "bad_request", "reason": "missing prompt"])
40
65
  return
@@ -69,6 +94,192 @@ struct AgentDeckFMHelper {
69
94
  #endif
70
95
  }
71
96
 
97
+ // MARK: - Speech to text
98
+
99
+ /// On-device transcription of a WAV the daemon already captured (from the
100
+ /// host mic or streamed off a board). `requiresOnDeviceRecognition` keeps
101
+ /// audio local — the captured speech routinely contains project and code
102
+ /// names that must not reach Apple's servers. Mirrors the Swift daemon's
103
+ /// `VoiceSpeechTranscriber` so both daemons transcribe identically.
104
+ private static func handleTranscribe(_ request: HelperRequest) async {
105
+ guard let path = request.wav, !path.isEmpty else {
106
+ write(["id": request.id, "error": "bad_request", "reason": "missing wav path"])
107
+ return
108
+ }
109
+ guard FileManager.default.fileExists(atPath: path) else {
110
+ write(["id": request.id, "error": "bad_request", "reason": "wav not found: \(path)"])
111
+ return
112
+ }
113
+
114
+ let status = await ensureSpeechAuthorization()
115
+ guard status == .authorized else {
116
+ write([
117
+ "id": request.id,
118
+ "error": "unauthorized",
119
+ "reason": "speech recognition not authorized (\(status.rawValue)) — grant it in System Settings › Privacy & Security › Speech Recognition",
120
+ ])
121
+ return
122
+ }
123
+
124
+ var locales: [Locale] = []
125
+ if let tag = request.locale, !tag.isEmpty { locales.append(Locale(identifier: tag)) }
126
+ locales.append(Locale.current)
127
+ locales.append(Locale(identifier: "en-US"))
128
+
129
+ guard let recognizer = locales.lazy.compactMap({ SFSpeechRecognizer(locale: $0) }).first(where: { $0.isAvailable }) else {
130
+ write([
131
+ "id": request.id,
132
+ "error": "unavailable",
133
+ "reason": "no available on-device recognizer — the dictation model may still be downloading",
134
+ ])
135
+ return
136
+ }
137
+
138
+ let speechRequest = SFSpeechURLRecognitionRequest(url: URL(fileURLWithPath: path))
139
+ speechRequest.shouldReportPartialResults = false
140
+ speechRequest.requiresOnDeviceRecognition = true
141
+ speechRequest.taskHint = .dictation
142
+
143
+ let result: [String: Any] = await withCheckedContinuation { continuation in
144
+ let box = ResumeOnce(continuation)
145
+ _ = recognizer.recognitionTask(with: speechRequest) { result, error in
146
+ if let error {
147
+ box.resume(["id": request.id, "error": "speech_error", "reason": String(describing: error)])
148
+ return
149
+ }
150
+ guard let result, result.isFinal else { return }
151
+ let text = result.bestTranscription.formattedString
152
+ .trimmingCharacters(in: .whitespacesAndNewlines)
153
+ box.resume(["id": request.id, "text": text])
154
+ }
155
+ }
156
+ write(result)
157
+ }
158
+
159
+ private static func ensureSpeechAuthorization() async -> SFSpeechRecognizerAuthorizationStatus {
160
+ let current = SFSpeechRecognizer.authorizationStatus()
161
+ guard current == .notDetermined else { return current }
162
+ return await withCheckedContinuation { continuation in
163
+ let box = ResumeOnce(continuation)
164
+ SFSpeechRecognizer.requestAuthorization { box.resume($0) }
165
+ }
166
+ }
167
+
168
+ // MARK: - Text to speech
169
+
170
+ /// Speak a reply through the host's audio output. Kept in the same helper
171
+ /// so the voice round trip needs exactly one bundled binary.
172
+ private static func handleSpeak(_ request: HelperRequest) async {
173
+ guard let text = request.text, !text.isEmpty else {
174
+ write(["id": request.id, "error": "bad_request", "reason": "missing text"])
175
+ return
176
+ }
177
+ let utterance = AVSpeechUtterance(string: text)
178
+ if let voiceId = request.voice, let v = AVSpeechSynthesisVoice(identifier: voiceId) {
179
+ utterance.voice = v
180
+ } else if let tag = request.locale, let v = AVSpeechSynthesisVoice(language: tag) {
181
+ utterance.voice = v
182
+ }
183
+ if let rate = request.rate { utterance.rate = Float(rate) }
184
+
185
+ let synthesizer = AVSpeechSynthesizer()
186
+ let delegate = SpeakDelegate()
187
+ synthesizer.delegate = delegate
188
+ await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
189
+ delegate.onFinish = ResumeOnce(continuation)
190
+ synthesizer.speak(utterance)
191
+ }
192
+ write(["id": request.id, "spoken": true])
193
+ }
194
+
195
+ /// Synthesize to a WAV file instead of the host's speakers, so the daemon
196
+ /// can stream the audio somewhere else — a battery-powered board holding
197
+ /// the only speaker the user is near. Fixed at 16 kHz mono PCM16: that is
198
+ /// what the boards' I2S amplifiers are configured for and what keeps a
199
+ /// spoken reply inside a WiFi budget an ESP32 can actually drain.
200
+ private static func handleSynthesize(_ request: HelperRequest) async {
201
+ guard let text = request.text, !text.isEmpty else {
202
+ write(["id": request.id, "error": "bad_request", "reason": "missing text"])
203
+ return
204
+ }
205
+ guard let outPath = request.wav, !outPath.isEmpty else {
206
+ write(["id": request.id, "error": "bad_request", "reason": "missing wav output path"])
207
+ return
208
+ }
209
+ let sampleRate = 16000.0
210
+ guard let outFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
211
+ sampleRate: sampleRate,
212
+ channels: 1,
213
+ interleaved: true) else {
214
+ write(["id": request.id, "error": "synthesis_failed", "reason": "output format unavailable"])
215
+ return
216
+ }
217
+
218
+ let utterance = AVSpeechUtterance(string: text)
219
+ if let voiceId = request.voice, let v = AVSpeechSynthesisVoice(identifier: voiceId) {
220
+ utterance.voice = v
221
+ } else if let tag = request.locale, let v = AVSpeechSynthesisVoice(language: tag) {
222
+ utterance.voice = v
223
+ }
224
+ if let rate = request.rate { utterance.rate = Float(rate) }
225
+
226
+ // The write callback is not documented to run on any particular queue,
227
+ // and it appends to shared state, so the accumulator owns a lock.
228
+ let sink = PCMSink()
229
+ let synthesizer = AVSpeechSynthesizer()
230
+ await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
231
+ let done = ResumeOnce(continuation)
232
+ synthesizer.write(utterance) { buffer in
233
+ guard let input = buffer as? AVAudioPCMBuffer else { return }
234
+ // A zero-length buffer is the documented end-of-synthesis
235
+ // signal; without it this call never completes.
236
+ if input.frameLength == 0 {
237
+ done.resume(())
238
+ return
239
+ }
240
+ sink.append(input, to: outFormat)
241
+ }
242
+ }
243
+
244
+ if let failure = sink.failure {
245
+ write(["id": request.id, "error": "synthesis_failed", "reason": failure])
246
+ return
247
+ }
248
+ let pcm = sink.pcm
249
+ guard !pcm.isEmpty else {
250
+ write(["id": request.id, "error": "synthesis_failed", "reason": "no audio produced"])
251
+ return
252
+ }
253
+ do {
254
+ try wavData(pcm: pcm, sampleRate: Int(sampleRate)).write(to: URL(fileURLWithPath: outPath))
255
+ } catch {
256
+ write(["id": request.id, "error": "synthesis_failed", "reason": String(describing: error)])
257
+ return
258
+ }
259
+ write([
260
+ "id": request.id,
261
+ "wav": outPath,
262
+ "sampleRate": Int(sampleRate),
263
+ "samples": pcm.count / 2,
264
+ "durationMs": Int(Double(pcm.count / 2) / sampleRate * 1000.0),
265
+ ])
266
+ }
267
+
268
+ /// Canonical 44-byte RIFF header + samples. Kept here rather than shelling
269
+ /// out so the helper stays the single bundled binary.
270
+ private static func wavData(pcm: Data, sampleRate: Int) -> Data {
271
+ var out = Data()
272
+ func u32(_ v: Int) { withUnsafeBytes(of: UInt32(v).littleEndian) { out.append(contentsOf: $0) } }
273
+ func u16(_ v: Int) { withUnsafeBytes(of: UInt16(v).littleEndian) { out.append(contentsOf: $0) } }
274
+ out.append(contentsOf: Array("RIFF".utf8)); u32(36 + pcm.count)
275
+ out.append(contentsOf: Array("WAVE".utf8))
276
+ out.append(contentsOf: Array("fmt ".utf8)); u32(16)
277
+ u16(1); u16(1); u32(sampleRate); u32(sampleRate * 2); u16(2); u16(16)
278
+ out.append(contentsOf: Array("data".utf8)); u32(pcm.count)
279
+ out.append(pcm)
280
+ return out
281
+ }
282
+
72
283
  private static func healthResponse(id: Int) -> [String: Any] {
73
284
  #if canImport(FoundationModels)
74
285
  if #available(macOS 26.0, *) {
@@ -110,3 +321,95 @@ struct AgentDeckFMHelper {
110
321
  FileHandle.standardOutput.write(Data((text + "\n").utf8))
111
322
  }
112
323
  }
324
+
325
+ /// One-shot continuation guard: speech and synthesis callbacks can fire more
326
+ /// than once, and resuming a continuation twice traps.
327
+ private final class ResumeOnce<T>: @unchecked Sendable {
328
+ private var continuation: CheckedContinuation<T, Never>?
329
+ private let lock = NSLock()
330
+
331
+ init(_ continuation: CheckedContinuation<T, Never>) {
332
+ self.continuation = continuation
333
+ }
334
+
335
+ func resume(_ value: T) {
336
+ lock.lock()
337
+ let c = continuation
338
+ continuation = nil
339
+ lock.unlock()
340
+ c?.resume(returning: value)
341
+ }
342
+ }
343
+
344
+ /// Accumulates synthesized audio, resampling each delivered buffer to the
345
+ /// target format. The converter is built from the first buffer because the
346
+ /// synthesizer picks its own rate and sample type per voice.
347
+ private final class PCMSink: @unchecked Sendable {
348
+ private let lock = NSLock()
349
+ private var buffer = Data()
350
+ private var converter: AVAudioConverter?
351
+ private var error: String?
352
+
353
+ var pcm: Data {
354
+ lock.lock(); defer { lock.unlock() }
355
+ return buffer
356
+ }
357
+
358
+ var failure: String? {
359
+ lock.lock(); defer { lock.unlock() }
360
+ return error
361
+ }
362
+
363
+ func append(_ input: AVAudioPCMBuffer, to outFormat: AVAudioFormat) {
364
+ lock.lock(); defer { lock.unlock() }
365
+ if error != nil { return }
366
+ if converter == nil {
367
+ converter = AVAudioConverter(from: input.format, to: outFormat)
368
+ if converter == nil {
369
+ error = "no converter from \(input.format) to \(outFormat)"
370
+ return
371
+ }
372
+ }
373
+ guard let converter else { return }
374
+ // Round up, and leave slack: the converter may emit slightly more than
375
+ // the naive ratio when it flushes filter state.
376
+ let ratio = outFormat.sampleRate / input.format.sampleRate
377
+ let capacity = AVAudioFrameCount((Double(input.frameLength) * ratio).rounded(.up)) + 1024
378
+ guard let output = AVAudioPCMBuffer(pcmFormat: outFormat, frameCapacity: capacity) else {
379
+ error = "output buffer allocation failed"
380
+ return
381
+ }
382
+ var supplied = false
383
+ var convertError: NSError?
384
+ let status = converter.convert(to: output, error: &convertError) { _, outStatus in
385
+ if supplied {
386
+ outStatus.pointee = .noDataNow
387
+ return nil
388
+ }
389
+ supplied = true
390
+ outStatus.pointee = .haveData
391
+ return input
392
+ }
393
+ if status == .error {
394
+ error = convertError?.localizedDescription ?? "conversion failed"
395
+ return
396
+ }
397
+ guard let channel = output.int16ChannelData, output.frameLength > 0 else { return }
398
+ let count = Int(output.frameLength)
399
+ channel[0].withMemoryRebound(to: UInt8.self, capacity: count * 2) { raw in
400
+ buffer.append(raw, count: count * 2)
401
+ }
402
+ }
403
+ }
404
+
405
+ private final class SpeakDelegate: NSObject, AVSpeechSynthesizerDelegate {
406
+ var onFinish: ResumeOnce<Void>?
407
+
408
+ func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
409
+ onFinish?.resume(())
410
+ }
411
+
412
+ func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
413
+ onFinish?.resume(())
414
+ }
415
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentdeck/bridge",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,8 +28,8 @@
28
28
  "qrcode": "^1.5.4",
29
29
  "strip-ansi": "^7.2.0",
30
30
  "ws": "^8.20.0",
31
- "@agentdeck/shared": "1.0.1",
32
- "@agentdeck/hooks": "1.0.1"
31
+ "@agentdeck/shared": "1.0.3",
32
+ "@agentdeck/hooks": "1.0.3"
33
33
  },
34
34
  "optionalDependencies": {
35
35
  "@picovoice/porcupine-node": "^4.0.2",
@@ -1,62 +0,0 @@
1
- /**
2
- * D200H HID Protocol — D200-compatible 1024-byte packet protocol
3
- *
4
- * Packet structure:
5
- * [0-1] Header: 0x7C 0x7C
6
- * [2-3] Command: big-endian uint16
7
- * [4-7] Length: little-endian uint32
8
- * [8-1023] Payload (zero-padded)
9
- *
10
- * Image delivery: ZIP(manifest.json + icons/*.png) sent in chunked 1024-byte packets.
11
- * ZIP quirk: bytes at offset 1016, 2040, 3064... must NOT be 0x00 or 0x7C.
12
- */
13
- export declare const CMD: {
14
- readonly SET_BUTTONS: 1;
15
- readonly PARTIAL_UPDATE: 13;
16
- readonly SET_SMALL_WINDOW: 6;
17
- readonly SET_BRIGHTNESS: 10;
18
- readonly SET_LABEL_STYLE: 11;
19
- readonly IN_BUTTON: 257;
20
- readonly IN_DEVICE_INFO: 771;
21
- };
22
- export declare function buildPacket(command: number, payload: Buffer, totalLength?: number): Buffer;
23
- /**
24
- * Build all packets for a ZIP file transfer (SET_BUTTONS or PARTIAL_UPDATE).
25
- * First packet has the header; subsequent packets are raw 1024-byte chunks.
26
- */
27
- export declare function buildZipPackets(zipData: Buffer, command?: number): Buffer[];
28
- export declare function buildBrightnessPacket(brightness: number): Buffer;
29
- export declare function buildSmallWindowPacket(mode: number, cpu: number, mem: number, time: string, gpu: number): Buffer;
30
- export interface ButtonEvent {
31
- index: number;
32
- pressed: boolean;
33
- state: number;
34
- }
35
- export interface DeviceInfo {
36
- serialNumber: string;
37
- firmwareVersion: string;
38
- deviceType: string;
39
- hardwareVersion: string;
40
- }
41
- export type IncomingEvent = {
42
- type: 'button';
43
- data: ButtonEvent;
44
- } | {
45
- type: 'device_info';
46
- data: DeviceInfo;
47
- } | {
48
- type: 'unknown';
49
- command: number;
50
- raw: Buffer;
51
- };
52
- export declare function parseIncoming(data: Buffer): IncomingEvent | null;
53
- /**
54
- * Check if a ZIP has invalid bytes at 1024-byte chunk boundaries.
55
- * At offset 1016, 2040, 3064... the byte must not be 0x00 or 0x7C.
56
- */
57
- export declare function validateZipBoundaries(zipData: Buffer): boolean;
58
- /**
59
- * Generate a random dummy string to pad the ZIP until boundaries are valid.
60
- */
61
- export declare function generateDummyPadding(attempt: number): string;
62
- //# sourceMappingURL=hid-protocol.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hid-protocol.d.ts","sourceRoot":"","sources":["../../src/d200h/hid-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH,eAAO,MAAM,GAAG;;;;;;;;CAQN,CAAC;AAIX,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAY1F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAwB,GAAG,MAAM,EAAE,CAiB5F;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGhH;AAID,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoChE;AAID;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO9D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -1,124 +0,0 @@
1
- /**
2
- * D200H HID Protocol — D200-compatible 1024-byte packet protocol
3
- *
4
- * Packet structure:
5
- * [0-1] Header: 0x7C 0x7C
6
- * [2-3] Command: big-endian uint16
7
- * [4-7] Length: little-endian uint32
8
- * [8-1023] Payload (zero-padded)
9
- *
10
- * Image delivery: ZIP(manifest.json + icons/*.png) sent in chunked 1024-byte packets.
11
- * ZIP quirk: bytes at offset 1016, 2040, 3064... must NOT be 0x00 or 0x7C.
12
- */
13
- import { randomBytes } from 'crypto';
14
- const TAG = 'd200h-hid';
15
- const PACKET_SIZE = 1024;
16
- const HEADER = Buffer.from([0x7c, 0x7c]);
17
- const INVALID_BOUNDARY_BYTES = [0x00, 0x7c];
18
- // --- Commands ---
19
- export const CMD = {
20
- SET_BUTTONS: 0x0001,
21
- PARTIAL_UPDATE: 0x000d,
22
- SET_SMALL_WINDOW: 0x0006,
23
- SET_BRIGHTNESS: 0x000a,
24
- SET_LABEL_STYLE: 0x000b,
25
- IN_BUTTON: 0x0101,
26
- IN_DEVICE_INFO: 0x0303,
27
- };
28
- // --- Packet building ---
29
- export function buildPacket(command, payload, totalLength) {
30
- const pkt = Buffer.alloc(PACKET_SIZE, 0);
31
- // Header
32
- pkt[0] = 0x7c;
33
- pkt[1] = 0x7c;
34
- // Command (big-endian uint16)
35
- pkt.writeUInt16BE(command, 2);
36
- // Length (little-endian uint32) — total file size for multi-packet, else payload length
37
- pkt.writeUInt32LE(totalLength ?? payload.length, 4);
38
- // Payload
39
- payload.copy(pkt, 8, 0, Math.min(payload.length, PACKET_SIZE - 8));
40
- return pkt;
41
- }
42
- /**
43
- * Build all packets for a ZIP file transfer (SET_BUTTONS or PARTIAL_UPDATE).
44
- * First packet has the header; subsequent packets are raw 1024-byte chunks.
45
- */
46
- export function buildZipPackets(zipData, command = CMD.SET_BUTTONS) {
47
- const packets = [];
48
- const fileSize = zipData.length;
49
- // First packet: header(8) + first chunk
50
- const firstChunkSize = PACKET_SIZE - 8;
51
- const firstChunk = zipData.subarray(0, firstChunkSize);
52
- packets.push(buildPacket(command, firstChunk, fileSize));
53
- // Remaining chunks (raw, no header)
54
- for (let offset = firstChunkSize; offset < fileSize; offset += PACKET_SIZE) {
55
- const chunk = Buffer.alloc(PACKET_SIZE, 0);
56
- zipData.copy(chunk, 0, offset, Math.min(offset + PACKET_SIZE, fileSize));
57
- packets.push(chunk);
58
- }
59
- return packets;
60
- }
61
- export function buildBrightnessPacket(brightness) {
62
- const val = Math.max(0, Math.min(100, Math.round(brightness)));
63
- return buildPacket(CMD.SET_BRIGHTNESS, Buffer.from(String(val), 'utf-8'));
64
- }
65
- export function buildSmallWindowPacket(mode, cpu, mem, time, gpu) {
66
- const data = `${mode}|${cpu}|${mem}|${time}|${gpu}`;
67
- return buildPacket(CMD.SET_SMALL_WINDOW, Buffer.from(data, 'utf-8'));
68
- }
69
- export function parseIncoming(data) {
70
- if (data.length < 8)
71
- return null;
72
- if (data[0] !== 0x7c || data[1] !== 0x7c)
73
- return null;
74
- const command = data.readUInt16BE(2);
75
- if (command === CMD.IN_BUTTON && data.length >= 12) {
76
- return {
77
- type: 'button',
78
- data: {
79
- state: data[8],
80
- index: data[9],
81
- pressed: data[11] === 0x01,
82
- },
83
- };
84
- }
85
- if (command === CMD.IN_DEVICE_INFO) {
86
- try {
87
- const jsonStr = data.subarray(8).toString('ascii').split('\0')[0];
88
- const info = JSON.parse(jsonStr);
89
- return {
90
- type: 'device_info',
91
- data: {
92
- serialNumber: info.SerialNumber ?? '',
93
- firmwareVersion: info.Dversion ?? '',
94
- deviceType: info.DeviceType ?? '',
95
- hardwareVersion: info.HardwareVersion ?? '',
96
- },
97
- };
98
- }
99
- catch {
100
- return { type: 'unknown', command, raw: data };
101
- }
102
- }
103
- return { type: 'unknown', command, raw: data };
104
- }
105
- // --- ZIP validation (boundary byte workaround) ---
106
- /**
107
- * Check if a ZIP has invalid bytes at 1024-byte chunk boundaries.
108
- * At offset 1016, 2040, 3064... the byte must not be 0x00 or 0x7C.
109
- */
110
- export function validateZipBoundaries(zipData) {
111
- for (let i = 1016; i < zipData.length; i += PACKET_SIZE) {
112
- if (INVALID_BOUNDARY_BYTES.includes(zipData[i])) {
113
- return false;
114
- }
115
- }
116
- return true;
117
- }
118
- /**
119
- * Generate a random dummy string to pad the ZIP until boundaries are valid.
120
- */
121
- export function generateDummyPadding(attempt) {
122
- return 'AgentDeck ' + randomBytes(8 * (attempt + 1)).toString('hex');
123
- }
124
- //# sourceMappingURL=hid-protocol.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hid-protocol.js","sourceRoot":"","sources":["../../src/d200h/hid-protocol.ts"],"names":[],"mappings":"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"}
@@ -1,24 +0,0 @@
1
- /**
2
- * D200H Image Renderer — Renders AgentDeck state as 196×196 PNG key icons
3
- * using the shared SVG renderers (same visual output as Stream Deck plugin).
4
- *
5
- * Pipeline: state → shared SVG generators (144×144) → resvg rasterize (196×196 PNG) → ZIP
6
- *
7
- * Falls back to solid-color PNGs if resvg-js is not available.
8
- */
9
- import { parseState, buildButtonCommandMap } from '@agentdeck/shared';
10
- export { parseState, buildButtonCommandMap };
11
- export type { DashState, ButtonCommand, KeySlot } from '@agentdeck/shared';
12
- /** Initialize the renderer (call once at module start). */
13
- export declare function initRenderer(): Promise<void>;
14
- export declare function isResvgLoaded(): boolean;
15
- /**
16
- * Render the full AgentDeck dashboard as a ZIP ready for SET_BUTTONS.
17
- * Uses shared SVG renderers → resvg rasterization for SD+-quality output.
18
- */
19
- export declare function renderDashboardZip(stateEvt: any): Buffer;
20
- /**
21
- * Create a simple hash of the visual state for change detection.
22
- */
23
- export declare function stateHash(stateEvt: any): string;
24
- //# sourceMappingURL=image-renderer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"image-renderer.d.ts","sourceRoot":"","sources":["../../src/d200h/image-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,EAEL,UAAU,EACV,qBAAqB,EAEtB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,CAAC;AAC7C,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AA2D3E,2DAA2D;AAC3D,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAElD;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AA+LD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,GAAG,GAAG,MAAM,CAmExD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,GAAG,GAAG,MAAM,CAI/C"}