@camstack/addon-ai 0.4.17 → 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 +119 -13
  2. package/dist/addon.mjs +119 -13
  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).
@@ -14669,12 +14691,15 @@ var NcOccupancyConditionSchema = object({
14669
14691
  * there is no second switch that can disagree with the first and every rule
14670
14692
  * authored before the decision migrates for free (`audioModeOf`):
14671
14693
  *
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.
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.
14678
14703
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14679
14704
  * the condition: at least `hitPercent`% of the samples over
14680
14705
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14701,14 +14726,22 @@ var NcOccupancyConditionSchema = object({
14701
14726
  * an operator who typed `dog` mean the same thing.
14702
14727
  */
14703
14728
  var NcAudioConditionSchema = object({
14704
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14729
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14705
14730
  labels: array(string().min(1)).min(1).optional(),
14706
14731
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14707
14732
  dbThreshold: number$1().min(-96).max(0).optional(),
14708
14733
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14709
14734
  hitPercent: number$1().int().min(1).max(100).default(60),
14710
14735
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14711
- 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()
14712
14745
  });
14713
14746
  /**
14714
14747
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17082,6 +17115,46 @@ var RecentTracksPageSchema = object({
17082
17115
  /** Cursor for the next page, or null when this page is the last. */
17083
17116
  nextCursor: string().nullable()
17084
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
+ });
17085
17158
  var KeyEventQueryInput = object({
17086
17159
  deviceId: number$1(),
17087
17160
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17157,7 +17230,9 @@ var TrackCascadeCountsSchema = object({
17157
17230
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17158
17231
  plates: number$1().int(),
17159
17232
  /** Per-track CLIP search vectors removed (best-effort). */
17160
- 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()
17161
17236
  });
17162
17237
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17163
17238
  var DiskReconcileCountsSchema = object({
@@ -17303,7 +17378,10 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
17303
17378
  * stationary registry). Default false: the timeline lists passages,
17304
17379
  * not parking records (operator decision, 2026-08-15). */
17305
17380
  includeStationary: boolean().optional()
17306
- }), 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(), {
17307
17385
  kind: "mutation",
17308
17386
  auth: "admin"
17309
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({
@@ -17613,7 +17691,8 @@ var PipelineModelOptionSchema = object({
17613
17691
  sizeMB: number$1()
17614
17692
  })),
17615
17693
  group: ModelVariantGroupSchema.optional(),
17616
- legacy: boolean().optional()
17694
+ legacy: boolean().optional(),
17695
+ provider: ModelProviderIdSchema.optional()
17617
17696
  });
17618
17697
  var ConfigFieldBridge = custom();
17619
17698
  var PipelineAddonSchemaSchema = object({
@@ -24197,7 +24276,12 @@ var PlateInfoSchema = object({
24197
24276
  plateBbox: BoundingBoxSchema.optional(),
24198
24277
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24199
24278
  keyFrameMediaKey: string().optional(),
24200
- 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()
24201
24285
  });
24202
24286
  var MediaFileLiteSchema = object({
24203
24287
  key: string(),
@@ -29667,6 +29751,12 @@ Object.freeze({
29667
29751
  addonId: null,
29668
29752
  access: "view"
29669
29753
  },
29754
+ "pipelineAnalytics.getGroup": {
29755
+ capName: "pipeline-analytics",
29756
+ capScope: "device",
29757
+ addonId: null,
29758
+ access: "view"
29759
+ },
29670
29760
  "pipelineAnalytics.getKeyEvents": {
29671
29761
  capName: "pipeline-analytics",
29672
29762
  capScope: "device",
@@ -29751,6 +29841,12 @@ Object.freeze({
29751
29841
  addonId: null,
29752
29842
  access: "view"
29753
29843
  },
29844
+ "pipelineAnalytics.listGroups": {
29845
+ capName: "pipeline-analytics",
29846
+ capScope: "device",
29847
+ addonId: null,
29848
+ access: "view"
29849
+ },
29754
29850
  "pipelineAnalytics.listOpsLog": {
29755
29851
  capName: "pipeline-analytics",
29756
29852
  capScope: "device",
@@ -33170,6 +33266,11 @@ Object.freeze({
33170
33266
  form: "single",
33171
33267
  optional: false
33172
33268
  }],
33269
+ "pipelineAnalytics.getGroup": [{
33270
+ name: "deviceId",
33271
+ form: "single",
33272
+ optional: false
33273
+ }],
33173
33274
  "pipelineAnalytics.getKeyEvents": [{
33174
33275
  name: "deviceId",
33175
33276
  form: "single",
@@ -33225,6 +33326,11 @@ Object.freeze({
33225
33326
  form: "array",
33226
33327
  optional: false
33227
33328
  }],
33329
+ "pipelineAnalytics.listGroups": [{
33330
+ name: "deviceIds",
33331
+ form: "array",
33332
+ optional: false
33333
+ }],
33228
33334
  "pipelineAnalytics.listOpsLog": [{
33229
33335
  name: "deviceId",
33230
33336
  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).
@@ -14695,12 +14717,15 @@ var NcOccupancyConditionSchema = object({
14695
14717
  * there is no second switch that can disagree with the first and every rule
14696
14718
  * authored before the decision migrates for free (`audioModeOf`):
14697
14719
  *
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.
14720
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14721
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14722
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14723
+ * frames is the wrong question for a classifier that labels 1–3 frames
14724
+ * per episode. The count window is the brake that drops a single-frame
14725
+ * false positive; the rule's own `throttle` cooldown is the other. The
14726
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14727
+ * per device) — a label only reaches this condition if the classifier was
14728
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14704
14729
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14705
14730
  * the condition: at least `hitPercent`% of the samples over
14706
14731
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14727,14 +14752,22 @@ var NcOccupancyConditionSchema = object({
14727
14752
  * an operator who typed `dog` mean the same thing.
14728
14753
  */
14729
14754
  var NcAudioConditionSchema = object({
14730
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14755
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14731
14756
  labels: array(string().min(1)).min(1).optional(),
14732
14757
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14733
14758
  dbThreshold: number$1().min(-96).max(0).optional(),
14734
14759
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14735
14760
  hitPercent: number$1().int().min(1).max(100).default(60),
14736
14761
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14737
- samplingSeconds: number$1().int().min(1).max(300).default(10)
14762
+ samplingSeconds: number$1().int().min(1).max(300).default(10),
14763
+ /**
14764
+ * LABEL MODE: how many labelled frames must land inside
14765
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14766
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14767
+ */
14768
+ confirmHits: number$1().int().min(1).max(20).optional(),
14769
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14770
+ confirmWindowSec: number$1().int().min(1).max(60).optional()
14738
14771
  });
14739
14772
  /**
14740
14773
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17108,6 +17141,46 @@ var RecentTracksPageSchema = object({
17108
17141
  /** Cursor for the next page, or null when this page is the last. */
17109
17142
  nextCursor: string().nullable()
17110
17143
  });
17144
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17145
+ var LIST_GROUPS_MAX_LIMIT = 100;
17146
+ var AnalyticsGroupRecordSchema = object({
17147
+ id: string(),
17148
+ deviceId: number$1().int(),
17149
+ openedAt: number$1().int(),
17150
+ closedAt: number$1().int(),
17151
+ timestamp: number$1().int(),
17152
+ memberCount: number$1().int(),
17153
+ memberTrackIds: array(string()).readonly(),
17154
+ className: string(),
17155
+ classes: array(string()).readonly(),
17156
+ /** Relative event-media path, or null when the group has no picture yet. */
17157
+ mediaUrl: string().nullable(),
17158
+ singleton: boolean()
17159
+ });
17160
+ var AnalyticsGroupMemberSchema = object({
17161
+ trackId: string(),
17162
+ deviceId: number$1().int(),
17163
+ className: string(),
17164
+ firstSeen: number$1().int(),
17165
+ lastSeen: number$1().int(),
17166
+ mediaUrl: string().nullable()
17167
+ });
17168
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17169
+ var ListGroupsQueryInput = object({
17170
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17171
+ deviceIds: array(number$1()),
17172
+ /** Window lower bound on `closedAt` (inclusive). */
17173
+ since: number$1().optional(),
17174
+ /** Window upper bound on `openedAt` (inclusive). */
17175
+ until: number$1().optional(),
17176
+ limit: number$1().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17177
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17178
+ cursor: string().optional()
17179
+ });
17180
+ var ListGroupsPageSchema = object({
17181
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17182
+ nextCursor: string().nullable()
17183
+ });
17111
17184
  var KeyEventQueryInput = object({
17112
17185
  deviceId: number$1(),
17113
17186
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17183,7 +17256,9 @@ var TrackCascadeCountsSchema = object({
17183
17256
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17184
17257
  plates: number$1().int(),
17185
17258
  /** Per-track CLIP search vectors removed (best-effort). */
17186
- embeddings: number$1().int()
17259
+ embeddings: number$1().int(),
17260
+ /** Group membership + group rows removed with their last member (best-effort). */
17261
+ groups: number$1().int()
17187
17262
  });
17188
17263
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17189
17264
  var DiskReconcileCountsSchema = object({
@@ -17329,7 +17404,10 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
17329
17404
  * stationary registry). Default false: the timeline lists passages,
17330
17405
  * not parking records (operator decision, 2026-08-15). */
17331
17406
  includeStationary: boolean().optional()
17332
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number$1() }), _void(), {
17407
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17408
+ deviceId: number$1(),
17409
+ groupId: string().min(1)
17410
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number$1() }), _void(), {
17333
17411
  kind: "mutation",
17334
17412
  auth: "admin"
17335
17413
  }), 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 +17717,8 @@ var PipelineModelOptionSchema = object({
17639
17717
  sizeMB: number$1()
17640
17718
  })),
17641
17719
  group: ModelVariantGroupSchema.optional(),
17642
- legacy: boolean().optional()
17720
+ legacy: boolean().optional(),
17721
+ provider: ModelProviderIdSchema.optional()
17643
17722
  });
17644
17723
  var ConfigFieldBridge = custom();
17645
17724
  var PipelineAddonSchemaSchema = object({
@@ -24223,7 +24302,12 @@ var PlateInfoSchema = object({
24223
24302
  plateBbox: BoundingBoxSchema.optional(),
24224
24303
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24225
24304
  keyFrameMediaKey: string().optional(),
24226
- base64: string().optional()
24305
+ base64: string().optional(),
24306
+ /**
24307
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24308
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24309
+ */
24310
+ cropUrl: string().optional()
24227
24311
  });
24228
24312
  var MediaFileLiteSchema = object({
24229
24313
  key: string(),
@@ -29693,6 +29777,12 @@ Object.freeze({
29693
29777
  addonId: null,
29694
29778
  access: "view"
29695
29779
  },
29780
+ "pipelineAnalytics.getGroup": {
29781
+ capName: "pipeline-analytics",
29782
+ capScope: "device",
29783
+ addonId: null,
29784
+ access: "view"
29785
+ },
29696
29786
  "pipelineAnalytics.getKeyEvents": {
29697
29787
  capName: "pipeline-analytics",
29698
29788
  capScope: "device",
@@ -29777,6 +29867,12 @@ Object.freeze({
29777
29867
  addonId: null,
29778
29868
  access: "view"
29779
29869
  },
29870
+ "pipelineAnalytics.listGroups": {
29871
+ capName: "pipeline-analytics",
29872
+ capScope: "device",
29873
+ addonId: null,
29874
+ access: "view"
29875
+ },
29780
29876
  "pipelineAnalytics.listOpsLog": {
29781
29877
  capName: "pipeline-analytics",
29782
29878
  capScope: "device",
@@ -33196,6 +33292,11 @@ Object.freeze({
33196
33292
  form: "single",
33197
33293
  optional: false
33198
33294
  }],
33295
+ "pipelineAnalytics.getGroup": [{
33296
+ name: "deviceId",
33297
+ form: "single",
33298
+ optional: false
33299
+ }],
33199
33300
  "pipelineAnalytics.getKeyEvents": [{
33200
33301
  name: "deviceId",
33201
33302
  form: "single",
@@ -33251,6 +33352,11 @@ Object.freeze({
33251
33352
  form: "array",
33252
33353
  optional: false
33253
33354
  }],
33355
+ "pipelineAnalytics.listGroups": [{
33356
+ name: "deviceIds",
33357
+ form: "array",
33358
+ optional: false
33359
+ }],
33254
33360
  "pipelineAnalytics.listOpsLog": [{
33255
33361
  name: "deviceId",
33256
33362
  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.18",
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",