@camstack/addon-ai 0.4.17 → 0.4.19

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 +184 -34
  2. package/dist/addon.mjs +184 -34
  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
  }
@@ -8318,6 +8328,12 @@ var ModelVariantGroupSchema = object({
8318
8328
  */
8319
8329
  resolution: number$1().int().positive().optional()
8320
8330
  });
8331
+ var ModelProviderIdSchema = _enum([
8332
+ "camstack",
8333
+ "frigate",
8334
+ "scrypted",
8335
+ "custom"
8336
+ ]);
8321
8337
  var ModelCatalogEntrySchema = object({
8322
8338
  id: string(),
8323
8339
  name: string(),
@@ -8415,6 +8431,12 @@ var ModelCatalogEntrySchema = object({
8415
8431
  */
8416
8432
  group: ModelVariantGroupSchema.optional(),
8417
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
+ /**
8418
8440
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8419
8441
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8420
8442
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11942,6 +11964,27 @@ var LinkedDeviceSchema = object({
11942
11964
  features: array(string()),
11943
11965
  producesTrackedEvents: boolean().optional()
11944
11966
  });
11967
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11968
+ * The batch answer needs the tag; the single-device answer already has it
11969
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11970
+ var LinkedDevicesForDeviceSchema = object({
11971
+ deviceId: number$1(),
11972
+ mode: LinkedDevicesModeSchema,
11973
+ devices: array(LinkedDeviceSchema)
11974
+ });
11975
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11976
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11977
+ * object literal is exactly how the three drift apart. */
11978
+ var DeviceBindingsForDeviceSchema = object({
11979
+ deviceId: number$1(),
11980
+ entries: array(object({
11981
+ capName: string(),
11982
+ kind: _enum(["native", "wrapped"]),
11983
+ providerAddonId: string(),
11984
+ providerNodeId: string(),
11985
+ nativeAddonId: string()
11986
+ }))
11987
+ });
11945
11988
  var SavedDeviceRowSchema = object({
11946
11989
  /** Numeric id reserved at allocateDeviceId time. */
11947
11990
  id: number$1(),
@@ -12167,11 +12210,25 @@ method(object({
12167
12210
  projection: _enum(["full", "slim"]).optional(),
12168
12211
  /** Return only camera devices. Filtering server-side instead of
12169
12212
  * shipping 293 rows to find 12. */
12170
- isCamera: boolean().optional()
12213
+ isCamera: boolean().optional(),
12214
+ /**
12215
+ * Return only these device ids. For the caller that already KNOWS the
12216
+ * handful it wants and needs a field the id-bearing answer does not
12217
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12218
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12219
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12220
+ * refetches on the reconcile interval, on a phone.
12221
+ *
12222
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12223
+ * keys rather than rejecting them (verified against the live hub
12224
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12225
+ * it answers today and the caller filters as it already does.
12226
+ */
12227
+ deviceIds: array(number$1()).optional()
12171
12228
  }), array(DeviceInfoSchema)), method(object({ deviceId: number$1() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number$1() }), array(DeviceInfoSchema)), method(object({ deviceId: number$1() }), object({
12172
12229
  mode: LinkedDevicesModeSchema,
12173
12230
  devices: array(LinkedDeviceSchema)
12174
- })), method(object({ deviceId: number$1() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number$1() }), array(ConfigEntrySchema)), method(object({ deviceId: number$1() }), ConfigUISchemaOutput), method(object({
12231
+ })), method(object({ deviceIds: array(number$1()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number$1() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number$1() }), array(ConfigEntrySchema)), method(object({ deviceId: number$1() }), ConfigUISchemaOutput), method(object({
12175
12232
  deviceId: number$1(),
12176
12233
  values: record(string(), unknown())
12177
12234
  }), object({ success: literal(true) }), {
@@ -12198,25 +12255,7 @@ method(object({
12198
12255
  }), method(object({ deviceId: number$1() }), array(StreamProbeResultSchema), {
12199
12256
  kind: "mutation",
12200
12257
  auth: "admin"
12201
- }), method(object({ deviceId: number$1() }), object({
12202
- deviceId: number$1(),
12203
- entries: array(object({
12204
- capName: string(),
12205
- kind: _enum(["native", "wrapped"]),
12206
- providerAddonId: string(),
12207
- providerNodeId: string(),
12208
- nativeAddonId: string()
12209
- }))
12210
- })), method(object({}), array(object({
12211
- deviceId: number$1(),
12212
- entries: array(object({
12213
- capName: string(),
12214
- kind: _enum(["native", "wrapped"]),
12215
- providerAddonId: string(),
12216
- providerNodeId: string(),
12217
- nativeAddonId: string()
12218
- }))
12219
- }))), method(object({
12258
+ }), method(object({ deviceId: number$1() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number$1()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12220
12259
  deviceId: number$1(),
12221
12260
  capName: string(),
12222
12261
  wrapperAddonId: string(),
@@ -14669,12 +14708,15 @@ var NcOccupancyConditionSchema = object({
14669
14708
  * there is no second switch that can disagree with the first and every rule
14670
14709
  * authored before the decision migrates for free (`audioModeOf`):
14671
14710
  *
14672
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14673
- * classifier labels with one of them. No window, no percentage:
14674
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14675
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14676
- * the analyzer's (`classificationMinScore`, per device) a label only
14677
- * reaches this condition if the classifier was already confident enough.
14711
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14712
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14713
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14714
+ * frames is the wrong question for a classifier that labels 1–3 frames
14715
+ * per episode. The count window is the brake that drops a single-frame
14716
+ * false positive; the rule's own `throttle` cooldown is the other. The
14717
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14718
+ * per device) — a label only reaches this condition if the classifier was
14719
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14678
14720
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14679
14721
  * the condition: at least `hitPercent`% of the samples over
14680
14722
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14701,14 +14743,22 @@ var NcOccupancyConditionSchema = object({
14701
14743
  * an operator who typed `dog` mean the same thing.
14702
14744
  */
14703
14745
  var NcAudioConditionSchema = object({
14704
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14746
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14705
14747
  labels: array(string().min(1)).min(1).optional(),
14706
14748
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14707
14749
  dbThreshold: number$1().min(-96).max(0).optional(),
14708
14750
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14709
14751
  hitPercent: number$1().int().min(1).max(100).default(60),
14710
14752
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14711
- samplingSeconds: number$1().int().min(1).max(300).default(10)
14753
+ samplingSeconds: number$1().int().min(1).max(300).default(10),
14754
+ /**
14755
+ * LABEL MODE: how many labelled frames must land inside
14756
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14757
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14758
+ */
14759
+ confirmHits: number$1().int().min(1).max(20).optional(),
14760
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14761
+ confirmWindowSec: number$1().int().min(1).max(60).optional()
14712
14762
  });
14713
14763
  /**
14714
14764
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17082,6 +17132,46 @@ var RecentTracksPageSchema = object({
17082
17132
  /** Cursor for the next page, or null when this page is the last. */
17083
17133
  nextCursor: string().nullable()
17084
17134
  });
17135
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17136
+ var LIST_GROUPS_MAX_LIMIT = 100;
17137
+ var AnalyticsGroupRecordSchema = object({
17138
+ id: string(),
17139
+ deviceId: number$1().int(),
17140
+ openedAt: number$1().int(),
17141
+ closedAt: number$1().int(),
17142
+ timestamp: number$1().int(),
17143
+ memberCount: number$1().int(),
17144
+ memberTrackIds: array(string()).readonly(),
17145
+ className: string(),
17146
+ classes: array(string()).readonly(),
17147
+ /** Relative event-media path, or null when the group has no picture yet. */
17148
+ mediaUrl: string().nullable(),
17149
+ singleton: boolean()
17150
+ });
17151
+ var AnalyticsGroupMemberSchema = object({
17152
+ trackId: string(),
17153
+ deviceId: number$1().int(),
17154
+ className: string(),
17155
+ firstSeen: number$1().int(),
17156
+ lastSeen: number$1().int(),
17157
+ mediaUrl: string().nullable()
17158
+ });
17159
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17160
+ var ListGroupsQueryInput = object({
17161
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17162
+ deviceIds: array(number$1()),
17163
+ /** Window lower bound on `closedAt` (inclusive). */
17164
+ since: number$1().optional(),
17165
+ /** Window upper bound on `openedAt` (inclusive). */
17166
+ until: number$1().optional(),
17167
+ limit: number$1().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17168
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17169
+ cursor: string().optional()
17170
+ });
17171
+ var ListGroupsPageSchema = object({
17172
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17173
+ nextCursor: string().nullable()
17174
+ });
17085
17175
  var KeyEventQueryInput = object({
17086
17176
  deviceId: number$1(),
17087
17177
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17157,7 +17247,9 @@ var TrackCascadeCountsSchema = object({
17157
17247
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17158
17248
  plates: number$1().int(),
17159
17249
  /** Per-track CLIP search vectors removed (best-effort). */
17160
- embeddings: number$1().int()
17250
+ embeddings: number$1().int(),
17251
+ /** Group membership + group rows removed with their last member (best-effort). */
17252
+ groups: number$1().int()
17161
17253
  });
17162
17254
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17163
17255
  var DiskReconcileCountsSchema = object({
@@ -17303,7 +17395,10 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
17303
17395
  * stationary registry). Default false: the timeline lists passages,
17304
17396
  * not parking records (operator decision, 2026-08-15). */
17305
17397
  includeStationary: boolean().optional()
17306
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number$1() }), _void(), {
17398
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17399
+ deviceId: number$1(),
17400
+ groupId: string().min(1)
17401
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number$1() }), _void(), {
17307
17402
  kind: "mutation",
17308
17403
  auth: "admin"
17309
17404
  }), 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({
@@ -17613,7 +17708,8 @@ var PipelineModelOptionSchema = object({
17613
17708
  sizeMB: number$1()
17614
17709
  })),
17615
17710
  group: ModelVariantGroupSchema.optional(),
17616
- legacy: boolean().optional()
17711
+ legacy: boolean().optional(),
17712
+ provider: ModelProviderIdSchema.optional()
17617
17713
  });
17618
17714
  var ConfigFieldBridge = custom();
17619
17715
  var PipelineAddonSchemaSchema = object({
@@ -24197,7 +24293,12 @@ var PlateInfoSchema = object({
24197
24293
  plateBbox: BoundingBoxSchema.optional(),
24198
24294
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24199
24295
  keyFrameMediaKey: string().optional(),
24200
- base64: string().optional()
24296
+ base64: string().optional(),
24297
+ /**
24298
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24299
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24300
+ */
24301
+ cropUrl: string().optional()
24201
24302
  });
24202
24303
  var MediaFileLiteSchema = object({
24203
24304
  key: string(),
@@ -27837,6 +27938,12 @@ Object.freeze({
27837
27938
  addonId: null,
27838
27939
  access: "view"
27839
27940
  },
27941
+ "deviceManager.getBindingsBatch": {
27942
+ capName: "device-manager",
27943
+ capScope: "system",
27944
+ addonId: null,
27945
+ access: "view"
27946
+ },
27840
27947
  "deviceManager.getChildren": {
27841
27948
  capName: "device-manager",
27842
27949
  capScope: "system",
@@ -27897,6 +28004,12 @@ Object.freeze({
27897
28004
  addonId: null,
27898
28005
  access: "view"
27899
28006
  },
28007
+ "deviceManager.getLinkedDevicesBatch": {
28008
+ capName: "device-manager",
28009
+ capScope: "system",
28010
+ addonId: null,
28011
+ access: "view"
28012
+ },
27900
28013
  "deviceManager.getRoleDisplayDefaults": {
27901
28014
  capName: "device-manager",
27902
28015
  capScope: "system",
@@ -29667,6 +29780,12 @@ Object.freeze({
29667
29780
  addonId: null,
29668
29781
  access: "view"
29669
29782
  },
29783
+ "pipelineAnalytics.getGroup": {
29784
+ capName: "pipeline-analytics",
29785
+ capScope: "device",
29786
+ addonId: null,
29787
+ access: "view"
29788
+ },
29670
29789
  "pipelineAnalytics.getKeyEvents": {
29671
29790
  capName: "pipeline-analytics",
29672
29791
  capScope: "device",
@@ -29751,6 +29870,12 @@ Object.freeze({
29751
29870
  addonId: null,
29752
29871
  access: "view"
29753
29872
  },
29873
+ "pipelineAnalytics.listGroups": {
29874
+ capName: "pipeline-analytics",
29875
+ capScope: "device",
29876
+ addonId: null,
29877
+ access: "view"
29878
+ },
29754
29879
  "pipelineAnalytics.listOpsLog": {
29755
29880
  capName: "pipeline-analytics",
29756
29881
  capScope: "device",
@@ -32532,6 +32657,11 @@ Object.freeze({
32532
32657
  form: "single",
32533
32658
  optional: false
32534
32659
  }],
32660
+ "deviceManager.getBindingsBatch": [{
32661
+ name: "deviceIds",
32662
+ form: "array",
32663
+ optional: false
32664
+ }],
32535
32665
  "deviceManager.getChildren": [{
32536
32666
  name: "parentDeviceId",
32537
32667
  form: "single",
@@ -32577,6 +32707,11 @@ Object.freeze({
32577
32707
  form: "single",
32578
32708
  optional: false
32579
32709
  }],
32710
+ "deviceManager.getLinkedDevicesBatch": [{
32711
+ name: "deviceIds",
32712
+ form: "array",
32713
+ optional: false
32714
+ }],
32580
32715
  "deviceManager.getSettingsSchema": [{
32581
32716
  name: "deviceId",
32582
32717
  form: "single",
@@ -32597,6 +32732,11 @@ Object.freeze({
32597
32732
  form: "single",
32598
32733
  optional: false
32599
32734
  }],
32735
+ "deviceManager.listAll": [{
32736
+ name: "deviceIds",
32737
+ form: "array",
32738
+ optional: true
32739
+ }],
32600
32740
  "deviceManager.loadConfig": [{
32601
32741
  name: "deviceId",
32602
32742
  form: "single",
@@ -33170,6 +33310,11 @@ Object.freeze({
33170
33310
  form: "single",
33171
33311
  optional: false
33172
33312
  }],
33313
+ "pipelineAnalytics.getGroup": [{
33314
+ name: "deviceId",
33315
+ form: "single",
33316
+ optional: false
33317
+ }],
33173
33318
  "pipelineAnalytics.getKeyEvents": [{
33174
33319
  name: "deviceId",
33175
33320
  form: "single",
@@ -33225,6 +33370,11 @@ Object.freeze({
33225
33370
  form: "array",
33226
33371
  optional: false
33227
33372
  }],
33373
+ "pipelineAnalytics.listGroups": [{
33374
+ name: "deviceIds",
33375
+ form: "array",
33376
+ optional: false
33377
+ }],
33228
33378
  "pipelineAnalytics.listOpsLog": [{
33229
33379
  name: "deviceId",
33230
33380
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5990,6 +5990,13 @@ var BaseAddon = class {
5990
5990
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5991
5991
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5992
5992
  _registeredCapNames = [];
5993
+ /**
5994
+ * True only after `readAddonStore` actually answered. Constructor
5995
+ * defaults look like stored config when the store is down — a forked
5996
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5997
+ * mode, 2026-08-25) is not "the operator chose this".
5998
+ */
5999
+ settingsStoreReady = false;
5993
6000
  /** Default config values. Provided via constructor. */
5994
6001
  defaults;
5995
6002
  constructor(defaults) {
@@ -6390,7 +6397,9 @@ var BaseAddon = class {
6390
6397
  ];
6391
6398
  let lastErr;
6392
6399
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6393
- return await settings.readAddonStore() ?? {};
6400
+ const stored = await settings.readAddonStore() ?? {};
6401
+ this.settingsStoreReady = true;
6402
+ return stored;
6394
6403
  } catch (err) {
6395
6404
  lastErr = err;
6396
6405
  const msg = err instanceof Error ? err.message : String(err);
@@ -6398,6 +6407,7 @@ var BaseAddon = class {
6398
6407
  if (attempt === delaysMs.length) break;
6399
6408
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6400
6409
  }
6410
+ this.settingsStoreReady = false;
6401
6411
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6402
6412
  return {};
6403
6413
  }
@@ -8344,6 +8354,12 @@ var ModelVariantGroupSchema = object({
8344
8354
  */
8345
8355
  resolution: number$1().int().positive().optional()
8346
8356
  });
8357
+ var ModelProviderIdSchema = _enum([
8358
+ "camstack",
8359
+ "frigate",
8360
+ "scrypted",
8361
+ "custom"
8362
+ ]);
8347
8363
  var ModelCatalogEntrySchema = object({
8348
8364
  id: string(),
8349
8365
  name: string(),
@@ -8441,6 +8457,12 @@ var ModelCatalogEntrySchema = object({
8441
8457
  */
8442
8458
  group: ModelVariantGroupSchema.optional(),
8443
8459
  /**
8460
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8461
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8462
+ * persisted before this field existed (`inferModelProvider` fills those).
8463
+ */
8464
+ provider: ModelProviderIdSchema.optional(),
8465
+ /**
8444
8466
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8445
8467
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8446
8468
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11968,6 +11990,27 @@ var LinkedDeviceSchema = object({
11968
11990
  features: array(string()),
11969
11991
  producesTrackedEvents: boolean().optional()
11970
11992
  });
11993
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11994
+ * The batch answer needs the tag; the single-device answer already has it
11995
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11996
+ var LinkedDevicesForDeviceSchema = object({
11997
+ deviceId: number$1(),
11998
+ mode: LinkedDevicesModeSchema,
11999
+ devices: array(LinkedDeviceSchema)
12000
+ });
12001
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12002
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12003
+ * object literal is exactly how the three drift apart. */
12004
+ var DeviceBindingsForDeviceSchema = object({
12005
+ deviceId: number$1(),
12006
+ entries: array(object({
12007
+ capName: string(),
12008
+ kind: _enum(["native", "wrapped"]),
12009
+ providerAddonId: string(),
12010
+ providerNodeId: string(),
12011
+ nativeAddonId: string()
12012
+ }))
12013
+ });
11971
12014
  var SavedDeviceRowSchema = object({
11972
12015
  /** Numeric id reserved at allocateDeviceId time. */
11973
12016
  id: number$1(),
@@ -12193,11 +12236,25 @@ method(object({
12193
12236
  projection: _enum(["full", "slim"]).optional(),
12194
12237
  /** Return only camera devices. Filtering server-side instead of
12195
12238
  * shipping 293 rows to find 12. */
12196
- isCamera: boolean().optional()
12239
+ isCamera: boolean().optional(),
12240
+ /**
12241
+ * Return only these device ids. For the caller that already KNOWS the
12242
+ * handful it wants and needs a field the id-bearing answer does not
12243
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12244
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12245
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12246
+ * refetches on the reconcile interval, on a phone.
12247
+ *
12248
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12249
+ * keys rather than rejecting them (verified against the live hub
12250
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12251
+ * it answers today and the caller filters as it already does.
12252
+ */
12253
+ deviceIds: array(number$1()).optional()
12197
12254
  }), array(DeviceInfoSchema)), method(object({ deviceId: number$1() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number$1() }), array(DeviceInfoSchema)), method(object({ deviceId: number$1() }), object({
12198
12255
  mode: LinkedDevicesModeSchema,
12199
12256
  devices: array(LinkedDeviceSchema)
12200
- })), method(object({ deviceId: number$1() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number$1() }), array(ConfigEntrySchema)), method(object({ deviceId: number$1() }), ConfigUISchemaOutput), method(object({
12257
+ })), method(object({ deviceIds: array(number$1()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number$1() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number$1() }), array(ConfigEntrySchema)), method(object({ deviceId: number$1() }), ConfigUISchemaOutput), method(object({
12201
12258
  deviceId: number$1(),
12202
12259
  values: record(string(), unknown())
12203
12260
  }), object({ success: literal(true) }), {
@@ -12224,25 +12281,7 @@ method(object({
12224
12281
  }), method(object({ deviceId: number$1() }), array(StreamProbeResultSchema), {
12225
12282
  kind: "mutation",
12226
12283
  auth: "admin"
12227
- }), method(object({ deviceId: number$1() }), object({
12228
- deviceId: number$1(),
12229
- entries: array(object({
12230
- capName: string(),
12231
- kind: _enum(["native", "wrapped"]),
12232
- providerAddonId: string(),
12233
- providerNodeId: string(),
12234
- nativeAddonId: string()
12235
- }))
12236
- })), method(object({}), array(object({
12237
- deviceId: number$1(),
12238
- entries: array(object({
12239
- capName: string(),
12240
- kind: _enum(["native", "wrapped"]),
12241
- providerAddonId: string(),
12242
- providerNodeId: string(),
12243
- nativeAddonId: string()
12244
- }))
12245
- }))), method(object({
12284
+ }), method(object({ deviceId: number$1() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number$1()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12246
12285
  deviceId: number$1(),
12247
12286
  capName: string(),
12248
12287
  wrapperAddonId: string(),
@@ -14695,12 +14734,15 @@ var NcOccupancyConditionSchema = object({
14695
14734
  * there is no second switch that can disagree with the first and every rule
14696
14735
  * authored before the decision migrates for free (`audioModeOf`):
14697
14736
  *
14698
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14699
- * classifier labels with one of them. No window, no percentage:
14700
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14701
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14702
- * the analyzer's (`classificationMinScore`, per device) a label only
14703
- * reaches this condition if the classifier was already confident enough.
14737
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14738
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14739
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14740
+ * frames is the wrong question for a classifier that labels 1–3 frames
14741
+ * per episode. The count window is the brake that drops a single-frame
14742
+ * false positive; the rule's own `throttle` cooldown is the other. The
14743
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14744
+ * per device) — a label only reaches this condition if the classifier was
14745
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14704
14746
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14705
14747
  * the condition: at least `hitPercent`% of the samples over
14706
14748
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14727,14 +14769,22 @@ var NcOccupancyConditionSchema = object({
14727
14769
  * an operator who typed `dog` mean the same thing.
14728
14770
  */
14729
14771
  var NcAudioConditionSchema = object({
14730
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14772
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14731
14773
  labels: array(string().min(1)).min(1).optional(),
14732
14774
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14733
14775
  dbThreshold: number$1().min(-96).max(0).optional(),
14734
14776
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14735
14777
  hitPercent: number$1().int().min(1).max(100).default(60),
14736
14778
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14737
- samplingSeconds: number$1().int().min(1).max(300).default(10)
14779
+ samplingSeconds: number$1().int().min(1).max(300).default(10),
14780
+ /**
14781
+ * LABEL MODE: how many labelled frames must land inside
14782
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14783
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14784
+ */
14785
+ confirmHits: number$1().int().min(1).max(20).optional(),
14786
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14787
+ confirmWindowSec: number$1().int().min(1).max(60).optional()
14738
14788
  });
14739
14789
  /**
14740
14790
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17108,6 +17158,46 @@ var RecentTracksPageSchema = object({
17108
17158
  /** Cursor for the next page, or null when this page is the last. */
17109
17159
  nextCursor: string().nullable()
17110
17160
  });
17161
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17162
+ var LIST_GROUPS_MAX_LIMIT = 100;
17163
+ var AnalyticsGroupRecordSchema = object({
17164
+ id: string(),
17165
+ deviceId: number$1().int(),
17166
+ openedAt: number$1().int(),
17167
+ closedAt: number$1().int(),
17168
+ timestamp: number$1().int(),
17169
+ memberCount: number$1().int(),
17170
+ memberTrackIds: array(string()).readonly(),
17171
+ className: string(),
17172
+ classes: array(string()).readonly(),
17173
+ /** Relative event-media path, or null when the group has no picture yet. */
17174
+ mediaUrl: string().nullable(),
17175
+ singleton: boolean()
17176
+ });
17177
+ var AnalyticsGroupMemberSchema = object({
17178
+ trackId: string(),
17179
+ deviceId: number$1().int(),
17180
+ className: string(),
17181
+ firstSeen: number$1().int(),
17182
+ lastSeen: number$1().int(),
17183
+ mediaUrl: string().nullable()
17184
+ });
17185
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17186
+ var ListGroupsQueryInput = object({
17187
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17188
+ deviceIds: array(number$1()),
17189
+ /** Window lower bound on `closedAt` (inclusive). */
17190
+ since: number$1().optional(),
17191
+ /** Window upper bound on `openedAt` (inclusive). */
17192
+ until: number$1().optional(),
17193
+ limit: number$1().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17194
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17195
+ cursor: string().optional()
17196
+ });
17197
+ var ListGroupsPageSchema = object({
17198
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17199
+ nextCursor: string().nullable()
17200
+ });
17111
17201
  var KeyEventQueryInput = object({
17112
17202
  deviceId: number$1(),
17113
17203
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17183,7 +17273,9 @@ var TrackCascadeCountsSchema = object({
17183
17273
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17184
17274
  plates: number$1().int(),
17185
17275
  /** Per-track CLIP search vectors removed (best-effort). */
17186
- embeddings: number$1().int()
17276
+ embeddings: number$1().int(),
17277
+ /** Group membership + group rows removed with their last member (best-effort). */
17278
+ groups: number$1().int()
17187
17279
  });
17188
17280
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17189
17281
  var DiskReconcileCountsSchema = object({
@@ -17329,7 +17421,10 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
17329
17421
  * stationary registry). Default false: the timeline lists passages,
17330
17422
  * not parking records (operator decision, 2026-08-15). */
17331
17423
  includeStationary: boolean().optional()
17332
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number$1() }), _void(), {
17424
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17425
+ deviceId: number$1(),
17426
+ groupId: string().min(1)
17427
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number$1() }), _void(), {
17333
17428
  kind: "mutation",
17334
17429
  auth: "admin"
17335
17430
  }), 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({
@@ -17639,7 +17734,8 @@ var PipelineModelOptionSchema = object({
17639
17734
  sizeMB: number$1()
17640
17735
  })),
17641
17736
  group: ModelVariantGroupSchema.optional(),
17642
- legacy: boolean().optional()
17737
+ legacy: boolean().optional(),
17738
+ provider: ModelProviderIdSchema.optional()
17643
17739
  });
17644
17740
  var ConfigFieldBridge = custom();
17645
17741
  var PipelineAddonSchemaSchema = object({
@@ -24223,7 +24319,12 @@ var PlateInfoSchema = object({
24223
24319
  plateBbox: BoundingBoxSchema.optional(),
24224
24320
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24225
24321
  keyFrameMediaKey: string().optional(),
24226
- base64: string().optional()
24322
+ base64: string().optional(),
24323
+ /**
24324
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24325
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24326
+ */
24327
+ cropUrl: string().optional()
24227
24328
  });
24228
24329
  var MediaFileLiteSchema = object({
24229
24330
  key: string(),
@@ -27863,6 +27964,12 @@ Object.freeze({
27863
27964
  addonId: null,
27864
27965
  access: "view"
27865
27966
  },
27967
+ "deviceManager.getBindingsBatch": {
27968
+ capName: "device-manager",
27969
+ capScope: "system",
27970
+ addonId: null,
27971
+ access: "view"
27972
+ },
27866
27973
  "deviceManager.getChildren": {
27867
27974
  capName: "device-manager",
27868
27975
  capScope: "system",
@@ -27923,6 +28030,12 @@ Object.freeze({
27923
28030
  addonId: null,
27924
28031
  access: "view"
27925
28032
  },
28033
+ "deviceManager.getLinkedDevicesBatch": {
28034
+ capName: "device-manager",
28035
+ capScope: "system",
28036
+ addonId: null,
28037
+ access: "view"
28038
+ },
27926
28039
  "deviceManager.getRoleDisplayDefaults": {
27927
28040
  capName: "device-manager",
27928
28041
  capScope: "system",
@@ -29693,6 +29806,12 @@ Object.freeze({
29693
29806
  addonId: null,
29694
29807
  access: "view"
29695
29808
  },
29809
+ "pipelineAnalytics.getGroup": {
29810
+ capName: "pipeline-analytics",
29811
+ capScope: "device",
29812
+ addonId: null,
29813
+ access: "view"
29814
+ },
29696
29815
  "pipelineAnalytics.getKeyEvents": {
29697
29816
  capName: "pipeline-analytics",
29698
29817
  capScope: "device",
@@ -29777,6 +29896,12 @@ Object.freeze({
29777
29896
  addonId: null,
29778
29897
  access: "view"
29779
29898
  },
29899
+ "pipelineAnalytics.listGroups": {
29900
+ capName: "pipeline-analytics",
29901
+ capScope: "device",
29902
+ addonId: null,
29903
+ access: "view"
29904
+ },
29780
29905
  "pipelineAnalytics.listOpsLog": {
29781
29906
  capName: "pipeline-analytics",
29782
29907
  capScope: "device",
@@ -32558,6 +32683,11 @@ Object.freeze({
32558
32683
  form: "single",
32559
32684
  optional: false
32560
32685
  }],
32686
+ "deviceManager.getBindingsBatch": [{
32687
+ name: "deviceIds",
32688
+ form: "array",
32689
+ optional: false
32690
+ }],
32561
32691
  "deviceManager.getChildren": [{
32562
32692
  name: "parentDeviceId",
32563
32693
  form: "single",
@@ -32603,6 +32733,11 @@ Object.freeze({
32603
32733
  form: "single",
32604
32734
  optional: false
32605
32735
  }],
32736
+ "deviceManager.getLinkedDevicesBatch": [{
32737
+ name: "deviceIds",
32738
+ form: "array",
32739
+ optional: false
32740
+ }],
32606
32741
  "deviceManager.getSettingsSchema": [{
32607
32742
  name: "deviceId",
32608
32743
  form: "single",
@@ -32623,6 +32758,11 @@ Object.freeze({
32623
32758
  form: "single",
32624
32759
  optional: false
32625
32760
  }],
32761
+ "deviceManager.listAll": [{
32762
+ name: "deviceIds",
32763
+ form: "array",
32764
+ optional: true
32765
+ }],
32626
32766
  "deviceManager.loadConfig": [{
32627
32767
  name: "deviceId",
32628
32768
  form: "single",
@@ -33196,6 +33336,11 @@ Object.freeze({
33196
33336
  form: "single",
33197
33337
  optional: false
33198
33338
  }],
33339
+ "pipelineAnalytics.getGroup": [{
33340
+ name: "deviceId",
33341
+ form: "single",
33342
+ optional: false
33343
+ }],
33199
33344
  "pipelineAnalytics.getKeyEvents": [{
33200
33345
  name: "deviceId",
33201
33346
  form: "single",
@@ -33251,6 +33396,11 @@ Object.freeze({
33251
33396
  form: "array",
33252
33397
  optional: false
33253
33398
  }],
33399
+ "pipelineAnalytics.listGroups": [{
33400
+ name: "deviceIds",
33401
+ form: "array",
33402
+ optional: false
33403
+ }],
33254
33404
  "pipelineAnalytics.listOpsLog": [{
33255
33405
  name: "deviceId",
33256
33406
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.17",
3
+ "version": "0.4.19",
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",