@camstack/addon-provider-petkit 0.2.25 → 0.2.27

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 +258 -17
  2. package/dist/addon.mjs +258 -17
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -1101,7 +1101,7 @@ var Nodepetkit = class {
1101
1101
  }
1102
1102
  };
1103
1103
  //#endregion
1104
- //#region ../types/dist/event-category-XfKNtfCc.mjs
1104
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
1105
1105
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
1106
1106
  EventCategory["SystemBoot"] = "system.boot";
1107
1107
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1117,6 +1117,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1117
1117
  */
1118
1118
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
1119
1119
  /**
1120
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
1121
+ * Emitted only when the material on disk actually changed, so an
1122
+ * operator who trusted the old certificate by hand is told rather than
1123
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
1124
+ *
1125
+ * Rule: docs/decisions/adr-0227-*.md
1126
+ */
1127
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
1128
+ /**
1120
1129
  * A newer addon or server-root package version was found by the
1121
1130
  * authoritative registry check. Emitted once when any observed
1122
1131
  * `latestVersion` changes (or a package/node first appears behind);
@@ -9113,6 +9122,15 @@ var LabelDefinitionSchema = object({
9113
9122
  description: string().optional(),
9114
9123
  icon: string().optional()
9115
9124
  });
9125
+ var ClassMapDefinitionSchema = object({
9126
+ mapping: record(string(), _enum([
9127
+ "person",
9128
+ "vehicle",
9129
+ "animal",
9130
+ "package"
9131
+ ])),
9132
+ preserveOriginal: boolean()
9133
+ });
9116
9134
  var MODEL_FORMATS = [
9117
9135
  "onnx",
9118
9136
  "coreml",
@@ -9291,7 +9309,13 @@ var ModelCatalogEntrySchema = object({
9291
9309
  * `id` stays the source of truth for resolution/download/persistence; grouping
9292
9310
  * is a presentation overlay resolved back to an `id`.
9293
9311
  */
9294
- group: ModelVariantGroupSchema.optional()
9312
+ group: ModelVariantGroupSchema.optional(),
9313
+ /**
9314
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9315
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9316
+ * labels already ARE the CamStack macros (Scrypted identity map).
9317
+ */
9318
+ classMap: ClassMapDefinitionSchema.optional()
9295
9319
  });
