@camstack/addon-ai 0.4.30 → 0.4.31

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
@@ -6076,6 +6076,40 @@ var BaseAddon = class {
6076
6076
  deviceSettingsSchema() {
6077
6077
  return null;
6078
6078
  }
6079
+ /**
6080
+ * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
6081
+ * ARE the configuration of its integration.
6082
+ *
6083
+ * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
6084
+ * operator should find on the addon's integration page (System →
6085
+ * Integrations → <name>) rather than only in the cluster-wide list of every
6086
+ * addon. Empty (the default) means the addon has no integration-level
6087
+ * settings and no such surface is offered — this is opt-in, because whether
6088
+ * an addon's configuration IS its integration's configuration depends on the
6089
+ * nature of the integration.
6090
+ *
6091
+ * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
6092
+ * the ONE global schema, in the ONE addon store, written by the ONE
6093
+ * `updateGlobalSettings` path. There is deliberately no
6094
+ * `updateIntegrationSettings`: a second write path is how a surface acquires
6095
+ * a second store key, and this repo has shipped that twice (`btmPath@hub`,
6096
+ * D266). Selecting sections cannot introduce a key that selecting cannot.
6097
+ *
6098
+ * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
6099
+ * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
6100
+ * removed with the reason recorded at
6101
+ * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
6102
+ * determined by WHICH schema it lives in, not by a field-level marker."* A
6103
+ * marker sprinkled across sections also has to borrow a field that already
6104
+ * means something else; borrowing `section.tab` put the literal word
6105
+ * "integration" into an operator-facing tab bar, because `tab` means "how to
6106
+ * GROUP this visually" and cannot also mean "where this lives" (D269
6107
+ * supersedes D268). One declaration, in one place, next to the schema whose
6108
+ * ids it names.
6109
+ */
6110
+ integrationSettingSections() {
6111
+ return [];
6112
+ }
6079
6113
  async getGlobalSettings(overlay, cap, nodeId) {
6080
6114
  const schema = this.globalSettingsSchema(cap);
6081
6115
  if (!schema) return { sections: [] };
@@ -6086,6 +6120,55 @@ var BaseAddon = class {
6086
6120
  } : projected);
6087
6121
  }
6088
6122
  /**
6123
+ * The integration-level view of this addon's settings: exactly the sections
6124
+ * named by {@link integrationSettingSections}, hydrated from the SAME store
6125
+ * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
6126
+ *
6127
+ * Returns `null` when the addon declared nothing — an addon that opts out has
6128
+ * no integration settings surface at all, rather than an empty one that reads
6129
+ * as a failed load.
6130
+ *
6131
+ * Three properties hold BY CONSTRUCTION, which is why they are here in core
6132
+ * and not in whichever UI happens to render this:
6133
+ *
6134
+ * 1. **One key.** The payload is a SUBSET of the global schema, so a field
6135
+ * shown here is the same field, with the same bare key, that the addon's
6136
+ * own page shows. There is no integration-specific writer — callers save
6137
+ * through `updateGlobalSettings` — so a second store key is unreachable,
6138
+ * not merely discouraged.
6139
+ * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
6140
+ * is `<key>@<nodeId>` and an integration is not a node; whichever node
6141
+ * such a field silently picked would be a wrong answer for the operator
6142
+ * who opened the page (D266).
6143
+ * 3. **No silent typo.** A declared id that names no section throws. The
6144
+ * alternative — skip it — turns a rename into a surface that quietly
6145
+ * empties, which looks exactly like an addon with nothing to configure.
6146
+ */
6147
+ async getIntegrationSettings(nodeId) {
6148
+ const declared = this.integrationSettingSections();
6149
+ if (declared.length === 0) return null;
6150
+ const schema = this.globalSettingsSchema();
6151
+ if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
6152
+ const byId = new Map(schema.sections.map((section) => [section.id, section]));
6153
+ const sections = [];
6154
+ for (const id of declared) {
6155
+ const section = byId.get(id);
6156
+ if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
6157
+ const fields = dropPerNodeFields(section.fields);
6158
+ if (fields.length === 0) continue;
6159
+ sections.push({
6160
+ ...section,
6161
+ fields
6162
+ });
6163
+ }
6164
+ if (sections.length === 0) return null;
6165
+ const projected = await this.resolveGlobalStore(nodeId);
6166
+ return hydrateSchema({
6167
+ ...schema,
6168
+ sections
6169
+ }, projected);
6170
+ }
6171
+ /**
6089
6172
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
6090
6173
  * every `perNode: true` field carries THAT node's scoped value on its bare
6091
6174
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -6389,6 +6472,41 @@ var BaseAddon = class {
6389
6472
  * `hydrateSchema` does. Valueless structural fields (separator/info/…)
6390
6473
  * don't declare `perNode` and are excluded by the `in` narrowing.
6391
6474
  */
