@agentdeck/bridge 1.0.2 → 1.0.4

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 (175) 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 +232 -69
  31. package/dist/cli.js.map +1 -1
  32. package/dist/daemon-server.d.ts.map +1 -1
  33. package/dist/daemon-server.js +1008 -47
  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/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
  61. package/dist/idotmatrix/idotmatrix-daemon-sync.js +5 -18
  62. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
  63. package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -1
  64. package/dist/idotmatrix/idotmatrix-discover.js +6 -14
  65. package/dist/idotmatrix/idotmatrix-discover.js.map +1 -1
  66. package/dist/index.js +2 -2
  67. package/dist/index.js.map +1 -1
  68. package/dist/mdns.d.ts +11 -0
  69. package/dist/mdns.d.ts.map +1 -1
  70. package/dist/mdns.js +13 -0
  71. package/dist/mdns.js.map +1 -1
  72. package/dist/observed-inject.d.ts +119 -0
  73. package/dist/observed-inject.d.ts.map +1 -0
  74. package/dist/observed-inject.js +405 -0
  75. package/dist/observed-inject.js.map +1 -0
  76. package/dist/passive-observer.d.ts +14 -0
  77. package/dist/passive-observer.d.ts.map +1 -1
  78. package/dist/passive-observer.js +35 -5
  79. package/dist/passive-observer.js.map +1 -1
  80. package/dist/peripheral-mapping.d.ts +65 -0
  81. package/dist/peripheral-mapping.d.ts.map +1 -0
  82. package/dist/peripheral-mapping.js +115 -0
  83. package/dist/peripheral-mapping.js.map +1 -0
  84. package/dist/python-ble-runtime.d.ts +62 -0
  85. package/dist/python-ble-runtime.d.ts.map +1 -0
  86. package/dist/python-ble-runtime.js +215 -0
  87. package/dist/python-ble-runtime.js.map +1 -0
  88. package/dist/session-aggregator.d.ts +4 -1
  89. package/dist/session-aggregator.d.ts.map +1 -1
  90. package/dist/session-aggregator.js +5 -5
  91. package/dist/session-aggregator.js.map +1 -1
  92. package/dist/session-id-resolve.d.ts +17 -0
  93. package/dist/session-id-resolve.d.ts.map +1 -0
  94. package/dist/session-id-resolve.js +35 -0
  95. package/dist/session-id-resolve.js.map +1 -0
  96. package/dist/session-transcript-timeline.d.ts +7 -0
  97. package/dist/session-transcript-timeline.d.ts.map +1 -1
  98. package/dist/session-transcript-timeline.js +16 -6
  99. package/dist/session-transcript-timeline.js.map +1 -1
  100. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
  101. package/dist/timebox/timebox-daemon-sync.js +5 -20
  102. package/dist/timebox/timebox-daemon-sync.js.map +1 -1
  103. package/dist/timebox/timebox-discover.d.ts.map +1 -1
  104. package/dist/timebox/timebox-discover.js +6 -15
  105. package/dist/timebox/timebox-discover.js.map +1 -1
  106. package/dist/timeline-store.js +2 -2
  107. package/dist/timeline-store.js.map +1 -1
  108. package/dist/usage-event.d.ts.map +1 -1
  109. package/dist/usage-event.js +14 -7
  110. package/dist/usage-event.js.map +1 -1
  111. package/dist/voice.d.ts +13 -10
  112. package/dist/voice.d.ts.map +1 -1
  113. package/dist/voice.js +26 -272
  114. package/dist/voice.js.map +1 -1
  115. package/dist/ws-server.d.ts +14 -0
  116. package/dist/ws-server.d.ts.map +1 -1
  117. package/dist/ws-server.js +66 -1
  118. package/dist/ws-server.js.map +1 -1
  119. package/fm-helper/AgentDeckFMHelper.swift +303 -0
  120. package/package.json +8 -4
  121. package/python/requirements-ble.txt +5 -0
  122. package/src/idotmatrix/brightness.py +40 -0
  123. package/src/idotmatrix/scan.py +35 -0
  124. package/src/idotmatrix/sync.py +400 -0
  125. package/src/pysync/matrix_sync_common.py +89 -0
  126. package/src/timebox/scan_ble.py +52 -0
  127. package/src/timebox/sync_ble.py +394 -0
  128. package/dist/d200h/hid-protocol.d.ts +0 -62
  129. package/dist/d200h/hid-protocol.d.ts.map +0 -1
  130. package/dist/d200h/hid-protocol.js +0 -124
  131. package/dist/d200h/hid-protocol.js.map +0 -1
  132. package/dist/d200h/image-renderer.d.ts +0 -24
  133. package/dist/d200h/image-renderer.d.ts.map +0 -1
  134. package/dist/d200h/image-renderer.js +0 -321
  135. package/dist/d200h/image-renderer.js.map +0 -1
  136. package/dist/hook-migration.d.ts +0 -2
  137. package/dist/hook-migration.d.ts.map +0 -1
  138. package/dist/hook-migration.js +0 -104
  139. package/dist/hook-migration.js.map +0 -1
  140. package/dist/modules/d200h-module.d.ts +0 -65
  141. package/dist/modules/d200h-module.d.ts.map +0 -1
  142. package/dist/modules/d200h-module.js +0 -495
  143. package/dist/modules/d200h-module.js.map +0 -1
  144. package/dist/modules/d200h-renderer.d.ts +0 -7
  145. package/dist/modules/d200h-renderer.d.ts.map +0 -1
  146. package/dist/modules/d200h-renderer.js +0 -321
  147. package/dist/modules/d200h-renderer.js.map +0 -1
  148. package/dist/modules/trmnl-module.d.ts +0 -38
  149. package/dist/modules/trmnl-module.d.ts.map +0 -1
  150. package/dist/modules/trmnl-module.js +0 -111
  151. package/dist/modules/trmnl-module.js.map +0 -1
  152. package/dist/trmnl/byos-server.d.ts +0 -39
  153. package/dist/trmnl/byos-server.d.ts.map +0 -1
  154. package/dist/trmnl/byos-server.js +0 -301
  155. package/dist/trmnl/byos-server.js.map +0 -1
  156. package/dist/trmnl/frame-cache.d.ts +0 -37
  157. package/dist/trmnl/frame-cache.d.ts.map +0 -1
  158. package/dist/trmnl/frame-cache.js +0 -157
  159. package/dist/trmnl/frame-cache.js.map +0 -1
  160. package/dist/trmnl/image-renderer.d.ts +0 -27
  161. package/dist/trmnl/image-renderer.d.ts.map +0 -1
  162. package/dist/trmnl/image-renderer.js +0 -235
  163. package/dist/trmnl/image-renderer.js.map +0 -1
  164. package/dist/trmnl/trmnl-settings.d.ts +0 -96
  165. package/dist/trmnl/trmnl-settings.d.ts.map +0 -1
  166. package/dist/trmnl/trmnl-settings.js +0 -177
  167. package/dist/trmnl/trmnl-settings.js.map +0 -1
  168. package/dist/trmnl/trmnl-telemetry.d.ts +0 -47
  169. package/dist/trmnl/trmnl-telemetry.d.ts.map +0 -1
  170. package/dist/trmnl/trmnl-telemetry.js +0 -56
  171. package/dist/trmnl/trmnl-telemetry.js.map +0 -1
  172. package/dist/whisper-server-manager.d.ts +0 -19
  173. package/dist/whisper-server-manager.d.ts.map +0 -1
  174. package/dist/whisper-server-manager.js +0 -171
  175. package/dist/whisper-server-manager.js.map +0 -1
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
8
8
  import { createRequire } from 'module';
