@camstack/addon-ai 0.4.16 → 0.4.17

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 (3) hide show
  1. package/dist/addon.js +298 -30
  2. package/dist/addon.mjs +298 -30
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8226,6 +8226,15 @@ var LabelDefinitionSchema = object({
8226
8226
  description: string().optional(),
8227
8227
  icon: string().optional()
8228
8228
  });
8229
+ var ClassMapDefinitionSchema = object({
8230
+ mapping: record(string(), _enum([
8231
+ "person",
8232
+ "vehicle",
8233
+ "animal",
8234
+ "package"
8235
+ ])),
8236
+ preserveOriginal: boolean()
8237
+ });
8229
8238
  var MODEL_FORMATS = [
8230
8239
  "onnx",
8231
8240
  "coreml",
@@ -8404,7 +8413,13 @@ var ModelCatalogEntrySchema = object({
8404
8413
  * `id` stays the source of truth for resolution/download/persistence; grouping
8405
8414
  * is a presentation overlay resolved back to an `id`.
8406
8415
  */
8407
- group: ModelVariantGroupSchema.optional()
8416
+ group: ModelVariantGroupSchema.optional(),
8417
+ /**
8418
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8419
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8420
+ * labels already ARE the CamStack macros (Scrypted identity map).
8421
+ */
8422
+ classMap: ClassMapDefinitionSchema.optional()
8408
8423
  });
8409
8424
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8410
8425
  format: literal("openvino"),
@@ -8433,7 +8448,8 @@ var ModelConvertMetadataSchema = object({
8433
8448
  "ocr",
8434
8449
  "segmentation"
8435
8450
  ]),
8436
- faceAlignment: boolean().optional()
8451
+ faceAlignment: boolean().optional(),
8452
+ classMap: ClassMapDefinitionSchema.optional()
8437
8453
  });
8438
8454
  var ConvertResultSchema = object({
8439
8455
  entry: ModelCatalogEntrySchema,
@@ -17505,6 +17521,33 @@ var NativeCropRefSchema = object({
17505
17521
  h: number$1()
17506
17522
  })
17507
17523
  });
17524
+ object({
17525
+ crop: object({
17526
+ left: number$1(),
17527
+ top: number$1(),
17528
+ width: number$1().positive(),
17529
+ height: number$1().positive()
17530
+ }).optional(),
17531
+ content: object({
17532
+ width: number$1().int().positive(),
17533
+ height: number$1().int().positive()
17534
+ }),
17535
+ fit: _enum(["stretch", "contain"]),
17536
+ format: _enum([
17537
+ "rgb",
17538
+ "gray",
17539
+ "jpeg"
17540
+ ])
17541
+ });
17542
+ var FrameRefSchema = object({
17543
+ registryId: string().min(1),
17544
+ id: string().min(1),
17545
+ width: number$1().int().positive(),
17546
+ height: number$1().int().positive(),
17547
+ format: _enum(["rgb", "gray"]),
17548
+ timestamp: number$1(),
17549
+ capturedAt: number$1().optional()
17550
+ });
17508
17551
  var ModelFormatSchema$1 = _enum([
17509
17552
  "onnx",
17510
17553
  "coreml",
@@ -17749,6 +17792,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17749
17792
  steps: array(PipelineStepInputSchema).min(1),
17750
17793
  frame: FrameInputSchema.optional(),
17751
17794
  /**
17795
+ * Process-local lazy frame. Valid only when caller and provider resolve
17796
+ * in the same execution-group process; split/cross-node callers use
17797
+ * `frame`/`image` inline compatibility instead.
17798
+ */
17799
+ frameRef: FrameRefSchema.optional(),
17800
+ /**
17752
17801
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17753
17802
  * the decoded pixels live in. One more member of the one-of
17754
17803
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -18004,7 +18053,10 @@ var NativeCropResultSchema = object({
18004
18053
  * Which source served this crop, so a quality-sensitive consumer (the native
18005
18054
  * `keyFrame`) can reject a degraded fallback:
18006
18055
  * - `native` — cut from the decode worker's retained NATIVE surface (the
18007
- * quality path).
18056
+ * quality path). A subject-tile serve is also native-resolution and stays
18057
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18058
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18059
+ * internal crop result (`nativeHits` vs `tileHits`).
18008
18060
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18009
18061
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18010
18062
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18495,12 +18547,41 @@ var RunnerLocalLoadSchema = object({
18495
18547
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18496
18548
  * working unchanged when they switch to reading from the runner cap.
18497
18549
  */
18550
+ var FrameLazyCountersSchema = object({
18551
+ framesDecoded: number$1(),
18552
+ framesAdmitted: number$1(),
18553
+ framesDroppedPixelFree: number$1(),
18554
+ viewsMaterialized: number$1(),
18555
+ viewsSkipped: number$1(),
18556
+ workerToRunnerBytes: number$1(),
18557
+ runnerToPoolRawBytes: number$1(),
18558
+ runnerToPoolJpegBytes: number$1(),
18559
+ onDemandFullFrameRequests: number$1(),
18560
+ onDemandCropRequests: number$1(),
18561
+ nativeHits: number$1(),
18562
+ nativeMisses: number$1(),
18563
+ tileHits: number$1(),
18564
+ tileMisses: number$1(),
18565
+ fallbackHits: number$1(),
18566
+ fallbackMisses: number$1(),
18567
+ retainedWritesAvoided: number$1(),
18568
+ residentRefs: number$1(),
18569
+ residentBytes: number$1(),
18570
+ releases: number$1(),
18571
+ evictions: number$1(),
18572
+ staleMisses: number$1()
18573
+ });
18574
+ var FrameLazyMetricsSchema = object({
18575
+ node: FrameLazyCountersSchema,
18576
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number$1() }))
18577
+ });
18498
18578
  var RunnerLocalMetricsSchema = object({
18499
18579
  nodeId: string(),
18500
18580
  activeCameras: number$1(),
18501
18581
  throttledCameras: number$1(),
18502
18582
  avgInferenceTimeMs: number$1(),
18503
- queueDepth: number$1()
18583
+ queueDepth: number$1(),
18584
+ frameLazy: FrameLazyMetricsSchema.optional()
18504
18585
  });
