@camstack/core 0.1.13 → 0.1.15

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 (161) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +220 -0
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js.map +1 -0
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +9 -0
  4. package/dist/builtins/addon-pages-aggregator/index.js +222 -0
  5. package/dist/builtins/addon-pages-aggregator/index.js.map +1 -0
  6. package/dist/builtins/addon-pages-aggregator/index.mjs +9 -0
  7. package/dist/builtins/addon-pages-aggregator/index.mjs.map +1 -0
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +200 -0
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -0
  10. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +9 -0
  11. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -0
  12. package/dist/builtins/addon-widgets-aggregator/index.js +202 -0
  13. package/dist/builtins/addon-widgets-aggregator/index.js.map +1 -0
  14. package/dist/builtins/addon-widgets-aggregator/index.mjs +9 -0
  15. package/dist/builtins/addon-widgets-aggregator/index.mjs.map +1 -0
  16. package/dist/builtins/alerts/alerts.addon.js +443 -0
  17. package/dist/builtins/alerts/alerts.addon.js.map +1 -0
  18. package/dist/builtins/alerts/alerts.addon.mjs +9 -0
  19. package/dist/builtins/alerts/alerts.addon.mjs.map +1 -0
  20. package/dist/builtins/alerts/index.js +443 -0
  21. package/dist/builtins/alerts/index.js.map +1 -0
  22. package/dist/builtins/alerts/index.mjs +8 -0
  23. package/dist/builtins/alerts/index.mjs.map +1 -0
  24. package/dist/builtins/console-logging/index.js +242 -0
  25. package/dist/builtins/console-logging/index.js.map +1 -0
  26. package/dist/builtins/console-logging/index.mjs +11 -0
  27. package/dist/builtins/console-logging/index.mjs.map +1 -0
  28. package/dist/builtins/device-manager/device-manager.addon.js +2155 -0
  29. package/dist/builtins/device-manager/device-manager.addon.js.map +1 -0
  30. package/dist/builtins/device-manager/device-manager.addon.mjs +9 -0
  31. package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -0
  32. package/dist/builtins/device-manager/index.js +2157 -0
  33. package/dist/builtins/device-manager/index.js.map +1 -0
  34. package/dist/builtins/device-manager/index.mjs +10 -0
  35. package/dist/builtins/device-manager/index.mjs.map +1 -0
  36. package/dist/builtins/hub-forwarder/index.js +297 -0
  37. package/dist/builtins/hub-forwarder/index.js.map +1 -0
  38. package/dist/builtins/hub-forwarder/index.mjs +11 -0
  39. package/dist/builtins/hub-forwarder/index.mjs.map +1 -0
  40. package/dist/builtins/local-auth/index.js +623 -0
  41. package/dist/builtins/local-auth/index.js.map +1 -0
  42. package/dist/builtins/local-auth/index.mjs +8 -0
  43. package/dist/builtins/local-auth/index.mjs.map +1 -0
  44. package/dist/builtins/local-auth/local-auth.addon.js +623 -0
  45. package/dist/builtins/local-auth/local-auth.addon.js.map +1 -0
  46. package/dist/builtins/local-auth/local-auth.addon.mjs +9 -0
  47. package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -0
  48. package/dist/builtins/local-backup/index.js +53 -68
  49. package/dist/builtins/local-backup/index.js.map +1 -1
  50. package/dist/builtins/local-backup/index.mjs +1 -1
  51. package/dist/builtins/native-metrics/native-metrics.addon.js +898 -0
  52. package/dist/builtins/native-metrics/native-metrics.addon.js.map +1 -0
  53. package/dist/builtins/native-metrics/native-metrics.addon.mjs +7 -0
  54. package/dist/builtins/native-metrics/native-metrics.addon.mjs.map +1 -0
  55. package/dist/builtins/snapshot/index.js +504 -0
  56. package/dist/builtins/snapshot/index.js.map +1 -0
  57. package/dist/builtins/snapshot/index.mjs +477 -0
  58. package/dist/builtins/snapshot/index.mjs.map +1 -0
  59. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +16 -166
  60. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js.map +1 -1
  61. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  62. package/dist/builtins/sqlite-storage/index.js +554 -621
  63. package/dist/builtins/sqlite-storage/index.js.map +1 -1
  64. package/dist/builtins/sqlite-storage/index.mjs +9 -11
  65. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +368 -130
  66. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js.map +1 -1
  67. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  68. package/dist/builtins/system-config/index.js +189 -0
  69. package/dist/builtins/system-config/index.js.map +1 -0
  70. package/dist/builtins/system-config/index.mjs +10 -0
  71. package/dist/builtins/system-config/index.mjs.map +1 -0
  72. package/dist/builtins/system-config/system-config.addon.js +187 -0
  73. package/dist/builtins/system-config/system-config.addon.js.map +1 -0
  74. package/dist/builtins/system-config/system-config.addon.mjs +9 -0
  75. package/dist/builtins/system-config/system-config.addon.mjs.map +1 -0
  76. package/dist/builtins/winston-logging/index.js +185 -65
  77. package/dist/builtins/winston-logging/index.js.map +1 -1
  78. package/dist/builtins/winston-logging/index.mjs +2 -1
  79. package/dist/chunk-2CIYKDRN.mjs +1 -0
  80. package/dist/chunk-2CIYKDRN.mjs.map +1 -0
  81. package/dist/chunk-2F76X6NL.mjs +136 -0
  82. package/dist/chunk-2F76X6NL.mjs.map +1 -0
  83. package/dist/chunk-2QUFBZ7M.mjs +1 -0
  84. package/dist/chunk-2QUFBZ7M.mjs.map +1 -0
  85. package/dist/chunk-3BK2Y7GY.mjs +593 -0
  86. package/dist/chunk-3BK2Y7GY.mjs.map +1 -0
  87. package/dist/chunk-4OOHFJHT.mjs +421 -0
  88. package/dist/chunk-4OOHFJHT.mjs.map +1 -0
  89. package/dist/chunk-4XHB7IHT.mjs +809 -0
  90. package/dist/chunk-4XHB7IHT.mjs.map +1 -0
  91. package/dist/{chunk-2F3XZYRW.mjs → chunk-6M2HSSTQ.mjs} +16 -7
  92. package/dist/chunk-6M2HSSTQ.mjs.map +1 -0
  93. package/dist/{chunk-SO4LROOT.mjs → chunk-7FI7SQS7.mjs} +54 -69
  94. package/dist/chunk-7FI7SQS7.mjs.map +1 -0
  95. package/dist/chunk-ED57RCQE.mjs +171 -0
  96. package/dist/chunk-ED57RCQE.mjs.map +1 -0
  97. package/dist/chunk-FZN56HGQ.mjs +626 -0
  98. package/dist/chunk-FZN56HGQ.mjs.map +1 -0
  99. package/dist/chunk-GL4OOB25.mjs +51 -0
  100. package/dist/chunk-GL4OOB25.mjs.map +1 -0
  101. package/dist/chunk-KDG2NTDB.mjs +137 -0
  102. package/dist/chunk-KDG2NTDB.mjs.map +1 -0
  103. package/dist/chunk-NRBQWBDM.mjs +191 -0
  104. package/dist/chunk-NRBQWBDM.mjs.map +1 -0
  105. package/dist/chunk-O4V246GG.mjs +2137 -0
  106. package/dist/chunk-O4V246GG.mjs.map +1 -0
  107. package/dist/chunk-QT57H266.mjs +163 -0
  108. package/dist/chunk-QT57H266.mjs.map +1 -0
  109. package/dist/chunk-QX4RH25I.mjs +141 -0
  110. package/dist/chunk-QX4RH25I.mjs.map +1 -0
  111. package/dist/chunk-TB562PZX.mjs +86 -0
  112. package/dist/chunk-TB562PZX.mjs.map +1 -0
  113. package/dist/chunk-TDYPZXK5.mjs +1 -0
  114. package/dist/chunk-TDYPZXK5.mjs.map +1 -0
  115. package/dist/chunk-UJI4LN5P.mjs +36 -0
  116. package/dist/chunk-UJI4LN5P.mjs.map +1 -0
  117. package/dist/chunk-W6RTHQGP.mjs +1 -0
  118. package/dist/chunk-W6RTHQGP.mjs.map +1 -0
  119. package/dist/chunk-ZELBCPDC.mjs +369 -0
  120. package/dist/chunk-ZELBCPDC.mjs.map +1 -0
  121. package/dist/index.d.mts +1103 -544
  122. package/dist/index.d.ts +1103 -544
  123. package/dist/index.js +7032 -6033
  124. package/dist/index.js.map +1 -1
  125. package/dist/index.mjs +568 -2226
  126. package/dist/index.mjs.map +1 -1
  127. package/dist/resource-monitor-UZUGPIAU.mjs +9 -0
  128. package/dist/resource-monitor-UZUGPIAU.mjs.map +1 -0
  129. package/dist/storage-location-manager-HFNB3PCS.mjs +7 -0
  130. package/dist/storage-location-manager-HFNB3PCS.mjs.map +1 -0
  131. package/package.json +123 -2
  132. package/dist/builtins/local-backup/index.d.mts +0 -42
  133. package/dist/builtins/local-backup/index.d.ts +0 -42
  134. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.mts +0 -2
  135. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +0 -2
  136. package/dist/builtins/sqlite-storage/index.d.mts +0 -4
  137. package/dist/builtins/sqlite-storage/index.d.ts +0 -4
  138. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.mts +0 -2
  139. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +0 -2
  140. package/dist/builtins/winston-logging/index.d.mts +0 -30
  141. package/dist/builtins/winston-logging/index.d.ts +0 -30
  142. package/dist/chunk-2F3XZYRW.mjs.map +0 -1
  143. package/dist/chunk-LQFPAEQF.mjs +0 -147
  144. package/dist/chunk-LQFPAEQF.mjs.map +0 -1
  145. package/dist/chunk-R3DIIBBX.mjs +0 -532
  146. package/dist/chunk-R3DIIBBX.mjs.map +0 -1
  147. package/dist/chunk-SMNR44VG.mjs +0 -386
  148. package/dist/chunk-SMNR44VG.mjs.map +0 -1
  149. package/dist/chunk-SO4LROOT.mjs.map +0 -1
  150. package/dist/chunk-SPA4JBKN.mjs +0 -175
  151. package/dist/chunk-SPA4JBKN.mjs.map +0 -1
  152. package/dist/dist-3BY63UQ5.mjs +0 -2151
  153. package/dist/dist-3BY63UQ5.mjs.map +0 -1
  154. package/dist/filesystem-storage.addon-C42r589X.d.mts +0 -57
  155. package/dist/filesystem-storage.addon-C42r589X.d.ts +0 -57
  156. package/dist/sql-schema-CKz78rId.d.mts +0 -97
  157. package/dist/sql-schema-CKz78rId.d.ts +0 -97
  158. package/dist/sqlite-settings.addon-KwG-uKMP.d.mts +0 -79
  159. package/dist/sqlite-settings.addon-KwG-uKMP.d.ts +0 -79
  160. package/dist/storage-location-manager-KKDQNAKA.mjs +0 -7
  161. /package/dist/{storage-location-manager-KKDQNAKA.mjs.map → builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs.map} +0 -0
package/dist/index.mjs CHANGED
@@ -1,259 +1,85 @@
1
+ import "./chunk-2QUFBZ7M.mjs";
2
+ import {
3
+ DeviceManagerAddon
4
+ } from "./chunk-O4V246GG.mjs";
5
+ import "./chunk-TDYPZXK5.mjs";
6
+ import {
7
+ ApiKeyManager,
8
+ AuthManager,
9
+ LocalAuthAddon,
10
+ ScopedTokenManager,
11
+ UserManager
12
+ } from "./chunk-3BK2Y7GY.mjs";
13
+ import {
14
+ getPidStats,
15
+ getSinglePidStats
16
+ } from "./chunk-GL4OOB25.mjs";
17
+ import {
18
+ FsStorageBackend,
19
+ StorageLocationManager
20
+ } from "./chunk-6M2HSSTQ.mjs";
21
+ import {
22
+ NativeMetricsAddon,
23
+ NativeMetricsProvider
24
+ } from "./chunk-4XHB7IHT.mjs";
25
+ import "./chunk-2CIYKDRN.mjs";
26
+ import {
27
+ AlertCenterAddon
28
+ } from "./chunk-4OOHFJHT.mjs";
29
+ import "./chunk-W6RTHQGP.mjs";
30
+ import {
31
+ SystemConfigAddon
32
+ } from "./chunk-QT57H266.mjs";
1
33
  import {
2
34
  CORE_TABLE_DDL,
3
- FileSystemStorage,
35
+ ConfigStore,
36
+ DeviceStore,
37
+ FilesystemStorageProvider,
4
38
  SettingsStore,
5
- SqliteStorageAddon,
6
- SqliteStorageProvider,
7
39
  addonTableToDdl
8
- } from "./chunk-R3DIIBBX.mjs";
9
- import "./chunk-SPA4JBKN.mjs";
10
- import "./chunk-SMNR44VG.mjs";
40
+ } from "./chunk-ZELBCPDC.mjs";
41
+ import {
42
+ FilesystemStorageAddon
43
+ } from "./chunk-UJI4LN5P.mjs";
44
+ import {
45
+ SqliteSettingsAddon,
46
+ SqliteSettingsBackend
47
+ } from "./chunk-FZN56HGQ.mjs";
11
48
  import {
12
49
  WinstonDestination,
13
50
  WinstonLoggingAddon
14
- } from "./chunk-LQFPAEQF.mjs";
51
+ } from "./chunk-KDG2NTDB.mjs";
52
+ import {
53
+ ConsoleDestination,
54
+ ConsoleLoggingAddon
55
+ } from "./chunk-TB562PZX.mjs";
56
+ import {
57
+ HubForwarderAddon,
58
+ HubForwarderDestination
59
+ } from "./chunk-QX4RH25I.mjs";
60
+ import {
61
+ formatLogLine
62
+ } from "./chunk-2F76X6NL.mjs";
15
63
  import {
16
64
  LocalBackupAddon,
17
65
  LocalBackupService
18
- } from "./chunk-SO4LROOT.mjs";
19
- import {
20
- FsStorageBackend,
21
- StorageLocationManager
22
- } from "./chunk-2F3XZYRW.mjs";
23
-
24
- // src/deps/binary-downloader.ts
25
- import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from "fs";
26
- import { join } from "path";
27
- import { pipeline } from "stream/promises";
28
- import { Readable } from "stream";
29
- import { execFileSync } from "child_process";
30
- function getPlatformInfo() {
31
- return {
32
- platform: process.platform,
33
- arch: process.arch
34
- };
35
- }
36
- function buildBinaryPath(dataDir, name, platform2) {
37
- const ext = (platform2 ?? process.platform) === "win32" ? ".exe" : "";
38
- return join(dataDir, "deps", `${name}${ext}`);
39
- }
40
- function findInPath(name) {
41
- try {
42
- execFileSync(name, ["--version"], { stdio: "pipe", timeout: 5e3 });
43
- return name;
44
- } catch {
45
- return null;
46
- }
47
- }
48
- async function downloadBinary(opts) {
49
- const { name, url, targetDir, targetName, logger, isArchive, archiveFormat, archiveInnerPath } = opts;
50
- const targetPath = join(targetDir, targetName);
51
- if (existsSync(targetPath)) {
52
- logger.debug(`${name} binary already exists at ${targetPath}`);
53
- return targetPath;
54
- }
55
- mkdirSync(targetDir, { recursive: true });
56
- logger.info(`Downloading ${name} from ${url}`);
57
- const response = await fetch(url, { redirect: "follow" });
58
- if (!response.ok || !response.body) {
59
- throw new Error(`Failed to download ${name}: ${response.status} ${response.statusText}`);
60
- }
61
- if (isArchive) {
62
- const ext = archiveFormat ?? "tar.gz";
63
- const tmpArchive = join(targetDir, `${name}-download.${ext}`);
64
- const nodeStream = Readable.fromWeb(response.body);
65
- await pipeline(nodeStream, createWriteStream(tmpArchive));
66
- const tmpExtractDir = join(targetDir, `${name}-extract`);
67
- mkdirSync(tmpExtractDir, { recursive: true });
68
- if (ext === "zip") {
69
- try {
70
- execFileSync("unzip", ["-o", "-q", tmpArchive, "-d", tmpExtractDir], { stdio: "pipe" });
71
- } catch {
72
- execFileSync("tar", ["-xf", tmpArchive, "-C", tmpExtractDir], { stdio: "pipe" });
73
- }
74
- } else if (ext === "tar.xz") {
75
- execFileSync("tar", ["-xJf", tmpArchive, "-C", tmpExtractDir], { stdio: "pipe" });
76
- } else {
77
- execFileSync("tar", ["-xzf", tmpArchive, "-C", tmpExtractDir], { stdio: "pipe" });
78
- }
79
- if (archiveInnerPath) {
80
- const { copyFileSync } = await import("fs");
81
- const sourcePath = join(tmpExtractDir, archiveInnerPath);
82
- if (!existsSync(sourcePath)) {
83
- throw new Error(`Binary not found in archive at ${archiveInnerPath}`);
84
- }
85
- copyFileSync(sourcePath, targetPath);
86
- }
87
- unlinkSync(tmpArchive);
88
- const { rmSync: rmSync2 } = await import("fs");
89
- rmSync2(tmpExtractDir, { recursive: true, force: true });
90
- } else {
91
- const nodeStream = Readable.fromWeb(response.body);
92
- await pipeline(nodeStream, createWriteStream(targetPath));
93
- }
94
- chmodSync(targetPath, 493);
95
- logger.info(`${name} downloaded to ${targetPath}`);
96
- return targetPath;
97
- }
98
- async function ensureBinary(opts) {
99
- const ext = process.platform === "win32" ? ".exe" : "";
100
- const targetName = `${opts.name}${ext}`;
101
- const targetPath = join(opts.targetDir, targetName);
102
- if (existsSync(targetPath)) {
103
- opts.logger.debug(`${opts.name} found at ${targetPath}`);
104
- return targetPath;
105
- }
106
- const inPath = findInPath(opts.name);
107
- if (inPath) {
108
- opts.logger.info(`${opts.name} found in system PATH`);
109
- return inPath;
110
- }
111
- return downloadBinary({
112
- name: opts.name,
113
- url: opts.downloadUrl,
114
- targetDir: opts.targetDir,
115
- targetName,
116
- logger: opts.logger,
117
- isArchive: opts.isArchive,
118
- archiveFormat: opts.archiveFormat,
119
- archiveInnerPath: opts.archiveInnerPath
120
- });
121
- }
122
-
123
- // src/deps/ffmpeg-downloader.ts
124
- import { join as join2 } from "path";
125
- var FFMPEG_VERSION = "7.1";
126
- function getFfmpegDownloadUrl(platform2, arch2) {
127
- switch (platform2) {
128
- case "linux": {
129
- const archMap = { x64: "amd64", arm64: "arm64" };
130
- const a = archMap[arch2];
131
- if (!a) throw new Error(`Unsupported Linux architecture: ${arch2}`);
132
- return `https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${a}-static.tar.xz`;
133
- }
134
- case "darwin": {
135
- const archMap = { arm64: "arm64", x64: "amd64" };
136
- const a = archMap[arch2];
137
- if (!a) throw new Error(`Unsupported macOS architecture: ${arch2}`);
138
- return `https://www.osxexperts.net/ffmpeg${FFMPEG_VERSION.replace(".", "")}arm.zip`;
139
- }
140
- default:
141
- throw new Error(`Unsupported platform: ${platform2}`);
142
- }
143
- }
144
- function getFfmpegArchiveInfo(platform2) {
145
- switch (platform2) {
146
- case "linux":
147
- return {
148
- isArchive: true,
149
- archiveFormat: "tar.xz",
150
- archiveInnerPath: `ffmpeg-${FFMPEG_VERSION}-amd64-static/ffmpeg`
151
- };
152
- case "darwin":
153
- return {
154
- isArchive: true,
155
- archiveFormat: "zip",
156
- archiveInnerPath: "ffmpeg"
157
- };
158
- default:
159
- throw new Error(`Unsupported platform: ${platform2}`);
160
- }
161
- }
162
- async function ensureFfmpeg(dataDir, logger) {
163
- const depsDir = join2(dataDir, "deps");
164
- const platform2 = process.platform;
165
- const arch2 = process.arch;
166
- const archiveInfo = getFfmpegArchiveInfo(platform2);
167
- return ensureBinary({
168
- name: "ffmpeg",
169
- targetDir: depsDir,
170
- downloadUrl: getFfmpegDownloadUrl(platform2, arch2),
171
- logger,
172
- ...archiveInfo
173
- });
174
- }
66
+ } from "./chunk-7FI7SQS7.mjs";
175
67
 