9
9
  import { request } from 'http';
10
10
  import { BRIDGE_WS_PORT } from './types.js';
11
+ import { ensureBleRuntime, getBleRuntimeStatus } from './python-ble-runtime.js';
11
12
  import { TASK_NAME, installWindowsTask, taskExists, runWindowsTask, endWindowsTask, deleteWindowsTask, } from './windows-service.js';
12
13
  const require = createRequire(import.meta.url);
13
14
  const packageJson = require('../package.json');
@@ -135,19 +136,30 @@ async function isDaemonPort(port) {
135
136
  return false;
136
137
  }
137
138
  }
138
- function resolveTimeboxSyncPaths() {
139
- const distPath = dirname(fileURLToPath(import.meta.url));
140
- const projectRoot = join(distPath, '..', '..');
141
- const timeboxDir = join(projectRoot, 'bridge', 'src', 'timebox');
142
- return {
143
- python: join(projectRoot, '.venv', 'bin', 'python'),
144
- bleScript: join(timeboxDir, 'sync_ble.py'),
145
- scanScript: join(timeboxDir, 'scan_ble.py'),
146
- };
147
- }
148
139
  function projectRootPath() {
149
140
  return join(dirname(fileURLToPath(import.meta.url)), '..', '..');
150
141
  }
142
+ async function runBlePython(python, args, captureStdout = false) {
143
+ return new Promise((resolve, reject) => {
144
+ const child = spawn(python, args, captureStdout ? { stdio: ['ignore', 'pipe', 'inherit'] } : { stdio: 'inherit' });
145
+ let stdout = '';
146
+ child.stdout?.on('data', (data) => {
147
+ stdout += data.toString();
148
+ });
149
+ child.once('error', reject);
150
+ child.once('close', (code) => resolve({ code, stdout }));
151
+ });
152
+ }
153
+ function prepareBleRuntime() {
154
+ try {
155
+ return ensureBleRuntime({ log });
156
+ }
157
+ catch (err) {
158
+ log(`BLE support unavailable: ${err instanceof Error ? err.message : String(err)}`);
159
+ process.exitCode = 1;
160
+ return null;
161
+ }
162
+ }
151
163
  const ESP32_OTA_BOARDS = [
152
164
  { board: 'inkdeck', env: 'inkdeck', aliases: [] },
153
165
  { board: 'ulanzi_tc001', env: 'led8x32', aliases: ['led8x32'] },
@@ -156,6 +168,8 @@ const ESP32_OTA_BOARDS = [
156
168
  { board: 'ips_35', env: 'ips35', aliases: ['ips35'] },
157
169
  { board: '86box', env: 'box_86', aliases: ['box_86', 'box_40'] },
158
170
  { board: 'ips_10', env: 'ips10', aliases: ['ips10', 'ips_101'] },
171
+ { board: 't_embed', env: 't_embed', aliases: ['tembed', 'knob'] },
172
+ { board: 't_display_pro', env: 't_display_pro', aliases: ['tdisplaypro', 'ticker', 's3pro'] },
159
173
  ];
160
174
  // alias/board/env-key → { env, board }. Built from the SSOT above.
161
175
  export const ESP32_OTA_BY_TARGET = {};
@@ -712,6 +726,45 @@ program
712
726
  }
