@deadair/plugin-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1062 -0
  3. package/dist/article.parse.d.ts +82 -0
  4. package/dist/article.parse.d.ts.map +1 -0
  5. package/dist/boundary.json.safe.d.ts +191 -0
  6. package/dist/boundary.json.safe.d.ts.map +1 -0
  7. package/dist/capabilities/analysis.d.ts +330 -0
  8. package/dist/capabilities/analysis.d.ts.map +1 -0
  9. package/dist/capabilities/charts.d.ts +134 -0
  10. package/dist/capabilities/charts.d.ts.map +1 -0
  11. package/dist/capabilities/enrichment.d.ts +255 -0
  12. package/dist/capabilities/enrichment.d.ts.map +1 -0
  13. package/dist/capabilities/llm.d.ts +318 -0
  14. package/dist/capabilities/llm.d.ts.map +1 -0
  15. package/dist/capabilities/mixer.d.ts +183 -0
  16. package/dist/capabilities/mixer.d.ts.map +1 -0
  17. package/dist/capabilities/music.provider.d.ts +245 -0
  18. package/dist/capabilities/music.provider.d.ts.map +1 -0
  19. package/dist/capabilities/news.d.ts +171 -0
  20. package/dist/capabilities/news.d.ts.map +1 -0
  21. package/dist/capabilities/scrobble.d.ts +133 -0
  22. package/dist/capabilities/scrobble.d.ts.map +1 -0
  23. package/dist/capabilities/search.d.ts +122 -0
  24. package/dist/capabilities/search.d.ts.map +1 -0
  25. package/dist/capabilities/similarity.d.ts +101 -0
  26. package/dist/capabilities/similarity.d.ts.map +1 -0
  27. package/dist/capabilities/speech.d.ts +211 -0
  28. package/dist/capabilities/speech.d.ts.map +1 -0
  29. package/dist/capabilities/weather.d.ts +192 -0
  30. package/dist/capabilities/weather.d.ts.map +1 -0
  31. package/dist/chunk-7QVYU63E.js +7 -0
  32. package/dist/chunk-7QVYU63E.js.map +1 -0
  33. package/dist/define.plugin.d.ts +56 -0
  34. package/dist/define.plugin.d.ts.map +1 -0
  35. package/dist/feed.parse.d.ts +97 -0
  36. package/dist/feed.parse.d.ts.map +1 -0
  37. package/dist/html.text.d.ts +71 -0
  38. package/dist/html.text.d.ts.map +1 -0
  39. package/dist/index.d.ts +30 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +1153 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/match.text.d.ts +28 -0
  44. package/dist/match.text.d.ts.map +1 -0
  45. package/dist/plugin.api.version.d.ts +9 -0
  46. package/dist/plugin.api.version.d.ts.map +1 -0
  47. package/dist/plugin.base.d.ts +80 -0
  48. package/dist/plugin.base.d.ts.map +1 -0
  49. package/dist/plugin.config.fields.d.ts +524 -0
  50. package/dist/plugin.config.fields.d.ts.map +1 -0
  51. package/dist/plugin.config.read.d.ts +52 -0
  52. package/dist/plugin.config.read.d.ts.map +1 -0
  53. package/dist/plugin.error.d.ts +177 -0
  54. package/dist/plugin.error.d.ts.map +1 -0
  55. package/dist/plugin.host.d.ts +220 -0
  56. package/dist/plugin.host.d.ts.map +1 -0
  57. package/dist/plugin.host.response.d.ts +42 -0
  58. package/dist/plugin.host.response.d.ts.map +1 -0
  59. package/dist/plugin.http.d.ts +80 -0
  60. package/dist/plugin.http.d.ts.map +1 -0
  61. package/dist/plugin.lifecycle.d.ts +68 -0
  62. package/dist/plugin.lifecycle.d.ts.map +1 -0
  63. package/dist/plugin.manifest.d.ts +282 -0
  64. package/dist/plugin.manifest.d.ts.map +1 -0
  65. package/dist/plugin.permissions.d.ts +186 -0
  66. package/dist/plugin.permissions.d.ts.map +1 -0
  67. package/dist/testing/fake.plugin.host.d.ts +85 -0
  68. package/dist/testing/fake.plugin.host.d.ts.map +1 -0
  69. package/dist/testing/index.d.ts +10 -0
  70. package/dist/testing/index.d.ts.map +1 -0
  71. package/dist/testing/index.js +141 -0
  72. package/dist/testing/index.js.map +1 -0
  73. package/package.json +67 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.api.version.d.ts","sourceRoot":"","sources":["../src/plugin.api.version.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,UAAU,CAAC"}
