@camstack/addon-ai 0.4.16 → 0.4.18

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 +417 -43
  2. package/dist/addon.mjs +417 -43
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5964,6 +5964,13 @@ var BaseAddon = class {
5964
5964
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5965
5965
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5966
5966
  _registeredCapNames = [];
5967
+ /**
5968
+ * True only after `readAddonStore` actually answered. Constructor
5969
+ * defaults look like stored config when the store is down — a forked
5970
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5971
+ * mode, 2026-08-25) is not "the operator chose this".
5972
+ */
5973
+ settingsStoreReady = false;
5967
5974
  /** Default config values. Provided via constructor. */
5968
5975
  defaults;
5969
5976
  constructor(defaults) {
@@ -6364,7 +6371,9 @@ var BaseAddon = class {
6364
6371
  ];
6365
6372
  let lastErr;
6366
6373
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6367
- return await settings.readAddonStore() ?? {};
6374
+ const stored = await settings.readAddonStore() ?? {};
6375
+ this.settingsStoreReady = true;
6376
+ return stored;
6368
6377
  } catch (err) {
6369
6378
  lastErr = err;
6370
6379
  const msg = err instanceof Error ? err.message : String(err);
@@ -6372,6 +6381,7 @@ var BaseAddon = class {
6372
6381
  if (attempt === delaysMs.length) break;
6373
6382
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6374
6383
  }
6384
+ this.settingsStoreReady = false;
6375
6385
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6376
6386
  return {};
6377
6387
  }
@@ -8226,6 +8236,15 @@ var LabelDefinitionSchema = object({
8226
8236
  description: string().optional(),
8227
8237
  icon: string().optional()
8228
8238
  });
8239
+ var ClassMapDefinitionSchema = object({
8240
+ mapping: record(string(), _enum([
8241
+ "person",
8242
+ "vehicle",
8243
+ "animal",
8244
+ "package"
8245
+ ])),
8246
+ preserveOriginal: boolean()
8247
+ });
8229
8248
  var MODEL_FORMATS = [
8230
8249
  "onnx",
8231
8250
  "coreml",
@@ -8309,6 +8328,12 @@ var ModelVariantGroupSchema = object({
8309
8328
  */
8310
8329
  resolution: number$1().int().positive().optional()
8311
8330
  });
8331
+ var ModelProviderIdSchema = _enum([
8332
+ "camstack",
8333
+ "frigate",
8334
+ "scrypted",
8335
+ "custom"
8336
+ ]);
8312
8337
  var ModelCatalogEntrySchema = object({
8313
8338
  id: string(),
8314
8339
  name: string(),
@@ -8404,7 +8429,19 @@ var ModelCatalogEntrySchema = object({
8404
8429
  * `id` stays the source of truth for resolution/download/persistence; grouping
8405
8430
  * is a presentation overlay resolved back to an `id`.
8406
8431
  */
8407
- group: ModelVariantGroupSchema.optional()
8432
+ group: ModelVariantGroupSchema.optional(),
8433
+ /**
8434
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8435
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8436
+ * persisted before this field existed (`inferModelProvider` fills those).
8437
+ */
8438
+ provider: ModelProviderIdSchema.optional(),
8439
+ /**
8440
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8441
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8442
+ * labels already ARE the CamStack macros (Scrypted identity map).
8443
+ */
8444
+ classMap: ClassMapDefinitionSchema.optional()
8408
8445
  });
8409
8446
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8410
8447
  format: literal("openvino"),
@@ -8433,7 +8470,8 @@ var ModelConvertMetadataSchema = object({
8433
8470
  "ocr",
8434
8471
  "segmentation"
8435
8472
  ]),
8436
- faceAlignment: boolean().optional()
8473
+ faceAlignment: boolean().optional(),
8474
+ classMap: ClassMapDefinitionSchema.optional()
8437
8475
  });
8438
8476
  var ConvertResultSchema = object({
8439
8477
  entry: ModelCatalogEntrySchema,
@@ -14653,12 +14691,15 @@ var NcOccupancyConditionSchema = object({
14653
14691
  * there is no second switch that can disagree with the first and every rule
14654
14692
  * authored before the decision migrates for free (`audioModeOf`):
14655
14693
  *
14656
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14657
- * classifier labels with one of them. No window, no percentage:
14658
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14659
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14660
- * the analyzer's (`classificationMinScore`, per device) a label only
14661
- * reaches this condition if the classifier was already confident enough.
14694
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14695
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14696
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14697
+ * frames is the wrong question for a classifier that labels 1–3 frames
14698
+ * per episode. The count window is the brake that drops a single-frame
14699
+ * false positive; the rule's own `throttle` cooldown is the other. The
14700
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14701
+ * per device) — a label only reaches this condition if the classifier was
14702
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14662
14703
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14663
14704
  * the condition: at least `hitPercent`% of the samples over
14664
14705
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14685,14 +14726,22 @@ var NcOccupancyConditionSchema = object({
14685
14726
  * an operator who typed `dog` mean the same thing.
14686
14727
  */
14687
14728
  var NcAudioConditionSchema = object({
14688
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14729
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14689
14730
  labels: array(string().min(1)).min(1).optional(),
14690
14731
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14691
14732
  dbThreshold: number$1().min(-96).max(0).optional(),
14692
14733
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14693
14734
  hitPercent: number$1().int().min(1).max(100).default(60),
14694
14735
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14695
- samplingSeconds: number$1().int().min(1).max(300).default(10)
14736
+ samplingSeconds: number$1().int().min(1).max(300).default(10),
14737
+ /**
14738
+ * LABEL MODE: how many labelled frames must land inside
14739
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14740
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14741
+ */
14742
+ confirmHits: number$1().int().min(1).max(20).optional(),
14743
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14744
+ confirmWindowSec: number$1().int().min(1).max(60).optional()
14696
14745
  });
14697
14746
  /**
14698
14747
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17066,6 +17115,46 @@ var RecentTracksPageSchema = object({
17066
17115
  /** Cursor for the next page, or null when this page is the last. */
17067
17116
  nextCursor: string().nullable()
17068
17117
  });
17118
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17119
+ var LIST_GROUPS_MAX_LIMIT = 100;
17120
+ var AnalyticsGroupRecordSchema = object({
17121
+ id: string(),
17122
+ deviceId: number$1().int(),
17123
+ openedAt: number$1().int(),
17124
+ closedAt: number$1().int(),
17125
+ timestamp: number$1().int(),
17126
+ memberCount: number$1().int(),
17127
+ memberTrackIds: array(string()).readonly(),
17128
+ className: string(),
17129
+ classes: array(string()).readonly(),
17130
+ /** Relative event-media path, or null when the group has no picture yet. */
17131
+ mediaUrl: string().nullable(),
17132
+ singleton: boolean()
17133
+ });
17134
+ var AnalyticsGroupMemberSchema = object({
17135
+ trackId: string(),
17136
+ deviceId: number$1().int(),
17137
+ className: string(),
17138
+ firstSeen: number$1().int(),
17139
+ lastSeen: number$1().int(),
17140
+ mediaUrl: string().nullable()
17141
+ });
17142
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17143
+ var ListGroupsQueryInput = object({
17144
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17145
+ deviceIds: array(number$1()),
17146
+ /** Window lower bound on `closedAt` (inclusive). */
17147
+ since: number$1().optional(),
17148
+ /** Window upper bound on `openedAt` (inclusive). */
17149
+ until: number$1().optional(),
17150
+ limit: number$1().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17151
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17152
+ cursor: string().optional()
17153
+ });
17154
+ var ListGroupsPageSchema = object({
17155
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17156
+ nextCursor: string().nullable()
17157
+ });
17069
17158
  var KeyEventQueryInput = object({
17070
17159
  deviceId: number$1(),
17071
17160
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17141,7 +17230,9 @@ var TrackCascadeCountsSchema = object({
17141
17230
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17142
17231
  plates: number$1().int(),
17143
17232
  /** Per-track CLIP search vectors removed (best-effort). */
17144
- embeddings: number$1().int()
17233
+ embeddings: number$1().int(),
17234
+ /** Group membership + group rows removed with their last member (best-effort). */
17235
+ groups: number$1().int()
17145
17236
  });
17146
17237
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17147
17238
  var DiskReconcileCountsSchema = object({
@@ -17287,7 +17378,10 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
17287
17378
  * stationary registry). Default false: the timeline lists passages,
17288
17379
  * not parking records (operator decision, 2026-08-15). */
17289
17380
  includeStationary: boolean().optional()
17290
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number$1() }), _void(), {
17381
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17382
+ deviceId: number$1(),
17383
+ groupId: string().min(1)
17384
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number$1() }), _void(), {
17291
17385
  kind: "mutation",
17292
17386
  auth: "admin"
17293
17387
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number$1() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number$1()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17505,6 +17599,33 @@ var NativeCropRefSchema = object({
17505
17599
  h: number$1()
17506
17600
  })
17507
17601
  });
17602
+ object({
17603
+ crop: object({
17604
+ left: number$1(),
17605
+ top: number$1(),
17606
+ width: number$1().positive(),
17607
+ height: number$1().positive()
17608
+ }).optional(),
17609
+ content: object({
17610
+ width: number$1().int().positive(),
17611
+ height: number$1().int().positive()
17612
+ }),
17613
+ fit: _enum(["stretch", "contain"]),
17614
+ format: _enum([
17615
+ "rgb",
17616
+ "gray",
17617
+ "jpeg"
17618
+ ])
17619
+ });
17620
+ var FrameRefSchema = object({
17621
+ registryId: string().min(1),
17622
+ id: string().min(1),
17623
+ width: number$1().int().positive(),
17624
+ height: number$1().int().positive(),
17625
+ format: _enum(["rgb", "gray"]),
17626
+ timestamp: number$1(),
17627
+ capturedAt: number$1().optional()
17628
+ });
17508
17629
  var ModelFormatSchema$1 = _enum([
17509
17630
  "onnx",
17510
17631
  "coreml",
@@ -17570,7 +17691,8 @@ var PipelineModelOptionSchema = object({
17570
17691
  sizeMB: number$1()
17571
17692
  })),
17572
17693
  group: ModelVariantGroupSchema.optional(),
17573
- legacy: boolean().optional()
17694
+ legacy: boolean().optional(),
17695
+ provider: ModelProviderIdSchema.optional()
17574
17696
  });
17575
17697
  var ConfigFieldBridge = custom();
17576
17698
  var PipelineAddonSchemaSchema = object({
@@ -17749,6 +17871,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17749
17871
  steps: array(PipelineStepInputSchema).min(1),
17750
17872
  frame: FrameInputSchema.optional(),
17751
17873
  /**
17874
+ * Process-local lazy frame. Valid only when caller and provider resolve
17875
+ * in the same execution-group process; split/cross-node callers use
17876
+ * `frame`/`image` inline compatibility instead.
17877
+ */
17878
+ frameRef: FrameRefSchema.optional(),
17879
+ /**
17752
17880
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17753
17881
  * the decoded pixels live in. One more member of the one-of
17754
17882
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -18004,7 +18132,10 @@ var NativeCropResultSchema = object({
18004
18132
  * Which source served this crop, so a quality-sensitive consumer (the native
18005
18133
  * `keyFrame`) can reject a degraded fallback:
18006
18134
  * - `native` — cut from the decode worker's retained NATIVE surface (the
18007
- * quality path).
18135
+ * quality path). A subject-tile serve is also native-resolution and stays
18136
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18137
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18138
+ * internal crop result (`nativeHits` vs `tileHits`).
18008
18139
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18009
18140
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18010
18141
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18495,12 +18626,41 @@ var RunnerLocalLoadSchema = object({
18495
18626
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18496
18627
  * working unchanged when they switch to reading from the runner cap.
18497
18628
  */
18629
+ var FrameLazyCountersSchema = object({
18630
+ framesDecoded: number$1(),
18631
+ framesAdmitted: number$1(),
18632
+ framesDroppedPixelFree: number$1(),
18633
+ viewsMaterialized: number$1(),
18634
+ viewsSkipped: number$1(),
18635
+ workerToRunnerBytes: number$1(),
18636
+ runnerToPoolRawBytes: number$1(),
18637
+ runnerToPoolJpegBytes: number$1(),
18638
+ onDemandFullFrameRequests: number$1(),
18639
+ onDemandCropRequests: number$1(),
18640
+ nativeHits: number$1(),
18641
+ nativeMisses: number$1(),
18642
+ tileHits: number$1(),
18643
+ tileMisses: number$1(),
18644
+ fallbackHits: number$1(),
18645
+ fallbackMisses: number$1(),
18646
+ retainedWritesAvoided: number$1(),
18647
+ residentRefs: number$1(),
18648
+ residentBytes: number$1(),
18649
+ releases: number$1(),
18650
+ evictions: number$1(),
18651
+ staleMisses: number$1()
18652
+ });
18653
+ var FrameLazyMetricsSchema = object({
18654
+ node: FrameLazyCountersSchema,
18655
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number$1() }))
18656
+ });
18498
18657
  var RunnerLocalMetricsSchema = object({
18499
18658
  nodeId: string(),
18500
18659
  activeCameras: number$1(),
18501
18660
  throttledCameras: number$1(),
18502
18661
  avgInferenceTimeMs: number$1(),
18503
- queueDepth: number$1()
18662
+ queueDepth: number$1(),
18663
+ frameLazy: FrameLazyMetricsSchema.optional()
18504
18664
  });
18505
18665
  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
18666
  handle: FrameHandleSchema,
@@ -19800,6 +19960,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19800
19960
  location: StorageLocationSchema,
19801
19961
  relativePath: string()
19802
19962
  }), _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" });