9296
9320
  var ConvertTargetSchema = discriminatedUnion("format", [object({
9297
9321
  format: literal("openvino"),
@@ -9320,7 +9344,8 @@ var ModelConvertMetadataSchema = object({
9320
9344
  "ocr",
9321
9345
  "segmentation"
9322
9346
  ]),
9323
- faceAlignment: boolean().optional()
9347
+ faceAlignment: boolean().optional(),
9348
+ classMap: ClassMapDefinitionSchema.optional()
9324
9349
  });
9325
9350
  var ConvertResultSchema = object({
9326
9351
  entry: ModelCatalogEntrySchema,
@@ -18630,6 +18655,33 @@ var NativeCropRefSchema = object({
18630
18655
  h: number()
18631
18656
  })
18632
18657
  });
18658
+ object({
18659
+ crop: object({
18660
+ left: number(),
18661
+ top: number(),
18662
+ width: number().positive(),
18663
+ height: number().positive()
18664
+ }).optional(),
18665
+ content: object({
18666
+ width: number().int().positive(),
18667
+ height: number().int().positive()
18668
+ }),
18669
+ fit: _enum(["stretch", "contain"]),
18670
+ format: _enum([
18671
+ "rgb",
18672
+ "gray",
18673
+ "jpeg"
18674
+ ])
18675
+ });
18676
+ var FrameRefSchema = object({
18677
+ registryId: string().min(1),
18678
+ id: string().min(1),
18679
+ width: number().int().positive(),
18680
+ height: number().int().positive(),
18681
+ format: _enum(["rgb", "gray"]),
18682
+ timestamp: number(),
18683
+ capturedAt: number().optional()
18684
+ });
18633
18685
  var ModelFormatSchema$1 = _enum([
18634
18686
  "onnx",
18635
18687
  "coreml",
@@ -18874,6 +18926,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
18874
18926
  steps: array(PipelineStepInputSchema).min(1),
18875
18927
  frame: FrameInputSchema.optional(),
18876
18928
  /**
18929
+ * Process-local lazy frame. Valid only when caller and provider resolve
18930
+ * in the same execution-group process; split/cross-node callers use
18931
+ * `frame`/`image` inline compatibility instead.
18932
+ */
18933
+ frameRef: FrameRefSchema.optional(),
18934
+ /**
18877
18935
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
18878
18936
  * the decoded pixels live in. One more member of the one-of
18879
18937
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -19169,7 +19227,10 @@ var NativeCropResultSchema = object({
19169
19227
  * Which source served this crop, so a quality-sensitive consumer (the native
19170
19228
  * `keyFrame`) can reject a degraded fallback:
19171
19229
  * - `native` — cut from the decode worker's retained NATIVE surface (the
19172
- * quality path).
19230
+ * quality path). A subject-tile serve is also native-resolution and stays
19231
+ * `native` here: the public enum cannot name `tile` without a breaking cap
19232
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
19233
+ * internal crop result (`nativeHits` vs `tileHits`).
19173
19234
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
19174
19235
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
19175
19236
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -19660,12 +19721,41 @@ var RunnerLocalLoadSchema = object({
19660
19721
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
19661
19722
  * working unchanged when they switch to reading from the runner cap.
19662
19723
  */
19724
+ var FrameLazyCountersSchema = object({
19725
+ framesDecoded: number(),
19726
+ framesAdmitted: number(),
19727
+ framesDroppedPixelFree: number(),
19728
+ viewsMaterialized: number(),
19729
+ viewsSkipped: number(),
19730
+ workerToRunnerBytes: number(),
19731
+ runnerToPoolRawBytes: number(),
19732
+ runnerToPoolJpegBytes: number(),
19733
+ onDemandFullFrameRequests: number(),
19734
+ onDemandCropRequests: number(),
19735
+ nativeHits: number(),
19736
+ nativeMisses: number(),
19737
+ tileHits: number(),
19738
+ tileMisses: number(),
19739
+ fallbackHits: number(),
19740
+ fallbackMisses: number(),
19741
+ retainedWritesAvoided: number(),
19742
+ residentRefs: number(),
19743
+ residentBytes: number(),
19744
+ releases: number(),
19745
+ evictions: number(),
19746
+ staleMisses: number()
19747
+ });
19748
+ var FrameLazyMetricsSchema = object({
19749
+ node: FrameLazyCountersSchema,
19750
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
19751
+ });
19663
19752
  var RunnerLocalMetricsSchema = object({
19664
19753
  nodeId: string(),
19665
19754
  activeCameras: number(),
19666
19755
  throttledCameras: number(),
19667
19756
  avgInferenceTimeMs: number(),
19668
- queueDepth: number()
19757
+ queueDepth: number(),
19758
+ frameLazy: FrameLazyMetricsSchema.optional()
19669
19759
  });
19670
19760
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
19671
19761
  handle: FrameHandleSchema,
@@ -20965,6 +21055,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
20965
21055
  location: StorageLocationSchema,
20966
21056
  relativePath: string()
20967
21057
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
21058
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
21059
+ var ProfileSettingsSchemaBridge = unknown().nullable();
21060
+ var ProfileSettingsBagSchema = record(string(), unknown());
20968
21061
  /**
20969
21062
  * A live terminal session hosted by the provider addon. Output and input do
20970
21063
  * NOT flow through the capability — they use the addon data plane
@@ -20994,7 +21087,14 @@ var TerminalSessionInfoSchema = object({
20994
21087
  var TerminalProfileInfoSchema = object({
20995
21088
  profileId: string(),
20996
21089
  label: string(),
20997
- description: string().optional()
21090
+ description: string().optional(),
21091
+ /** Spawn defaults the instance form copies on create. */
21092
+ executable: string().optional(),
21093
+ args: array(string()).readonly().optional(),
21094
+ cwd: string().optional(),
21095
+ environment: array(string()).readonly().optional(),
21096
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
21097
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
20998
21098
  });
20999
21099
  /**
21000
21100
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -21007,7 +21107,12 @@ var TerminalInstanceInfoSchema = object({
21007
21107
  profileId: string(),
21008
21108
  profileLabel: string(),
21009
21109
  name: string(),
21010
- enabled: boolean()
21110
+ enabled: boolean(),
21111
+ executable: string(),
21112
+ args: array(string()).readonly(),
21113
+ cwd: string(),
21114
+ environment: array(string()).readonly(),
21115
+ profileSettings: ProfileSettingsBagSchema
21011
21116
  });
21012
21117
  var TerminalLegacyCameraSchema = object({
21013
21118
  stableId: string(),
@@ -21037,7 +21142,23 @@ var TerminalOutputBatchSchema = object({
21037
21142
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
21038
21143
  targetNodeId: string().min(1),
21039
21144
  profileId: string().min(1),
21040
- name: string().trim().min(1).max(160).optional()
21145
+ name: string().trim().min(1).max(160).optional(),
21146
+ executable: string().max(1024).optional(),
21147
+ args: array(string().max(2048)).max(64).optional(),
21148
+ cwd: string().max(1024).optional(),
21149
+ environment: array(string().max(4096)).max(64).optional(),
21150
+ profileSettings: ProfileSettingsBagSchema.optional()
21151
+ }), TerminalInstanceInfoSchema, {
21152
+ kind: "mutation",
21153
+ auth: "admin"
21154
+ }), method(object({
21155
+ instanceId: string().min(1),
21156
+ name: string().trim().min(1).max(160).optional(),
21157
+ executable: string().max(1024).optional(),
21158
+ args: array(string().max(2048)).max(64).optional(),
21159
+ cwd: string().max(1024).optional(),
21160
+ environment: array(string().max(4096)).max(64).optional(),
21161
+ profileSettings: ProfileSettingsBagSchema.optional()
21041
21162
  }), TerminalInstanceInfoSchema, {
21042
21163
  kind: "mutation",
21043
21164
  auth: "admin"
@@ -21059,7 +21180,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
21059
21180
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
21060
21181
  profileId: string(),
21061
21182
  cols: number().int().positive(),
21062
- rows: number().int().positive()
21183
+ rows: number().int().positive(),
21184
+ executable: string().max(1024).optional(),
21185
+ args: array(string().max(2048)).max(64).optional(),
21186
+ cwd: string().max(1024).optional(),
21187
+ environment: array(string().max(4096)).max(64).optional()
21063
21188
  }), TerminalSessionInfoSchema, {
21064
21189
  kind: "mutation",
21065
21190
  auth: "admin"
@@ -24942,10 +25067,10 @@ var lawnMowerControlCapability = {
24942
25067
  *
24943
25068
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
24944
25069
  * to receive an ordered list of candidate base URLs it should race
24945
- * on connect — LAN IPv4 first (lowest latency when on same network),
24946
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
24947
- * race them with short timeouts and stick with the winner for the
24948
- * session.
25070
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
25071
+ * when on the same network), then public hostname (if a tunnel is
25072
+ * up). The SDK can race them with short timeouts and stick with the
25073
+ * winner for the session.
24949
25074
  *
24950
25075
  * Why hub-only: agents are not directly addressable by the operator's
24951
25076
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -25100,6 +25225,17 @@ var NotificationEndpointSchema = object({
25100
25225
  /** What the ranking currently resolves to (null when nothing is reachable). */
25101
25226
  resolved: string().nullable()
25102
25227
  });