18505
18586
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number$1() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number$1() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number$1()).readonly()), method(object({
18506
18587
  handle: FrameHandleSchema,
@@ -19800,6 +19881,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19800
19881
  location: StorageLocationSchema,
19801
19882
  relativePath: string()
19802
19883
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number$1().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19884
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19885
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19886
+ var ProfileSettingsBagSchema = record(string(), unknown());
19803
19887
  /**
19804
19888
  * A live terminal session hosted by the provider addon. Output and input do
19805
19889
  * NOT flow through the capability — they use the addon data plane
@@ -19829,7 +19913,14 @@ var TerminalSessionInfoSchema = object({
19829
19913
  var TerminalProfileInfoSchema = object({
19830
19914
  profileId: string(),
19831
19915
  label: string(),
19832
- description: string().optional()
19916
+ description: string().optional(),
19917
+ /** Spawn defaults the instance form copies on create. */
19918
+ executable: string().optional(),
19919
+ args: array(string()).readonly().optional(),
19920
+ cwd: string().optional(),
19921
+ environment: array(string()).readonly().optional(),
19922
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
19923
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19833
19924
  });
19834
19925
  /**
19835
19926
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19842,7 +19933,12 @@ var TerminalInstanceInfoSchema = object({
19842
19933
  profileId: string(),
19843
19934
  profileLabel: string(),
19844
19935
  name: string(),
19845
- enabled: boolean()
19936
+ enabled: boolean(),
19937
+ executable: string(),
19938
+ args: array(string()).readonly(),
19939
+ cwd: string(),
19940
+ environment: array(string()).readonly(),
19941
+ profileSettings: ProfileSettingsBagSchema
19846
19942
  });
19847
19943
  var TerminalLegacyCameraSchema = object({
19848
19944
  stableId: string(),
@@ -19872,7 +19968,23 @@ var TerminalOutputBatchSchema = object({
19872
19968
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19873
19969
  targetNodeId: string().min(1),
19874
19970
  profileId: string().min(1),
19875
- name: string().trim().min(1).max(160).optional()
19971
+ name: string().trim().min(1).max(160).optional(),
19972
+ executable: string().max(1024).optional(),
19973
+ args: array(string().max(2048)).max(64).optional(),
19974
+ cwd: string().max(1024).optional(),
19975
+ environment: array(string().max(4096)).max(64).optional(),
19976
+ profileSettings: ProfileSettingsBagSchema.optional()
19977
+ }), TerminalInstanceInfoSchema, {
19978
+ kind: "mutation",
19979
+ auth: "admin"
19980
+ }), method(object({
19981
+ instanceId: string().min(1),
19982
+ name: string().trim().min(1).max(160).optional(),
19983
+ executable: string().max(1024).optional(),
19984
+ args: array(string().max(2048)).max(64).optional(),
19985
+ cwd: string().max(1024).optional(),
19986
+ environment: array(string().max(4096)).max(64).optional(),
19987
+ profileSettings: ProfileSettingsBagSchema.optional()
19876
19988
  }), TerminalInstanceInfoSchema, {
19877
19989
  kind: "mutation",
19878
19990
  auth: "admin"
@@ -19894,7 +20006,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19894
20006
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19895
20007
  profileId: string(),
19896
20008
  cols: number$1().int().positive(),
19897
- rows: number$1().int().positive()
20009
+ rows: number$1().int().positive(),
20010
+ executable: string().max(1024).optional(),
20011
+ args: array(string().max(2048)).max(64).optional(),
20012
+ cwd: string().max(1024).optional(),
20013
+ environment: array(string().max(4096)).max(64).optional()
19898
20014
  }), TerminalSessionInfoSchema, {
19899
20015
  kind: "mutation",
19900
20016
  auth: "admin"
@@ -22676,10 +22792,10 @@ DeviceType.LawnMower, method(object({ deviceId: number$1().int().nonnegative() }
22676
22792
  *
22677
22793
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22678
22794
  * to receive an ordered list of candidate base URLs it should race
22679
- * on connect — LAN IPv4 first (lowest latency when on same network),
22680
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22681
- * race them with short timeouts and stick with the winner for the
22682
- * session.
22795
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
22796
+ * when on the same network), then public hostname (if a tunnel is
22797
+ * up). The SDK can race them with short timeouts and stick with the
22798
+ * winner for the session.
22683
22799
  *
22684
22800
  * Why hub-only: agents are not directly addressable by the operator's
22685
22801
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22834,6 +22950,17 @@ var NotificationEndpointSchema = object({
22834
22950
  /** What the ranking currently resolves to (null when nothing is reachable). */
22835
22951
  resolved: string().nullable()
22836
22952
  });