19963
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19964
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19965
+ var ProfileSettingsBagSchema = record(string(), unknown());
19803
19966
  /**
19804
19967
  * A live terminal session hosted by the provider addon. Output and input do
19805
19968
  * NOT flow through the capability — they use the addon data plane
@@ -19829,7 +19992,14 @@ var TerminalSessionInfoSchema = object({
19829
19992
  var TerminalProfileInfoSchema = object({
19830
19993
  profileId: string(),
19831
19994
  label: string(),
19832
- description: string().optional()
19995
+ description: string().optional(),
19996
+ /** Spawn defaults the instance form copies on create. */
19997
+ executable: string().optional(),
19998
+ args: array(string()).readonly().optional(),
19999
+ cwd: string().optional(),
20000
+ environment: array(string()).readonly().optional(),
20001
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20002
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19833
20003
  });
19834
20004
  /**
19835
20005
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19842,7 +20012,12 @@ var TerminalInstanceInfoSchema = object({
19842
20012
  profileId: string(),
19843
20013
  profileLabel: string(),
19844
20014
  name: string(),
19845
- enabled: boolean()
20015
+ enabled: boolean(),
20016
+ executable: string(),
20017
+ args: array(string()).readonly(),
20018
+ cwd: string(),
20019
+ environment: array(string()).readonly(),
20020
+ profileSettings: ProfileSettingsBagSchema
19846
20021
  });
19847
20022
  var TerminalLegacyCameraSchema = object({
19848
20023
  stableId: string(),
@@ -19872,7 +20047,23 @@ var TerminalOutputBatchSchema = object({
19872
20047
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19873
20048
  targetNodeId: string().min(1),
19874
20049
  profileId: string().min(1),
19875
- name: string().trim().min(1).max(160).optional()
20050
+ name: string().trim().min(1).max(160).optional(),
20051
+ executable: string().max(1024).optional(),
20052
+ args: array(string().max(2048)).max(64).optional(),
20053
+ cwd: string().max(1024).optional(),
20054
+ environment: array(string().max(4096)).max(64).optional(),
20055
+ profileSettings: ProfileSettingsBagSchema.optional()
20056
+ }), TerminalInstanceInfoSchema, {
20057
+ kind: "mutation",
20058
+ auth: "admin"
20059
+ }), method(object({
20060
+ instanceId: string().min(1),
20061
+ name: string().trim().min(1).max(160).optional(),
20062
+ executable: string().max(1024).optional(),
20063
+ args: array(string().max(2048)).max(64).optional(),
20064
+ cwd: string().max(1024).optional(),
20065
+ environment: array(string().max(4096)).max(64).optional(),
20066
+ profileSettings: ProfileSettingsBagSchema.optional()
19876
20067
  }), TerminalInstanceInfoSchema, {
19877
20068
  kind: "mutation",
19878
20069
  auth: "admin"
@@ -19894,7 +20085,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19894
20085
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19895
20086
  profileId: string(),
19896
20087
  cols: number$1().int().positive(),
19897
- rows: number$1().int().positive()
20088
+ rows: number$1().int().positive(),
20089
+ executable: string().max(1024).optional(),
20090
+ args: array(string().max(2048)).max(64).optional(),
20091
+ cwd: string().max(1024).optional(),
20092
+ environment: array(string().max(4096)).max(64).optional()
19898
20093
  }), TerminalSessionInfoSchema, {
19899
20094
  kind: "mutation",
19900
20095
  auth: "admin"
@@ -22676,10 +22871,10 @@ DeviceType.LawnMower, method(object({ deviceId: number$1().int().nonnegative() }
22676
22871
  *
22677
22872
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22678
22873
  * 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.
22874
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
22875
+ * when on the same network), then public hostname (if a tunnel is
22876
+ * up). The SDK can race them with short timeouts and stick with the
22877
+ * winner for the session.
22683
22878
  *
22684
22879
  * Why hub-only: agents are not directly addressable by the operator's
22685
22880
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22834,6 +23029,17 @@ var NotificationEndpointSchema = object({
22834
23029
  /** What the ranking currently resolves to (null when nothing is reachable). */