25228
+ /**
25229
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
25230
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
25231
+ * currently expands to, so the UI can show the effective set either way.
25232
+ */
25233
+ var ViewerEndpointsSchema = object({
25234
+ /** The operator's explicit race set, or empty for AUTO. */
25235
+ baseUrls: array(string()).readonly(),
25236
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
25237
+ resolved: array(string()).readonly()
25238
+ });
25103
25239
  var AllowedAddressesSchema = object({
25104
25240
  /**
25105
25241
  * Allowlist of interface addresses operators have explicitly opted
@@ -25108,6 +25244,20 @@ var AllowedAddressesSchema = object({
25108
25244
  * Network Addresses admin page and persisted by the addon.
25109
25245
  */
25110
25246
  addresses: array(string()).readonly() });
25247
+ var TlsStatusSchema = object({
25248
+ mode: _enum([
25249
+ "generated",
25250
+ "uploaded",
25251
+ "disabled"
25252
+ ]),
25253
+ leafFingerprintSha256: string().nullable(),
25254
+ caFingerprintSha256: string().nullable(),
25255
+ validTo: string().nullable(),
25256
+ sans: array(string()),
25257
+ caCertPem: string().nullable(),
25258
+ reissueError: string().nullable(),
25259
+ restartRequired: boolean()
25260
+ });
25111
25261
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
25112
25262
  /**
25113
25263
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -25117,17 +25267,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
25117
25267
  */
25118
25268
  port: number().int().min(1).max(65535).optional(),
25119
25269
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
25120
- * candidate. Default `true`. */
25270
+ * candidate. Default `false` — loopback is not a client route. */
25121
25271
  includeLoopback: boolean().optional(),
25122
- /** Skip IPv6 entries. Some legacy clients can't parse them.
25123
- * Default `false`. */
25272
+ /** Skip IPv6 entries. Default `false` the palette includes stable
25273
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
25274
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
25124
25275
  ipv4Only: boolean().optional(),
25125
25276
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
25126
25277
  * Pass `'https'` when the caller is itself loaded over HTTPS
25127
25278
  * to avoid mixed-content blocks in the browser. The public
25128
25279
  * tunnel always emits `https://` regardless. */
25129
25280
  scheme: _enum(["http", "https"]).optional()