176
- // src/deps/python-downloader.ts
177
- import { join as join3 } from "path";
178
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "fs";
179
- import { execFileSync as execFileSync2 } from "child_process";
180
- import { pipeline as pipeline2 } from "stream/promises";
181
- import { Readable as Readable2 } from "stream";
182
- import { createWriteStream as createWriteStream2 } from "fs";
183
- var PYTHON_VERSION = "3.12.12";
184
- var PP_BASE = `https://github.com/bjia56/portable-python/releases/download/cpython-v${PYTHON_VERSION}-build.0`;
185
- function getPythonDownloadUrl(platform2, arch2) {
186
- switch (platform2) {
187
- case "linux": {
188
- const archMap = { x64: "x86_64", arm64: "aarch64" };
189
- const a = archMap[arch2];
190
- if (!a) throw new Error(`Unsupported Linux architecture for Python: ${arch2}`);
191
- return `${PP_BASE}/python-headless-${PYTHON_VERSION}-linux-${a}.zip`;
192
- }
193
- case "darwin": {
194
- return `${PP_BASE}/python-headless-${PYTHON_VERSION}-darwin-universal2.zip`;
195
- }
196
- default:
197
- throw new Error(`Unsupported platform for portable Python: ${platform2}`);
198
- }
199
- }
200
- async function ensurePython(dataDir, logger) {
201
- const pythonDir = join3(dataDir, "deps", "python");
202
- const pythonBin = join3(pythonDir, "bin", "python3");
203
- if (existsSync2(pythonBin)) {
204
- logger.debug(`Portable Python found at ${pythonBin}`);
205
- return pythonBin;
206
- }
207
- for (const name of ["python3", "python"]) {
208
- const inPath = findInPath(name);
209
- if (inPath) {
210
- logger.info(`Python found in system PATH: ${name}`);
211
- return inPath;
212
- }
213
- }
214
- logger.info(`Downloading portable Python ${PYTHON_VERSION} (headless)...`);
215
- const depsDir = join3(dataDir, "deps");
216
- mkdirSync2(depsDir, { recursive: true });
217
- const url = getPythonDownloadUrl(process.platform, process.arch);
218
- const tmpArchive = join3(depsDir, "python-download.zip");
219
- logger.info(` Source: ${url}`);
220
- const response = await fetch(url, { redirect: "follow" });
221
- if (!response.ok || !response.body) {
222
- logger.warn(`Failed to download Python: ${response.status} \u2014 ML detection will not be available`);
223
- return null;
224
- }
225
- const nodeStream = Readable2.fromWeb(response.body);
226
- await pipeline2(nodeStream, createWriteStream2(tmpArchive));
227
- try {
228
- execFileSync2("unzip", ["-o", "-q", tmpArchive, "-d", depsDir], { stdio: "pipe" });
229
- } catch {
230
- execFileSync2("python3", ["-m", "zipfile", "-e", tmpArchive, depsDir], { stdio: "pipe" });
231
- }
232
- const { unlinkSync: unlinkSync3 } = await import("fs");
233
- unlinkSync3(tmpArchive);
234
- if (!existsSync2(pythonBin)) {
235
- logger.warn("Python extraction succeeded but binary not found at expected path");
236
- return null;
237
- }
238
- chmodSync2(pythonBin, 493);
239
- logger.info(`Portable Python ${PYTHON_VERSION} installed at ${pythonBin}`);
240
- return pythonBin;
241
- }
242
- async function installPythonPackages(pythonPath, packages, logger) {
243
- if (packages.length === 0) return;
244
- logger.info(`Installing Python packages: ${packages.join(", ")}`);
245
- try {
246
- execFileSync2(pythonPath, ["-m", "pip", "install", "--quiet", ...packages], {
247
- stdio: "pipe",
248
- timeout: 3e5
249
- // 5 minutes for large packages like torch
250
- });
251
- logger.info("Python packages installed successfully");
252
- } catch (err) {
253
- logger.error(`Failed to install Python packages: ${err instanceof Error ? err.message : err}`);
254
- throw err;
255
- }
256
- }
68
+ // src/index.ts
69
+ import {
70
+ ensureBinary,
71
+ downloadBinary,
72
+ findInPath,
73
+ getPlatformInfo,
74
+ buildBinaryPath,
75
+ ensureFfmpeg,
76
+ getFfmpegDownloadUrl,
77
+ ensurePython,
78
+ installPythonPackages,
79
+ installPythonRequirements,
80
+ getPythonDownloadUrl,
81
+ PYTHON_VERSION
82
+ } from "@camstack/types/node";
257
83
 
258
84
  // src/events/event-bus.ts
