@camstack/addon-import-alexa 0.2.165 → 0.2.167

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.
package/dist/addon.js CHANGED
@@ -5248,6 +5248,14 @@ function object(shape, params) {
5248
5248
  ...normalizeParams(params)
5249
5249
  });
5250
5250
  }
5251
+ function strictObject(shape, params) {
5252
+ return new ZodObject({
5253
+ type: "object",
5254
+ shape,
5255
+ catchall: never(),
5256
+ ...normalizeParams(params)
5257
+ });
5258
+ }
5251
5259
  function looseObject(shape, params) {
5252
5260
  return new ZodObject({
5253
5261
  type: "object",
@@ -5627,7 +5635,7 @@ var ZodIssueCode = {
5627
5635
  var ZodFirstPartyTypeKind;
5628
5636
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5629
5637
  //#endregion
5630
- //#region ../types/dist/sleep-Crlmu_tO.mjs
5638
+ //#region ../types/dist/sleep-6eG9C8Xl.mjs
5631
5639
  /**
5632
5640
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5633
5641
  * window to float samples (D455).
@@ -8862,9 +8870,76 @@ function optionalMethod(input, output, options) {
8862
8870
  function event(data) {
8863
8871
  return { data };
8864
8872
  }
8873
+ /**
8874
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8875
+ * declarations. Every addon that contributes a UI page registers a
8876
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8877
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8878
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8879
+ * that admin-ui consumes.
8880
+ *
8881
+ * The split exists because the public listing has a different output
8882
+ * shape than the per-provider raw declarations, and we want both ends
8883
+ * to flow through codegen instead of relying on a hand-written wrapper.
8884
+ */
8885
+ var AddonPageDeclarationSchema = object({
8886
+ id: string(),
8887
+ label: string(),
8888
+ icon: string(),
8889
+ path: string(),
8890
+ /**
8891
+ * Module Federation remote name — must match the `name` field on the
8892
+ * page addon's `federation()` plugin config. Used by admin-ui's
8893
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8894
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8895
+ * contain hyphens).
8896
+ */
8897
+ remoteName: string(),
8898
+ /**
8899
+ * Bundle filename inside the addon's `dist/` dir served at
8900
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8901
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8902
+ * the static-file route can compute an mtime-based cache-buster URL
8903
+ * without a separate filesystem stat.
8904
+ */
8905
+ bundle: string(),
8906
+ /**
8907
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8908
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8909
+ * Any OTHER string creates (or joins) a custom section rendered after
8910
+ * the built-in groups; its label comes from `sectionLabel` (first
8911
+ * declaration wins), falling back to the id. Absent → the legacy
8912
+ * "Addon Pages" group.
8913
+ */
8914
+ section: string().optional(),
8915
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8916
+ sectionLabel: string().optional()
8917
+ });
8918
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
8865
8919
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8866
8920
  var VersionOutputSchema$1 = object({ version: string() });
8867
8921
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
8922
+ /**
8923
+ * The pipeline step-input shape and the run-result bridge, in a module that
8924
+ * imports nothing but zod and types.
8925
+ *
8926
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8927
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8928
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8929
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8930
+ * them, so every existing import keeps working; addon server code takes them
8931
+ * from `@camstack/types/addon`.
8932
+ */
8933
+ var PipelineStepInputSchema = lazy(() => object({
8934
+ addonId: string(),
8935
+ modelId: string().optional(),
8936
+ enabled: boolean().default(true),
8937
+ children: array(PipelineStepInputSchema).optional(),
8938
+ settings: record(string(), unknown()).optional(),
8939
+ jumpDeviceKey: string().optional()
8940
+ }));
8941
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8942
+ var PipelineRunResultBridge = custom();
8868
8943
  var StaticDirOutputSchema = object({ staticDir: string() });
8869
8944
  var VersionOutputSchema = object({ version: string() });
8870
8945
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9020,6 +9095,7 @@ var CAP_NAMES = Object.freeze([
9020
9095
  "image-settings",
9021
9096
  "integrations",
9022
9097
  "intercom",
9098
+ "ir-transmitter",
9023
9099
  "keychain",
9024
9100
  "keychain-admin",
9025
9101
  "lawn-mower-control",
@@ -9048,6 +9124,7 @@ var CAP_NAMES = Object.freeze([
9048
9124
  "network-link",
9049
9125
  "network-quality",
9050
9126
  "node-certificates",
9127
+ "node-removal-hook",
9051
9128
  "nodes",
9052
9129
  "notification-output",
9053
9130
  "notification-rules",
@@ -9058,6 +9135,7 @@ var CAP_NAMES = Object.freeze([
9058
9135
  "osd",
9059
9136
  "osd-manager",
9060
9137
  "pet-feeder",
9138
+ "pet-fountain",
9061
9139
  "pipeline-analytics",
9062
9140
  "pipeline-executor",
9063
9141
  "pipeline-orchestrator",
@@ -9078,6 +9156,7 @@ var CAP_NAMES = Object.freeze([
9078
9156
  "recording-onboard",
9079
9157
  "recording-signal",
9080
9158
  "records",
9159
+ "remote",
9081
9160
  "running-state",
9082
9161
  "scene-monitor",
9083
9162
  "script-runner",
@@ -9101,6 +9180,7 @@ var CAP_NAMES = Object.freeze([
9101
9180
  "system",
9102
9181
  "tamper",
9103
9182
  "temperature-sensor",
9183
+ "template",
9104
9184
  "terminal-session",
9105
9185
  "toast",
9106
9186
  "training",
@@ -10041,6 +10121,11 @@ Object.freeze({
10041
10121
  "standby"
10042
10122
  ]
10043
10123
  },
10124
+ {
10125
+ "path": "stateSource",
10126
+ "valueType": "enum",
10127
+ "enumValues": ["reported", "assumed"]
10128
+ },
10044
10129
  {
10045
10130
  "path": "volumeLevel",
10046
10131
  "valueType": "number"
@@ -10267,6 +10352,11 @@ Object.freeze({
10267
10352
  "path": "batteryPower",
10268
10353
  "valueType": "number"
10269
10354
  },
10355
+ {
10356
+ "path": "powerSource",
10357
+ "valueType": "enum",
10358
+ "enumValues": ["mains", "battery"]
10359
+ },
10270
10360
  {
10271
10361
  "path": "desiccantLeftDays",
10272
10362
  "valueType": "number"
@@ -10311,6 +10401,68 @@ Object.freeze({
10311
10401
  {
10312
10402
  "path": "volume",
10313
10403
  "valueType": "number"
10404
+ },
10405
+ {
10406
+ "path": "lastFeedGrams",
10407
+ "valueType": "number"
10408
+ },
10409
+ {
10410
+ "path": "lastFeedHopper1",
10411
+ "valueType": "number"
10412
+ },
10413
+ {
10414
+ "path": "lastFeedHopper2",
10415
+ "valueType": "number"
10416
+ }
10417
+ ],
10418
+ "pet-fountain": [
10419
+ {
10420
+ "path": "waterLevel",
10421
+ "valueType": "number"
10422
+ },
10423
+ {
10424
+ "path": "waterVolume",
10425
+ "valueType": "number"
10426
+ },
10427
+ {
10428
+ "path": "empty",
10429
+ "valueType": "boolean"
10430
+ },
10431
+ {
10432
+ "path": "pumpRunning",
10433
+ "valueType": "boolean"
10434
+ },
10435
+ {
10436
+ "path": "pumpIntervalEnabled",
10437
+ "valueType": "boolean"
10438
+ },
10439
+ {
10440
+ "path": "pumpOnTime",
10441
+ "valueType": "number"
10442
+ },
10443
+ {
10444
+ "path": "pumpOffTime",
10445
+ "valueType": "number"
10446
+ },
10447
+ {
10448
+ "path": "lastDrink",
10449
+ "valueType": "number"
10450
+ },
10451
+ {
10452
+ "path": "currentHourDrink",
10453
+ "valueType": "number"
10454
+ },
10455
+ {
10456
+ "path": "last24hDrink",
10457
+ "valueType": "number"
10458
+ },
10459
+ {
10460
+ "path": "todayDrink",
10461
+ "valueType": "number"
10462
+ },
10463
+ {
10464
+ "path": "error",
10465
+ "valueType": "string"
10314
10466
  }
10315
10467
  ],
10316
10468
  "power-meter": [
@@ -10850,7 +11002,7 @@ object({
10850
11002
  params: record(string(), union([string(), number()])).optional()
10851
11003
  });
10852
11004
  //#endregion
10853
- //#region ../types/dist/err-msg-DIorGYeC.mjs
11005
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
10854
11006
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10855
11007
  DeviceType["Camera"] = "camera";
10856
11008
  DeviceType["Hub"] = "hub";
@@ -10942,6 +11094,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10942
11094
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10943
11095
  * integrations sharing the same food/desiccant/hopper surface. */
10944
11096
  DeviceType["PetFeeder"] = "pet-feeder";
11097
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11098
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11099
+ * of a container and is BUILT as a composed device (D967). */
11100
+ DeviceType["PetFountain"] = "pet-fountain";
10945
11101
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10946
11102
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10947
11103
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11196,6 +11352,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11196
11352
  /** Fallback when the notifier service name doesn't match a known
11197
11353
  * pattern. */
11198
11354
  DeviceRole["GenericNotifier"] = "generic-notifier";
11355
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
11356
+ * cap. A `DeviceType.Generic` device. */
11357
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
11199
11358
  return DeviceRole;
11200
11359
  }({});
11201
11360
  /**
@@ -11209,7 +11368,7 @@ function errMsg(err) {
11209
11368
  return String(err);
11210
11369
  }
11211
11370
  //#endregion
11212
- //#region ../types/dist/composition-DwA0uVW5.mjs
11371
+ //#region ../types/dist/composition-kSuOifuG.mjs
11213
11372
  /**
11214
11373
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11215
11374
  *
@@ -11257,11 +11416,11 @@ function errMsg(err) {
11257
11416
  */
11258
11417
  /** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
11259
11418
  var FFMPEG_RELEASE = "8.1.2-5";
11260
- var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
11419
+ var BASE$1 = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
11261
11420
  function portable(target) {
11262
- return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
11421
+ return `${BASE$1}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
11263
11422
  }
11264
- portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE}${FFMPEG_RELEASE}`;
11423
+ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE$1}${FFMPEG_RELEASE}`;
11265
11424
  /**
11266
11425
  * THE table "which boolean of this device type counts as HIGH", and the
11267
11426
  * pure evaluator of its EDGE.
@@ -12294,12 +12453,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12294
12453
  ])
12295
12454
  })
12296
12455
  ]);
12297
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12456
+ var forwardArgUnion = union([
12457
+ string(),
12458
+ number(),
12459
+ boolean(),
12460
+ strictObject({ ["$input"]: string().min(1) }),
12461
+ record(string().min(1), lazy(() => forwardArgUnion))
12462
+ ]);
12463
+ var CompositionForwardArgSchema = forwardArgUnion;
12464
+ /**
12465
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12466
+ * call of `cap.method` on `source`. Without `args` the built method's input
12467
+ * passes through minus `deviceId`; with `args` the template IS the source's
12468
+ * whole input. `deviceId` is always the source's own, never an argument.
12469
+ */
12470
+ var CompositionForwardCommandSchema = object({
12298
12471
  kind: literal("forward"),
12299
12472
  source: CompositionSourceRefSchema,
12300
12473
  cap: string().min(1),
12301
- method: string().min(1)
12302
- }), CompositionCodeSourceSchema]);
12474
+ method: string().min(1),
12475
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12476
+ });
12477
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12303
12478
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12304
12479
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12305
12480
  /**
@@ -12309,7 +12484,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12309
12484
  */
12310
12485
  var CompositionItemEntrySchema = object({
12311
12486
  label: string().min(1).max(80),
12312
- fields: record(string().min(1), CompositionFieldSourceSchema)
12487
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12488
+ /**
12489
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12490
+ * is resettable and a `reset` call naming its key forwards here. The item key
12491
+ * selects the command, so it is never an argument: there is no pass-through.
12492
+ */
12493
+ reset: CompositionForwardCommandSchema.optional()
12313
12494
  });
12314
12495
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12315
12496
  kind: literal("fields"),
@@ -12321,7 +12502,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12321
12502
  * owns the WHOLE array field: the composed value holds exactly these items.
12322
12503
  */
12323
12504
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12324
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12505
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12325
12506
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12326
12507
  }), object({
12327
12508
  kind: literal("passthrough"),
@@ -16378,6 +16559,449 @@ object({
16378
16559
  lastError: string().nullable()
16379
16560
  });
16380
16561
  /**
16562
+ * notification-output — canonical, capability-gated notification delivery.
16563
+ *
16564
+ * Apprise-derived model (see
16565
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16566
+ * callers emit ONE canonical `Notification`; each provider declares a
16567
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16568
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16569
+ * message to what the kind supports — callers never special-case a service.
16570
+ *
16571
+ * DESIGN DECISIONS (locked):
16572
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16573
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16574
+ * cap. Rationale: the admin UI needs one uniform surface across the
16575
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16576
+ * alternative would fork the UI per addon and cannot host the
16577
+ * discovery→adopt flow.
16578
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16579
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16580
+ * registered provider (notifiers addon + HA addon) so one catalog is
16581
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16582
+ * `addonId` the generated collection router extracts from the call input.
16583
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16584
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16585
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16586
+ * base64 fallback needed.
16587
+ *
16588
+ * TODO (deferred, closed-set change — separate decision): add
16589
+ * `providerKind: 'notify'` so notification providers surface on the unified
16590
+ * admin "Integrations" page.
16591
+ */
16592
+ /**
16593
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16594
+ * adapter picks what it supports and the degrade engine filters the rest.
16595
+ */
16596
+ var AttachmentMediaTypeSchema = _enum([
16597
+ "image",
16598
+ "video",
16599
+ "gif",
16600
+ "audio",
16601
+ "icon"
16602
+ ]);
16603
+ /**
16604
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16605
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16606
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16607
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16608
+ */
16609
+ var AttachmentSchema = object({
16610
+ mediaType: AttachmentMediaTypeSchema,
16611
+ url: string().optional(),
16612
+ bytes: _instanceof(Uint8Array).optional(),
16613
+ mime: string().optional(),
16614
+ name: string().optional()
16615
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16616
+ var NotificationFormatSchema = _enum([
16617
+ "text",
16618
+ "markdown",
16619
+ "html"
16620
+ ]);
16621
+ /**
16622
+ * The CLOSED icon vocabulary an action button may use.
16623
+ *
16624
+ * A closed set, not a free string, and that is the whole point: an arbitrary
16625
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
16626
+ * notices — the same class of gap as a zone vocabulary nothing produced
16627
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
16628
+ * declares `actionIcons: false` and the degrade engine strips the field.
16629
+ *
16630
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
16631
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
16632
+ * differently.
16633
+ *
16634
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
16635
+ *
16636
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
16637
+ * the whole set onto its own renderer's vocabulary through a
16638
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
16639
+ * fallback, so adding a member here fails every adapter's build until someone
16640
+ * decides its glyph, which is the only place that decision can be made
16641
+ * honestly.
16642
+ *
16643
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
16644
+ * `disarm` straight through; iOS feeds that string to
16645
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
16646
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
16647
+ * mapping, and "the field is documented" is not "the value renders".
16648
+ *
16649
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
16650
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
16651
+ * so an addon that emits a token the running hub does not know does not lose an
16652
+ * icon — its whole `send` fails Zod validation and the notification never
16653
+ * arrives. Never emit a new token from an addon before the train carrying it.
16654
+ */
16655
+ var NotificationActionIconSchema = _enum([
16656
+ "acknowledge",
16657
+ "dismiss",
16658
+ "silence",
16659
+ "snooze",
16660
+ "view",
16661
+ "play",
16662
+ "open",
16663
+ "close",
16664
+ "lock",
16665
+ "unlock",
16666
+ "arm",
16667
+ "arm-home",
16668
+ "arm-away",
16669
+ "arm-night",
16670
+ "disarm",
16671
+ "light",
16672
+ "alert",
16673
+ "camera"
16674
+ ]);
16675
+ /** A single tap-through action button. */
16676
+ var NotificationActionSchema = object({
16677
+ id: string(),
16678
+ label: string(),
16679
+ url: string().optional(),
16680
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
16681
+ icon: NotificationActionIconSchema.optional(),
16682
+ /**
16683
+ * Renders in a warning style where the notifier supports it.
16684
+ *
16685
+ * A HINT, never a gate. The callback's authority is its token and nothing
16686
+ * else — see `notification-center/action-token.ts` for what that does and
16687
+ * does not buy.
16688
+ */
16689
+ destructive: boolean().optional(),
16690
+ /**
16691
+ * How the tap should REACH the url.
16692
+ *
16693
+ * `navigate` (absent, and every button authored before this field) opens it:
16694
+ * the phone leaves the notification and shows whatever the callback returns.
16695
+ * That is right for a button whose answer the operator wants to read.
16696
+ *
16697
+ * `background` fires it as a POST and stays put. It exists for the buttons
16698
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
16699
+ * an answer to the notification, and being thrown into a browser tab to
16700
+ * confirm it costs more attention than the notification did. A backend that
16701
+ * cannot do a background call renders it as an ordinary link (the adapters
16702
+ * fall back rather than dropping the button), so this is a preference, never
16703
+ * a requirement.
16704
+ */
16705
+ mode: _enum(["navigate", "background"]).optional()
16706
+ });
16707
+ /** A delivery parameter value: what a notifier declared it takes. */
16708
+ var DeliveryParamValueSchema = union([
16709
+ string(),
16710
+ number(),
16711
+ boolean()
16712
+ ]);
16713
+ /**
16714
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
16715
+ * addresses a target of that kind may set it; a kind that declares nothing is
16716
+ * never handed one. The admin UI derives its control from this descriptor —
16717
+ * no editor knows a service by name.
16718
+ *
16719
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
16720
+ * run on the addon cap path).
16721
+ */
16722
+ var DeliveryParamSchema = object({
16723
+ key: string().min(1),
16724
+ label: string(),
16725
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
16726
+ labelKey: string().optional(),
16727
+ type: _enum([
16728
+ "enum",
16729
+ "number",
16730
+ "boolean",
16731
+ "string"
16732
+ ]),
16733
+ /** `enum` only: the closed set of accepted values. */
16734
+ options: array(object({
16735
+ value: string(),
16736
+ label: string()
16737
+ })).optional(),
16738
+ /** `number` only. */
16739
+ min: number().optional(),
16740
+ max: number().optional(),
16741
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
16742
+ default: DeliveryParamValueSchema.optional()
16743
+ });
16744
+ /**
16745
+ * The canonical notification. `body` is the only hard field (Apprise model).
16746
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16747
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16748
+ * the adapter maps this ordinal onto its native level. `level?` is an
16749
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16750
+ * `priority` for that one target.
16751
+ */
16752
+ var NotificationSchema = object({
16753
+ body: string(),
16754
+ title: string().optional(),
16755
+ format: NotificationFormatSchema.default("text"),
16756
+ priority: number().int().min(1).max(5).default(3),
16757
+ level: string().optional(),
16758
+ attachments: array(AttachmentSchema).optional(),
16759
+ clickUrl: string().optional(),
16760
+ actions: array(NotificationActionSchema).optional(),
16761
+ sound: string().optional(),
16762
+ ttl: number().optional(),
16763
+ tag: string().optional(),
16764
+ deviceId: number().optional(),
16765
+ eventId: string().optional(),
16766
+ metadata: record(string(), unknown()).optional(),
16767
+ /**
16768
+ * Values for the parameters the target's kind DECLARES in
16769
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
16770
+ * declaration by the sender; a notifier reads only the keys it declared.
16771
+ */
16772
+ delivery: record(string(), DeliveryParamValueSchema).optional()
16773
+ });
16774
+ /** One declared native severity/priority level for a kind. */
16775
+ var TargetKindLevelSchema = object({
16776
+ id: string(),
16777
+ label: string(),
16778
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16779
+ ordinal: number().int().min(1).max(5).nullable(),
16780
+ flags: object({
16781
+ critical: boolean().optional(),
16782
+ silent: boolean().optional(),
16783
+ noPush: boolean().optional()
16784
+ }).optional(),
16785
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16786
+ requires: array(string()).optional(),
16787
+ description: string().optional()
16788
+ });
16789
+ /** The full capability block consulted before dispatch. */
16790
+ var TargetKindCapsSchema = object({
16791
+ attachments: object({
16792
+ mediaTypes: array(AttachmentMediaTypeSchema),
16793
+ mode: _enum([
16794
+ "url",
16795
+ "bytes",
16796
+ "both"
16797
+ ]),
16798
+ max: number().int().nonnegative(),
16799
+ maxBytes: number().int().positive().optional()
16800
+ }),
16801
+ /** Max action buttons (0 = none). */
16802
+ actions: number().int().nonnegative(),
16803
+ /**
16804
+ * Whether this kind renders a per-action ICON.
16805
+ *
16806
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
16807
+ * run on the addon cap path — three production failures in one day taught
16808
+ * this repo that once. Absent is read as false by the degrade engine, which
16809
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
16810
+ * assumed and dropped costs the operator's trust in the field.
16811
+ */
16812
+ actionIcons: boolean().optional(),
16813
+ levels: array(TargetKindLevelSchema),
16814
+ format: array(NotificationFormatSchema),
16815
+ clickUrl: boolean(),
16816
+ sound: boolean(),
16817
+ ttl: boolean(),
16818
+ bodyMaxLen: number().int().positive()
16819
+ });
16820
+ /**
16821
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16822
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16823
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16824
+ * the union is large and not meant for runtime validation here; the exported
16825
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16826
+ */
16827
+ var ConfigSchemaPassthrough$3 = unknown();
16828
+ var TargetKindSchema = object({
16829
+ kind: string(),
16830
+ label: string(),
16831
+ icon: string(),
16832
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16833
+ addonId: string(),
16834
+ /**
16835
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
16836
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
16837
+ * when the addon bundles no icon for that kind — the client then falls back
16838
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
16839
+ *
16840
+ * Root-relative on purpose: it resolves against whatever origin serves a web
16841
+ * client, and a native client joins it onto its own hub base.
16842
+ *
16843
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
16844
+ * field that survived only because the runtime cap-router forwards provider
16845
+ * output verbatim — so every consumer had to re-declare it by hand to stop
16846
+ * its own Zod parse from stripping it, and the whole arrangement would have
16847
+ * broken silently the moment output validation was tightened anywhere.
16848
+ */
16849
+ iconUrl: string().optional(),
16850
+ /**
16851
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
16852
+ *
16853
+ * The server knows this and therefore says it, because the client cannot
16854
+ * safely guess: a React-Native client renders SVG and raster through two
16855
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
16856
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
16857
+ * placeholder glyph for every vector icon while the web build looked fine.
16858
+ *
16859
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
16860
+ * not been updated — a client that cannot determine the type should prefer
16861
+ * its raster path, which is the safe default for an unknown image.
16862
+ */
16863
+ iconMediaType: string().optional(),
16864
+ configSchema: ConfigSchemaPassthrough$3,
16865
+ /**
16866
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16867
+ * base URL, instance, chat, recipient). A save that changes any of them
16868
+ * while leaving a secret redacted is refused by the provider (D634), so an
16869
+ * editor uses this to ask for the secret on exactly those edits instead of
16870
+ * on every edit.
16871
+ *
16872
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16873
+ * kinds) and from a provider older than this field — an editor that finds
16874
+ * it absent must assume ANY edit may need the secret.
16875
+ */
16876
+ destinationKeys: array(string()).optional(),
16877
+ supportsDiscovery: boolean(),
16878
+ /**
16879
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
16880
+ * Absent = none; the rule editor then offers nothing.
16881
+ */
16882
+ deliveryParams: array(DeliveryParamSchema).optional(),
16883
+ caps: TargetKindCapsSchema
16884
+ });
16885
+ /**
16886
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16887
+ * (return a presence marker only) when serving `listTargets` — never
16888
+ * round-trip a stored secret to the UI.
16889
+ */
16890
+ var TargetSchema = object({
16891
+ id: string(),
16892
+ name: string(),
16893
+ kind: string(),
16894
+ addonId: string(),
16895
+ enabled: boolean(),
16896
+ config: record(string(), unknown())
16897
+ });
16898
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16899
+ var DiscoveredTargetSchema = object({
16900
+ kind: string(),
16901
+ suggestedName: string(),
16902
+ config: record(string(), unknown())
16903
+ });
16904
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16905
+ var RenderedAsSchema = object({
16906
+ level: string(),
16907
+ format: NotificationFormatSchema,
16908
+ attachmentsSent: number().int().nonnegative(),
16909
+ actionsSent: number().int().nonnegative(),
16910
+ truncated: boolean(),
16911
+ dropped: array(string())
16912
+ });
16913
+ var SendResultSchema = object({
16914
+ success: boolean(),
16915
+ error: string().optional(),
16916
+ renderedAs: RenderedAsSchema.optional()
16917
+ });
16918
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16919
+ var TestResultSchema = SendResultSchema;
16920
+ /**
16921
+ * Pre-save credential probe against a partial (unsaved) target config.
16922
+ * `resolvedUrl` never includes secrets (token stripped from path).
16923
+ */
16924
+ var ProbeTargetConfigResultSchema = object({
16925
+ ok: boolean(),
16926
+ resolvedUrl: string(),
16927
+ httpStatus: number().int().optional(),
16928
+ stateInstance: string().optional(),
16929
+ error: string().optional()
16930
+ });
16931
+ var notificationOutputCapability = {
16932
+ name: "notification-output",
16933
+ scope: "system",
16934
+ mode: "collection",
16935
+ methods: {
16936
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
16937
+ listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
16938
+ /**
16939
+ * One target WITH secrets — admin edit / duplicate hydration only.
16940
+ * `listTargets` stays redacted; the editor loads this so password fields
16941
+ * are filled (masked in the input) and a save can round-trip the token
16942
+ * when a destination changes (Duplicate with a new chatId, etc.).
16943
+ */
16944
+ getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
16945
+ discoverTargets: method(object({
16946
+ kind: string(),
16947
+ config: record(string(), unknown()).optional(),
16948
+ /**
16949
+ * When editing / duplicating, the blank password field never round-trips
16950
+ * the stored token. Pass the source target id so the provider can restore
16951
+ * secrets server-side before dialling (same merge as upsert).
16952
+ */
16953
+ targetId: string().optional()
16954
+ }), array(DiscoveredTargetSchema), { auth: "admin" }),
16955
+ probeTargetConfig: method(object({
16956
+ kind: string(),
16957
+ config: record(string(), unknown()),
16958
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
16959
+ targetId: string().optional()
16960
+ }), ProbeTargetConfigResultSchema, {
16961
+ kind: "mutation",
16962
+ auth: "admin"
16963
+ }),
16964
+ send: method(object({
16965
+ targetId: string(),
16966
+ notification: NotificationSchema
16967
+ }), SendResultSchema, {
16968
+ kind: "mutation",
16969
+ auth: "admin"
16970
+ }),
16971
+ testTarget: method(object({
16972
+ targetId: string(),
16973
+ sample: NotificationSchema.optional()
16974
+ }), TestResultSchema, {
16975
+ kind: "mutation",
16976
+ auth: "admin"
16977
+ }),
16978
+ upsertTarget: method(object({
16979
+ target: TargetSchema,
16980
+ /**
16981
+ * Duplicate flow: create a NEW target id while copying secrets from an
16982
+ * existing row. The UI never holds the stored token; blank / redacted
16983
+ * password fields are restored from this source (D634 still applies if
16984
+ * destinations diverge without a re-typed secret).
16985
+ */
16986
+ cloneSecretsFrom: string().optional()
16987
+ }), TargetSchema, {
16988
+ kind: "mutation",
16989
+ auth: "admin"
16990
+ }),
16991
+ deleteTarget: method(object({ targetId: string() }), _void(), {
16992
+ kind: "mutation",
16993
+ auth: "admin"
16994
+ }),
16995
+ setTargetEnabled: method(object({
16996
+ targetId: string(),
16997
+ enabled: boolean()
16998
+ }), _void(), {
16999
+ kind: "mutation",
17000
+ auth: "admin"
17001
+ })
17002
+ }
17003
+ };
17004
+ /**
16381
17005
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16382
17006
  * surface that admin-ui consumes through `useAddonPagesListPages()`.
16383
17007
  *
@@ -16409,52 +17033,6 @@ var AddonPageInfoSchema = object({
16409
17033
  bundleUrl: string()
16410
17034
  });
16411
17035
  method(_void(), array(AddonPageInfoSchema).readonly());
16412
- /**
16413
- * `addon-pages-source` — collection cap exposing per-provider raw page
16414
- * declarations. Every addon that contributes a UI page registers a
16415
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
16416
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
16417
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
16418
- * that admin-ui consumes.
16419
- *
16420
- * The split exists because the public listing has a different output
16421
- * shape than the per-provider raw declarations, and we want both ends
16422
- * to flow through codegen instead of relying on a hand-written wrapper.
16423
- */
16424
- var AddonPageDeclarationSchema = object({
16425
- id: string(),
16426
- label: string(),
16427
- icon: string(),
16428
- path: string(),
16429
- /**
16430
- * Module Federation remote name — must match the `name` field on the
16431
- * page addon's `federation()` plugin config. Used by admin-ui's
16432
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
16433
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
16434
- * contain hyphens).
16435
- */
16436
- remoteName: string(),
16437
- /**
16438
- * Bundle filename inside the addon's `dist/` dir served at
16439
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
16440
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
16441
- * the static-file route can compute an mtime-based cache-buster URL
16442
- * without a separate filesystem stat.
16443
- */
16444
- bundle: string(),
16445
- /**
16446
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
16447
- * `'cluster'`, `'administration'` — the page renders inside that group.
16448
- * Any OTHER string creates (or joins) a custom section rendered after
16449
- * the built-in groups; its label comes from `sectionLabel` (first
16450
- * declaration wins), falling back to the id. Absent → the legacy
16451
- * "Addon Pages" group.
16452
- */
16453
- section: string().optional(),
16454
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16455
- sectionLabel: string().optional()
16456
- });
16457
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
16458
17036
  var AddonHttpRouteSchema = object({
16459
17037
  method: _enum([
16460
17038
  "GET",
@@ -16755,9 +17333,21 @@ var AlertSeveritySchema = _enum([
16755
17333
  "error",
16756
17334
  "critical"
16757
17335
  ]);
17336
+ /**
17337
+ * An action that RUNS something instead of going somewhere: the admin UI calls
17338
+ * the addon's custom action (`addons.custom`) with exactly these fields. The
17339
+ * producer owns the meaning; the UI knows nothing about the addon or the kind.
17340
+ */
17341
+ var AlertActionInvokeSchema = object({
17342
+ addonId: string().min(1),
17343
+ action: string().min(1),
17344
+ input: record(string(), unknown()).optional()
17345
+ });
17346
+ /** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
16758
17347
  var AlertActionSchema = object({
16759
17348
  label: string(),
16760
17349
  href: string().optional(),
17350
+ invoke: AlertActionInvokeSchema.optional(),
16761
17351
  /**
16762
17352
  * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
16763
17353
  * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
@@ -19159,7 +19749,11 @@ var CompositionProblemSchema = object({
19159
19749
  "mixed-quantity",
19160
19750
  "helper-mismatch",
19161
19751
  "member-constraint",
19162
- "retention"
19752
+ "retention",
19753
+ "command-method-unknown",
19754
+ "command-source-unknown",
19755
+ "command-input-unknown",
19756
+ "command-input-missing"
19163
19757
  ]),
19164
19758
  path: string(),
19165
19759
  message: string()
@@ -19198,7 +19792,9 @@ var CompositionFieldRoleSchema = _enum([
19198
19792
  var CompositionFieldItemSchema = object({
19199
19793
  arrayPath: string(),
19200
19794
  key: string(),
19201
- label: string()
19795
+ label: string(),
19796
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
19797
+ resettable: boolean().optional()
19202
19798
  });
19203
19799
  var CompositionFieldPlanSchema = object({
19204
19800
  cap: string(),
@@ -19403,7 +19999,7 @@ Object.freeze({
19403
19999
  * One catalog row per base reading a group helper can build (D944 §3).
19404
20000
  * Data, not a form default written into the block.
19405
20001
  */
19406
- var NOW$2 = {
20002
+ var NOW$3 = {
19407
20003
  fieldPath: "lastFetchedAt",
19408
20004
  expr: "now"
19409
20005
  };
@@ -19455,7 +20051,7 @@ var GROUP_TEMPLATES = [
19455
20051
  quantity: "temperature",
19456
20052
  defaultAggregate: "mean",
19457
20053
  empty: "null",
19458
- companions: [NOW$2],
20054
+ companions: [NOW$3],
19459
20055
  alsoFrom: {
19460
20056
  deviceTypes: CLIMATE_DEVICES,
19461
20057
  accept: {
@@ -19473,7 +20069,7 @@ var GROUP_TEMPLATES = [
19473
20069
  quantity: "humidity",
19474
20070
  defaultAggregate: "mean",
19475
20071
  empty: "null",
19476
- companions: [NOW$2],
20072
+ companions: [NOW$3],
19477
20073
  alsoFrom: {
19478
20074
  deviceTypes: CLIMATE_DEVICES,
19479
20075
  accept: {
@@ -19491,7 +20087,7 @@ var GROUP_TEMPLATES = [
19491
20087
  quantity: "pressure",
19492
20088
  defaultAggregate: "mean",
19493
20089
  empty: "null",
19494
- companions: [NOW$2]
20090
+ companions: [NOW$3]
19495
20091
  }),
19496
20092
  numeric({
19497
20093
  id: "illuminance",
@@ -19502,7 +20098,7 @@ var GROUP_TEMPLATES = [
19502
20098
  quantity: "illuminance",
19503
20099
  defaultAggregate: "mean",
19504
20100
  empty: "null",
19505
- companions: [NOW$2]
20101
+ companions: [NOW$3]
19506
20102
  }),
19507
20103
  numeric({
19508
20104
  id: "power",
@@ -19513,7 +20109,7 @@ var GROUP_TEMPLATES = [
19513
20109
  quantity: "power",
19514
20110
  defaultAggregate: "sum",
19515
20111
  empty: "omit",
19516
- companions: [NOW$2]
20112
+ companions: [NOW$3]
19517
20113
  }),
19518
20114
  numeric({
19519
20115
  id: "energy",
@@ -19524,7 +20120,7 @@ var GROUP_TEMPLATES = [
19524
20120
  quantity: "energy",
19525
20121
  defaultAggregate: "sum",
19526
20122
  empty: "omit",
19527
- companions: [NOW$2]
20123
+ companions: [NOW$3]
19528
20124
  }),
19529
20125
  numeric({
19530
20126
  id: "battery",
@@ -19559,7 +20155,7 @@ var GROUP_TEMPLATES = [
19559
20155
  quantity: "device",
19560
20156
  defaultAggregate: "mean",
19561
20157
  empty: "null",
19562
- companions: [NOW$2]
20158
+ companions: [NOW$3]
19563
20159
  }),
19564
20160
  binary({
19565
20161
  id: "motion",
@@ -22033,12 +22629,48 @@ var ExposeInputSchema = object({
22033
22629
  capabilities: array(string()).optional()
22034
22630
  });
22035
22631
  var UnexposeInputSchema = object({ deviceId: string() });
22632
+ /**
22633
+ * What an exporter may expose besides devices (D968). An action and a
22634
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
22635
+ * exporter's own selection, not the device registry.
22636
+ */
22637
+ var ExportEntityKindSchema = _enum([
22638
+ "device",
22639
+ "action",
22640
+ "notifier"
22641
+ ]);
22642
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
22643
+ object({
22644
+ kind: literal("device"),
22645
+ deviceId: string()
22646
+ }),
22647
+ object({
22648
+ kind: literal("action"),
22649
+ actionId: string().min(1)
22650
+ }),
22651
+ object({
22652
+ kind: literal("notifier"),
22653
+ targetId: string().min(1)
22654
+ })
22655
+ ]);
22656
+ var ExposedEntitySchema = object({
22657
+ entity: ExportEntityRefSchema,
22658
+ exposedAs: string().optional()
22659
+ });
22660
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22661
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22036
22662
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
22037
22663
  kind: "mutation",
22038
22664
  auth: "admin"
22039
22665
  }), method(UnexposeInputSchema, _void(), {
22040
22666
  kind: "mutation",
22041
22667
  auth: "admin"
22668
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
22669
+ kind: "mutation",
22670
+ auth: "admin"
22671
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
22672
+ kind: "mutation",
22673
+ auth: "admin"
22042
22674
  });
22043
22675
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
22044
22676
  var ICON_CATEGORY_IDS = [
@@ -43260,6 +43892,14 @@ method(object({
43260
43892
  kind: "mutation",
43261
43893
  auth: "admin"
43262
43894
  });
43895
+ method(object({
43896
+ nodeId: string().min(1),
43897
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
43898
+ removedAt: number()
43899
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
43900
+ kind: "mutation",
43901
+ auth: "admin"
43902
+ });
43263
43903
  /**
43264
43904
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
43265
43905
  * so it can delete what it keeps for that device (D865).
@@ -43980,7 +44620,7 @@ var LlmProviderWidgetSchema = object({
43980
44620
  /** ConfigUISchema tree passed through untyped on the wire (the
43981
44621
  * notification-output `ConfigSchemaPassthrough` precedent at
43982
44622
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
43983
- var ConfigSchemaPassthrough$3 = unknown();
44623
+ var ConfigSchemaPassthrough$2 = unknown();
43984
44624
  var LlmProfileKindDescriptorSchema = object({
43985
44625
  kind: LlmProfileKindSchema,
43986
44626
  label: string(),
@@ -43989,9 +44629,9 @@ var LlmProfileKindDescriptorSchema = object({
43989
44629
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
43990
44630
  addonId: string(),
43991
44631
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
43992
- connectionSchema: ConfigSchemaPassthrough$3,
44632
+ connectionSchema: ConfigSchemaPassthrough$2,
43993
44633
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
43994
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
44634
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
43995
44635
  /** The kind's defaults; a model from `listModels` may override any of them. */
43996
44636
  features: LlmModelFeaturesSchema,
43997
44637
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -44517,10 +45157,10 @@ var CpuBreakdownSchema = object({
44517
45157
  cores: number()
44518
45158
  });
44519
45159
  var MemoryInfoSchema = object({
44520
- percent: number(),
45160
+ percent: number().nullable(),
44521
45161
  totalBytes: number(),
44522
- usedBytes: number(),
44523
- availableBytes: number(),
45162
+ usedBytes: number().nullable(),
45163
+ availableBytes: number().nullable(),
44524
45164
  swapUsedBytes: number(),
44525
45165
  swapTotalBytes: number()
44526
45166
  });
@@ -44824,8 +45464,9 @@ var GetLoadSeriesInputSchema = object({
44824
45464
  });
44825
45465
  var SystemMetricsSchema = object({
44826
45466
  cpuPercent: number(),
44827
- memoryPercent: number(),
44828
- memoryUsedMB: number(),
45467
+ /** `null` when the memory sample failed or was not taken yet (D393). */
45468
+ memoryPercent: number().nullable(),
45469
+ memoryUsedMB: number().nullable(),
44829
45470
  memoryTotalMB: number(),
44830
45471
  diskPercent: number().optional(),
44831
45472
  temperature: number().optional(),
@@ -44945,565 +45586,122 @@ var BrokerInfoSchema$1 = object({
44945
45586
  error: string().optional(),
44946
45587
  /** Embedded brokers only: number of MQTT clients currently connected. */
44947
45588
  connectedClients: number().int().nonnegative().optional(),
44948
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
44949
- lastCheckedAt: number().optional()
44950
- });
44951
- /**
44952
- * Connection details — what a consumer needs to call
44953
- * `mqtt.connect(url, options)`. We split URL + credentials so the
44954
- * consumer can pass them as `mqtt.connect(url, { username, password })`
44955
- * instead of stuffing creds into the URL (which leaks them into logs).
44956
- */
44957
- var BrokerConnectionDetailsSchema = object({
44958
- url: string(),
44959
- username: string().optional(),
44960
- password: string().optional(),
44961
- /**
44962
- * Suggested prefix for `clientId`. Each consumer should suffix this
44963
- * with its own discriminator (addon id, instance id) so reconnects
44964
- * don't kick each other off (MQTT spec: clientId must be unique per
44965
- * broker).
44966
- */
44967
- clientIdPrefix: string().optional()
44968
- });
44969
- var AddBrokerInputSchema = object({
44970
- name: string().min(1),
44971
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
44972
- username: string().optional(),
44973
- password: string().optional(),
44974
- clientIdPrefix: string().optional()
44975
- });
44976
- var AddBrokerResultSchema = object({ id: string() });
44977
- var IdInputSchema = object({ id: string() });
44978
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
44979
- ok: literal(true),
44980
- latencyMs: number()
44981
- }), object({
44982
- ok: literal(false),
44983
- error: string()
44984
- })]);
44985
- var StartEmbeddedInputSchema = object({
44986
- port: number().int().min(1).max(65535).default(1883),
44987
- /** Allow anonymous connect (no username/password). Default: false. */
44988
- allowAnonymous: boolean().default(false),
44989
- /** Optional shared username/password for clients. */
44990
- username: string().optional(),
44991
- password: string().optional()
44992
- });
44993
- var StartEmbeddedResultSchema = object({
44994
- id: string(),
44995
- url: string()
44996
- });
44997
- var StatusSchema = object({
44998
- brokerCount: number(),
44999
- embeddedRunning: boolean()
45000
- });
45001
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45002
- kind: "mutation",
45003
- auth: "admin"
45004
- }), method(IdInputSchema, _void(), {
45005
- kind: "mutation",
45006
- auth: "admin"
45007
- }), method(IdInputSchema, TestResultSchema$1, {
45008
- kind: "mutation",
45009
- auth: "admin"
45010
- }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45011
- kind: "mutation",
45012
- auth: "admin"
45013
- }), method(IdInputSchema, _void(), {
45014
- kind: "mutation",
45015
- auth: "admin"
45016
- }), method(_void(), StatusSchema);
45017
- var NetworkEndpointSchema = object({
45018
- url: string(),
45019
- hostname: string(),
45020
- port: number(),
45021
- protocol: _enum(["http", "https"]),
45022
- /**
45023
- * Whether this address survives a restart of its provider. `false` = it is
45024
- * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45025
- * hub never selects such an address AUTOMATICALLY for a role (D860), though
45026
- * it stays visible and an operator may still flag it. Absent = stable: every
45027
- * provider except a Quick tunnel owns a name that does not change.
45028
- */
45029
- stable: boolean().optional()
45030
- });
45031
- object({
45032
- url: string(),
45033
- /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45034
- stable: boolean().optional(),
45035
- updated: boolean().optional()
45036
- });
45037
- var NetworkAccessStatusSchema = object({
45038
- connected: boolean(),
45039
- endpoint: NetworkEndpointSchema.nullable(),
45040
- error: string().optional()
45041
- });
45042
- /**
45043
- * Optional, richer endpoint shape returned by providers that expose
45044
- * MORE than one ingress concurrently (Tailscale Funnel with mixed
45045
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45046
- * the originating provider config (mode + sourcePort) so the
45047
- * orchestrator UI can label rows distinctly. Providers that expose only
45048
- * one endpoint just omit `listEndpoints` from their provider impl.
45049
- */
45050
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45051
- /**
45052
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
45053
- * the orchestrator can dedupe across `listEndpoints` polls.
45054
- */
45055
- id: string(),
45056
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45057
- label: string(),
45058
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45059
- mode: string().optional(),
45060
- /** Originating local port the ingress fronts (informational). */
45061
- sourcePort: number().optional()
45062
- });
45063
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45064
- /**
45065
- * notification-output — canonical, capability-gated notification delivery.
45066
- *
45067
- * Apprise-derived model (see
45068
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
45069
- * callers emit ONE canonical `Notification`; each provider declares a
45070
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
45071
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
45072
- * message to what the kind supports — callers never special-case a service.
45073
- *
45074
- * DESIGN DECISIONS (locked):
45075
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
45076
- * `setTargetEnabled`), each provider persisting via the `settings-store`
45077
- * cap. Rationale: the admin UI needs one uniform surface across the
45078
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
45079
- * alternative would fork the UI per addon and cannot host the
45080
- * discovery→adopt flow.
45081
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
45082
- * the generated cap-mount auto-`concatCollection`-fans them across every
45083
- * registered provider (notifiers addon + HA addon) so one catalog is
45084
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
45085
- * `addonId` the generated collection router extracts from the call input.
45086
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
45087
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
45088
- * `storage` / `storage-provider` / `recording` caps over the same path. No
45089
- * base64 fallback needed.
45090
- *
45091
- * TODO (deferred, closed-set change — separate decision): add
45092
- * `providerKind: 'notify'` so notification providers surface on the unified
45093
- * admin "Integrations" page.
45094
- */
45095
- /**
45096
- * Zentik-derived typed-media enum — the superset across every kind. Each
45097
- * adapter picks what it supports and the degrade engine filters the rest.
45098
- */
45099
- var AttachmentMediaTypeSchema = _enum([
45100
- "image",
45101
- "video",
45102
- "gif",
45103
- "audio",
45104
- "icon"
45105
- ]);
45106
- /**
45107
- * A single attachment. Exactly one of `url` (remote source, most adapters
45108
- * prefer this) or `bytes` (inline source; required for Pushover-style
45109
- * bytes-only kinds) MUST be present — the degrade engine expresses a
45110
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
45111
- */
45112
- var AttachmentSchema = object({
45113
- mediaType: AttachmentMediaTypeSchema,
45114
- url: string().optional(),
45115
- bytes: _instanceof(Uint8Array).optional(),
45116
- mime: string().optional(),
45117
- name: string().optional()
45118
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
45119
- var NotificationFormatSchema = _enum([
45120
- "text",
45121
- "markdown",
45122
- "html"
45123
- ]);
45124
- /**
45125
- * The CLOSED icon vocabulary an action button may use.
45126
- *
45127
- * A closed set, not a free string, and that is the whole point: an arbitrary
45128
- * icon name is one that ntfy renders, zentik silently drops, and nobody
45129
- * notices — the same class of gap as a zone vocabulary nothing produced
45130
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
45131
- * declares `actionIcons: false` and the degrade engine strips the field.
45132
- *
45133
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
45134
- * renderer's icon set; "acknowledge" survives an adapter that draws it
45135
- * differently.
45136
- *
45137
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
45138
- *
45139
- * These names are for US. **No adapter may forward one verbatim.** Each maps
45140
- * the whole set onto its own renderer's vocabulary through a
45141
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
45142
- * fallback, so adding a member here fails every adapter's build until someone
45143
- * decides its glyph, which is the only place that decision can be made
45144
- * honestly.
45145
- *
45146
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
45147
- * `disarm` straight through; iOS feeds that string to
45148
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
45149
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
45150
- * mapping, and "the field is documented" is not "the value renders".
45151
- *
45152
- * Adding a member is TRAIN-BOUND. The enum lives in the published
45153
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
45154
- * so an addon that emits a token the running hub does not know does not lose an
45155
- * icon — its whole `send` fails Zod validation and the notification never
45156
- * arrives. Never emit a new token from an addon before the train carrying it.
45157
- */
45158
- var NotificationActionIconSchema = _enum([
45159
- "acknowledge",
45160
- "dismiss",
45161
- "silence",
45162
- "snooze",
45163
- "view",
45164
- "play",
45165
- "open",
45166
- "close",
45167
- "lock",
45168
- "unlock",
45169
- "arm",
45170
- "arm-home",
45171
- "arm-away",
45172
- "arm-night",
45173
- "disarm",
45174
- "light",
45175
- "alert",
45176
- "camera"
45177
- ]);
45178
- /** A single tap-through action button. */
45179
- var NotificationActionSchema = object({
45180
- id: string(),
45181
- label: string(),
45182
- url: string().optional(),
45183
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
45184
- icon: NotificationActionIconSchema.optional(),
45185
- /**
45186
- * Renders in a warning style where the notifier supports it.
45187
- *
45188
- * A HINT, never a gate. The callback's authority is its token and nothing
45189
- * else — see `notification-center/action-token.ts` for what that does and
45190
- * does not buy.
45191
- */
45192
- destructive: boolean().optional(),
45193
- /**
45194
- * How the tap should REACH the url.
45195
- *
45196
- * `navigate` (absent, and every button authored before this field) opens it:
45197
- * the phone leaves the notification and shows whatever the callback returns.
45198
- * That is right for a button whose answer the operator wants to read.
45199
- *
45200
- * `background` fires it as a POST and stays put. It exists for the buttons
45201
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
45202
- * an answer to the notification, and being thrown into a browser tab to
45203
- * confirm it costs more attention than the notification did. A backend that
45204
- * cannot do a background call renders it as an ordinary link (the adapters
45205
- * fall back rather than dropping the button), so this is a preference, never
45206
- * a requirement.
45207
- */
45208
- mode: _enum(["navigate", "background"]).optional()
45209
- });
45210
- /** A delivery parameter value: what a notifier declared it takes. */
45211
- var DeliveryParamValueSchema = union([
45212
- string(),
45213
- number(),
45214
- boolean()
45215
- ]);
45216
- /**
45217
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
45218
- * addresses a target of that kind may set it; a kind that declares nothing is
45219
- * never handed one. The admin UI derives its control from this descriptor —
45220
- * no editor knows a service by name.
45221
- *
45222
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
45223
- * run on the addon cap path).
45224
- */
45225
- var DeliveryParamSchema = object({
45226
- key: string().min(1),
45227
- label: string(),
45228
- /** i18n key of the label, resolved by the declaring addon's namespace. */
45229
- labelKey: string().optional(),
45230
- type: _enum([
45231
- "enum",
45232
- "number",
45233
- "boolean",
45234
- "string"
45235
- ]),
45236
- /** `enum` only: the closed set of accepted values. */
45237
- options: array(object({
45238
- value: string(),
45239
- label: string()
45240
- })).optional(),
45241
- /** `number` only. */
45242
- min: number().optional(),
45243
- max: number().optional(),
45244
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
45245
- default: DeliveryParamValueSchema.optional()
45246
- });
45247
- /**
45248
- * The canonical notification. `body` is the only hard field (Apprise model).
45249
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
45250
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
45251
- * the adapter maps this ordinal onto its native level. `level?` is an
45252
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
45253
- * `priority` for that one target.
45254
- */
45255
- var NotificationSchema = object({
45256
- body: string(),
45257
- title: string().optional(),
45258
- format: NotificationFormatSchema.default("text"),
45259
- priority: number().int().min(1).max(5).default(3),
45260
- level: string().optional(),
45261
- attachments: array(AttachmentSchema).optional(),
45262
- clickUrl: string().optional(),
45263
- actions: array(NotificationActionSchema).optional(),
45264
- sound: string().optional(),
45265
- ttl: number().optional(),
45266
- tag: string().optional(),
45267
- deviceId: number().optional(),
45268
- eventId: string().optional(),
45269
- metadata: record(string(), unknown()).optional(),
45270
- /**
45271
- * Values for the parameters the target's kind DECLARES in
45272
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
45273
- * declaration by the sender; a notifier reads only the keys it declared.
45274
- */
45275
- delivery: record(string(), DeliveryParamValueSchema).optional()
45276
- });
45277
- /** One declared native severity/priority level for a kind. */
45278
- var TargetKindLevelSchema = object({
45279
- id: string(),
45280
- label: string(),
45281
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
45282
- ordinal: number().int().min(1).max(5).nullable(),
45283
- flags: object({
45284
- critical: boolean().optional(),
45285
- silent: boolean().optional(),
45286
- noPush: boolean().optional()
45287
- }).optional(),
45288
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
45289
- requires: array(string()).optional(),
45290
- description: string().optional()
45291
- });
45292
- /** The full capability block consulted before dispatch. */
45293
- var TargetKindCapsSchema = object({
45294
- attachments: object({
45295
- mediaTypes: array(AttachmentMediaTypeSchema),
45296
- mode: _enum([
45297
- "url",
45298
- "bytes",
45299
- "both"
45300
- ]),
45301
- max: number().int().nonnegative(),
45302
- maxBytes: number().int().positive().optional()
45303
- }),
45304
- /** Max action buttons (0 = none). */
45305
- actions: number().int().nonnegative(),
45306
- /**
45307
- * Whether this kind renders a per-action ICON.
45308
- *
45309
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
45310
- * run on the addon cap path — three production failures in one day taught
45311
- * this repo that once. Absent is read as false by the degrade engine, which
45312
- * is the safe direction: an icon that is not rendered costs nothing, an icon
45313
- * assumed and dropped costs the operator's trust in the field.
45314
- */
45315
- actionIcons: boolean().optional(),
45316
- levels: array(TargetKindLevelSchema),
45317
- format: array(NotificationFormatSchema),
45318
- clickUrl: boolean(),
45319
- sound: boolean(),
45320
- ttl: boolean(),
45321
- bodyMaxLen: number().int().positive()
45589
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
45590
+ lastCheckedAt: number().optional()
45322
45591
  });
45323
45592
  /**
45324
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
45325
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
45326
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
45327
- * the union is large and not meant for runtime validation here; the exported
45328
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
45593
+ * Connection details — what a consumer needs to call
45594
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
45595
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
45596
+ * instead of stuffing creds into the URL (which leaks them into logs).
45329
45597
  */
45330
- var ConfigSchemaPassthrough$2 = unknown();
45331
- var TargetKindSchema = object({
45332
- kind: string(),
45333
- label: string(),
45334
- icon: string(),
45335
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
45336
- addonId: string(),
45337
- /**
45338
- * URL of the kind's bundled BRAND icon, served by the providing addon over
45339
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
45340
- * when the addon bundles no icon for that kind — the client then falls back
45341
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
45342
- *
45343
- * Root-relative on purpose: it resolves against whatever origin serves a web
45344
- * client, and a native client joins it onto its own hub base.
45345
- *
45346
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
45347
- * field that survived only because the runtime cap-router forwards provider
45348
- * output verbatim — so every consumer had to re-declare it by hand to stop
45349
- * its own Zod parse from stripping it, and the whole arrangement would have
45350
- * broken silently the moment output validation was tightened anywhere.
45351
- */
45352
- iconUrl: string().optional(),
45353
- /**
45354
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
45355
- *
45356
- * The server knows this and therefore says it, because the client cannot
45357
- * safely guess: a React-Native client renders SVG and raster through two
45358
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
45359
- * not decode SVG on iOS/Android), so without this it silently fell back to a
45360
- * placeholder glyph for every vector icon while the web build looked fine.
45361
- *
45362
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
45363
- * not been updated — a client that cannot determine the type should prefer
45364
- * its raster path, which is the safe default for an unknown image.
45365
- */
45366
- iconMediaType: string().optional(),
45367
- configSchema: ConfigSchemaPassthrough$2,
45368
- /**
45369
- * The config keys that decide WHERE this kind's secrets are sent (server,
45370
- * base URL, instance, chat, recipient). A save that changes any of them
45371
- * while leaving a secret redacted is refused by the provider (D634), so an
45372
- * editor uses this to ask for the secret on exactly those edits instead of
45373
- * on every edit.
45374
- *
45375
- * Optional: absent from a provider that holds no secret (the Home Assistant
45376
- * kinds) and from a provider older than this field — an editor that finds
45377
- * it absent must assume ANY edit may need the secret.
45378
- */
45379
- destinationKeys: array(string()).optional(),
45380
- supportsDiscovery: boolean(),
45598
+ var BrokerConnectionDetailsSchema = object({
45599
+ url: string(),
45600
+ username: string().optional(),
45601
+ password: string().optional(),
45381
45602
  /**
45382
- * Extra delivery parameters a rule may set for a target of this kind (D938).
45383
- * Absent = none; the rule editor then offers nothing.
45603
+ * Suggested prefix for `clientId`. Each consumer should suffix this
45604
+ * with its own discriminator (addon id, instance id) so reconnects
45605
+ * don't kick each other off (MQTT spec: clientId must be unique per
45606
+ * broker).
45384
45607
  */
45385
- deliveryParams: array(DeliveryParamSchema).optional(),
45386
- caps: TargetKindCapsSchema
45608
+ clientIdPrefix: string().optional()
45387
45609
  });
45388
- /**
45389
- * A persisted target. `config` holds secrets; providers REDACT secret fields
45390
- * (return a presence marker only) when serving `listTargets` — never
45391
- * round-trip a stored secret to the UI.
45392
- */
45393
- var TargetSchema = object({
45610
+ var AddBrokerInputSchema = object({
45611
+ name: string().min(1),
45612
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45613
+ username: string().optional(),
45614
+ password: string().optional(),
45615
+ clientIdPrefix: string().optional()
45616
+ });
45617
+ var AddBrokerResultSchema = object({ id: string() });
45618
+ var IdInputSchema = object({ id: string() });
45619
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
45620
+ ok: literal(true),
45621
+ latencyMs: number()
45622
+ }), object({
45623
+ ok: literal(false),
45624
+ error: string()
45625
+ })]);
45626
+ var StartEmbeddedInputSchema = object({
45627
+ port: number().int().min(1).max(65535).default(1883),
45628
+ /** Allow anonymous connect (no username/password). Default: false. */
45629
+ allowAnonymous: boolean().default(false),
45630
+ /** Optional shared username/password for clients. */
45631
+ username: string().optional(),
45632
+ password: string().optional()
45633
+ });
45634
+ var StartEmbeddedResultSchema = object({
45394
45635
  id: string(),
45395
- name: string(),
45396
- kind: string(),
45397
- addonId: string(),
45398
- enabled: boolean(),
45399
- config: record(string(), unknown())
45636
+ url: string()
45400
45637
  });
45401
- /** A discovery-surfaced candidate (config is partial + non-secret). */
45402
- var DiscoveredTargetSchema = object({
45403
- kind: string(),
45404
- suggestedName: string(),
45405
- config: record(string(), unknown())
45638
+ var StatusSchema = object({
45639
+ brokerCount: number(),
45640
+ embeddedRunning: boolean()
45406
45641
  });
45407
- /** The degrade engine's report — what was resolved / dropped / degraded. */
45408
- var RenderedAsSchema = object({
45409
- level: string(),
45410
- format: NotificationFormatSchema,
45411
- attachmentsSent: number().int().nonnegative(),
45412
- actionsSent: number().int().nonnegative(),
45413
- truncated: boolean(),
45414
- dropped: array(string())
45642
+ method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45643
+ kind: "mutation",
45644
+ auth: "admin"
45645
+ }), method(IdInputSchema, _void(), {
45646
+ kind: "mutation",
45647
+ auth: "admin"
45648
+ }), method(IdInputSchema, TestResultSchema$1, {
45649
+ kind: "mutation",
45650
+ auth: "admin"
45651
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45652
+ kind: "mutation",
45653
+ auth: "admin"
45654
+ }), method(IdInputSchema, _void(), {
45655
+ kind: "mutation",
45656
+ auth: "admin"
45657
+ }), method(_void(), StatusSchema);
45658
+ var NetworkEndpointSchema = object({
45659
+ url: string(),
45660
+ hostname: string(),
45661
+ port: number(),
45662
+ protocol: _enum(["http", "https"]),
45663
+ /**
45664
+ * Whether this address survives a restart of its provider. `false` = it is
45665
+ * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45666
+ * hub never selects such an address AUTOMATICALLY for a role (D860), though
45667
+ * it stays visible and an operator may still flag it. Absent = stable: every
45668
+ * provider except a Quick tunnel owns a name that does not change.
45669
+ */
45670
+ stable: boolean().optional()
45415
45671
  });
45416
- var SendResultSchema = object({
45417
- success: boolean(),
45418
- error: string().optional(),
45419
- renderedAs: RenderedAsSchema.optional()
45672
+ object({
45673
+ url: string(),
45674
+ /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45675
+ stable: boolean().optional(),
45676
+ updated: boolean().optional()
45677
+ });
45678
+ var NetworkAccessStatusSchema = object({
45679
+ connected: boolean(),
45680
+ endpoint: NetworkEndpointSchema.nullable(),
45681
+ error: string().optional()
45420
45682
  });
45421
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
45422
- var TestResultSchema = SendResultSchema;
45423
45683
  /**
45424
- * Pre-save credential probe against a partial (unsaved) target config.
45425
- * `resolvedUrl` never includes secrets (token stripped from path).
45684
+ * Optional, richer endpoint shape returned by providers that expose
45685
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
45686
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45687
+ * the originating provider config (mode + sourcePort) so the
45688
+ * orchestrator UI can label rows distinctly. Providers that expose only
45689
+ * one endpoint just omit `listEndpoints` from their provider impl.
45426
45690
  */
45427
- var ProbeTargetConfigResultSchema = object({
45428
- ok: boolean(),
45429
- resolvedUrl: string(),
45430
- httpStatus: number().int().optional(),
45431
- stateInstance: string().optional(),
45432
- error: string().optional()
45691
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45692
+ /**
45693
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
45694
+ * the orchestrator can dedupe across `listEndpoints` polls.
45695
+ */
45696
+ id: string(),
45697
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45698
+ label: string(),
45699
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45700
+ mode: string().optional(),
45701
+ /** Originating local port the ingress fronts (informational). */
45702
+ sourcePort: number().optional()
45433
45703
  });
45434
- var notificationOutputCapability = {
45435
- name: "notification-output",
45436
- scope: "system",
45437
- mode: "collection",
45438
- methods: {
45439
- listTargetKinds: method(object({}), array(TargetKindSchema)),
45440
- listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
45441
- /**
45442
- * One target WITH secrets — admin edit / duplicate hydration only.
45443
- * `listTargets` stays redacted; the editor loads this so password fields
45444
- * are filled (masked in the input) and a save can round-trip the token
45445
- * when a destination changes (Duplicate with a new chatId, etc.).
45446
- */
45447
- getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
45448
- discoverTargets: method(object({
45449
- kind: string(),
45450
- config: record(string(), unknown()).optional(),
45451
- /**
45452
- * When editing / duplicating, the blank password field never round-trips
45453
- * the stored token. Pass the source target id so the provider can restore
45454
- * secrets server-side before dialling (same merge as upsert).
45455
- */
45456
- targetId: string().optional()
45457
- }), array(DiscoveredTargetSchema), { auth: "admin" }),
45458
- probeTargetConfig: method(object({
45459
- kind: string(),
45460
- config: record(string(), unknown()),
45461
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
45462
- targetId: string().optional()
45463
- }), ProbeTargetConfigResultSchema, {
45464
- kind: "mutation",
45465
- auth: "admin"
45466
- }),
45467
- send: method(object({
45468
- targetId: string(),
45469
- notification: NotificationSchema
45470
- }), SendResultSchema, {
45471
- kind: "mutation",
45472
- auth: "admin"
45473
- }),
45474
- testTarget: method(object({
45475
- targetId: string(),
45476
- sample: NotificationSchema.optional()
45477
- }), TestResultSchema, {
45478
- kind: "mutation",
45479
- auth: "admin"
45480
- }),
45481
- upsertTarget: method(object({
45482
- target: TargetSchema,
45483
- /**
45484
- * Duplicate flow: create a NEW target id while copying secrets from an
45485
- * existing row. The UI never holds the stored token; blank / redacted
45486
- * password fields are restored from this source (D634 still applies if
45487
- * destinations diverge without a re-typed secret).
45488
- */
45489
- cloneSecretsFrom: string().optional()
45490
- }), TargetSchema, {
45491
- kind: "mutation",
45492
- auth: "admin"
45493
- }),
45494
- deleteTarget: method(object({ targetId: string() }), _void(), {
45495
- kind: "mutation",
45496
- auth: "admin"
45497
- }),
45498
- setTargetEnabled: method(object({
45499
- targetId: string(),
45500
- enabled: boolean()
45501
- }), _void(), {
45502
- kind: "mutation",
45503
- auth: "admin"
45504
- })
45505
- }
45506
- };
45704
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45507
45705
  var AUDIO_MACRO_LABELS = [
45508
45706
  {
45509
45707
  id: "speech",
@@ -51948,14 +52146,6 @@ var EngineProvisioningSchema = object({
51948
52146
  */
51949
52147
  configIssues: array(string()).optional()
51950
52148
  });
51951
- var PipelineStepInputSchema = lazy(() => object({
51952
- addonId: string(),
51953
- modelId: string().optional(),
51954
- enabled: boolean().default(true),
51955
- children: array(PipelineStepInputSchema).optional(),
51956
- settings: record(string(), unknown()).optional(),
51957
- jumpDeviceKey: string().optional()
51958
- }));
51959
52149
  var ModelSubstitutionSchema = object({
51960
52150
  addonId: string(),
51961
52151
  chosen: string(),
@@ -52034,7 +52224,6 @@ var ConfigUISchemaBridge = custom();
52034
52224
  var ConfigUISchemaNullableBridge = custom();
52035
52225
  var InferenceCapabilitiesBridge = custom();
52036
52226
  var ModelAvailabilityListBridge = custom();
52037
- var PipelineRunResultBridge = custom();
52038
52227
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
52039
52228
  modelId: string(),
52040
52229
  settings: record(string(), unknown()).readonly()
@@ -55240,6 +55429,12 @@ var ServerUpdateActionResultSchema = object({
55240
55429
  */
55241
55430
  blockers: array(UpdateBlockerSchema).optional()
55242
55431
  });
55432
+ var LaunchOverridesSchema = object({
55433
+ /** The persisted heap ceiling in MB, or `null` when none is set. */
55434
+ heapMb: number().int().nullable() });
55435
+ var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
55436
+ /** Always true: the override is read at launch, not by the running process. */
55437
+ restartRequired: boolean() });
55243
55438
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
55244
55439
  kind: "mutation",
55245
55440
  auth: "admin"
@@ -55262,6 +55457,9 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
55262
55457
  }), method(_void(), ServerUpdateActionResultSchema, {
55263
55458
  kind: "mutation",
55264
55459
  auth: "admin"
55460
+ }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(object({ heapMb: number().int().min(512).max(65536).nullable() }), SetLaunchOverridesResultSchema, {
55461
+ kind: "mutation",
55462
+ auth: "admin"
55265
55463
  });
55266
55464
  /**
55267
55465
  * `smtp-provider` — pluggable email delivery surface.
@@ -58828,6 +59026,318 @@ onBrightnessChanged: { data: object({
58828
59026
  } } }
58829
59027
  };
58830
59028
  /**
59029
+ * The role vocabulary of a remote (D963): what a button MEANS, independent of
59030
+ * how it is wired. A layout template places buttons by role; a device binds a
59031
+ * role to an action; an exporter asks for a role by name. Roles are shared so
59032
+ * one layout fits every appliance and one voice command fits every binding.
59033
+ *
59034
+ * A role outside this list is `custom:<key>` — the operator's own button (a
59035
+ * SmartIR source, a channel preset). The key is a lowercase slug.
59036
+ */
59037
+ var REMOTE_KNOWN_ROLES = [
59038
+ "power",
59039
+ "power_on",
59040
+ "power_off",
59041
+ "volume_up",
59042
+ "volume_down",
59043
+ "mute",
59044
+ "channel_up",
59045
+ "channel_down",
59046
+ "up",
59047
+ "down",
59048
+ "left",
59049
+ "right",
59050
+ "ok",
59051
+ "back",
59052
+ "home",
59053
+ "menu",
59054
+ "info",
59055
+ "guide",
59056
+ "exit",
59057
+ "input",
59058
+ "play",
59059
+ "pause",
59060
+ "play_pause",
59061
+ "stop",
59062
+ "next",
59063
+ "previous",
59064
+ "fast_forward",
59065
+ "rewind",
59066
+ "digit_0",
59067
+ "digit_1",
59068
+ "digit_2",
59069
+ "digit_3",
59070
+ "digit_4",
59071
+ "digit_5",
59072
+ "digit_6",
59073
+ "digit_7",
59074
+ "digit_8",
59075
+ "digit_9"
59076
+ ];
59077
+ var CUSTOM_KEY_RE = /^[a-z0-9][a-z0-9_]{0,47}$/;
59078
+ var KNOWN_ROLES = new Set(REMOTE_KNOWN_ROLES);
59079
+ function isRemoteKnownRole(value) {
59080
+ return KNOWN_ROLES.has(value);
59081
+ }
59082
+ function isRemoteCustomKey(key) {
59083
+ return CUSTOM_KEY_RE.test(key);
59084
+ }
59085
+ /** The key of a `custom:<key>` role, or null for any other string. */
59086
+ function customKeyOfRole(role) {
59087
+ if (!role.startsWith("custom:")) return null;
59088
+ const key = role.slice(7);
59089
+ return isRemoteCustomKey(key) ? key : null;
59090
+ }
59091
+ function isRemoteRole(value) {
59092
+ return isRemoteKnownRole(value) || customKeyOfRole(value) !== null;
59093
+ }
59094
+ var RemoteRoleSchema = string().refine(isRemoteRole, { message: "a role is one of the known roles or custom:<key> (lowercase letters, digits, _)" });
59095
+ var RemoteBindingSchema = object({ action: AutomationCapStepSchema.extend({ deviceId: number().int().nonnegative() }) });
59096
+ var RemoteCustomButtonSchema = object({
59097
+ /** The slug after `custom:` in the role. */
59098
+ key: string().refine(isRemoteCustomKey, { message: "a lowercase slug" }),
59099
+ /** What the operator calls it, and what a voice command may name. */
59100
+ name: string().min(1).max(60)
59101
+ });
59102
+ var RemotePowerModeSchema = _enum(["toggle", "discrete"]);
59103
+ var RemoteConfigSchema = object({
59104
+ layoutTemplateId: string().min(1).nullable(),
59105
+ /**
59106
+ * The transmitter device new bindings and an import send through. A binding
59107
+ * names its own device, so one command may override it.
59108
+ */
59109
+ defaultTransmitterId: number().int().nonnegative().nullable().default(null),
59110
+ /** Keyed by role. A role with no entry falls back to the native cap. */
59111
+ bindings: record(string(), RemoteBindingSchema),
59112
+ customButtons: array(RemoteCustomButtonSchema).max(100),
59113
+ power: object({ mode: RemotePowerModeSchema })
59114
+ }).superRefine((config, ctx) => {
59115
+ const roles = Object.keys(config.bindings);
59116
+ if (roles.length > 200) ctx.addIssue({
59117
+ code: "custom",
59118
+ message: "too many bindings",
59119
+ path: ["bindings"]
59120
+ });
59121
+ for (const role of roles) if (!isRemoteRole(role)) ctx.addIssue({
59122
+ code: "custom",
59123
+ message: `"${role}" is not a role`,
59124
+ path: ["bindings", role]
59125
+ });
59126
+ const keys = /* @__PURE__ */ new Set();
59127
+ config.customButtons.forEach((button, index) => {
59128
+ if (keys.has(button.key)) ctx.addIssue({
59129
+ code: "custom",
59130
+ message: `custom button "${button.key}" is listed twice`,
59131
+ path: [
59132
+ "customButtons",
59133
+ index,
59134
+ "key"
59135
+ ]
59136
+ });
59137
+ keys.add(button.key);
59138
+ });
59139
+ for (const role of roles) {
59140
+ const key = customKeyOfRole(role);
59141
+ if (key !== null && !keys.has(key)) ctx.addIssue({
59142
+ code: "custom",
59143
+ message: `${role} is bound but has no entry in customButtons`,
59144
+ path: ["bindings", role]
59145
+ });
59146
+ }
59147
+ });
59148
+ object({
59149
+ power: object({
59150
+ state: _enum(["on", "off"]),
59151
+ at: number()
59152
+ }).nullable(),
59153
+ muted: object({
59154
+ value: boolean(),
59155
+ at: number()
59156
+ }).nullable()
59157
+ });
59158
+ var RemoteLayoutButtonSchema = object({
59159
+ id: string().min(1).max(40),
59160
+ /** 0-based grid position. */
59161
+ row: number().int().min(0).max(24),
59162
+ col: number().int().min(0).max(24),
59163
+ rowSpan: number().int().min(1).max(24),
59164
+ colSpan: number().int().min(1).max(24),
59165
+ icon: string().min(1).max(60).optional(),
59166
+ label: string().min(1).max(40).optional(),
59167
+ role: RemoteRoleSchema
59168
+ });
59169
+ object({
59170
+ columns: number().int().min(1).max(24),
59171
+ rows: number().int().min(1).max(24),
59172
+ buttons: array(RemoteLayoutButtonSchema).max(200)
59173
+ }).superRefine((grid, ctx) => {
59174
+ const ids = /* @__PURE__ */ new Set();
59175
+ const taken = /* @__PURE__ */ new Map();
59176
+ grid.buttons.forEach((button, index) => {
59177
+ if (ids.has(button.id)) ctx.addIssue({
59178
+ code: "custom",
59179
+ message: `button id "${button.id}" is used twice`,
59180
+ path: [
59181
+ "buttons",
59182
+ index,
59183
+ "id"
59184
+ ]
59185
+ });
59186
+ ids.add(button.id);
59187
+ if (button.row + button.rowSpan > grid.rows || button.col + button.colSpan > grid.columns) {
59188
+ ctx.addIssue({
59189
+ code: "custom",
59190
+ message: `button "${button.id}" leaves the grid`,
59191
+ path: ["buttons", index]
59192
+ });
59193
+ return;
59194
+ }
59195
+ for (let r = button.row; r < button.row + button.rowSpan; r += 1) for (let c = button.col; c < button.col + button.colSpan; c += 1) {
59196
+ const cell = `${r}:${c}`;
59197
+ const other = taken.get(cell);
59198
+ if (other !== void 0) {
59199
+ ctx.addIssue({
59200
+ code: "custom",
59201
+ message: `buttons "${other}" and "${button.id}" overlap`,
59202
+ path: ["buttons", index]
59203
+ });
59204
+ return;
59205
+ }
59206
+ taken.set(cell, button.id);
59207
+ }
59208
+ });
59209
+ });
59210
+ var RemoteBoundCommandSchema = object({
59211
+ role: RemoteRoleSchema,
59212
+ /** The operator's name for a custom button; absent for a known role. */
59213
+ label: string().optional(),
59214
+ custom: boolean()
59215
+ });
59216
+ var RemoteCommandListSchema = object({
59217
+ layoutTemplateId: string().nullable(),
59218
+ power: object({ mode: RemotePowerModeSchema }),
59219
+ /** Only the roles that are BOUND — the ones a press can reach. */
59220
+ commands: array(RemoteBoundCommandSchema)
59221
+ });
59222
+ var RemoteRefusalReasonSchema = _enum([
59223
+ "no-remote",
59224
+ "unbound-role",
59225
+ "action-failed"
59226
+ ]);
59227
+ var RemoteSendResultSchema = discriminatedUnion("status", [
59228
+ object({
59229
+ status: literal("sent"),
59230
+ /** How many times the action ran. */
59231
+ sends: number().int().min(1)
59232
+ }),
59233
+ object({
59234
+ status: literal("skipped"),
59235
+ reason: literal("already-in-state")
59236
+ }),
59237
+ object({
59238
+ status: literal("refused"),
59239
+ reason: RemoteRefusalReasonSchema,
59240
+ detail: string().optional()
59241
+ })
59242
+ ]);
59243
+ var RemoteMediaCommandSchema = _enum([
59244
+ "play",
59245
+ "pause",
59246
+ "stop",
59247
+ "next",
59248
+ "previous",
59249
+ "setMute",
59250
+ "selectSource"
59251
+ ]);
59252
+ /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
59253
+ var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
59254
+ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status: literal("saved") }), object({
59255
+ status: literal("refused"),
59256
+ reason: _enum(["unknown-template", "not-a-media-player"]),
59257
+ detail: string()
59258
+ })]);
59259
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
59260
+ DeviceType.MediaPlayer;
59261
+ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
59262
+ kind: "mutation",
59263
+ auth: "admin"
59264
+ }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
59265
+ role: RemoteRoleSchema,
59266
+ /** Sends in a row (volume steps). */
59267
+ repeat: number().int().min(1).max(50).default(1)
59268
+ }), RemoteSendResultSchema, {
59269
+ kind: "mutation",
59270
+ auth: "protected"
59271
+ }), method(deviceInput.extend({
59272
+ command: RemoteMediaCommandSchema,
59273
+ muted: boolean().optional(),
59274
+ source: string().optional(),
59275
+ repeat: number().int().min(1).max(50).default(1)
59276
+ }), RemoteMediaCommandResultSchema, {
59277
+ kind: "mutation",
59278
+ auth: "protected"
59279
+ });
59280
+ var TemplateKindSchema = _enum(["remote"]);
59281
+ /**
59282
+ * template — the generic store of reusable, named operator data (D963). A
59283
+ * record is `{ id, kind, name, payload }`; the kinds (today only `remote`, the
59284
+ * button grid of a remote) and the schema each payload must satisfy are
59285
+ * declared by the system in `template-kinds.ts`. Nothing here is specific to a
59286
+ * kind: the engine validates a payload against its kind's schema and, on a
59287
+ * delete, asks the kind's owner whether anything still points at the record.
59288
+ *
59289
+ * Every refusal is a typed result — a UI says "unknown kind" or names the
59290
+ * devices that still use a template, without parsing text.
59291
+ */
59292
+ var TemplateRecordSchema = object({
59293
+ id: string().min(1),
59294
+ kind: TemplateKindSchema,
59295
+ name: string().min(1).max(120),
59296
+ /** Shaped by the kind; `checkTemplatePayload(kind, payload)` reads it. */
59297
+ payload: unknown(),
59298
+ createdAt: number(),
59299
+ updatedAt: number()
59300
+ });
59301
+ var TemplateWriteResultSchema = discriminatedUnion("status", [object({
59302
+ status: literal("saved"),
59303
+ template: TemplateRecordSchema
59304
+ }), object({
59305
+ status: literal("refused"),
59306
+ reason: _enum(["invalid-payload", "not-found"]),
59307
+ detail: string()
59308
+ })]);
59309
+ /** A device still pointing at a template. */
59310
+ var TemplateUserSchema = object({
59311
+ deviceId: number().int().nonnegative(),
59312
+ name: string()
59313
+ });
59314
+ var TemplateDeleteResultSchema = discriminatedUnion("status", [
59315
+ object({ status: literal("deleted") }),
59316
+ object({ status: literal("not-found") }),
59317
+ object({
59318
+ status: literal("in-use"),
59319
+ devices: array(TemplateUserSchema).min(1)
59320
+ })
59321
+ ]);
59322
+ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSchema), { auth: "protected" }), method(object({ id: string().min(1) }), TemplateRecordSchema.nullable(), { auth: "protected" }), method(object({
59323
+ kind: TemplateKindSchema,
59324
+ name: string().min(1).max(120),
59325
+ payload: unknown()
59326
+ }), TemplateWriteResultSchema, {
59327
+ kind: "mutation",
59328
+ auth: "admin"
59329
+ }), method(object({
59330
+ id: string().min(1),
59331
+ name: string().min(1).max(120).optional(),
59332
+ payload: unknown().optional()
59333
+ }), TemplateWriteResultSchema, {
59334
+ kind: "mutation",
59335
+ auth: "admin"
59336
+ }), method(object({ id: string().min(1) }), TemplateDeleteResultSchema, {
59337
+ kind: "mutation",
59338
+ auth: "admin"
59339
+ });
59340
+ /**
58831
59341
  * button — device-scoped capability for HA `button.*` / `input_button.*`
58832
59342
  * fire-only actuators. A button entity has no readable state — pressing it
58833
59343
  * triggers an action on the HA side. There is no `status` / `runtimeState`:
@@ -58849,6 +59359,56 @@ var buttonCapability = {
58849
59359
  }) }
58850
59360
  };
58851
59361
  /**
59362
+ * A pulse train in signed microseconds: mark (+) first, then space (-), strictly
59363
+ * alternating, no zeros, 2..1024 entries.
59364
+ */
59365
+ var IrTimingsSchema = array(number().int()).min(2).max(1024).superRefine((timings, ctx) => {
59366
+ timings.forEach((value, index) => {
59367
+ const expectMark = index % 2 === 0;
59368
+ if (value === 0 || value > 0 !== expectMark) ctx.addIssue({
59369
+ code: "custom",
59370
+ message: `timing ${index} must be a non-zero ${expectMark ? "mark (positive)" : "space (negative)"}`,
59371
+ path: [index]
59372
+ });
59373
+ });
59374
+ });
59375
+ /** Per-command transmit options. Every key is optional; a transmitter applies what it can. */
59376
+ var TransmitSettingsSchema = object({
59377
+ carrierHz: number().int().positive().optional(),
59378
+ dutyCyclePercent: number().int().min(1).max(100).optional(),
59379
+ repeat: number().int().min(0).max(20).optional(),
59380
+ repeatDelayMs: number().int().min(0).optional()
59381
+ });
59382
+ /** The setting keys a transmitter may declare as settable (`getInfo.settable`). */
59383
+ var TransmitSettingKeySchema = _enum([
59384
+ "carrierHz",
59385
+ "dutyCyclePercent",
59386
+ "repeat",
59387
+ "repeatDelayMs"
59388
+ ]);
59389
+ var IrMediumSchema = _enum(["ir", "rf"]);
59390
+ var IrSendResultSchema = object({
59391
+ ok: literal(true),
59392
+ /** Settings the transmitter could not apply; the train was still sent. */
59393
+ ignoredSettings: array(string())
59394
+ });
59395
+ var IrTransmitterInfoSchema = object({
59396
+ media: array(IrMediumSchema),
59397
+ settable: array(TransmitSettingKeySchema),
59398
+ maxTimings: number().int().positive()
59399
+ });
59400
+ DeviceType.Generic, method(object({
59401
+ deviceId: number().int().nonnegative(),
59402
+ timings: IrTimingsSchema,
59403
+ settings: TransmitSettingsSchema.optional()
59404
+ }), IrSendResultSchema, {
59405
+ kind: "mutation",
59406
+ auth: "protected"
59407
+ }), method(object({ deviceId: number().int().nonnegative() }), IrTransmitterInfoSchema, {
59408
+ kind: "query",
59409
+ auth: "protected"
59410
+ });
59411
+ /**
58852
59412
  * `calendar-source` — collection cap exposing per-provider calendars and
58853
59413
  * their events. Every addon that contributes calendars (Home Assistant,
58854
59414
  * a future Google Calendar addon, …) registers a provider here. The hub
@@ -59808,14 +60368,13 @@ reset: method(object({
59808
60368
  auth: "admin"
59809
60369
  }) },
59810
60370
  /**
59811
- * A composed consumable (item added to a device with no native consumables
59812
- * provider) refuses `reset` by name: "Only meaningful when the item's
59813
- * `resettable` is true" — a composed item is never resettable until
59814
- * commands (`forward`) arrive in slice 3 (D663).
60371
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
60372
+ * name: only a composed device of a NEW type forwards an item's reset to a
60373
+ * source button, and marks that item `resettable` (D967).
59815
60374
  */
59816
60375
  composedMethods: { reset: {
59817
60376
  kind: "refuse",
59818
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
60377
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
59819
60378
  } },
59820
60379
  status: {
59821
60380
  schema: ConsumablesStatusSchema,
@@ -60409,7 +60968,7 @@ var DishwasherStatusSchema = object({
60409
60968
  /** Ms epoch of the last value received from the appliance; 0 = never. */
60410
60969
  lastFetchedAt: number()
60411
60970
  });
60412
- var DeviceIdSchema = number().int().nonnegative();
60971
+ var DeviceIdSchema$1 = number().int().nonnegative();
60413
60972
  /** A value a program option or setting can take. */
60414
60973
  var DishwasherValueSchema = union([
60415
60974
  boolean(),
@@ -60484,77 +61043,77 @@ var dishwasherCapability = {
60484
61043
  * option feature keys; each is checked against the program's constraints.
60485
61044
  */
60486
61045
  start: method(object({
60487
- deviceId: DeviceIdSchema,
61046
+ deviceId: DeviceIdSchema$1,
60488
61047
  program: string().min(1).optional(),
60489
61048
  options: record(string(), DishwasherValueSchema).optional()
60490
61049
  }), _void(), {
60491
61050
  kind: "mutation",
60492
61051
  auth: "protected"
60493
61052
  }),
60494
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
61053
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60495
61054
  kind: "mutation",
60496
61055
  auth: "protected"
60497
61056
  }),
60498
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
61057
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60499
61058
  kind: "mutation",
60500
61059
  auth: "protected"
60501
61060
  }),
60502
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
61061
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60503
61062
  kind: "mutation",
60504
61063
  auth: "protected"
60505
61064
  }),
60506
61065
  /** Select (not start) a program; refused when the appliance does not offer it. */
60507
61066
  setProgram: method(object({
60508
- deviceId: DeviceIdSchema,
61067
+ deviceId: DeviceIdSchema$1,
60509
61068
  program: string().min(1)
60510
61069
  }), _void(), {
60511
61070
  kind: "mutation",
60512
61071
  auth: "protected"
60513
61072
  }),
60514
61073
  setPower: method(object({
60515
- deviceId: DeviceIdSchema,
61074
+ deviceId: DeviceIdSchema$1,
60516
61075
  on: boolean()
60517
61076
  }), _void(), {
60518
61077
  kind: "mutation",
60519
61078
  auth: "protected"
60520
61079
  }),
60521
61080
  setHalfLoad: method(object({
60522
- deviceId: DeviceIdSchema,
61081
+ deviceId: DeviceIdSchema$1,
60523
61082
  on: boolean()
60524
61083
  }), _void(), {
60525
61084
  kind: "mutation",
60526
61085
  auth: "protected"
60527
61086
  }),
60528
61087
  setHygienePlus: method(object({
60529
- deviceId: DeviceIdSchema,
61088
+ deviceId: DeviceIdSchema$1,
60530
61089
  on: boolean()
60531
61090
  }), _void(), {
60532
61091
  kind: "mutation",
60533
61092
  auth: "protected"
60534
61093
  }),
60535
61094
  setExtraDry: method(object({
60536
- deviceId: DeviceIdSchema,
61095
+ deviceId: DeviceIdSchema$1,
60537
61096
  on: boolean()
60538
61097
  }), _void(), {
60539
61098
  kind: "mutation",
60540
61099
  auth: "protected"
60541
61100
  }),
60542
61101
  setIntensiveZone: method(object({
60543
- deviceId: DeviceIdSchema,
61102
+ deviceId: DeviceIdSchema$1,
60544
61103
  on: boolean()
60545
61104
  }), _void(), {
60546
61105
  kind: "mutation",
60547
61106
  auth: "protected"
60548
61107
  }),
60549
61108
  setVarioSpeedPlus: method(object({
60550
- deviceId: DeviceIdSchema,
61109
+ deviceId: DeviceIdSchema$1,
60551
61110
  on: boolean()
60552
61111
  }), _void(), {
60553
61112
  kind: "mutation",
60554
61113
  auth: "protected"
60555
61114
  }),
60556
61115
  setChildLock: method(object({
60557
- deviceId: DeviceIdSchema,
61116
+ deviceId: DeviceIdSchema$1,
60558
61117
  on: boolean()
60559
61118
  }), _void(), {
60560
61119
  kind: "mutation",
@@ -60566,7 +61125,7 @@ var dishwasherCapability = {
60566
61125
  * its declared range / members.
60567
61126
  */
60568
61127
  setOption: method(object({
60569
- deviceId: DeviceIdSchema,
61128
+ deviceId: DeviceIdSchema$1,
60570
61129
  key: string().min(1),
60571
61130
  value: DishwasherValueSchema
60572
61131
  }), _void(), {
@@ -60574,7 +61133,7 @@ var dishwasherCapability = {
60574
61133
  auth: "protected"
60575
61134
  }),
60576
61135
  /** What this appliance can do, from its own description — the UI is derived from it. */
60577
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
61136
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
60578
61137
  },
60579
61138
  status: {
60580
61139
  schema: DishwasherStatusSchema,
@@ -62945,9 +63504,35 @@ var MediaInfoSchema = object({
62945
63504
  /** Optional cover-art / thumbnail URL. */
62946
63505
  imageUrl: string().optional()
62947
63506
  });
63507
+ /**
63508
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
63509
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
63510
+ * tracks what it last SENT; it moves only after a send was accepted, and a
63511
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
63512
+ * guess, never as a reading.
63513
+ */
63514
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
63515
+ /** The fields of a media-player status a driver can only guess. */
63516
+ var MediaPlayerAssumedFieldSchema = _enum([
63517
+ "state",
63518
+ "volumeLevel",
63519
+ "isMuted",
63520
+ "source"
63521
+ ]);
62948
63522
  var MediaPlayerStatusSchema = object({
62949
63523
  /** Playback lifecycle state. */
62950
63524
  state: MediaPlayerStateSchema,
63525
+ /**
63526
+ * Whether the state fields are readings or the driver's own bookkeeping.
63527
+ * A slice written before this field existed parses as `reported`.
63528
+ */
63529
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
63530
+ /**
63531
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
63532
+ * this is not empty; a field not listed here is a reading even then (power
63533
+ * from a running-state source, mute guessed from the last send).
63534
+ */
63535
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
62951
63536
  /** Volume as 0..100 inclusive. Null when the device has no volume
62952
63537
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62953
63538
  volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
@@ -64239,7 +64824,8 @@ var TopologyNodeSchema = object({
64239
64824
  isHub: boolean(),
64240
64825
  isOnline: boolean(),
64241
64826
  cpuPercent: number(),
64242
- memoryPercent: number(),
64827
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
64828
+ memoryPercent: number().nullable(),
64243
64829
  uptime: number(),
64244
64830
  lastSeen: string(),
64245
64831
  localIps: array(string()).readonly(),
@@ -64857,9 +65443,10 @@ var PetFeederStatusSchema = object({
64857
65443
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
64858
65444
  * the device has not reported yet. */
64859
65445
  foodLevel: number().nullable().meta(readingMeta("none")),
64860
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
64861
- * feeder's own units. Null when the model reports none. */
64862
- bowlFood: number().nullable().meta(readingMeta("none")),
65446
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
65447
+ * feeder with a bowl scale; null when the model has none or its reading is
65448
+ * of an unproven unit. */
65449
+ bowlFood: number().nullable().meta(readingMeta("mass")),
64863
65450
  /** True while the pet is eating (`state.eating`). */
64864
65451
  eating: boolean(),
64865
65452
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -64875,9 +65462,14 @@ var PetFeederStatusSchema = object({
64875
65462
  * (`device_class: problem`, on = low). True when the tank reports empty,
64876
65463
  * or, on a dual-hopper model, when EITHER hopper is empty. */
64877
65464
  lowFood: boolean(),
64878
- /** Battery charge 0..100 (%). Null on mains-powered models or when the
64879
- * device has no battery reading. */
65465
+ /** Battery charge 0..100 (%). On a feeder running on `mains` this is the
65466
+ * BACKUP battery's charge, not the power source. Null when the device has
65467
+ * no battery reading. */
64880
65468
  batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65469
+ /** What the feeder is powered from right now: `mains` (plugged in, the
65470
+ * battery is only a backup) or `battery`. Null when the device does not
65471
+ * report it. */
65472
+ powerSource: _enum(["mains", "battery"]).nullable(),
64881
65473
  /**
64882
65474
  * Desiccant life remaining, stored in seconds (duration base, D944).
64883
65475
  * Providers convert from the days the feeder reports. Null when the
@@ -64905,6 +65497,14 @@ var PetFeederStatusSchema = object({
64905
65497
  feedSound: boolean(),
64906
65498
  /** Speaker / prompt volume level (device-scaled integer). */
64907
65499
  volume: number().meta(readingMeta("none")),
65500
+ /** Portion (grams) of the last manual feed sent from a single-hopper
65501
+ * stepper — the default the next manual feed starts from. Persisted per
65502
+ * device by the provider; null until a manual feed has been sent. */
65503
+ lastFeedGrams: gramsPortion.nullable(),
65504
+ /** Hopper-1 portion of the last manual feed (dual-hopper); null until sent. */
65505
+ lastFeedHopper1: gramsPortion.nullable(),
65506
+ /** Hopper-2 portion of the last manual feed (dual-hopper); null until sent. */
65507
+ lastFeedHopper2: gramsPortion.nullable(),
64908
65508
  /** Ms epoch when the slice was last refreshed from the cloud. */
64909
65509
  lastFetchedAt: number()
64910
65510
  });
@@ -65030,6 +65630,137 @@ var petFeederCapability = {
65030
65630
  }
65031
65631
  } }
65032
65632
  };
65633
+ /**
65634
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
65635
+ * circulating drinking fountain: how much water is in the tank, how much the
65636
+ * pet drank, whether the pump runs and on what duty cycle.
65637
+ *
65638
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
65639
+ * a container of loose entities (a pump switch, two number boxes, a "filter
65640
+ * changed" button), and an operator BUILDS the device from them — a composed
65641
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
65642
+ * method of ONE source entity, and every reading is a field derived from a
65643
+ * source entity; the provider-shaped interface below exists so the composed
65644
+ * device and any future native provider answer the same contract.
65645
+ *
65646
+ * Quantities are stored in the base unit of their kind (D944): volumes in
65647
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
65648
+ * reports millilitres or minutes is converted in the composition's expression,
65649
+ * never here. Every reading may be `null`: a source that is offline or does
65650
+ * not report is unknown, never zero.
65651
+ *
65652
+ * The filter is not a field of this cap: it is an item of the `consumables`
65653
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
65654
+ */
65655
+ var PetFountainStatusSchema = object({
65656
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
65657
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65658
+ /** Water in the tank, in litres. Null while the source has not reported. */
65659
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
65660
+ /** True when the tank is empty. Null while unknown. */
65661
+ empty: boolean().nullable(),
65662
+ /** True while the pump is running. Null while unknown. */
65663
+ pumpRunning: boolean().nullable(),
65664
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
65665
+ pumpIntervalEnabled: boolean().nullable(),
65666
+ /** How long the pump runs per cycle, in seconds. */
65667
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
65668
+ /** How long the pump rests per cycle, in seconds. */
65669
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
65670
+ /** Water drunk in the last drink, in litres. */
65671
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
65672
+ /** Water drunk in the current hour, in litres. */
65673
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
65674
+ /** Water drunk over the last 24 hours (rolling), in litres. */
65675
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
65676
+ /** Water drunk since local midnight, in litres. */
65677
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
65678
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
65679
+ error: string().nullable(),
65680
+ /** Ms epoch when the slice was last refreshed. */
65681
+ lastFetchedAt: number()
65682
+ });
65683
+ var DeviceIdSchema = number().int().nonnegative();
65684
+ var MinutesSchema = number().int().min(0);
65685
+ var petFountainCapability = {
65686
+ name: "pet-fountain",
65687
+ scope: "device",
65688
+ deviceNative: true,
65689
+ mode: "singleton",
65690
+ deviceTypes: [DeviceType.PetFountain],
65691
+ methods: {
65692
+ /** Start or stop the pump. */
65693
+ setPump: method(object({
65694
+ deviceId: DeviceIdSchema,
65695
+ on: boolean()
65696
+ }), _void(), {
65697
+ kind: "mutation",
65698
+ auth: "protected"
65699
+ }),
65700
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
65701
+ setPumpInterval: method(object({
65702
+ deviceId: DeviceIdSchema,
65703
+ on: boolean()
65704
+ }), _void(), {
65705
+ kind: "mutation",
65706
+ auth: "protected"
65707
+ }),
65708
+ /** Set how many minutes the pump runs per cycle. */
65709
+ setPumpOnMinutes: method(object({
65710
+ deviceId: DeviceIdSchema,
65711
+ minutes: MinutesSchema
65712
+ }), _void(), {
65713
+ kind: "mutation",
65714
+ auth: "protected"
65715
+ }),
65716
+ /** Set how many minutes the pump rests per cycle. */
65717
+ setPumpOffMinutes: method(object({
65718
+ deviceId: DeviceIdSchema,
65719
+ minutes: MinutesSchema
65720
+ }), _void(), {
65721
+ kind: "mutation",
65722
+ auth: "protected"
65723
+ }),
65724
+ /** Clear the rolling daily consumption counters on the device. */
65725
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
65726
+ kind: "mutation",
65727
+ auth: "protected"
65728
+ })
65729
+ },
65730
+ status: {
65731
+ schema: PetFountainStatusSchema,
65732
+ kind: "push"
65733
+ },
65734
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
65735
+ runtimeState: PetFountainStatusSchema,
65736
+ /**
65737
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
65738
+ *
65739
+ * See `RuntimeStateDurability`. Enforced by
65740
+ * `scripts/check-runtime-state-durability.ts`.
65741
+ */
65742
+ durability: "session",
65743
+ /** History (D850): recorded series. */
65744
+ history: { fields: {
65745
+ waterLevel: {
65746
+ kind: "gauge",
65747
+ unit: "%",
65748
+ deadband: 1
65749
+ },
65750
+ waterVolume: {
65751
+ kind: "gauge",
65752
+ unit: "L"
65753
+ },
65754
+ todayDrink: {
65755
+ kind: "gauge",
65756
+ unit: "L"
65757
+ },
65758
+ last24hDrink: {
65759
+ kind: "gauge",
65760
+ unit: "L"
65761
+ }
65762
+ } }
65763
+ };
65033
65764
  var VehicleSchema = object({
65034
65765
  id: string(),
65035
65766
  name: string(),
@@ -70942,9 +71673,17 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
70942
71673
  kind: "mutation",
70943
71674
  auth: "admin"
70944
71675
  });
71676
+ discriminatedUnion("kind", [object({
71677
+ kind: literal("raw"),
71678
+ timings: array(number().int()),
71679
+ settings: TransmitSettingsSchema.optional()
71680
+ }), object({
71681
+ kind: literal("pronto"),
71682
+ code: string()
71683
+ })]);
70945
71684
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
70946
71685
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
70947
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
71686
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
70948
71687
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
70949
71688
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
70950
71689
  new Set(CAP_NAMES);
@@ -71295,6 +72034,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
71295
72034
  notifier: notifierCapability,
71296
72035
  numericSensor: numericSensorCapability,
71297
72036
  petFeeder: petFeederCapability,
72037
+ petFountain: petFountainCapability,
71298
72038
  powerMeter: powerMeterCapability,
71299
72039
  presence: presenceCapability,
71300
72040
  pressureSensor: pressureSensorCapability,
@@ -72545,7 +73285,7 @@ var BaseDeviceProvider = class extends BaseAddon {
72545
73285
  return toDeviceSummary(device, this.addonId);
72546
73286
  }
72547
73287
  };
72548
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
73288
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
72549
73289
  new Set(Object.values(DeviceType));
72550
73290
  Object.freeze({
72551
73291
  "accessories.getStatus": {
@@ -74480,6 +75220,13 @@ Object.freeze({
74480
75220
  access: "create",
74481
75221
  role: "admin"
74482
75222
  },
75223
+ "deviceExport.exposeEntity": {
75224
+ capName: "device-export",
75225
+ capScope: "system",
75226
+ addonId: null,
75227
+ access: "create",
75228
+ role: "admin"
75229
+ },
74483
75230
  "deviceExport.getDeviceLiveContribution": {
74484
75231
  capName: "device-export",
74485
75232
  capScope: "system",
@@ -74508,6 +75255,13 @@ Object.freeze({
74508
75255
  access: "view",
74509
75256
  role: "admin"
74510
75257
  },
75258
+ "deviceExport.listExposedEntities": {
75259
+ capName: "device-export",
75260
+ capScope: "system",
75261
+ addonId: null,
75262
+ access: "view",
75263
+ role: "admin"
75264
+ },
74511
75265
  "deviceExport.listSupportedDeviceKinds": {
74512
75266
  capName: "device-export",
74513
75267
  capScope: "system",
@@ -74515,6 +75269,13 @@ Object.freeze({
74515
75269
  access: "view",
74516
75270
  role: "admin"
74517
75271
  },
75272
+ "deviceExport.listSupportedEntityKinds": {
75273
+ capName: "device-export",
75274
+ capScope: "system",
75275
+ addonId: null,
75276
+ access: "view",
75277
+ role: "admin"
75278
+ },
74518
75279
  "deviceExport.unexposeDevice": {
74519
75280
  capName: "device-export",
74520
75281
  capScope: "system",
@@ -74522,6 +75283,13 @@ Object.freeze({
74522
75283
  access: "create",
74523
75284
  role: "admin"
74524
75285
  },
75286
+ "deviceExport.unexposeEntity": {
75287
+ capName: "device-export",
75288
+ capScope: "system",
75289
+ addonId: null,
75290
+ access: "create",
75291
+ role: "admin"
75292
+ },
74525
75293
  "deviceExtension.listCandidates": {
74526
75294
  capName: "device-extension",
74527
75295
  capScope: "device",
@@ -76097,6 +76865,20 @@ Object.freeze({
76097
76865
  access: "create",
76098
76866
  role: "viewer"
76099
76867
  },
76868
+ "irTransmitter.getInfo": {
76869
+ capName: "ir-transmitter",
76870
+ capScope: "device",
76871
+ addonId: null,
76872
+ access: "view",
76873
+ role: "viewer"
76874
+ },
76875
+ "irTransmitter.send": {
76876
+ capName: "ir-transmitter",
76877
+ capScope: "device",
76878
+ addonId: null,
76879
+ access: "create",
76880
+ role: "viewer"
76881
+ },
76100
76882
  "keychain.delete": {
76101
76883
  capName: "keychain",
76102
76884
  capScope: "system",
@@ -77329,6 +78111,13 @@ Object.freeze({
77329
78111
  access: "create",
77330
78112
  role: "admin"
77331
78113
  },
78114
+ "nodeRemovalHook.onNodeRemoved": {
78115
+ capName: "node-removal-hook",
78116
+ capScope: "system",
78117
+ addonId: null,
78118
+ access: "create",
78119
+ role: "admin"
78120
+ },
77332
78121
  "nodes.clusterAddonStatus": {
77333
78122
  capName: "nodes",
77334
78123
  capScope: "system",
@@ -77966,6 +78755,48 @@ Object.freeze({
77966
78755
  access: "create",
77967
78756
  role: "viewer"
77968
78757
  },
78758
+ "petFountain.getStatus": {
78759
+ capName: "pet-fountain",
78760
+ capScope: "device",
78761
+ addonId: null,
78762
+ access: "view",
78763
+ role: "viewer"
78764
+ },
78765
+ "petFountain.resetDailyConsumption": {
78766
+ capName: "pet-fountain",
78767
+ capScope: "device",
78768
+ addonId: null,
78769
+ access: "delete",
78770
+ role: "viewer"
78771
+ },
78772
+ "petFountain.setPump": {
78773
+ capName: "pet-fountain",
78774
+ capScope: "device",
78775
+ addonId: null,
78776
+ access: "create",
78777
+ role: "viewer"
78778
+ },
78779
+ "petFountain.setPumpInterval": {
78780
+ capName: "pet-fountain",
78781
+ capScope: "device",
78782
+ addonId: null,
78783
+ access: "create",
78784
+ role: "viewer"
78785
+ },
78786
+ "petFountain.setPumpOffMinutes": {
78787
+ capName: "pet-fountain",
78788
+ capScope: "device",
78789
+ addonId: null,
78790
+ access: "create",
78791
+ role: "viewer"
78792
+ },
78793
+ "petFountain.setPumpOnMinutes": {
78794
+ capName: "pet-fountain",
78795
+ capScope: "device",
78796
+ addonId: null,
78797
+ access: "create",
78798
+ role: "viewer"
78799
+ },
77969
78800
  "pipelineAnalytics.addRetrainFrameFromRecording": {
77970
78801
  capName: "pipeline-analytics",
77971
78802
  capScope: "device",
@@ -80220,6 +81051,41 @@ Object.freeze({
80220
81051
  access: "create",
80221
81052
  role: "admin"
80222
81053
  },
81054
+ "remote.getConfig": {
81055
+ capName: "remote",
81056
+ capScope: "system",
81057
+ addonId: null,
81058
+ access: "view",
81059
+ role: "admin"
81060
+ },
81061
+ "remote.listCommands": {
81062
+ capName: "remote",
81063
+ capScope: "system",
81064
+ addonId: null,
81065
+ access: "view",
81066
+ role: "admin"
81067
+ },
81068
+ "remote.sendCommand": {
81069
+ capName: "remote",
81070
+ capScope: "system",
81071
+ addonId: null,
81072
+ access: "create",
81073
+ role: "admin"
81074
+ },
81075
+ "remote.sendMediaCommand": {
81076
+ capName: "remote",
81077
+ capScope: "system",
81078
+ addonId: null,
81079
+ access: "create",
81080
+ role: "admin"
81081
+ },
81082
+ "remote.setConfig": {
81083
+ capName: "remote",
81084
+ capScope: "system",
81085
+ addonId: null,
81086
+ access: "create",
81087
+ role: "admin"
81088
+ },
80223
81089
  "runningState.getStatus": {
80224
81090
  capName: "running-state",
80225
81091
  capScope: "device",
@@ -80332,6 +81198,13 @@ Object.freeze({
80332
81198
  access: "create",
80333
81199
  role: "admin"
80334
81200
  },
81201
+ "serverManagement.getLaunchOverrides": {
81202
+ capName: "server-management",
81203
+ capScope: "system",
81204
+ addonId: null,
81205
+ access: "view",
81206
+ role: "admin"
81207
+ },
80335
81208
  "serverManagement.getServerPackageStatus": {
80336
81209
  capName: "server-management",
80337
81210
  capScope: "system",
@@ -80353,6 +81226,13 @@ Object.freeze({
80353
81226
  access: "create",
80354
81227
  role: "admin"
80355
81228
  },
81229
+ "serverManagement.setLaunchOverrides": {
81230
+ capName: "server-management",
81231
+ capScope: "system",
81232
+ addonId: null,
81233
+ access: "create",
81234
+ role: "admin"
81235
+ },
80356
81236
  "settingsStore.aggregate": {
80357
81237
  capName: "settings-store",
80358
81238
  capScope: "system",
@@ -81410,6 +82290,41 @@ Object.freeze({
81410
82290
  access: "view",
81411
82291
  role: "viewer"
81412
82292
  },
82293
+ "template.create": {
82294
+ capName: "template",
82295
+ capScope: "system",
82296
+ addonId: null,
82297
+ access: "create",
82298
+ role: "admin"
82299
+ },
82300
+ "template.delete": {
82301
+ capName: "template",
82302
+ capScope: "system",
82303
+ addonId: null,
82304
+ access: "delete",
82305
+ role: "admin"
82306
+ },
82307
+ "template.get": {
82308
+ capName: "template",
82309
+ capScope: "system",
82310
+ addonId: null,
82311
+ access: "view",
82312
+ role: "admin"
82313
+ },
82314
+ "template.list": {
82315
+ capName: "template",
82316
+ capScope: "system",
82317
+ addonId: null,
82318
+ access: "view",
82319
+ role: "admin"
82320
+ },
82321
+ "template.update": {
82322
+ capName: "template",
82323
+ capScope: "system",
82324
+ addonId: null,
82325
+ access: "create",
82326
+ role: "admin"
82327
+ },
81413
82328
  "terminalSession.adoptLegacyMonitor": {
81414
82329
  capName: "terminal-session",
81415
82330
  capScope: "system",
@@ -83720,6 +84635,11 @@ var ACTUATOR_CAP_VOCABULARY = {
83720
84635
  icon: "circle-dot"
83721
84636
  }]
83722
84637
  },
84638
+ "ir-transmitter": {
84639
+ scope: "device",
84640
+ noun: t("IR / RF transmitter", "Trasmettitore IR / RF"),
84641
+ notOperatorFacing: { send: "a raw pulse train is not something an operator composes in an automation; the remote that owns the command table sends it" }
84642
+ },
83723
84643
  control: {
83724
84644
  scope: "device",
83725
84645
  noun: t("Control", "Controllo"),
@@ -84373,6 +85293,100 @@ var APPLIANCE_CAP_VOCABULARY = {
84373
85293
  }
84374
85294
  ]
84375
85295
  },
85296
+ "pet-fountain": {
85297
+ scope: "device",
85298
+ noun: t("Pet fountain", "Fontana per animali"),
85299
+ actions: [
85300
+ {
85301
+ id: "petFountain.pumpOn",
85302
+ method: "setPump",
85303
+ label: t("Pump on", "Pompa accesa"),
85304
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
85305
+ icon: "play",
85306
+ args: { on: true }
85307
+ },
85308
+ {
85309
+ id: "petFountain.pumpOff",
85310
+ method: "setPump",
85311
+ label: t("Pump off", "Pompa spenta"),
85312
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
85313
+ icon: "square",
85314
+ args: { on: false }
85315
+ },
85316
+ {
85317
+ id: "petFountain.intervalOn",
85318
+ method: "setPumpInterval",
85319
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
85320
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
85321
+ icon: "timer",
85322
+ args: { on: true }
85323
+ },
85324
+ {
85325
+ id: "petFountain.intervalOff",
85326
+ method: "setPumpInterval",
85327
+ label: t("Pump cycle off", "Ciclo pompa spento"),
85328
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
85329
+ icon: "timer-off",
85330
+ args: { on: false }
85331
+ },
85332
+ {
85333
+ id: "petFountain.pumpOnMinutes",
85334
+ method: "setPumpOnMinutes",
85335
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
85336
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
85337
+ icon: "timer",
85338
+ openArgs: [{
85339
+ key: "minutes",
85340
+ label: t("Minutes", "Minuti"),
85341
+ offer: {
85342
+ kind: "number",
85343
+ min: 0,
85344
+ step: 1,
85345
+ unit: "min"
85346
+ }
85347
+ }]
85348
+ },
85349
+ {
85350
+ id: "petFountain.pumpOffMinutes",
85351
+ method: "setPumpOffMinutes",
85352
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
85353
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
85354
+ icon: "timer-off",
85355
+ openArgs: [{
85356
+ key: "minutes",
85357
+ label: t("Minutes", "Minuti"),
85358
+ offer: {
85359
+ kind: "number",
85360
+ min: 0,
85361
+ step: 1,
85362
+ unit: "min"
85363
+ }
85364
+ }]
85365
+ },
85366
+ {
85367
+ id: "petFountain.resetDailyConsumption",
85368
+ method: "resetDailyConsumption",
85369
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
85370
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
85371
+ icon: "rotate-ccw"
85372
+ }
85373
+ ],
85374
+ state: [{
85375
+ kind: "boolean",
85376
+ field: "empty",
85377
+ label: t("Tank empty", "Serbatoio vuoto"),
85378
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
85379
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
85380
+ icon: "droplet"
85381
+ }, {
85382
+ kind: "numeric",
85383
+ field: "waterLevel",
85384
+ quantity: t("the water level", "il livello dell’acqua"),
85385
+ label: t("Water level", "Livello dell’acqua"),
85386
+ unit: "%",
85387
+ icon: "droplets"
85388
+ }]
85389
+ },
84376
85390
  "media-player": {
84377
85391
  scope: "device",
84378
85392
  noun: t("Media player", "Lettore multimediale"),
@@ -86197,6 +87211,7 @@ var SYSTEM_CAP_VOCABULARY = {
86197
87211
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
86198
87212
  "device-provider": system(INTERNAL),
86199
87213
  "device-removal-hook": system(INTERNAL),
87214
+ "node-removal-hook": system(INTERNAL),
86200
87215
  "device-state": system("the runtime-state mirror every trigger already reads through"),
86201
87216
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
86202
87217
  "embedding-encoder": system(ML),
@@ -86236,6 +87251,7 @@ var SYSTEM_CAP_VOCABULARY = {
86236
87251
  "recording-archive": system(STORAGE),
86237
87252
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
86238
87253
  records: system(INTERNAL),
87254
+ remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
86239
87255
  "server-management": system(CLUSTER),
86240
87256
  "settings-store": system(INTERNAL),
86241
87257
  "smtp-provider": system(INTERNAL),
@@ -86248,6 +87264,7 @@ var SYSTEM_CAP_VOCABULARY = {
86248
87264
  "storage-provider": system(STORAGE),
86249
87265
  "stream-broker": system(PIPELINE),
86250
87266
  system: system(CLUSTER),
87267
+ template: system("operator-authored templates — edited in their own editor, never actuated"),
86251
87268
  "terminal-session": system(CLUSTER),
86252
87269
  toast: system(UI_PLUMBING),
86253
87270
  training: system(ML),
@@ -86307,7 +87324,7 @@ function projectAction(cap, action) {
86307
87324
  };
86308
87325
  }
86309
87326
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
86310
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
87327
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
86311
87328
  DeviceFeature.BatteryOperated;
86312
87329
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
86313
87330
  var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
@@ -87439,6 +88456,16 @@ Object.freeze({
87439
88456
  form: "single",
87440
88457
  optional: false
87441
88458
  }],
88459
+ "irTransmitter.getInfo": [{
88460
+ name: "deviceId",
88461
+ form: "single",
88462
+ optional: false
88463
+ }],
88464
+ "irTransmitter.send": [{
88465
+ name: "deviceId",
88466
+ form: "single",
88467
+ optional: false
88468
+ }],
87442
88469
  "keychain.deleteDeviceScope": [{
87443
88470
  name: "deviceId",
87444
88471
  form: "single",
@@ -87848,6 +88875,36 @@ Object.freeze({
87848
88875
  form: "single",
87849
88876
  optional: false
87850
88877
  }],
88878
+ "petFountain.getStatus": [{
88879
+ name: "deviceId",
88880
+ form: "single",
88881
+ optional: false
88882
+ }],
88883
+ "petFountain.resetDailyConsumption": [{
88884
+ name: "deviceId",
88885
+ form: "single",
88886
+ optional: false
88887
+ }],
88888
+ "petFountain.setPump": [{
88889
+ name: "deviceId",
88890
+ form: "single",
88891
+ optional: false
88892
+ }],
88893
+ "petFountain.setPumpInterval": [{
88894
+ name: "deviceId",
88895
+ form: "single",
88896
+ optional: false
88897
+ }],
88898
+ "petFountain.setPumpOffMinutes": [{
88899
+ name: "deviceId",
88900
+ form: "single",
88901
+ optional: false
88902
+ }],
88903
+ "petFountain.setPumpOnMinutes": [{
88904
+ name: "deviceId",
88905
+ form: "single",
88906
+ optional: false
88907
+ }],
87851
88908
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
87852
88909
  name: "deviceId",
87853
88910
  form: "single",
@@ -88664,6 +89721,31 @@ Object.freeze({
88664
89721
  form: "single",
88665
89722
  optional: true
88666
89723
  }],
89724
+ "remote.getConfig": [{
89725
+ name: "deviceId",
89726
+ form: "single",
89727
+ optional: false
89728
+ }],
89729
+ "remote.listCommands": [{
89730
+ name: "deviceId",
89731
+ form: "single",
89732
+ optional: false
89733
+ }],
89734
+ "remote.sendCommand": [{
89735
+ name: "deviceId",
89736
+ form: "single",
89737
+ optional: false
89738
+ }],
89739
+ "remote.sendMediaCommand": [{
89740
+ name: "deviceId",
89741
+ form: "single",
89742
+ optional: false
89743
+ }],
89744
+ "remote.setConfig": [{
89745
+ name: "deviceId",
89746
+ form: "single",
89747
+ optional: false
89748
+ }],
88667
89749
  "runningState.getStatus": [{
88668
89750
  name: "deviceId",
88669
89751
  form: "single",
@@ -90898,6 +91980,81 @@ function rewriteFormBody(body, route) {
90898
91980
  function filterForwardedCookies(cookieHeader) {
90899
91981
  return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
90900
91982
  }
91983
+ /** A short stable label for the attempt: enough to group lines, useless as a capability. */
91984
+ function attemptLabel(attemptId) {
91985
+ let hash = 2166136261;
91986
+ for (let i = 0; i < attemptId.length; i += 1) hash = Math.imul(hash ^ attemptId.charCodeAt(i), 16777619) >>> 0;
91987
+ return hash.toString(16).padStart(8, "0");
91988
+ }
91989
+ /** Distinct parameter names of a `?a=1&b=2` string (or of a urlencoded body), in order. */
91990
+ function paramNames(query) {
91991
+ const names = [];
91992
+ const raw = query.startsWith("?") ? query.slice(1) : query;
91993
+ for (const pair of raw.split("&")) {
91994
+ if (pair.length === 0) continue;
91995
+ const eq = pair.indexOf("=");
91996
+ const rawName = eq === -1 ? pair : pair.slice(0, eq);
91997
+ let name;
91998
+ try {
91999
+ name = decodeURIComponent(rawName.replace(/\+/g, " "));
92000
+ } catch {
92001
+ name = rawName;
92002
+ }
92003
+ if (!names.includes(name)) names.push(name);
92004
+ }
92005
+ return names;
92006
+ }
92007
+ function hasOa2(names) {
92008
+ return names.some((name) => name.startsWith("openid.oa2.") || name === "openid.ns.oa2");
92009
+ }
92010
+ function cookieNamesOf(setCookies) {
92011
+ return setCookies.map((c) => c.split(";")[0]?.split("=")[0]?.trim() ?? "");
92012
+ }
92013
+ function mediaType(contentType) {
92014
+ return (contentType ?? "none").split(";")[0]?.trim().toLowerCase() || "none";
92015
+ }
92016
+ /** Host and path of a Location (no query), or 'unparsable'. */
92017
+ function locationShape(location, base) {
92018
+ try {
92019
+ const url = new URL(location, base);
92020
+ return {
92021
+ target: `${url.host}${url.pathname}`,
92022
+ names: paramNames(url.search)
92023
+ };
92024
+ } catch {
92025
+ return {
92026
+ target: "unparsable",
92027
+ names: []
92028
+ };
92029
+ }
92030
+ }
92031
+ /** The `meta` of one trace line. Values: never. */
92032
+ function flowTraceMeta(input) {
92033
+ const queryNames = paramNames(input.requestSearch);
92034
+ const formNames = input.formBody === null ? null : paramNames(input.formBody);
92035
+ const meta = {
92036
+ session: attemptLabel(input.attemptId),
92037
+ method: input.method,
92038
+ upstream: `${input.upstreamHost}${input.upstreamPath}`,
92039
+ queryNames: queryNames.join(",") || "none",
92040
+ oa2InQuery: hasOa2(queryNames),
92041
+ status: input.status,
92042
+ setCookieNames: cookieNamesOf(input.setCookies).join(",") || "none",
92043
+ cookiesSent: (input.cookieHeader ?? "").split(";").filter((p) => p.trim().length > 0).length
92044
+ };
92045
+ if (formNames !== null) {
92046
+ meta["contentType"] = mediaType(input.contentType);
92047
+ meta["formNames"] = formNames.join(",") || "none";
92048
+ meta["oa2InForm"] = hasOa2(formNames);
92049
+ }
92050
+ if (input.location !== null) {
92051
+ const loc = locationShape(input.location, `http://${input.upstreamHost}/`);
92052
+ meta["location"] = loc.target;
92053
+ meta["locationQueryNames"] = loc.names.join(",") || "none";
92054
+ meta["oa2InLocation"] = hasOa2(loc.names);
92055
+ }
92056
+ return meta;
92057
+ }
90901
92058
  var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
90902
92059
  var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
90903
92060
  /** Request headers that are never forwarded to the vendor. */
@@ -91002,6 +92159,8 @@ var VendorLoginProxy = class {
91002
92159
  #now;
91003
92160
  #fetch;
91004
92161
  #attempts = /* @__PURE__ */ new Map();
92162
+ /** Trace lines written per attempt (bounded by FLOW_TRACE_MAX_LINES). */
92163
+ #traced = /* @__PURE__ */ new Map();
91005
92164
  constructor(options) {
91006
92165
  this.#addonId = options.addonId;
91007
92166
  this.#logger = options.logger;
@@ -91028,6 +92187,7 @@ var VendorLoginProxy = class {
91028
92187
  }
91029
92188
  close(attemptId) {
91030
92189
  this.#attempts.delete(attemptId);
92190
+ this.#traced.delete(attemptId);
91031
92191
  }
91032
92192
  isOpen(attemptId) {
91033
92193
  const attempt = this.#attempts.get(attemptId);
@@ -91074,7 +92234,7 @@ var VendorLoginProxy = class {
91074
92234
  const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
91075
92235
  const attempt = this.#attempts.get(parsed.attemptId);
91076
92236
  if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
91077
- if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
92237
+ if (attempt !== void 0) this.close(parsed.attemptId);
91078
92238
  this.#logger.info("vendor login proxy: attempt refused", { meta: {
91079
92239
  addonId: this.#addonId,
91080
92240
  reason: attempt === void 0 ? "missing" : "expired"
@@ -91099,13 +92259,17 @@ var VendorLoginProxy = class {
91099
92259
  }
91100
92260
  const method = (req.method ?? "GET").toUpperCase();
91101
92261
  let body;
92262
+ let formBodyRaw = null;
91102
92263
  if (method !== "GET" && method !== "HEAD") {
91103
92264
  const read = await readRequestBody(req);
91104
92265
  if (read === "too-large") {
91105
92266
  sendPlain(res, 413, "Request too large");
91106
92267
  return;
91107
92268
  }
91108
- body = isFormEncoded(req.headers["content-type"]) ? Buffer.from(rewriteFormBody(read.toString("utf8"), route), "utf8") : read;
92269
+ if (isFormEncoded(req.headers["content-type"])) {
92270
+ formBodyRaw = read.toString("utf8");
92271
+ body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
92272
+ } else body = read;
91109
92273
  }
91110
92274
  const headers = this.#requestHeaders(req, route, upstream);
91111
92275
  const upstreamRes = await this.#fetch(target, {
@@ -91130,6 +92294,19 @@ var VendorLoginProxy = class {
91130
92294
  sentNames: cookieNames(headers["cookie"]),
91131
92295
  vendorSets: setCookies.map(setCookieShape).join(" | ")
91132
92296
  } });
92297
+ this.#trace(parsed.attemptId, {
92298
+ attemptId: parsed.attemptId,
92299
+ method,
92300
+ upstreamHost: upstream.host,
92301
+ upstreamPath: target.pathname,
92302
+ requestSearch: target.search,
92303
+ formBody: formBodyRaw,
92304
+ contentType: firstHeader(req.headers["content-type"]),
92305
+ status: upstreamRes.status,
92306
+ location,
92307
+ setCookies,
92308
+ cookieHeader: headers["cookie"]
92309
+ });
91133
92310
  const settled = await attempt.evaluate({
91134
92311
  status: upstreamRes.status,
91135
92312
  url: target.toString(),
@@ -91138,7 +92315,7 @@ var VendorLoginProxy = class {
91138
92315
  bodyText: text
91139
92316
  });
91140
92317
  if (settled.kind === "done") {
91141
- this.#attempts.delete(parsed.attemptId);
92318
+ this.close(parsed.attemptId);
91142
92319
  this.#logger.info("vendor login proxy: sign-in finished", { meta: {
91143
92320
  addonId: this.#addonId,
91144
92321
  status: settled.landing.status
@@ -91162,6 +92339,16 @@ var VendorLoginProxy = class {
91162
92339
  res.writeHead(upstreamRes.status, out);
91163
92340
  await this.#pipeBody(upstreamRes, res);
91164
92341
  }
92342
+ /** One names-only line per proxied request, at most FLOW_TRACE_MAX_LINES per attempt. */
92343
+ #trace(attemptId, input) {
92344
+ const written = this.#traced.get(attemptId) ?? 0;
92345
+ if (written >= 200) return;
92346
+ this.#traced.set(attemptId, written + 1);
92347
+ this.#logger.info("vendor login proxy: flow", { meta: {
92348
+ addonId: this.#addonId,
92349
+ ...flowTraceMeta(input)
92350
+ } });
92351
+ }
91165
92352
  #requestHeaders(req, route, upstream) {
91166
92353
  const headers = {};
91167
92354
  for (const [name, value] of Object.entries(req.headers)) {
@@ -91203,6 +92390,14 @@ var VendorLoginProxy = class {
91203
92390
  };
91204
92391
  var MB = 1024 * 1024;
91205
92392
  1024 * MB, 3072 * MB;
92393
+ object({
92394
+ manufacturer: string().optional(),
92395
+ supportedModels: array(string()).optional(),
92396
+ supportedController: string().optional(),
92397
+ commandsEncoding: string(),
92398
+ delay: number().nonnegative().optional(),
92399
+ commands: record(string(), unknown())
92400
+ });
91206
92401
  //#endregion
91207
92402
  //#region src/config.ts
91208
92403
  /**
@@ -132863,6 +134058,38 @@ function alexaRegionOptions(amazonDomain) {
132863
134058
  };
132864
134059
  }
132865
134060
  //#endregion
134061
+ //#region src/alexa-url-params.ts
134062
+ var BASE = "http://placeholder.invalid";
134063
+ function describeUrlParams(url) {
134064
+ let parsed;
134065
+ try {
134066
+ parsed = new URL(url, BASE);
134067
+ } catch {
134068
+ return {
134069
+ path: "",
134070
+ names: [],
134071
+ oa2: false,
134072
+ returnTo: false,
134073
+ assocHandle: false,
134074
+ mode: false
134075
+ };
134076
+ }
134077
+ const names = [...new Set(parsed.searchParams.keys())].sort();
134078
+ return {
134079
+ path: parsed.pathname,
134080
+ names,
134081
+ oa2: names.some((n) => n.startsWith("openid.oa2.")),
134082
+ returnTo: names.includes("openid.return_to"),
134083
+ assocHandle: names.includes("openid.assoc_handle"),
134084
+ mode: names.includes("openid.mode")
134085
+ };
134086
+ }
134087
+ /** One-line, value-free rendering of {@link describeUrlParams}. */
134088
+ function formatUrlParams(url) {
134089
+ const d = describeUrlParams(url);
134090
+ return `names=[${d.names.join(",")}] oa2=${d.oa2} return_to=${d.returnTo} assoc_handle=${d.assocHandle} mode=${d.mode}`;
134091
+ }
134092
+ //#endregion
132866
134093
  //#region src/alexa-proxy-trace.ts
132867
134094
  /**
132868
134095
  * Sanitised one-line trace of the alexa-cookie2 proxy's own log lines.
@@ -132883,17 +134110,21 @@ function stripQuery(target) {
132883
134110
  const cut = target.search(/[?#]/);
132884
134111
  return cut === -1 ? target : target.slice(0, cut);
132885
134112
  }
134113
+ /** The target without its query, plus the value-free parameter summary. */
134114
+ function withParams(target) {
134115
+ return `${stripQuery(target)} ${formatUrlParams(target)}`;
134116
+ }
132886
134117
  function sanitizeProxyTraceLine(line) {
132887
134118
  const router = ROUTER_RE.exec(line);
132888
- if (router !== null) return `router ${router[2]} ${stripQuery(router[1] ?? "")}`;
134119
+ if (router !== null) return `router ${router[2]} ${withParams(router[1] ?? "")}`;
132889
134120
  const request = PROXY_REQUEST_RE.exec(line);
132890
- if (request !== null) return `proxy-request ${request[1]} ${stripQuery(request[2] ?? "")}`;
134121
+ if (request !== null) return `proxy-request ${request[1]} ${withParams(request[2] ?? "")}`;
132891
134122
  const response = PROXY_RESPONSE_RE.exec(line);
132892
134123
  if (response !== null) return `proxy-response host=${response[1]}`;
132893
134124
  const original = REDIRECT_ORIGINAL_RE.exec(line);
132894
- if (original !== null) return `redirect-original ${stripQuery(original[1] ?? "")}`;
134125
+ if (original !== null) return `redirect-original ${withParams(original[1] ?? "")}`;
132895
134126
  const final = REDIRECT_FINAL_RE.exec(line);
132896
- if (final !== null) return `redirect-final ${stripQuery(final[1] ?? "")}`;
134127
+ if (final !== null) return `redirect-final ${withParams(final[1] ?? "")}`;
132897
134128
  return null;
132898
134129
  }
132899
134130
  //#endregion
@@ -133009,7 +134240,7 @@ var AlexaProxyLoginSession = class {
133009
134240
  const line = args.map(String).join(" ");
133010
134241
  const trace = sanitizeProxyTraceLine(line);
133011
134242
  if (trace !== null) {
133012
- this.#logger.info(`alexa proxy-login: ${trace}`, { meta: { sessionId: this.#opts.sessionId } });
134243
+ this.#logger.info(`[alexa-login-trace] ${trace}`, { meta: { sessionId: this.#opts.sessionId } });
133013
134244
  return;
133014
134245
  }
133015
134246
  if (SECRET_LINE_RE.test(line)) return;
@@ -133141,20 +134372,36 @@ var ADDON_ID = "import-alexa";
133141
134372
  var ATTEMPT_TTL_MS = 600 * 1e3;
133142
134373
  /** How long the browser waits on the Amazon page for alexa-cookie2 to finish registering. */
133143
134374
  var CAPTURE_WAIT_MS = 6e4;
134375
+ /** The query parameter Amazon sets on `/ap/maplanding` when it honoured the oa2 request. */
134376
+ var AUTHORIZATION_CODE_PARAM = "openid.oa2.authorization_code";
133144
134377
  /**
133145
- * Terminal condition: alexa-cookie2 turns Amazon's final redirect into a 302 to its own
133146
- * `/cookie-success` page once it holds the login cookie. Matching on the path of the raw
133147
- * `Location` keeps this independent of the page's HTML.
134378
+ * Terminal condition. The login is over when alexa-cookie2 turns Amazon's final redirect into
134379
+ * a 302 to its own `/cookie-success` page (it only does so when the landing carries the oa2
134380
+ * authorization code), OR when Amazon's `/ap/maplanding` itself shows up as a `Location` or as
134381
+ * the request. A maplanding WITHOUT the authorization code is a dead end: alexa-cookie2 cannot
134382
+ * register a device from it and no cookie will ever come, so it ends at once as
134383
+ * `no_authorization_code` instead of after the capture wait. Only paths and parameter NAMES are
134384
+ * read, never a value (the query carries signed openid values).
133148
134385
  */
133149
134386
  function cookieSuccessTerminal(view) {
133150
- if (view.location === null) return { kind: "continue" };
133151
- try {
133152
- return new URL(view.location, `http://127.0.0.1`).pathname === "/cookie-success" ? {
134387
+ for (const raw of [view.url, view.location]) {
134388
+ if (raw === null) continue;
134389
+ const end = loginEnd(raw);
134390
+ if (end !== null) return {
133153
134391
  kind: "done",
133154
- payload: true
133155
- } : { kind: "continue" };
134392
+ payload: end
134393
+ };
134394
+ }
134395
+ return { kind: "continue" };
134396
+ }
134397
+ function loginEnd(raw) {
134398
+ try {
134399
+ const url = new URL(raw, `http://${ALEXA_PROXY_LOOPBACK_HOST}`);
134400
+ if (url.pathname === "/cookie-success") return "registering";
134401
+ if (!url.pathname.endsWith("/ap/maplanding")) return null;
134402
+ return url.searchParams.has(AUTHORIZATION_CODE_PARAM) ? "registering" : "no_authorization_code";
133156
134403
  } catch {
133157
- return { kind: "continue" };
134404
+ return null;
133158
134405
  }
133159
134406
  }
133160
134407
  var AlexaProxyLoginManager = class {
@@ -133171,6 +134418,13 @@ var AlexaProxyLoginManager = class {
133171
134418
  this.#getLoginProxy = deps.getLoginProxy;
133172
134419
  this.#captureWaitMs = deps.captureWaitMs ?? CAPTURE_WAIT_MS;
133173
134420
  }
134421
+ /** One value-free line per upstream hop the hub login proxy sees (names only). */
134422
+ #traceHop(sessionId, view) {
134423
+ const url = describeUrlParams(view.url);
134424
+ const landing = url.path.endsWith("/ap/maplanding") ? " maplanding" : "";
134425
+ const location = view.location === null ? "none" : `${describeUrlParams(view.location).path} ${formatUrlParams(view.location)}`;
134426
+ this.#logger.info(`[alexa-login-trace] hub-hop${landing} status=${view.status} url=${url.path} ${formatUrlParams(view.url)} location=${location}`, { meta: { sessionId } });
134427
+ }
133174
134428
  /**
133175
134429
  * Start a fresh proxy-login. Cancels any in-flight session first (single
133176
134430
  * fixed port). Returns the `loginUrl` for the onboarding UI to open.
@@ -133189,8 +134443,19 @@ var AlexaProxyLoginManager = class {
133189
134443
  host: upstreamHost,
133190
134444
  scheme: "http"
133191
134445
  }],
133192
- terminal: cookieSuccessTerminal,
133193
- onDone: async () => {
134446
+ terminal: (view) => {
134447
+ this.#traceHop(sessionId, view);
134448
+ return cookieSuccessTerminal(view);
134449
+ },
134450
+ onDone: async (end) => {
134451
+ if (end === "no_authorization_code") {
134452
+ this.#logger.warn("alexa proxy-login: Amazon ended the login without an authorization code", { meta: { sessionId } });
134453
+ return {
134454
+ status: "error",
134455
+ addonId: ADDON_ID,
134456
+ reason: "no_authorization_code"
134457
+ };
134458
+ }
133194
134459
  return await this.#session?.whenCaptured(this.#captureWaitMs) === true ? {
133195
134460
  status: "success",
133196
134461
  addonId: ADDON_ID