@camstack/addon-terminal 0.1.32 → 0.1.33

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
@@ -5833,6 +5833,13 @@ var BaseAddon = class {
5833
5833
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5834
5834
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5835
5835
  _registeredCapNames = [];
5836
+ /**
5837
+ * True only after `readAddonStore` actually answered. Constructor
5838
+ * defaults look like stored config when the store is down — a forked
5839
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5840
+ * mode, 2026-08-25) is not "the operator chose this".
5841
+ */
5842
+ settingsStoreReady = false;
5836
5843
  /** Default config values. Provided via constructor. */
5837
5844
  defaults;
5838
5845
  constructor(defaults) {
@@ -6233,7 +6240,9 @@ var BaseAddon = class {
6233
6240
  ];
6234
6241
  let lastErr;
6235
6242
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6236
- return await settings.readAddonStore() ?? {};
6243
+ const stored = await settings.readAddonStore() ?? {};
6244
+ this.settingsStoreReady = true;
6245
+ return stored;
6237
6246
  } catch (err) {
6238
6247
  lastErr = err;
6239
6248
  const msg = err instanceof Error ? err.message : String(err);
@@ -6241,6 +6250,7 @@ var BaseAddon = class {
6241
6250
  if (attempt === delaysMs.length) break;
6242
6251
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6243
6252
  }
6253
+ this.settingsStoreReady = false;
6244
6254
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6245
6255
  return {};
6246
6256
  }
@@ -8211,6 +8221,12 @@ var ModelVariantGroupSchema = object({
8211
8221
  */
8212
8222
  resolution: number().int().positive().optional()
8213
8223
  });
8224
+ var ModelProviderIdSchema = _enum([
8225
+ "camstack",
8226
+ "frigate",
8227
+ "scrypted",
8228
+ "custom"
8229
+ ]);
8214
8230
  var ModelCatalogEntrySchema = object({
8215
8231
  id: string(),
8216
8232
  name: string(),
@@ -8308,6 +8324,12 @@ var ModelCatalogEntrySchema = object({
8308
8324
  */
8309
8325
  group: ModelVariantGroupSchema.optional(),
8310
8326
  /**
8327
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8328
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8329
+ * persisted before this field existed (`inferModelProvider` fills those).
8330
+ */
8331
+ provider: ModelProviderIdSchema.optional(),
8332
+ /**
8311
8333
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8312
8334
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8313
8335
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12044,6 +12066,27 @@ var LinkedDeviceSchema = object({
12044
12066
  features: array(string()),
12045
12067
  producesTrackedEvents: boolean().optional()
12046
12068
  });
12069
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12070
+ * The batch answer needs the tag; the single-device answer already has it
12071
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12072
+ var LinkedDevicesForDeviceSchema = object({
12073
+ deviceId: number(),
12074
+ mode: LinkedDevicesModeSchema,
12075
+ devices: array(LinkedDeviceSchema)
12076
+ });
12077
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12078
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12079
+ * object literal is exactly how the three drift apart. */
12080
+ var DeviceBindingsForDeviceSchema = object({
12081
+ deviceId: number(),
12082
+ entries: array(object({
12083
+ capName: string(),
12084
+ kind: _enum(["native", "wrapped"]),
12085
+ providerAddonId: string(),
12086
+ providerNodeId: string(),
12087
+ nativeAddonId: string()
12088
+ }))
12089
+ });
12047
12090
  var SavedDeviceRowSchema = object({
12048
12091
  /** Numeric id reserved at allocateDeviceId time. */
12049
12092
  id: number(),
@@ -12269,11 +12312,25 @@ method(object({
12269
12312
  projection: _enum(["full", "slim"]).optional(),
12270
12313
  /** Return only camera devices. Filtering server-side instead of
12271
12314
  * shipping 293 rows to find 12. */
12272
- isCamera: boolean().optional()
12315
+ isCamera: boolean().optional(),
12316
+ /**
12317
+ * Return only these device ids. For the caller that already KNOWS the
12318
+ * handful it wants and needs a field the id-bearing answer does not
12319
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12320
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12321
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12322
+ * refetches on the reconcile interval, on a phone.
12323
+ *
12324
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12325
+ * keys rather than rejecting them (verified against the live hub
12326
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12327
+ * it answers today and the caller filters as it already does.
12328
+ */
12329
+ deviceIds: array(number()).optional()
12273
12330
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12274
12331
  mode: LinkedDevicesModeSchema,
12275
12332
  devices: array(LinkedDeviceSchema)
12276
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12333
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12277
12334
  deviceId: number(),
12278
12335
  values: record(string(), unknown())
12279
12336
  }), object({ success: literal(true) }), {
@@ -12300,25 +12357,7 @@ method(object({
12300
12357
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12301
12358
  kind: "mutation",
12302
12359
  auth: "admin"
12303
- }), method(object({ deviceId: number() }), object({
12304
- deviceId: number(),
12305
- entries: array(object({
12306
- capName: string(),
12307
- kind: _enum(["native", "wrapped"]),
12308
- providerAddonId: string(),
12309
- providerNodeId: string(),
12310
- nativeAddonId: string()
12311
- }))
12312
- })), method(object({}), array(object({
12313
- deviceId: number(),
12314
- entries: array(object({
12315
- capName: string(),
12316
- kind: _enum(["native", "wrapped"]),
12317
- providerAddonId: string(),
12318
- providerNodeId: string(),
12319
- nativeAddonId: string()
12320
- }))
12321
- }))), method(object({
12360
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12322
12361
  deviceId: number(),
12323
12362
  capName: string(),
12324
12363
  wrapperAddonId: string(),
@@ -14728,12 +14767,15 @@ var NcOccupancyConditionSchema = object({
14728
14767
  * there is no second switch that can disagree with the first and every rule
14729
14768
  * authored before the decision migrates for free (`audioModeOf`):
14730
14769
  *
14731
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14732
- * classifier labels with one of them. No window, no percentage:
14733
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14734
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14735
- * the analyzer's (`classificationMinScore`, per device) a label only
14736
- * reaches this condition if the classifier was already confident enough.
14770
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14771
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14772
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14773
+ * frames is the wrong question for a classifier that labels 1–3 frames
14774
+ * per episode. The count window is the brake that drops a single-frame
14775
+ * false positive; the rule's own `throttle` cooldown is the other. The
14776
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14777
+ * per device) — a label only reaches this condition if the classifier was
14778
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14737
14779
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14738
14780
  * the condition: at least `hitPercent`% of the samples over
14739
14781
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14760,14 +14802,22 @@ var NcOccupancyConditionSchema = object({
14760
14802
  * an operator who typed `dog` mean the same thing.
14761
14803
  */
14762
14804
  var NcAudioConditionSchema = object({
14763
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14805
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14764
14806
  labels: array(string().min(1)).min(1).optional(),
14765
14807
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14766
14808
  dbThreshold: number().min(-96).max(0).optional(),
14767
14809
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14768
14810
  hitPercent: number().int().min(1).max(100).default(60),
14769
14811
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14770
- samplingSeconds: number().int().min(1).max(300).default(10)
14812
+ samplingSeconds: number().int().min(1).max(300).default(10),
14813
+ /**
14814
+ * LABEL MODE: how many labelled frames must land inside
14815
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14816
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14817
+ */
14818
+ confirmHits: number().int().min(1).max(20).optional(),
14819
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14820
+ confirmWindowSec: number().int().min(1).max(60).optional()
14771
14821
  });
14772
14822
  /**
14773
14823
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17141,6 +17191,46 @@ var RecentTracksPageSchema = object({
17141
17191
  /** Cursor for the next page, or null when this page is the last. */
17142
17192
  nextCursor: string().nullable()
17143
17193
  });
17194
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17195
+ var LIST_GROUPS_MAX_LIMIT = 100;
17196
+ var AnalyticsGroupRecordSchema = object({
17197
+ id: string(),
17198
+ deviceId: number().int(),
17199
+ openedAt: number().int(),
17200
+ closedAt: number().int(),
17201
+ timestamp: number().int(),
17202
+ memberCount: number().int(),
17203
+ memberTrackIds: array(string()).readonly(),
17204
+ className: string(),
17205
+ classes: array(string()).readonly(),
17206
+ /** Relative event-media path, or null when the group has no picture yet. */
17207
+ mediaUrl: string().nullable(),
17208
+ singleton: boolean()
17209
+ });
17210
+ var AnalyticsGroupMemberSchema = object({
17211
+ trackId: string(),
17212
+ deviceId: number().int(),
17213
+ className: string(),
17214
+ firstSeen: number().int(),
17215
+ lastSeen: number().int(),
17216
+ mediaUrl: string().nullable()
17217
+ });
17218
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17219
+ var ListGroupsQueryInput = object({
17220
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17221
+ deviceIds: array(number()),
17222
+ /** Window lower bound on `closedAt` (inclusive). */
17223
+ since: number().optional(),
17224
+ /** Window upper bound on `openedAt` (inclusive). */
17225
+ until: number().optional(),
17226
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17227
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17228
+ cursor: string().optional()
17229
+ });
17230
+ var ListGroupsPageSchema = object({
17231
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17232
+ nextCursor: string().nullable()
17233
+ });
17144
17234
  var KeyEventQueryInput = object({
17145
17235
  deviceId: number(),
17146
17236
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17216,7 +17306,9 @@ var TrackCascadeCountsSchema = object({
17216
17306
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17217
17307
  plates: number().int(),
17218
17308
  /** Per-track CLIP search vectors removed (best-effort). */
17219
- embeddings: number().int()
17309
+ embeddings: number().int(),
17310
+ /** Group membership + group rows removed with their last member (best-effort). */
17311
+ groups: number().int()
17220
17312
  });
17221
17313
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17222
17314
  var DiskReconcileCountsSchema = object({
@@ -17362,7 +17454,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17362
17454
  * stationary registry). Default false: the timeline lists passages,
17363
17455
  * not parking records (operator decision, 2026-08-15). */
17364
17456
  includeStationary: boolean().optional()
17365
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17457
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17458
+ deviceId: number(),
17459
+ groupId: string().min(1)
17460
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17366
17461
  kind: "mutation",
17367
17462
  auth: "admin"
17368
17463
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17672,7 +17767,8 @@ var PipelineModelOptionSchema = object({
17672
17767
  sizeMB: number()
17673
17768
  })),
17674
17769
  group: ModelVariantGroupSchema.optional(),
17675
- legacy: boolean().optional()
17770
+ legacy: boolean().optional(),
17771
+ provider: ModelProviderIdSchema.optional()
17676
17772
  });
17677
17773
  var ConfigFieldBridge = custom();
17678
17774
  var PipelineAddonSchemaSchema = object({
@@ -25917,7 +26013,12 @@ var PlateInfoSchema = object({
25917
26013
  plateBbox: BoundingBoxSchema.optional(),
25918
26014
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25919
26015
  keyFrameMediaKey: string().optional(),
25920
- base64: string().optional()
26016
+ base64: string().optional(),
26017
+ /**
26018
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
26019
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
26020
+ */
26021
+ cropUrl: string().optional()
25921
26022
  });
25922
26023
  var MediaFileLiteSchema = object({
25923
26024
  key: string(),
@@ -31439,6 +31540,12 @@ Object.freeze({
31439
31540
  addonId: null,
31440
31541
  access: "view"
31441
31542
  },
31543
+ "deviceManager.getBindingsBatch": {
31544
+ capName: "device-manager",
31545
+ capScope: "system",
31546
+ addonId: null,
31547
+ access: "view"
31548
+ },
31442
31549
  "deviceManager.getChildren": {
31443
31550
  capName: "device-manager",
31444
31551
  capScope: "system",
@@ -31499,6 +31606,12 @@ Object.freeze({
31499
31606
  addonId: null,
31500
31607
  access: "view"
31501
31608
  },
31609
+ "deviceManager.getLinkedDevicesBatch": {
31610
+ capName: "device-manager",
31611
+ capScope: "system",
31612
+ addonId: null,
31613
+ access: "view"
31614
+ },
31502
31615
  "deviceManager.getRoleDisplayDefaults": {
31503
31616
  capName: "device-manager",
31504
31617
  capScope: "system",
@@ -33269,6 +33382,12 @@ Object.freeze({
33269
33382
  addonId: null,
33270
33383
  access: "view"
33271
33384
  },
33385
+ "pipelineAnalytics.getGroup": {
33386
+ capName: "pipeline-analytics",
33387
+ capScope: "device",
33388
+ addonId: null,
33389
+ access: "view"
33390
+ },
33272
33391
  "pipelineAnalytics.getKeyEvents": {
33273
33392
  capName: "pipeline-analytics",
33274
33393
  capScope: "device",
@@ -33353,6 +33472,12 @@ Object.freeze({
33353
33472
  addonId: null,
33354
33473
  access: "view"
33355
33474
  },
33475
+ "pipelineAnalytics.listGroups": {
33476
+ capName: "pipeline-analytics",
33477
+ capScope: "device",
33478
+ addonId: null,
33479
+ access: "view"
33480
+ },
33356
33481
  "pipelineAnalytics.listOpsLog": {
33357
33482
  capName: "pipeline-analytics",
33358
33483
  capScope: "device",
@@ -36134,6 +36259,11 @@ Object.freeze({
36134
36259
  form: "single",
36135
36260
  optional: false
36136
36261
  }],
36262
+ "deviceManager.getBindingsBatch": [{
36263
+ name: "deviceIds",
36264
+ form: "array",
36265
+ optional: false
36266
+ }],
36137
36267
  "deviceManager.getChildren": [{
36138
36268
  name: "parentDeviceId",
36139
36269
  form: "single",
@@ -36179,6 +36309,11 @@ Object.freeze({
36179
36309
  form: "single",
36180
36310
  optional: false
36181
36311
  }],
36312
+ "deviceManager.getLinkedDevicesBatch": [{
36313
+ name: "deviceIds",
36314
+ form: "array",
36315
+ optional: false
36316
+ }],
36182
36317
  "deviceManager.getSettingsSchema": [{
36183
36318
  name: "deviceId",
36184
36319
  form: "single",
@@ -36199,6 +36334,11 @@ Object.freeze({
36199
36334
  form: "single",
36200
36335
  optional: false
36201
36336
  }],
36337
+ "deviceManager.listAll": [{
36338
+ name: "deviceIds",
36339
+ form: "array",
36340
+ optional: true
36341
+ }],
36202
36342
  "deviceManager.loadConfig": [{
36203
36343
  name: "deviceId",
36204
36344
  form: "single",
@@ -36772,6 +36912,11 @@ Object.freeze({
36772
36912
  form: "single",
36773
36913
  optional: false
36774
36914
  }],
36915
+ "pipelineAnalytics.getGroup": [{
36916
+ name: "deviceId",
36917
+ form: "single",
36918
+ optional: false
36919
+ }],
36775
36920
  "pipelineAnalytics.getKeyEvents": [{
36776
36921
  name: "deviceId",
36777
36922
  form: "single",
@@ -36827,6 +36972,11 @@ Object.freeze({
36827
36972
  form: "array",
36828
36973
  optional: false
36829
36974
  }],
36975
+ "pipelineAnalytics.listGroups": [{
36976
+ name: "deviceIds",
36977
+ form: "array",
36978
+ optional: false
36979
+ }],
36830
36980
  "pipelineAnalytics.listOpsLog": [{
36831
36981
  name: "deviceId",
36832
36982
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5810,6 +5810,13 @@ var BaseAddon = class {
5810
5810
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5811
5811
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5812
5812
  _registeredCapNames = [];
5813
+ /**
5814
+ * True only after `readAddonStore` actually answered. Constructor
5815
+ * defaults look like stored config when the store is down — a forked
5816
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5817
+ * mode, 2026-08-25) is not "the operator chose this".
5818
+ */
5819
+ settingsStoreReady = false;
5813
5820
  /** Default config values. Provided via constructor. */
5814
5821
  defaults;
5815
5822
  constructor(defaults) {
@@ -6210,7 +6217,9 @@ var BaseAddon = class {
6210
6217
  ];
6211
6218
  let lastErr;
6212
6219
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6213
- return await settings.readAddonStore() ?? {};
6220
+ const stored = await settings.readAddonStore() ?? {};
6221
+ this.settingsStoreReady = true;
6222
+ return stored;
6214
6223
  } catch (err) {
6215
6224
  lastErr = err;
6216
6225
  const msg = err instanceof Error ? err.message : String(err);
@@ -6218,6 +6227,7 @@ var BaseAddon = class {
6218
6227
  if (attempt === delaysMs.length) break;
6219
6228
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6220
6229
  }
6230
+ this.settingsStoreReady = false;
6221
6231
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6222
6232
  return {};
6223
6233
  }
@@ -8188,6 +8198,12 @@ var ModelVariantGroupSchema = object({
8188
8198
  */
8189
8199
  resolution: number().int().positive().optional()
8190
8200
  });
8201
+ var ModelProviderIdSchema = _enum([
8202
+ "camstack",
8203
+ "frigate",
8204
+ "scrypted",
8205
+ "custom"
8206
+ ]);
8191
8207
  var ModelCatalogEntrySchema = object({
8192
8208
  id: string(),
8193
8209
  name: string(),
@@ -8285,6 +8301,12 @@ var ModelCatalogEntrySchema = object({
8285
8301
  */
8286
8302
  group: ModelVariantGroupSchema.optional(),
8287
8303
  /**
8304
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8305
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8306
+ * persisted before this field existed (`inferModelProvider` fills those).
8307
+ */
8308
+ provider: ModelProviderIdSchema.optional(),
8309
+ /**
8288
8310
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8289
8311
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8290
8312
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12021,6 +12043,27 @@ var LinkedDeviceSchema = object({
12021
12043
  features: array(string()),
12022
12044
  producesTrackedEvents: boolean().optional()
12023
12045
  });
12046
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12047
+ * The batch answer needs the tag; the single-device answer already has it
12048
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12049
+ var LinkedDevicesForDeviceSchema = object({
12050
+ deviceId: number(),
12051
+ mode: LinkedDevicesModeSchema,
12052
+ devices: array(LinkedDeviceSchema)
12053
+ });
12054
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12055
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12056
+ * object literal is exactly how the three drift apart. */
12057
+ var DeviceBindingsForDeviceSchema = object({
12058
+ deviceId: number(),
12059
+ entries: array(object({
12060
+ capName: string(),
12061
+ kind: _enum(["native", "wrapped"]),
12062
+ providerAddonId: string(),
12063
+ providerNodeId: string(),
12064
+ nativeAddonId: string()
12065
+ }))
12066
+ });
12024
12067
  var SavedDeviceRowSchema = object({
12025
12068
  /** Numeric id reserved at allocateDeviceId time. */
12026
12069
  id: number(),
@@ -12246,11 +12289,25 @@ method(object({
12246
12289
  projection: _enum(["full", "slim"]).optional(),
12247
12290
  /** Return only camera devices. Filtering server-side instead of
12248
12291
  * shipping 293 rows to find 12. */
12249
- isCamera: boolean().optional()
12292
+ isCamera: boolean().optional(),
12293
+ /**
12294
+ * Return only these device ids. For the caller that already KNOWS the
12295
+ * handful it wants and needs a field the id-bearing answer does not
12296
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12297
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12298
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12299
+ * refetches on the reconcile interval, on a phone.
12300
+ *
12301
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12302
+ * keys rather than rejecting them (verified against the live hub
12303
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12304
+ * it answers today and the caller filters as it already does.
12305
+ */
12306
+ deviceIds: array(number()).optional()
12250
12307
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12251
12308
  mode: LinkedDevicesModeSchema,
12252
12309
  devices: array(LinkedDeviceSchema)
12253
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12310
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12254
12311
  deviceId: number(),
12255
12312
  values: record(string(), unknown())
12256
12313
  }), object({ success: literal(true) }), {
@@ -12277,25 +12334,7 @@ method(object({
12277
12334
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12278
12335
  kind: "mutation",
12279
12336
  auth: "admin"
12280
- }), method(object({ deviceId: number() }), object({
12281
- deviceId: number(),
12282
- entries: array(object({
12283
- capName: string(),
12284
- kind: _enum(["native", "wrapped"]),
12285
- providerAddonId: string(),
12286
- providerNodeId: string(),
12287
- nativeAddonId: string()
12288
- }))
12289
- })), method(object({}), array(object({
12290
- deviceId: number(),
12291
- entries: array(object({
12292
- capName: string(),
12293
- kind: _enum(["native", "wrapped"]),
12294
- providerAddonId: string(),
12295
- providerNodeId: string(),
12296
- nativeAddonId: string()
12297
- }))
12298
- }))), method(object({
12337
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12299
12338
  deviceId: number(),
12300
12339
  capName: string(),
12301
12340
  wrapperAddonId: string(),
@@ -14705,12 +14744,15 @@ var NcOccupancyConditionSchema = object({
14705
14744
  * there is no second switch that can disagree with the first and every rule
14706
14745
  * authored before the decision migrates for free (`audioModeOf`):
14707
14746
  *
14708
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14709
- * classifier labels with one of them. No window, no percentage:
14710
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14711
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14712
- * the analyzer's (`classificationMinScore`, per device) a label only
14713
- * reaches this condition if the classifier was already confident enough.
14747
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14748
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14749
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14750
+ * frames is the wrong question for a classifier that labels 1–3 frames
14751
+ * per episode. The count window is the brake that drops a single-frame
14752
+ * false positive; the rule's own `throttle` cooldown is the other. The
14753
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14754
+ * per device) — a label only reaches this condition if the classifier was
14755
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14714
14756
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14715
14757
  * the condition: at least `hitPercent`% of the samples over
14716
14758
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14737,14 +14779,22 @@ var NcOccupancyConditionSchema = object({
14737
14779
  * an operator who typed `dog` mean the same thing.
14738
14780
  */
14739
14781
  var NcAudioConditionSchema = object({
14740
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14782
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14741
14783
  labels: array(string().min(1)).min(1).optional(),
14742
14784
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14743
14785
  dbThreshold: number().min(-96).max(0).optional(),
14744
14786
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14745
14787
  hitPercent: number().int().min(1).max(100).default(60),
14746
14788
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14747
- samplingSeconds: number().int().min(1).max(300).default(10)
14789
+ samplingSeconds: number().int().min(1).max(300).default(10),
14790
+ /**
14791
+ * LABEL MODE: how many labelled frames must land inside
14792
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14793
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14794
+ */
14795
+ confirmHits: number().int().min(1).max(20).optional(),
14796
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14797
+ confirmWindowSec: number().int().min(1).max(60).optional()
14748
14798
  });
14749
14799
  /**
14750
14800
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17118,6 +17168,46 @@ var RecentTracksPageSchema = object({
17118
17168
  /** Cursor for the next page, or null when this page is the last. */
17119
17169
  nextCursor: string().nullable()
17120
17170
  });
17171
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17172
+ var LIST_GROUPS_MAX_LIMIT = 100;
17173
+ var AnalyticsGroupRecordSchema = object({
17174
+ id: string(),
17175
+ deviceId: number().int(),
17176
+ openedAt: number().int(),
17177
+ closedAt: number().int(),
17178
+ timestamp: number().int(),
17179
+ memberCount: number().int(),
17180
+ memberTrackIds: array(string()).readonly(),
17181
+ className: string(),
17182
+ classes: array(string()).readonly(),
17183
+ /** Relative event-media path, or null when the group has no picture yet. */
17184
+ mediaUrl: string().nullable(),
17185
+ singleton: boolean()
17186
+ });
17187
+ var AnalyticsGroupMemberSchema = object({
17188
+ trackId: string(),
17189
+ deviceId: number().int(),
17190
+ className: string(),
17191
+ firstSeen: number().int(),
17192
+ lastSeen: number().int(),
17193
+ mediaUrl: string().nullable()
17194
+ });
17195
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17196
+ var ListGroupsQueryInput = object({
17197
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17198
+ deviceIds: array(number()),
17199
+ /** Window lower bound on `closedAt` (inclusive). */
17200
+ since: number().optional(),
17201
+ /** Window upper bound on `openedAt` (inclusive). */
17202
+ until: number().optional(),
17203
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17204
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17205
+ cursor: string().optional()
17206
+ });
17207
+ var ListGroupsPageSchema = object({
17208
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17209
+ nextCursor: string().nullable()
17210
+ });
17121
17211
  var KeyEventQueryInput = object({
17122
17212
  deviceId: number(),
17123
17213
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17193,7 +17283,9 @@ var TrackCascadeCountsSchema = object({
17193
17283
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17194
17284
  plates: number().int(),
17195
17285
  /** Per-track CLIP search vectors removed (best-effort). */
17196
- embeddings: number().int()
17286
+ embeddings: number().int(),
17287
+ /** Group membership + group rows removed with their last member (best-effort). */
17288
+ groups: number().int()
17197
17289
  });
17198
17290
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17199
17291
  var DiskReconcileCountsSchema = object({
@@ -17339,7 +17431,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17339
17431
  * stationary registry). Default false: the timeline lists passages,
17340
17432
  * not parking records (operator decision, 2026-08-15). */
17341
17433
  includeStationary: boolean().optional()
17342
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17434
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17435
+ deviceId: number(),
17436
+ groupId: string().min(1)
17437
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17343
17438
  kind: "mutation",
17344
17439
  auth: "admin"
17345
17440
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17649,7 +17744,8 @@ var PipelineModelOptionSchema = object({
17649
17744
  sizeMB: number()
17650
17745
  })),
17651
17746
  group: ModelVariantGroupSchema.optional(),
17652
- legacy: boolean().optional()
17747
+ legacy: boolean().optional(),
17748
+ provider: ModelProviderIdSchema.optional()
17653
17749
  });
17654
17750
  var ConfigFieldBridge = custom();
17655
17751
  var PipelineAddonSchemaSchema = object({
@@ -25894,7 +25990,12 @@ var PlateInfoSchema = object({
25894
25990
  plateBbox: BoundingBoxSchema.optional(),
25895
25991
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25896
25992
  keyFrameMediaKey: string().optional(),
25897
- base64: string().optional()
25993
+ base64: string().optional(),
25994
+ /**
25995
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25996
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25997
+ */
25998
+ cropUrl: string().optional()
25898
25999
  });
25899
26000
  var MediaFileLiteSchema = object({
25900
26001
  key: string(),
@@ -31416,6 +31517,12 @@ Object.freeze({
31416
31517
  addonId: null,
31417
31518
  access: "view"
31418
31519
  },
31520
+ "deviceManager.getBindingsBatch": {
31521
+ capName: "device-manager",
31522
+ capScope: "system",
31523
+ addonId: null,
31524
+ access: "view"
31525
+ },
31419
31526
  "deviceManager.getChildren": {
31420
31527
  capName: "device-manager",
31421
31528
  capScope: "system",
@@ -31476,6 +31583,12 @@ Object.freeze({
31476
31583
  addonId: null,
31477
31584
  access: "view"
31478
31585
  },
31586
+ "deviceManager.getLinkedDevicesBatch": {
31587
+ capName: "device-manager",
31588
+ capScope: "system",
31589
+ addonId: null,
31590
+ access: "view"
31591
+ },
31479
31592
  "deviceManager.getRoleDisplayDefaults": {
31480
31593
  capName: "device-manager",
31481
31594
  capScope: "system",
@@ -33246,6 +33359,12 @@ Object.freeze({
33246
33359
  addonId: null,
33247
33360
  access: "view"
33248
33361
  },
33362
+ "pipelineAnalytics.getGroup": {
33363
+ capName: "pipeline-analytics",
33364
+ capScope: "device",
33365
+ addonId: null,
33366
+ access: "view"
33367
+ },
33249
33368
  "pipelineAnalytics.getKeyEvents": {
33250
33369
  capName: "pipeline-analytics",
33251
33370
  capScope: "device",
@@ -33330,6 +33449,12 @@ Object.freeze({
33330
33449
  addonId: null,
33331
33450
  access: "view"
33332
33451
  },
33452
+ "pipelineAnalytics.listGroups": {
33453
+ capName: "pipeline-analytics",
33454
+ capScope: "device",
33455
+ addonId: null,
33456
+ access: "view"
33457
+ },
33333
33458
  "pipelineAnalytics.listOpsLog": {
33334
33459
  capName: "pipeline-analytics",
33335
33460
  capScope: "device",
@@ -36111,6 +36236,11 @@ Object.freeze({
36111
36236
  form: "single",
36112
36237
  optional: false
36113
36238
  }],
36239
+ "deviceManager.getBindingsBatch": [{
36240
+ name: "deviceIds",
36241
+ form: "array",
36242
+ optional: false
36243
+ }],
36114
36244
  "deviceManager.getChildren": [{
36115
36245
  name: "parentDeviceId",
36116
36246
  form: "single",
@@ -36156,6 +36286,11 @@ Object.freeze({
36156
36286
  form: "single",
36157
36287
  optional: false
36158
36288
  }],
36289
+ "deviceManager.getLinkedDevicesBatch": [{
36290
+ name: "deviceIds",
36291
+ form: "array",
36292
+ optional: false
36293
+ }],
36159
36294
  "deviceManager.getSettingsSchema": [{
36160
36295
  name: "deviceId",
36161
36296
  form: "single",
@@ -36176,6 +36311,11 @@ Object.freeze({
36176
36311
  form: "single",
36177
36312
  optional: false
36178
36313
  }],
36314
+ "deviceManager.listAll": [{
36315
+ name: "deviceIds",
36316
+ form: "array",
36317
+ optional: true
36318
+ }],
36179
36319
  "deviceManager.loadConfig": [{
36180
36320
  name: "deviceId",
36181
36321
  form: "single",
@@ -36749,6 +36889,11 @@ Object.freeze({
36749
36889
  form: "single",
36750
36890
  optional: false
36751
36891
  }],
36892
+ "pipelineAnalytics.getGroup": [{
36893
+ name: "deviceId",
36894
+ form: "single",
36895
+ optional: false
36896
+ }],
36752
36897
  "pipelineAnalytics.getKeyEvents": [{
36753
36898
  name: "deviceId",
36754
36899
  form: "single",
@@ -36804,6 +36949,11 @@ Object.freeze({
36804
36949
  form: "array",
36805
36950
  optional: false
36806
36951
  }],
36952
+ "pipelineAnalytics.listGroups": [{
36953
+ name: "deviceIds",
36954
+ form: "array",
36955
+ optional: false
36956
+ }],
36807
36957
  "pipelineAnalytics.listOpsLog": [{
36808
36958
  name: "deviceId",
36809
36959
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",