6475
+ /**
6476
+ * The same fields with every `perNode: true` one removed, recursing into layout
6477
+ * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
6478
+ * with no child is dropped rather than rendered empty.
6479
+ *
6480
+ * Used by `getIntegrationSettings`: an integration is not a node, so a field
6481
+ * whose store key is `<key>@<nodeId>` has no node to belong to there.
6482
+ */
6483
+ function dropPerNodeFields(fields) {
6484
+ const kept = [];
6485
+ for (const field of fields) {
6486
+ if (field.type === "group") {
6487
+ const inner = dropPerNodeFields(field.fields);
6488
+ if (inner.length > 0) kept.push({
6489
+ ...field,
6490
+ fields: inner
6491
+ });
6492
+ continue;
6493
+ }
6494
+ if (field.type === "sub-tabs") {
6495
+ const tabs = field.tabs.map((tab) => ({
6496
+ ...tab,
6497
+ fields: dropPerNodeFields(tab.fields)
6498
+ })).filter((tab) => tab.fields.length > 0);
6499
+ if (tabs.length > 0) kept.push({
6500
+ ...field,
6501
+ tabs
6502
+ });
6503
+ continue;
6504
+ }
6505
+ if ("perNode" in field && field.perNode === true) continue;
6506
+ kept.push(field);
6507
+ }
6508
+ return kept;
6509
+ }
6392
6510
  function collectPerNodeFieldKeys(fields) {
6393
6511
  const collected = [];
6394
6512
  for (const field of fields) {
@@ -9601,6 +9719,9 @@ method(object({
9601
9719
  kind: "mutation",
9602
9720
  auth: "admin"
9603
9721
  }), method(object({
9722
+ addonId: string(),
9723
+ nodeId: string().optional()
9724
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
9604
9725
  addonId: string(),
9605
9726
  deviceId: number$1(),
9606
9727
  nodeId: string().optional()
@@ -28089,6 +28210,12 @@ Object.freeze({
28089
28210
  addonId: null,
28090
28211
  access: "view"
28091
28212
  },
28213
+ "addonSettings.getIntegrationSettings": {
28214
+ capName: "addon-settings",
28215
+ capScope: "system",
28216
+ addonId: null,
28217
+ access: "view"
28218
+ },
28092
28219
  "addonSettings.updateDeviceSettings": {
28093
28220
  capName: "addon-settings",
28094
28221
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -6103,6 +6103,40 @@ var BaseAddon = class {
6103
6103
  deviceSettingsSchema() {
6104
6104
  return null;
6105
6105
  }
6106
+ /**
6107
+ * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
6108
+ * ARE the configuration of its integration.
6109
+ *
6110
+ * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
6111
+ * operator should find on the addon's integration page (System →
6112
+ * Integrations → <name>) rather than only in the cluster-wide list of every
6113
+ * addon. Empty (the default) means the addon has no integration-level
6114
+ * settings and no such surface is offered — this is opt-in, because whether
6115
+ * an addon's configuration IS its integration's configuration depends on the
6116
+ * nature of the integration.
6117
+ *
6118
+ * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
6119
+ * the ONE global schema, in the ONE addon store, written by the ONE
6120
+ * `updateGlobalSettings` path. There is deliberately no
6121
+ * `updateIntegrationSettings`: a second write path is how a surface acquires
6122
+ * a second store key, and this repo has shipped that twice (`btmPath@hub`,
6123
+ * D266). Selecting sections cannot introduce a key that selecting cannot.
6124
+ *
6125
+ * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
6126
+ * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
6127
+ * removed with the reason recorded at
6128
+ * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
6129
+ * determined by WHICH schema it lives in, not by a field-level marker."* A
6130
+ * marker sprinkled across sections also has to borrow a field that already
6131
+ * means something else; borrowing `section.tab` put the literal word
6132
+ * "integration" into an operator-facing tab bar, because `tab` means "how to
6133
+ * GROUP this visually" and cannot also mean "where this lives" (D269
6134
+ * supersedes D268). One declaration, in one place, next to the schema whose
6135
+ * ids it names.
6136
+ */
6137
+ integrationSettingSections() {
6138
+ return [];
6139
+ }
6106
6140
  async getGlobalSettings(overlay, cap, nodeId) {
6107
6141
  const schema = this.globalSettingsSchema(cap);
6108
6142
  if (!schema) return { sections: [] };
@@ -6113,6 +6147,55 @@ var BaseAddon = class {
6113
6147
  } : projected);
6114
6148
  }
6115
6149
  /**
6150
+ * The integration-level view of this addon's settings: exactly the sections
6151
+ * named by {@link integrationSettingSections}, hydrated from the SAME store
6152
+ * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
6153
+ *
6154
+ * Returns `null` when the addon declared nothing — an addon that opts out has
6155
+ * no integration settings surface at all, rather than an empty one that reads
6156
+ * as a failed load.
6157
+ *
6158
+ * Three properties hold BY CONSTRUCTION, which is why they are here in core
6159
+ * and not in whichever UI happens to render this:
6160
+ *
6161
+ * 1. **One key.** The payload is a SUBSET of the global schema, so a field
6162
+ * shown here is the same field, with the same bare key, that the addon's
6163
+ * own page shows. There is no integration-specific writer — callers save
6164
+ * through `updateGlobalSettings` — so a second store key is unreachable,
6165
+ * not merely discouraged.
6166
+ * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
6167
+ * is `<key>@<nodeId>` and an integration is not a node; whichever node
6168
+ * such a field silently picked would be a wrong answer for the operator
6169
+ * who opened the page (D266).
6170
+ * 3. **No silent typo.** A declared id that names no section throws. The
6171
+ * alternative — skip it — turns a rename into a surface that quietly
6172
+ * empties, which looks exactly like an addon with nothing to configure.
6173
+ */
6174
+ async getIntegrationSettings(nodeId) {
6175
+ const declared = this.integrationSettingSections();
6176
+ if (declared.length === 0) return null;
6177
+ const schema = this.globalSettingsSchema();
6178
+ if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
6179
+ const byId = new Map(schema.sections.map((section) => [section.id, section]));
6180
+ const sections = [];
6181
+ for (const id of declared) {
6182
+ const section = byId.get(id);
6183
+ if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
6184
+ const fields = dropPerNodeFields(section.fields);
6185
+ if (fields.length === 0) continue;
6186
+ sections.push({
6187
+ ...section,
6188
+ fields
6189
+ });
6190
+ }
6191
+ if (sections.length === 0) return null;
6192
+ const projected = await this.resolveGlobalStore(nodeId);
6193
+ return hydrateSchema({
6194
+ ...schema,
6195
+ sections
6196
+ }, projected);
6197
+ }
6198
+ /**
6116
6199
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
6117
6200
  * every `perNode: true` field carries THAT node's scoped value on its bare
6118
6201
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -6416,6 +6499,41 @@ var BaseAddon = class {
6416
6499
  * `hydrateSchema` does. Valueless structural fields (separator/info/…)
6417
6500
  * don't declare `perNode` and are excluded by the `in` narrowing.
6418
6501
  */
6502
+ /**
6503
+ * The same fields with every `perNode: true` one removed, recursing into layout
6504
+ * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
6505
+ * with no child is dropped rather than rendered empty.
6506
+ *
6507
+ * Used by `getIntegrationSettings`: an integration is not a node, so a field
6508
+ * whose store key is `<key>@<nodeId>` has no node to belong to there.
6509
+ */
6510
+ function dropPerNodeFields(fields) {
6511
+ const kept = [];
6512
+ for (const field of fields) {
6513
+ if (field.type === "group") {
6514
+ const inner = dropPerNodeFields(field.fields);
6515
+ if (inner.length > 0) kept.push({
6516
+ ...field,
6517
+ fields: inner
6518
+ });
6519
+ continue;
6520
+ }
6521
+ if (field.type === "sub-tabs") {
6522
+ const tabs = field.tabs.map((tab) => ({
6523
+ ...tab,
6524
+ fields: dropPerNodeFields(tab.fields)
6525
+ })).filter((tab) => tab.fields.length > 0);
6526
+ if (tabs.length > 0) kept.push({
6527
+ ...field,
6528
+ tabs
6529
+ });
6530
+ continue;
6531
+ }
6532
+ if ("perNode" in field && field.perNode === true) continue;
6533
+ kept.push(field);
6534
+ }
6535
+ return kept;
6536
+ }
6419
6537
  function collectPerNodeFieldKeys(fields) {
6420
6538
  const collected = [];
6421
6539
  for (const field of fields) {
@@ -9628,6 +9746,9 @@ method(object({
9628
9746
  kind: "mutation",
9629
9747
  auth: "admin"
9630
9748
  }), method(object({
9749
+ addonId: string(),
9750
+ nodeId: string().optional()
9751
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
9631
9752
  addonId: string(),
9632
9753
  deviceId: number$1(),
9633
9754
  nodeId: string().optional()
@@ -28116,6 +28237,12 @@ Object.freeze({
28116
28237
  addonId: null,
28117
28238
  access: "view"
28118
28239
  },
28240
+ "addonSettings.getIntegrationSettings": {
28241
+ capName: "addon-settings",
28242
+ capScope: "system",
28243
+ addonId: null,
28244
+ access: "view"
28245
+ },
28119
28246
  "addonSettings.updateDeviceSettings": {
28120
28247
  capName: "addon-settings",
28121
28248
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.30",
3
+ "version": "0.4.31",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",