@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
@@ -0,0 +1,153 @@
1
+ #!/bin/bash
2
+ #
3
+ # postinstall — runs after the home-domain payload is laid down under
4
+ # ~/Library/Trillboards/edge. Home-domain installs run this AS THE USER (no
5
+ # root), so $HOME is the operator's home.
6
+ #
7
+ # Responsibilities (idempotent, no customer terminal):
8
+ # - ensure the runtime dirs exist (Application Support config dir + Logs dir)
9
+ # - make the bundled binaries executable (pkg payloads can lose +x)
10
+ # - install and health-check the bundled local AI model
11
+ # - open camera pairing only after local AI is ready
12
+ # - DO NOT start the LaunchAgent: there is no paired config yet, and a
13
+ # config-less cloud-sense crash-loops. The agent is bootstrapped only after
14
+ # a successful setup (the installed pairing engine does that).
15
+ #
16
+ # Cosmetic permission repairs remain non-fatal. Local AI is a required customer
17
+ # dependency, so a failed model install stops setup before pairing can begin.
18
+
19
+ set -u
20
+
21
+ # Resolve the OWNER of this install. A home-domain install runs us AS THE USER,
22
+ # but an admin/system install (the operator clicked through with admin rights)
23
+ # runs us AS ROOT. macOS still sets $USER to the invoking account even in the
24
+ # system domain; fall back to the logged-in console user. Knowing the real owner
25
+ # lets us hand the runtime dirs back so a root install can't leave them
26
+ # ROOT-OWNED — which is exactly what made the operator's later setup attempt hit
27
+ # `EACCES` writing the paired config (misreported as "check the code").
28
+ OWNER="${USER:-}"
29
+ if [ -z "$OWNER" ] || [ "$OWNER" = "root" ]; then
30
+ OWNER="$(stat -f %Su /dev/console 2>/dev/null || true)"
31
+ fi
32
+
33
+ # Resolve the install root. On a home-domain install $HOME is already the
34
+ # operator's home. Under a root/admin install $HOME is /var/root — derive the
35
+ # target user's home from $OWNER instead, so the runtime dirs land under
36
+ # /Users/<owner> and NEVER /var/root.
37
+ if [ "$(id -u)" = "0" ] && [ -n "$OWNER" ] && [ "$OWNER" != "root" ]; then
38
+ USER_HOME="$(eval echo "~$OWNER")"
39
+ else
40
+ USER_HOME="${HOME:-$(eval echo "~$(id -un)")}"
41
+ fi
42
+ EDGE_ROOT="$USER_HOME/Library/Trillboards/edge"
43
+ APP_SUPPORT="$USER_HOME/Library/Application Support/Trillboards"
44
+ LOG_DIR="$USER_HOME/Library/Logs/Trillboards"
45
+
46
+ log() { /usr/bin/logger -t com.trillboards.edge "postinstall: $*" 2>/dev/null || true; printf '[trillboards postinstall] %s\n' "$*"; }
47
+
48
+ # Hand a path (dir or file) back to the installing user. Meaningful ONLY under a
49
+ # root/admin install — a home-domain install already runs as the user, so this
50
+ # is a no-op there. Pass "-R" first to recurse (runtime dirs we fully own); omit
51
+ # it for a shallow chown (~/Applications, so we don't rewrite the operator's
52
+ # OTHER apps). Fully non-fatal: a chown failure must never fail the install.
53
+ reassert_owner() {
54
+ [ "$(id -u)" = "0" ] || return 0
55
+ [ -n "$OWNER" ] && [ "$OWNER" != "root" ] || return 0
56
+ if [ "${1:-}" = "-R" ]; then
57
+ shift
58
+ chown -R "$OWNER" "$@" 2>/dev/null || true
59
+ else
60
+ chown "$OWNER" "$@" 2>/dev/null || true
61
+ fi
62
+ }
63
+
64
+ log "install root: $EDGE_ROOT"
65
+
66
+ # 1. Runtime dirs (config lives in Application Support, logs in Logs).
67
+ mkdir -p "$APP_SUPPORT" 2>/dev/null || log "could not create $APP_SUPPORT"
68
+ mkdir -p "$LOG_DIR" 2>/dev/null || log "could not create $LOG_DIR"
69
+ chmod 700 "$APP_SUPPORT" 2>/dev/null || true
70
+
71
+ # Under a root/admin install the dirs just created — and the bundled payload —
72
+ # are ROOT-OWNED. Hand them to the operator NOW so the pairing helper (which
73
+ # runs as the user) can write the paired config without hitting EACCES. No-op on
74
+ # a home-domain install (we already run as the user).
75
+ reassert_owner -R "$APP_SUPPORT" "$LOG_DIR" "$EDGE_ROOT"
76
+
77
+ # 2. Re-assert +x on the bundled executables (idempotent).
78
+ for b in node ffmpeg trillboards-edge trillboards-pair; do
79
+ if [ -f "$EDGE_ROOT/bin/$b" ]; then
80
+ chmod +x "$EDGE_ROOT/bin/$b" 2>/dev/null || true
81
+ fi
82
+ done
83
+
84
+ # 3. Install the bundled local AI model before pairing. Under an admin install,
85
+ # run the per-user provisioner in the console user's launchd session so its
86
+ # model store and LaunchAgent never land under /var/root.
87
+ AI_SETUP="$EDGE_ROOT/share/setup-litertlm.sh"
88
+ if [ ! -x "$AI_SETUP" ]; then
89
+ log "ERROR: required local AI installer is missing at $AI_SETUP"
90
+ exit 1
91
+ fi
92
+
93
+ log "installing and verifying bundled local AI before camera pairing"
94
+ AI_RC=0
95
+ if [ "$(id -u)" = "0" ] && [ -n "$OWNER" ] && [ "$OWNER" != "root" ]; then
96
+ LAUNCH_UID="$(id -u "$OWNER" 2>/dev/null || echo '')"
97
+ if [ -z "$LAUNCH_UID" ]; then
98
+ log "ERROR: could not resolve the installing user's launch session"
99
+ exit 1
100
+ fi
101
+ /bin/launchctl asuser "$LAUNCH_UID" /usr/bin/sudo -u "$OWNER" \
102
+ /usr/bin/env HOME="$USER_HOME" TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1 \
103
+ /bin/bash "$AI_SETUP" || AI_RC=$?
104
+ else
105
+ TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1 /bin/bash "$AI_SETUP" || AI_RC=$?
106
+ fi
107
+ if [ "$AI_RC" -ne 0 ]; then
108
+ log "ERROR: local AI installation failed (exit $AI_RC); camera pairing was not opened"
109
+ exit "$AI_RC"
110
+ fi
111
+ log "bundled local AI is installed and healthy"
112
+
113
+ # 4. Launch the real native setup application. It runs the canonical pairer as
114
+ # an invisible child process and renders its credential-free progress file,
115
+ # so customers never see Terminal or a command window. The app itself is
116
+ # part of the signed payload under ~/Applications.
117
+ APPS_DIR="$USER_HOME/Applications"
118
+ SETUP_APP="$APPS_DIR/Trillboards CCTV Setup.app"
119
+ LEGACY_PAIR_LAUNCHER="$APPS_DIR/Trillboards Pair.command"
120
+ rm -f "$LEGACY_PAIR_LAUNCHER" 2>/dev/null || true
121
+ if [ -d "$SETUP_APP" ] && [ -x "$EDGE_ROOT/bin/trillboards-pair" ]; then
122
+ log "native setup app ready: \"$SETUP_APP\" (opening it)"
123
+ if [ "$(id -u)" = "0" ] && [ -n "$OWNER" ] && [ "$OWNER" != "root" ]; then
124
+ LAUNCH_UID="$(id -u "$OWNER" 2>/dev/null || echo '')"
125
+ if [ -n "$LAUNCH_UID" ]; then
126
+ /bin/launchctl asuser "$LAUNCH_UID" /usr/bin/sudo -u "$OWNER" \
127
+ /usr/bin/env -u TMPDIR HOME="$USER_HOME" /usr/bin/open "$SETUP_APP" 2>/dev/null \
128
+ || /bin/launchctl asuser "$LAUNCH_UID" /usr/bin/sudo -u "$OWNER" \
129
+ /usr/bin/env -u TMPDIR HOME="$USER_HOME" /usr/bin/open -R "$SETUP_APP" 2>/dev/null || true
130
+ fi
131
+ else
132
+ /usr/bin/env -u TMPDIR /usr/bin/open "$SETUP_APP" 2>/dev/null \
133
+ || /usr/bin/env -u TMPDIR /usr/bin/open -R "$SETUP_APP" 2>/dev/null || true
134
+ fi
135
+ else
136
+ log "WARNING: native setup app or pairing engine is missing"
137
+ fi
138
+
139
+ # Under a root install the payload app is root-owned. Hand only this app bundle
140
+ # back to the operator, never their other applications.
141
+ reassert_owner "$APPS_DIR"
142
+ reassert_owner -R "$SETUP_APP"
143
+
144
+ # 5. Sanity check (logged only — never fail the install on this).
145
+ if [ -x "$EDGE_ROOT/bin/trillboards-edge" ]; then
146
+ VER="$("$EDGE_ROOT/bin/trillboards-edge" version 2>/dev/null || echo '?')"
147
+ log "bundled connector: $VER"
148
+ else
149
+ log "WARNING: launcher not executable at $EDGE_ROOT/bin/trillboards-edge"
150
+ fi
151
+
152
+ log "done. Connector and local AI installed; Trillboards CCTV Setup opened automatically."
153
+ exit 0
@@ -0,0 +1,272 @@
1
+ #!/bin/bash
2
+ #
3
+ # setup-litertlm.sh — provision the LiteRT-LM on-device VLM runtime for the
4
+ # BYO-CCTV LOCAL-inference connector. Runs AS THE USER (home-domain, no sudo).
5
+ #
6
+ # Idempotent and safe to re-run; logs to the Trillboards log dir. Customer
7
+ # installers set TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1 so every provisioning
8
+ # failure is fatal and network model fallback is disabled.
9
+ # Steps:
10
+ # 1. Ensure `uv` (the Python packaging tool) is available — install via the
11
+ # official standalone installer if missing (home-domain, no sudo).
12
+ # 2. Install/repair `litert-lm` with uv-managed Python 3.12. Explicitly
13
+ # choosing Python avoids Apple's legacy system Python 3.9, which cannot
14
+ # import current LiteRT-LM releases.
15
+ # 3. `litert-lm import` the bundled Gemma 4 E2B model
16
+ # (gemma-4-E2B-it.litertlm, ~2.4 GB) after verifying its checksum.
17
+ # 4. Render + bootstrap the `litert-lm serve` LaunchAgent so the OpenAI-
18
+ # compatible endpoint (:9379) is up at login. The connector's
19
+ # LiteRtVlmProvider talks to it.
20
+ #
21
+ # The installer runs this before opening camera pairing. There is no model
22
+ # download after cameras have been created.
23
+
24
+ set -u
25
+ USER_HOME="${HOME:-$(eval echo "~$(id -un)")}"
26
+ LOG_DIR="$USER_HOME/Library/Logs/Trillboards"
27
+ SHARE_DIR="$USER_HOME/Library/Trillboards/edge/share"
28
+ LA_DIR="$USER_HOME/Library/LaunchAgents"
29
+ LOCAL_BIN="$USER_HOME/.local/bin"
30
+ MODEL_ID="gemma-4-E2B-it.litertlm"
31
+ HF_REPO="litert-community/gemma-4-E2B-it-litert-lm"
32
+ SERVE_LABEL="com.trillboards.litertlm"
33
+ SERVE_PORT="9379"
34
+ LITERT_PYTHON="${LITERT_LM_PYTHON:-3.12}"
35
+ REQUIRE_BUNDLED_MODEL="${TRILLBOARDS_REQUIRE_BUNDLED_MODEL:-0}"
36
+ ALLOW_MODEL_DOWNLOAD="${TRILLBOARDS_ALLOW_MODEL_DOWNLOAD:-0}"
37
+ # Customer packages stage the model here. A network fallback remains available
38
+ # only for legacy/engineering invocations that do not require the bundle.
39
+ LITERT_DIR="${LITERT_LM_DIR:-$USER_HOME/.litert-lm}"
40
+ STORE_DIR="$LITERT_DIR/models/$MODEL_ID"
41
+ BUNDLED_MODEL="$SHARE_DIR/models/$MODEL_ID"
42
+ BUNDLED_MODEL_SHA="$BUNDLED_MODEL.sha256"
43
+ # Installed model version — must match the sensing_model_registry `gemma_4_e2b`
44
+ # row. The connector's edge-core ModelManager seeds this (via the marker written
45
+ # below) so its first OTA check does NOT re-download the already-installed model.
46
+ # Bump GEMMA_VLM_VERSION alongside the registry row when a newer model ships.
47
+ MODEL_VERSION="${GEMMA_VLM_VERSION:-1.0.0}"
48
+ VERSION_MARKER="$STORE_DIR/trillboards-bundled-version.json"
49
+ mkdir -p "$LOG_DIR" "$LA_DIR" 2>/dev/null || true
50
+ LOG="$LOG_DIR/litertlm-setup.log"
51
+ log() { printf '[trillboards setup-litertlm] %s %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOG" 2>&1; printf '[trillboards setup-litertlm] %s\n' "$*"; }
52
+ fail() { log "ERROR: $*"; exit 1; }
53
+ local_ai_healthy() {
54
+ curl -fsS --max-time 3 "http://127.0.0.1:$SERVE_PORT/v1/models" 2>/dev/null \
55
+ | grep -Fq "$MODEL_ID"
56
+ }
57
+ launch_agent_loaded() {
58
+ launchctl print "gui/$(id -u)/$SERVE_LABEL" >/dev/null 2>&1
59
+ }
60
+ wait_for_launch_agent_removal() {
61
+ local removal_attempt=1
62
+ while [ "$removal_attempt" -le 40 ]; do
63
+ launch_agent_loaded || return 0
64
+ sleep 0.25
65
+ removal_attempt=$((removal_attempt + 1))
66
+ done
67
+ return 1
68
+ }
69
+ restart_launch_agent() {
70
+ local service_domain service_target bootstrap_attempt
71
+ service_domain="gui/$(id -u)"
72
+ service_target="$service_domain/$SERVE_LABEL"
73
+
74
+ if launch_agent_loaded; then
75
+ log "stopping existing LiteRT-LM LaunchAgent"
76
+ launchctl bootout "$service_target" >>"$LOG" 2>&1 \
77
+ || log "WARN: launchctl bootout returned non-zero; waiting for service state"
78
+ if ! wait_for_launch_agent_removal; then
79
+ log "ERROR: existing LiteRT-LM LaunchAgent did not finish stopping"
80
+ return 1
81
+ fi
82
+ fi
83
+
84
+ bootstrap_attempt=1
85
+ while [ "$bootstrap_attempt" -le 10 ]; do
86
+ if launchctl bootstrap "$service_domain" "$TARGET" >>"$LOG" 2>&1; then
87
+ log "litert-lm serve LaunchAgent bootstrapped on :$SERVE_PORT"
88
+ return 0
89
+ fi
90
+
91
+ # launchd may accept the job even when bootstrap reports a transient error.
92
+ if launch_agent_loaded; then
93
+ log "litert-lm serve LaunchAgent is loaded after bootstrap retry"
94
+ return 0
95
+ fi
96
+
97
+ if [ "$bootstrap_attempt" -lt 10 ]; then
98
+ log "WARN: launchctl bootstrap attempt $bootstrap_attempt failed; retrying"
99
+ sleep 1
100
+ fi
101
+ bootstrap_attempt=$((bootstrap_attempt + 1))
102
+ done
103
+
104
+ log "ERROR: launchctl bootstrap failed after $((bootstrap_attempt - 1)) attempts"
105
+ return 1
106
+ }
107
+ verify_bundled_model() {
108
+ [ -f "$BUNDLED_MODEL_SHA" ] || fail "bundled model checksum is missing"
109
+ EXPECTED_SHA="$(awk 'NR == 1 { print $1 }' "$BUNDLED_MODEL_SHA" 2>/dev/null || echo '')"
110
+ ACTUAL_SHA="$(shasum -a 256 "$BUNDLED_MODEL" 2>/dev/null | awk '{print $1}')"
111
+ if [ -z "$EXPECTED_SHA" ] || [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
112
+ fail "bundled model checksum mismatch"
113
+ fi
114
+ log "verified bundled model SHA-256"
115
+ }
116
+
117
+ # One provisioning process per user. Re-opening the pairing helper is a normal
118
+ # recovery path, so duplicate launches must collapse without duplicate downloads.
119
+ LOCK_DIR="$LOG_DIR/litertlm-setup.lock"
120
+ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
121
+ LOCK_PID="$(cat "$LOCK_DIR/pid" 2>/dev/null || echo '')"
122
+ if [ -n "$LOCK_PID" ] &&
123
+ kill -0 "$LOCK_PID" 2>/dev/null &&
124
+ ps -p "$LOCK_PID" -o command= 2>/dev/null | grep -q '[s]etup-litertlm.sh'; then
125
+ log "LiteRT-LM provisioning is already running (pid $LOCK_PID)"
126
+ if [ "$REQUIRE_BUNDLED_MODEL" = "1" ]; then
127
+ for _attempt in $(seq 1 120); do
128
+ local_ai_healthy && exit 0
129
+ sleep 2
130
+ done
131
+ fail "the existing local AI installation did not become healthy"
132
+ fi
133
+ exit 0
134
+ fi
135
+ rm -rf "$LOCK_DIR" 2>/dev/null || true
136
+ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
137
+ log "LiteRT-LM provisioning is already starting in another process"
138
+ exit 0
139
+ fi
140
+ fi
141
+ printf '%s\n' "$$" > "$LOCK_DIR/pid" 2>/dev/null || true
142
+ cleanup_lock() { rm -rf "$LOCK_DIR" 2>/dev/null || true; }
143
+ trap cleanup_lock EXIT
144
+ trap 'exit 130' INT
145
+ trap 'exit 143' TERM
146
+
147
+ log "starting LiteRT-LM provisioning"
148
+
149
+ HOST_OS="$(uname -s 2>/dev/null || echo unknown)"
150
+ HOST_ARCH="$(uname -m 2>/dev/null || echo unknown)"
151
+ if [ "$HOST_OS" = "Darwin" ] && [ "$HOST_ARCH" != "arm64" ]; then
152
+ log "ERROR: local AI requires Apple silicon; cloud inference remains disabled unless explicitly opted in."
153
+ launchctl bootout "gui/$(id -u)/$SERVE_LABEL" 2>/dev/null || true
154
+ rm -f "$LA_DIR/$SERVE_LABEL.plist" 2>/dev/null || true
155
+ log "removed any stale LiteRT-LM LaunchAgent for unsupported local inference host"
156
+ [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "local AI requires Apple silicon"
157
+ exit 0
158
+ fi
159
+
160
+ # 1. uv (PATH first; else official standalone installer, home-domain).
161
+ export PATH="$LOCAL_BIN:$USER_HOME/.cargo/bin:$PATH"
162
+ if ! command -v uv >/dev/null 2>&1; then
163
+ log "uv not found — installing via astral.sh standalone installer (no sudo)"
164
+ curl -LsSf https://astral.sh/uv/install.sh | sh >>"$LOG" 2>&1 || log "WARN: uv install failed"
165
+ export PATH="$LOCAL_BIN:$USER_HOME/.local/share/uv/bin:$PATH"
166
+ fi
167
+ UV="$(command -v uv || echo "$LOCAL_BIN/uv")"
168
+ if ! command -v "$UV" >/dev/null 2>&1 && [ ! -x "$UV" ]; then
169
+ [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "uv is unavailable; cannot install the local AI runtime"
170
+ log "ERROR: uv unavailable; cannot provision LiteRT-LM"; exit 0
171
+ fi
172
+
173
+ # 2. litert-lm CLI. Keep a healthy existing install, but actively repair a
174
+ # broken one. Older connector versions could let uv bind the tool to Apple's
175
+ # Python 3.9; current LiteRT-LM uses Python features introduced in 3.10 and then
176
+ # crash-loops before serving. --force rebuilds that existing tool environment
177
+ # with the explicit managed interpreter while --upgrade refreshes the package.
178
+ LITERTLM="$LOCAL_BIN/litert-lm"
179
+ [ -x "$LITERTLM" ] || LITERTLM="$(command -v litert-lm || echo "$LITERTLM")"
180
+ if [ -x "$LITERTLM" ] && "$LITERTLM" list >/dev/null 2>&1; then
181
+ log "litert-lm CLI is healthy — keeping current install"
182
+ else
183
+ log "installing/repairing litert-lm with uv-managed Python $LITERT_PYTHON"
184
+ if ! "$UV" tool install --python "$LITERT_PYTHON" --managed-python --upgrade --force litert-lm >>"$LOG" 2>&1; then
185
+ [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "litert-lm install/repair failed"
186
+ log "ERROR: litert-lm install/repair failed; local AI will retry later"
187
+ exit 0
188
+ fi
189
+ LITERTLM="$LOCAL_BIN/litert-lm"
190
+ [ -x "$LITERTLM" ] || LITERTLM="$(command -v litert-lm || echo "$LITERTLM")"
191
+ if [ ! -x "$LITERTLM" ] || ! "$LITERTLM" list >/dev/null 2>&1; then
192
+ [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "litert-lm remains unhealthy after repair"
193
+ log "ERROR: litert-lm remains unhealthy after repair; local AI will retry later"
194
+ exit 0
195
+ fi
196
+ log "litert-lm CLI repaired successfully"
197
+ fi
198
+
199
+ # 3. Model - import the verified bundled file. Idempotent: skip entirely if
200
+ # litert-lm already has the model. Engineering runs may use HF only with the
201
+ # explicit TRILLBOARDS_ALLOW_MODEL_DOWNLOAD=1 opt-in.
202
+ HAVE_MODEL=0
203
+ if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
204
+ log "model $MODEL_ID already present — skipping import"
205
+ HAVE_MODEL=1
206
+ if [ -f "$BUNDLED_MODEL" ]; then
207
+ verify_bundled_model
208
+ rm -f "$BUNDLED_MODEL" "$BUNDLED_MODEL_SHA" 2>/dev/null || true
209
+ log "removed redundant staged model from upgrade"
210
+ fi
211
+ elif [ -f "$BUNDLED_MODEL" ]; then
212
+ verify_bundled_model
213
+ log "importing $MODEL_ID from BUNDLED file $BUNDLED_MODEL (no download) …"
214
+ if "$LITERTLM" import "$BUNDLED_MODEL" "$MODEL_ID" >>"$LOG" 2>&1; then
215
+ log "model imported from bundle"
216
+ HAVE_MODEL=1
217
+ # Reclaim the staged ~2.4 GB — the litert-lm store now holds the model.
218
+ rm -f "$BUNDLED_MODEL" "$BUNDLED_MODEL_SHA" 2>/dev/null || true
219
+ else
220
+ [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "bundled model import failed"
221
+ [ "$ALLOW_MODEL_DOWNLOAD" = "1" ] || fail "bundled model import failed and network fallback is disabled"
222
+ log "WARN: bundled import failed - falling back to HF"
223
+ fi
224
+ fi
225
+ if [ "$HAVE_MODEL" -eq 0 ]; then
226
+ [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "required bundled model is missing"
227
+ [ "$ALLOW_MODEL_DOWNLOAD" = "1" ] || fail "local AI model is missing and network fallback is disabled"
228
+ log "importing $MODEL_ID from $HF_REPO (~2.4 GB, public repo) …"
229
+ "$LITERTLM" import --from-huggingface-repo "$HF_REPO" "$MODEL_ID" >>"$LOG" 2>&1 \
230
+ && { log "model imported from HF"; HAVE_MODEL=1; } \
231
+ || log "WARN: model import failed (serve will retry on next run)"
232
+ fi
233
+ [ "$HAVE_MODEL" -eq 1 ] || { [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "local AI model is unavailable"; }
234
+
235
+ # Record the installed version next to the store model so the connector's
236
+ # edge-core ModelManager seeds it (readBundledModelVersion) and does not
237
+ # download the same model again on its first gemma-vlm OTA check.
238
+ if [ "$HAVE_MODEL" -eq 1 ]; then
239
+ mkdir -p "$STORE_DIR" 2>/dev/null || true
240
+ printf '{"version":"%s","modelType":"gemma-vlm"}\n' "$MODEL_VERSION" > "$VERSION_MARKER" 2>/dev/null \
241
+ && log "recorded installed version $MODEL_VERSION at $VERSION_MARKER" \
242
+ || log "WARN: could not write version marker $VERSION_MARKER"
243
+ fi
244
+
245
+ # 4. Render + bootstrap the serve LaunchAgent.
246
+ TEMPLATE="$SHARE_DIR/$SERVE_LABEL.plist"
247
+ TARGET="$LA_DIR/$SERVE_LABEL.plist"
248
+ if [ -f "$TEMPLATE" ]; then
249
+ sed -e "s#__LITERTLM__#$LITERTLM#g" \
250
+ -e "s#__PORT__#$SERVE_PORT#g" \
251
+ -e "s#__LOG_DIR__#$LOG_DIR#g" \
252
+ "$TEMPLATE" > "$TARGET" 2>/dev/null || log "WARN: could not render serve plist"
253
+ restart_launch_agent \
254
+ || { [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "local AI service could not be started"; log "WARN: launchctl bootstrap failed for serve agent"; }
255
+ else
256
+ [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "local AI service template is missing"
257
+ log "WARN: serve plist template missing at $TEMPLATE"
258
+ fi
259
+
260
+ if [ "$REQUIRE_BUNDLED_MODEL" = "1" ]; then
261
+ log "waiting for local AI health check"
262
+ HEALTHY=0
263
+ for _attempt in $(seq 1 60); do
264
+ if local_ai_healthy; then HEALTHY=1; break; fi
265
+ sleep 2
266
+ done
267
+ [ "$HEALTHY" -eq 1 ] || fail "Gemma is installed, but the local AI service did not respond"
268
+ log "local AI is installed and healthy"
269
+ fi
270
+
271
+ log "done"
272
+ exit 0