22953
+ /**
22954
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
22955
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
22956
+ * currently expands to, so the UI can show the effective set either way.
22957
+ */
22958
+ var ViewerEndpointsSchema = object({
22959
+ /** The operator's explicit race set, or empty for AUTO. */
22960
+ baseUrls: array(string()).readonly(),
22961
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
22962
+ resolved: array(string()).readonly()
22963
+ });
22837
22964
  var AllowedAddressesSchema = object({
22838
22965
  /**
22839
22966
  * Allowlist of interface addresses operators have explicitly opted
@@ -22842,6 +22969,20 @@ var AllowedAddressesSchema = object({
22842
22969
  * Network Addresses admin page and persisted by the addon.
22843
22970
  */
22844
22971
  addresses: array(string()).readonly() });
22972
+ var TlsStatusSchema = object({
22973
+ mode: _enum([
22974
+ "generated",
22975
+ "uploaded",
22976
+ "disabled"
22977
+ ]),
22978
+ leafFingerprintSha256: string().nullable(),
22979
+ caFingerprintSha256: string().nullable(),
22980
+ validTo: string().nullable(),
22981
+ sans: array(string()),
22982
+ caCertPem: string().nullable(),
22983
+ reissueError: string().nullable(),
22984
+ restartRequired: boolean()
22985
+ });
22845
22986
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22846
22987
  /**
22847
22988
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22851,17 +22992,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22851
22992
  */
22852
22993
  port: number$1().int().min(1).max(65535).optional(),
22853
22994
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22854
- * candidate. Default `true`. */
22995
+ * candidate. Default `false` — loopback is not a client route. */
22855
22996
  includeLoopback: boolean().optional(),
22856
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22857
- * Default `false`. */
22997
+ /** Skip IPv6 entries. Default `false` the palette includes stable
22998
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
22999
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22858
23000
  ipv4Only: boolean().optional(),
22859
23001
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22860
23002
  * Pass `'https'` when the caller is itself loaded over HTTPS
22861
23003
  * to avoid mixed-content blocks in the browser. The public
22862
23004
  * tunnel always emits `https://` regardless. */
22863
23005
  scheme: _enum(["http", "https"]).optional()