25130
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
25281
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
25282
+ kind: "mutation",
25283
+ auth: "admin"
25284
+ }), method(object({
25285
+ certPem: string().min(1),
25286
+ keyPem: string().min(1),
25287
+ caPem: string().optional()
25288
+ }), TlsStatusSchema, {
25289
+ kind: "mutation",
25290
+ auth: "admin"
25291
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
25292
+ kind: "mutation",
25293
+ auth: "admin"
25294
+ });
25131
25295
  var LockControlStatusSchema = object({
25132
25296
  /** Lifecycle state of the lock. `jammed` means the motor reported
25133
25297
  * failure to reach the target — operator intervention required. */
@@ -33154,6 +33318,12 @@ Object.freeze({
33154
33318
  addonId: null,
33155
33319
  access: "create"
33156
33320
  },
33321
+ "localNetwork.downloadCa": {
33322
+ capName: "local-network",
33323
+ capScope: "system",
33324
+ addonId: null,
33325
+ access: "view"
33326
+ },
33157
33327
  "localNetwork.getAllowedAddresses": {
33158
33328
  capName: "local-network",
33159
33329
  capScope: "system",
@@ -33178,18 +33348,42 @@ Object.freeze({
33178
33348
  addonId: null,
33179
33349
  access: "view"
33180
33350
  },
33351
+ "localNetwork.getTlsStatus": {
33352
+ capName: "local-network",
33353
+ capScope: "system",
33354
+ addonId: null,
33355
+ access: "view"
33356
+ },
33357
+ "localNetwork.getViewerEndpoints": {
33358
+ capName: "local-network",
33359
+ capScope: "system",
33360
+ addonId: null,
33361
+ access: "view"
33362
+ },
33181
33363
  "localNetwork.list": {
33182
33364
  capName: "local-network",
33183
33365
  capScope: "system",
33184
33366
  addonId: null,
33185
33367
  access: "view"
33186
33368
  },
33369
+ "localNetwork.regenerateCertificate": {
33370
+ capName: "local-network",
33371
+ capScope: "system",
33372
+ addonId: null,
33373
+ access: "create"
33374
+ },
33187
33375
  "localNetwork.resetAllowlistToBestMatch": {
33188
33376
  capName: "local-network",
33189
33377
  capScope: "system",
33190
33378
  addonId: null,
33191
33379
  access: "delete"
33192
33380
  },
33381
+ "localNetwork.revertToGeneratedCertificate": {
33382
+ capName: "local-network",
33383
+ capScope: "system",
33384
+ addonId: null,
33385
+ access: "create"
33386
+ },
33193
33387
  "localNetwork.setAllowedAddresses": {
33194
33388
  capName: "local-network",
33195
33389
  capScope: "system",
@@ -33202,6 +33396,18 @@ Object.freeze({
33202
33396
  addonId: null,
33203
33397
  access: "create"
33204
33398
  },
33399
+ "localNetwork.setViewerEndpoints": {
33400
+ capName: "local-network",
33401
+ capScope: "system",
33402
+ addonId: null,
33403
+ access: "create"
33404
+ },
33405
+ "localNetwork.uploadCertificate": {
33406
+ capName: "local-network",
33407
+ capScope: "system",
33408
+ addonId: null,
33409
+ access: "create"
33410
+ },
33205
33411
  "lockControl.lock": {
33206
33412
  capName: "lock-control",
33207
33413
  capScope: "device",
@@ -36082,6 +36288,12 @@ Object.freeze({
36082
36288
  addonId: null,
36083
36289
  access: "create"
36084
36290
  },
36291
+ "terminalSession.updateInstance": {
36292
+ capName: "terminal-session",
36293
+ capScope: "system",
36294
+ addonId: null,
36295
+ access: "create"
36296
+ },
36085
36297
  "terminalSession.writeInput": {
36086
36298
  capName: "terminal-session",
36087
36299
  capScope: "system",
@@ -38569,6 +38781,35 @@ Object.freeze(Object.fromEntries([{
38569
38781
  }]
38570
38782
  }].map((s) => [s.stepId, s.defaultModelId])));
38571
38783
  string().min(1);
38784
+ var CLUSTER_STEP_SETTING_FIELDS = [{
38785
+ stepId: "face-embedding",
38786
+ key: "minLandmarkFaceSize",
38787
+ label: "Min face size for recognition (detection px)",
38788
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
38789
+ type: "slider",
38790
+ min: 0,
38791
+ max: 64,
38792
+ step: 2,
38793
+ default: 24
38794
+ }];
38795
+ function clusterStepSettingKey(stepId, fieldKey) {
38796
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
38797
+ }
38798
+ var ClusterSettingNumberSchema = number().finite();
38799
+ function readClusterStepSettings(config) {
38800
+ const out = {};
38801
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
38802
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
38803
+ const value = parsed.success ? parsed.data : field.default;
38804
+ const existing = out[field.stepId] ?? {};
38805
+ out[field.stepId] = {
38806
+ ...existing,
38807
+ [field.key]: value
38808
+ };
38809
+ }
38810
+ return out;
38811
+ }
38812
+ readClusterStepSettings({});
38572
38813
  object({
38573
38814
  /**
38574
38815
  * Fraction of the box's own size added on EACH side before cutting.
package/dist/addon.mjs CHANGED
@@ -1100,7 +1100,7 @@ var Nodepetkit = class {
1100
1100
  }
1101
1101
  };
1102
1102
  //#endregion
1103
- //#region ../types/dist/event-category-XfKNtfCc.mjs
1103
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
1104
1104
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
1105
1105
  EventCategory["SystemBoot"] = "system.boot";
1106
1106
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1116,6 +1116,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1116
1116
  */
1117
1117
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
1118
1118
  /**
1119
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
1120
+ * Emitted only when the material on disk actually changed, so an
1121
+ * operator who trusted the old certificate by hand is told rather than
1122
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
1123
+ *
1124
+ * Rule: docs/decisions/adr-0227-*.md
1125
+ */
1126
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
1127
+ /**
1119
1128
  * A newer addon or server-root package version was found by the
1120
1129
  * authoritative registry check. Emitted once when any observed
1121
1130
  * `latestVersion` changes (or a package/node first appears behind);
@@ -9112,6 +9121,15 @@ var LabelDefinitionSchema = object({
9112
9121
  description: string().optional(),
9113
9122
  icon: string().optional()
9114
9123
  });
9124
+ var ClassMapDefinitionSchema = object({
9125
+ mapping: record(string(), _enum([
9126
+ "person",
9127
+ "vehicle",
9128
+ "animal",
9129
+ "package"
9130
+ ])),
9131
+ preserveOriginal: boolean()
9132
+ });
9115
9133
  var MODEL_FORMATS = [
9116
9134
  "onnx",
9117
9135
  "coreml",
@@ -9290,7 +9308,13 @@ var ModelCatalogEntrySchema = object({
9290
9308
  * `id` stays the source of truth for resolution/download/persistence; grouping
9291
9309
  * is a presentation overlay resolved back to an `id`.
9292
9310
  */
9293
- group: ModelVariantGroupSchema.optional()
9311
+ group: ModelVariantGroupSchema.optional(),
9312
+ /**
9313
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9314
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9315
+ * labels already ARE the CamStack macros (Scrypted identity map).
9316
+ */
9317
+ classMap: ClassMapDefinitionSchema.optional()
9294
9318
  });
9295
9319
  var ConvertTargetSchema = discriminatedUnion("format", [object({
9296
9320
  format: literal("openvino"),
@@ -9319,7 +9343,8 @@ var ModelConvertMetadataSchema = object({
9319
9343
  "ocr",
9320
9344
  "segmentation"
9321
9345
  ]),
9322
- faceAlignment: boolean().optional()
9346
+ faceAlignment: boolean().optional(),
9347
+ classMap: ClassMapDefinitionSchema.optional()
9323
9348
  });
9324
9349
  var ConvertResultSchema = object({
9325
9350
  entry: ModelCatalogEntrySchema,
@@ -18629,6 +18654,33 @@ var NativeCropRefSchema = object({
18629
18654
  h: number()
18630
18655
  })
18631
18656
  });
18657
+ object({
18658
+ crop: object({
18659
+ left: number(),
18660
+ top: number(),
18661
+ width: number().positive(),
18662
+ height: number().positive()
18663
+ }).optional(),
18664
+ content: object({
18665
+ width: number().int().positive(),
18666
+ height: number().int().positive()
18667
+ }),
18668
+ fit: _enum(["stretch", "contain"]),
18669
+ format: _enum([
18670
+ "rgb",
18671
+ "gray",
18672
+ "jpeg"
18673
+ ])
18674
+ });
18675
+ var FrameRefSchema = object({
18676
+ registryId: string().min(1),
18677
+ id: string().min(1),
18678
+ width: number().int().positive(),
18679
+ height: number().int().positive(),
18680
+ format: _enum(["rgb", "gray"]),
18681
+ timestamp: number(),
18682
+ capturedAt: number().optional()
18683
+ });
18632
18684
  var ModelFormatSchema$1 = _enum([
18633
18685
  "onnx",
18634
18686
  "coreml",
@@ -18873,6 +18925,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
18873
18925
  steps: array(PipelineStepInputSchema).min(1),
18874
18926
  frame: FrameInputSchema.optional(),
18875
18927
  /**
18928
+ * Process-local lazy frame. Valid only when caller and provider resolve
18929
+ * in the same execution-group process; split/cross-node callers use
18930
+ * `frame`/`image` inline compatibility instead.
18931
+ */
18932
+ frameRef: FrameRefSchema.optional(),
18933
+ /**
18876
18934
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
18877
18935
  * the decoded pixels live in. One more member of the one-of
18878
18936
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -19168,7 +19226,10 @@ var NativeCropResultSchema = object({
19168
19226
  * Which source served this crop, so a quality-sensitive consumer (the native
19169
19227
  * `keyFrame`) can reject a degraded fallback:
19170
19228
  * - `native` — cut from the decode worker's retained NATIVE surface (the
19171
- * quality path).
19229
+ * quality path). A subject-tile serve is also native-resolution and stays
19230
+ * `native` here: the public enum cannot name `tile` without a breaking cap
19231
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
19232
+ * internal crop result (`nativeHits` vs `tileHits`).
19172
19233
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
19173
19234
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
19174
19235
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -19659,12 +19720,41 @@ var RunnerLocalLoadSchema = object({
19659
19720
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
19660
19721
  * working unchanged when they switch to reading from the runner cap.
19661
19722
  */
19723
+ var FrameLazyCountersSchema = object({
19724
+ framesDecoded: number(),
19725
+ framesAdmitted: number(),
19726
+ framesDroppedPixelFree: number(),
19727
+ viewsMaterialized: number(),
19728
+ viewsSkipped: number(),
19729
+ workerToRunnerBytes: number(),
19730
+ runnerToPoolRawBytes: number(),
19731
+ runnerToPoolJpegBytes: number(),
19732
+ onDemandFullFrameRequests: number(),
19733
+ onDemandCropRequests: number(),
19734
+ nativeHits: number(),
19735
+ nativeMisses: number(),
19736
+ tileHits: number(),
19737
+ tileMisses: number(),
19738
+ fallbackHits: number(),
19739
+ fallbackMisses: number(),
19740
+ retainedWritesAvoided: number(),
19741
+ residentRefs: number(),
19742
+ residentBytes: number(),
19743
+ releases: number(),
19744
+ evictions: number(),
19745
+ staleMisses: number()
19746
+ });
19747
+ var FrameLazyMetricsSchema = object({
19748
+ node: FrameLazyCountersSchema,
19749
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
19750
+ });
19662
19751
  var RunnerLocalMetricsSchema = object({
19663
19752
  nodeId: string(),
19664
19753
  activeCameras: number(),
19665
19754
  throttledCameras: number(),
19666
19755
  avgInferenceTimeMs: number(),
19667
- queueDepth: number()
19756
+ queueDepth: number(),
19757
+ frameLazy: FrameLazyMetricsSchema.optional()
19668
19758
  });
19669
19759
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
19670
19760
  handle: FrameHandleSchema,
@@ -20964,6 +21054,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
20964
21054
  location: StorageLocationSchema,
20965
21055
  relativePath: string()
20966
21056
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
21057
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
21058
+ var ProfileSettingsSchemaBridge = unknown().nullable();
21059
+ var ProfileSettingsBagSchema = record(string(), unknown());
20967
21060
  /**
20968
21061
  * A live terminal session hosted by the provider addon. Output and input do
20969
21062
  * NOT flow through the capability — they use the addon data plane
@@ -20993,7 +21086,14 @@ var TerminalSessionInfoSchema = object({
20993
21086
  var TerminalProfileInfoSchema = object({
20994
21087
  profileId: string(),
20995
21088
  label: string(),
20996
- description: string().optional()
21089
+ description: string().optional(),
21090
+ /** Spawn defaults the instance form copies on create. */
21091
+ executable: string().optional(),
21092
+ args: array(string()).readonly().optional(),
21093
+ cwd: string().optional(),
21094
+ environment: array(string()).readonly().optional(),
21095
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
21096
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
20997
21097
  });
20998
21098
  /**
20999
21099
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -21006,7 +21106,12 @@ var TerminalInstanceInfoSchema = object({
21006
21106
  profileId: string(),
21007
21107
  profileLabel: string(),
21008
21108
  name: string(),
21009
- enabled: boolean()
21109
+ enabled: boolean(),
21110
+ executable: string(),
21111
+ args: array(string()).readonly(),
21112
+ cwd: string(),
21113
+ environment: array(string()).readonly(),
21114
+ profileSettings: ProfileSettingsBagSchema
21010
21115
  });
21011
21116
  var TerminalLegacyCameraSchema = object({
21012
21117
  stableId: string(),
@@ -21036,7 +21141,23 @@ var TerminalOutputBatchSchema = object({
21036
21141
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
21037
21142
  targetNodeId: string().min(1),
21038
21143
  profileId: string().min(1),
21039
- name: string().trim().min(1).max(160).optional()
21144
+ name: string().trim().min(1).max(160).optional(),
21145
+ executable: string().max(1024).optional(),
21146
+ args: array(string().max(2048)).max(64).optional(),
21147
+ cwd: string().max(1024).optional(),
21148
+ environment: array(string().max(4096)).max(64).optional(),
21149
+ profileSettings: ProfileSettingsBagSchema.optional()
21150
+ }), TerminalInstanceInfoSchema, {
21151
+ kind: "mutation",
21152
+ auth: "admin"
21153
+ }), method(object({
21154
+ instanceId: string().min(1),
21155
+ name: string().trim().min(1).max(160).optional(),
21156
+ executable: string().max(1024).optional(),
21157
+ args: array(string().max(2048)).max(64).optional(),
21158
+ cwd: string().max(1024).optional(),
21159
+ environment: array(string().max(4096)).max(64).optional(),
21160
+ profileSettings: ProfileSettingsBagSchema.optional()
21040
21161
  }), TerminalInstanceInfoSchema, {
21041
21162
  kind: "mutation",
21042
21163
  auth: "admin"
@@ -21058,7 +21179,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
21058
21179
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
21059
21180
  profileId: string(),
21060
21181
  cols: number().int().positive(),
21061
- rows: number().int().positive()
21182
+ rows: number().int().positive(),
21183
+ executable: string().max(1024).optional(),
21184
+ args: array(string().max(2048)).max(64).optional(),
21185
+ cwd: string().max(1024).optional(),
21186
+ environment: array(string().max(4096)).max(64).optional()
21062
21187
  }), TerminalSessionInfoSchema, {
21063
21188
  kind: "mutation",
21064
21189
  auth: "admin"
@@ -24941,10 +25066,10 @@ var lawnMowerControlCapability = {
24941
25066
  *
24942
25067
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
24943
25068
  * to receive an ordered list of candidate base URLs it should race
24944
- * on connect — LAN IPv4 first (lowest latency when on same network),
24945
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
24946
- * race them with short timeouts and stick with the winner for the
24947
- * session.
25069
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
25070
+ * when on the same network), then public hostname (if a tunnel is
25071
+ * up). The SDK can race them with short timeouts and stick with the
25072
+ * winner for the session.
24948
25073
  *
24949
25074
  * Why hub-only: agents are not directly addressable by the operator's
24950
25075
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -25099,6 +25224,17 @@ var NotificationEndpointSchema = object({
25099
25224
  /** What the ranking currently resolves to (null when nothing is reachable). */
25100
25225
  resolved: string().nullable()
25101
25226
  });
25227
+ /**
25228
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
25229
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
25230
+ * currently expands to, so the UI can show the effective set either way.
25231
+ */
25232
+ var ViewerEndpointsSchema = object({
25233
+ /** The operator's explicit race set, or empty for AUTO. */
25234
+ baseUrls: array(string()).readonly(),
25235
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
25236
+ resolved: array(string()).readonly()
25237
+ });
25102
25238
  var AllowedAddressesSchema = object({
25103
25239
  /**
25104
25240
  * Allowlist of interface addresses operators have explicitly opted
@@ -25107,6 +25243,20 @@ var AllowedAddressesSchema = object({
25107
25243
  * Network Addresses admin page and persisted by the addon.
25108
25244
  */
25109
25245
  addresses: array(string()).readonly() });
25246
+ var TlsStatusSchema = object({
25247
+ mode: _enum([
25248
+ "generated",
25249
+ "uploaded",
25250
+ "disabled"
25251
+ ]),
25252
+ leafFingerprintSha256: string().nullable(),
25253
+ caFingerprintSha256: string().nullable(),
25254
+ validTo: string().nullable(),
25255
+ sans: array(string()),
25256
+ caCertPem: string().nullable(),
25257
+ reissueError: string().nullable(),
25258
+ restartRequired: boolean()
25259
+ });
25110
25260
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
25111
25261
  /**
25112
25262
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -25116,17 +25266,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
25116
25266
  */
25117
25267
  port: number().int().min(1).max(65535).optional(),
25118
25268
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
25119
- * candidate. Default `true`. */
25269
+ * candidate. Default `false` — loopback is not a client route. */
25120
25270
  includeLoopback: boolean().optional(),
25121
- /** Skip IPv6 entries. Some legacy clients can't parse them.
25122
- * Default `false`. */
25271
+ /** Skip IPv6 entries. Default `false` the palette includes stable
25272
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
25273
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
25123
25274
  ipv4Only: boolean().optional(),
25124
25275
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
25125
25276
  * Pass `'https'` when the caller is itself loaded over HTTPS
25126
25277
  * to avoid mixed-content blocks in the browser. The public
25127
25278
  * tunnel always emits `https://` regardless. */
25128
25279
  scheme: _enum(["http", "https"]).optional()
25129
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
25280
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
25281
+ kind: "mutation",
25282
+ auth: "admin"
25283
+ }), method(object({
25284
+ certPem: string().min(1),
25285
+ keyPem: string().min(1),
25286
+ caPem: string().optional()
25287
+ }), TlsStatusSchema, {
25288
+ kind: "mutation",
25289
+ auth: "admin"
25290
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
25291
+ kind: "mutation",
25292
+ auth: "admin"
25293
+ });
25130
25294
  var LockControlStatusSchema = object({
25131
25295
  /** Lifecycle state of the lock. `jammed` means the motor reported
25132
25296
  * failure to reach the target — operator intervention required. */
@@ -33153,6 +33317,12 @@ Object.freeze({
33153
33317
  addonId: null,
33154
33318
  access: "create"
33155
33319
  },
33320
+ "localNetwork.downloadCa": {
33321
+ capName: "local-network",
33322
+ capScope: "system",
33323
+ addonId: null,
33324
+ access: "view"
33325
+ },
33156
33326
  "localNetwork.getAllowedAddresses": {
33157
33327
  capName: "local-network",
33158
33328
  capScope: "system",
@@ -33177,18 +33347,42 @@ Object.freeze({
33177
33347
  addonId: null,
33178
33348
  access: "view"
33179
33349
  },
33350
+ "localNetwork.getTlsStatus": {
33351
+ capName: "local-network",
33352
+ capScope: "system",
33353
+ addonId: null,
33354
+ access: "view"
33355
+ },
33356
+ "localNetwork.getViewerEndpoints": {
33357
+ capName: "local-network",
33358
+ capScope: "system",
33359
+ addonId: null,
33360
+ access: "view"
33361
+ },
33180
33362
  "localNetwork.list": {
33181
33363
  capName: "local-network",
33182
33364
  capScope: "system",
33183
33365
  addonId: null,
33184
33366
  access: "view"
33185
33367
  },
33368
+ "localNetwork.regenerateCertificate": {
33369
+ capName: "local-network",
33370
+ capScope: "system",
33371
+ addonId: null,
33372
+ access: "create"
33373
+ },
33186
33374
  "localNetwork.resetAllowlistToBestMatch": {
33187
33375
  capName: "local-network",
33188
33376
  capScope: "system",
33189
33377
  addonId: null,
33190
33378
  access: "delete"
33191
33379
  },
33380
+ "localNetwork.revertToGeneratedCertificate": {
33381
+ capName: "local-network",
33382
+ capScope: "system",
33383
+ addonId: null,
33384
+ access: "create"
33385
+ },
33192
33386
  "localNetwork.setAllowedAddresses": {
33193
33387
  capName: "local-network",
33194
33388
  capScope: "system",
@@ -33201,6 +33395,18 @@ Object.freeze({
33201
33395
  addonId: null,
33202
33396
  access: "create"
33203
33397
  },
33398
+ "localNetwork.setViewerEndpoints": {
33399
+ capName: "local-network",
33400
+ capScope: "system",
33401
+ addonId: null,
33402
+ access: "create"
33403
+ },
33404
+ "localNetwork.uploadCertificate": {
33405
+ capName: "local-network",
33406
+ capScope: "system",
33407
+ addonId: null,
33408
+ access: "create"
33409
+ },
33204
33410
  "lockControl.lock": {
33205
33411
  capName: "lock-control",
33206
33412
  capScope: "device",
@@ -36081,6 +36287,12 @@ Object.freeze({
36081
36287
  addonId: null,
36082
36288
  access: "create"
36083
36289
  },
36290
+ "terminalSession.updateInstance": {
36291
+ capName: "terminal-session",
36292
+ capScope: "system",
36293
+ addonId: null,
36294
+ access: "create"
36295
+ },
36084
36296
  "terminalSession.writeInput": {
36085
36297
  capName: "terminal-session",
36086
36298
  capScope: "system",
@@ -38568,6 +38780,35 @@ Object.freeze(Object.fromEntries([{
38568
38780
  }]
38569
38781
  }].map((s) => [s.stepId, s.defaultModelId])));
38570
38782
  string().min(1);
38783
+ var CLUSTER_STEP_SETTING_FIELDS = [{
38784
+ stepId: "face-embedding",
38785
+ key: "minLandmarkFaceSize",
38786
+ label: "Min face size for recognition (detection px)",
38787
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
38788
+ type: "slider",
38789
+ min: 0,
38790
+ max: 64,
38791
+ step: 2,
38792
+ default: 24
38793
+ }];
38794
+ function clusterStepSettingKey(stepId, fieldKey) {
38795
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
38796
+ }
38797
+ var ClusterSettingNumberSchema = number().finite();
38798
+ function readClusterStepSettings(config) {
38799
+ const out = {};
38800
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
38801
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
38802
+ const value = parsed.success ? parsed.data : field.default;
38803
+ const existing = out[field.stepId] ?? {};
38804
+ out[field.stepId] = {
38805
+ ...existing,
38806
+ [field.key]: value
38807
+ };
38808
+ }
38809
+ return out;
38810
+ }
38811
+ readClusterStepSettings({});
38571
38812
  object({
38572
38813
  /**
38573
38814
  * Fraction of the box's own size added on EACH side before cutting.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",