713
727
  });
714
728
  // ===== Device commands =====
729
+ // Bench test for the playback half of the voice loop. The production path only
730
+ // speaks after a dictation arms a board and its session finishes a turn, which
731
+ // is no way to check whether a freshly flashed amplifier works.
732
+ program
733
+ .command('speak <board> <text>')
734
+ .description('Speak text on a board\'s speaker (voice playback bench test)')
735
+ .option('-p, --port <port>', 'Bridge server port')
736
+ .option('-l, --locale <bcp47>', 'TTS locale, e.g. ko-KR (default: settings voice.locale)')
737
+ .option('-f, --force', 'Stream even if the board does not advertise audio_out')
738
+ .action(async (board, text, opts) => {
739
+ const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
740
+ const info = readDaemonInfo();
741
+ const port = opts.port != null
742
+ ? parseInt(opts.port, 10)
743
+ : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
744
+ try {
745
+ const res = await fetch(`http://127.0.0.1:${port}/voice/speak`, {
746
+ method: 'POST',
747
+ headers: { 'Content-Type': 'application/json' },
748
+ body: JSON.stringify({ board, text, force: opts.force === true, locale: opts.locale }),
749
+ // Synthesis plus paced playback: a 700-char reply runs well past a
750
+ // minute at speech rate, and the daemon answers only once it is done.
751
+ signal: AbortSignal.timeout(180_000),
752
+ });
753
+ const data = await res.json();
754
+ if (data.error) {
755
+ log(`✗ ${data.error}`);
756
+ process.exitCode = 1;
757
+ return;
758
+ }
759
+ log(data.spoke ? `✓ spoke on ${board} — ${data.detail}` : `✗ ${data.detail}`);
760
+ if (!data.spoke)
761
+ process.exitCode = 1;
762
+ }
763
+ catch (err) {
764
+ log(`✗ daemon not reachable on port ${port}: ${String(err)}`);
765
+ process.exitCode = 1;
766
+ }
767
+ });
715
768
  program