22835
23030
  resolved: string().nullable()
22836
23031
  });
23032
+ /**
23033
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
23034
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
23035
+ * currently expands to, so the UI can show the effective set either way.
23036
+ */
23037
+ var ViewerEndpointsSchema = object({
23038
+ /** The operator's explicit race set, or empty for AUTO. */
23039
+ baseUrls: array(string()).readonly(),
23040
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
23041
+ resolved: array(string()).readonly()
23042
+ });
22837
23043
  var AllowedAddressesSchema = object({
22838
23044
  /**
22839
23045
  * Allowlist of interface addresses operators have explicitly opted
@@ -22842,6 +23048,20 @@ var AllowedAddressesSchema = object({
22842
23048
  * Network Addresses admin page and persisted by the addon.
22843
23049
  */
22844
23050
  addresses: array(string()).readonly() });
23051
+ var TlsStatusSchema = object({
23052
+ mode: _enum([
23053
+ "generated",
23054
+ "uploaded",
23055
+ "disabled"
23056
+ ]),
23057
+ leafFingerprintSha256: string().nullable(),
23058
+ caFingerprintSha256: string().nullable(),
23059
+ validTo: string().nullable(),
23060
+ sans: array(string()),
23061
+ caCertPem: string().nullable(),
23062
+ reissueError: string().nullable(),
23063
+ restartRequired: boolean()
23064
+ });
22845
23065
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22846
23066
  /**
22847
23067
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22851,17 +23071,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22851
23071
  */