22864
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23006
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
23007
+ kind: "mutation",
23008
+ auth: "admin"
23009
+ }), method(object({
23010
+ certPem: string().min(1),
23011
+ keyPem: string().min(1),
23012
+ caPem: string().optional()
23013
+ }), TlsStatusSchema, {
23014
+ kind: "mutation",
23015
+ auth: "admin"
23016
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23017
+ kind: "mutation",
23018
+ auth: "admin"
23019
+ });
22865
23020
  object({
22866
23021
  /** Lifecycle state of the lock. `jammed` means the motor reported
22867
23022
  * failure to reach the target — operator intervention required. */
@@ -28624,6 +28779,12 @@ Object.freeze({
28624
28779
  addonId: null,
28625
28780
  access: "create"
28626
28781
  },
28782
+ "localNetwork.downloadCa": {
28783
+ capName: "local-network",
28784
+ capScope: "system",
28785
+ addonId: null,
28786
+ access: "view"
28787
+ },
28627
28788
  "localNetwork.getAllowedAddresses": {
28628
28789
  capName: "local-network",
28629
28790
  capScope: "system",
@@ -28648,18 +28809,42 @@ Object.freeze({
28648
28809
  addonId: null,
28649
28810
  access: "view"
28650
28811
  },
28812
+ "localNetwork.getTlsStatus": {
28813
+ capName: "local-network",
28814
+ capScope: "system",
28815
+ addonId: null,
28816
+ access: "view"
28817
+ },
28818
+ "localNetwork.getViewerEndpoints": {
28819
+ capName: "local-network",
28820
+ capScope: "system",
28821
+ addonId: null,
28822
+ access: "view"
28823
+ },
28651
28824
  "localNetwork.list": {
28652
28825
  capName: "local-network",
28653
28826
  capScope: "system",
28654
28827
  addonId: null,
28655
28828
  access: "view"
28656
28829
  },
28830
+ "localNetwork.regenerateCertificate": {
28831
+ capName: "local-network",
28832
+ capScope: "system",
28833
+ addonId: null,
28834
+ access: "create"
28835
+ },
28657
28836
  "localNetwork.resetAllowlistToBestMatch": {
28658
28837
  capName: "local-network",
28659
28838
  capScope: "system",
28660
28839
  addonId: null,
28661
28840
  access: "delete"
28662
28841
  },
28842
+ "localNetwork.revertToGeneratedCertificate": {
28843
+ capName: "local-network",
28844
+ capScope: "system",
28845
+ addonId: null,
28846
+ access: "create"
28847
+ },
28663
28848
  "localNetwork.setAllowedAddresses": {
28664
28849
  capName: "local-network",
28665
28850
  capScope: "system",
@@ -28672,6 +28857,18 @@ Object.freeze({
28672
28857
  addonId: null,
28673
28858
  access: "create"
28674
28859
  },
28860
+ "localNetwork.setViewerEndpoints": {
28861
+ capName: "local-network",
28862
+ capScope: "system",
28863
+ addonId: null,
28864
+ access: "create"
28865
+ },
28866
+ "localNetwork.uploadCertificate": {
28867
+ capName: "local-network",
28868
+ capScope: "system",
28869
+ addonId: null,
28870
+ access: "create"
28871
+ },
28675
28872
  "lockControl.lock": {
28676
28873
  capName: "lock-control",
28677
28874
  capScope: "device",
@@ -31552,6 +31749,12 @@ Object.freeze({
31552
31749
  addonId: null,
31553
31750
  access: "create"
31554
31751
  },
31752
+ "terminalSession.updateInstance": {
31753
+ capName: "terminal-session",
31754
+ capScope: "system",
31755
+ addonId: null,
31756
+ access: "create"
31757
+ },
31555
31758
  "terminalSession.writeInput": {
31556
31759
  capName: "terminal-session",
31557
31760
  capScope: "system",
@@ -34039,6 +34242,35 @@ Object.freeze(Object.fromEntries([{
34039
34242
  }]
34040
34243
  }].map((s) => [s.stepId, s.defaultModelId])));
34041
34244
  string().min(1);
34245
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34246
+ stepId: "face-embedding",
34247
+ key: "minLandmarkFaceSize",
34248
+ label: "Min face size for recognition (detection px)",
34249
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
34250
+ type: "slider",
34251
+ min: 0,
34252
+ max: 64,
34253
+ step: 2,
34254
+ default: 24
34255
+ }];
34256
+ function clusterStepSettingKey(stepId, fieldKey) {
34257
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34258
+ }
34259
+ var ClusterSettingNumberSchema = number$1().finite();
34260
+ function readClusterStepSettings(config) {
34261
+ const out = {};
34262
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34263
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34264
+ const value = parsed.success ? parsed.data : field.default;
34265
+ const existing = out[field.stepId] ?? {};
34266
+ out[field.stepId] = {
34267
+ ...existing,
34268
+ [field.key]: value
34269
+ };
34270
+ }
34271
+ return out;
34272
+ }
34273
+ readClusterStepSettings({});
34042
34274
  object({
34043
34275
  /**
34044
34276
  * Fraction of the box's own size added on EACH side before cutting.
@@ -71715,7 +71947,7 @@ function entryForRef(ref) {
71715
71947
  };
71716
71948
  }
71717
71949
  //#endregion
71718
- //#region ../system/dist/file-data-plane-CuE_hBli.mjs
71950
+ //#region ../system/dist/file-data-plane-BhKdxJgf.mjs
71719
71951
  function isNonEmptyFile(filePath) {
71720
71952
  return node_fs.existsSync(filePath) && node_fs.statSync(filePath).size > 0;
71721
71953
  }
@@ -71737,21 +71969,56 @@ function buildHeaders(url) {
71737
71969
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
71738
71970
  return headers;
71739
71971
  }
71740
- /**
71741
- * Download a single file from a URL to a destination path.
71742
- * Uses native fetch() (Node 22+) which handles redirects natively.
71743
- * Streams to disk with optional progress callback.
71744
- * Returns the destination path. Skips download if file already exists.
71745
- */
71746
- async function downloadFile(url, destPath, onProgress) {
71972
+ var DEFAULT_MAX_REDIRECTS = 5;
71973
+ function normalizeDownloadOptions(third) {
71974
+ if (typeof third === "function") return { onProgress: third };
71975
+ return third ?? {};
71976
+ }
71977
+ function isRedirectStatus(status) {
71978
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
71979
+ }
71980
+ function resolveRedirectUrl(current, location) {
71981
+ return new URL(location, current);
71982
+ }
71983
+ async function downloadFile(url, destPath, onProgressOrOptions) {
71747
71984
  if (node_fs.existsSync(destPath)) return destPath;
71985
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
71986
+ const fetchImpl = opts.fetchImpl ?? fetch;
71987
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
71748
71988
  node_fs.mkdirSync(node_path$1.dirname(destPath), { recursive: true });
71749
71989
  const tmpPath = destPath + ".downloading";
71750
71990
  try {
71751
- const response = await fetch(url, {
71752
- redirect: "follow",
71753
- headers: buildHeaders(url)
71754
- });
71991
+ let current = url;
71992
+ const seen = /* @__PURE__ */ new Set();
71993
+ let response;
71994
+ const manual = opts.redirectPolicy !== void 0;
71995
+ for (let hop = 0; hop <= maxRedirects; hop++) {
71996
+ const parsed = new URL(current);
71997
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
71998
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
71999
+ seen.add(parsed.href);
72000
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
72001
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
72002
+ try {
72003
+ response = await fetchImpl(current, {
72004
+ redirect: manual ? "manual" : "follow",
72005
+ headers: buildHeaders(current),
72006
+ ...controller ? { signal: controller.signal } : {}
72007
+ });
72008
+ } finally {
72009
+ if (timer) clearTimeout(timer);
72010
+ }
72011
+ if (manual && isRedirectStatus(response.status)) {
72012
+ const location = response.headers.get("location");
72013
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
72014
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
72015
+ current = resolveRedirectUrl(current, location).href;
72016
+ continue;
72017
+ }
72018
+ break;
72019
+ }
72020
+ if (!response) throw new Error(`No response downloading ${url}`);
72021
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
71755
72022
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
71756
72023
  if (!response.body) throw new Error(`No response body from ${url}`);
71757
72024
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -71762,9 +72029,10 @@ async function downloadFile(url, destPath, onProgress) {
71762
72029
  for (;;) {
71763
72030
  const { done, value } = await reader.read();
71764
72031
  if (done || !value) break;
71765
- fileStream.write(value);
71766
72032
  downloaded += value.length;
71767
- onProgress?.(downloaded, total);
72033
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
72034
+ fileStream.write(value);
72035
+ opts.onProgress?.(downloaded, total);
71768
72036
  }
71769
72037
  } finally {
71770
72038
  fileStream.end();
package/dist/addon.mjs CHANGED
@@ -8252,6 +8252,15 @@ var LabelDefinitionSchema = object({
8252
8252
  description: string().optional(),
8253
8253
  icon: string().optional()
8254
8254
  });
8255
+ var ClassMapDefinitionSchema = object({
8256
+ mapping: record(string(), _enum([
8257
+ "person",
8258
+ "vehicle",
8259
+ "animal",
8260
+ "package"
8261
+ ])),
8262
+ preserveOriginal: boolean()
8263
+ });
8255
8264
  var MODEL_FORMATS = [
8256
8265
  "onnx",
8257
8266
  "coreml",
@@ -8430,7 +8439,13 @@ var ModelCatalogEntrySchema = object({
8430
8439
  * `id` stays the source of truth for resolution/download/persistence; grouping
8431
8440
  * is a presentation overlay resolved back to an `id`.
8432
8441
  */
8433
- group: ModelVariantGroupSchema.optional()
8442
+ group: ModelVariantGroupSchema.optional(),
8443
+ /**
8444
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8445
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8446
+ * labels already ARE the CamStack macros (Scrypted identity map).
8447
+ */
8448
+ classMap: ClassMapDefinitionSchema.optional()
8434
8449
  });
8435
8450
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8436
8451
  format: literal("openvino"),
@@ -8459,7 +8474,8 @@ var ModelConvertMetadataSchema = object({
8459
8474
  "ocr",
8460
8475
  "segmentation"
8461
8476
  ]),
8462
- faceAlignment: boolean().optional()
8477
+ faceAlignment: boolean().optional(),
8478
+ classMap: ClassMapDefinitionSchema.optional()
8463
8479
  });
8464
8480
  var ConvertResultSchema = object({
8465
8481
  entry: ModelCatalogEntrySchema,
@@ -17531,6 +17547,33 @@ var NativeCropRefSchema = object({
17531
17547
  h: number$1()
17532
17548
  })
17533
17549
  });
17550
+ object({
17551
+ crop: object({
17552
+ left: number$1(),
17553
+ top: number$1(),
17554
+ width: number$1().positive(),
17555
+ height: number$1().positive()
17556
+ }).optional(),
17557
+ content: object({
17558
+ width: number$1().int().positive(),
17559
+ height: number$1().int().positive()
17560
+ }),
17561
+ fit: _enum(["stretch", "contain"]),
17562
+ format: _enum([
17563
+ "rgb",
17564
+ "gray",
17565
+ "jpeg"
17566
+ ])
17567
+ });
17568
+ var FrameRefSchema = object({
17569
+ registryId: string().min(1),
17570
+ id: string().min(1),
17571
+ width: number$1().int().positive(),
17572
+ height: number$1().int().positive(),
17573
+ format: _enum(["rgb", "gray"]),
17574
+ timestamp: number$1(),
17575
+ capturedAt: number$1().optional()
17576
+ });
17534
17577
  var ModelFormatSchema$1 = _enum([
17535
17578
  "onnx",
17536
17579
  "coreml",
@@ -17775,6 +17818,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17775
17818
  steps: array(PipelineStepInputSchema).min(1),
17776
17819
  frame: FrameInputSchema.optional(),
17777
17820
  /**
17821
+ * Process-local lazy frame. Valid only when caller and provider resolve
17822
+ * in the same execution-group process; split/cross-node callers use
17823
+ * `frame`/`image` inline compatibility instead.
17824
+ */
17825
+ frameRef: FrameRefSchema.optional(),
17826
+ /**
17778
17827
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17779
17828
  * the decoded pixels live in. One more member of the one-of
17780
17829
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -18030,7 +18079,10 @@ var NativeCropResultSchema = object({
18030
18079
  * Which source served this crop, so a quality-sensitive consumer (the native
18031
18080
  * `keyFrame`) can reject a degraded fallback:
18032
18081
  * - `native` — cut from the decode worker's retained NATIVE surface (the
18033
- * quality path).
18082
+ * quality path). A subject-tile serve is also native-resolution and stays
18083
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18084
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18085
+ * internal crop result (`nativeHits` vs `tileHits`).
18034
18086
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18035
18087
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18036
18088
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18521,12 +18573,41 @@ var RunnerLocalLoadSchema = object({
18521
18573
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18522
18574
  * working unchanged when they switch to reading from the runner cap.
18523
18575
  */
18576
+ var FrameLazyCountersSchema = object({
18577
+ framesDecoded: number$1(),
18578
+ framesAdmitted: number$1(),
18579
+ framesDroppedPixelFree: number$1(),
18580
+ viewsMaterialized: number$1(),
18581
+ viewsSkipped: number$1(),
18582
+ workerToRunnerBytes: number$1(),
18583
+ runnerToPoolRawBytes: number$1(),
18584
+ runnerToPoolJpegBytes: number$1(),
18585
+ onDemandFullFrameRequests: number$1(),
18586
+ onDemandCropRequests: number$1(),
18587
+ nativeHits: number$1(),
18588
+ nativeMisses: number$1(),
18589
+ tileHits: number$1(),
18590
+ tileMisses: number$1(),
18591
+ fallbackHits: number$1(),
18592
+ fallbackMisses: number$1(),
18593
+ retainedWritesAvoided: number$1(),
18594
+ residentRefs: number$1(),
18595
+ residentBytes: number$1(),
18596
+ releases: number$1(),
18597
+ evictions: number$1(),
18598
+ staleMisses: number$1()
18599
+ });
18600
+ var FrameLazyMetricsSchema = object({
18601
+ node: FrameLazyCountersSchema,
18602
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number$1() }))
18603
+ });
18524
18604
  var RunnerLocalMetricsSchema = object({
18525
18605
  nodeId: string(),
18526
18606
  activeCameras: number$1(),
18527
18607
  throttledCameras: number$1(),
18528
18608
  avgInferenceTimeMs: number$1(),
18529
- queueDepth: number$1()
18609
+ queueDepth: number$1(),
18610
+ frameLazy: FrameLazyMetricsSchema.optional()
18530
18611
  });
18531
18612
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number$1() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number$1() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number$1()).readonly()), method(object({
18532
18613
  handle: FrameHandleSchema,
@@ -19826,6 +19907,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19826
19907
  location: StorageLocationSchema,
19827
19908
  relativePath: string()
19828
19909
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number$1().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19910
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19911
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19912
+ var ProfileSettingsBagSchema = record(string(), unknown());
19829
19913
  /**
19830
19914
  * A live terminal session hosted by the provider addon. Output and input do
19831
19915
  * NOT flow through the capability — they use the addon data plane
@@ -19855,7 +19939,14 @@ var TerminalSessionInfoSchema = object({
19855
19939
  var TerminalProfileInfoSchema = object({
19856
19940
  profileId: string(),
19857
19941
  label: string(),
19858
- description: string().optional()
19942
+ description: string().optional(),
19943
+ /** Spawn defaults the instance form copies on create. */
19944
+ executable: string().optional(),
19945
+ args: array(string()).readonly().optional(),
19946
+ cwd: string().optional(),
19947
+ environment: array(string()).readonly().optional(),
19948
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
19949
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19859
19950
  });
19860
19951
  /**
19861
19952
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19868,7 +19959,12 @@ var TerminalInstanceInfoSchema = object({
19868
19959
  profileId: string(),
19869
19960
  profileLabel: string(),
19870
19961
  name: string(),
19871
- enabled: boolean()
19962
+ enabled: boolean(),
19963
+ executable: string(),
19964
+ args: array(string()).readonly(),
19965
+ cwd: string(),
19966
+ environment: array(string()).readonly(),
19967
+ profileSettings: ProfileSettingsBagSchema
19872
19968
  });
19873
19969
  var TerminalLegacyCameraSchema = object({
19874
19970
  stableId: string(),
@@ -19898,7 +19994,23 @@ var TerminalOutputBatchSchema = object({
19898
19994
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19899
19995
  targetNodeId: string().min(1),
19900
19996
  profileId: string().min(1),
19901
- name: string().trim().min(1).max(160).optional()
19997
+ name: string().trim().min(1).max(160).optional(),
19998
+ executable: string().max(1024).optional(),
19999
+ args: array(string().max(2048)).max(64).optional(),
20000
+ cwd: string().max(1024).optional(),
20001
+ environment: array(string().max(4096)).max(64).optional(),
20002
+ profileSettings: ProfileSettingsBagSchema.optional()
20003
+ }), TerminalInstanceInfoSchema, {
20004
+ kind: "mutation",
20005
+ auth: "admin"
20006
+ }), method(object({
20007
+ instanceId: string().min(1),
20008
+ name: string().trim().min(1).max(160).optional(),
20009
+ executable: string().max(1024).optional(),
20010
+ args: array(string().max(2048)).max(64).optional(),
20011
+ cwd: string().max(1024).optional(),
20012
+ environment: array(string().max(4096)).max(64).optional(),
20013
+ profileSettings: ProfileSettingsBagSchema.optional()
19902
20014
  }), TerminalInstanceInfoSchema, {
19903
20015
  kind: "mutation",
19904
20016
  auth: "admin"
@@ -19920,7 +20032,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19920
20032
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19921
20033
  profileId: string(),
19922
20034
  cols: number$1().int().positive(),
19923
- rows: number$1().int().positive()
20035
+ rows: number$1().int().positive(),
20036
+ executable: string().max(1024).optional(),
20037
+ args: array(string().max(2048)).max(64).optional(),
20038
+ cwd: string().max(1024).optional(),
20039
+ environment: array(string().max(4096)).max(64).optional()
19924
20040
  }), TerminalSessionInfoSchema, {
19925
20041
  kind: "mutation",
19926
20042
  auth: "admin"
@@ -22702,10 +22818,10 @@ DeviceType.LawnMower, method(object({ deviceId: number$1().int().nonnegative() }
22702
22818
  *
22703
22819
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22704
22820
  * to receive an ordered list of candidate base URLs it should race
22705
- * on connect — LAN IPv4 first (lowest latency when on same network),
22706
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22707
- * race them with short timeouts and stick with the winner for the
22708
- * session.
22821
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
22822
+ * when on the same network), then public hostname (if a tunnel is
22823
+ * up). The SDK can race them with short timeouts and stick with the
22824
+ * winner for the session.
22709
22825
  *
22710
22826
  * Why hub-only: agents are not directly addressable by the operator's
22711
22827
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22860,6 +22976,17 @@ var NotificationEndpointSchema = object({
22860
22976
  /** What the ranking currently resolves to (null when nothing is reachable). */
22861
22977
  resolved: string().nullable()
22862
22978
  });
22979
+ /**
22980
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
22981
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
22982
+ * currently expands to, so the UI can show the effective set either way.
22983
+ */
22984
+ var ViewerEndpointsSchema = object({
22985
+ /** The operator's explicit race set, or empty for AUTO. */
22986
+ baseUrls: array(string()).readonly(),
22987
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
22988
+ resolved: array(string()).readonly()
22989
+ });
22863
22990
  var AllowedAddressesSchema = object({
22864
22991
  /**
22865
22992
  * Allowlist of interface addresses operators have explicitly opted
@@ -22868,6 +22995,20 @@ var AllowedAddressesSchema = object({
22868
22995
  * Network Addresses admin page and persisted by the addon.
22869
22996
  */
22870
22997
  addresses: array(string()).readonly() });
22998
+ var TlsStatusSchema = object({
22999
+ mode: _enum([
23000
+ "generated",
23001
+ "uploaded",
23002
+ "disabled"
23003
+ ]),
23004
+ leafFingerprintSha256: string().nullable(),
23005
+ caFingerprintSha256: string().nullable(),
23006
+ validTo: string().nullable(),
23007
+ sans: array(string()),
23008
+ caCertPem: string().nullable(),
23009
+ reissueError: string().nullable(),
23010
+ restartRequired: boolean()
23011
+ });
22871
23012
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22872
23013
  /**
22873
23014
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22877,17 +23018,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22877
23018
  */
22878
23019
  port: number$1().int().min(1).max(65535).optional(),
22879
23020
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22880
- * candidate. Default `true`. */
23021
+ * candidate. Default `false` — loopback is not a client route. */
22881
23022
  includeLoopback: boolean().optional(),
22882
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22883
- * Default `false`. */
23023
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23024
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23025
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22884
23026
  ipv4Only: boolean().optional(),
22885
23027
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22886
23028
  * Pass `'https'` when the caller is itself loaded over HTTPS
22887
23029
  * to avoid mixed-content blocks in the browser. The public
22888
23030
  * tunnel always emits `https://` regardless. */
22889
23031
  scheme: _enum(["http", "https"]).optional()
22890
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23032
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
23033
+ kind: "mutation",
23034
+ auth: "admin"
23035
+ }), method(object({
23036
+ certPem: string().min(1),
23037
+ keyPem: string().min(1),
23038
+ caPem: string().optional()
23039
+ }), TlsStatusSchema, {
23040
+ kind: "mutation",
23041
+ auth: "admin"
23042
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23043
+ kind: "mutation",
23044
+ auth: "admin"
23045
+ });
22891
23046
  object({
22892
23047
  /** Lifecycle state of the lock. `jammed` means the motor reported
22893
23048
  * failure to reach the target — operator intervention required. */
@@ -28650,6 +28805,12 @@ Object.freeze({
28650
28805
  addonId: null,
28651
28806
  access: "create"
28652
28807
  },
28808
+ "localNetwork.downloadCa": {
28809
+ capName: "local-network",
28810
+ capScope: "system",
28811
+ addonId: null,
28812
+ access: "view"
28813
+ },
28653
28814
  "localNetwork.getAllowedAddresses": {
28654
28815
  capName: "local-network",
28655
28816
  capScope: "system",
@@ -28674,18 +28835,42 @@ Object.freeze({
28674
28835
  addonId: null,
28675
28836
  access: "view"
28676
28837
  },
28838
+ "localNetwork.getTlsStatus": {
28839
+ capName: "local-network",
28840
+ capScope: "system",
28841
+ addonId: null,
28842
+ access: "view"
28843
+ },
28844
+ "localNetwork.getViewerEndpoints": {
28845
+ capName: "local-network",
28846
+ capScope: "system",
28847
+ addonId: null,
28848
+ access: "view"
28849
+ },
28677
28850
  "localNetwork.list": {
28678
28851
  capName: "local-network",
28679
28852
  capScope: "system",
28680
28853
  addonId: null,
28681
28854
  access: "view"
28682
28855
  },
28856
+ "localNetwork.regenerateCertificate": {
28857
+ capName: "local-network",
28858
+ capScope: "system",
28859
+ addonId: null,
28860
+ access: "create"
28861
+ },
28683
28862
  "localNetwork.resetAllowlistToBestMatch": {
28684
28863
  capName: "local-network",
28685
28864
  capScope: "system",
28686
28865
  addonId: null,
28687
28866
  access: "delete"
28688
28867
  },
28868
+ "localNetwork.revertToGeneratedCertificate": {
28869
+ capName: "local-network",
28870
+ capScope: "system",
28871
+ addonId: null,
28872
+ access: "create"
28873
+ },
28689
28874
  "localNetwork.setAllowedAddresses": {
28690
28875
  capName: "local-network",
28691
28876
  capScope: "system",
@@ -28698,6 +28883,18 @@ Object.freeze({
28698
28883
  addonId: null,
28699
28884
  access: "create"
28700
28885
  },
28886
+ "localNetwork.setViewerEndpoints": {
28887
+ capName: "local-network",
28888
+ capScope: "system",
28889
+ addonId: null,
28890
+ access: "create"
28891
+ },
28892
+ "localNetwork.uploadCertificate": {
28893
+ capName: "local-network",
28894
+ capScope: "system",
28895
+ addonId: null,
28896
+ access: "create"
28897
+ },
28701
28898
  "lockControl.lock": {
28702
28899
  capName: "lock-control",
28703
28900
  capScope: "device",
@@ -31578,6 +31775,12 @@ Object.freeze({
31578
31775
  addonId: null,
31579
31776
  access: "create"
31580
31777
  },
31778
+ "terminalSession.updateInstance": {
31779
+ capName: "terminal-session",
31780
+ capScope: "system",
31781
+ addonId: null,
31782
+ access: "create"
31783
+ },
31581
31784
  "terminalSession.writeInput": {
31582
31785
  capName: "terminal-session",
31583
31786
  capScope: "system",
@@ -34065,6 +34268,35 @@ Object.freeze(Object.fromEntries([{
34065
34268
  }]
34066
34269
  }].map((s) => [s.stepId, s.defaultModelId])));
34067
34270
  string().min(1);
34271
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34272
+ stepId: "face-embedding",
34273
+ key: "minLandmarkFaceSize",
34274
+ label: "Min face size for recognition (detection px)",
34275
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
34276
+ type: "slider",
34277
+ min: 0,
34278
+ max: 64,
34279
+ step: 2,
34280
+ default: 24
34281
+ }];
34282
+ function clusterStepSettingKey(stepId, fieldKey) {
34283
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34284
+ }
34285
+ var ClusterSettingNumberSchema = number$1().finite();
34286
+ function readClusterStepSettings(config) {
34287
+ const out = {};
34288
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34289
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34290
+ const value = parsed.success ? parsed.data : field.default;
34291
+ const existing = out[field.stepId] ?? {};
34292
+ out[field.stepId] = {
34293
+ ...existing,
34294
+ [field.key]: value
34295
+ };
34296
+ }
34297
+ return out;
34298
+ }
34299
+ readClusterStepSettings({});
34068
34300
  object({
34069
34301
  /**
34070
34302
  * Fraction of the box's own size added on EACH side before cutting.
@@ -71741,7 +71973,7 @@ function entryForRef(ref) {
71741
71973
  };
71742
71974
  }
71743
71975
  //#endregion
71744
- //#region ../system/dist/file-data-plane-CuE_hBli.mjs
71976
+ //#region ../system/dist/file-data-plane-BhKdxJgf.mjs
71745
71977
  function isNonEmptyFile(filePath) {
71746
71978
  return fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
71747
71979
  }
@@ -71763,21 +71995,56 @@ function buildHeaders(url) {
71763
71995
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
71764
71996
  return headers;
71765
71997
  }
71766
- /**
71767
- * Download a single file from a URL to a destination path.
71768
- * Uses native fetch() (Node 22+) which handles redirects natively.
71769
- * Streams to disk with optional progress callback.
71770
- * Returns the destination path. Skips download if file already exists.
71771
- */
71772
- async function downloadFile(url, destPath, onProgress) {
71998
+ var DEFAULT_MAX_REDIRECTS = 5;
71999
+ function normalizeDownloadOptions(third) {
72000
+ if (typeof third === "function") return { onProgress: third };
72001
+ return third ?? {};
72002
+ }
72003
+ function isRedirectStatus(status) {
72004
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
72005
+ }
72006
+ function resolveRedirectUrl(current, location) {
72007
+ return new URL(location, current);
72008
+ }
72009
+ async function downloadFile(url, destPath, onProgressOrOptions) {
71773
72010
  if (fs.existsSync(destPath)) return destPath;
72011
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
72012
+ const fetchImpl = opts.fetchImpl ?? fetch;
72013
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
71774
72014
  fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
71775
72015
  const tmpPath = destPath + ".downloading";
71776
72016
  try {
71777
- const response = await fetch(url, {
71778
- redirect: "follow",
71779
- headers: buildHeaders(url)
71780
- });
72017
+ let current = url;
72018
+ const seen = /* @__PURE__ */ new Set();
72019
+ let response;
72020
+ const manual = opts.redirectPolicy !== void 0;
72021
+ for (let hop = 0; hop <= maxRedirects; hop++) {
72022
+ const parsed = new URL(current);
72023
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
72024
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
72025
+ seen.add(parsed.href);
72026
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
72027
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
72028
+ try {
72029
+ response = await fetchImpl(current, {
72030
+ redirect: manual ? "manual" : "follow",
72031
+ headers: buildHeaders(current),
72032
+ ...controller ? { signal: controller.signal } : {}
72033
+ });
72034
+ } finally {
72035
+ if (timer) clearTimeout(timer);
72036
+ }
72037
+ if (manual && isRedirectStatus(response.status)) {
72038
+ const location = response.headers.get("location");
72039
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
72040
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
72041
+ current = resolveRedirectUrl(current, location).href;
72042
+ continue;
72043
+ }
72044
+ break;
72045
+ }
72046
+ if (!response) throw new Error(`No response downloading ${url}`);
72047
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
71781
72048
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
71782
72049
  if (!response.body) throw new Error(`No response body from ${url}`);
71783
72050
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -71788,9 +72055,10 @@ async function downloadFile(url, destPath, onProgress) {
71788
72055
  for (;;) {
71789
72056
  const { done, value } = await reader.read();
71790
72057
  if (done || !value) break;
71791
- fileStream.write(value);
71792
72058
  downloaded += value.length;
71793
- onProgress?.(downloaded, total);
72059
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
72060
+ fileStream.write(value);
72061
+ opts.onProgress?.(downloaded, total);
71794
72062
  }
71795
72063
  } finally {
71796
72064
  fileStream.end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",