716
769
  .command('devices')
717
770
  .description('Show connected devices (WebSocket, ESP32, Pixoo, Timebox, ADB)')
@@ -780,6 +833,26 @@ program
780
833
  total += devices.length;
781
834
  }
782
835
  }
836
+ else if (d.type === 'card-feed') {
837
+ // Pull-sync (M6) clients. A battery board is asleep almost always, so
838
+ // "last pull + observed interval" is its liveness — not a socket. The
839
+ // interval vs. the advertised cadence is the timer-wake evidence.
840
+ const clients = (d.clients ?? []);
841
+ if (clients.length) {
842
+ lines.push(` Card feed ${clients.length} pull client${clients.length !== 1 ? 's' : ''}`);
843
+ for (const c of clients) {
844
+ const who = c.board ? `${c.board} @ ${c.client}` : c.client;
845
+ const ago = `${Math.round((Date.now() - c.lastPullAt) / 1000)}s ago`;
846
+ const cadence = c.lastIntervalSec !== undefined
847
+ ? `, last gap ${c.lastIntervalSec}s vs ${c.lastNextPullSec}s asked`
848
+ : '';
849
+ const honoured = c.pulls > 1
850
+ ? ` [${c.cadenceHonouredCount}/${c.pulls - 1} on cadence]`
851
+ : '';
852
+ lines.push(` ${who} — ${c.pulls} pull${c.pulls !== 1 ? 's' : ''}, last ${ago}${cadence}${honoured}`);
853
+ }
854
+ }
855
+ }
783
856
  else if (d.type === 'pixoo') {
784
857
  // Node daemon: { details: [...] }, Swift daemon: { deviceIps: [...] }
785
858
  if (d.details) {
@@ -958,6 +1031,38 @@ program
958
1031
  // qrcode not available
959
1032
  }
960
1033
  });
1034
+ program
1035
+ .command('inject-test')
1036
+ .description('Test observed-answer injection into a terminal or app (tuning aid)')
1037
+ .option('--tty <tty>', 'Controlling terminal, e.g. ttys007')
1038
+ .option('--app <name>', 'Hosting app for GUI sessions, e.g. Claude / ChatGPT')
1039
+ .option('--label <label>', 'Option label — app hosts press the matching button first')
1040
+ .option('-i, --index <n>', 'Zero-based option index (Down presses)', '0')
1041
+ .option('--text <text>', 'Type this text and submit it, instead of picking an option')
1042
+ .action(async (opts) => {
1043
+ if (!opts.tty && !opts.app) {
1044
+ console.error('Give --tty <ttysNNN> or --app <Name>. Find a session\'s tty with:');
1045
+ console.error(" ps -eo pid=,tty=,command= | grep -E ' claude| codex'");
1046
+ process.exit(1);
1047
+ }
1048
+ const { injectObservedSelection, injectObservedText } = await import('./observed-inject.js');
1049
+ const index = parseInt(opts.index, 10) || 0;
1050
+ // --text exercises the dictation path (voice → prompt), which submits a
1051
+ // whole line; without it only the option-picking path was testable, and
1052
+ // "the text arrived but nothing was submitted" had no repro.
1053
+ const result = opts.text
1054
+ ? await injectObservedText({ tty: opts.tty, appName: opts.app }, opts.text)
1055
+ : await injectObservedSelection({ tty: opts.tty, appName: opts.app, label: opts.label }, index);
1056
+ if (result.ok) {
1057
+ console.log(opts.text
1058
+ ? `Delivered via ${result.via} (text).`
1059
+ : `Delivered via ${result.via} (index ${index}).`);
1060
+ }
1061
+ else {
1062
+ console.error(`Not delivered: ${result.reason}`);
1063
+ process.exit(2);
1064
+ }
1065
+ });
961
1066
  program
962
1067
  .command('diag')
963
1068
  .description('Generate diagnostic dump')
