@agentdeck/bridge 1.0.5 → 1.0.7

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 (128) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/dist/apme/collector.d.ts +10 -0
  3. package/dist/apme/collector.d.ts.map +1 -1
  4. package/dist/apme/collector.js +39 -10
  5. package/dist/apme/collector.js.map +1 -1
  6. package/dist/apme/dashboard-html.d.ts.map +1 -1
  7. package/dist/apme/dashboard-html.js +289 -0
  8. package/dist/apme/dashboard-html.js.map +1 -1
  9. package/dist/apme/graph.d.ts +48 -0
  10. package/dist/apme/graph.d.ts.map +1 -0
  11. package/dist/apme/graph.js +226 -0
  12. package/dist/apme/graph.js.map +1 -0
  13. package/dist/apme/http.d.ts.map +1 -1
  14. package/dist/apme/http.js +52 -0
  15. package/dist/apme/http.js.map +1 -1
  16. package/dist/apme/judge-detect.d.ts.map +1 -1
  17. package/dist/apme/judge-detect.js +26 -2
  18. package/dist/apme/judge-detect.js.map +1 -1
  19. package/dist/apme/store.d.ts +69 -1
  20. package/dist/apme/store.d.ts.map +1 -1
  21. package/dist/apme/store.js +238 -4
  22. package/dist/apme/store.js.map +1 -1
  23. package/dist/apme/types.d.ts +1 -1
  24. package/dist/apme/types.d.ts.map +1 -1
  25. package/dist/apme/types.js.map +1 -1
  26. package/dist/ble-sync-spawn.d.ts +25 -2
  27. package/dist/ble-sync-spawn.d.ts.map +1 -1
  28. package/dist/ble-sync-spawn.js +38 -2
  29. package/dist/ble-sync-spawn.js.map +1 -1
  30. package/dist/ble-sync-status.d.ts +77 -0
  31. package/dist/ble-sync-status.d.ts.map +1 -0
  32. package/dist/ble-sync-status.js +157 -0
  33. package/dist/ble-sync-status.js.map +1 -0
  34. package/dist/bridge-core.d.ts.map +1 -1
  35. package/dist/bridge-core.js +7 -1
  36. package/dist/bridge-core.js.map +1 -1
  37. package/dist/cli.js +0 -0
  38. package/dist/codex-rate-limits-live.d.ts +60 -0
  39. package/dist/codex-rate-limits-live.d.ts.map +1 -0
  40. package/dist/codex-rate-limits-live.js +282 -0
  41. package/dist/codex-rate-limits-live.js.map +1 -0
  42. package/dist/d200h/hid-protocol.d.ts +62 -0
  43. package/dist/d200h/hid-protocol.d.ts.map +1 -0
  44. package/dist/d200h/hid-protocol.js +124 -0
  45. package/dist/d200h/hid-protocol.js.map +1 -0
  46. package/dist/d200h/image-renderer.d.ts +24 -0
  47. package/dist/d200h/image-renderer.d.ts.map +1 -0
  48. package/dist/d200h/image-renderer.js +321 -0
  49. package/dist/d200h/image-renderer.js.map +1 -0
  50. package/dist/daemon-server.d.ts.map +1 -1
  51. package/dist/daemon-server.js +163 -10
  52. package/dist/daemon-server.js.map +1 -1
  53. package/dist/daemon.js +1 -1
  54. package/dist/device-voice-reply.d.ts +1 -10
  55. package/dist/device-voice-reply.d.ts.map +1 -1
  56. package/dist/device-voice-reply.js +4 -33
  57. package/dist/device-voice-reply.js.map +1 -1
  58. package/dist/foundation-models-helper.d.ts.map +1 -1
  59. package/dist/foundation-models-helper.js +59 -22
  60. package/dist/foundation-models-helper.js.map +1 -1
  61. package/dist/hook-migration.d.ts +2 -0
  62. package/dist/hook-migration.d.ts.map +1 -0
  63. package/dist/hook-migration.js +116 -0
  64. package/dist/hook-migration.js.map +1 -0
  65. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +8 -0
  66. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
  67. package/dist/idotmatrix/idotmatrix-daemon-sync.js +19 -3
  68. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
  69. package/dist/modules/d200h-module.d.ts +65 -0
  70. package/dist/modules/d200h-module.d.ts.map +1 -0
  71. package/dist/modules/d200h-module.js +495 -0
  72. package/dist/modules/d200h-module.js.map +1 -0
  73. package/dist/modules/d200h-renderer.d.ts +7 -0
  74. package/dist/modules/d200h-renderer.d.ts.map +1 -0
  75. package/dist/modules/d200h-renderer.js +321 -0
  76. package/dist/modules/d200h-renderer.js.map +1 -0
  77. package/dist/modules/idotmatrix-module.d.ts.map +1 -1
  78. package/dist/modules/idotmatrix-module.js +12 -1
  79. package/dist/modules/idotmatrix-module.js.map +1 -1
  80. package/dist/modules/timebox-module.d.ts.map +1 -1
  81. package/dist/modules/timebox-module.js +12 -1
  82. package/dist/modules/timebox-module.js.map +1 -1
  83. package/dist/modules/trmnl-module.d.ts +38 -0
  84. package/dist/modules/trmnl-module.d.ts.map +1 -0
  85. package/dist/modules/trmnl-module.js +111 -0
  86. package/dist/modules/trmnl-module.js.map +1 -0
  87. package/dist/openclaw-session.d.ts +6 -0
  88. package/dist/openclaw-session.d.ts.map +1 -1
  89. package/dist/openclaw-session.js +7 -1
  90. package/dist/openclaw-session.js.map +1 -1
  91. package/dist/timebox/timebox-daemon-sync.d.ts +8 -0
  92. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
  93. package/dist/timebox/timebox-daemon-sync.js +29 -1
  94. package/dist/timebox/timebox-daemon-sync.js.map +1 -1
  95. package/dist/timeline-summarizer.d.ts +12 -2
  96. package/dist/timeline-summarizer.d.ts.map +1 -1
  97. package/dist/timeline-summarizer.js +66 -4
  98. package/dist/timeline-summarizer.js.map +1 -1
  99. package/dist/trmnl/byos-server.d.ts +39 -0
  100. package/dist/trmnl/byos-server.d.ts.map +1 -0
  101. package/dist/trmnl/byos-server.js +301 -0
  102. package/dist/trmnl/byos-server.js.map +1 -0
  103. package/dist/trmnl/frame-cache.d.ts +37 -0
  104. package/dist/trmnl/frame-cache.d.ts.map +1 -0
  105. package/dist/trmnl/frame-cache.js +157 -0
  106. package/dist/trmnl/frame-cache.js.map +1 -0
  107. package/dist/trmnl/image-renderer.d.ts +27 -0
  108. package/dist/trmnl/image-renderer.d.ts.map +1 -0
  109. package/dist/trmnl/image-renderer.js +235 -0
  110. package/dist/trmnl/image-renderer.js.map +1 -0
  111. package/dist/trmnl/trmnl-settings.d.ts +96 -0
  112. package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
  113. package/dist/trmnl/trmnl-settings.js +177 -0
  114. package/dist/trmnl/trmnl-settings.js.map +1 -0
  115. package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
  116. package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
  117. package/dist/trmnl/trmnl-telemetry.js +56 -0
  118. package/dist/trmnl/trmnl-telemetry.js.map +1 -0
  119. package/dist/whisper-server-manager.d.ts +19 -0
  120. package/dist/whisper-server-manager.d.ts.map +1 -0
  121. package/dist/whisper-server-manager.js +171 -0
  122. package/dist/whisper-server-manager.js.map +1 -0
  123. package/fm-helper/Info.plist +27 -0
  124. package/package.json +13 -14
  125. package/scripts/build-fm-helper.mjs +35 -3
  126. package/src/idotmatrix/sync.py +16 -1
  127. package/src/pysync/matrix_sync_common.py +83 -0
  128. package/src/timebox/sync_ble.py +16 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * TRMNL BYOS settings — reads/writes the `trmnl` block in
