@trillboards/edge-sdk 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +4 -3
  2. package/deploy/docker/Dockerfile.cloud-sense +117 -0
  3. package/deploy/docker/Dockerfile.cpu +30 -21
  4. package/deploy/docker/Dockerfile.cuda +30 -21
  5. package/deploy/docker/Dockerfile.openvino +30 -20
  6. package/deploy/docker/README.md +9 -4
  7. package/deploy/docker/connector-entrypoint.sh +122 -0
  8. package/deploy/helm/README.md +1 -1
  9. package/deploy/k8s/daemonset.yaml +1 -1
  10. package/deploy/macos/README.md +161 -0
  11. package/deploy/macos/TrillboardsSetupApp.m +603 -0
  12. package/deploy/macos/build-macos-pkg.sh +588 -0
  13. package/deploy/macos/com.trillboards.edge.plist +62 -0
  14. package/deploy/macos/com.trillboards.litertlm.plist +45 -0
  15. package/deploy/macos/pair.command +519 -0
  16. package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
  17. package/deploy/macos/scripts/postinstall +153 -0
  18. package/deploy/macos/scripts/setup-litertlm.sh +272 -0
  19. package/deploy/macos/test-install-pair.sh +383 -0
  20. package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
  21. package/deploy/systemd/install.sh +25 -15
  22. package/deploy/systemd/trillboards-edge.service +4 -1
  23. package/deploy/windows/README.md +70 -0
  24. package/deploy/windows/build-windows.ps1 +225 -0
  25. package/deploy/windows/bundle-windows-installer.ps1 +153 -0
  26. package/deploy/windows/check-feeds.js +26 -0
  27. package/deploy/windows/e2e/fixtures/person.jpg +0 -0
  28. package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
  29. package/deploy/windows/install-update.ps1 +101 -0
  30. package/deploy/windows/installer.nsi +138 -0
  31. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  32. package/deploy/windows/register-task.ps1 +44 -0
  33. package/deploy/windows/run-connector.ps1 +69 -0
  34. package/deploy/windows/setup-litertlm.ps1 +361 -0
  35. package/deploy/windows/stop-existing.ps1 +72 -0
  36. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  37. package/deploy/windows/trillboards-edge.cmd +33 -0
  38. package/deploy/windows/trillboards-pair.cmd +276 -0
  39. package/deploy/windows/trillboards-setup.ps1 +456 -0
  40. package/deploy/windows/trillboards-setup.vbs +13 -0
  41. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  42. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  43. package/dist/AudienceAnalyzeClient.js +246 -0
  44. package/dist/AudienceAnalyzeClient.js.map +1 -0
  45. package/dist/ChannelProber.d.ts +150 -0
  46. package/dist/ChannelProber.d.ts.map +1 -0
  47. package/dist/ChannelProber.js +887 -0
  48. package/dist/ChannelProber.js.map +1 -0
  49. package/dist/CloudEscalationPolicy.d.ts +143 -0
  50. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  51. package/dist/CloudEscalationPolicy.js +137 -0
  52. package/dist/CloudEscalationPolicy.js.map +1 -0
  53. package/dist/CloudSensingManager.d.ts +533 -0
  54. package/dist/CloudSensingManager.d.ts.map +1 -0
  55. package/dist/CloudSensingManager.js +1254 -0
  56. package/dist/CloudSensingManager.js.map +1 -0
  57. package/dist/DiscoveryReportClient.d.ts +66 -0
  58. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  59. package/dist/DiscoveryReportClient.js +85 -0
  60. package/dist/DiscoveryReportClient.js.map +1 -0
  61. package/dist/EdgeAgent.d.ts +12 -2
  62. package/dist/EdgeAgent.d.ts.map +1 -1
  63. package/dist/EdgeAgent.js +55 -13
  64. package/dist/EdgeAgent.js.map +1 -1
  65. package/dist/LiteRtVlmProvider.d.ts +193 -0
  66. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  67. package/dist/LiteRtVlmProvider.js +436 -0
  68. package/dist/LiteRtVlmProvider.js.map +1 -0
  69. package/dist/MosaicSource.d.ts +108 -0
  70. package/dist/MosaicSource.d.ts.map +1 -0
  71. package/dist/MosaicSource.js +274 -0
  72. package/dist/MosaicSource.js.map +1 -0
  73. package/dist/OnvifClient.d.ts +83 -0
  74. package/dist/OnvifClient.d.ts.map +1 -0
  75. package/dist/OnvifClient.js +209 -0
  76. package/dist/OnvifClient.js.map +1 -0
  77. package/dist/PairClient.d.ts +106 -0
  78. package/dist/PairClient.d.ts.map +1 -0
  79. package/dist/PairClient.js +186 -0
  80. package/dist/PairClient.js.map +1 -0
  81. package/dist/RecorderAddressRecovery.d.ts +53 -0
  82. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  83. package/dist/RecorderAddressRecovery.js +359 -0
  84. package/dist/RecorderAddressRecovery.js.map +1 -0
  85. package/dist/SceneChangeDetector.d.ts +291 -0
  86. package/dist/SceneChangeDetector.d.ts.map +1 -0
  87. package/dist/SceneChangeDetector.js +682 -0
  88. package/dist/SceneChangeDetector.js.map +1 -0
  89. package/dist/SensingProfileClient.d.ts +59 -0
  90. package/dist/SensingProfileClient.d.ts.map +1 -0
  91. package/dist/SensingProfileClient.js +78 -0
  92. package/dist/SensingProfileClient.js.map +1 -0
  93. package/dist/SetupSessionClient.d.ts +114 -0
  94. package/dist/SetupSessionClient.d.ts.map +1 -0
  95. package/dist/SetupSessionClient.js +95 -0
  96. package/dist/SetupSessionClient.js.map +1 -0
  97. package/dist/VlmAdmissionGate.d.ts +126 -0
  98. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  99. package/dist/VlmAdmissionGate.js +166 -0
  100. package/dist/VlmAdmissionGate.js.map +1 -0
  101. package/dist/autoSense.d.ts +216 -0
  102. package/dist/autoSense.d.ts.map +1 -0
  103. package/dist/autoSense.js +521 -0
  104. package/dist/autoSense.js.map +1 -0
  105. package/dist/cli.d.ts +27 -1
  106. package/dist/cli.d.ts.map +1 -1
  107. package/dist/cli.js +511 -21
  108. package/dist/cli.js.map +1 -1
  109. package/dist/combinedSense.d.ts +128 -0
  110. package/dist/combinedSense.d.ts.map +1 -0
  111. package/dist/combinedSense.js +261 -0
  112. package/dist/combinedSense.js.map +1 -0
  113. package/dist/config.d.ts +351 -0
  114. package/dist/config.d.ts.map +1 -1
  115. package/dist/config.js +99 -2
  116. package/dist/config.js.map +1 -1
  117. package/dist/connectorCadence.d.ts +135 -0
  118. package/dist/connectorCadence.d.ts.map +1 -0
  119. package/dist/connectorCadence.js +151 -0
  120. package/dist/connectorCadence.js.map +1 -0
  121. package/dist/dataDir.d.ts +33 -0
  122. package/dist/dataDir.d.ts.map +1 -0
  123. package/dist/dataDir.js +76 -0
  124. package/dist/dataDir.js.map +1 -0
  125. package/dist/deployedCadence.d.ts +167 -0
  126. package/dist/deployedCadence.d.ts.map +1 -0
  127. package/dist/deployedCadence.js +209 -0
  128. package/dist/deployedCadence.js.map +1 -0
  129. package/dist/deployedCadenceSource.d.ts +48 -0
  130. package/dist/deployedCadenceSource.d.ts.map +1 -0
  131. package/dist/deployedCadenceSource.js +74 -0
  132. package/dist/deployedCadenceSource.js.map +1 -0
  133. package/dist/diagnose.d.ts +1 -1
  134. package/dist/diagnose.d.ts.map +1 -1
  135. package/dist/diagnose.js +13 -12
  136. package/dist/diagnose.js.map +1 -1
  137. package/dist/discovery.d.ts +131 -0
  138. package/dist/discovery.d.ts.map +1 -0
  139. package/dist/discovery.js +499 -0
  140. package/dist/discovery.js.map +1 -0
  141. package/dist/gemmaModelOta.d.ts +118 -0
  142. package/dist/gemmaModelOta.d.ts.map +1 -0
  143. package/dist/gemmaModelOta.js +291 -0
  144. package/dist/gemmaModelOta.js.map +1 -0
  145. package/dist/index.d.ts +16 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +40 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/init.d.ts.map +1 -1
  150. package/dist/init.js +41 -18
  151. package/dist/init.js.map +1 -1
  152. package/dist/keepAwake.d.ts +33 -0
  153. package/dist/keepAwake.d.ts.map +1 -0
  154. package/dist/keepAwake.js +54 -0
  155. package/dist/keepAwake.js.map +1 -0
  156. package/dist/legacyRecorderProfile.d.ts +20 -0
  157. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  158. package/dist/legacyRecorderProfile.js +27 -0
  159. package/dist/legacyRecorderProfile.js.map +1 -0
  160. package/dist/localInferenceSchema.d.ts +132 -0
  161. package/dist/localInferenceSchema.d.ts.map +1 -0
  162. package/dist/localInferenceSchema.js +274 -0
  163. package/dist/localInferenceSchema.js.map +1 -0
  164. package/dist/localSenseE2eHarness.d.ts +101 -0
  165. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  166. package/dist/localSenseE2eHarness.js +279 -0
  167. package/dist/localSenseE2eHarness.js.map +1 -0
  168. package/dist/localSensing.d.ts +268 -0
  169. package/dist/localSensing.d.ts.map +1 -0
  170. package/dist/localSensing.js +492 -0
  171. package/dist/localSensing.js.map +1 -0
  172. package/dist/modelIntegrity.d.ts +59 -0
  173. package/dist/modelIntegrity.d.ts.map +1 -0
  174. package/dist/modelIntegrity.js +165 -0
  175. package/dist/modelIntegrity.js.map +1 -0
  176. package/dist/pairVerify.d.ts +100 -0
  177. package/dist/pairVerify.d.ts.map +1 -0
  178. package/dist/pairVerify.js +296 -0
  179. package/dist/pairVerify.js.map +1 -0
  180. package/dist/qrScanner.d.ts +33 -0
  181. package/dist/qrScanner.d.ts.map +1 -0
  182. package/dist/qrScanner.js +103 -0
  183. package/dist/qrScanner.js.map +1 -0
  184. package/dist/runtimePaths.d.ts +10 -0
  185. package/dist/runtimePaths.d.ts.map +1 -0
  186. package/dist/runtimePaths.js +79 -0
  187. package/dist/runtimePaths.js.map +1 -0
  188. package/dist/sensingRunMode.d.ts +68 -0
  189. package/dist/sensingRunMode.d.ts.map +1 -0
  190. package/dist/sensingRunMode.js +346 -0
  191. package/dist/sensingRunMode.js.map +1 -0
  192. package/dist/setupMode.d.ts +98 -0
  193. package/dist/setupMode.d.ts.map +1 -0
  194. package/dist/setupMode.js +828 -0
  195. package/dist/setupMode.js.map +1 -0
  196. package/dist/setupProgress.d.ts +31 -0
  197. package/dist/setupProgress.d.ts.map +1 -0
  198. package/dist/setupProgress.js +173 -0
  199. package/dist/setupProgress.js.map +1 -0
  200. package/package.json +13 -4