@@ -1056,35 +1161,51 @@ task
1056
1161
  .action(async (opts) => {
1057
1162
  await postTaskClose({ signal: 'manual', outcome: 'abandoned', sessionId: opts.session });
1058
1163
  });
1164
+ // ===== Optional Python BLE runtime =====
1165
+ const ble = program.command('ble').description('Prepare optional Python support for BLE display devices');
1166
+ ble
1167
+ .command('setup')
1168
+ .description('Create the private BLE environment and install its Python dependencies')
1169
+ .action(() => {
1170
+ const runtime = prepareBleRuntime();
1171
+ if (runtime)
1172
+ log(`BLE support ready (${runtime.python}).`);
1173
+ });
1174
+ ble
1175
+ .command('status')
1176
+ .description('Show whether optional BLE support is ready')
1177
+ .action(() => {
1178
+ const status = getBleRuntimeStatus();
1179
+ if (status.ready) {
1180
+ log(`BLE support ready (${status.python}).`);
1181
+ return;
1182
+ }
1183
+ log(`BLE support is not ready: ${status.reason}. Run \`agentdeck ble setup\`.`);
1184
+ process.exitCode = 1;
1185
+ });
1059
1186
  // ===== iDotMatrix commands =====
1060
1187
  const idotmatrix = program.command('idotmatrix').description('Manage iDotMatrix BLE pixel display devices');
1061
1188
  idotmatrix
1062
1189
  .command('scan')
1063
1190
  .description('Discover iDotMatrix devices via BLE')
1064
1191
  .action(async () => {
1065
- const { dirname } = await import('path');
1066
- const { fileURLToPath } = await import('url');
1067
- const { spawn } = await import('child_process');
1068
- const distPath = dirname(fileURLToPath(import.meta.url));
1069
- const projectRoot = join(distPath, '..', '..');
1070
- const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1071
- const scanScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'scan.py');
1192
+ const runtime = prepareBleRuntime();
1193
+ if (!runtime)
1194
+ return;
1072
1195
  log('Scanning for BLE devices (5 seconds)...');
1073
- const py = spawn(venvPython, [scanScript]);
1074
- let stdoutData = '';
1075
- py.stdout.on('data', (data) => {
1076
- stdoutData += data.toString();
1077
- });
1078
- py.on('close', (code) => {
1196
+ try {
1197
+ const { code, stdout } = await runBlePython(runtime.python, [runtime.paths.scripts.idotmatrixScan], true);
1079
1198
  if (code !== 0) {
1080
1199
  log('Failed to run scan script.');
1081
- process.exit(1);
1200
+ process.exitCode = 1;
1201
+ return;
1082
1202
  }
1083
1203
  try {
1084
- const devices = JSON.parse(stdoutData.trim());
1204
+ const devices = JSON.parse(stdout.trim());
1085
1205
  if (devices.error) {
1086
1206
  log(`Scan error: ${devices.error}`);
1087
- process.exit(1);
1207
+ process.exitCode = 1;
1208
+ return;
1088
1209
  }
1089
1210
  if (!Array.isArray(devices) || devices.length === 0) {
1090
1211
  log('No BLE devices found.');
@@ -1096,10 +1217,15 @@ idotmatrix
1096
1217
  log(`${prefix}${d.name} (${d.address})`);
1097
1218
  }
1098
1219
  }
1099
- catch (e) {
1100
- log(`Failed to parse scan output: ${stdoutData}`);
1220
+ catch {
1221
+ log(`Failed to parse scan output: ${stdout}`);
1222
+ process.exitCode = 1;
1101
1223
  }
1102
- });
1224
+ }
1225
+ catch (err) {
1226
+ log(`Failed to start BLE scan: ${err instanceof Error ? err.message : String(err)}`);
1227
+ process.exitCode = 1;
1228
+ }
1103
1229
  });
1104
1230
  idotmatrix
1105
1231
  .command('add <address>')
@@ -1150,9 +1276,6 @@ idotmatrix
1150
1276
  .option('-a, --address <address>', 'BLE Address (defaults to first configured device)')