3
+ * ~/.agentdeck/settings.json, preserving all other keys (same pattern as
4
+ * pixoo-settings.ts).
5
+ *
6
+ * Shape:
7
+ * "trmnl": {
8
+ * "enabled": false, // force the module on even with no device yet
9
+ * "refreshRate": 180, // seconds between device polls
10
+ * "autoRegister": true, // auto-enroll a device that hits /api/setup
11
+ * "devices": [ { mac, apiKey, friendlyId, name? } ]
12
+ * }
13
+ */
14
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
15
+ import { join } from 'path';
16
+ import { homedir } from 'os';
17
+ import { randomBytes } from 'crypto';
18
+ // Resolve lazily so tests (and explicit pinning) can redirect via
19
+ // AGENTDECK_DATA_DIR, matching apme/settings.ts and esp32-serial.ts.
20
+ function settingsDir() {
21
+ return process.env.AGENTDECK_DATA_DIR || join(homedir(), '.agentdeck');
22
+ }
23
+ function settingsPath() {
24
+ return join(settingsDir(), 'settings.json');
25
+ }
26
+ export const TRMNL_DEFAULT_REFRESH = 180;
27
+ /**
28
+ * Cadence used only while an agent is AWAITING the user. NOT used for "working":
29
+ * a battery e-ink panel deep-sleeps between polls and full-flashes the screen on
30
+ * every refresh, so we only tighten the loop when the user actually needs to act.
31
+ */
32
+ export const TRMNL_DEFAULT_REFRESH_ACTIVE = 60;
33
+ /** Floor on any cadence — too-frequent polls drain the panel battery + flash it. */
34
+ export const TRMNL_MIN_REFRESH = 30;
35
+ /**
36
+ * Seconds the firmware waits for the image download before giving up (its
37
+ * `image_url_timeout`). Generous so a slow/flaky WiFi link doesn't trip the
38
+ * device's "not responding" (WIFI_FAILED) screen. Firmware caps at ~65s, so we
39
+ * keep a healthy default (most "not responding" cycles are a lossy image GET
40
+ * timing out, not a server error — see docs/devices.md TRMNL section).
41
+ */
42
+ export const TRMNL_DEFAULT_IMAGE_TIMEOUT = 50;
43
+ /** Hard cap the firmware honors (it clamps `image_url_timeout` ~65s internally). */
44
+ export const TRMNL_MAX_IMAGE_TIMEOUT = 65;
45
+ /**
46
+ * RSSI (dBm) at or below which the panel's WiFi link is treated as weak/lossy.
47
+ * A weak link is the dominant real-world cause of "not responding" (WIFI_FAILED):
48
+ * the image download drops packets and times out. RSSI is sent by the firmware on
49
+ * every poll, so we can react per-poll. (-78 ≈ marginal; below it, retransmits
50
+ * stack up and the default window can be too tight.)
51
+ */
52
+ export const TRMNL_WEAK_RSSI_DBM = -78;
53
+ /** Image-download window served on a weak link — near the firmware cap. */
54
+ export const TRMNL_WEAK_LINK_IMAGE_TIMEOUT = 60;
55
+ /**
56
+ * Cadence for a poll. We only speed up for AWAITING (the user needs to act);
57
+ * "working" stays on the slow cadence because a deep-sleep e-ink panel can't be
58
+ * pushed and each wake full-flashes the screen + costs battery.
59
+ */
60
+ export function effectiveRefreshRate(cfg, activity) {
61
+ return Math.max(TRMNL_MIN_REFRESH, activity.awaiting > 0 ? cfg.refreshActive : cfg.refreshRate);
62
+ }
63
+ /**
64
+ * Image-download timeout (seconds) for a single poll. Widens toward the firmware
65
+ * cap when the panel reports a weak WiFi signal, so a lossy image GET still has
66
+ * the maximum window to finish before the firmware shows "not responding"
67
+ * (WIFI_FAILED) — the #1 source of TRMNL flicker on a marginal link. A strong link
68
+ * keeps the lower default so a genuinely dead link doesn't hold the radio on
69
+ * (battery). RSSI absent (older firmware) ⇒ default; the default itself is already
70
+ * generous. Clamped to [5, firmware cap].
71
+ */
72
+ export function effectiveImageTimeout(cfg, opts = {}) {
73
+ let t = cfg.imageUrlTimeout;
74
+ if (opts.rssi != null && Number.isFinite(opts.rssi) && opts.rssi <= TRMNL_WEAK_RSSI_DBM) {
75
+ t = Math.max(t, TRMNL_WEAK_LINK_IMAGE_TIMEOUT);
76
+ }
77
+ return Math.min(TRMNL_MAX_IMAGE_TIMEOUT, Math.max(5, Math.floor(t)));
78
+ }
79
+ function readSettings() {
80
+ try {
81
+ return JSON.parse(readFileSync(settingsPath(), 'utf-8'));
82
+ }
83
+ catch {
84
+ return {};
85
+ }
86
+ }
87
+ function writeSettings(settings) {
88
+ mkdirSync(settingsDir(), { recursive: true });
89
+ writeFileSync(settingsPath(), JSON.stringify(settings, null, 2) + '\n');
90
+ }
91
+ /** Load the trmnl config with defaults filled in. */
92
+ export function loadTrmnlConfig() {
93
+ const raw = (readSettings().trmnl ?? {});
94
+ const refreshRate = typeof raw.refreshRate === 'number' && raw.refreshRate >= 5 ? Math.floor(raw.refreshRate) : TRMNL_DEFAULT_REFRESH;
95
+ const refreshActive = typeof raw.refreshActive === 'number' && raw.refreshActive >= 5
96
+ ? Math.floor(raw.refreshActive)
97
+ : TRMNL_DEFAULT_REFRESH_ACTIVE;
98
+ const imageUrlTimeout = typeof raw.imageUrlTimeout === 'number' && raw.imageUrlTimeout > 0
99
+ ? Math.min(TRMNL_MAX_IMAGE_TIMEOUT, Math.floor(raw.imageUrlTimeout))
100
+ : TRMNL_DEFAULT_IMAGE_TIMEOUT;
101
+ return {
102
+ enabled: raw.enabled === true,
103
+ refreshRate,
104
+ refreshActive,
105
+ imageUrlTimeout,
106
+ autoRegister: raw.autoRegister !== false, // default on
107
+ devices: Array.isArray(raw.devices) ? raw.devices.filter((d) => d && typeof d.mac === 'string') : [],
108
+ };
109
+ }
110
+ function saveTrmnlConfig(cfg) {
111
+ const settings = readSettings();
112
+ settings.trmnl = cfg;
113
+ writeSettings(settings);
114
+ }
115
+ /**
116
+ * Normalize a MAC to a stable identity key. A canonical 12-hex address becomes
117
+ * uppercase colon-separated pairs; any other id (some firmware/cloud setups
118
+ * report non-standard or punctuated identifiers) collapses to its bare uppercase
119
+ * hex digits. Returning the raw, untrimmed string for the odd case would orphan
120
+ * or duplicate a device whose punctuation varies between polls — so we always
121
+ * derive a deterministic key from the hex content.
122
+ */
123
+ export function normalizeMac(mac) {
124
+ const hex = (mac || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
125
+ if (hex.length === 12)
126
+ return hex.match(/.{2}/g).join(':');
127
+ return hex || (mac || '').trim().toUpperCase();
128
+ }
129
+ /** True when two MAC spellings resolve to the same canonical identity. */
130
+ export function sameMac(a, b) {
131
+ return normalizeMac(a) === normalizeMac(b);
132
+ }
133
+ export function findDeviceByMac(mac) {
134
+ return loadTrmnlConfig().devices.find((d) => sameMac(d.mac, mac));
135
+ }
136
+ function genFriendlyId() {
137
+ // 6 chars from an unambiguous base32 alphabet (no 0/1/O/I).
138
+ const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
139
+ const bytes = randomBytes(6);
140
+ let out = '';
141
+ for (let i = 0; i < 6; i++)
142
+ out += alphabet[bytes[i] % alphabet.length];
143
+ return out;
144
+ }
145
+ /**
146
+ * Find or create a device record for the given MAC. Returns the device and
147
+ * whether it was newly created. Respects `autoRegister`: when off, a new MAC
148
+ * is NOT persisted and `created` is false with `device` undefined.
149
+ */
150
+ export function ensureDevice(mac, name) {
151
+ const cfg = loadTrmnlConfig();
152
+ const norm = normalizeMac(mac);
153
+ const existing = cfg.devices.find((d) => sameMac(d.mac, mac));
154
+ if (existing)
155
+ return { device: existing, created: false };
156
+ if (!cfg.autoRegister)
157
+ return { created: false };
158
+ const device = {
159
+ mac: norm,
160
+ apiKey: randomBytes(16).toString('hex'),
161
+ friendlyId: genFriendlyId(),
162
+ name,
163
+ };
164
+ cfg.devices.push(device);
165
+ saveTrmnlConfig(cfg);
166
+ return { device, created: true };
167
+ }
168
+ export function removeDevice(mac) {
169
+ const cfg = loadTrmnlConfig();
170
+ const filtered = cfg.devices.filter((d) => !sameMac(d.mac, mac));
171
+ if (filtered.length === cfg.devices.length)
172
+ return false;
173
+ cfg.devices = filtered;
174
+ saveTrmnlConfig(cfg);
175
+ return true;
176
+ }
177
+ //# sourceMappingURL=trmnl-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trmnl-settings.js","sourceRoot":"","sources":["../../src/trmnl/trmnl-settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErC,kEAAkE;AAClE,qEAAqE;AACrE,SAAS,WAAW;IAClB,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AACzE,CAAC;AACD,SAAS,YAAY;IACnB,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,eAAe,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AACzC;;;;GAIG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAC/C,oFAAoF;AACpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AACpC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAC9C,oFAAoF;AACpF,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAC1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAAE,CAAC;AACvC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,CAAC;AAyBhD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAgB,EAChB,QAA+C;IAE/C,OAAO,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,QAAQ,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AAClG,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAgB,EAAE,OAAiC,EAAE;IACzF,IAAI,CAAC,GAAG,GAAG,CAAC,eAAe,CAAC;IAC5B,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,mBAAmB,EAAE,CAAC;QACxF,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,6BAA6B,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,uBAAuB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,YAAY;IACnB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,QAAiC;IACtD,SAAS,CAAC,WAAW,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,aAAa,CAAC,YAAY,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC1E,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC,KAAK,IAAI,EAAE,CAAyB,CAAC;IACjE,MAAM,WAAW,GACf,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,IAAI,GAAG,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC;IACpH,MAAM,aAAa,GACjB,OAAO,GAAG,CAAC,aAAa,KAAK,QAAQ,IAAI,GAAG,CAAC,aAAa,IAAI,CAAC;QAC7D,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC;QAC/B,CAAC,CAAC,4BAA4B,CAAC;IACnC,MAAM,eAAe,GACnB,OAAO,GAAG,CAAC,eAAe,KAAK,QAAQ,IAAI,GAAG,CAAC,eAAe,GAAG,CAAC;QAChE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,uBAAuB,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACpE,CAAC,CAAC,2BAA2B,CAAC;IAClC,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,IAAI;QAC7B,WAAW;QACX,aAAa;QACb,eAAe;QACf,YAAY,EAAE,GAAG,CAAC,YAAY,KAAK,KAAK,EAAE,aAAa;QACvD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;KACrG,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,GAAgB;IACvC,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,KAAK,GAAG,GAAG,CAAC;IACrB,aAAa,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACnE,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE;QAAE,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,CAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5D,OAAO,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACjD,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,OAAO,CAAC,CAAS,EAAE,CAAS;IAC1C,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO,eAAe,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,aAAa;IACpB,4DAA4D;IAC5D,MAAM,QAAQ,GAAG,kCAAkC,CAAC;IACpD,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,GAAG,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxE,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,IAAa;IACrD,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9D,IAAI,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC1D,IAAI,CAAC,GAAG,CAAC,YAAY;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAEjD,MAAM,MAAM,GAAgB;QAC1B,GAAG,EAAE,IAAI;QACT,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QACvC,UAAU,EAAE,aAAa,EAAE;QAC3B,IAAI;KACL,CAAC;IACF,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzB,eAAe,CAAC,GAAG,CAAC,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;IAC9B,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IACjE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACzD,GAAG,CAAC,OAAO,GAAG,QAAQ,CAAC;IACvB,eAAe,CAAC,GAAG,CAAC,CAAC;IACrB,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,47 @@
1
+ export interface DeviceTelemetry {
2
+ /** Normalized MAC identity (matches TrmnlDevice.mac). */
3
+ mac: string;
4
+ fwVersion: string;
5
+ batteryVoltage: number | null;
6
+ rssi: number | null;
7
+ width: number | null;
8
+ height: number | null;
9
+ /** Cadence the device says it's polling at (we still dictate it on response). */
10
+ refreshRate: number | null;
11
+ userAgent: string;
12
+ /** Epoch ms of the most recent poll. */
13
+ lastSeen: number;
14
+ }
15
+ export interface TelemetryInput {
16
+ fwVersion?: string;
17
+ batteryVoltage?: number | null;
18
+ rssi?: number | null;
19
+ width?: number | null;
20
+ height?: number | null;
21
+ refreshRate?: number | null;
22
+ userAgent?: string;
23
+ }
24
+ /**
25
+ * Upsert the last-seen telemetry for a device. `now` is injected at the daemon
26
+ * boundary (defaults to wall clock) so layout/render code stays clock-free.
27
+ * Returns the previous `lastSeen` (epoch ms) so callers can detect poll gaps.
28
+ */
29
+ export declare function recordTelemetry(mac: string, t: TelemetryInput, now?: number): number | null;
30
+ /** Snapshot of all known device telemetry, most-recently-seen first. */
31
+ export declare function getTelemetry(): DeviceTelemetry[];
32
+ /** Telemetry plus derived freshness — `secondsSinceSeen` and a `stale` flag. */
33
+ export interface DeviceHealth extends DeviceTelemetry {
34
+ secondsSinceSeen: number;
35
+ /** True when the panel hasn't polled in over 2× its expected cadence. */
36
+ stale: boolean;
37
+ }
38
+ /**
39
+ * Telemetry annotated with health. A panel that hasn't polled within 2× its
40
+ * cadence is flagged `stale` so the dashboard can distinguish "panel stuck /
41
+ * offline" from "nothing changed". `now`/`refreshRate` are injected at the daemon
42
+ * boundary so this stays clock- and config-free.
43
+ */
44
+ export declare function getTelemetryHealth(refreshRate: number, now?: number): DeviceHealth[];
45
+ /** Test helper — clear the runtime map. */
46
+ export declare function _resetTelemetry(): void;
47
+ //# sourceMappingURL=trmnl-telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trmnl-telemetry.d.ts","sourceRoot":"","sources":["../../src/trmnl/trmnl-telemetry.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iFAAiF;IACjF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAgBvG;AAED,wEAAwE;AACxE,wBAAgB,YAAY,IAAI,eAAe,EAAE,CAEhD;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAa,SAAQ,eAAe;IACnD,gBAAgB,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,YAAY,EAAE,CAMhG;AAED,2CAA2C;AAC3C,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * TRMNL device telemetry — a runtime-only, per-MAC snapshot of the headers a
3
+ * BYOS panel reports on each /api/setup or /api/display poll (firmware version,
4
+ * battery voltage, WiFi signal, panel resolution). This is NOT persisted to
5
+ * settings.json: polls arrive every ~180s and writing the settings file on each
6
+ * one would thrash it and race the user's manual edits. The daemon status
7
+ * snapshot reads this map for diagnostics; it's lost on daemon restart, which is
8
+ * fine — the next poll repopulates it.
9
+ */
10
+ import { normalizeMac } from './trmnl-settings.js';
11
+ const telemetry = new Map();
12
+ /**
13
+ * Upsert the last-seen telemetry for a device. `now` is injected at the daemon
14
+ * boundary (defaults to wall clock) so layout/render code stays clock-free.
15
+ * Returns the previous `lastSeen` (epoch ms) so callers can detect poll gaps.
16
+ */
17
+ export function recordTelemetry(mac, t, now = Date.now()) {
18
+ const key = normalizeMac(mac);
19
+ if (!key)
20
+ return null;
21
+ const prevLastSeen = telemetry.get(key)?.lastSeen ?? null;
22
+ telemetry.set(key, {
23
+ mac: key,
24
+ fwVersion: t.fwVersion ?? '',
25
+ batteryVoltage: t.batteryVoltage ?? null,
26
+ rssi: t.rssi ?? null,
27
+ width: t.width ?? null,
28
+ height: t.height ?? null,
29
+ refreshRate: t.refreshRate ?? null,
30
+ userAgent: t.userAgent ?? '',
31
+ lastSeen: now,
32
+ });
33
+ return prevLastSeen;
34
+ }
35
+ /** Snapshot of all known device telemetry, most-recently-seen first. */
36
+ export function getTelemetry() {
37
+ return [...telemetry.values()].sort((a, b) => b.lastSeen - a.lastSeen);
38
+ }
39
+ /**
40
+ * Telemetry annotated with health. A panel that hasn't polled within 2× its
41
+ * cadence is flagged `stale` so the dashboard can distinguish "panel stuck /
42
+ * offline" from "nothing changed". `now`/`refreshRate` are injected at the daemon
43
+ * boundary so this stays clock- and config-free.
44
+ */
45
+ export function getTelemetryHealth(refreshRate, now = Date.now()) {
46
+ const staleAfterMs = Math.max(30, refreshRate) * 2 * 1000;
47
+ return getTelemetry().map((t) => {
48
+ const ageMs = now - t.lastSeen;
49
+ return { ...t, secondsSinceSeen: Math.round(ageMs / 1000), stale: ageMs > staleAfterMs };
50
+ });
51
+ }
52
+ /** Test helper — clear the runtime map. */
53
+ export function _resetTelemetry() {
54
+ telemetry.clear();
55
+ }
56
+ //# sourceMappingURL=trmnl-telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trmnl-telemetry.js","sourceRoot":"","sources":["../../src/trmnl/trmnl-telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AA2BnD,MAAM,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;AAErD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,CAAiB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACtF,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,QAAQ,IAAI,IAAI,CAAC;IAC1D,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE;QACjB,GAAG,EAAE,GAAG;QACR,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;QAC5B,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,IAAI;QACxC,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI;QACpB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI;QACtB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI;QACxB,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI;QAClC,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;QAC5B,QAAQ,EAAE,GAAG;KACd,CAAC,CAAC;IACH,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,YAAY;IAC1B,OAAO,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;AACzE,CAAC;AASD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC9E,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC1D,OAAO,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC;QAC/B,OAAO,EAAE,GAAG,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,YAAY,EAAE,CAAC;IAC3F,CAAC,CAAC,CAAC;AACL,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,eAAe;IAC7B,SAAS,CAAC,KAAK,EAAE,CAAC;AACpB,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Whisper-server singleton manager.
3
+ *
4
+ * Fixed port 9100 — one server shared across all agentdeck sessions.
5
+ * Info file at ~/.agentdeck/whisper-server.json for discovery.
6
+ * Last session to exit kills the server.
7
+ */
8
+ export declare const WHISPER_SERVER_PORT = 9100;
9
+ /**
10
+ * Ensure a whisper-server is running on the singleton port.
11
+ * Returns port on success, null if unavailable (binary missing, model missing, etc).
12
+ */
13
+ export declare function ensureWhisperServer(serverBin: string, modelPath: string): Promise<number | null>;
14
+ /**
15
+ * Release the whisper-server if no other sessions remain.
16
+ * Call AFTER deregisterSession() so the count is already decremented.
17
+ */
18
+ export declare function releaseWhisperServer(): void;
19
+ //# sourceMappingURL=whisper-server-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whisper-server-manager.d.ts","sourceRoot":"","sources":["../src/whisper-server-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AA2DxC;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6ExB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAiB3C"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Whisper-server singleton manager.
3
+ *
4
+ * Fixed port 9100 — one server shared across all agentdeck sessions.
5
+ * Info file at ~/.agentdeck/whisper-server.json for discovery.
6
+ * Last session to exit kills the server.
7
+ */
8
+ import { spawn } from 'child_process';
9
+ import { readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, existsSync, } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+ import { randomUUID } from 'crypto';
13
+ import { listActive } from './session-registry.js';
14
+ import { debug } from './logger.js';
15
+ export const WHISPER_SERVER_PORT = 9100;
16
+ const INFO_DIR = join(homedir(), '.agentdeck');
17
+ const INFO_FILE = join(INFO_DIR, 'whisper-server.json');
18
+ function ensureDir() {
19
+ if (!existsSync(INFO_DIR)) {
20
+ mkdirSync(INFO_DIR, { recursive: true });
21
+ }
22
+ }
23
+ function readInfo() {
24
+ try {
25
+ return JSON.parse(readFileSync(INFO_FILE, 'utf-8'));
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function writeInfo(info) {
32
+ ensureDir();
33
+ const tmp = join(INFO_DIR, `.whisper-server.${randomUUID()}.tmp`);
34
+ writeFileSync(tmp, JSON.stringify(info, null, 2), 'utf-8');
35
+ renameSync(tmp, INFO_FILE);
36
+ }
37
+ function removeInfo() {
38
+ try {
39
+ unlinkSync(INFO_FILE);
40
+ }
41
+ catch { /* already gone */ }
42
+ }
43
+ function isProcessAlive(pid) {
44
+ try {
45
+ process.kill(pid, 0);
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ async function isServerResponding(port) {
53
+ try {
54
+ await fetch(`http://127.0.0.1:${port}`, { signal: AbortSignal.timeout(1000) });
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ async function pollUntilReady(port, maxMs) {
62
+ const start = Date.now();
63
+ while (Date.now() - start < maxMs) {
64
+ if (await isServerResponding(port))
65
+ return true;
66
+ await new Promise((r) => setTimeout(r, 500));
67
+ }
68
+ return false;
69
+ }
70
+ /**
71
+ * Ensure a whisper-server is running on the singleton port.
72
+ * Returns port on success, null if unavailable (binary missing, model missing, etc).
73
+ */
74
+ export async function ensureWhisperServer(serverBin, modelPath) {
75
+ if (!existsSync(serverBin)) {
76
+ debug('WhisperMgr', `whisper-server not found at ${serverBin}, skipping`);
77
+ return null;
78
+ }
79
+ // 1. Check existing info file
80
+ const info = readInfo();
81
+ if (info) {
82
+ if (isProcessAlive(info.pid)) {
83
+ // PID alive — check if server is responding
84
+ if (await isServerResponding(info.port)) {
85
+ debug('WhisperMgr', `Reusing existing server (pid=${info.pid}, port=${info.port})`);
86
+ return info.port;
87
+ }
88
+ // PID alive but not responding — model may still be loading, poll 30s
89
+ debug('WhisperMgr', `Server pid=${info.pid} alive but not responding, waiting for model load...`);
90
+ if (await pollUntilReady(info.port, 30_000)) {
91
+ debug('WhisperMgr', `Server became ready (pid=${info.pid}, port=${info.port})`);
92
+ return info.port;
93
+ }
94
+ // Still not responding after 30s — stale
95
+ debug('WhisperMgr', `Server pid=${info.pid} never responded, treating as stale`);
96
+ }
97
+ // PID dead or stale — clean up
98
+ removeInfo();
99
+ }
100
+ // 2. Spawn new server (detached so it survives parent exit)
101
+ debug('WhisperMgr', `Starting whisper-server on port ${WHISPER_SERVER_PORT}...`);
102
+ const args = [
103
+ '--model', modelPath,
104
+ '--port', String(WHISPER_SERVER_PORT),
105
+ '--host', '127.0.0.1',
106
+ '-l', 'auto',
107
+ '--no-timestamps',
108
+ '--convert',
109
+ '--prompt', 'coding, programming, Claude, terminal, git, function, component, API',
110
+ ];
111
+ const proc = spawn(serverBin, args, {
112
+ stdio: 'ignore',
113
+ detached: true,
114
+ });
115
+ proc.unref();
116
+ const pid = proc.pid;
117
+ if (!pid) {
118
+ debug('WhisperMgr', 'Failed to spawn whisper-server (no pid)');
119
+ return null;
120
+ }
121
+ // 3. Brief wait to detect immediate crash (e.g. port conflict)
122
+ await new Promise((r) => setTimeout(r, 200));
123
+ if (!isProcessAlive(pid)) {
124
+ debug('WhisperMgr', `whisper-server died immediately (pid=${pid}), checking for existing server...`);
125
+ // Another process may have won the port race — re-check info file
126
+ const otherInfo = readInfo();
127
+ if (otherInfo && isProcessAlive(otherInfo.pid) && await isServerResponding(otherInfo.port)) {
128
+ debug('WhisperMgr', `Found winner server (pid=${otherInfo.pid}, port=${otherInfo.port})`);
129
+ return otherInfo.port;
130
+ }
131
+ removeInfo();
132
+ return null;
133
+ }
134
+ // 4. Write info file
135
+ writeInfo({ pid, port: WHISPER_SERVER_PORT, modelPath, startedAt: new Date().toISOString() });
136
+ // 5. Poll until ready (up to 30s for model loading)
137
+ if (await pollUntilReady(WHISPER_SERVER_PORT, 30_000)) {
138
+ debug('WhisperMgr', `whisper-server ready on port ${WHISPER_SERVER_PORT} (pid=${pid})`);
139
+ return WHISPER_SERVER_PORT;
140
+ }
141
+ debug('WhisperMgr', 'whisper-server failed to become ready within 30s');
142
+ return null;
143
+ }
144
+ /**
145
+ * Release the whisper-server if no other sessions remain.
146
+ * Call AFTER deregisterSession() so the count is already decremented.
147
+ */
148
+ export function releaseWhisperServer() {
149
+ const activeSessions = listActive();
150
+ if (activeSessions.length > 0) {
151
+ debug('WhisperMgr', `Keeping whisper-server alive (${activeSessions.length} session(s) remain)`);
152
+ return;
153
+ }
154
+ const info = readInfo();
155
+ if (!info)
156
+ return;
157
+ debug('WhisperMgr', `Killing whisper-server (pid=${info.pid})`);
158
+ try {
159
+ process.kill(info.pid, 'SIGTERM');
160
+ }
161
+ catch { /* already dead */ }
162
+ // Force kill after 3s
163
+ setTimeout(() => {
164
+ try {
165
+ process.kill(info.pid, 'SIGKILL');
166
+ }
167
+ catch { /* already dead */ }
168
+ }, 3000);
169
+ removeInfo();
170
+ }
171
+ //# sourceMappingURL=whisper-server-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whisper-server-manager.js","sourceRoot":"","sources":["../src/whisper-server-manager.ts"],"names":[],"mappings":"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"}
@@ -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.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,18 +24,7 @@
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
- },
36
27
  "dependencies": {
37
- "@agentdeck/hooks": "workspace:*",
38
- "@agentdeck/shared": "workspace:*",
39
28
  "@anthropic-ai/sdk": "^0.111.0",
40
29
  "bonjour-service": "^1.3.0",
41
30
  "commander": "^14.0.3",
@@ -43,7 +32,9 @@
43
32
  "qrcode": "^1.5.4",
44
33
  "sharp": "^0.34.5",
45
34
  "strip-ansi": "^7.2.0",
46
- "ws": "^8.20.0"
35
+ "ws": "^8.20.0",
36
+ "@agentdeck/hooks": "1.0.7",
37
+ "@agentdeck/shared": "1.0.7"
47
38
  },
48
39
  "optionalDependencies": {
49
40
  "@picovoice/porcupine-node": "^4.0.2",
@@ -59,5 +50,13 @@
59
50
  "@types/qrcode": "^1.5.6",
60
51
  "@types/ws": "^8.18.1",
61
52
  "typescript": "^6.0.3"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc",
56
+ "build:fm-helper": "node scripts/build-fm-helper.mjs",
57
+ "dev": "tsc --watch",
58
+ "clean": "rm -rf dist",
59
+ "typecheck": "tsc --noEmit",
60
+ "start": "node dist/cli.js claude"
62
61
  }
63
- }
62
+ }
@@ -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 out = join(root, 'assets', 'fm-helper', 'agentdeck-fm-helper');
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
- execFileSync(swiftc, ['-parse-as-library', '-sdk', sdk, '-target', target, source, '-o', out], { stdio: 'inherit' });
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}`);