259
85
  var EventBus = class {
@@ -324,7 +150,7 @@ async function downloadFile(url, destPath, onProgress) {
324
150
  try {
325
151
  for (; ; ) {
326
152
  const { done, value } = await reader.read();
327
- if (done) break;
153
+ if (done || !value) break;
328
154
  fileStream.write(value);
329
155
  downloaded += value.length;
330
156
  onProgress?.(downloaded, total);
@@ -370,10 +196,10 @@ async function downloadModel(options) {
370
196
  try {
371
197
  await downloadFile(tryUrl, destPath, onProgress);
372
198
  if (expectedSha256) {
373
- const hash2 = await computeSha256(destPath);
374
- if (hash2 !== expectedSha256) {
199
+ const hash = await computeSha256(destPath);
200
+ if (hash !== expectedSha256) {
375
201
  fs.unlinkSync(destPath);
376
- throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${hash2}`);
202
+ throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${hash}`);
377
203
  }
378
204
  }
379
205
  const stat = fs.statSync(destPath);
@@ -387,13 +213,102 @@ async function downloadModel(options) {
387
213
  }
388
214
  async function computeSha256(filePath) {
389
215
  return new Promise((resolve, reject) => {
390
- const hash2 = createHash("sha256");
216
+ const hash = createHash("sha256");
391
217
  const stream = fs.createReadStream(filePath);
392
- stream.on("data", (chunk) => hash2.update(chunk));
393
- stream.on("end", () => resolve(hash2.digest("hex")));
218
+ stream.on("data", (chunk) => hash.update(chunk));
219
+ stream.on("end", () => resolve(hash.digest("hex")));
394
220
  stream.on("error", reject);
395
221
  });
396
222
  }
223
+ async function downloadDirectory(url, destDir, knownFiles, onProgress) {
224
+ const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
225
+ if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
226
+ const [, repo, dirPath] = match;
227
+ const files = (knownFiles ?? []).map((f) => ({
228
+ relativePath: f,
229
+ fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
230
+ }));
231
+ if (files.length === 0) {
232
+ throw new Error(`Directory bundle requires explicit \`files\` list (got none for ${url})`);
233
+ }
234
+ const tmpDir = destDir + ".downloading";
235
+ fs.rmSync(tmpDir, { recursive: true, force: true });
236
+ fs.mkdirSync(tmpDir, { recursive: true });
237
+ let totalDownloaded = 0;
238
+ try {
239
+ for (const file of files) {
240
+ const destPath = path.join(tmpDir, file.relativePath);
241
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
242
+ await downloadFile(file.fileUrl, destPath, (downloaded, _total) => {
243
+ onProgress?.(totalDownloaded + downloaded, void 0);
244
+ });
245
+ totalDownloaded += fs.statSync(destPath).size;
246
+ }
247
+ fs.rmSync(destDir, { recursive: true, force: true });
248
+ fs.renameSync(tmpDir, destDir);
249
+ } catch (err) {
250
+ fs.rmSync(tmpDir, { recursive: true, force: true });
251
+ throw err;
252
+ }
253
+ }
254
+ async function ensureModel(modelsDir, entry, format, onProgress) {
255
+ const formatEntry = entry.formats[format];
256
+ if (!formatEntry) {
257
+ throw new Error(
258
+ `Model "${entry.id}" has no ${format} format. Available: ${Object.keys(entry.formats).join(", ")}`
259
+ );
260
+ }
261
+ if (entry.extraFiles) {
262
+ for (const extra of entry.extraFiles) {
263
+ await downloadFile(extra.url, path.join(modelsDir, extra.filename));
264
+ }
265
+ }
266
+ const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
267
+ const modelPath = path.join(modelsDir, filename);
268
+ if (fs.existsSync(modelPath)) {
269
+ if (formatEntry.isDirectory && !fs.existsSync(path.join(modelPath, "Manifest.json"))) {
270
+ fs.rmSync(modelPath, { recursive: true, force: true });
271
+ } else {
272
+ return modelPath;
273
+ }
274
+ }
275
+ fs.mkdirSync(modelsDir, { recursive: true });
276
+ if (formatEntry.isDirectory) {
277
+ await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
278
+ } else {
279
+ await downloadFile(
280
+ formatEntry.url,
281
+ modelPath,
282
+ (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total)
283
+ );
284
+ }
285
+ return modelPath;
286
+ }
287
+ function getModelFilePath(modelsDir, entry, format) {
288
+ const formatEntry = entry.formats[format];
289
+ if (!formatEntry) return null;
290
+ const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
291
+ return path.join(modelsDir, filename);
292
+ }
293
+ function isModelDownloaded(modelsDir, entry, format) {
294
+ const formatEntry = entry.formats[format];
295
+ if (!formatEntry) return false;
296
+ const modelPath = getModelFilePath(modelsDir, entry, format);
297
+ if (!modelPath || !fs.existsSync(modelPath)) return false;
298
+ if (formatEntry.isDirectory) return fs.existsSync(path.join(modelPath, "Manifest.json"));
299
+ return fs.statSync(modelPath).size > 0;
300
+ }
301
+ function deleteModelFromDisk(modelsDir, entry, format) {
302
+ const modelPath = getModelFilePath(modelsDir, entry, format);
303
+ if (!modelPath || !fs.existsSync(modelPath)) return false;
304
+ const formatEntry = entry.formats[format];
305
+ if (formatEntry?.isDirectory) {
306
+ fs.rmSync(modelPath, { recursive: true, force: true });
307
+ } else {
308
+ fs.unlinkSync(modelPath);
309
+ }
310
+ return true;
311
+ }
397
312
 
398
313
  // src/download/model-download-service.ts
399
314
  import * as fs2 from "fs";
@@ -408,6 +323,8 @@ var ModelDownloadService = class {
408
323
  }
409
324
  this.catalog = map;
410
325
  }
326
+ modelsDir;
327
+ onProgress;
411
328
  catalog;
412
329
  /**
413
330
  * Ensure a model (and its extra files) is downloaded.
@@ -492,13 +409,6 @@ var ModelDownloadService = class {
492
409
  }
493
410
  return fs2.statSync(modelPath).size > 0;
494
411
  }
495
- /**
496
- * Legacy API: download a model by ID (delegates to ensure with default format).
497
- * Required by IAddonModelManager interface.
498
- */
499
- async downloadModel(id) {
500
- return this.ensure(id);
501
- }
502
412
  /** Get the catalog entry for a model by ID. */
503
413
  getEntry(modelId) {
504
414
  return this.catalog.get(modelId);
@@ -528,7 +438,7 @@ var ModelDownloadService = class {
528
438
  * Download a directory bundle (e.g., .mlpackage) from HuggingFace.
529
439
  * ATOMIC: downloads to temp dir, renames only on complete success.
530
440
  */
531
- async downloadDirectory(url, destDir, knownFiles, modelId) {
441
+ async downloadDirectory(url, destDir, knownFiles, _modelId) {
532
442
  const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
533
443
  if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
534
444
  const [, repo, dirPath] = match;
@@ -588,12 +498,11 @@ import * as fs3 from "fs";
588
498
  import * as path3 from "path";
589
499
  var execFileAsync = promisify(execFile);
590
500
  var PythonEnvManager = class {
501
+ venvPath;
502
+ cachedProbe = null;
591
503
  constructor(dataDir) {
592
- this.dataDir = dataDir;
593
504
  this.venvPath = path3.join(dataDir, ".venv");
594
505
  }
595
- venvPath;
596
- cachedProbe = null;
597
506
  async probe() {
598
507
  if (this.cachedProbe) return this.cachedProbe;
599
508
  for (const cmd of ["python3", "python"]) {
@@ -648,17 +557,18 @@ var PythonEnvManager = class {
648
557
 
649
558
  // src/pipeline/pipeline-validator.ts
650
559
  var PipelineValidator = class {
651
- constructor(loader) {
652
- this.loader = loader;
560
+ constructor(stepExists) {
561
+ this.stepExists = stepExists;
653
562
  }
563
+ stepExists;
654
564
  validate(config) {
655
565
  const errors = [];
656
566
  const warnings = [];
657
567
  for (const node of config.video) {
658
- this.validateNode(node, errors, warnings, []);
568
+ this.validateNode(node, errors, warnings);
659
569
  }
660
570
  if (config.audio) {
661
- this.validateNode(config.audio, errors, warnings, []);
571
+ this.validateNode(config.audio, errors, warnings);
662
572
  }
663
573
  return {
664
574
  valid: errors.length === 0,
@@ -666,24 +576,19 @@ var PipelineValidator = class {
666
576
  warnings
667
577
  };
668
578
  }
669
- validateNode(node, errors, warnings, ancestors) {
670
- if (!this.loader.hasAddon(node.addon)) {
579
+ validateNode(node, errors, warnings) {
580
+ if (!this.stepExists(node.addon)) {
671
581
  errors.push({
672
582
  step: node.step,
673
583
  addon: node.addon,
674
- message: `Addon "${node.addon}" is not registered`,
584
+ message: `Step "${node.addon}" has no provider \u2014 the addon that provided it may have been removed`,
675
585
  severity: "error"
676
586
  });
677
587
  return;
678
588
  }
679
- const registered = this.loader.getAddon(node.addon);
680
- if (registered) {
681
- const manifest = registered.declaration;
682
- void manifest;
683
- }
684
589
  if (node.children) {
685
590
  for (const child of node.children) {
686
- this.validateNode(child, errors, warnings, [...ancestors, node]);
591
+ this.validateNode(child, errors, warnings);
687
592
  }
688
593
  }
689
594
  }
@@ -691,11 +596,12 @@ var PipelineValidator = class {
691
596
 
692
597
  // src/pipeline/pipeline-runner.ts
693
598
  import { randomUUID } from "crypto";
599
+ import { errMsg } from "@camstack/types";
694
600
  var PipelineRunner = class {
695
- constructor(engineManager, addonConfigs) {
696
- this.engineManager = engineManager;
697
- this.addonConfigs = addonConfigs;
601
+ constructor(addonResolver) {
602
+ this.addonResolver = addonResolver;
698
603
  }
604
+ addonResolver;
699
605
  async run(frame, config) {
700
606
  const startTime = performance.now();
701
607
  const results = [];
@@ -713,74 +619,12 @@ var PipelineRunner = class {
713
619
  frameTimestamp: frame.timestamp
714
620
  };
715
621
  }
716
- /**
717
- * Run only the audio classification node on an audio chunk.
718
- * Used by the audio path in DetectionWiringService (separate from video pipeline).
719
- */
720
- async runAudioNode(chunk, audioNode) {
721
- const startTime = performance.now();
722
- const results = [];
723
- const timings = {};
724
- const resultId = randomUUID();
725
- const stepStart = performance.now();
726
- try {
727
- const globalConfig = this.addonConfigs.get(audioNode.addon) ?? {};
728
- const engine = await this.engineManager.getOrCreateEngine(
729
- audioNode.addon,
730
- globalConfig,
731
- audioNode.configOverride
732
- );
733
- if (!("classifyAudio" in engine) || typeof engine["classifyAudio"] !== "function") {
734
- throw new Error(`Addon "${audioNode.addon}" has no classifyAudio method`);
735
- }
736
- const output = await engine.classifyAudio(chunk);
737
- const stepMs = performance.now() - stepStart;
738
- const stepResult = {
739
- addon: audioNode.addon,
740
- slot: "classifier",
741
- output,
742
- resultId,
743
- inferenceMs: output.inferenceMs,
744
- preprocessMs: 0,
745
- totalMs: stepMs
746
- };
747
- results.push(stepResult);
748
- timings[audioNode.step] = stepMs;
749
- } catch (error) {
750
- const stepMs = performance.now() - stepStart;
751
- const message = error instanceof Error ? error.message : String(error);
752
- results.push({
753
- addon: audioNode.addon,
754
- slot: "classifier",
755
- output: { classifications: [], inferenceMs: 0, modelId: "" },
756
- resultId,
757
- inferenceMs: 0,
758
- preprocessMs: 0,
759
- totalMs: stepMs,
760
- error: {
761
- code: "ADDON_ERROR",
762
- message,
763
- childrenSkipped: true
764
- }
765
- });
766
- }
767
- return {
768
- results,
769
- totalMs: performance.now() - startTime,
770
- timings,
771
- frameTimestamp: chunk.timestamp
772
- };
773
- }
774
622
  async executeNode(node, frame, parentDetection, results, timings) {
775
623
  const resultId = randomUUID();
776
624
  const stepStart = performance.now();
777
625
  try {
778
- const globalConfig = this.addonConfigs.get(node.addon) ?? {};
779
- const engine = await this.engineManager.getOrCreateEngine(
780
- node.addon,
781
- globalConfig,
782
- node.configOverride
783
- );
626
+ const effectiveConfig = { ...node.configOverride };
627
+ const caller = await this.addonResolver.resolve(node.addon, effectiveConfig);
784
628
  let output;
785
629
  if (parentDetection) {
786
630
  const cropInput = {
@@ -788,28 +632,9 @@ var PipelineRunner = class {
788
632
  roi: parentDetection.det.bbox,
789
633
  parentDetection: parentDetection.det
790
634
  };
791
- if ("crop" in engine && typeof engine["crop"] === "function") {
792
- output = await engine.crop(cropInput);
793
- } else if ("classify" in engine && typeof engine["classify"] === "function") {
794
- output = await engine.classify(cropInput);
795
- } else if ("detect" in engine && typeof engine["detect"] === "function") {
796
- output = await engine.detect(frame);
797
- } else {
798
- throw new Error(`Addon "${node.addon}" has no detect/crop/classify method`);
799
- }
635
+ output = await caller.process(cropInput);
800
636
  } else {
801
- if ("detect" in engine && typeof engine["detect"] === "function") {
802
- output = await engine.detect(frame);
803
- } else if ("classify" in engine && typeof engine["classify"] === "function") {
804
- const rootCropInput = {
805
- frame,
806
- roi: { x: 0, y: 0, w: frame.width, h: frame.height },
807
- parentDetection: { class: "", originalClass: "", score: 1, bbox: { x: 0, y: 0, w: frame.width, h: frame.height } }
808
- };
809
- output = await engine.classify(rootCropInput);
810
- } else {
811
- throw new Error(`Addon "${node.addon}" has no detect/classify method`);
812
- }
637
+ output = await caller.process(frame);
813
638
  }
814
639
  const stepMs = performance.now() - stepStart;
815
640
  const slot = "detections" in output ? "detector" : "crops" in output ? "cropper" : "classifier";
@@ -831,7 +656,7 @@ var PipelineRunner = class {
831
656
  }
832
657
  } catch (error) {
833
658
  const stepMs = performance.now() - stepStart;
834
- const message = error instanceof Error ? error.message : String(error);
659
+ const message = errMsg(error);
835
660
  results.push({
836
661
  addon: node.addon,
837
662
  slot: "detector",
@@ -852,6 +677,7 @@ var PipelineRunner = class {
852
677
  async executeChildren(children, frame, parentDetections, parentResultId, results, timings) {
853
678
  const promises2 = [];
854
679
  for (const detection of parentDetections) {
680
+ if (detection.bbox.w < 1 || detection.bbox.h < 1) continue;
855
681
  for (const child of children) {
856
682
  promises2.push(
857
683
  this.executeNode(child, frame, { det: detection, resultId: parentResultId }, results, timings)
@@ -862,207 +688,25 @@ var PipelineRunner = class {
862
688
  }
863
689
  };
864
690
 
865
- // src/process/managed-process.ts
866
- import { fork, spawn as spawn2 } from "child_process";
867
- var DEFAULT_MAX_RESTARTS = 10;
868
- var MAX_BACKOFF_MS = 6e4;
869
- var GRACEFUL_SHUTDOWN_MS = 5e3;
870
- var ManagedProcess = class {
871
- constructor(config, events, logger) {
872
- this.config = config;
873
- this.events = events;
874
- this.logger = logger;
875
- }
876
- childProcess = null;
877
- _state = "stopped";
878
- _startedAt;
879
- restartTimer;
880
- _restartCount = 0;
881
- _lastCrashAt;
882
- _lastCrashError;
883
- get state() {
884
- return this._state;
885
- }
886
- async start() {
887
- this._state = "starting";
888
- try {
889
- if (this.config.modulePath) {
890
- this.childProcess = fork(this.config.modulePath, this.config.args ?? [], {
891
- env: { ...process.env, ...this.config.env },
892
- stdio: ["pipe", "pipe", "pipe", "ipc"]
893
- });
894
- } else if (this.config.command) {
895
- this.childProcess = spawn2(this.config.command, this.config.args ?? [], {
896
- env: { ...process.env, ...this.config.env },
897
- stdio: ["pipe", "pipe", "pipe"]
898
- });
899
- } else {
900
- throw new Error("No command or modulePath specified");
901
- }
902
- this.childProcess.stdout?.on("data", (data) => {
903
- this.logger.debug(data.toString().trim());
904
- });
905
- this.childProcess.stderr?.on("data", (data) => {
906
- this.logger.warn(data.toString().trim());
907
- });
908
- this.childProcess.on("exit", (code, signal) => {
909
- const msg = `Process exited: code=${code}, signal=${signal}`;
910
- if (code === 0) {
911
- this.logger.info(msg);
912
- this._state = "stopped";
913
- } else {
914
- this._lastCrashAt = Date.now();
915
- this._lastCrashError = msg;
916
- this.logger.error(msg);
917
- this._state = "error";
918
- this.events.emitProcessCrashed(
919
- this.config.id,
920
- code,
921
- signal,
922
- this._restartCount
923
- );
924
- const maxRestarts = this.config.maxRestarts ?? DEFAULT_MAX_RESTARTS;
925
- if (this.config.autoRestart && this._restartCount < maxRestarts) {
926
- this.scheduleRestart();
927
- }
928
- }
929
- this.childProcess = null;
930
- });
931
- this.childProcess.on("error", (err) => {
932
- this.logger.error(`Process error: ${err.message}`);
933
- this._lastCrashError = err.message;
934
- this._state = "error";
935
- });
936
- this._state = "running";
937
- this._startedAt = Date.now();
938
- } catch (err) {
939
- const msg = err instanceof Error ? err.message : String(err);
940
- this._state = "error";
941
- this._lastCrashError = msg;
942
- throw err;
943
- }
944
- }
945
- async stop() {
946
- if (this.restartTimer) {
947
- clearTimeout(this.restartTimer);
948
- this.restartTimer = void 0;
949
- }
950
- if (this._state === "stopped") {
951
- return;
952
- }
953
- this._state = "stopping";
954
- if (this.childProcess && !this.childProcess.killed) {
955
- this.childProcess.kill("SIGTERM");
956
- await new Promise((resolve) => {
957
- const timeout = setTimeout(() => {
958
- if (this.childProcess && !this.childProcess.killed) {
959
- this.childProcess.kill("SIGKILL");
960
- }
961
- resolve();
962
- }, GRACEFUL_SHUTDOWN_MS);
963
- this.childProcess?.on("exit", () => {
964
- clearTimeout(timeout);
965
- resolve();
966
- });
967
- });
968
- }
969
- const currentState = this._state;
970
- if (currentState !== "stopped") {
971
- this._state = "stopped";
972
- }
691
+ // src/pipeline/engine-manager-resolver.ts
692
+ var EngineManagerResolver = class {
693
+ constructor(engineManager, addonConfigs) {
694
+ this.engineManager = engineManager;
695
+ this.addonConfigs = addonConfigs;
973
696
  }
974
- getStatus() {
697
+ engineManager;
698
+ addonConfigs;
699
+ async resolve(addonId, config) {
700
+ const globalConfig = this.addonConfigs.get(addonId) ?? {};
701
+ const engine = await this.engineManager.getOrCreateEngine(addonId, globalConfig, config);
975
702
  return {
976
- id: this.config.id,
977
- label: this.config.label,
978
- state: this._state,
979
- pid: this.childProcess?.pid,
980
- stats: this.childProcess?.pid ? this.getStats() : void 0,
981
- lastCrashAt: this._lastCrashAt,
982
- lastCrashError: this._lastCrashError,
983
- restartCount: this._restartCount,
984
- nextRestartAt: this.restartTimer ? this.getNextRestartTime() : void 0
985
- };
986
- }
987
- scheduleRestart() {
988
- this._restartCount++;
989
- const delayMs = Math.min(1e3 * Math.pow(2, this._restartCount - 1), MAX_BACKOFF_MS);
990
- this.logger.info(`Scheduling restart #${this._restartCount} in ${delayMs}ms`);
991
- this.events.emitProcessRestartScheduled(
992
- this.config.id,
993
- this._restartCount,
994
- delayMs
995
- );
996
- this.restartTimer = setTimeout(async () => {
997
- this.restartTimer = void 0;
998
- try {
999
- await this.start();
1000
- this.events.emitProcessRestarted(this.config.id, this._restartCount);
1001
- } catch (err) {
1002
- this.logger.error(`Restart failed: ${err}`);
703
+ process: async (input) => {
704
+ return engine.process(input);
1003
705
  }
1004
- }, delayMs);
1005
- }
1006
- getStats() {
1007
- if (!this.childProcess?.pid) return void 0;
1008
- const uptime = this._startedAt ? Date.now() - this._startedAt : 0;
1009
- return {
1010
- pid: this.childProcess.pid,
1011
- cpu: 0,
1012
- memory: 0,
1013
- uptime,
1014
- restartCount: this._restartCount
1015
706
  };
1016
707
  }
1017
- getNextRestartTime() {
1018
- if (!this._lastCrashAt) return void 0;
1019
- const delayMs = Math.min(1e3 * Math.pow(2, this._restartCount - 1), MAX_BACKOFF_MS);
1020
- return this._lastCrashAt + delayMs;
1021
- }
1022
- };
1023
-
1024
- // src/process/process-manager.ts
1025
- var ProcessManager = class {
1026
- constructor(events, loggerFactory) {
1027
- this.events = events;
1028
- this.loggerFactory = loggerFactory;
1029
- }
1030
- processes = /* @__PURE__ */ new Map();
1031
- register(config) {
1032
- if (this.processes.has(config.id)) {
1033
- throw new Error(`Process already registered: ${config.id}`);
1034
- }
1035
- const logger = this.loggerFactory.createLogger(`process:${config.id}`);
1036
- const managed = new ManagedProcess(config, this.events, logger);
1037
- this.processes.set(config.id, managed);
1038
- return managed;
1039
- }
1040
- async start(id) {
1041
- const p = this.get(id);
1042
- await p.start();
1043
- }
1044
- async stop(id) {
1045
- const p = this.get(id);
1046
- await p.stop();
1047
- }
1048
- async restart(id) {
1049
- const p = this.get(id);
1050
- await p.stop();
1051
- await p.start();
1052
- }
1053
- get(id) {
1054
- const p = this.processes.get(id);
1055
- if (!p) throw new Error(`Process not found: ${id}`);
1056
- return p;
1057
- }
1058
- listAll() {
1059
- return [...this.processes.values()].map((p) => p.getStatus());
1060
- }
1061
708
  async shutdownAll() {
1062
- const running = [...this.processes.values()].filter(
1063
- (p) => p.getStatus().state === "running"
1064
- );
1065
- await Promise.all(running.map((p) => p.stop()));
709
+ await this.engineManager.shutdownAll();
1066
710
  }
1067
711
  };
1068
712
 
@@ -1134,14 +778,15 @@ var ReplEngine = class {
1134
778
  constructor(contextProvider) {
1135
779
  this.contextProvider = contextProvider;
1136
780
  }
781
+ contextProvider;
1137
782
  async execute(code, context) {
1138
783
  const start = Date.now();
1139
- const sandbox = this.buildSandbox(context);
784
+ const sandbox = await this.buildSandbox(context);
1140
785
  try {
1141
786
  const vmContext = vm.createContext(sandbox);
1142
787
  const script = new vm.Script(code);
1143
788
  let result = script.runInContext(vmContext, { timeout: EXECUTION_TIMEOUT_MS });
1144
- if (result && typeof result.then === "function") {
789
+ if (result && typeof result === "object" && "then" in result && typeof result.then === "function") {
1145
790
  result = await Promise.race([
1146
791
  result,
1147
792
  new Promise(
@@ -1173,7 +818,7 @@ var ReplEngine = class {
1173
818
  }
1174
819
  }
1175
820
  async getCompletions(partial, context) {
1176
- const sandbox = this.buildSandbox(context);
821
+ const sandbox = await this.buildSandbox(context);
1177
822
  const keys = Object.keys(sandbox);
1178
823
  if (!partial) return keys;
1179
824
  const lastDot = partial.lastIndexOf(".");
@@ -1194,7 +839,7 @@ var ReplEngine = class {
1194
839
  }
1195
840
  return [];
1196
841
  }
1197
- buildSandbox(context) {
842
+ async buildSandbox(context) {
1198
843
  const base = {
1199
844
  console: {
1200
845
  log: (...args) => util.inspect(args),
@@ -1223,583 +868,40 @@ var ReplEngine = class {
1223
868
  };
1224
869
  switch (context.scope.type) {
1225
870
  case "system":
1226
- return { ...base, ...this.contextProvider.getSystemSandbox() };
871
+ return { ...base, ...await this.contextProvider.getSystemSandbox() };
1227
872
  case "device":
1228
- return { ...base, ...this.contextProvider.getDeviceSandbox(context.scope.deviceId) };
873
+ return { ...base, ...await this.contextProvider.getDeviceSandbox(context.scope.deviceId) };
1229
874
  case "provider":
1230
- return { ...base, ...this.contextProvider.getProviderSandbox(context.scope.providerId) };
875
+ return { ...base, ...await this.contextProvider.getProviderSandbox(context.scope.providerId) };
1231
876
  case "addon":
1232
- return { ...base, ...this.contextProvider.getAddonSandbox(context.scope.addonId) };
877
+ return { ...base, ...await this.contextProvider.getAddonSandbox(context.scope.addonId) };
1233
878
  default:
1234
879
  return base;
1235
880
  }
1236
881
  }
1237
882
  };
1238
883
 
1239
- // src/agent/agent-registry.ts
1240
- var AgentRegistry = class {
1241
- constructor(events, heartbeatTimeoutMs = 3e4, heartbeatCheckIntervalMs = 1e4) {
1242
- this.events = events;
1243
- this.heartbeatTimeoutMs = heartbeatTimeoutMs;
1244
- this.heartbeatCheckIntervalMs = heartbeatCheckIntervalMs;
1245
- }
1246
- agents = /* @__PURE__ */ new Map();
1247
- heartbeatIntervals = /* @__PURE__ */ new Map();
1248
- /** Register a new agent */
1249
- registerAgent(info) {
1250
- const entry = {
1251
- info,
1252
- state: "online",
1253
- connectedSince: Date.now(),
1254
- lastHeartbeat: Date.now(),
1255
- activeTaskCount: 0,
1256
- completedTaskCount: 0,
1257
- failedTaskCount: 0
1258
- };
1259
- this.agents.set(info.id, entry);
1260
- this.startHeartbeatCheck(info.id);
1261
- this.events.emitAgentRegistered(info.id, [...info.capabilities]);
1262
- }
1263
- /** Remove agent */
1264
- unregisterAgent(id) {
1265
- const interval = this.heartbeatIntervals.get(id);
1266
- if (interval) {
1267
- clearInterval(interval);
1268
- this.heartbeatIntervals.delete(id);
1269
- }
1270
- this.agents.delete(id);
1271
- this.events.emitAgentUnregistered(id);
1272
- }
1273
- /** Update heartbeat timestamp */
1274
- heartbeat(id) {
1275
- const entry = this.agents.get(id);
1276
- if (!entry) return;
1277
- entry.lastHeartbeat = Date.now();
1278
- if (entry.state === "offline") {
1279
- entry.state = "online";
1280
- this.events.emitAgentOnline(id);
1281
- }
1282
- }
1283
- /** Find agents with a specific capability */
1284
- getAgentsWithCapability(capability) {
1285
- return [...this.agents.values()].filter(
1286
- (a) => a.state === "online" && a.info.capabilities.includes(capability)
1287
- );
1288
- }
1289
- /** Get best agent for a task (least loaded) */
1290
- selectAgent(capability, preferredId) {
1291
- const capable = this.getAgentsWithCapability(capability);
1292
- if (capable.length === 0) return null;
1293
- if (preferredId) {
1294
- const preferred = capable.find((a) => a.info.id === preferredId);
1295
- if (preferred) return preferred;
1296
- }
1297
- const sorted = [...capable].sort((a, b) => a.activeTaskCount - b.activeTaskCount);
1298
- return sorted[0] ?? null;
1299
- }
1300
- /** List all agents with status */
1301
- listAgents() {
1302
- return [...this.agents.values()].map((entry) => ({
1303
- id: entry.info.id,
1304
- name: entry.info.name,
1305
- state: entry.state,
1306
- capabilities: [...entry.info.capabilities],
1307
- lastHeartbeat: entry.lastHeartbeat,
1308
- connectedSince: entry.connectedSince,
1309
- resources: entry.info.resources,
1310
- activeTaskCount: entry.activeTaskCount,
1311
- completedTaskCount: entry.completedTaskCount,
1312
- failedTaskCount: entry.failedTaskCount
1313
- }));
1314
- }
1315
- /** Get single agent */
1316
- getAgent(id) {
1317
- return this.agents.get(id) ?? null;
1318
- }
1319
- /** Destroy all heartbeat intervals */
1320
- destroy() {
1321
- for (const interval of this.heartbeatIntervals.values()) {
1322
- clearInterval(interval);
1323
- }
1324
- this.heartbeatIntervals.clear();
1325
- }
1326
- startHeartbeatCheck(id) {
1327
- const interval = setInterval(() => {
1328
- const entry = this.agents.get(id);
1329
- if (!entry) return;
1330
- const elapsed = Date.now() - entry.lastHeartbeat;
1331
- if (elapsed > this.heartbeatTimeoutMs && entry.state === "online") {
1332
- entry.state = "offline";
1333
- this.events.emitAgentOffline(id);
1334
- }
1335
- }, this.heartbeatCheckIntervalMs);
1336
- this.heartbeatIntervals.set(id, interval);
1337
- }
884
+ // src/lifecycle/lifecycle-state-machine.ts
885
+ import { randomUUID as randomUUID2 } from "crypto";
886
+ var VALID_TRANSITIONS = {
887
+ stopped: ["starting", "disabled"],
888
+ starting: ["running", "error", "stopping"],
889
+ running: ["stopping", "error"],
890
+ stopping: ["stopped", "error"],
891
+ error: ["starting", "stopped", "disabled"],
892
+ disabled: ["stopped"]
1338
893
  };
1339
-
1340
- // src/agent/task-dispatcher.ts
1341
- var TaskDispatcher = class {
1342
- constructor(agentRegistry, events) {
1343
- this.agentRegistry = agentRegistry;
1344
- this.events = events;
1345
- }
1346
- pendingTasks = /* @__PURE__ */ new Map();
1347
- localExecutors = /* @__PURE__ */ new Map();
1348
- /** Dispatch a task to the best available agent */
1349
- async dispatch(task, options) {
1350
- const capability = options?.capability ?? task.capability;
1351
- const agent = this.agentRegistry.selectAgent(capability, options?.preferredAgent);
1352
- if (!agent && !options?.remoteOnly) {
1353
- return this.executeLocally(task);
1354
- }
1355
- if (!agent) {
1356
- return {
1357
- taskId: task.id,
1358
- agentId: "none",
1359
- status: "error",
1360
- error: "No agent available",
1361
- durationMs: 0
1362
- };
1363
- }
1364
- return this.sendToAgent(task, agent);
1365
- }
1366
- /** Register a local executor for a capability (in-process fallback) */
1367
- registerLocalExecutor(capability, executor) {
1368
- this.localExecutors.set(capability, executor);
1369
- }
1370
- /** Called when an agent returns a result */
1371
- handleTaskResult(result) {
1372
- const pending = this.pendingTasks.get(result.taskId);
1373
- if (pending) {
1374
- clearTimeout(pending.timeout);
1375
- this.pendingTasks.delete(result.taskId);
1376
- pending.resolve(result);
1377
- }
1378
- const agent = this.agentRegistry.getAgent(result.agentId);
1379
- if (agent) {
1380
- agent.activeTaskCount--;
1381
- if (result.status === "success") {
1382
- agent.completedTaskCount++;
1383
- } else {
1384
- agent.failedTaskCount++;
1385
- }
1386
- }
1387
- }
1388
- async executeLocally(task) {
1389
- const executor = this.localExecutors.get(task.capability);
1390
- if (!executor) {
1391
- return {
1392
- taskId: task.id,
1393
- agentId: "local",
1394
- status: "error",
1395
- error: `No local executor for ${task.capability}`,
1396
- durationMs: 0
1397
- };
1398
- }
1399
- const start = Date.now();
1400
- try {
1401
- const output = await executor(task.input);
1402
- return {
1403
- taskId: task.id,
1404
- agentId: "local",
1405
- status: "success",
1406
- output,
1407
- durationMs: Date.now() - start
1408
- };
1409
- } catch (err) {
1410
- return {
1411
- taskId: task.id,
1412
- agentId: "local",
1413
- status: "error",
1414
- error: String(err),
1415
- durationMs: Date.now() - start
1416
- };
1417
- }
894
+ var LifecycleStateMachine = class {
895
+ constructor(elementId, elementType, eventBus, logger) {
896
+ this.elementId = elementId;
897
+ this.elementType = elementType;
898
+ this.eventBus = eventBus;
899
+ this.logger = logger;
1418
900
  }
1419
- sendToAgent(task, agent) {
1420
- agent.activeTaskCount++;
1421
- return new Promise((resolve) => {
1422
- const timeout = setTimeout(() => {
1423
- this.pendingTasks.delete(task.id);
1424
- agent.activeTaskCount--;
1425
- agent.failedTaskCount++;
1426
- resolve({
1427
- taskId: task.id,
1428
- agentId: agent.info.id,
1429
- status: "timeout",
1430
- durationMs: task.timeout,
1431
- error: "Task timed out"
1432
- });
1433
- }, task.timeout);
1434
- this.pendingTasks.set(task.id, { resolve, reject: () => {
1435
- }, timeout });
1436
- this.events.emitTaskDispatched(task.id, agent.info.id, task.capability);
1437
- });
1438
- }
1439
- };
1440
-
1441
- // src/agent/agent-client.ts
1442
- var RECONNECT_BASE_MS = 1e3;
1443
- var RECONNECT_MAX_MS = 3e4;
1444
- var HEARTBEAT_INTERVAL_MS = 1e4;
1445
- var AgentClient = class {
1446
- ws = null;
1447
- reconnectAttempt = 0;
1448
- reconnectTimer = null;
1449
- heartbeatTimer = null;
1450
- destroyed = false;
1451
- messageHandlers = [];
1452
- binaryHandlers = [];
1453
- connectHandlers = [];
1454
- disconnectHandlers = [];
901
+ elementId;
902
+ elementType;
903
+ eventBus;
1455
904
  logger;
1456
- hubUrl;
1457
- token;
1458
- registrationInfo;
1459
- runtimeStatus = {
1460
- activeCameras: 0,
1461
- cpuPercent: 0,
1462
- memoryPercent: 0,
1463
- fps: {},
1464
- errors: []
1465
- };
1466
- constructor(config) {
1467
- this.hubUrl = config.hubUrl;
1468
- this.token = config.token;
1469
- this.logger = config.logger;
1470
- this.registrationInfo = config.registrationInfo;
1471
- }
1472
- /** Connect to the hub WebSocket */
1473
- async connect() {
1474
- return new Promise((resolve, reject) => {
1475
- this.destroyed = false;
1476
- this.doConnect(resolve, reject);
1477
- });
1478
- }
1479
- /** Disconnect and stop reconnecting */
1480
- disconnect() {
1481
- this.destroyed = true;
1482
- this.clearTimers();
1483
- if (this.ws) {
1484
- this.ws.close();
1485
- this.ws = null;
1486
- }
1487
- }
1488
- /** Send a JSON control message to the hub */
1489
- send(msg) {
1490
- if (!this.ws || this.ws.readyState !== 1) {
1491
- this.logger.warn("send() called while not connected");
1492
- return;
1493
- }
1494
- this.ws.send(JSON.stringify(msg));
1495
- }
1496
- /** Send a binary frame to the hub */
1497
- sendBinary(data) {
1498
- if (!this.ws || this.ws.readyState !== 1) {
1499
- return;
1500
- }
1501
- this.ws.send(data);
1502
- }
1503
- /** Register a handler for JSON messages from hub */
1504
- onMessage(handler) {
1505
- this.messageHandlers.push(handler);
1506
- }
1507
- /** Register a handler for binary frames from hub */
1508
- onBinaryFrame(handler) {
1509
- this.binaryHandlers.push(handler);
1510
- }
1511
- /** Register a handler for successful connection */
1512
- onConnect(handler) {
1513
- this.connectHandlers.push(handler);
1514
- }
1515
- /** Register a handler for disconnection */
1516
- onDisconnect(handler) {
1517
- this.disconnectHandlers.push(handler);
1518
- }
1519
- /** Update the runtime status (used in heartbeat) */
1520
- updateStatus(status) {
1521
- this.runtimeStatus = status;
1522
- }
1523
- /** Whether currently connected */
1524
- get connected() {
1525
- return this.ws?.readyState === 1;
1526
- }
1527
- doConnect(onConnect, onError) {
1528
- if (this.destroyed) return;
1529
- import("ws").then(({ default: WebSocket }) => {
1530
- if (this.destroyed) return;
1531
- const url = this.token ? `${this.hubUrl}?token=${encodeURIComponent(this.token)}` : this.hubUrl;
1532
- const wsOptions = this.hubUrl.startsWith("wss://") ? { rejectUnauthorized: false } : {};
1533
- try {
1534
- this.ws = new WebSocket(url, wsOptions);
1535
- } catch (err) {
1536
- this.scheduleReconnect();
1537
- if (onError) {
1538
- onError(err instanceof Error ? err : new Error(String(err)));
1539
- onError = void 0;
1540
- }
1541
- return;
1542
- }
1543
- this.ws.once("open", () => {
1544
- this.reconnectAttempt = 0;
1545
- this.logger.info(`Connected to hub: ${this.hubUrl}`);
1546
- this.send({ type: "register", info: this.registrationInfo });
1547
- this.startHeartbeat();
1548
- for (const h of this.connectHandlers) h();
1549
- if (onConnect) {
1550
- onConnect();
1551
- onConnect = void 0;
1552
- }
1553
- });
1554
- this.ws.on("message", (data, isBinary) => {
1555
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
1556
- if (isBinary) {
1557
- for (const h of this.binaryHandlers) h(buf);
1558
- } else {
1559
- try {
1560
- const msg = JSON.parse(buf.toString());
1561
- this.handleBuiltinMessage(msg);
1562
- for (const h of this.messageHandlers) h(msg);
1563
- } catch {
1564
- this.logger.warn("Failed to parse message from hub");
1565
- }
1566
- }
1567
- });
1568
- this.ws.once("close", () => {
1569
- this.stopHeartbeat();
1570
- for (const h of this.disconnectHandlers) h();
1571
- if (!this.destroyed) {
1572
- this.logger.warn("Disconnected from hub, scheduling reconnect");
1573
- this.scheduleReconnect();
1574
- }
1575
- });
1576
- this.ws.once("error", (err) => {
1577
- this.logger.error("WebSocket error", { message: err.message });
1578
- if (onError) {
1579
- onError(err);
1580
- onError = void 0;
1581
- }
1582
- });
1583
- }).catch((err) => {
1584
- this.logger.error("Failed to import ws module", { message: String(err) });
1585
- if (onError) {
1586
- onError(err instanceof Error ? err : new Error(String(err)));
1587
- }
1588
- });
1589
- }
1590
- handleBuiltinMessage(msg) {
1591
- if (msg.type === "ping") {
1592
- this.send({ type: "pong" });
1593
- }
1594
- }
1595
- scheduleReconnect() {
1596
- if (this.destroyed) return;
1597
- const delay = Math.min(
1598
- RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt),
1599
- RECONNECT_MAX_MS
1600
- );
1601
- this.reconnectAttempt++;
1602
- this.logger.info(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
1603
- this.reconnectTimer = setTimeout(() => {
1604
- this.doConnect();
1605
- }, delay);
1606
- }
1607
- startHeartbeat() {
1608
- this.stopHeartbeat();
1609
- this.heartbeatTimer = setInterval(() => {
1610
- this.send({ type: "heartbeat", status: this.runtimeStatus });
1611
- }, HEARTBEAT_INTERVAL_MS);
1612
- }
1613
- stopHeartbeat() {
1614
- if (this.heartbeatTimer) {
1615
- clearInterval(this.heartbeatTimer);
1616
- this.heartbeatTimer = null;
1617
- }
1618
- }
1619
- clearTimers() {
1620
- this.stopHeartbeat();
1621
- if (this.reconnectTimer) {
1622
- clearTimeout(this.reconnectTimer);
1623
- this.reconnectTimer = null;
1624
- }
1625
- }
1626
- };
1627
-
1628
- // src/agent/agent-task-runner.ts
1629
- var AgentTaskRunner = class {
1630
- constructor(agentId, client, logger) {
1631
- this.agentId = agentId;
1632
- this.client = client;
1633
- this.logger = logger.child("TaskRunner");
1634
- }
1635
- handlers = /* @__PURE__ */ new Map();
1636
- runningTasks = /* @__PURE__ */ new Map();
1637
- logger;
1638
- /** Register a task handler for a given task type */
1639
- registerHandler(handler) {
1640
- if (this.handlers.has(handler.taskType)) {
1641
- this.logger.warn(`Overwriting handler for task type: ${handler.taskType}`);
1642
- }
1643
- this.handlers.set(handler.taskType, handler);
1644
- this.logger.debug(`Registered handler: ${handler.taskType}`);
1645
- }
1646
- /** Unregister a task handler */
1647
- unregisterHandler(taskType) {
1648
- this.handlers.delete(taskType);
1649
- }
1650
- /** Get all registered task types */
1651
- getTaskTypes() {
1652
- return [...this.handlers.keys()];
1653
- }
1654
- /** Get a handler by task type */
1655
- getHandler(taskType) {
1656
- return this.handlers.get(taskType);
1657
- }
1658
- /**
1659
- * Execute a task dispatched from the hub.
1660
- * Sends result or error back to hub via the AgentClient.
1661
- */
1662
- async executeTask(taskId, taskType, payload) {
1663
- const handler = this.handlers.get(taskType);
1664
- if (!handler) {
1665
- this.client.send({
1666
- type: "task.result",
1667
- taskId,
1668
- success: false,
1669
- error: `No handler registered for task type: ${taskType}`
1670
- });
1671
- return;
1672
- }
1673
- const runningTask = { taskId, taskType, cancelled: false };
1674
- this.runningTasks.set(taskId, runningTask);
1675
- const context = {
1676
- taskId,
1677
- agentId: this.agentId,
1678
- logger: this.logger.child(taskType),
1679
- reportProgress: (progress) => {
1680
- this.client.send({ type: "task.progress", taskId, progress });
1681
- },
1682
- isCancelled: () => runningTask.cancelled
1683
- };
1684
- try {
1685
- const result = await handler.handle(payload, context);
1686
- this.runningTasks.delete(taskId);
1687
- if (!runningTask.cancelled) {
1688
- this.client.send({
1689
- type: "task.result",
1690
- taskId,
1691
- success: true,
1692
- result
1693
- });
1694
- }
1695
- } catch (err) {
1696
- this.runningTasks.delete(taskId);
1697
- this.logger.error(`Task ${taskType} (${taskId}) failed`, {
1698
- message: err instanceof Error ? err.message : String(err)
1699
- });
1700
- this.client.send({
1701
- type: "task.result",
1702
- taskId,
1703
- success: false,
1704
- error: err instanceof Error ? err.message : String(err)
1705
- });
1706
- }
1707
- }
1708
- /** Cancel a running task */
1709
- async cancelTask(taskId) {
1710
- const running = this.runningTasks.get(taskId);
1711
- if (!running) return;
1712
- running.cancelled = true;
1713
- const handler = this.handlers.get(running.taskType);
1714
- if (handler?.cancel) {
1715
- try {
1716
- await handler.cancel();
1717
- } catch (err) {
1718
- this.logger.error(`Error cancelling task ${taskId}`, {
1719
- message: err instanceof Error ? err.message : String(err)
1720
- });
1721
- }
1722
- }
1723
- this.runningTasks.delete(taskId);
1724
- }
1725
- /** Number of currently running tasks */
1726
- get activeTaskCount() {
1727
- return this.runningTasks.size;
1728
- }
1729
- /** Destroy: cancel all running tasks */
1730
- async destroy() {
1731
- const taskIds = [...this.runningTasks.keys()];
1732
- for (const taskId of taskIds) {
1733
- await this.cancelTask(taskId);
1734
- }
1735
- this.handlers.clear();
1736
- }
1737
- };
1738
-
1739
- // src/agent/pipeline-task-handlers.ts
1740
- var DecodeTaskHandler = class {
1741
- taskType = "pipeline.decode";
1742
- description = "Decode an RTSP stream and produce JPEG frames";
1743
- async handle(payload, context) {
1744
- const { logger } = context;
1745
- const config = payload;
1746
- if (!config.cameraId || !config.rtspUrl) {
1747
- throw new Error("pipeline.decode requires cameraId and rtspUrl in payload");
1748
- }
1749
- logger.info(`Decode task started: camera=${config.cameraId} url=${config.rtspUrl} fps=${config.fps ?? 1}`);
1750
- return { status: "started", cameraId: config.cameraId };
1751
- }
1752
- async cancel() {
1753
- }
1754
- };
1755
- var DetectTaskHandler = class {
1756
- taskType = "pipeline.detect";
1757
- description = "Run object detection on received frames";
1758
- async handle(payload, context) {
1759
- const { logger } = context;
1760
- const config = payload;
1761
- if (!config.cameraId) {
1762
- throw new Error("pipeline.detect requires cameraId in payload");
1763
- }
1764
- logger.info(`Detect task started: camera=${config.cameraId} model=${config.modelId ?? "default"} runtime=${config.runtime ?? "auto"}`);
1765
- return { status: "started", cameraId: config.cameraId };
1766
- }
1767
- async cancel() {
1768
- }
1769
- };
1770
- var RecordTaskHandler = class {
1771
- taskType = "pipeline.record";
1772
- description = "Record an RTSP stream to disk segments";
1773
- async handle(payload, context) {
1774
- const { logger } = context;
1775
- const config = payload;
1776
- if (!config.cameraId || !config.rtspUrl) {
1777
- throw new Error("pipeline.record requires cameraId and rtspUrl in payload");
1778
- }
1779
- logger.info(`Record task started: camera=${config.cameraId} url=${config.rtspUrl}`);
1780
- return { status: "started", cameraId: config.cameraId };
1781
- }
1782
- async cancel() {
1783
- }
1784
- };
1785
-
1786
- // src/lifecycle/lifecycle-state-machine.ts
1787
- import { randomUUID as randomUUID2 } from "crypto";
1788
- var VALID_TRANSITIONS = {
1789
- stopped: ["starting", "disabled"],
1790
- starting: ["running", "error", "stopping"],
1791
- running: ["stopping", "error"],
1792
- stopping: ["stopped", "error"],
1793
- error: ["starting", "stopped", "disabled"],
1794
- disabled: ["stopped"]
1795
- };
1796
- var LifecycleStateMachine = class {
1797
- constructor(elementId, elementType, eventBus, logger) {
1798
- this.elementId = elementId;
1799
- this.elementType = elementType;
1800
- this.eventBus = eventBus;
1801
- this.logger = logger;
1802
- }
1803
905
  _state = "stopped";
1804
906
  _error;
1805
907
  _startedAt;
@@ -1822,7 +924,7 @@ var LifecycleStateMachine = class {
1822
924
  transition(to, error) {
1823
925
  const from = this._state;
1824
926
  if (!this.isValidTransition(from, to)) {
1825
- this.logger.warn(`Invalid state transition: ${from} \u2192 ${to}`);
927
+ this.logger.warn("Invalid state transition", { meta: { from, to } });
1826
928
  return false;
1827
929
  }
1828
930
  this._state = to;
@@ -1838,7 +940,7 @@ var LifecycleStateMachine = class {
1838
940
  if (to === "stopped" || to === "error" || to === "disabled") {
1839
941
  this._stoppedAt = Date.now();
1840
942
  }
1841
- this.logger.info(`State: ${from} \u2192 ${to}${error ? ` (${error})` : ""}`);
943
+ this.logger.info("State transition", { meta: { from, to, error } });
1842
944
  this.eventBus.emit({
1843
945
  id: randomUUID2(),
1844
946
  timestamp: /* @__PURE__ */ new Date(),
@@ -1861,6 +963,7 @@ var FeatureManager = class {
1861
963
  constructor(configReader) {
1862
964
  this.configReader = configReader;
1863
965
  }
966
+ configReader;
1864
967
  isEnabled(flag) {
1865
968
  return this.configReader.features[flag];
1866
969
  }
@@ -1873,8 +976,9 @@ var FeatureManager = class {
1873
976
  var EventRingBuffer = class {
1874
977
  constructor(capacity) {
1875
978
  this.capacity = capacity;
1876
- this.buffer = new Array(capacity);
979
+ this.buffer = Array.from({ length: capacity });
1877
980
  }
981
+ capacity;
1878
982
  buffer;
1879
983
  head = 0;
1880
984
  count = 0;
@@ -1912,14 +1016,36 @@ function matchesCategory(eventCategory, filterCategory) {
1912
1016
  return eventCategory === filterCategory;
1913
1017
  }
1914
1018
  function matchesFilter(event, filter) {
1915
- if (filter.category && !matchesCategory(event.category, filter.category)) {
1916
- return false;
1019
+ if (filter.category) {
1020
+ const raw = filter.category;
1021
+ const categories = Array.isArray(raw) ? raw.map(String) : [String(raw)];
1022
+ if (!categories.some((cat) => matchesCategory(event.category, cat))) {
1023
+ return false;
1024
+ }
1917
1025
  }
1918
1026
  if (filter.source) {
1919
1027
  if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) {
1920
1028
  return false;
1921
1029
  }
1922
1030
  }
1031
+ if (filter.agentId) {
1032
+ const eventNodeId = event.source.nodeId;
1033
+ if (!eventNodeId || !eventNodeId.startsWith(filter.agentId)) {
1034
+ return false;
1035
+ }
1036
+ }
1037
+ if (filter.addonId) {
1038
+ const eventAddonId = event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0);
1039
+ if (eventAddonId !== filter.addonId) {
1040
+ return false;
1041
+ }
1042
+ }
1043
+ if (filter.deviceId !== void 0) {
1044
+ const eventDeviceId = event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0);
1045
+ if (eventDeviceId !== filter.deviceId) {
1046
+ return false;
1047
+ }
1048
+ }
1923
1049
  if (filter.since && event.timestamp < filter.since) {
1924
1050
  return false;
1925
1051
  }
@@ -1945,6 +1071,10 @@ var SystemEventBus = class {
1945
1071
  }
1946
1072
  }
1947
1073
  }
1074
+ /**
1075
+ * Subscribe to events matching a filter.
1076
+ * When called with a typed category filter, the handler receives typed event data.
1077
+ */
1948
1078
  subscribe(filter, handler) {
1949
1079
  const subscriber = { filter, callback: handler };
1950
1080
  this.subscribers.push(subscriber);
@@ -1961,11 +1091,32 @@ var SystemEventBus = class {
1961
1091
  };
1962
1092
 
1963
1093
  // src/logging/log-ring-buffer.ts
1094
+ function matchesLogTags(entryTags, filterTags) {
1095
+ if (!entryTags) return false;
1096
+ for (const [key, value] of Object.entries(filterTags)) {
1097
+ if (value === void 0) continue;
1098
+ const entryValue = entryTags[key];
1099
+ if (key === "addonId") {
1100
+ const bare = typeof entryValue === "string" ? entryValue.split("@")[0] : entryValue;
1101
+ if (bare !== value) return false;
1102
+ } else if (key === "agentId") {
1103
+ const nodeId = entryTags["nodeId"];
1104
+ const matchEmitter = entryValue === value;
1105
+ const matchExactNode = nodeId === value;
1106
+ const matchWorker = typeof nodeId === "string" && nodeId.startsWith(`${value}/`);
1107
+ if (!matchEmitter && !matchExactNode && !matchWorker) return false;
1108
+ } else {
1109
+ if (entryValue !== value) return false;
1110
+ }
1111
+ }
1112
+ return true;
1113
+ }
1964
1114
  var LogRingBuffer = class {
1965
1115
  constructor(capacity = 1e4) {
1966
1116
  this.capacity = capacity;
1967
- this.buffer = new Array(capacity);
1117
+ this.buffer = Array.from({ length: capacity });
1968
1118
  }
1119
+ capacity;
1969
1120
  buffer;
1970
1121
  head = 0;
1971
1122
  count = 0;
@@ -1987,14 +1138,6 @@ var LogRingBuffer = class {
1987
1138
  query(filter) {
1988
1139
  const all = this.getAll();
1989
1140
  let result = all;
1990
- if (filter.scope && filter.scope.length > 0) {
1991
- result = result.filter((entry) => {
1992
- for (let i = 0; i < filter.scope.length; i++) {
1993
- if (entry.scope[i] !== filter.scope[i]) return false;
1994
- }
1995
- return true;
1996
- });
1997
- }
1998
1141
  if (filter.level) {
1999
1142
  result = result.filter((entry) => entry.level === filter.level);
2000
1143
  }
@@ -2004,44 +1147,98 @@ var LogRingBuffer = class {
2004
1147
  if (filter.until) {
2005
1148
  result = result.filter((entry) => entry.timestamp <= filter.until);
2006
1149
  }
1150
+ if (filter.tags) {
1151
+ const filterTags = filter.tags;
1152
+ result = result.filter((entry) => matchesLogTags(entry.tags, filterTags));
1153
+ }
2007
1154
  if (filter.limit !== void 0 && filter.limit > 0) {
2008
1155
  result = result.slice(0, filter.limit);
2009
1156
  }
2010
1157
  return result;
2011
1158
  }
1159
+ /**
1160
+ * Drop entries that match `filter`. Returns the number of entries
1161
+ * removed. Triggered by the UI's "Clear logs" button so the operator
1162
+ * can wipe the historical buffer for a specific scope (per-device,
1163
+ * per-addon, per-agent) — without it, closing and reopening the
1164
+ * panel re-populates the cleared rows from the server's ring buffer.
1165
+ *
1166
+ * Filter semantics mirror `query` (same level / since / until / tags
1167
+ * handling) — whatever you'd see listed by `query(filter)` is what
1168
+ * gets removed.
1169
+ */
1170
+ clear(filter = {}) {
1171
+ if (this.count === 0) return 0;
1172
+ const survivors = [];
1173
+ for (let i = 0; i < this.count; i++) {
1174
+ const index = (this.head - 1 - i + this.capacity) % this.capacity;
1175
+ const entry = this.buffer[index];
1176
+ let matches = true;
1177
+ if (filter.level !== void 0 && entry.level !== filter.level) matches = false;
1178
+ if (matches && filter.since !== void 0 && entry.timestamp < filter.since) matches = false;
1179
+ if (matches && filter.until !== void 0 && entry.timestamp > filter.until) matches = false;
1180
+ if (matches && filter.tags !== void 0 && !matchesLogTags(entry.tags, filter.tags)) matches = false;
1181
+ if (!matches) survivors.push(entry);
1182
+ }
1183
+ const removed = this.count - survivors.length;
1184
+ if (removed === 0) return 0;
1185
+ this.buffer.fill(void 0);
1186
+ this.head = 0;
1187
+ this.count = 0;
1188
+ for (let i = survivors.length - 1; i >= 0; i--) {
1189
+ this.push(survivors[i]);
1190
+ }
1191
+ return removed;
1192
+ }
2012
1193
  };
2013
1194
 
2014
1195
  // src/logging/scoped-logger.ts
2015
1196
  var ScopedLogger = class _ScopedLogger {
2016
- constructor(scope, writeFn) {
1197
+ constructor(scope, writeFn, tags) {
2017
1198
  this.scope = scope;
2018
1199
  this.writeFn = writeFn;
1200
+ this.tags = tags;
2019
1201
  }
2020
- debug(message, meta) {
2021
- this.write("debug", message, meta);
1202
+ scope;
1203
+ writeFn;
1204
+ tags;
1205
+ debug(message, extras) {
1206
+ this.write("debug", message, extras);
2022
1207
  }
2023
- info(message, meta) {
2024
- this.write("info", message, meta);
1208
+ info(message, extras) {
1209
+ this.write("info", message, extras);
2025
1210
  }
2026
- warn(message, meta) {
2027
- this.write("warn", message, meta);
1211
+ warn(message, extras) {
1212
+ this.write("warn", message, extras);
2028
1213
  }
2029
- error(message, meta) {
2030
- this.write("error", message, meta);
1214
+ error(message, extras) {
1215
+ this.write("error", message, extras);
2031
1216
  }
2032
1217
  child(childScope) {
2033
- return new _ScopedLogger([...this.scope, childScope], this.writeFn);
1218
+ return new _ScopedLogger(childScope, this.writeFn, this.tags);
1219
+ }
1220
+ withTags(tags) {
1221
+ const merged = this.tags ? { ...this.tags, ...tags } : tags;
1222
+ return new _ScopedLogger(this.scope, this.writeFn, merged);
2034
1223
  }
2035
- write(level, message, meta) {
1224
+ write(level, message, extras) {
1225
+ const mergedTags = this.mergeTags(extras?.tags);
2036
1226
  const entry = {
2037
1227
  timestamp: /* @__PURE__ */ new Date(),
2038
1228
  level,
2039
- scope: this.scope,
2040
1229
  message,
2041
- ...meta !== void 0 ? { meta } : {}
1230
+ ...this.scope !== void 0 ? { scope: this.scope } : {},
1231
+ ...extras?.meta !== void 0 ? { meta: extras.meta } : {},
1232
+ ...mergedTags ? { tags: mergedTags } : {}
2042
1233
  };
2043
1234
  this.writeFn(entry);
2044
1235
  }
1236
+ mergeTags(extraTags) {
1237
+ if (!this.tags && !extraTags) return void 0;
1238
+ if (!this.tags) return extraTags;
1239
+ if (!extraTags) return this.tags;
1240
+ return { ...this.tags, ...extraTags };
1241
+ }
2045
1242
  };
2046
1243
 
2047
1244
  // src/logging/log-manager.ts
@@ -2049,25 +1246,50 @@ var LogManager = class {
2049
1246
  ringBuffer;
2050
1247
  destinations = [];
2051
1248
  subscribers = /* @__PURE__ */ new Map();
1249
+ deviceNameLookup = null;
2052
1250
  constructor(bufferSize = 1e4) {
2053
1251
  this.ringBuffer = new LogRingBuffer(bufferSize);
2054
1252
  }
1253
+ /**
1254
+ * Register a callback that resolves `deviceId → deviceName`. Called
1255
+ * for every emitted entry that carries `tags.deviceId` but no
1256
+ * explicit `tags.deviceName` — the LogManager injects the resolved
1257
+ * name directly into the entry's tags BEFORE ring-buffer / destination
1258
+ * write, so every destination (local console, file, remote forwarder,
1259
+ * addon-bundled copies of core) sees the enriched shape regardless of
1260
+ * which module instance the destination was built from.
1261
+ */
1262
+ setDeviceNameLookup(lookup) {
1263
+ this.deviceNameLookup = lookup;
1264
+ }
2055
1265
  createLogger(scope) {
2056
- return new ScopedLogger([scope], (entry) => {
2057
- this.ringBuffer.push(entry);
1266
+ return new ScopedLogger(scope, (entry) => {
1267
+ const enriched = this.enrichDeviceName(entry);
1268
+ this.ringBuffer.push(enriched);
2058
1269
  for (const dest of this.destinations) {
2059
- dest.write(entry);
1270
+ dest.write(enriched);
2060
1271
  }
2061
1272
  for (const [, sub] of this.subscribers) {
2062
- if (this.matchesFilter(entry, sub.filter)) {
1273
+ if (this.matchesFilter(enriched, sub.filter)) {
2063
1274
  try {
2064
- sub.callback(entry);
1275
+ sub.callback(enriched);
2065
1276
  } catch {
2066
1277
  }
2067
1278
  }
2068
1279
  }
2069
1280
  });
2070
1281
  }
1282
+ enrichDeviceName(entry) {
1283
+ if (!this.deviceNameLookup) return entry;
1284
+ const tags = entry.tags;
1285
+ if (!tags) return entry;
1286
+ const deviceId = tags.deviceId;
1287
+ if (typeof deviceId !== "number" || !Number.isFinite(deviceId)) return entry;
1288
+ if (typeof tags.deviceName === "string") return entry;
1289
+ const name = this.deviceNameLookup(deviceId);
1290
+ if (!name) return entry;
1291
+ return { ...entry, tags: { ...tags, deviceName: name } };
1292
+ }
2071
1293
  /** Subscribe to live logs matching a filter. Returns an unsubscribe function. */
2072
1294
  subscribe(filter, callback) {
2073
1295
  const id = Math.random().toString(36).slice(2);
@@ -2077,20 +1299,36 @@ var LogManager = class {
2077
1299
  };
2078
1300
  }
2079
1301
  matchesFilter(entry, filter) {
2080
- if (filter.scope && filter.scope.length > 0) {
2081
- const entryScope = entry.scope?.[0] ?? "";
2082
- if (!filter.scope.some((s) => entryScope === s || entryScope.startsWith(s))) return false;
2083
- }
2084
1302
  if (filter.level) {
2085
1303
  const levels = ["debug", "info", "warn", "error"];
2086
1304
  const minIdx = levels.indexOf(filter.level);
2087
1305
  const entryIdx = levels.indexOf(entry.level);
2088
1306
  if (entryIdx < minIdx) return false;
2089
1307
  }
1308
+ if (filter.tags) {
1309
+ if (!matchesLogTags(entry.tags, filter.tags)) return false;
1310
+ }
2090
1311
  return true;
2091
1312
  }
2092
- addDestination(dest) {
1313
+ /**
1314
+ * Register a log destination.
1315
+ *
1316
+ * @param opts.replay If true (default), replay every entry currently in
1317
+ * the ring buffer to the new destination before live
1318
+ * traffic starts. Lets a destination added mid-boot
1319
+ * still receive boot-time log entries.
1320
+ */
1321
+ addDestination(dest, opts = {}) {
1322
+ const replay = opts.replay ?? true;
2093
1323
  this.destinations.push(dest);
1324
+ if (replay) {
1325
+ for (const entry of this.ringBuffer.query({})) {
1326
+ try {
1327
+ dest.write(entry);
1328
+ } catch {
1329
+ }
1330
+ }
1331
+ }
2094
1332
  }
2095
1333
  removeDestination(dest) {
2096
1334
  const index = this.destinations.indexOf(dest);
@@ -2101,6 +1339,16 @@ var LogManager = class {
2101
1339
  query(filter) {
2102
1340
  return this.ringBuffer.query(filter);
2103
1341
  }
1342
+ /**
1343
+ * Drop ring-buffer entries matching `filter`. Returns the count
1344
+ * removed. Backs the UI's "Clear logs" admin action so an operator
1345
+ * can wipe the historical window for a scope (per-device, per-addon,
1346
+ * agent-level) without restarting the server. External destinations
1347
+ * (file, forwarder) are NOT touched — only the in-memory ring.
1348
+ */
1349
+ clear(filter = {}) {
1350
+ return this.ringBuffer.clear(filter);
1351
+ }
2104
1352
  };
2105
1353
 
2106
1354
  // src/storage/storage-manager.ts
@@ -2113,7 +1361,7 @@ var StorageManager = class {
2113
1361
  settingsBackend = null;
2114
1362
  /** @deprecated Set by legacy capability consumer — use setNewStorageProvider instead */
2115
1363
  setProvider(provider) {
2116
- if ("getLocation" in provider && typeof provider.getLocation === "function") {
1364
+ if ("getLocation" in provider) {
2117
1365
  this.legacyProvider = provider;
2118
1366
  } else {
2119
1367
  this.newStorageProvider = provider;
@@ -2125,6 +1373,12 @@ var StorageManager = class {
2125
1373
  setSettingsBackend(backend) {
2126
1374
  this.settingsBackend = backend;
2127
1375
  }
1376
+ getSettingsBackend() {
1377
+ if (!this.settingsBackend) {
1378
+ throw new Error("SettingsBackend not initialized \u2014 storage addon not yet loaded");
1379
+ }
1380
+ return this.settingsBackend;
1381
+ }
2128
1382
  getProvider() {
2129
1383
  if (this.legacyProvider) return this.legacyProvider;
2130
1384
  if (!this.newStorageProvider && !this.settingsBackend && !this.locationManager) {
@@ -2142,7 +1396,7 @@ var StorageManager = class {
2142
1396
  return this.locationManager;
2143
1397
  }
2144
1398
  async initializeLocations(dataPath) {
2145
- const { StorageLocationManager: StorageLocationManager2 } = await import("./storage-location-manager-KKDQNAKA.mjs");
1399
+ const { StorageLocationManager: StorageLocationManager2 } = await import("./storage-location-manager-HFNB3PCS.mjs");
2146
1400
  const manager = new StorageLocationManager2(dataPath);
2147
1401
  await manager.initializeDefaults();
2148
1402
  this.locationManager = manager;
@@ -2161,7 +1415,8 @@ var StorageManager = class {
2161
1415
  if (this.legacyProvider) {
2162
1416
  try {
2163
1417
  const LEGACY_MAP = { config: "data", events: "data", addon: "data" };
2164
- const mappedName = LEGACY_MAP[name] ?? name;
1418
+ const mapped = name in LEGACY_MAP ? LEGACY_MAP[name] : name;
1419
+ const mappedName = mapped;
2165
1420
  const location2 = this.legacyProvider.getLocation(mappedName);
2166
1421
  return namespace ? this.createNamespacedLocation(location2, namespace) : location2;
2167
1422
  } catch {
@@ -2172,29 +1427,29 @@ var StorageManager = class {
2172
1427
  const backend = this.settingsBackend;
2173
1428
  location.structured = {
2174
1429
  async query(collection, filter) {
2175
- const results = await backend.query(collection, filter);
1430
+ const results = await backend.query({ collection, filter });
2176
1431
  return results.map((r) => ({ collection, id: r.id, data: r.data }));
2177
1432
  },
2178
1433
  async insert(record) {
2179
1434
  const id = record.id || (await import("crypto")).randomUUID();
2180
- await backend.insert(record.collection, { id, data: record.data });
1435
+ await backend.insert({ collection: record.collection, record: { id, data: record.data } });
2181
1436
  return { ...record, id };
2182
1437
  },
2183
1438
  async update(collection, id, data) {
2184
- await backend.update(collection, id, data);
1439
+ await backend.update({ collection, id, data });
2185
1440
  return { collection, id, data };
2186
1441
  },
2187
1442
  async delete(collection, id) {
2188
- await backend.delete(collection, id);
1443
+ await backend.delete({ collection, key: id });
2189
1444
  },
2190
1445
  async count(collection, filter) {
2191
- return backend.count(collection, filter);
1446
+ return backend.count({ collection, filter });
2192
1447
  }
2193
1448
  };
2194
1449
  }
2195
1450
  if (this.locationManager) {
2196
1451
  const LEGACY_MAP = { config: "data", events: "data", addon: "data" };
2197
- const mappedName = LEGACY_MAP[name] ?? name;
1452
+ const mappedName = name in LEGACY_MAP ? LEGACY_MAP[name] : name;
2198
1453
  try {
2199
1454
  const basePath = this.locationManager.getBackend(mappedName).basePath;
2200
1455
  location.files = {
@@ -2290,387 +1545,13 @@ var StorageManager = class {
2290
1545
  }
2291
1546
  };
2292
1547
 
2293
- // src/auth/auth-manager.ts
2294
- import * as jwt from "jsonwebtoken";
2295
- import * as bcrypt from "bcryptjs";
2296
- import * as crypto from "crypto";
2297
- var AuthManager = class {
2298
- constructor(config) {
2299
- this.config = config;
2300
- const configured = this.config.get("auth.jwtSecret");
2301
- if (configured) {
2302
- this.jwtSecret = configured;
2303
- } else {
2304
- const secret = crypto.randomBytes(32).toString("hex");
2305
- this.config.update("auth", { jwtSecret: secret });
2306
- console.log("[AuthManager] Generated JWT secret and saved to config.yaml (auth.jwtSecret)");
2307
- this.jwtSecret = secret;
2308
- }
2309
- }
2310
- jwtSecret;
2311
- scopedTokenManager = null;
2312
- signToken(payload) {
2313
- return jwt.sign({ ...payload }, this.jwtSecret, { expiresIn: "24h" });
2314
- }
2315
- verifyToken(token) {
2316
- return jwt.verify(token, this.jwtSecret);
2317
- }
2318
- async hashPassword(password) {
2319
- return bcrypt.hash(password, 10);
2320
- }
2321
- async comparePassword(password, hash2) {
2322
- return bcrypt.compare(password, hash2);
2323
- }
2324
- generateApiKey() {
2325
- const token = crypto.randomBytes(32).toString("hex");
2326
- const hash2 = crypto.createHash("sha256").update(token).digest("hex");
2327
- const prefix = token.slice(0, 8);
2328
- return { token, hash: hash2, prefix };
2329
- }
2330
- validateApiKey(token, storedHash) {
2331
- const hash2 = crypto.createHash("sha256").update(token).digest("hex");
2332
- return hash2 === storedHash;
2333
- }
2334
- /**
2335
- * Create a service token for agent/worker authentication.
2336
- * Used when forking workers or when agents register.
2337
- */
2338
- createServiceToken(opts) {
2339
- const payload = {
2340
- userId: opts.agentId,
2341
- username: opts.agentId,
2342
- role: opts.role ?? "agent",
2343
- type: "service",
2344
- agentId: opts.agentId,
2345
- allowedProviders: "*",
2346
- allowedDevices: {}
2347
- };
2348
- const expiresIn = opts.expiresIn ?? "24h";
2349
- return jwt.sign(payload, this.jwtSecret, { expiresIn });
2350
- }
2351
- /**
2352
- * Set the scoped token manager for the auth chain.
2353
- */
2354
- setScopedTokenManager(manager) {
2355
- this.scopedTokenManager = manager;
2356
- }
2357
- /**
2358
- * Validate a scoped token string.
2359
- * Returns the token record if valid, null otherwise.
2360
- */
2361
- async validateScopedToken(rawToken) {
2362
- if (!this.scopedTokenManager) {
2363
- return null;
2364
- }
2365
- return this.scopedTokenManager.validate(rawToken);
2366
- }
2367
- /**
2368
- * Check whether a scoped token grants access to a given addon/route/capability.
2369
- */
2370
- matchesScopedTokenScope(token, addonId, routePath, capability) {
2371
- if (!this.scopedTokenManager) {
2372
- return false;
2373
- }
2374
- return this.scopedTokenManager.matchesScope(token, addonId, routePath, capability);
2375
- }
2376
- };
2377
-
2378
- // src/auth/api-key-manager.ts
2379
- import * as crypto2 from "crypto";
2380
- var API_KEYS_COLLECTION = "api_keys";
2381
- var ApiKeyManager = class {
2382
- constructor(storageAccess, auth) {
2383
- this.storageAccess = storageAccess;
2384
- this.auth = auth;
2385
- }
2386
- get structured() {
2387
- return this.storageAccess.getStructuredStorage();
2388
- }
2389
- async create(input) {
2390
- const { token, hash: hash2, prefix } = this.auth.generateApiKey();
2391
- const now = Date.now();
2392
- const record = {
2393
- id: crypto2.randomUUID(),
2394
- label: input.label,
2395
- role: input.role,
2396
- allowedProviders: input.allowedProviders ?? "*",
2397
- allowedDevices: input.allowedDevices ?? {},
2398
- tokenHash: hash2,
2399
- tokenPrefix: prefix,
2400
- createdAt: now
2401
- };
2402
- await this.structured.insert({
2403
- collection: API_KEYS_COLLECTION,
2404
- id: record.id,
2405
- data: record
2406
- });
2407
- return { record, token };
2408
- }
2409
- async validateToken(token) {
2410
- const allKeys = await this.structured.query(API_KEYS_COLLECTION);
2411
- for (const entry of allKeys) {
2412
- const record = entry.data;
2413
- if (this.auth.validateApiKey(token, record.tokenHash)) {
2414
- const updatedData = {
2415
- ...record,
2416
- lastUsedAt: Date.now()
2417
- };
2418
- await this.structured.update(API_KEYS_COLLECTION, record.id, updatedData);
2419
- return { ...record, lastUsedAt: updatedData.lastUsedAt };
2420
- }
2421
- }
2422
- return null;
2423
- }
2424
- async listAll() {
2425
- const results = await this.structured.query(API_KEYS_COLLECTION);
2426
- return results.map((r) => {
2427
- const { tokenHash, ...rest } = r.data;
2428
- return rest;
2429
- });
2430
- }
2431
- async revoke(id) {
2432
- await this.structured.delete(API_KEYS_COLLECTION, id);
2433
- }
2434
- async findById(id) {
2435
- const results = await this.structured.query(API_KEYS_COLLECTION, {
2436
- where: { id }
2437
- });
2438
- if (results.length === 0) {
2439
- return null;
2440
- }
2441
- return results[0].data;
2442
- }
2443
- };
2444
-
2445
- // src/auth/user-manager.ts
2446
- import * as crypto3 from "crypto";
2447
- var USERS_COLLECTION = "users";
2448
- var UserManager = class {
2449
- constructor(storageAccess, auth, config) {
2450
- this.storageAccess = storageAccess;
2451
- this.auth = auth;
2452
- this.config = config;
2453
- }
2454
- get structured() {
2455
- return this.storageAccess.getStructuredStorage();
2456
- }
2457
- async create(input) {
2458
- const existing = await this.findByUsername(input.username);
2459
- if (existing) {
2460
- throw new Error(`User with username "${input.username}" already exists`);
2461
- }
2462
- const passwordHash = await this.auth.hashPassword(input.password);
2463
- const now = Date.now();
2464
- const record = {
2465
- id: crypto3.randomUUID(),
2466
- username: input.username,
2467
- passwordHash,
2468
- role: input.role,
2469
- allowedProviders: input.allowedProviders ?? "*",
2470
- allowedDevices: input.allowedDevices ?? {},
2471
- createdAt: now,
2472
- updatedAt: now
2473
- };
2474
- await this.structured.insert({
2475
- collection: USERS_COLLECTION,
2476
- id: record.id,
2477
- data: record
2478
- });
2479
- return record;
2480
- }
2481
- async findByUsername(username) {
2482
- const results = await this.structured.query(USERS_COLLECTION, {
2483
- where: { username }
2484
- });
2485
- if (results.length === 0) {
2486
- return null;
2487
- }
2488
- return results[0].data;
2489
- }
2490
- async findById(id) {
2491
- const results = await this.structured.query(USERS_COLLECTION, {
2492
- where: { id }
2493
- });
2494
- if (results.length === 0) {
2495
- return null;
2496
- }
2497
- return results[0].data;
2498
- }
2499
- async validateCredentials(username, password) {
2500
- const user = await this.findByUsername(username);
2501
- if (!user) {
2502
- return null;
2503
- }
2504
- const valid = await this.auth.comparePassword(password, user.passwordHash);
2505
- return valid ? user : null;
2506
- }
2507
- async listAll() {
2508
- const results = await this.structured.query(USERS_COLLECTION);
2509
- return results.map((r) => {
2510
- const { passwordHash, ...rest } = r.data;
2511
- return rest;
2512
- });
2513
- }
2514
- async update(id, data) {
2515
- const existing = await this.findById(id);
2516
- if (!existing) {
2517
- throw new Error(`User with id "${id}" not found`);
2518
- }
2519
- const updatedData = {
2520
- ...existing,
2521
- ...data,
2522
- updatedAt: Date.now()
2523
- };
2524
- await this.structured.update(USERS_COLLECTION, id, updatedData);
2525
- }
2526
- async delete(id) {
2527
- await this.structured.delete(USERS_COLLECTION, id);
2528
- }
2529
- async resetPassword(id, newPassword) {
2530
- const existing = await this.findById(id);
2531
- if (!existing) {
2532
- throw new Error(`User with id "${id}" not found`);
2533
- }
2534
- const passwordHash = await this.auth.hashPassword(newPassword);
2535
- const updatedData = {
2536
- ...existing,
2537
- passwordHash,
2538
- updatedAt: Date.now()
2539
- };
2540
- await this.structured.update(USERS_COLLECTION, id, updatedData);
2541
- }
2542
- async ensureAdminExists() {
2543
- const adminUsername = this.config.get("auth.adminUsername");
2544
- const adminPassword = this.config.get("auth.adminPassword");
2545
- if (!adminUsername || !adminPassword) {
2546
- return;
2547
- }
2548
- const existing = await this.findByUsername(adminUsername);
2549
- if (existing) {
2550
- return;
2551
- }
2552
- await this.create({
2553
- username: adminUsername,
2554
- password: adminPassword,
2555
- role: "super_admin",
2556
- allowedProviders: "*",
2557
- allowedDevices: {}
2558
- });
2559
- }
2560
- };
2561
-
2562
- // src/auth/scoped-token-manager.ts
2563
- import * as crypto4 from "crypto";
2564
- var TOKENS_COLLECTION = "scoped_tokens";
2565
- var TOKEN_PREFIX = "cst_";
2566
- var ScopedTokenManager = class {
2567
- constructor(storage) {
2568
- this.storage = storage;
2569
- }
2570
- /**
2571
- * Create a new scoped token. Returns the raw token string (shown once)
2572
- * and the stored record (with hash, not the raw token).
2573
- */
2574
- async create(userId, name, scopes, expiresAt) {
2575
- const rawHex = crypto4.randomBytes(32).toString("hex");
2576
- const rawToken = `${TOKEN_PREFIX}${rawHex}`;
2577
- const tokenHash = crypto4.createHash("sha256").update(rawToken).digest("hex");
2578
- const tokenPrefix = rawToken.slice(0, 12);
2579
- const record = {
2580
- id: crypto4.randomUUID(),
2581
- userId,
2582
- name,
2583
- tokenHash,
2584
- tokenPrefix,
2585
- scopes: scopes.map((s) => ({ ...s })),
2586
- expiresAt,
2587
- lastUsedAt: void 0,
2588
- createdAt: Date.now()
2589
- };
2590
- await this.storage.insert({
2591
- collection: TOKENS_COLLECTION,
2592
- id: record.id,
2593
- data: record
2594
- });
2595
- return { token: rawToken, record };
2596
- }
2597
- /**
2598
- * Validate a raw token string. Returns the token record if valid, null otherwise.
2599
- */
2600
- async validate(rawToken) {
2601
- if (!rawToken.startsWith(TOKEN_PREFIX)) {
2602
- return null;
2603
- }
2604
- const tokenHash = crypto4.createHash("sha256").update(rawToken).digest("hex");
2605
- const results = await this.storage.query(TOKENS_COLLECTION, {
2606
- where: { tokenHash }
2607
- });
2608
- if (results.length === 0) {
2609
- return null;
2610
- }
2611
- const record = results[0].data;
2612
- if (record.expiresAt !== void 0 && record.expiresAt !== null && Date.now() > record.expiresAt) {
2613
- return null;
2614
- }
2615
- this.updateLastUsed(record.id).catch(() => {
2616
- });
2617
- return record;
2618
- }
2619
- /**
2620
- * Check whether a token's scopes grant access to the given addon, route, or capability.
2621
- */
2622
- matchesScope(token, addonId, routePath, capability) {
2623
- for (const scope of token.scopes) {
2624
- switch (scope.type) {
2625
- case "addon":
2626
- if (addonId && scope.target === addonId) return true;
2627
- break;
2628
- case "route-prefix":
2629
- if (routePath && routePath.startsWith(scope.target)) return true;
2630
- break;
2631
- case "capability":
2632
- if (capability && scope.target === capability) return true;
2633
- break;
2634
- }
2635
- }
2636
- return false;
2637
- }
2638
- /**
2639
- * Revoke a token by ID.
2640
- */
2641
- async revoke(tokenId) {
2642
- await this.storage.delete(TOKENS_COLLECTION, tokenId);
2643
- }
2644
- /**
2645
- * List all tokens for a user (without exposing the raw token).
2646
- */
2647
- async listForUser(userId) {
2648
- const results = await this.storage.query(TOKENS_COLLECTION, {
2649
- where: { userId }
2650
- });
2651
- return results.map((r) => r.data);
2652
- }
2653
- /**
2654
- * Update the lastUsedAt timestamp for a token.
2655
- */
2656
- async updateLastUsed(tokenId) {
2657
- const results = await this.storage.query(TOKENS_COLLECTION, {
2658
- where: { id: tokenId }
2659
- });
2660
- if (results.length === 0) return;
2661
- const existing = results[0].data;
2662
- await this.storage.update(TOKENS_COLLECTION, tokenId, {
2663
- ...existing,
2664
- lastUsedAt: Date.now()
2665
- });
2666
- }
2667
- };
2668
-
2669
1548
  // src/notification/notification-service.ts
1549
+ import { errMsg as errMsg2 } from "@camstack/types";
2670
1550
  var NotificationService = class {
2671
1551
  constructor(logger) {
2672
1552
  this.logger = logger;
2673
1553
  }
1554
+ logger;
2674
1555
  localOutputs = /* @__PURE__ */ new Map();
2675
1556
  routing = /* @__PURE__ */ new Map();
2676
1557
  rateLimits = /* @__PURE__ */ new Map();
@@ -2692,15 +1573,15 @@ var NotificationService = class {
2692
1573
  }
2693
1574
  return this.localOutputs;
2694
1575
  }
2695
- /** @deprecated Use registry-based resolution. Kept for backward compat only. */
2696
- addOutput(output) {
1576
+ /** Register an output in the local fallback map (used when no registry is set). */
1577
+ registerLocalOutput(output) {
2697
1578
  this.localOutputs.set(output.id, output);
2698
- this.logger.info(`Notification output added: ${output.name} (${output.id})`);
1579
+ this.logger.info("Notification output added", { meta: { name: output.name, outputId: output.id } });
2699
1580
  }
2700
- /** @deprecated Use registry-based resolution. Kept for backward compat only. */
2701
- removeOutput(id) {
1581
+ /** Remove an output from the local fallback map. */
1582
+ unregisterLocalOutput(id) {
2702
1583
  this.localOutputs.delete(id);
2703
- this.logger.info(`Notification output removed: ${id}`);
1584
+ this.logger.info("Notification output removed", { meta: { outputId: id } });
2704
1585
  }
2705
1586
  setRouting(category, outputIds) {
2706
1587
  this.routing.set(category, [...outputIds]);
@@ -2715,13 +1596,13 @@ var NotificationService = class {
2715
1596
  if (minInterval > 0) {
2716
1597
  const last = this.lastSent.get(rateLimitKey) ?? 0;
2717
1598
  if (notification.timestamp - last < minInterval) {
2718
- this.logger.debug(`Rate limited: ${rateLimitKey}`);
1599
+ this.logger.debug("Rate limited", { meta: { rateLimitKey } });
2719
1600
  return;
2720
1601
  }
2721
1602
  }
2722
1603
  const targetIds = this.routing.get(category) ?? this.routing.get("*") ?? [];
2723
1604
  if (targetIds.length === 0) {
2724
- this.logger.debug(`No routing configured for category "${category}"`);
1605
+ this.logger.debug("No routing configured for category", { meta: { category } });
2725
1606
  return;
2726
1607
  }
2727
1608
  const currentOutputs = this.outputs;
@@ -2731,8 +1612,8 @@ var NotificationService = class {
2731
1612
  try {
2732
1613
  await output.send(notification);
2733
1614
  } catch (err) {
2734
- const msg = err instanceof Error ? err.message : String(err);
2735
- this.logger.error(`Notification output "${output.id}" failed: ${msg}`);
1615
+ const msg = errMsg2(err);
1616
+ this.logger.error("Notification output failed", { meta: { outputId: output.id, error: msg } });
2736
1617
  }
2737
1618
  })
2738
1619
  );
@@ -2865,142 +1746,19 @@ function matchPath(pattern, path5) {
2865
1746
  return params;
2866
1747
  }
2867
1748
 
2868
- // src/device/device-registry.ts
2869
- import { randomUUID as randomUUID6 } from "crypto";
2870
- var DeviceRegistry = class {
2871
- constructor(eventBus, loggingService) {
2872
- this.eventBus = eventBus;
2873
- this.logger = loggingService.createLogger("device-registry");
2874
- }
2875
- devices = /* @__PURE__ */ new Map();
2876
- logger;
2877
- registerDevice(device) {
2878
- this.devices.set(device.id, device);
2879
- this.logger.info(`Device registered: ${device.id} (${device.name})`);
2880
- this.eventBus.emit({
2881
- id: randomUUID6(),
2882
- timestamp: /* @__PURE__ */ new Date(),
2883
- source: { type: "core", id: "device-registry" },
2884
- category: "device.registered",
2885
- data: { deviceId: device.id, name: device.name, providerId: device.providerId }
2886
- });
2887
- }
2888
- unregisterDevice(id) {
2889
- const device = this.devices.get(id);
2890
- if (!device) {
2891
- return;
2892
- }
2893
- this.devices.delete(id);
2894
- this.logger.info(`Device unregistered: ${id}`);
2895
- this.eventBus.emit({
2896
- id: randomUUID6(),
2897
- timestamp: /* @__PURE__ */ new Date(),
2898
- source: { type: "core", id: "device-registry" },
2899
- category: "device.unregistered",
2900
- data: { deviceId: id }
2901
- });
2902
- }
2903
- getDevice(id) {
2904
- return this.devices.get(id) ?? null;
2905
- }
2906
- listDevices() {
2907
- return Array.from(this.devices.values());
2908
- }
2909
- getDevicesByProvider(providerId) {
2910
- return Array.from(this.devices.values()).filter(
2911
- (device) => device.providerId === providerId
2912
- );
2913
- }
2914
- getDevicesWithCapability(cap) {
2915
- return Array.from(this.devices.values()).filter(
2916
- (device) => device.capabilities.includes(cap)
2917
- );
2918
- }
2919
- registerProviderDevices(providerId, devices) {
2920
- for (const device of devices) {
2921
- this.registerDevice(device);
2922
- }
2923
- this.logger.info(`Bulk registered ${devices.length} devices for provider ${providerId}`);
2924
- }
2925
- unregisterProviderDevices(providerId) {
2926
- const providerDevices = this.getDevicesByProvider(providerId);
2927
- for (const device of providerDevices) {
2928
- this.unregisterDevice(device.id);
2929
- }
2930
- this.logger.info(`Bulk unregistered ${providerDevices.length} devices for provider ${providerId}`);
2931
- }
2932
- };
2933
-
2934
- // src/device/capability-resolver.ts
2935
- var CapabilityResolver = class {
2936
- constructor(addonRegistry) {
2937
- this.addonRegistry = addonRegistry;
2938
- }
2939
- bindings = /* @__PURE__ */ new Map();
2940
- resolve(device, cap) {
2941
- const deviceBindings = this.bindings.get(device.id);
2942
- const binding = deviceBindings?.[cap];
2943
- if (binding) {
2944
- if (binding.source === "disabled") {
2945
- return null;
2946
- }
2947
- if (binding.source === "addon" && binding.addonId) {
2948
- const addon = this.addonRegistry.getAddon(binding.addonId);
2949
- if (addon && typeof addon.getCapabilityForDevice === "function") {
2950
- return addon.getCapabilityForDevice(device, cap, binding.config) ?? null;
2951
- }
2952
- return null;
2953
- }
2954
- }
2955
- return device.getCapability(cap);
2956
- }
2957
- setBinding(deviceId, cap, binding) {
2958
- const existing = this.bindings.get(deviceId) ?? {};
2959
- this.bindings.set(deviceId, { ...existing, [cap]: binding });
2960
- }
2961
- removeBinding(deviceId, cap) {
2962
- const existing = this.bindings.get(deviceId);
2963
- if (!existing) {
2964
- return;
2965
- }
2966
- const updated = { ...existing };
2967
- delete updated[cap];
2968
- this.bindings.set(deviceId, updated);
2969
- }
2970
- getBindings(deviceId) {
2971
- return this.bindings.get(deviceId) ?? {};
2972
- }
2973
- getEffectiveCapabilities(device) {
2974
- const deviceBindings = this.bindings.get(device.id) ?? {};
2975
- const result = [];
2976
- for (const cap of device.capabilities) {
2977
- const binding = deviceBindings[cap];
2978
- if (!binding || binding.source !== "disabled") {
2979
- result.push(cap);
2980
- }
2981
- }
2982
- for (const [cap, binding] of Object.entries(deviceBindings)) {
2983
- if (binding && binding.source === "addon" && !device.capabilities.includes(cap)) {
2984
- result.push(cap);
2985
- }
2986
- }
2987
- return result;
2988
- }
2989
- };
2990
-
2991
1749
  // src/tls/cert-manager.ts
2992
1750
  import { X509Certificate } from "crypto";
2993
1751
  import { execFile as execFile2 } from "child_process";
2994
1752
  import { promisify as promisify2 } from "util";
2995
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync } from "fs";
2996
- import { join as join8 } from "path";
1753
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync } from "fs";
1754
+ import { join as join5 } from "path";
2997
1755
  import * as os from "os";
2998
1756
  var execFileAsync2 = promisify2(execFile2);
2999
1757
  async function ensureTlsCert(dataDir, options) {
3000
- const tlsDir = join8(dataDir, "tls");
3001
- const certPath = join8(tlsDir, "camstack.crt");
3002
- const keyPath = join8(tlsDir, "camstack.key");
3003
- if (existsSync6(certPath) && existsSync6(keyPath)) {
1758
+ const tlsDir = join5(dataDir, "tls");
1759
+ const certPath = join5(tlsDir, "camstack.crt");
1760
+ const keyPath = join5(tlsDir, "camstack.key");
1761
+ if (existsSync4(certPath) && existsSync4(keyPath)) {
3004
1762
  try {
3005
1763
  const certPem = readFileSync(certPath);
3006
1764
  const x509 = new X509Certificate(certPem);
@@ -3011,7 +1769,7 @@ async function ensureTlsCert(dataDir, options) {
3011
1769
  } catch {
3012
1770
  }
3013
1771
  }
3014
- mkdirSync5(tlsDir, { recursive: true });
1772
+ mkdirSync3(tlsDir, { recursive: true });
3015
1773
  const cn = options?.commonName ?? "camstack.local";
3016
1774
  const validDays = options?.validDays ?? 825;
3017
1775
  const sanDns = /* @__PURE__ */ new Set(["localhost", cn, os.hostname()]);
@@ -3069,316 +1827,52 @@ function loadTlsCert(certPath, keyPath) {
3069
1827
  }
3070
1828
 
3071
1829
  // src/addon/addon-api-factory.ts
1830
+ function wrapCallerAsClient(caller) {
1831
+ return new Proxy(/* @__PURE__ */ Object.create(null), {
1832
+ get(_target, prop) {
1833
+ if (typeof prop !== "string") return void 0;
1834
+ if (prop === "query" || prop === "mutate") {
1835
+ if (typeof caller !== "function") {
1836
+ throw new Error(
1837
+ `Cannot call .${prop}() \u2014 this tRPC node is not a procedure`
1838
+ );
1839
+ }
1840
+ const fn = caller;
1841
+ return (input) => fn(input);
1842
+ }
1843
+ if (prop === "subscribe") {
1844
+ return () => {
1845
+ throw new Error(
1846
+ `subscribe() is not supported on direct caller \u2014 use the WSS client for subscriptions`
1847
+ );
1848
+ };
1849
+ }
1850
+ const child = caller?.[prop];
1851
+ if (child === void 0 || child === null) return void 0;
1852
+ return wrapCallerAsClient(child);
1853
+ }
1854
+ });
1855
+ }
3072
1856
  var AddonApiFactory = class {
3073
- /**
3074
- * Build a WSS URL from host and port.
3075
- */
3076
- buildWssUrl(host, port) {
3077
- return `wss://${host}:${port}/trpc`;
3078
- }
3079
1857
  /**
3080
1858
  * Create a direct caller -- calls tRPC procedures directly in-process.
3081
1859
  * Zero network overhead. Used for in-process addons and dev mode.
3082
1860
  *
3083
- * @param options.router - The tRPC router from buildAppRouter()
3084
- * @param options.context - The auth context (service account)
3085
- * @returns A tRPC caller that can be used as context.api
1861
+ * Returns `AddonApi` the same tRPC client surface that broker-routed
1862
+ * callers get. Callers treat it uniformly as `context.api` regardless
1863
+ * of the underlying transport.
3086
1864
  */
3087
1865
  async createDirectCaller(options) {
3088
1866
  const { initTRPC } = await import("@trpc/server");
3089
1867
  const t = initTRPC.create();
3090
1868
  const callerFactory = t.createCallerFactory(options.router);
3091
- return callerFactory(options.context);
3092
- }
3093
- /**
3094
- * Create a WSS tRPC client -- connects to the server via WebSocket.
3095
- * Used for forked workers and remote agents.
3096
- *
3097
- * @param options.url - WSS URL (e.g., wss://localhost:4443/trpc)
3098
- * @param options.token - Bearer token for authentication
3099
- * @returns A tRPC client that can be used as context.api
3100
- */
3101
- async createWssClient(options) {
3102
- const { createTRPCClient, createWSClient, wsLink } = await import("./dist-3BY63UQ5.mjs");
3103
- const wsClient = createWSClient({
3104
- url: options.url,
3105
- connectionParams: () => ({ token: options.token })
3106
- });
3107
- const client = createTRPCClient({
3108
- links: [wsLink({ client: wsClient })]
3109
- });
3110
- return {
3111
- client,
3112
- close: () => wsClient.close()
3113
- };
3114
- }
3115
- };
3116
-
3117
- // src/platform/platform-scorer.ts
3118
- import * as os2 from "os";
3119
- import { execFileSync as execFileSync3 } from "child_process";
3120
- async function getAvailableRAM_MB() {
3121
- try {
3122
- const si = await import("systeminformation");
3123
- const mem = await si.mem();
3124
- return Math.round(mem.available / 1024 / 1024);
3125
- } catch {
3126
- const platform2 = os2.platform();
3127
- try {
3128
- if (platform2 === "darwin") {
3129
- const output = execFileSync3("vm_stat", { encoding: "utf8", timeout: 3e3 });
3130
- const pageSize = parseInt(output.match(/page size of (\d+)/)?.[1] ?? "16384");
3131
- const free = parseInt(output.match(/Pages free:\s+(\d+)/)?.[1] ?? "0");
3132
- const inactive = parseInt(output.match(/Pages inactive:\s+(\d+)/)?.[1] ?? "0");
3133
- const purgeable = parseInt(output.match(/Pages purgeable:\s+(\d+)/)?.[1] ?? "0");
3134
- return Math.round((free + inactive + purgeable) * pageSize / 1024 / 1024);
3135
- } else if (platform2 === "linux") {
3136
- const { readFileSync: readFileSync2 } = await import("fs");
3137
- const meminfo = readFileSync2("/proc/meminfo", "utf8");
3138
- const match = meminfo.match(/MemAvailable:\s+(\d+)\s+kB/);
3139
- if (match) return Math.round(parseInt(match[1]) / 1024);
3140
- }
3141
- } catch {
3142
- }
3143
- return Math.round(os2.totalmem() / 1024 / 1024);
3144
- }
3145
- }
3146
- var PlatformScorer = class {
3147
- cached = null;
3148
- /** Probe hardware + runtimes and score all backend combos. Cached after first call. */
3149
- async probe() {
3150
- if (this.cached) return this.cached;
3151
- const start = Date.now();
3152
- console.log("[PlatformScorer] Probing hardware...");
3153
- const hardware = await this.probeHardware();
3154
- console.log(`[PlatformScorer] Hardware: ${hardware.platform}/${hardware.arch}, ${hardware.cpuModel} (${hardware.cpuCores} cores), RAM ${Math.round(hardware.totalRAM_MB / 1024)}GB`);
3155
- if (hardware.gpu) console.log(`[PlatformScorer] GPU: ${hardware.gpu.name}`);
3156
- if (hardware.npu) console.log(`[PlatformScorer] NPU: ${hardware.npu.type}`);
3157
- console.log("[PlatformScorer] Probing Node.js backends...");
3158
- const nodeBackends = await this.probeNodeBackends();
3159
- console.log(`[PlatformScorer] Node backends: ${nodeBackends.map((b) => b.id).join(", ")}`);
3160
- console.log("[PlatformScorer] Probing Python backends...");
3161
- const pythonInfo = this.probePythonBackends();
3162
- if (pythonInfo.pythonPath) {
3163
- console.log(`[PlatformScorer] Python: ${pythonInfo.pythonPath} \u2192 ${pythonInfo.backends.filter((b) => b.available).map((b) => b.id).join(", ") || "no backends"}`);
3164
- } else {
3165
- console.log("[PlatformScorer] Python: not found");
3166
- }
3167
- const scores = this.scoreBackends(hardware, nodeBackends, pythonInfo.backends);
3168
- const bestScore = scores.find((s) => s.available) ?? scores[scores.length - 1];
3169
- const elapsed = Date.now() - start;
3170
- console.log(`[PlatformScorer] Scoring complete in ${elapsed}ms \u2014 ${scores.length} combos`);
3171
- console.log(`[PlatformScorer] Best: ${bestScore.runtime}/${bestScore.backend} (${bestScore.format}) \u2014 ${bestScore.reason} [score: ${bestScore.score}]`);
3172
- for (const s of scores) {
3173
- console.log(`[PlatformScorer] ${s.available ? "\u2713" : "\u2717"} ${s.runtime}/${s.backend} (${s.format}) \u2014 score ${s.score} \u2014 ${s.reason}`);
3174
- }
3175
- this.cached = {
3176
- hardware,
3177
- scores,
3178
- bestScore,
3179
- pythonPath: pythonInfo.pythonPath
3180
- };
3181
- return this.cached;
3182
- }
3183
- async probeHardware() {
3184
- const platform2 = os2.platform();
3185
- const arch2 = os2.arch();
3186
- const cpus2 = os2.cpus();
3187
- const cpuModel = cpus2[0]?.model ?? "unknown";
3188
- const cpuCores = cpus2.length;
3189
- const totalRAM_MB = Math.round(os2.totalmem() / 1024 / 1024);
3190
- const availableRAM_MB = await getAvailableRAM_MB();
3191
- console.log(`[PlatformScorer] RAM: total=${totalRAM_MB}MB, available=${availableRAM_MB}MB (via systeminformation)`);
3192
- let gpu = null;
3193
- let npu = null;
3194
- if (platform2 === "darwin" && arch2 === "arm64") {
3195
- gpu = { type: "apple", name: "Apple Silicon GPU" };
3196
- npu = { type: "apple-ane" };
3197
- }
3198
- if (platform2 === "linux") {
3199
- try {
3200
- const output = execFileSync3("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader"], {
3201
- encoding: "utf8",
3202
- timeout: 5e3,
3203
- stdio: ["pipe", "pipe", "pipe"]
3204
- }).trim();
3205
- if (output) {
3206
- const [name, mem] = output.split(",").map((s) => s.trim());
3207
- gpu = {
3208
- type: "nvidia",
3209
- name: name ?? "NVIDIA GPU",
3210
- memoryMB: parseInt(mem ?? "0")
3211
- };
3212
- }
3213
- } catch {
3214
- }
3215
- }
3216
- return { platform: platform2, arch: arch2, cpuModel, cpuCores, totalRAM_MB, availableRAM_MB, gpu, npu };
3217
- }
3218
- async probeNodeBackends() {
3219
- const backends = [
3220
- { id: "cpu", available: true }
3221
- ];
3222
- try {
3223
- const ort = await import("onnxruntime-node");
3224
- const providers = ort.InferenceSession?.getAvailableProviders?.() ?? [];
3225
- for (const p of providers) {
3226
- const n = p.toLowerCase().replace("executionprovider", "");
3227
- if (n === "coreml") backends.push({ id: "coreml", available: true });
3228
- else if (n === "cuda") backends.push({ id: "cuda", available: true });
3229
- else if (n === "tensorrt") backends.push({ id: "tensorrt", available: true });
3230
- }
3231
- } catch {
3232
- }
3233
- if (os2.platform() === "darwin" && !backends.some((b) => b.id === "coreml")) {
3234
- backends.push({ id: "coreml", available: true });
3235
- }
3236
- return backends;
3237
- }
3238
- probePythonBackends() {
3239
- let pythonPath = null;
3240
- for (const cmd of ["python3", "python"]) {
3241
- try {
3242
- execFileSync3(cmd, ["--version"], { timeout: 5e3, stdio: "pipe" });
3243
- pythonPath = cmd;
3244
- break;
3245
- } catch {
3246
- }
3247
- }
3248
- if (!pythonPath) return { pythonPath: null, backends: [] };
3249
- const checks = [
3250
- // [pythonModule, backendId, modelFormat]
3251
- ["coremltools", "coreml", "coreml"],
3252
- ["openvino.runtime", "openvino", "openvino"],
3253
- ["torch", "pytorch", "onnx"],
3254
- ["onnxruntime", "onnx-py", "onnx"]
3255
- ];
3256
- const backends = [];
3257
- for (const [mod, id, format] of checks) {
3258
- let available = false;
3259
- const probeStart = Date.now();
3260
- try {
3261
- execFileSync3(pythonPath, ["-c", `import ${mod}`], { timeout: 3e4, stdio: "ignore" });
3262
- available = true;
3263
- } catch {
3264
- }
3265
- const probeMs = Date.now() - probeStart;
3266
- console.log(`[PlatformScorer] Python ${mod}: ${available ? "\u2713" : "\u2717"} (${probeMs}ms)`);
3267
- if (id === "coreml" && os2.platform() === "darwin") {
3268
- backends.push({ id, format, available });
3269
- } else if (available) {
3270
- backends.push({ id, format, available });
3271
- }
3272
- }
3273
- return { pythonPath, backends };
3274
- }
3275
- scoreBackends(hardware, nodeBackends, pythonBackends) {
3276
- const scores = [];
3277
- if (hardware.platform === "darwin" && hardware.arch === "arm64") {
3278
- const pyCoreMl = pythonBackends.find((b) => b.id === "coreml");
3279
- if (pyCoreMl) {
3280
- scores.push({ runtime: "python", backend: "coreml", format: "coreml", score: 95, reason: "Apple Neural Engine (Python CoreML)", available: pyCoreMl.available });
3281
- }
3282
- const nodeCoreMl = nodeBackends.find((b) => b.id === "coreml");
3283
- if (nodeCoreMl) {
3284
- scores.push({ runtime: "node", backend: "coreml", format: "onnx", score: 90, reason: "CoreML via ONNX Runtime", available: nodeCoreMl.available });
3285
- }
3286
- }
3287
- if (hardware.gpu?.type === "nvidia") {
3288
- const tensorrt = nodeBackends.find((b) => b.id === "tensorrt");
3289
- if (tensorrt) scores.push({ runtime: "node", backend: "tensorrt", format: "onnx", score: 95, reason: "NVIDIA TensorRT", available: true });
3290
- const cuda = nodeBackends.find((b) => b.id === "cuda");
3291
- if (cuda) scores.push({ runtime: "node", backend: "cuda", format: "onnx", score: 85, reason: "NVIDIA CUDA", available: true });
3292
- }
3293
- const openvino = pythonBackends.find((b) => b.id === "openvino");
3294
- if (openvino) {
3295
- const score = hardware.npu?.type === "intel-npu" ? 90 : 80;
3296
- scores.push({ runtime: "python", backend: "openvino", format: "openvino", score, reason: "Intel OpenVINO", available: openvino.available });
3297
- }
3298
- scores.push({ runtime: "node", backend: "cpu", format: "onnx", score: 50, reason: "CPU (ONNX Runtime Node)", available: true });
3299
- const pyOnnx = pythonBackends.find((b) => b.id === "onnx-py");
3300
- if (pyOnnx) {
3301
- scores.push({ runtime: "python", backend: "cpu", format: "onnx", score: 45, reason: "CPU (Python ONNX)", available: pyOnnx.available });
3302
- }
3303
- return scores.sort((a, b) => b.score - a.score);
3304
- }
3305
- };
3306
-
3307
- // src/platform/inference-config-resolver.ts
3308
- var InferenceConfigResolver = class {
3309
- constructor(scores, hardware) {
3310
- this.scores = scores;
3311
- this.hardware = hardware;
3312
- }
3313
- /**
3314
- * Compute accuracy/backend weights based on available system RAM.
3315
- * availableRAM_MB is now sourced from systeminformation (reliable cross-platform),
3316
- * not os.freemem() which is broken on macOS.
3317
- *
3318
- * - > 16 GB available: prefer larger, more accurate models (accuracy 0.6, backend 0.4)
3319
- * - > 8 GB available: balanced (accuracy 0.5, backend 0.5)
3320
- * - <= 8 GB available: prefer speed (accuracy 0.4, backend 0.6)
3321
- */
3322
- getWeights() {
3323
- const ramMB = this.hardware.availableRAM_MB;
3324
- if (ramMB > 16384) return { accuracyWeight: 0.6, backendWeight: 0.4 };
3325
- if (ramMB > 8192) return { accuracyWeight: 0.5, backendWeight: 0.5 };
3326
- return { accuracyWeight: 0.4, backendWeight: 0.6 };
3327
- }
3328
- /**
3329
- * Given an addon's model requirements, pick the best model + runtime + backend.
3330
- *
3331
- * Algorithm:
3332
- * 1. Filter models by available RAM (minRAM_MB < 25% of available RAM)
3333
- * 2. For each remaining model, find the best platform score whose format
3334
- * is available in the model's formats
3335
- * 3. Pick the model with the highest combined score using RAM-adaptive weights:
3336
- * - High RAM (>16 GB): accuracy × 0.6 + backend × 0.4 (prefer accuracy)
3337
- * - Mid RAM (>8 GB): accuracy × 0.5 + backend × 0.5 (balanced)
3338
- * - Low RAM (<=8 GB): accuracy × 0.4 + backend × 0.6 (prefer speed)
3339
- */
3340
- resolve(requirements) {
3341
- if (requirements.length === 0) {
3342
- return { modelId: "", runtime: "node", backend: "cpu", format: "onnx", reason: "No models declared" };
3343
- }
3344
- const ramBudget = this.hardware.availableRAM_MB * 0.25;
3345
- const { accuracyWeight, backendWeight } = this.getWeights();
3346
- console.log(`[InferenceConfigResolver] availableRAM: ${this.hardware.availableRAM_MB}MB, budget: ${Math.round(ramBudget)}MB, weights: accuracy=${accuracyWeight}, backend=${backendWeight}`);
3347
- const fits = requirements.filter((m) => m.minRAM_MB < ramBudget);
3348
- const candidates = fits.length > 0 ? fits : [requirements[0]];
3349
- console.log(`[InferenceConfigResolver] ${candidates.length}/${requirements.length} models fit RAM budget`);
3350
- let bestCombo = null;
3351
- for (const model of candidates) {
3352
- for (const score of this.scores) {
3353
- if (!score.available) continue;
3354
- if (!model.formats.includes(score.format)) continue;
3355
- const combined = model.accuracyScore * accuracyWeight + score.score * backendWeight;
3356
- if (!bestCombo || combined > bestCombo.combined) {
3357
- console.log(`[InferenceConfigResolver] New best: ${model.modelId} (accuracy=${model.accuracyScore}) + ${score.backend}/${score.format} (score=${score.score}) \u2192 combined=${Math.round(combined)}`);
3358
- bestCombo = { model, score, combined };
3359
- }
3360
- }
3361
- }
3362
- if (!bestCombo) {
3363
- return {
3364
- modelId: candidates[0].modelId,
3365
- runtime: "node",
3366
- backend: "cpu",
3367
- format: "onnx",
3368
- reason: "No compatible backend \u2014 CPU fallback"
3369
- };
3370
- }
3371
- return {
3372
- modelId: bestCombo.model.modelId,
3373
- runtime: bestCombo.score.runtime,
3374
- backend: bestCombo.score.backend,
3375
- format: bestCombo.score.format,
3376
- reason: `${bestCombo.model.name} on ${bestCombo.score.reason} (score: ${Math.round(bestCombo.combined)})`
3377
- };
1869
+ const caller = callerFactory(options.context);
1870
+ return wrapCallerAsClient(caller);
3378
1871
  }
3379
1872
  };
3380
1873
 
3381
1874
  // src/builtins/sqlite-storage/integration-registry.ts
1875
+ import { parseJsonObject } from "@camstack/types";
3382
1876
  function serializeSetting(value) {
3383
1877
  if (typeof value === "number") return { value: String(value), valueType: "number" };
3384
1878
  if (typeof value === "boolean") return { value: String(value), valueType: "boolean" };
@@ -3479,10 +1973,10 @@ var IntegrationRegistry = class {
3479
1973
  }
3480
1974
  }
3481
1975
  // --- Integrations ---
3482
- createIntegration(input) {
1976
+ async createIntegration(input) {
3483
1977
  const id = nextIntegrationId();
3484
1978
  const now = Math.floor(Date.now() / 1e3);
3485
- void this.backend.tableInsert?.("integrations", {
1979
+ await this.backend.tableInsert?.("integrations", {
3486
1980
  id,
3487
1981
  addon_id: input.addonId,
3488
1982
  name: input.name,
@@ -3492,7 +1986,7 @@ var IntegrationRegistry = class {
3492
1986
  updated_at: now
3493
1987
  });
3494
1988
  if (input.settings) {
3495
- this.setIntegrationSettings(id, input.settings);
1989
+ await this.setIntegrationSettings(id, input.settings);
3496
1990
  }
3497
1991
  return {
3498
1992
  id,
@@ -3504,75 +1998,65 @@ var IntegrationRegistry = class {
3504
1998
  updatedAt: now
3505
1999
  };
3506
2000
  }
3507
- getIntegration(id) {
3508
- let result = null;
3509
- void this.backend.tableGet?.("integrations", { id }).then((row) => {
3510
- if (row) result = this.mapIntegration(row);
3511
- });
3512
- return result;
2001
+ async getIntegration(id) {
2002
+ const row = await this.backend.tableGet?.("integrations", { id });
2003
+ return row ? this.mapIntegration(row) : null;
3513
2004
  }
3514
- getIntegrationByAddonId(addonId) {
3515
- let result = null;
3516
- void this.backend.tableGet?.("integrations", { addon_id: addonId }).then((row) => {
3517
- if (row) result = this.mapIntegration(row);
3518
- });
3519
- return result;
2005
+ async getIntegrationByAddonId(addonId) {
2006
+ const row = await this.backend.tableGet?.("integrations", { addon_id: addonId });
2007
+ return row ? this.mapIntegration(row) : null;
3520
2008
  }
3521
- listIntegrations() {
3522
- let result = [];
3523
- void this.backend.tableQuery?.("integrations", { orderBy: { field: "created_at", direction: "asc" } }).then((rows) => {
3524
- result = rows.map((r) => this.mapIntegration(r));
3525
- });
3526
- return result;
2009
+ async listIntegrations() {
2010
+ const rows = await this.backend.tableQuery?.("integrations", { orderBy: { field: "created_at", direction: "asc" } }) ?? [];
2011
+ return rows.map((r) => this.mapIntegration(r));
3527
2012
  }
3528
- updateIntegration(id, updates) {
2013
+ async updateIntegration(id, updates) {
3529
2014
  const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
3530
2015
  if (updates.name !== void 0) updateRow["name"] = updates.name;
3531
2016
  if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
3532
2017
  if (updates.info !== void 0) updateRow["info"] = JSON.stringify(updates.info);
3533
- void this.backend.tableUpdate?.("integrations", { id }, updateRow);
2018
+ await this.backend.tableUpdate?.("integrations", { id }, updateRow);
3534
2019
  return this.getIntegration(id);
3535
2020
  }
3536
- deleteIntegration(id) {
3537
- const devices = this.listDevices(id);
2021
+ async deleteIntegration(id) {
2022
+ const devices = await this.listDevices(id);
3538
2023
  for (const d of devices) {
3539
- void this.backend.tableDelete?.("device_settings_kv", { device_id: d.id });
2024
+ await this.backend.tableDelete?.("device_settings_kv", { device_id: d.id });
3540
2025
  }
3541
- void this.backend.tableDelete?.("devices", { integration_id: id });
3542
- void this.backend.tableDelete?.("integration_settings", { integration_id: id });
3543
- void this.backend.tableDelete?.("integrations", { id });
2026
+ await this.backend.tableDelete?.("devices", { integration_id: id });
2027
+ await this.backend.tableDelete?.("integration_settings", { integration_id: id });
2028
+ await this.backend.tableDelete?.("integrations", { id });
3544
2029
  return true;
3545
2030
  }
3546
2031
  // --- Integration Settings ---
3547
- getIntegrationSettings(integrationId) {
2032
+ async getIntegrationSettings(integrationId) {
3548
2033
  const result = {};
3549
- void this.backend.tableQuery?.("integration_settings", { where: { integration_id: integrationId } }).then((rows) => {
3550
- for (const row of rows) {
3551
- result[String(row["key"])] = deserializeSetting(String(row["value"]), String(row["value_type"]));
3552
- }
3553
- });
2034
+ const rows = await this.backend.tableQuery?.("integration_settings", { where: { integration_id: integrationId } }) ?? [];
2035
+ for (const row of rows) {
2036
+ result[String(row["key"])] = deserializeSetting(String(row["value"]), String(row["value_type"]));
2037
+ }
3554
2038
  return result;
3555
2039
  }
3556
- setIntegrationSetting(integrationId, key, value) {
2040
+ async setIntegrationSetting(integrationId, key, value) {
3557
2041
  const s = serializeSetting(value);
3558
- void this.backend.tableDelete?.("integration_settings", { integration_id: integrationId, key });
3559
- void this.backend.tableInsert?.("integration_settings", {
2042
+ await this.backend.tableDelete?.("integration_settings", { integration_id: integrationId, key });
2043
+ await this.backend.tableInsert?.("integration_settings", {
3560
2044
  integration_id: integrationId,
3561
2045
  key,
3562
2046
  value: s.value,
3563
2047
  value_type: s.valueType
3564
2048
  });
3565
2049
  }
3566
- setIntegrationSettings(integrationId, settings) {
2050
+ async setIntegrationSettings(integrationId, settings) {
3567
2051
  for (const [key, value] of Object.entries(settings)) {
3568
- this.setIntegrationSetting(integrationId, key, value);
2052
+ await this.setIntegrationSetting(integrationId, key, value);
3569
2053
  }
3570
2054
  }
3571
2055
  // --- Devices ---
3572
- createDevice(input) {
2056
+ async createDevice(input) {
3573
2057
  const id = nextDeviceId();
3574
2058
  const now = Math.floor(Date.now() / 1e3);
3575
- void this.backend.tableInsert?.("devices", {
2059
+ await this.backend.tableInsert?.("devices", {
3576
2060
  id,
3577
2061
  integration_id: input.integrationId,
3578
2062
  stable_id: input.stableId,
@@ -3584,7 +2068,7 @@ var IntegrationRegistry = class {
3584
2068
  updated_at: now
3585
2069
  });
3586
2070
  if (input.settings) {
3587
- this.setDeviceSettings(id, input.settings);
2071
+ await this.setDeviceSettings(id, input.settings);
3588
2072
  }
3589
2073
  return {
3590
2074
  id,
@@ -3598,71 +2082,58 @@ var IntegrationRegistry = class {
3598
2082
  updatedAt: now
3599
2083
  };
3600
2084
  }
3601
- getDevice(id) {
3602
- let result = null;
3603
- void this.backend.tableGet?.("devices", { id }).then((row) => {
3604
- if (row) result = this.mapDevice(row);
3605
- });
3606
- return result;
2085
+ async getDevice(id) {
2086
+ const row = await this.backend.tableGet?.("devices", { id });
2087
+ return row ? this.mapDevice(row) : null;
3607
2088
  }
3608
- getDeviceByStableId(stableId) {
3609
- let result = null;
3610
- void this.backend.tableGet?.("devices", { stable_id: stableId }).then((row) => {
3611
- if (row) result = this.mapDevice(row);
3612
- });
3613
- return result;
2089
+ async getDeviceByStableId(stableId) {
2090
+ const row = await this.backend.tableGet?.("devices", { stable_id: stableId });
2091
+ return row ? this.mapDevice(row) : null;
3614
2092
  }
3615
- listDevices(integrationId) {
3616
- let result = [];
2093
+ async listDevices(integrationId) {
3617
2094
  const options = integrationId ? { where: { integration_id: integrationId }, orderBy: { field: "created_at", direction: "asc" } } : { orderBy: { field: "created_at", direction: "asc" } };
3618
- void this.backend.tableQuery?.("devices", options).then((rows) => {
3619
- result = rows.map((r) => this.mapDevice(r));
3620
- });
3621
- return result;
2095
+ const rows = await this.backend.tableQuery?.("devices", options) ?? [];
2096
+ return rows.map((r) => this.mapDevice(r));
3622
2097
  }
3623
- listCameras() {
3624
- let result = [];
3625
- void this.backend.tableQuery?.("devices", { where: { type: "camera" }, orderBy: { field: "created_at", direction: "asc" } }).then((rows) => {
3626
- result = rows.map((r) => this.mapDevice(r));
3627
- });
3628
- return result;
2098
+ async listCameras() {
2099
+ const rows = await this.backend.tableQuery?.("devices", { where: { type: "camera" }, orderBy: { field: "created_at", direction: "asc" } }) ?? [];
2100
+ return rows.map((r) => this.mapDevice(r));
3629
2101
  }
3630
- updateDevice(id, updates) {
2102
+ async updateDevice(id, updates) {
3631
2103
  const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
3632
2104
  if (updates.name !== void 0) updateRow["name"] = updates.name;
3633
2105
  if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
3634
2106
  if (updates.info !== void 0) updateRow["info"] = JSON.stringify(updates.info);
3635
- void this.backend.tableUpdate?.("devices", { id }, updateRow);
2107
+ await this.backend.tableUpdate?.("devices", { id }, updateRow);
3636
2108
  return this.getDevice(id);
3637
2109
  }
3638
- deleteDevice(id) {
3639
- void this.backend.tableDelete?.("device_settings_kv", { device_id: id });
3640
- void this.backend.tableDelete?.("devices", { id });
2110
+ async deleteDevice(id) {
2111
+ await this.backend.tableDelete?.("device_settings_kv", { device_id: id });
2112
+ await this.backend.tableDelete?.("devices", { id });
3641
2113
  return true;
3642
2114
  }
3643
2115
  // --- Device Settings ---
3644
- getDeviceSettings(deviceId) {
2116
+ async getDeviceSettings(deviceId) {
3645
2117
  const result = {};
3646
- void this.backend.tableQuery?.("device_settings_kv", { where: { device_id: deviceId } }).then((rows) => {
3647
- for (const row of rows) {
3648
- result[String(row["key"])] = deserializeSetting(String(row["value"]), String(row["value_type"]));
3649
- }
3650
- });
2118
+ const rows = await this.backend.tableQuery?.("device_settings_kv", { where: { device_id: deviceId } }) ?? [];
2119
+ for (const row of rows) {
2120
+ result[String(row["key"])] = deserializeSetting(String(row["value"]), String(row["value_type"]));
2121
+ }
3651
2122
  return result;
3652
2123
  }
3653
- setDeviceSetting(deviceId, key, value) {
2124
+ async setDeviceSetting(deviceId, key, value) {
3654
2125
  const s = serializeSetting(value);
3655
- void this.backend.tableDelete?.("device_settings_kv", { device_id: deviceId, key });
3656
- void this.backend.tableInsert?.("device_settings_kv", {
2126
+ await this.backend.tableDelete?.("device_settings_kv", { device_id: deviceId, key });
2127
+ await this.backend.tableInsert?.("device_settings_kv", {
3657
2128
  device_id: deviceId,
3658
2129
  key,
3659
2130
  value: s.value,
3660
2131
  value_type: s.valueType
3661
2132
  });
3662
2133
  }
3663
- setDeviceSettings(deviceId, settings) {
2134
+ async setDeviceSettings(deviceId, settings) {
3664
2135
  for (const [key, value] of Object.entries(settings)) {
3665
- this.setDeviceSetting(deviceId, key, value);
2136
+ await this.setDeviceSetting(deviceId, key, value);
3666
2137
  }
3667
2138
  }
3668
2139
  // --- Mappers ---
@@ -3672,7 +2143,7 @@ var IntegrationRegistry = class {
3672
2143
  addonId: String(row["addon_id"]),
3673
2144
  name: String(row["name"]),
3674
2145
  enabled: row["enabled"] === 1,
3675
- info: typeof row["info"] === "string" ? JSON.parse(row["info"]) : {},
2146
+ info: typeof row["info"] === "string" ? parseJsonObject(row["info"]) ?? {} : {},
3676
2147
  createdAt: Number(row["created_at"]),
3677
2148
  updatedAt: Number(row["updated_at"])
3678
2149
  };
@@ -3685,214 +2156,85 @@ var IntegrationRegistry = class {
3685
2156
  type: String(row["type"]),
3686
2157
  name: String(row["name"]),
3687
2158
  enabled: row["enabled"] === 1,
3688
- info: typeof row["info"] === "string" ? JSON.parse(row["info"]) : {},
2159
+ info: typeof row["info"] === "string" ? parseJsonObject(row["info"]) ?? {} : {},
3689
2160
  createdAt: Number(row["created_at"]),
3690
2161
  updatedAt: Number(row["updated_at"])
3691
2162
  };
3692
2163
  }
3693
2164
  };
3694
-
3695
- // src/provider/provider-manager.ts
3696
- import { randomUUID as randomUUID7 } from "crypto";
3697
- var ProviderManager = class {
3698
- constructor(deviceRegistry, eventBus, loggingService) {
3699
- this.deviceRegistry = deviceRegistry;
3700
- this.eventBus = eventBus;
3701
- this.loggingService = loggingService;
3702
- this.logger = loggingService.createLogger("provider-manager");
3703
- }
3704
- providers = /* @__PURE__ */ new Map();
3705
- logger;
3706
- registerProvider(provider) {
3707
- const providerLogger = this.loggingService.createLogger(`provider:${provider.id}`);
3708
- const lifecycle = new LifecycleStateMachine(
3709
- provider.id,
3710
- "provider",
3711
- this.eventBus,
3712
- providerLogger
3713
- );
3714
- this.providers.set(provider.id, { provider, lifecycle, started: false });
3715
- this.logger.info(`Provider registered: ${provider.id} (${provider.name})`);
3716
- }
3717
- async startProvider(id) {
3718
- const entry = this.providers.get(id);
3719
- if (!entry) {
3720
- throw new Error(`Provider "${id}" is not registered`);
3721
- }
3722
- if (entry.lifecycle.state === "disabled") {
3723
- throw new Error(`Provider "${id}" is disabled`);
3724
- }
3725
- entry.lifecycle.transition("starting");
3726
- try {
3727
- await entry.provider.start();
3728
- } catch (err) {
3729
- const message = err instanceof Error ? err.message : String(err);
3730
- entry.lifecycle.transition("error", message);
3731
- throw err;
3732
- }
3733
- entry.lifecycle.transition("running");
3734
- const devices = entry.provider.getDevices();
3735
- this.deviceRegistry.registerProviderDevices(id, devices);
3736
- const unsubscribe = entry.provider.subscribeLiveEvents((event) => {
3737
- this.eventBus.emit({
3738
- id: randomUUID7(),
3739
- timestamp: /* @__PURE__ */ new Date(),
3740
- source: { type: "provider", id },
3741
- category: `provider.${event.type}`,
3742
- data: event.data
3743
- });
3744
- });
3745
- entry.started = true;
3746
- entry.unsubscribe = unsubscribe;
3747
- this.eventBus.emit({
3748
- id: randomUUID7(),
3749
- timestamp: /* @__PURE__ */ new Date(),
3750
- source: { type: "core", id: "provider-manager" },
3751
- category: "provider.started",
3752
- data: { providerId: id }
3753
- });
3754
- this.logger.info(`Provider started: ${id}`);
3755
- }
3756
- async stopProvider(id) {
3757
- const entry = this.providers.get(id);
3758
- if (!entry) {
3759
- throw new Error(`Provider "${id}" is not registered`);
3760
- }
3761
- entry.lifecycle.transition("stopping");
3762
- if (entry.unsubscribe) {
3763
- entry.unsubscribe();
3764
- entry.unsubscribe = void 0;
3765
- }
3766
- this.deviceRegistry.unregisterProviderDevices(id);
3767
- await entry.provider.stop();
3768
- entry.started = false;
3769
- entry.lifecycle.transition("stopped");
3770
- this.eventBus.emit({
3771
- id: randomUUID7(),
3772
- timestamp: /* @__PURE__ */ new Date(),
3773
- source: { type: "core", id: "provider-manager" },
3774
- category: "provider.stopped",
3775
- data: { providerId: id }
3776
- });
3777
- this.logger.info(`Provider stopped: ${id}`);
3778
- }
3779
- async disableProvider(id) {
3780
- const entry = this.providers.get(id);
3781
- if (!entry) {
3782
- throw new Error(`Provider "${id}" is not registered`);
3783
- }
3784
- if (entry.lifecycle.state === "running" || entry.started) {
3785
- await this.stopProvider(id);
3786
- }
3787
- entry.lifecycle.transition("disabled");
3788
- this.logger.info(`Provider disabled: ${id}`);
3789
- }
3790
- async enableProvider(id) {
3791
- const entry = this.providers.get(id);
3792
- if (!entry) {
3793
- throw new Error(`Provider "${id}" is not registered`);
3794
- }
3795
- if (entry.lifecycle.state !== "disabled") {
3796
- throw new Error(`Provider "${id}" is not disabled`);
3797
- }
3798
- entry.lifecycle.transition("stopped");
3799
- this.logger.info(`Provider enabled: ${id}`);
3800
- }
3801
- async restartProvider(id) {
3802
- await this.stopProvider(id);
3803
- await this.startProvider(id);
3804
- }
3805
- getProvider(id) {
3806
- const entry = this.providers.get(id);
3807
- return entry?.provider ?? null;
3808
- }
3809
- getProviderStatus(id) {
3810
- const entry = this.providers.get(id);
3811
- return entry?.lifecycle.getStatus() ?? null;
3812
- }
3813
- listProviders() {
3814
- return Array.from(this.providers.values()).map((entry) => ({
3815
- id: entry.provider.id,
3816
- type: entry.provider.type,
3817
- name: entry.provider.name,
3818
- status: entry.provider.getStatus(),
3819
- started: entry.started,
3820
- lifecycle: entry.lifecycle.getStatus()
3821
- }));
3822
- }
3823
- async shutdownAll() {
3824
- const startedIds = Array.from(this.providers.entries()).filter(([, entry]) => entry.started).map(([id]) => id);
3825
- for (const id of startedIds) {
3826
- await this.stopProvider(id);
3827
- }
3828
- this.logger.info(`All providers shut down (${startedIds.length} stopped)`);
3829
- }
3830
- };
3831
2165
  export {
3832
2166
  AddonApiFactory,
3833
2167
  AddonRouteRegistry,
3834
- AgentClient,
3835
- AgentRegistry,
3836
- AgentTaskRunner,
2168
+ AlertCenterAddon,
3837
2169
  ApiKeyManager,
3838
2170
  AuthManager,
3839
2171
  CORE_TABLE_DDL,
3840
- CapabilityResolver,
3841
- DecodeTaskHandler,
3842
- DetectTaskHandler,
3843
- DeviceRegistry,
2172
+ ConfigStore,
2173
+ ConsoleDestination,
2174
+ ConsoleLoggingAddon,
2175
+ DeviceManagerAddon,
2176
+ DeviceStore,
2177
+ EngineManagerResolver,
3844
2178
  EventBus,
3845
2179
  FeatureManager,
3846
- FileSystemStorage,
2180
+ FilesystemStorageAddon,
2181
+ FilesystemStorageProvider,
3847
2182
  FsStorageBackend,
3848
- InferenceConfigResolver,
2183
+ HubForwarderAddon,
2184
+ HubForwarderDestination,
3849
2185
  IntegrationRegistry,
3850
2186
  LifecycleStateMachine,
2187
+ LocalAuthAddon,
3851
2188
  LocalBackupAddon,
3852
2189
  LocalBackupService,
3853
2190
  LogManager,
3854
2191
  LogRingBuffer,
3855
- ManagedProcess,
3856
2192
  ModelDownloadService,
2193
+ NativeMetricsAddon,
2194
+ NativeMetricsProvider,
3857
2195
  NetworkQualityTracker,
3858
2196
  NotificationService,
3859
2197
  PYTHON_VERSION,
3860
2198
  PipelineRunner,
3861
2199
  PipelineValidator,
3862
- PlatformScorer,
3863
- ProcessManager,
3864
- ProviderManager,
3865
2200
  PythonEnvManager,
3866
- RecordTaskHandler,
3867
2201
  ReplEngine,
3868
2202
  ScopedLogger,
3869
2203
  ScopedTokenManager,
3870
2204
  SettingsStore,
3871
- SqliteStorageAddon,
3872
- SqliteStorageProvider,
2205
+ SqliteSettingsAddon,
2206
+ SqliteSettingsBackend,
3873
2207
  StorageLocationManager,
3874
2208
  StorageManager,
2209
+ SystemConfigAddon,
3875
2210
  SystemEventBus,
3876
- TaskDispatcher,
3877
2211
  ToastService,
3878
2212
  UserManager,
3879
2213
  WinstonDestination,
3880
2214
  WinstonLoggingAddon,
3881
2215
  addonTableToDdl,
3882
2216
  buildBinaryPath,
2217
+ deleteModelFromDisk,
3883
2218
  downloadBinary,
3884
2219
  downloadFile,
3885
2220
  downloadModel,
3886
2221
  ensureBinary,
3887
2222
  ensureFfmpeg,
2223
+ ensureModel,
3888
2224
  ensurePython,
3889
2225
  ensureTlsCert,
3890
2226
  fetchJson,
3891
2227
  findInPath,
2228
+ formatLogLine,
3892
2229
  getFfmpegDownloadUrl,
2230
+ getModelFilePath,
2231
+ getPidStats,
3893
2232
  getPlatformInfo,
3894
2233
  getPythonDownloadUrl,
2234
+ getSinglePidStats,
3895
2235
  installPythonPackages,
2236
+ installPythonRequirements,
2237
+ isModelDownloaded,
3896
2238
  loadTlsCert
3897
2239
  };
3898
2240
  //# sourceMappingURL=index.mjs.map