1151
1277
  .action(async (valueStr, opts) => {
1152
1278
  const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
1153
- const { dirname } = await import('path');
1154
- const { fileURLToPath } = await import('url');
1155
- const { spawn } = await import('child_process');
1156
1279
  const value = parseInt(valueStr, 10);
1157
1280
  if (isNaN(value) || value < 5 || value > 100) {
1158
1281
  log('Brightness value must be between 5 and 100.');
@@ -1167,22 +1290,30 @@ idotmatrix
1167
1290
  }
1168
1291
  targetAddress = devices[0].address;
1169
1292
  }
1170
- const distPath = dirname(fileURLToPath(import.meta.url));
1171
- const projectRoot = join(distPath, '..', '..');
1172
- const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1173
- const brightnessScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'brightness.py');
1293
+ const runtime = prepareBleRuntime();
1294
+ if (!runtime)
1295
+ return;
1174
1296
  log(`Setting brightness of ${targetAddress} to ${value}%...`);
1175
- const py = spawn(venvPython, [brightnessScript, '-a', targetAddress, '-b', String(value)], {
1176
- stdio: 'inherit'
1177
- });
1178
- py.on('close', (code) => {
1297
+ try {
1298
+ const { code } = await runBlePython(runtime.python, [
1299
+ runtime.paths.scripts.idotmatrixBrightness,
1300
+ '-a',
1301
+ targetAddress,
1302
+ '-b',
1303
+ String(value),
1304
+ ]);
1179
1305
  if (code === 0) {
1180
1306
  log('Brightness updated successfully.');
1181
1307
  }
1182
1308
  else {
1183
1309
  log(`Failed to update brightness, process exited with code ${code}`);
1310
+ process.exitCode = code ?? 1;
1184
1311
  }
1185
- });
1312
+ }
1313
+ catch (err) {
1314
+ log(`Failed to start brightness command: ${err instanceof Error ? err.message : String(err)}`);
1315
+ process.exitCode = 1;
1316
+ }
1186
1317
  });
1187
1318
  idotmatrix
1188
1319
  .command('sync [address]')
@@ -1193,9 +1324,6 @@ idotmatrix
1193
1324
  .action(async (addressOpt, opts) => {
1194
1325
  const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
1195
1326
  const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
1196
- const { dirname } = await import('path');
1197
- const { fileURLToPath } = await import('url');
1198
- const { spawn } = await import('child_process');
1199
1327
  let targetAddress = addressOpt;
1200
1328
  let defaultBrightness = 100;
1201
1329
  const devices = loadIDotMatrixDevices();
@@ -1221,17 +1349,30 @@ idotmatrix
1221
1349
  ? parseInt(opts.port, 10)
1222
1350
  : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
1223
1351
  const url = `http://127.0.0.1:${port}`;
1224
- const distPath = dirname(fileURLToPath(import.meta.url));
1225
- const projectRoot = join(distPath, '..', '..');
1226
- const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1227
- const syncScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'sync.py');
1352
+ const runtime = prepareBleRuntime();
1353
+ if (!runtime)
1354
+ return;
1228
1355
  log(`Starting BLE sync client linking to bridge at ${url} (brightness ${brightness}%, boost ${boost}x)...`);
1229
- const py = spawn(venvPython, [syncScript, '-a', targetAddress, '-u', url, '-b', String(brightness), '--boost', String(boost)], {
1230
- stdio: 'inherit'
1231
- });
1232
- py.on('close', (code) => {
1356
+ try {
1357
+ const { code } = await runBlePython(runtime.python, [
1358
+ runtime.paths.scripts.idotmatrixSync,
1359
+ '-a',
1360
+ targetAddress,
1361
+ '-u',
1362
+ url,
1363
+ '-b',
1364
+ String(brightness),
1365
+ '--boost',
1366
+ String(boost),
1367
+ ]);
1233
1368
  log(`Sync process exited with code ${code}`);
1234
- });
1369
+ if (code !== 0)
1370
+ process.exitCode = code ?? 1;
1371
+ }
1372
+ catch (err) {
1373
+ log(`Failed to start BLE sync: ${err instanceof Error ? err.message : String(err)}`);
1374
+ process.exitCode = 1;
1375
+ }
1235
1376
  });
1236
1377
  // ===== Divoom Timebox Mini Light commands =====
1237
1378
  const timebox = program.command('timebox').description('Manage Divoom Timebox Mini devices (Bluetooth LE)');
@@ -1239,22 +1380,22 @@ timebox
1239
1380
  .command('scan')