@@ -0,0 +1,80 @@
1
+ import type { PluginHost } from './plugin.host.js';
2
+ import type { PluginLifecycle } from './plugin.lifecycle.js';
3
+ /** Undoes one thing a plugin set up. Run in reverse order of registration when the plugin unloads. */
4
+ export type PluginDisposer = () => void | Promise<void>;
5
+ /**
6
+ * The base a plugin extends, so unloading is correct by construction.
7
+ *
8
+ * Two jobs, both of them chores every plugin was otherwise doing by hand:
9
+ *
10
+ * 1. **`this.host` is there or it throws with a sentence.** Each plugin used to
11
+ * hold `host?: PluginHost` and write its own `hostOrThrow()`, or forget to
12
+ * and get a `TypeError` about reading a property of undefined instead.
13
+ * 2. **Teardown is registered next to setup.** {@link Plugin.register} takes an
14
+ * undo, and the base runs every one on unload, last registered first,
15
+ * whether or not {@link Plugin.onUnload} throws.
16
+ *
17
+ * The second matters MORE in this host, not less. Plugins run inside the API process
18
+ * (`packages/plugin-sdk/CLAUDE.md` § "Trust and egress"), so a timer or a socket a plugin forgets
19
+ * is not confined to a sandbox that gets torn down: it lives in the server until somebody restarts
20
+ * it. Reloading a plugin on every config change is a normal thing an operator does, so "forgets on
21
+ * unload" compounds.
22
+ *
23
+ * ```ts
24
+ * class MyPlugin extends Plugin implements EnrichmentPluginInstance {
25
+ * protected async onLoad(): Promise<void> {
26
+ * const timer = setInterval(() => void this.refresh(), 60_000);
27
+ * this.register(() => clearInterval(timer));
28
+ * }
29
+ * }
30
+ * ```
31
+ *
32
+ * Extending this is optional: the host only ever asks for `PluginLifecycle`, so
33
+ * a plugin that implements `init` and `dispose` itself is as valid as it was.
34
+ */
35
+ export declare abstract class Plugin implements PluginLifecycle {
36
+ private currentHost?;
37
+ private readonly disposers;
38
+ /**
39
+ * The host, once `init` has run.
40
+ *
41
+ * A getter rather than a field so the failure is a sentence naming the
42
+ * plugin instead of a `TypeError` from somewhere three calls deeper.
43
+ *
44
+ * @throws {PluginError} `internal` when read before `init` or after
45
+ * `dispose`. Both are host bugs rather than operator ones, which is what
46
+ * that code means.
47
+ */
48
+ protected get host(): PluginHost;
49
+ /**
50
+ * Registers something to undo when this plugin unloads.
51
+ *
52
+ * Call it next to the setup it undoes, which is the whole point: a
53
+ * `clearInterval` written beside its `setInterval` is one that cannot drift
54
+ * away from it as the file grows.
55
+ */
56
+ protected register(disposer: PluginDisposer): void;
57
+ /** {@link Plugin.register} for the common case. Works for `setTimeout` and `setInterval` alike. */
58
+ protected registerTimer(timer: ReturnType<typeof setTimeout>): void;
59
+ /** Your setup. Called once, after `this.host` is available and before any capability method. */
60
+ protected onLoad(): Promise<void>;
61
+ /**
62
+ * Anything left to undo that `register` could not express, such as dropping
63
+ * a cache. Runs after every registered disposer.
64
+ */
65
+ protected onUnload(): Promise<void>;
66
+ init(host: PluginHost): Promise<void>;
67
+ /**
68
+ * Undoes everything, in reverse.
69
+ *
70
+ * Every disposer runs even if an earlier one throws, because one broken
71
+ * undo must not strand the rest: the failure is logged and the walk
72
+ * continues. `onUnload` runs afterwards for the same reason it exists, and
73
+ * `this.host` is released last so both still have it.
74
+ *
75
+ * Idempotent. The host calls it on unload, on a config change and at
76
+ * shutdown, and those can overlap.
77
+ */
78
+ dispose(): Promise<void>;
79
+ }
80
+ //# sourceMappingURL=plugin.base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.base.d.ts","sourceRoot":"","sources":["../src/plugin.base.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,sGAAsG;AACtG,MAAM,MAAM,cAAc,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,8BAAsB,MAAO,YAAW,eAAe;IACnD,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD;;;;;;;;;OASG;IACH,SAAS,KAAK,IAAI,IAAI,UAAU,CAK/B;IAED;;;;;;OAMG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;IAIlD,mGAAmG;IACnG,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,IAAI;IAInE,gGAAgG;IAChG,SAAS,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3C;;;;;;;;;;OAUG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBjC"}
@@ -0,0 +1,524 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The kinds of input a plugin can ask the operator for.
4
+ *
5
+ * - `string` free text, one line
6
+ * - `text` free text over several lines, for anything a person writes
7
+ * rather than pastes: a prompt, a persona, a list of phrasings.
8
+ * Stored exactly like a `string`, so nothing downstream has to
9
+ * know it exists; the difference is the box the operator types
10
+ * into, and a one-line box for a paragraph is the reason a
11
+ * setting like that ends up being edited by hand in psql
12
+ * - `url` free text validated/normalised as a URL
13
+ * - `secret` write-only: the host encrypts it, the settings UI never reads
14
+ * it back, and only `host.secrets.get()` sees the plaintext
15
+ * - `number` numeric input
16
+ * - `boolean` toggle
17
+ * - `select` one of `options`
18
+ * - `multiselect` any number of `options`, stored as a JSON array of the chosen
19
+ * values. Read it back with {@link parseMultiSelect}
20
+ * - `list` any number of ROWS with the same `columns`, stored as a JSON
21
+ * array of objects. Read it back with {@link parseRows}. For a
22
+ * list whose entries have parts — a feed with a name and a
23
+ * category — where the alternative is a `text` field with a
24
+ * separator in it and a line an operator can mistype into
25
+ * silence
26
+ * - `note` not an input at all: static help text rendered in the form
27
+ */
28
+ export type ConfigFieldType = 'string' | 'text' | 'url' | 'secret' | 'number' | 'boolean' | 'select' | 'multiselect' | 'list' | 'note';
29
+ /**
30
+ * What a `number` field's value is measured in, so the form can offer a control a person can use.
31
+ *
32
+ * The VALUE is always stored in the unit named here — `bytes` means the row holds bytes — and the
33
+ * console converts on the way in and out. That is the whole point: a byte count is the right thing
34
+ * for code to compare against and a terrible thing to type, and the alternative to declaring it is
35
+ * either storing a friendlier unit (and doing the multiplication at every reader) or special-casing
36
+ * a particular setting key inside the form, which is the kind of thing nobody finds later.
37
+ *
38
+ * `fraction` is the second member and the one the first paragraph predicted: the row holds a share
39
+ * between 0 and 1, because that is what the code multiplying by it wants, and the console says
40
+ * `40%`, because that is what a person means. Honoured by a `slider`, which is the control every
41
+ * share in this station asks for; a fraction drawn as an ordinary spinner is still shown as the
42
+ * fraction it stores, since a number typed exactly is unambiguous either way.
43
+ *
44
+ * An enum rather than a boolean because the third member is obvious (a duration in milliseconds has
45
+ * exactly the same problem) and because a closed set is what the contract mirroring this can
46
+ * express.
47
+ */
48
+ export type ConfigFieldUnit = 'bytes' | 'fraction';
49
+ /**
50
+ * The control a field asks for, where the default one for its type is not the readable one.
51
+ *
52
+ * {@link ConfigFieldUnit}'s sibling, and the same bargain: what is STORED does not change, only the
53
+ * thing the operator touches. A `slider` over a share between 0 and 1 stores a fraction, and every
54
+ * reader still reads a fraction.
55
+ *
56
+ * Opt-in per field rather than inferred from `min` and `max` being present, which is the whole
57
+ * point. A slider is right for a value somebody feels for (a percentage, a trim in decibels, one
58
+ * pad every N breaks) and wrong for one they have to hit exactly: 3500 out of 0 to 600000 is a
59
+ * pixel, and a pause in milliseconds is a number an operator types rather than aims at. Declaring
60
+ * it makes that a judgement per setting instead of a rule that is right eight times and wrong six.
61
+ *
62
+ * A `slider` must declare both `min` and `max`, since a range with no ends is not one a track can
63
+ * be drawn for. A field asking for one without them falls back to the ordinary input rather than
64
+ * failing: this is a hint about drawing, and a form that renders nothing is worse than a form that
65
+ * renders a spinner.
66
+ *
67
+ * `tags` is for a `string` that is really a SET, stored as one comma-separated line because that is
68
+ * what the reader behind it splits. It changes nothing about the value: the form splits on the way
69
+ * in and joins on the way out, so `dialogueKinds()` keeps parsing exactly the string it always did.
70
+ * What it buys is that a set of names is added to and removed from one at a time, rather than by
71
+ * editing punctuation in a sentence — a stray comma in a one-line box is a kind the station simply
72
+ * does not have, and nothing says so. The cost is that a value CONTAINING a comma cannot be one
73
+ * tag, which is why this is asked for per field rather than being what a `string` does.
74
+ */
75
+ export type ConfigFieldControl = 'slider' | 'tags';
76
+ /** One choice in a `select`, a `multiselect`, or a suggestion list. */
77
+ export interface ConfigFieldOption {
78
+ value: string;
79
+ label: string;
80
+ }
81
+ /**
82
+ * Where a field's or a column's choices come from when neither the plugin nor the operator's own
83
+ * server is the one that knows them.
84
+ *
85
+ * A closed HOST vocabulary, and the third of three ways a choice can be offered: `options` is what
86
+ * the PLUGIN decided when its manifest was written, `suggestConfigOptions()` is what the operator's
87
+ * own server currently says, and this is what the STATION says. It exists because a plugin cannot
88
+ * ask — a news plugin has no way to learn which categories this station holds, and the alternative
89
+ * is a free-text cell where `sports` and `sport` are a silent miss nobody sees until bulletins start
90
+ * declining.
91
+ *
92
+ * `intl.timeZones` is the second member and stretches the name slightly: it is the PLATFORM's list
93
+ * rather than the station's, out of `Intl.supportedValuesOf('timeZone')`. It belongs here anyway,
94
+ * because the property is about who can answer rather than about where the answer is kept, and the
95
+ * console is again the only side that can — a zone name has to be one the browser and the server
96
+ * both know, and a server that enumerated its own would be answering for a different machine.
97
+ *
98
+ * `station.newsFeeds` answers the feeds the installed plugins currently offer, by their qualified id
99
+ * and the operator's own name for each. It is the one source whose value is minted by a PLUGIN and
100
+ * whose list only the station can assemble, which is why it is here rather than being something a
101
+ * plugin could answer: the ids are qualified with the plugin that offered them, and no plugin knows
102
+ * what the others are called.
103
+ *
104
+ * The four `plugins.*` members answer the enabled plugins that declare a given capability — speech,
105
+ * llm, mixer, analysis — by id and name, for the settings that pick which plugin a capability with
106
+ * several installed candidates uses. Those settings stay free text (`selectPlugin` in
107
+ * `plugin.selection.ts` accepts an id that is not currently a candidate without falling back), so
108
+ * this is a suggestion list rather than a closed `select` — the console resolves it the same way as
109
+ * the other sources here, against the plugin list rather than a static enum.
110
+ *
111
+ * `llm.models` is the odd one and the only member whose answer comes from a PLUGIN rather than from
112
+ * a station table: the models the selected model plugin currently offers, for the settings that say
113
+ * which model a particular writer should use. It resolves through the suggestions route every plugin
114
+ * settings form already uses, against whichever plugin `llm.pluginId` names, so the six writer
115
+ * settings offer the same list the plugin's own form does — including which provider each model
116
+ * lives on, since a model name carries that.
117
+ *
118
+ * An enum rather than a boolean for {@link ConfigFieldUnit}'s reason: the next one (voices,
119
+ * personas) is obvious, and a closed set is what the contract mirroring this can express. Whatever
120
+ * it names is resolved by the CONSOLE; nothing here reaches a plugin.
121
+ */
122
+ export type ConfigFieldOptionSource = 'station.newsCategories' | 'station.newsFeeds' | 'intl.timeZones' | 'plugins.speech' | 'plugins.llm' | 'plugins.mixer' | 'plugins.analysis' | 'llm.models';
123
+ /**
124
+ * One column of a `list` field.
125
+ *
126
+ * Still a smaller vocabulary than {@link ConfigFieldType} — no nested `list`, because a table inside
127
+ * a table is a form nobody can fill in — but `secret` is now in it, and the reason it was not is
128
+ * worth keeping because it is what had to be built to allow it. A row was stored as plain JSON with
129
+ * nothing to encrypt one cell against: a ciphertext has to belong to a ROW, and a row had no
130
+ * identity beyond its position in an array the console rewrites whole on every save. {@link ROW_ID_KEY}
131
+ * is that identity, and {@link rowSecretKey} is where the cell's ciphertext lives.
132
+ *
133
+ * Every ordinary cell is stored as a STRING in the row, so a column is about the control the
134
+ * operator gets rather than about the shape of what is kept. A `secret` cell is the exception and
135
+ * the exception is the point: it is never in the row at all, and the row an author reads back has
136
+ * no trace of it. See {@link readRowSecret}.
137
+ */
138
+ export interface ConfigFieldColumn {
139
+ /**
140
+ * Key this cell is stored under inside the row object.
141
+ *
142
+ * Whatever suits the plugin, dots included, exactly like {@link ConfigField.key}. A row editor
143
+ * addresses a cell by a path and a dot in a path means a step into a nested object, but that is
144
+ * the FORM's problem and it solves it the way it already solves the same problem for a
145
+ * dot-keyed station setting: it names its inputs positionally and puts the real keys back on
146
+ * the way out. Nothing a plugin author has to know about.
147
+ *
148
+ * One character is refused, and only because {@link rowSecretKey} joins on it: a `/`. That
149
+ * separator has to be unambiguous or a cell's ciphertext could be addressed by two different
150
+ * keys, so it is refused at the schema rather than escaped.
151
+ */
152
+ key: string;
153
+ /** Column heading. */
154
+ label: string;
155
+ /**
156
+ * `string` free text, `url` free text meant to be an address, `select` one of `options`, and
157
+ * `secret` a credential this row holds — write-only, encrypted per cell, and never in the row.
158
+ */
159
+ type: 'string' | 'url' | 'select' | 'secret';
160
+ /** Whether a row is only counted once this cell is filled in. */
161
+ required?: boolean;
162
+ /** Ghost text inside the cell. */
163
+ placeholder?: string;
164
+ /** Choices, fixed when the manifest is written. See {@link ConfigField.options}. */
165
+ options?: ConfigFieldOption[];
166
+ /** Choices only the station can enumerate. See {@link ConfigFieldOptionSource}. */
167
+ optionsFrom?: ConfigFieldOptionSource;
168
+ /**
169
+ * Key of another column in the same list. This cell only applies to a row whose cell there
170
+ * holds one of {@link dependsOnValues}.
171
+ *
172
+ * For the table whose columns are not all about the same row: a provider list where the
173
+ * address belongs to a self-hosted server and the API key to a vendor, a supplier list where
174
+ * one kind is reached by URL and another by account id. Without it every column is drawn on
175
+ * every row, so an operator meets a cell their row has no use for, with no way to tell it from
176
+ * one they have not filled in yet.
177
+ *
178
+ * **This is not the rendering hint {@link ConfigField.dependsOn} is**, and the difference is
179
+ * the whole reason it is here. A field-level `dependsOn` hides a control and the server never
180
+ * reads it. This says the cell DOES NOT APPLY, so the console declines to send it and the host
181
+ * declines to derive anything from it — which for a `url` column means the row contributes no
182
+ * hostname to the plugin's allowlist (`addressCells` in `plugin.host.factory.ts`). An address
183
+ * typed on a row before its kind was changed would otherwise widen the allowlist by a host the
184
+ * plugin can never call, which is exactly the quiet widening that path exists to refuse.
185
+ *
186
+ * Forgiving in three places, all of them the same instinct as `isVisible` in the console's
187
+ * form: a target this list does not declare shows the cell, a target cell that is EMPTY shows
188
+ * the cell, and values without a target are ignored. The empty case is the load-bearing one. A
189
+ * column has no `default`, so a row somebody has just added holds `''` in every cell, and a
190
+ * rule that hid a cell there would hide it on the one row that most needs filling in.
191
+ *
192
+ * A `secret` cell that does not apply keeps whatever is stored rather than being cleared.
193
+ * Absent already means "keep it" in `plugin.config.rows.ts`, and reading a visibility rule as
194
+ * an instruction to destroy a credential would be a surprise nobody asked for.
195
+ */
196
+ dependsOn?: string;
197
+ /**
198
+ * The values of the {@link dependsOn} cell that this one applies to. Ignored without a target.
199
+ *
200
+ * Omitted means "any value at all", which is what a field-level `dependsOn` already means, so
201
+ * an author who knows that one knows this.
202
+ */
203
+ dependsOnValues?: string[];
204
+ }
205
+ /**
206
+ * The chosen values of a `multiselect`, out of the string it is stored as.
207
+ *
208
+ * Stored as a JSON array because plugin config is a string map, and read back
209
+ * through here so every plugin agrees on the encoding. Tolerant on purpose: a
210
+ * value hand-edited into something unreadable answers empty rather than failing
211
+ * a load, which for a field like "which models can use tools" is the difference
212
+ * between a degraded station and one that will not start.
213
+ */
214
+ export declare function parseMultiSelect(raw: unknown): string[];
215
+ /**
216
+ * The cell key a row's own identity is stored under.
217
+ *
218
+ * A reserved name rather than a column an author declares, because it is not data: nobody types it,
219
+ * nothing renders it, and a plugin that ignores it entirely is a plugin that behaves exactly as it
220
+ * did before this existed. The host mints one when a row is first saved and preserves it forever
221
+ * after.
222
+ *
223
+ * ## Why a row needs a name at all
224
+ *
225
+ * Only so a {@link ConfigFieldColumn} may be a `secret`. A secret is encrypted and kept out of the
226
+ * row, which means something has to say which row a given ciphertext belongs to — and until this
227
+ * existed the only answer was "the third one", from a console that rewrites the whole array on
228
+ * every save and lets the operator reorder it. Position is not identity. A `$` leads it because no
229
+ * sensible column key does, and because a row whose keys are printed somewhere reads as obviously
230
+ * not-a-column.
231
+ */
232
+ export declare const ROW_ID_KEY = "$id";
233
+ /**
234
+ * Where a `secret` cell's ciphertext lives, in the same flat map a `secret` FIELD's does.
235
+ *
236
+ * `field/row/column`, joined on the one character a field key and a column key may not contain
237
+ * (both schemas refuse it below). That is what keeps this unambiguous against a plain secret
238
+ * field's key, which is a bare field key and can therefore never collide with a three-part one.
239
+ *
240
+ * One map rather than a nested shape because everything already built for secrets — encrypting per
241
+ * entry, reporting configured-ness as a boolean, the rule that no value ever leaves the server —
242
+ * works on a flat `Record<string, string>` and needed no changes to carry these.
243
+ */
244
+ export declare function rowSecretKey(fieldKey: string, rowId: string, columnKey: string): string;
245
+ /**
246
+ * Whether a stored secret key belongs to a row rather than to a field.
247
+ *
248
+ * For the host, which merges stored secrets back into the form to validate it: a row's cells belong
249
+ * inside their row and putting them at the top level would show a plugin's schema three keys it has
250
+ * never declared.
251
+ */
252
+ export declare function isRowSecretKey(key: string): boolean;
253
+ /**
254
+ * The rows of a `list` field, out of the string it is stored as.
255
+ *
256
+ * A JSON array of objects in a string, for {@link parseMultiSelect}'s reason and encoded the same
257
+ * way, so a `list` needs nothing of the storage path that a `string` did not already have. Tolerant
258
+ * in the same way and for the same stakes: anything unreadable is no rows rather than a plugin that
259
+ * will not load, and a cell that is not a string is dropped rather than stringified, since a number
260
+ * where a URL was expected is a mistake worth seeing as an empty cell.
261
+ *
262
+ * A row with nothing in it is dropped, because the form leaves one behind whenever an operator adds
263
+ * a row and thinks better of it.
264
+ */
265
+ export declare function parseRows(raw: unknown): Record<string, string>[];
266
+ /**
267
+ * A declarative description of one row in a plugin's settings form. The host
268
+ * renders these; plugins never ship UI.
269
+ */
270
+ export interface ConfigField {
271
+ /** Key this value is stored under, and the key `host.config`/`host.secrets` reads it back by. */
272
+ key: string;
273
+ /** Human label shown next to the input. */
274
+ label: string;
275
+ type: ConfigFieldType;
276
+ /** Whether the form refuses to save without a value. Defaults to false. */
277
+ required?: boolean;
278
+ /** Prefilled value. Never provide a default for a `secret`. */
279
+ default?: string | number | boolean;
280
+ /**
281
+ * What a `number`'s value is measured in. Ignored on every other type.
282
+ *
283
+ * See {@link ConfigFieldUnit}: the stored value stays in this unit and only the control the
284
+ * operator touches changes.
285
+ */
286
+ unit?: ConfigFieldUnit;
287
+ /**
288
+ * The control to draw this field with, where the ordinary one for its type reads badly.
289
+ *
290
+ * `slider` is for a `number` and is ignored without both `min` and `max`; `tags` is for a
291
+ * `string`. See {@link ConfigFieldControl}. Nothing about the stored value changes either way.
292
+ */
293
+ control?: ConfigFieldControl;
294
+ /**
295
+ * How coarsely a `control` moves. Ignored without one, and defaults to 1.
296
+ *
297
+ * The unit is the field's own, so a share between 0 and 1 wants `0.05` and a word count wants
298
+ * `10`. Worth setting on anything whose range is wider than the pixels it is drawn in, since
299
+ * the alternative is a control that can express values nobody wants and cannot be stopped on
300
+ * the ones they do.
301
+ */
302
+ step?: number;
303
+ /**
304
+ * The smallest and largest a `number` may be, inclusive. Ignored on every other type.
305
+ *
306
+ * A range the field is DECLARED with rather than one the reader clamps to, which is the whole
307
+ * point: a resolver that clamps answers a legal number for an illegal one, so the station runs
308
+ * on something the console never showed and the operator never chose. Declared here, the console
309
+ * refuses it in front of them and the station's own settings route refuses it again for anything
310
+ * that did not come through a console.
311
+ *
312
+ * A plugin's config is NOT validated against these — the host stores what it is handed and a
313
+ * plugin's own schema is what judges it — so for a plugin these are a hint to the form. For a
314
+ * station setting they are enforced, in `serializeSetting`.
315
+ */
316
+ min?: number;
317
+ max?: number;
318
+ /** Ghost text inside the input. */
319
+ placeholder?: string;
320
+ /** Longer explanation rendered under the input. */
321
+ help?: string;
322
+ /**
323
+ * Choices, for a `select` or a `multiselect`.
324
+ *
325
+ * Fixed when the manifest is written, so this is for a closed set the plugin
326
+ * decides. For anything the operator's own server decides, implement
327
+ * `suggestConfigOptions()` instead: what it returns for this key replaces
328
+ * these, and it can also turn a `string` into free text with suggestions.
329
+ */
330
+ options?: ConfigFieldOption[];
331
+ /**
332
+ * Choices only the console can enumerate. See {@link ConfigFieldOptionSource}.
333
+ *
334
+ * The field-level twin of {@link ConfigFieldColumn.optionsFrom}, and it resolves the same way
335
+ * and merges at the same point: whatever `suggestConfigOptions()` says for this key wins, and
336
+ * this is what is offered when it says nothing.
337
+ */
338
+ optionsFrom?: ConfigFieldOptionSource;
339
+ /**
340
+ * The columns of a `list`, in the order they are drawn. Ignored on every other type.
341
+ *
342
+ * A `list` with none is a field with nothing to fill in, so declare at least one.
343
+ */
344
+ columns?: ConfigFieldColumn[];
345
+ /**
346
+ * Key of another field in the same form. This field is only shown when
347
+ * that field has a truthy value.
348
+ */
349
+ dependsOn?: string;
350
+ /**
351
+ * Key of the `number` field that is the UPPER end of the range this one opens. Declared on the
352
+ * lower end only, and ignored on every other type.
353
+ *
354
+ * Two settings, still: each keeps its own key, its own row and its own validation, and
355
+ * `serializeSetting` refuses each one by name exactly as it did before. What this changes is
356
+ * that the console draws them as one control with two handles instead of two boxes that happen
357
+ * to sit next to each other.
358
+ *
359
+ * The reason is legibility. Two boxes cannot say that one is the far end of the other, so a
360
+ * range arrives as two settings whose labels have to carry the relationship ("fewest", "most",
361
+ * "the other end") and an operator reads the pair rather than seeing it. One track with two
362
+ * handles says it in the shape of the control.
363
+ *
364
+ * NOT correctness, which is worth stating because it is the plausible reason and it is wrong
365
+ * here: every reader of a paired setting in this station takes the two ends as an UNORDERED
366
+ * pair and sorts them, so a range stored the wrong way round has always been tolerated rather
367
+ * than obeyed. The handles not crossing is a nicety on top, not the point.
368
+ *
369
+ * Worth declaring only where the declared range is narrow enough that the whole track is
370
+ * usable. A pair bounded by a typo guard rather than by intent — one to a hundred and twenty
371
+ * minutes, for a station that runs eight to twelve — puts both handles in the first tenth and
372
+ * makes the exact figure somebody has in mind a pixel. Those stay two boxes.
373
+ *
374
+ * A rendering hint like {@link ConfigField.dependsOn}, and forgiving in the same way: if the
375
+ * named key is not in this form, both ends fall back to their own controls rather than one of
376
+ * them disappearing. A settings page that draws one group of a larger set is the ordinary case
377
+ * for that.
378
+ */
379
+ rangeWith?: string;
380
+ }
381
+ export declare const configFieldOptionSchema: z.ZodObject<{
382
+ value: z.ZodString;
383
+ label: z.ZodString;
384
+ }, z.core.$strip>;
385
+ export declare const configFieldTypeSchema: z.ZodEnum<{
386
+ string: "string";
387
+ number: "number";
388
+ boolean: "boolean";
389
+ text: "text";
390
+ url: "url";
391
+ secret: "secret";
392
+ select: "select";
393
+ multiselect: "multiselect";
394
+ list: "list";
395
+ note: "note";
396
+ }>;
397
+ export declare const configFieldUnitSchema: z.ZodEnum<{
398
+ bytes: "bytes";
399
+ fraction: "fraction";
400
+ }>;
401
+ export declare const configFieldControlSchema: z.ZodEnum<{
402
+ slider: "slider";
403
+ tags: "tags";
404
+ }>;
405
+ /**
406
+ * Every member of {@link ConfigFieldOptionSource}, and it has to stay every member: this validates
407
+ * real manifests at load, so a source missing here is a plugin the host refuses to start. Two were
408
+ * missing for exactly that reason and nothing caught it, because no bundled plugin had asked for
409
+ * one yet.
410
+ */
411
+ export declare const configFieldOptionSourceSchema: z.ZodEnum<{
412
+ "station.newsCategories": "station.newsCategories";
413
+ "station.newsFeeds": "station.newsFeeds";
414
+ "intl.timeZones": "intl.timeZones";
415
+ "plugins.speech": "plugins.speech";
416
+ "plugins.llm": "plugins.llm";
417
+ "plugins.mixer": "plugins.mixer";
418
+ "plugins.analysis": "plugins.analysis";
419
+ "llm.models": "llm.models";
420
+ }>;
421
+ export declare const configFieldColumnSchema: z.ZodObject<{
422
+ key: z.ZodString;
423
+ label: z.ZodString;
424
+ type: z.ZodEnum<{
425
+ string: "string";
426
+ url: "url";
427
+ secret: "secret";
428
+ select: "select";
429
+ }>;
430
+ required: z.ZodOptional<z.ZodBoolean>;
431
+ placeholder: z.ZodOptional<z.ZodString>;
432
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
433
+ value: z.ZodString;
434
+ label: z.ZodString;
435
+ }, z.core.$strip>>>;
436
+ optionsFrom: z.ZodOptional<z.ZodEnum<{
437
+ "station.newsCategories": "station.newsCategories";
438
+ "station.newsFeeds": "station.newsFeeds";
439
+ "intl.timeZones": "intl.timeZones";
440
+ "plugins.speech": "plugins.speech";
441
+ "plugins.llm": "plugins.llm";
442
+ "plugins.mixer": "plugins.mixer";
443
+ "plugins.analysis": "plugins.analysis";
444
+ "llm.models": "llm.models";
445
+ }>>;
446
+ dependsOn: z.ZodOptional<z.ZodString>;
447
+ dependsOnValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
448
+ }, z.core.$strip>;
449
+ export declare const configFieldSchema: z.ZodObject<{
450
+ key: z.ZodString;
451
+ label: z.ZodString;
452
+ type: z.ZodEnum<{
453
+ string: "string";
454
+ number: "number";
455
+ boolean: "boolean";
456
+ text: "text";
457
+ url: "url";
458
+ secret: "secret";
459
+ select: "select";
460
+ multiselect: "multiselect";
461
+ list: "list";
462
+ note: "note";
463
+ }>;
464
+ required: z.ZodOptional<z.ZodBoolean>;
465
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
466
+ unit: z.ZodOptional<z.ZodEnum<{
467
+ bytes: "bytes";
468
+ fraction: "fraction";
469
+ }>>;
470
+ control: z.ZodOptional<z.ZodEnum<{
471
+ slider: "slider";
472
+ tags: "tags";
473
+ }>>;
474
+ step: z.ZodOptional<z.ZodNumber>;
475
+ min: z.ZodOptional<z.ZodNumber>;
476
+ max: z.ZodOptional<z.ZodNumber>;
477
+ placeholder: z.ZodOptional<z.ZodString>;
478
+ help: z.ZodOptional<z.ZodString>;
479
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
480
+ value: z.ZodString;
481
+ label: z.ZodString;
482
+ }, z.core.$strip>>>;
483
+ optionsFrom: z.ZodOptional<z.ZodEnum<{
484
+ "station.newsCategories": "station.newsCategories";
485
+ "station.newsFeeds": "station.newsFeeds";
486
+ "intl.timeZones": "intl.timeZones";
487
+ "plugins.speech": "plugins.speech";
488
+ "plugins.llm": "plugins.llm";
489
+ "plugins.mixer": "plugins.mixer";
490
+ "plugins.analysis": "plugins.analysis";
491
+ "llm.models": "llm.models";
492
+ }>>;
493
+ columns: z.ZodOptional<z.ZodArray<z.ZodObject<{
494
+ key: z.ZodString;
495
+ label: z.ZodString;
496
+ type: z.ZodEnum<{
497
+ string: "string";
498
+ url: "url";
499
+ secret: "secret";
500
+ select: "select";
501
+ }>;
502
+ required: z.ZodOptional<z.ZodBoolean>;
503
+ placeholder: z.ZodOptional<z.ZodString>;
504
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
505
+ value: z.ZodString;
506
+ label: z.ZodString;
507
+ }, z.core.$strip>>>;
508
+ optionsFrom: z.ZodOptional<z.ZodEnum<{
509
+ "station.newsCategories": "station.newsCategories";
510
+ "station.newsFeeds": "station.newsFeeds";
511
+ "intl.timeZones": "intl.timeZones";
512
+ "plugins.speech": "plugins.speech";
513
+ "plugins.llm": "plugins.llm";
514
+ "plugins.mixer": "plugins.mixer";
515
+ "plugins.analysis": "plugins.analysis";
516
+ "llm.models": "llm.models";
517
+ }>>;
518
+ dependsOn: z.ZodOptional<z.ZodString>;
519
+ dependsOnValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
520
+ }, z.core.$strip>>>;
521
+ dependsOn: z.ZodOptional<z.ZodString>;
522
+ rangeWith: z.ZodOptional<z.ZodString>;
523
+ }, z.core.$strip>;
524
+ //# sourceMappingURL=plugin.config.fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.config.fields.d.ts","sourceRoot":"","sources":["../src/plugin.config.fields.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvI;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,UAAU,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEnD,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,MAAM,uBAAuB,GAC7B,wBAAwB,GACxB,mBAAmB,GACnB,gBAAgB,GAChB,gBAAgB,GAChB,aAAa,GACb,eAAe,GACf,kBAAkB,GAClB,YAAY,CAAC;AAEnB;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAC9B;;;;;;;;;;;;OAYG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,IAAI,EAAE,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAE7C,iEAAiE;IACjE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,oFAAoF;IACpF,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAE9B,mFAAmF;IACnF,WAAW,CAAC,EAAE,uBAAuB,CAAC;IAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAUvD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,QAAQ,CAAC;AAEhC;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAoBhE;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IACxB,iGAAiG;IACjG,GAAG,EAAE,MAAM,CAAC;IAEZ,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IAEd,IAAI,EAAE,eAAe,CAAC;IAEtB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAEpC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,eAAe,CAAC;IAEvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAE7B;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;;;;;;;;OAYG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAE9B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAC;IAEtC;;;;OAIG;IACH,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAE9B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,uBAAuB;;;iBAGlC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;EAA4G,CAAC;AAE/I,eAAO,MAAM,qBAAqB;;;EAAgC,CAAC;AAEnE,eAAO,MAAM,wBAAwB;;;EAA6B,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;EASxC,CAAC;AAMH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkBlC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkB5B,CAAC"}