22852
23072
  port: number$1().int().min(1).max(65535).optional(),
22853
23073
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22854
- * candidate. Default `true`. */
23074
+ * candidate. Default `false` — loopback is not a client route. */
22855
23075
  includeLoopback: boolean().optional(),
22856
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22857
- * Default `false`. */
23076
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23077
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23078
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22858
23079
  ipv4Only: boolean().optional(),
22859
23080
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22860
23081
  * Pass `'https'` when the caller is itself loaded over HTTPS
22861
23082
  * to avoid mixed-content blocks in the browser. The public
22862
23083
  * tunnel always emits `https://` regardless. */
22863
23084
  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" });
23085
+ }), 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, {
23086
+ kind: "mutation",
23087
+ auth: "admin"
23088
+ }), method(object({
23089
+ certPem: string().min(1),
23090
+ keyPem: string().min(1),
23091
+ caPem: string().optional()
23092
+ }), TlsStatusSchema, {
23093
+ kind: "mutation",
23094
+ auth: "admin"
23095
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23096
+ kind: "mutation",
23097
+ auth: "admin"
23098
+ });
22865
23099
  object({
22866
23100
  /** Lifecycle state of the lock. `jammed` means the motor reported
22867
23101
  * failure to reach the target — operator intervention required. */
@@ -24042,7 +24276,12 @@ var PlateInfoSchema = object({
24042
24276
  plateBbox: BoundingBoxSchema.optional(),
24043
24277
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24044
24278
  keyFrameMediaKey: string().optional(),
24045
- base64: string().optional()
24279
+ base64: string().optional(),
24280
+ /**
24281
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24282
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24283
+ */
24284
+ cropUrl: string().optional()
24046
24285
  });
24047
24286
  var MediaFileLiteSchema = object({
24048
24287
  key: string(),
@@ -28624,6 +28863,12 @@ Object.freeze({
28624
28863
  addonId: null,
28625
28864
  access: "create"
28626
28865
  },
28866
+ "localNetwork.downloadCa": {
28867
+ capName: "local-network",
28868
+ capScope: "system",
28869
+ addonId: null,
28870
+ access: "view"
28871
+ },
28627
28872
  "localNetwork.getAllowedAddresses": {
28628
28873
  capName: "local-network",
28629
28874
  capScope: "system",
@@ -28648,18 +28893,42 @@ Object.freeze({
28648
28893
  addonId: null,
28649
28894
  access: "view"
28650
28895
  },
28896
+ "localNetwork.getTlsStatus": {
28897
+ capName: "local-network",
28898
+ capScope: "system",
28899
+ addonId: null,
28900
+ access: "view"
28901
+ },
28902
+ "localNetwork.getViewerEndpoints": {
28903
+ capName: "local-network",
28904
+ capScope: "system",
28905
+ addonId: null,
28906
+ access: "view"
28907
+ },
28651
28908
  "localNetwork.list": {
28652
28909
  capName: "local-network",
28653
28910
  capScope: "system",
28654
28911
  addonId: null,
28655
28912
  access: "view"
28656
28913
  },
28914
+ "localNetwork.regenerateCertificate": {
28915
+ capName: "local-network",
28916
+ capScope: "system",
28917
+ addonId: null,
28918
+ access: "create"
28919
+ },
28657
28920
  "localNetwork.resetAllowlistToBestMatch": {
28658
28921
  capName: "local-network",
28659
28922
  capScope: "system",
28660
28923
  addonId: null,
28661
28924
  access: "delete"
28662
28925
  },
28926
+ "localNetwork.revertToGeneratedCertificate": {
28927
+ capName: "local-network",
28928
+ capScope: "system",
28929
+ addonId: null,
28930
+ access: "create"
28931
+ },
28663
28932
  "localNetwork.setAllowedAddresses": {
28664
28933
  capName: "local-network",
28665
28934
  capScope: "system",
@@ -28672,6 +28941,18 @@ Object.freeze({
28672
28941
  addonId: null,
28673
28942
  access: "create"
28674
28943
  },
28944
+ "localNetwork.setViewerEndpoints": {
28945
+ capName: "local-network",
28946
+ capScope: "system",
28947
+ addonId: null,
28948
+ access: "create"
28949
+ },
28950
+ "localNetwork.uploadCertificate": {
28951
+ capName: "local-network",
28952
+ capScope: "system",
28953
+ addonId: null,
28954
+ access: "create"
28955
+ },
28675
28956
  "lockControl.lock": {
28676
28957
  capName: "lock-control",
28677
28958
  capScope: "device",
@@ -29470,6 +29751,12 @@ Object.freeze({
29470
29751
  addonId: null,
29471
29752
  access: "view"
29472
29753
  },
29754
+ "pipelineAnalytics.getGroup": {
29755
+ capName: "pipeline-analytics",
29756
+ capScope: "device",
29757
+ addonId: null,
29758
+ access: "view"
29759
+ },
29473
29760
  "pipelineAnalytics.getKeyEvents": {
29474
29761
  capName: "pipeline-analytics",
29475
29762
  capScope: "device",
@@ -29554,6 +29841,12 @@ Object.freeze({
29554
29841
  addonId: null,
29555
29842
  access: "view"
29556
29843
  },
29844
+ "pipelineAnalytics.listGroups": {
29845
+ capName: "pipeline-analytics",
29846
+ capScope: "device",
29847
+ addonId: null,
29848
+ access: "view"
29849
+ },
29557
29850
  "pipelineAnalytics.listOpsLog": {
29558
29851
  capName: "pipeline-analytics",
29559
29852
  capScope: "device",
@@ -31552,6 +31845,12 @@ Object.freeze({
31552
31845
  addonId: null,
31553
31846
  access: "create"
31554
31847
  },
31848
+ "terminalSession.updateInstance": {
31849
+ capName: "terminal-session",
31850
+ capScope: "system",
31851
+ addonId: null,
31852
+ access: "create"
31853
+ },
31555
31854
  "terminalSession.writeInput": {
31556
31855
  capName: "terminal-session",
31557
31856
  capScope: "system",
@@ -32967,6 +33266,11 @@ Object.freeze({
32967
33266
  form: "single",
32968
33267
  optional: false
32969
33268
  }],
33269
+ "pipelineAnalytics.getGroup": [{
33270
+ name: "deviceId",
33271
+ form: "single",
33272
+ optional: false
33273
+ }],
32970
33274
  "pipelineAnalytics.getKeyEvents": [{
32971
33275
  name: "deviceId",
32972
33276
  form: "single",
@@ -33022,6 +33326,11 @@ Object.freeze({
33022
33326
  form: "array",
33023
33327
  optional: false
33024
33328
  }],
33329
+ "pipelineAnalytics.listGroups": [{
33330
+ name: "deviceIds",
33331
+ form: "array",
33332
+ optional: false
33333
+ }],
33025
33334
  "pipelineAnalytics.listOpsLog": [{
33026
33335
  name: "deviceId",
33027
33336
  form: "single",
@@ -34039,6 +34348,35 @@ Object.freeze(Object.fromEntries([{
34039
34348
  }]
34040
34349
  }].map((s) => [s.stepId, s.defaultModelId])));
34041
34350
  string().min(1);
34351
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34352
+ stepId: "face-embedding",
34353
+ key: "minLandmarkFaceSize",
34354
+ label: "Min face size for recognition (detection px)",
34355
+ 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.",
34356
+ type: "slider",
34357
+ min: 0,
34358
+ max: 64,
34359
+ step: 2,
34360
+ default: 24
34361
+ }];
34362
+ function clusterStepSettingKey(stepId, fieldKey) {
34363
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34364
+ }
34365
+ var ClusterSettingNumberSchema = number$1().finite();
34366
+ function readClusterStepSettings(config) {
34367
+ const out = {};
34368
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34369
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34370
+ const value = parsed.success ? parsed.data : field.default;
34371
+ const existing = out[field.stepId] ?? {};
34372
+ out[field.stepId] = {
34373
+ ...existing,
34374
+ [field.key]: value
34375
+ };
34376
+ }
34377
+ return out;
34378
+ }
34379
+ readClusterStepSettings({});
34042
34380
  object({
34043
34381
  /**
34044
34382
  * Fraction of the box's own size added on EACH side before cutting.
@@ -71715,7 +72053,7 @@ function entryForRef(ref) {
71715
72053
  };
71716
72054
  }
71717
72055
  //#endregion
71718
- //#region ../system/dist/file-data-plane-CuE_hBli.mjs
72056
+ //#region ../system/dist/file-data-plane-BhKdxJgf.mjs
71719
72057
  function isNonEmptyFile(filePath) {
71720
72058
  return node_fs.existsSync(filePath) && node_fs.statSync(filePath).size > 0;
71721
72059
  }
@@ -71737,21 +72075,56 @@ function buildHeaders(url) {
71737
72075
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
71738
72076
  return headers;
71739
72077
  }
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) {
72078
+ var DEFAULT_MAX_REDIRECTS = 5;
72079
+ function normalizeDownloadOptions(third) {
72080
+ if (typeof third === "function") return { onProgress: third };
72081
+ return third ?? {};
72082
+ }
72083
+ function isRedirectStatus(status) {
72084
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
72085
+ }
72086
+ function resolveRedirectUrl(current, location) {
72087
+ return new URL(location, current);
72088
+ }
72089
+ async function downloadFile(url, destPath, onProgressOrOptions) {
71747
72090
  if (node_fs.existsSync(destPath)) return destPath;
72091
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
72092
+ const fetchImpl = opts.fetchImpl ?? fetch;
72093
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
71748
72094
  node_fs.mkdirSync(node_path$1.dirname(destPath), { recursive: true });
71749
72095
  const tmpPath = destPath + ".downloading";
71750
72096
  try {
71751
- const response = await fetch(url, {
71752
- redirect: "follow",
71753
- headers: buildHeaders(url)
71754
- });
72097
+ let current = url;
72098
+ const seen = /* @__PURE__ */ new Set();
72099
+ let response;
72100
+ const manual = opts.redirectPolicy !== void 0;
72101
+ for (let hop = 0; hop <= maxRedirects; hop++) {
72102
+ const parsed = new URL(current);
72103
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
72104
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
72105
+ seen.add(parsed.href);
72106
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
72107
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
72108
+ try {
72109
+ response = await fetchImpl(current, {
72110
+ redirect: manual ? "manual" : "follow",
72111
+ headers: buildHeaders(current),
72112
+ ...controller ? { signal: controller.signal } : {}
72113
+ });
72114
+ } finally {
72115
+ if (timer) clearTimeout(timer);
72116
+ }
72117
+ if (manual && isRedirectStatus(response.status)) {
72118
+ const location = response.headers.get("location");
72119
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
72120
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
72121
+ current = resolveRedirectUrl(current, location).href;
72122
+ continue;
72123
+ }
72124
+ break;
72125
+ }
72126
+ if (!response) throw new Error(`No response downloading ${url}`);
72127
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
71755
72128
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
71756
72129
  if (!response.body) throw new Error(`No response body from ${url}`);
71757
72130
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -71762,9 +72135,10 @@ async function downloadFile(url, destPath, onProgress) {
71762
72135
  for (;;) {
71763
72136
  const { done, value } = await reader.read();
71764
72137
  if (done || !value) break;
71765
- fileStream.write(value);
71766
72138
  downloaded += value.length;
71767
- onProgress?.(downloaded, total);
72139
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
72140
+ fileStream.write(value);
72141
+ opts.onProgress?.(downloaded, total);
71768
72142
  }
71769
72143
  } finally {
71770
72144
  fileStream.end();