1240
1381
  .description('Discover BLE TimeBox-mini-light peripherals')
1241
1382
  .action(async () => {
1242
- const paths = resolveTimeboxSyncPaths();
1383
+ const runtime = prepareBleRuntime();
1384
+ if (!runtime)
1385
+ return;
1243
1386
  log('Scanning for BLE devices (5 seconds)...');
1244
- const py = spawn(paths.python, [paths.scanScript]);
1245
- let stdoutData = '';
1246
- py.stdout.on('data', (data) => {
1247
- stdoutData += data.toString();
1248
- });
1249
- py.on('close', (code) => {
1387
+ try {
1388
+ const { code, stdout } = await runBlePython(runtime.python, [runtime.paths.scripts.timeboxScan], true);
1250
1389
  if (code !== 0) {
1251
- log('BLE scan failed (is bleak installed in .venv?).');
1390
+ log('BLE scan failed.');
1391
+ process.exitCode = 1;
1252
1392
  return;
1253
1393
  }
1254
1394
  try {
1255
- const devices = JSON.parse(stdoutData.trim());
1395
+ const devices = JSON.parse(stdout.trim());
1256
1396
  if (devices.error) {
1257
1397
  log(`BLE scan error: ${devices.error}`);
1398
+ process.exitCode = 1;
1258
1399
  return;
1259
1400
  }
1260
1401
  if (!Array.isArray(devices) || devices.length === 0) {
@@ -1270,9 +1411,14 @@ timebox
1270
1411
  log('\nAdd the starred BLE device with: agentdeck timebox add <address>');
1271
1412
  }
1272
1413
  catch {
1273
- log(`Failed to parse BLE scan output: ${stdoutData}`);
1414
+ log(`Failed to parse BLE scan output: ${stdout}`);
1415
+ process.exitCode = 1;
1274
1416
  }
1275
- });
1417
+ }
1418
+ catch (err) {
1419
+ log(`Failed to start BLE scan: ${err instanceof Error ? err.message : String(err)}`);
1420
+ process.exitCode = 1;
1421
+ }
1276
1422
  });
1277
1423
  timebox
1278
1424
  .command('add <address>')
@@ -1349,11 +1495,22 @@ async function runTimeboxSync(targetOpt, opts, once) {
1349
1495
  ? parseInt(opts.bridgePort, 10)
1350
1496
  : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
1351
1497
  const url = `http://127.0.0.1:${bridgePort}`;
1352
- const paths = resolveTimeboxSyncPaths();
1353
- const args = [paths.bleScript, '--address', id, '--url', url, '--brightness', String(brightness), ...(once ? ['--once'] : [])];
1498
+ const runtime = prepareBleRuntime();
1499
+ if (!runtime)
1500
+ return;
1501
+ const args = [
1502
+ runtime.paths.scripts.timeboxSync,
1503
+ '--address',
1504
+ id,
1505
+ '--url',
1506
+ url,
1507
+ '--brightness',
1508
+ String(brightness),
1509
+ ...(once ? ['--once'] : []),
1510
+ ];
1354
1511
  log(`${once ? 'Sending one Timebox frame' : 'Starting Timebox sync'} via BLE ${id} from ${url} (brightness ${brightness}%)...`);
1355
- const py = spawn(paths.python, args, { stdio: 'inherit' });
1356
- py.on('close', (code) => {
1512
+ try {
1513
+ const { code } = await runBlePython(runtime.python, args);
1357
1514
  if (once) {
1358
1515
  if (code === 0) {
1359
1516
  log('Timebox test frame sent.');
@@ -1365,8 +1522,14 @@ async function runTimeboxSync(targetOpt, opts, once) {
1365
1522
  }
1366
1523
  else {
1367
1524
  log(`Timebox sync process exited with code ${code}`);
1525
+ if (code !== 0)
1526
+ process.exitCode = code ?? 1;
1368
1527
  }
1369
- });
1528
+ }
1529
+ catch (err) {
1530
+ log(`Failed to start Timebox BLE command: ${err instanceof Error ? err.message : String(err)}`);
1531
+ process.exitCode = 1;
1532
+ }
1370
1533
  }
1371
1534
  timebox
1372
1535
  .command('test [target]')