package/dist/cli.js CHANGED
@@ -34,35 +34,166 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.isPermissionError = isPermissionError;
38
+ exports.configWritePermissionMessage = configWritePermissionMessage;
39
+ exports.writePairedConfig = writePairedConfig;
37
40
  const config_1 = require("./config");
38
- const EdgeAgent_1 = require("./EdgeAgent");
39
- const init_1 = require("./init");
40
- const status_1 = require("./status");
41
- const diagnose_1 = require("./diagnose");
42
- const demo_1 = require("./demo");
41
+ const autoSense_1 = require("./autoSense");
42
+ const sensingRunMode_1 = require("./sensingRunMode");
43
+ const gemmaModelOta_1 = require("./gemmaModelOta");
44
+ const keepAwake_1 = require("./keepAwake");
45
+ const runtimePaths_1 = require("./runtimePaths");
46
+ const modelIntegrity_1 = require("./modelIntegrity");
47
+ const PairClient_1 = require("./PairClient");
48
+ const pairVerify_1 = require("./pairVerify");
43
49
  const path = __importStar(require("path"));
44
50
  const fs = __importStar(require("fs"));
45
51
  const https = __importStar(require("https"));
46
- const VERSION = '0.2.3';
52
+ const os = __importStar(require("os"));
53
+ const crypto_1 = require("crypto");
54
+ // NOTE: the heavyweight command handlers (`start` → EdgeAgent → edge-sensing →
55
+ // onnxruntime-node, plus init/status/diagnose/demo) are loaded LAZILY inside
56
+ // their switch arms rather than as top-level imports. This keeps the
57
+ // cloud-sensing + pair entrypoints (the macOS/edge "cloud-sense" connector
58
+ // installer surface) free of the 200 MB+ ONNX runtime: a bundled-runtime build
59
+ // that ships ONLY edge-core + edge-platform-rtsp + edge-sdk can run
60
+ // `cloud-sense`, `pair`, `version`, and `help` without the local-inference
61
+ // stack ever being required. `mcp` was already lazy-required for the same
62
+ // reason.
63
+ // Source the connector version from package.json (the single source of truth)
64
+ // so `version`/`--version`/help and the pairing log can never drift from the
65
+ // published package. dist/cli.js lives in dist/, so ../package.json resolves to
66
+ // the edge-sdk package root both in-repo and in the shipped .pkg (where
67
+ // build-macos-pkg.sh stages package.json next to dist/) — and npm always ships
68
+ // package.json. Required at runtime (commonjs), like the lazy command handlers.
69
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
70
+ const VERSION = require('../package.json').version;
47
71
  async function main() {
48
72
  const args = process.argv.slice(2);
49
73
  const command = args[0];
50
74
  switch (command) {
51
- case 'demo':
52
- await (0, demo_1.runDemo)();
75
+ case 'demo': {
76
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
77
+ const { runDemo } = require('./demo');
78
+ await runDemo();
53
79
  break;
54
- case 'init':
55
- await (0, init_1.runInit)();
80
+ }
81
+ case 'init': {
82
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
83
+ const { runInit } = require('./init');
84
+ await runInit();
56
85
  break;
86
+ }
57
87
  case 'start':
58
88
  await startAgent(args.slice(1));
59
89
  break;
60
- case 'status':
61
- await (0, status_1.runStatus)(args.slice(1));
90
+ case 'cloud-sense':
91
+ (0, keepAwake_1.startKeepAwake)();
92
+ await startCloudSensing(args.slice(1));
93
+ break;
94
+ case 'local-sense':
95
+ (0, keepAwake_1.startKeepAwake)();
96
+ await startLocalSensing(args.slice(1));
97
+ break;
98
+ case 'auto-sense':
99
+ // Prefer on-device local sensing and wait for LiteRT-LM when it is still
100
+ // provisioning. Cloud inference is available only through explicit config;
101
+ // the supervisor starts local automatically with NO restart once ready.
102
+ // The supervisor owns the lifecycle + builds the SAME managers cloud-sense /
103
+ // local-sense use (see autoSense.ts + sensingRunMode.ts).
104
+ (0, keepAwake_1.startKeepAwake)();
105
+ await (0, autoSense_1.runAutoSense)(args.slice(1));
106
+ break;
107
+ case 'sense':
108
+ case 'combined-sense': {
109
+ (0, keepAwake_1.startKeepAwake)();
110
+ // APK parity: run the on-device ONNX ZOO (EdgeAgent) AND the VLM sensing
111
+ // manager (auto-detected local/cloud, like auto-sense) CONCURRENTLY in ONE
112
+ // process. Lazy-require like `start` so the cloud-sense/pair bundle stays
113
+ // free of the ONNX runtime (combinedSense → EdgeAgent → onnxruntime-node).
114
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
115
+ const { runCombinedSense } = require('./combinedSense');
116
+ await runCombinedSense(args.slice(1));
117
+ break;
118
+ }
119
+ case 'pair':
120
+ await runPair(args.slice(1));
121
+ break;
122
+ case 'setup': {
123
+ // Validate-first onboarding (direction-inverted pairing): WE display a
124
+ // short code, the owner claims it in the portal, the portal drives
125
+ // discovery/validation through our setup session, and the finalized
126
+ // config lands here — no screens exist until streams are PROVEN. Exit
127
+ // codes: 0 config written (wrapper starts the service), 2 session
128
+ // expired/timeout (re-run for a fresh code), 1 hard error.
129
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
130
+ const { runSetupMode } = require('./setupMode');
131
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
132
+ const { createSetupProgressReporter } = require('./setupProgress');
133
+ const setupArgs = args.slice(1);
134
+ const statusFile = getArg(setupArgs, '--status-file');
135
+ const rc = await runSetupMode({
136
+ apiBaseUrl: getArg(setupArgs, '--api-base') || process.env.TRILLBOARDS_API_BASE || 'https://api.trillboards.com',
137
+ portalBaseUrl: getArg(setupArgs, '--portal-base') || process.env.TRILLBOARDS_PORTAL_BASE || 'https://trillboards.com',
138
+ configPath: getArg(setupArgs, '--config') || 'trillboards.config.json',
139
+ connectorVersion: VERSION,
140
+ ffmpegPath: (0, runtimePaths_1.resolveFfmpegBinaryPath)(getArg(setupArgs, '--ffmpeg')),
141
+ ...(statusFile ? { progress: createSetupProgressReporter(statusFile) } : {}),
142
+ });
143
+ process.exit(rc);
144
+ break;
145
+ }
146
+ case 'setup-status': {
147
+ // Internal platform-wrapper command. It advances the same credential-free
148
+ // progress file after setup mode hands off to model/service bootstrap.
149
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
150
+ const { SETUP_PROGRESS_STAGES, writeSetupProgressFile } = require('./setupProgress');
151
+ const statusArgs = args.slice(1);
152
+ const statusFile = getArg(statusArgs, '--status-file');
153
+ const stage = getArg(statusArgs, '--stage');
154
+ if (!statusFile || !stage || !SETUP_PROGRESS_STAGES.includes(stage)) {
155
+ process.exit(2);
156
+ }
157
+ const cameraCountRaw = getArg(statusArgs, '--camera-count');
158
+ writeSetupProgressFile(statusFile, {
159
+ stage,
160
+ title: getArg(statusArgs, '--title') || 'Camera setup',
161
+ message: getArg(statusArgs, '--message') || '',
162
+ ...(cameraCountRaw ? { cameraCount: Number(cameraCountRaw) } : {}),
163
+ ...(stage === 'error' || stage === 'local_ai_error' ? { recoverable: true } : {}),
164
+ });
165
+ break;
166
+ }
167
+ case 'verify-connection': {
168
+ // One authenticated round-trip proving the SAVED pairing works from this
169
+ // machine (network + token). The install wrappers (trillboards-pair.cmd /
170
+ // pair.command) branch on its exit codes — see pairVerify.ts for the
171
+ // contract. Lazy-require to keep the pair hot path lean.
172
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
173
+ const { runVerifyConnection } = require('./pairVerify');
174
+ await runVerifyConnection(args.slice(1));
62
175
  break;
63
- case 'diagnose':
64
- await (0, diagnose_1.runDiagnose)(args.slice(1));
176
+ }
177
+ case 'discover': {
178
+ // Lazy-require: discovery (UDP WS-Discovery + NVR channel-range expansion)
179
+ // is a small, optional helper — keep it off the cloud-sense/pair hot path.
180
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
181
+ const { runDiscover } = require('./discovery');
182
+ await runDiscover(args.slice(1));
183
+ break;
184
+ }
185
+ case 'status': {
186
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
187
+ const { runStatus } = require('./status');
188
+ await runStatus(args.slice(1));
189
+ break;
190
+ }
191
+ case 'diagnose': {
192
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
193
+ const { runDiagnose } = require('./diagnose');
194
+ await runDiagnose(args.slice(1));
65
195
  break;
196
+ }
66
197
  case 'mcp':
67
198
  await startMcpServer();
68
199
  break;
@@ -95,7 +226,13 @@ async function startAgent(args) {
95
226
  console.log(`Trillboards Edge AI SDK v${VERSION}`);
96
227
  console.log(`Loading config: ${configPath}`);
97
228
  const config = (0, config_1.loadConfig)(configPath);
98
- const agent = new EdgeAgent_1.EdgeAgent(config);
229
+ // Lazy-require so the cloud-sense/pair connector bundle (which omits the
230
+ // local ONNX inference stack) never pays for EdgeAgent → edge-sensing →
231
+ // onnxruntime-node at module-load. `start` is the full-agent (local
232
+ // inference) entrypoint and needs it; cloud-sense does not.
233
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
234
+ const { EdgeAgent } = require('./EdgeAgent');
235
+ const agent = new EdgeAgent(config);
99
236
  agent.on('started', ({ fingerprint, tier }) => {
100
237
  console.log(`\n Device: ${fingerprint}`);
101
238
  console.log(` Tier: TIER_${tier}`);
@@ -111,6 +248,314 @@ async function startAgent(args) {
111
248
  // Keep the process running
112
249
  await new Promise(() => { });
113
250
  }
251
+ /**
252
+ * Fire a best-effort ONVIF LAN scan + report on connector startup so the portal's
253
+ * "Find my cameras" can surface the venue's real cameras. Non-blocking, never
254
+ * throws — returns a promise the caller can settle before a no-feeds exit. Uses
255
+ * any feed's device token (all share the owner). Lazy-requires the reporter so the
256
+ * light entrypoints don't pay for it unless cloud-sensing actually runs.
257
+ */
258
+ function kickStartupDiscovery(config) {
259
+ try {
260
+ const deviceToken = (config.cameras || []).find((c) => c && c.deviceToken)?.deviceToken || config.deviceToken;
261
+ if (!config.apiBaseUrl || !deviceToken)
262
+ return Promise.resolve();
263
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
264
+ const { discoverAndReport } = require('./DiscoveryReportClient');
265
+ return discoverAndReport({
266
+ apiBaseUrl: config.apiBaseUrl,
267
+ deviceToken,
268
+ log: (m) => console.log(m),
269
+ }).then(() => undefined);
270
+ }
271
+ catch {
272
+ return Promise.resolve();
273
+ }
274
+ }
275
+ /**
276
+ * Thin cloud-sensing run mode: RTSP frame → POST /v2/earner/audience-analyze.
277
+ * No local ONNX. Supports multi-channel via `cameras: [...]` (N loops, one
278
+ * token + one profile per channel).
279
+ */
280
+ async function startCloudSensing(args) {
281
+ const configIdx = args.indexOf('--config');
282
+ const configPath = configIdx >= 0 && args[configIdx + 1] ? args[configIdx + 1] : 'trillboards.config.json';
283
+ console.log(`Trillboards Edge AI SDK v${VERSION} — cloud-sensing mode`);
284
+ console.log(`Loading config: ${configPath}`);
285
+ const config = (0, config_1.loadConfig)(configPath);
286
+ // Kick a best-effort ONVIF LAN scan on startup so the portal's "Find my
287
+ // cameras" surfaces the venue's real cameras — CRUCIALLY even when feeds were
288
+ // paired WITHOUT an rtspUrl (creds-local: the owner didn't know the DVR's IP),
289
+ // which yields zero sensable feeds below. Fire-and-forget; we only settle it
290
+ // before a no-feeds exit so the scan actually gets to POST.
291
+ const discoveryDone = kickStartupDiscovery(config);
292
+ // Build the manager (feed resolution + banner + event logging) via the shared
293
+ // helper — the SAME construction auto-sense uses. Null ⇒ no feeds configured.
294
+ const manager = (0, sensingRunMode_1.buildCloudSensingManager)(config, undefined, { configPath });
295
+ if (!manager) {
296
+ // No sensable feeds (e.g. all feeds are creds-local, no rtspUrl). Let the
297
+ // startup discovery finish + report so the owner still sees their cameras in
298
+ // the portal, then exit.
299
+ await discoveryDone;
300
+ process.exit(1);
301
+ return;
302
+ }
303
+ const shutdown = async () => {
304
+ console.log('\nStopping cloud-sensing…');
305
+ await manager.stop();
306
+ process.exit(0);
307
+ };
308
+ process.on('SIGINT', shutdown);
309
+ process.on('SIGTERM', shutdown);
310
+ await manager.start();
311
+ console.log('Cloud-sensing started. Press Ctrl+C to stop.');
312
+ // Keep the process running
313
+ await new Promise(() => { });
314
+ }
315
+ /**
316
+ * On-device LOCAL sensing: RTSP frame → LOCAL VLM (LiteRT-LM serving Gemma 4
317
+ * E2B, constrained structured output via tool-calling) → POST the STRUCTURED
318
+ * result as `preAnalyzed` to /v2/earner/audience-analyze (SKIPS the cloud VLM →
319
+ * no Vertex cost). For capable hosts running `litert-lm serve`. Multi-channel
320
+ * via `cameras: [...]`.
321
+ */
322
+ async function startLocalSensing(args) {
323
+ const configIdx = args.indexOf('--config');
324
+ const configPath = configIdx >= 0 && args[configIdx + 1] ? args[configIdx + 1] : 'trillboards.config.json';
325
+ console.log(`Trillboards Edge AI SDK v${VERSION} — LOCAL-sensing mode (on-device VLM)`);
326
+ console.log(`Loading config: ${configPath}`);
327
+ const config = (0, config_1.loadConfig)(configPath);
328
+ // Build the manager (feed resolution + banner + event logging) via the shared
329
+ // helper — the SAME construction auto-sense uses. Null ⇒ no feeds configured.
330
+ const manager = (0, sensingRunMode_1.buildLocalSensingManager)(config, undefined, { configPath });
331
+ if (!manager) {
332
+ process.exit(1);
333
+ return;
334
+ }
335
+ // Keep the on-device VLM (Gemma) up to date over the air while local-sense runs
336
+ // (edge-core ModelManager + /v1/federated/models, S3 presigned). Non-fatal: a
337
+ // null handle (disabled / startup failure) never blocks sensing.
338
+ const gemmaOta = (0, gemmaModelOta_1.startGemmaModelOta)(config);
339
+ const shutdown = async () => {
340
+ console.log('\nStopping local-sensing…');
341
+ gemmaOta?.stop();
342
+ await manager.stop();
343
+ process.exit(0);
344
+ };
345
+ process.on('SIGINT', shutdown);
346
+ process.on('SIGTERM', shutdown);
347
+ await manager.start();
348
+ console.log('Local-sensing started (on-device VLM). Press Ctrl+C to stop.');
349
+ // Keep the process running
350
+ await new Promise(() => { });
351
+ }
352
+ /** Read a `--flag value` argument (returns undefined if absent). */
353
+ function getArg(args, flag) {
354
+ const i = args.indexOf(flag);
355
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
356
+ }
357
+ /** Stable, non-PII connector fingerprint from this host (for pairing audit). */
358
+ function hostFingerprint() {
359
+ const raw = `${os.hostname()}|${process.platform}|${os.arch()}`;
360
+ return 'conn_' + (0, crypto_1.createHash)('sha256').update(raw).digest('hex').slice(0, 24);
361
+ }
362
+ /**
363
+ * True if a filesystem error is a PERMISSION failure (EACCES/EPERM), or its
364
+ * message says so. This is the signature of the paired-config write failing
365
+ * because ~/Library/Application Support/Trillboards is owned by another account
366
+ * — which happens when the `.pkg` was installed with admin rights and its
367
+ * postinstall ran as root, creating that dir ROOT-OWNED. It is NOT a bad or
368
+ * expired claim code, so it must never be reported as one.
369
+ */
370
+ function isPermissionError(err) {
371
+ const e = err;
372
+ if (!e)
373
+ return false;
374
+ if (e.code === 'EACCES' || e.code === 'EPERM')
375
+ return true;
376
+ return /permission denied/i.test(String(e.message ?? ''));
377
+ }
378
+ /**
379
+ * The operator-facing remediation when the paired config can't be written
380
+ * because its directory is owned by another account. Names the REAL cause and
381
+ * the exact one-line fix — deliberately NOT "double-check the code" (pairing
382
+ * already SUCCEEDED; only the local save failed). Mirrors the guidance in
383
+ * deploy/macos/pair.command so the GUI and headless paths say the same thing.
384
+ */
385
+ function configWritePermissionMessage(resolvedConfigPath) {
386
+ const dir = path.dirname(resolvedConfigPath);
387
+ return (`\nPaired successfully, but Trillboards could not save its settings to:\n` +
388
+ ` ${resolvedConfigPath}\n\n` +
389
+ `That folder is owned by another account: the installer was run with admin\n` +
390
+ `rights, so it created the folder as root and this pairing (which runs as you)\n` +
391
+ `can't write there. This is NOT a bad code — pairing worked.\n\n` +
392
+ `Fix it once by pasting this in Terminal, then run pairing again:\n` +
393
+ ` sudo chown -R "$(whoami)" "${dir}"\n`);
394
+ }
395
+ /**
396
+ * Persist the paired config, translating a PERMISSION failure into the clear
397
+ * remediation above (and exiting non-zero) instead of letting a raw EACCES
398
+ * bubble up as an opaque "Fatal error" that the GUI then mislabels "check the
399
+ * code". Any OTHER write error is re-thrown unchanged (surfaced as "Fatal
400
+ * error: …"). The writer is injectable for tests (mirrors PairClient's poster).
401
+ */
402
+ function writePairedConfig(resolvedConfigPath, serialized, writeFile = (p, data, opts) => fs.writeFileSync(p, data, opts)) {
403
+ try {
404
+ writeFile(resolvedConfigPath, serialized, { mode: 0o600 });
405
+ }
406
+ catch (err) {
407
+ if (isPermissionError(err)) {
408
+ console.error(configWritePermissionMessage(resolvedConfigPath));
409
+ process.exit(1);
410
+ return;
411
+ }
412
+ throw err;
413
+ }
414
+ }
415
+ /**
416
+ * First-run pairing: exchange a portal claim code for the connector config and
417
+ * write trillboards.config.json. The device token is delivered ONLY here (never
418
+ * baked into the installer), and RTSP credentials can be left local — feeds
419
+ * without an rtspUrl are flagged so the operator fills them in on this machine.
420
+ *
421
+ * trillboards-edge pair --code XXXX-XXXX [--api-base URL] [--config PATH] [--start]
422
+ * trillboards-edge pair --qr <image.png|.jpg> (scan the portal QR instead of typing the code)
423
+ *
424
+ * Exit codes (the install wrappers branch on these — keep stable):
425
+ * 0 paired, config written, AND the post-pair connection check passed
426
+ * 1 pairing failed (classified: bad/expired code, network, rate-limit, server)
427
+ * 6 paired + config written, but the connection check hit a NETWORK/SERVER
428
+ * failure — the code IS spent and the config IS saved; the operator fixes
429
+ * connectivity and re-runs the helper WITHOUT needing a new code.
430
+ * 7 paired + config written, but the API REJECTED the fresh pairing
431
+ * (verify kind 'auth', HTTP 401/403) — the saved config is unusable and
432
+ * the operator DOES need a fresh code. Distinct from 6 so the wrappers
433
+ * never promise "no new code needed" for an auth rejection.
434
+ */
435
+ async function runPair(args) {
436
+ let code = getArg(args, '--code') || getArg(args, '-c');
437
+ const qrPath = getArg(args, '--qr');
438
+ const apiBase = getArg(args, '--api-base') ||
439
+ process.env.TRILLBOARDS_API_BASE ||
440
+ 'https://api.trillboards.com';
441
+ const configPath = getArg(args, '--config') || 'trillboards.config.json';
442
+ const doStart = args.includes('--start');
443
+ // --qr: scan the claim code from an image of the portal's Install-step QR (a
444
+ // screenshot or phone photo) so the owner doesn't hand-type it. The decoder is
445
+ // lazy-required so the common `pair --code` path stays free of the image deps.
446
+ // An explicit --code always wins (no surprise when both are passed).
447
+ if (!code && qrPath) {
448
+ try {
449
+ const { decodeClaimCodeFromImageFile } = require('./qrScanner');
450
+ code = await decodeClaimCodeFromImageFile(qrPath);
451
+ console.log(` Scanned claim code from ${qrPath}`);
452
+ }
453
+ catch (err) {
454
+ console.error(`\nCouldn't read a claim code from the QR image: ${err instanceof Error ? err.message : err}`);
455
+ console.error("Use a clear PNG/JPEG of the QR shown on the portal's Install step.");
456
+ process.exit(1);
457
+ return;
458
+ }
459
+ }
460
+ if (!code) {
461
+ console.error('Pairing requires a claim code:');
462
+ console.error(' trillboards-edge pair --code XXXX-XXXX');
463
+ console.error(' trillboards-edge pair --qr /path/to/qr.png (scan the portal QR instead)');
464
+ console.error('Get one from the Trillboards portal → Add a camera → Install.');
465
+ process.exit(1);
466
+ return;
467
+ }
468
+ console.log(`Trillboards Edge AI SDK v${VERSION} — pairing`);
469
+ console.log(` API: ${apiBase}`);
470
+ console.log(` Code: ${(0, PairClient_1.normalizeClaimCode)(code)}`);
471
+ const logDir = (0, pairVerify_1.defaultLogDir)();
472
+ const client = new PairClient_1.PairClient({ apiBaseUrl: apiBase });
473
+ // Classify EVERY failure mode honestly. A transport throw (no response) and a
474
+ // non-2xx response get different, owner-grade copy — and only a SERVER
475
+ // rejection of the code may say "get a fresh code" (the historical failure was
476
+ // blaming valid codes for local EACCES/network problems — see pairVerify.ts).
477
+ let res;
478
+ try {
479
+ res = await client.pair({
480
+ claimCode: code,
481
+ connectorVersion: VERSION,
482
+ deviceFingerprint: hostFingerprint(),
483
+ });
484
+ }
485
+ catch (err) {
486
+ const failure = (0, pairVerify_1.classifyPairFailure)({ thrown: err });
487
+ const logFile = (0, pairVerify_1.appendPairLog)(logDir, [
488
+ `pair: transport failure (${failure.kind})`,
489
+ `pair: ${err instanceof Error ? err.message : String(err)}`,
490
+ ]);
491
+ console.error(`\nPairing did not complete.\n${failure.message}`);
492
+ if (logFile)
493
+ console.error(`\nDetails saved to: ${logFile}`);
494
+ process.exit(1);
495
+ return;
496
+ }
497
+ if (!res.ok || !res.config) {
498
+ const failure = (0, pairVerify_1.classifyPairFailure)({ statusCode: res.statusCode, error: res.error });
499
+ const logFile = (0, pairVerify_1.appendPairLog)(logDir, `pair: rejected (${failure.kind}, HTTP ${res.statusCode}): ${res.error ?? ''}`);
500
+ console.error(`\nPairing failed.\n${failure.message}`);
501
+ if (logFile)
502
+ console.error(`\nDetails saved to: ${logFile}`);
503
+ process.exit(1);
504
+ return;
505
+ }
506
+ const file = (0, PairClient_1.pairedConfigToFile)(res.config);
507
+ const resolved = path.resolve(configPath);
508
+ // Pairing succeeded — persist the config. A PERMISSION failure here (the
509
+ // config dir is root-owned from an admin/system install) is reported with the
510
+ // real cause + one-line chown fix, NOT as a bad code. See writePairedConfig.
511
+ writePairedConfig(resolved, JSON.stringify(file, null, 2) + '\n');
512
+ console.log(`\n Paired ${res.config.cameras.length} camera feed(s):`);
513
+ for (const cam of res.config.cameras) {
514
+ console.log(` - ${cam.name} → ${cam.screenId}${cam.rtspUrl ? '' : ' (add rtspUrl locally)'}`);
515
+ }
516
+ console.log(` Config written: ${resolved}`);
517
+ const needRtsp = res.config.cameras.filter((c) => !c.rtspUrl);
518
+ if (needRtsp.length > 0) {
519
+ console.log(`\n ${needRtsp.length} feed(s) need an RTSP URL added to ${configPath} ` +
520
+ '(camera credentials stay on this machine).');
521
+ }
522
+ // Post-pair verification: one authenticated round-trip with a freshly-minted
523
+ // device token, so "paired" is PROVEN to work from this machine (network path
524
+ // + token accepted), not assumed. On failure the code is already spent and the
525
+ // config already saved — exit 6 so the wrappers say "fix the connection and
526
+ // re-run, NO new code needed" instead of the failure path's "get a fresh code".
527
+ console.log('\n Checking the connection to Trillboards…');
528
+ const verifiable = (0, pairVerify_1.readVerifiableConfig)(resolved);
529
+ const verify = verifiable
530
+ ? await (0, pairVerify_1.verifyApiConnection)(verifiable)
531
+ : { ok: false, kind: 'no_config', message: 'saved config has no device token' };
532
+ (0, pairVerify_1.appendPairLog)(logDir, [
533
+ `pair: ok (${res.config.cameras.length} feed(s), ${needRtsp.length} missing rtspUrl)`,
534
+ `pair: connection check ${verify.kind}`,
535
+ ]);
536
+ if (verify.ok) {
537
+ console.log(' Connected to Trillboards — this machine can reach the API and the pairing is valid.');
538
+ }
539
+ else {
540
+ // Never auto-start against an unverified connection (--start included).
541
+ // An AUTH rejection (the fresh token itself was refused) needs a NEW code —
542
+ // exit 7 so the wrappers don't give the connectivity-resume copy ("you will
543
+ // NOT need a new code"), which would be a lie for this case. Network/server
544
+ // failures exit 6: fix connectivity, re-run, same code stays unnecessary.
545
+ console.error(`\n${verify.message}`);
546
+ process.exit(verify.kind === 'auth' ? 7 : 6);
547
+ return;
548
+ }
549
+ if (doStart) {
550
+ if (needRtsp.length > 0) {
551
+ console.error('\nCannot auto-start: some feeds are missing rtspUrl. Edit the config, then run `auto-sense`.');
552
+ process.exit(1);
553
+ return;
554
+ }
555
+ console.log('\nStarting local-first sensing…\n');
556
+ await (0, autoSense_1.runAutoSense)(['--config', configPath]);
557
+ }
558
+ }
114
559
  const MODEL_URLS = {
115
560
  'blazeface.onnx': 'https://models.trillboards.com/blazeface_128x128.onnx',
116
561
  'yamnet.onnx': 'https://models.trillboards.com/yamnet_classification.onnx',
@@ -124,6 +569,7 @@ async function downloadModels(args) {
124
569
  }
125
570
  console.log(`Downloading ONNX models to: ${resolved}`);
126
571
  console.log('');
572
+ const failed = [];
127
573
  for (const [filename, url] of Object.entries(MODEL_URLS)) {
128
574
  const dest = path.join(resolved, filename);
129
575
  if (fs.existsSync(dest)) {
@@ -133,17 +579,29 @@ async function downloadModels(args) {
133
579
  console.log(` [GET] ${filename}...`);
134
580
  try {
135
581
  await downloadFile(url, dest);
582
+ // A 200 is not a model. models.trillboards.com answers every path with the
583
+ // marketing SPA's index.html (2490 bytes, HTTP 200), so without this the
584
+ // command wrote a web page to blazeface.onnx and printed `[OK] (0.0 MB)`.
585
+ (0, modelIntegrity_1.assertOnnxModel)(dest, filename);
136
586
  const stats = fs.statSync(dest);
137
587
  const sizeMb = (stats.size / (1024 * 1024)).toFixed(1);
138
588
  console.log(` [OK] ${filename} (${sizeMb} MB)`);
139
589
  }
140
590
  catch (err) {
591
+ failed.push(filename);
141
592
  console.error(` [FAIL] ${filename}: ${err instanceof Error ? err.message : err}`);
142
593
  // Remove partial download
143
594
  if (fs.existsSync(dest))
144
595
  fs.unlinkSync(dest);
145
596
  }
146
597
  }
598
+ if (failed.length > 0) {
599
+ console.error(`\nFAILED to download ${failed.length} of ${Object.keys(MODEL_URLS).length} model(s): ${failed.join(', ')}.`);
600
+ console.error('No usable model was written. Sensing that needs these models will report the ' +
601
+ 'camera/microphone subsystem as ERROR until a valid model is placed in this directory.');
602
+ process.exitCode = 1;
603
+ return;
604
+ }
147
605
  console.log('\nDone. Set models.dir in your config to point to this directory.');
148
606
  }
149
607
  function downloadFile(url, dest, maxRedirects = 5) {
@@ -200,6 +658,14 @@ Usage:
200
658
  trillboards-edge demo Try it instantly — no token, no camera, no setup
201
659
  trillboards-edge init Interactive setup wizard
202
660
  trillboards-edge start [--config <path>] Start the edge agent
661
+ trillboards-edge cloud-sense [--config <path>] Stream RTSP frame(s) to cloud VLM (no local ONNX)
662
+ trillboards-edge auto-sense [--config <path>] Use on-device VLM; wait locally while its model prepares
663
+ trillboards-edge sense [--config <path>] Run the ONNX zoo AND the VLM together (APK parity)
664
+ trillboards-edge setup [--config <path>] Validate-first onboarding: opens the portal and keeps a code fallback
665
+ trillboards-edge pair --code <XXXX-XXXX> Pair with a portal claim code (writes config)
666
+ trillboards-edge pair --qr <image.png> Pair by scanning a screenshot/photo of the portal QR
667
+ trillboards-edge verify-connection [--config p] Prove the saved pairing works (exit 0=ok 2=network 3=auth 4=no config)
668
+ trillboards-edge discover Find cameras on your network (ONVIF) or expand an NVR's channels
203
669
  trillboards-edge status [--host h] [--port p] Show agent status dashboard
204
670
  trillboards-edge diagnose [--json] Run system diagnostics
205
671
  trillboards-edge mcp Start MCP server (for AI agent integration)
@@ -217,6 +683,10 @@ Examples:
217
683
  # Quick start with default config
218
684
  trillboards-edge start --config trillboards.config.json
219
685
 
686
+ # Find cameras without typing a single RTSP URL
687
+ trillboards-edge discover # scan the LAN for ONVIF cameras
688
+ trillboards-edge discover --nvr 192.168.1.10 --brand hikvision --channels 8 --user admin --pass ****
689
+
220
690
  # Check agent status (or watch in real-time)
221
691
  trillboards-edge status
222
692
  trillboards-edge status --watch
@@ -232,8 +702,8 @@ Examples:
232
702
  trillboards-edge mcp
233
703
 
234
704
  # Docker deployment
235
- docker run -e DEVICE_TOKEN=xxx trillboards/edge-sdk:latest
236
- docker run --gpus all -e DEVICE_TOKEN=xxx trillboards/edge-sdk:cuda
705
+ docker run -e DEVICE_TOKEN=xxx ghcr.io/trillboards/edge-sdk:latest
706
+ docker run --gpus all -e DEVICE_TOKEN=xxx ghcr.io/trillboards/edge-sdk:cuda
237
707
 
238
708
  Config file (trillboards.config.json):
239
709
  {
@@ -246,12 +716,25 @@ Config file (trillboards.config.json):
246
716
  "kiosk": { "enabled": true }
247
717
  }
248
718
 
719
+ Cloud-sensing config (point any RTSP camera / NVR at Trillboards — no ONNX):
720
+ {
721
+ "apiBaseUrl": "https://api.trillboards.com",
722
+ "cloudSensing": { "enabled": true },
723
+ "cameras": [
724
+ { "name": "Parking lot", "screenId": "scr_lot", "deviceToken": "tok1", "rtspUrl": "rtsp://user:pass@nvr:554/ch01" },
725
+ { "name": "Register", "screenId": "scr_reg", "deviceToken": "tok2", "rtspUrl": "rtsp://user:pass@nvr:554/ch02" }
726
+ ]
727
+ }
728
+ # Each feed = one screen = one device token = one sensing profile.
729
+ # Single feed: set "camera": { "rtspUrl": "..." } + top-level "deviceToken".
730
+
249
731
  Status endpoint (when agent is running):
250
732
  curl http://localhost:9090/status Full JSON status
251
733
  curl http://localhost:9090/health Health check (200/503)
252
734
  curl http://localhost:9090/metrics Prometheus metrics
253
735
 
254
736
  Supported platforms:
737
+ - macOS 12+ (Apple Silicon + Intel) — one-click signed installer
255
738
  - Linux (Ubuntu 18.04+, Debian 10+, ARM64/x64)
256
739
  - Windows 10+ (x64)
257
740
 
@@ -262,8 +745,15 @@ Execution providers:
262
745
  - cuda NVIDIA GPU (requires CUDA toolkit)
263
746
  `);
264
747
  }
265
- main().catch((err) => {
266
- console.error(`Fatal error: ${err instanceof Error ? err.message : err}`);
267
- process.exit(1);
268
- });
748
+ // Auto-run ONLY when this file is the process entrypoint. The bundled
749
+ // `trillboards-edge` launcher does `exec node …/dist/cli.js` and npm's bin shim
750
+ // runs it directly — both make require.main === module true, so production
751
+ // behavior is unchanged. Guarding here lets the unit tests import the exported
752
+ // helpers (writePairedConfig, …) without dispatching a real CLI command.
753
+ if (require.main === module) {
754
+ main().catch((err) => {
755
+ console.error(`Fatal error: ${err instanceof Error ? err.message : err}`);
756
+ process.exit(1);
757
+ });
758
+ }
269
759
  //# sourceMappingURL=cli.js.map