@desktalk/miniapp-preference 0.1.0-alpha.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeskTalk contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ import type { MiniAppManifest, MiniAppContext, MiniAppBackendActivation } from '@desktalk/sdk';
2
+ export declare const manifest: MiniAppManifest;
3
+ /**
4
+ * The Preference MiniApp uses ctx.storage as the persistence layer for settings.
5
+ * In production, the core would provide a privileged ctx.config hook that writes
6
+ * to <config>/config.toml. For now, ctx.storage serves as the backing store.
7
+ *
8
+ * All settings are stored under the key "config" as a flat Config object.
9
+ */
10
+ export declare function activate(ctx: MiniAppContext): MiniAppBackendActivation;
11
+ export declare function deactivate(): void;
12
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAmB/F,eAAO,MAAM,QAAQ,EAAE,eAMtB,CAAC;AAIF;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,wBAAwB,CAiTtE;AAID,wBAAgB,UAAU,IAAI,IAAI,CAEjC"}
@@ -0,0 +1,622 @@
1
+ // src/schema.ts
2
+ var DEFAULT_AI_PROVIDER_ID = "openai";
3
+ var AI_PROVIDER_DEFINITIONS = [
4
+ { id: "anthropic", label: "Anthropic", supportsApiKey: true, supportsBaseUrl: false },
5
+ {
6
+ id: "azure-openai-responses",
7
+ label: "Azure OpenAI",
8
+ supportsApiKey: true,
9
+ supportsBaseUrl: true
10
+ },
11
+ { id: DEFAULT_AI_PROVIDER_ID, label: "OpenAI", supportsApiKey: true, supportsBaseUrl: true },
12
+ { id: "google", label: "Google Gemini", supportsApiKey: true, supportsBaseUrl: false },
13
+ {
14
+ id: "mistral",
15
+ label: "Mistral",
16
+ supportsApiKey: true,
17
+ supportsBaseUrl: true
18
+ },
19
+ { id: "groq", label: "Groq", supportsApiKey: true, supportsBaseUrl: true },
20
+ {
21
+ id: "cerebras",
22
+ label: "Cerebras",
23
+ supportsApiKey: true,
24
+ supportsBaseUrl: true
25
+ },
26
+ { id: "xai", label: "xAI", supportsApiKey: true, supportsBaseUrl: true },
27
+ {
28
+ id: "openrouter",
29
+ label: "OpenRouter",
30
+ supportsApiKey: true,
31
+ supportsBaseUrl: true
32
+ },
33
+ {
34
+ id: "vercel-ai-gateway",
35
+ label: "Vercel AI Gateway",
36
+ supportsApiKey: true,
37
+ supportsBaseUrl: true
38
+ },
39
+ { id: "zai", label: "ZAI", supportsApiKey: true, supportsBaseUrl: false },
40
+ { id: "opencode", label: "OpenCode Zen", supportsApiKey: true, supportsBaseUrl: false },
41
+ { id: "opencode-go", label: "OpenCode Go", supportsApiKey: true, supportsBaseUrl: false },
42
+ {
43
+ id: "huggingface",
44
+ label: "Hugging Face",
45
+ supportsApiKey: true,
46
+ supportsBaseUrl: true
47
+ },
48
+ {
49
+ id: "kimi-coding",
50
+ label: "Kimi For Coding",
51
+ supportsApiKey: true,
52
+ supportsBaseUrl: false
53
+ },
54
+ { id: "minimax", label: "MiniMax", supportsApiKey: true, supportsBaseUrl: false },
55
+ {
56
+ id: "minimax-cn",
57
+ label: "MiniMax China",
58
+ supportsApiKey: true,
59
+ supportsBaseUrl: false
60
+ },
61
+ { id: "ollama", label: "Ollama", supportsApiKey: false, supportsBaseUrl: true }
62
+ ];
63
+ var DEFAULT_VOICE_PROVIDER_ID = "openai-whisper";
64
+ var VOICE_PROVIDER_DEFINITIONS = [
65
+ {
66
+ id: DEFAULT_VOICE_PROVIDER_ID,
67
+ label: "OpenAI Whisper",
68
+ supportsApiKey: true,
69
+ supportsBaseUrl: true,
70
+ supportsModel: true,
71
+ supportsAzureDeployment: false,
72
+ supportsAzureApiVersion: false
73
+ },
74
+ {
75
+ id: "azure-openai-whisper",
76
+ label: "Azure OpenAI Whisper",
77
+ supportsApiKey: true,
78
+ supportsBaseUrl: true,
79
+ supportsModel: false,
80
+ supportsAzureDeployment: true,
81
+ supportsAzureApiVersion: true
82
+ }
83
+ ];
84
+ var AI_PROVIDER_IDS = new Set(AI_PROVIDER_DEFINITIONS.map((provider) => provider.id));
85
+ var VOICE_PROVIDER_IDS = new Set(VOICE_PROVIDER_DEFINITIONS.map((provider) => provider.id));
86
+ function getAiProviderDefinition(providerId) {
87
+ return AI_PROVIDER_DEFINITIONS.find((provider) => provider.id === providerId);
88
+ }
89
+ function getAiProviderConfigKeys(providerId) {
90
+ const definition = getAiProviderDefinition(providerId);
91
+ if (!definition) {
92
+ return [];
93
+ }
94
+ const keys = [`ai.providers.${providerId}.model`];
95
+ if (definition.supportsApiKey) {
96
+ keys.push(`ai.providers.${providerId}.apiKey`);
97
+ }
98
+ if (definition.supportsBaseUrl) {
99
+ keys.push(`ai.providers.${providerId}.baseUrl`);
100
+ }
101
+ return keys;
102
+ }
103
+ function parseAiEnabledProviders(value) {
104
+ if (typeof value !== "string") {
105
+ return [DEFAULT_AI_PROVIDER_ID];
106
+ }
107
+ const providers = value.split(",").map((item) => item.trim()).filter(
108
+ (item, index, items) => item && items.indexOf(item) === index && AI_PROVIDER_IDS.has(item)
109
+ );
110
+ return providers.length > 0 ? providers : [DEFAULT_AI_PROVIDER_ID];
111
+ }
112
+ function serializeAiEnabledProviders(providerIds) {
113
+ return parseAiEnabledProviders(providerIds.join(",")).join(",");
114
+ }
115
+ function hasAiProviderConfig(config, providerId) {
116
+ return getAiProviderConfigKeys(providerId).some((key) => {
117
+ const value = config[key];
118
+ return typeof value === "string" && value.trim().length > 0;
119
+ });
120
+ }
121
+ function getVoiceProviderDefinition(providerId) {
122
+ return VOICE_PROVIDER_DEFINITIONS.find((provider) => provider.id === providerId);
123
+ }
124
+ function getVoiceProviderConfigKeys(providerId) {
125
+ const definition = getVoiceProviderDefinition(providerId);
126
+ if (!definition) {
127
+ return [];
128
+ }
129
+ const keys = [];
130
+ if (definition.supportsApiKey) {
131
+ keys.push(`voice.providers.${providerId}.apiKey`);
132
+ }
133
+ if (definition.supportsModel) {
134
+ keys.push(`voice.providers.${providerId}.model`);
135
+ }
136
+ if (definition.supportsBaseUrl) {
137
+ keys.push(`voice.providers.${providerId}.baseUrl`);
138
+ }
139
+ if (definition.supportsAzureDeployment) {
140
+ keys.push(`voice.providers.${providerId}.azureDeployment`);
141
+ }
142
+ if (definition.supportsAzureApiVersion) {
143
+ keys.push(`voice.providers.${providerId}.azureApiVersion`);
144
+ }
145
+ return keys;
146
+ }
147
+ function parseVoiceEnabledProviders(value) {
148
+ if (typeof value !== "string") {
149
+ return [DEFAULT_VOICE_PROVIDER_ID];
150
+ }
151
+ const providers = value.split(",").map((item) => item.trim()).filter(
152
+ (item, index, items) => item && items.indexOf(item) === index && VOICE_PROVIDER_IDS.has(item)
153
+ );
154
+ return providers.length > 0 ? providers : [DEFAULT_VOICE_PROVIDER_ID];
155
+ }
156
+ function serializeVoiceEnabledProviders(providerIds) {
157
+ return parseVoiceEnabledProviders(providerIds.join(",")).join(",");
158
+ }
159
+ function hasVoiceProviderConfig(config, providerId) {
160
+ return getVoiceProviderConfigKeys(providerId).some((key) => {
161
+ const value = config[key];
162
+ return typeof value === "string" && value.trim().length > 0;
163
+ });
164
+ }
165
+ function getAiProviderPreferenceSchemas() {
166
+ const schemas = [
167
+ {
168
+ key: "ai.enabledProviders",
169
+ label: "Enabled Providers",
170
+ description: "Ordered list of configured AI providers.",
171
+ type: "string",
172
+ default: DEFAULT_AI_PROVIDER_ID,
173
+ category: "AI"
174
+ },
175
+ {
176
+ key: "ai.defaultProvider",
177
+ label: "Default Provider",
178
+ description: "Provider selected by default for chat and tool execution.",
179
+ type: "string",
180
+ default: DEFAULT_AI_PROVIDER_ID,
181
+ options: AI_PROVIDER_DEFINITIONS.map((provider) => provider.id),
182
+ category: "AI"
183
+ }
184
+ ];
185
+ for (const provider of AI_PROVIDER_DEFINITIONS) {
186
+ schemas.push({
187
+ key: `ai.providers.${provider.id}.model`,
188
+ label: `${provider.label} Model`,
189
+ description: `Model identifier to use when ${provider.label} is selected.`,
190
+ type: "string",
191
+ default: "",
192
+ category: "AI"
193
+ });
194
+ if (provider.supportsApiKey) {
195
+ schemas.push({
196
+ key: `ai.providers.${provider.id}.apiKey`,
197
+ label: `${provider.label} API Key`,
198
+ description: `API key for ${provider.label}.`,
199
+ type: "string",
200
+ default: "",
201
+ category: "AI",
202
+ sensitive: true
203
+ });
204
+ }
205
+ if (provider.supportsBaseUrl) {
206
+ schemas.push({
207
+ key: `ai.providers.${provider.id}.baseUrl`,
208
+ label: `${provider.label} Base URL`,
209
+ description: `Optional custom API base URL for ${provider.label}.`,
210
+ type: "string",
211
+ default: "",
212
+ category: "AI"
213
+ });
214
+ }
215
+ }
216
+ schemas.push({
217
+ key: "ai.maxTokens",
218
+ label: "Max Tokens",
219
+ description: "Maximum tokens per AI response.",
220
+ type: "number",
221
+ default: 4096,
222
+ min: 256,
223
+ max: 128e3,
224
+ category: "AI"
225
+ });
226
+ return schemas;
227
+ }
228
+ var AI_PREFERENCE_SCHEMAS = getAiProviderPreferenceSchemas();
229
+ function getVoiceProviderPreferenceSchemas() {
230
+ const schemas = [
231
+ {
232
+ key: "voice.enabledProviders",
233
+ label: "Enabled Providers",
234
+ description: "Ordered list of configured STT providers.",
235
+ type: "string",
236
+ default: DEFAULT_VOICE_PROVIDER_ID,
237
+ category: "Voice"
238
+ },
239
+ {
240
+ key: "voice.defaultProvider",
241
+ label: "Default Provider",
242
+ description: "Provider selected by default for voice transcription.",
243
+ type: "string",
244
+ default: DEFAULT_VOICE_PROVIDER_ID,
245
+ options: VOICE_PROVIDER_DEFINITIONS.map((provider) => provider.id),
246
+ category: "Voice"
247
+ }
248
+ ];
249
+ for (const provider of VOICE_PROVIDER_DEFINITIONS) {
250
+ if (provider.supportsApiKey) {
251
+ schemas.push({
252
+ key: `voice.providers.${provider.id}.apiKey`,
253
+ label: `${provider.label} API Key`,
254
+ description: `API key for ${provider.label}.`,
255
+ type: "string",
256
+ default: "",
257
+ category: "Voice",
258
+ sensitive: true
259
+ });
260
+ }
261
+ if (provider.supportsModel) {
262
+ schemas.push({
263
+ key: `voice.providers.${provider.id}.model`,
264
+ label: `${provider.label} Model`,
265
+ description: `Model identifier to use when ${provider.label} is selected.`,
266
+ type: "string",
267
+ default: "whisper-1",
268
+ category: "Voice"
269
+ });
270
+ }
271
+ if (provider.supportsBaseUrl) {
272
+ schemas.push({
273
+ key: `voice.providers.${provider.id}.baseUrl`,
274
+ label: `${provider.label} Base URL`,
275
+ description: provider.id === "azure-openai-whisper" ? "Base URL for Azure OpenAI Whisper requests." : `Optional custom API base URL for ${provider.label}.`,
276
+ type: "string",
277
+ default: provider.id === DEFAULT_VOICE_PROVIDER_ID ? "https://api.openai.com/v1" : "",
278
+ category: "Voice"
279
+ });
280
+ }
281
+ if (provider.supportsAzureDeployment) {
282
+ schemas.push({
283
+ key: `voice.providers.${provider.id}.azureDeployment`,
284
+ label: `${provider.label} Deployment`,
285
+ description: "Azure OpenAI deployment name for Whisper transcription.",
286
+ type: "string",
287
+ default: "",
288
+ category: "Voice"
289
+ });
290
+ }
291
+ if (provider.supportsAzureApiVersion) {
292
+ schemas.push({
293
+ key: `voice.providers.${provider.id}.azureApiVersion`,
294
+ label: `${provider.label} API Version`,
295
+ description: "Azure OpenAI API version used for transcription requests.",
296
+ type: "string",
297
+ default: "2024-06-01",
298
+ category: "Voice"
299
+ });
300
+ }
301
+ }
302
+ return schemas;
303
+ }
304
+ var VOICE_PREFERENCE_SCHEMAS = getVoiceProviderPreferenceSchemas();
305
+ var PREFERENCE_SCHEMAS = [
306
+ // ─── General ─────────────────────────────────────────────────────────────
307
+ {
308
+ key: "general.theme",
309
+ label: "Theme",
310
+ description: "UI theme: light or dark.",
311
+ type: "string",
312
+ default: "light",
313
+ options: ["light", "dark"],
314
+ category: "General"
315
+ },
316
+ {
317
+ key: "general.accentColor",
318
+ label: "Accent Color",
319
+ description: "Primary theme color. Accepts hex values or any CSS color string.",
320
+ type: "string",
321
+ default: "#7c6ff7",
322
+ category: "General"
323
+ },
324
+ {
325
+ key: "general.language",
326
+ label: "Language",
327
+ description: "UI language/locale.",
328
+ type: "string",
329
+ default: "en",
330
+ options: ["en", "zh", "ja", "ko", "es", "fr", "de"],
331
+ category: "General"
332
+ },
333
+ {
334
+ key: "general.dataDirectory",
335
+ label: "Data Directory",
336
+ description: "Override the base data directory. Leave empty for platform default (resolved via env-paths).",
337
+ type: "string",
338
+ default: "",
339
+ category: "General",
340
+ requiresRestart: true
341
+ },
342
+ // ─── Server ──────────────────────────────────────────────────────────────
343
+ {
344
+ key: "server.host",
345
+ label: "Host",
346
+ description: "Server bind address.",
347
+ type: "string",
348
+ default: "localhost",
349
+ category: "Server",
350
+ requiresRestart: true
351
+ },
352
+ {
353
+ key: "server.port",
354
+ label: "Port",
355
+ description: "Server listen port.",
356
+ type: "number",
357
+ default: 3e3,
358
+ min: 1,
359
+ max: 65535,
360
+ category: "Server",
361
+ requiresRestart: true
362
+ },
363
+ // ─── AI ──────────────────────────────────────────────────────────────────
364
+ ...AI_PREFERENCE_SCHEMAS,
365
+ // ─── Voice ────────────────────────────────────────────────────────────
366
+ ...VOICE_PREFERENCE_SCHEMAS,
367
+ {
368
+ key: "voice.silenceTimeoutMs",
369
+ label: "Silence Timeout",
370
+ description: "Silence duration (ms) before finalizing an utterance.",
371
+ type: "number",
372
+ default: 800,
373
+ min: 200,
374
+ max: 5e3,
375
+ category: "Voice"
376
+ },
377
+ {
378
+ key: "voice.energyThreshold",
379
+ label: "Energy Threshold",
380
+ description: "RMS energy threshold for voice activity detection (0\u201332767).",
381
+ type: "number",
382
+ default: 500,
383
+ min: 50,
384
+ max: 1e4,
385
+ category: "Voice"
386
+ }
387
+ ];
388
+ function getDefaultConfig() {
389
+ const config = {};
390
+ for (const schema of PREFERENCE_SCHEMAS) {
391
+ config[schema.key] = schema.default;
392
+ }
393
+ return config;
394
+ }
395
+ function getSchema(key) {
396
+ return PREFERENCE_SCHEMAS.find((s) => s.key === key);
397
+ }
398
+ function maskSensitive(value) {
399
+ if (value.length <= 4) return "****";
400
+ return "*".repeat(value.length - 4) + value.slice(-4);
401
+ }
402
+
403
+ // src/backend.ts
404
+ var manifest = {
405
+ id: "preference",
406
+ name: "Preferences",
407
+ icon: "\u2699\uFE0F",
408
+ version: "0.1.0",
409
+ description: "Application settings and configuration"
410
+ };
411
+ function activate(ctx) {
412
+ ctx.logger.info("Preference MiniApp activated");
413
+ const defaults = getDefaultConfig();
414
+ async function loadConfig() {
415
+ const stored = await ctx.storage.get("config");
416
+ const config = { ...defaults, ...stored ?? {} };
417
+ const legacyProvider = typeof stored?.["ai.provider"] === "string" ? stored["ai.provider"] : void 0;
418
+ if (legacyProvider && typeof stored?.["ai.defaultProvider"] !== "string") {
419
+ config["ai.defaultProvider"] = legacyProvider;
420
+ }
421
+ if (legacyProvider) {
422
+ const legacyModel = typeof stored?.["ai.model"] === "string" ? stored["ai.model"] : void 0;
423
+ const legacyApiKey = typeof stored?.["ai.apiKey"] === "string" ? stored["ai.apiKey"] : void 0;
424
+ const legacyBaseUrl = typeof stored?.["ai.baseUrl"] === "string" ? stored["ai.baseUrl"] : void 0;
425
+ const providerModelKey = `ai.providers.${legacyProvider}.model`;
426
+ const providerApiKeyKey = `ai.providers.${legacyProvider}.apiKey`;
427
+ const providerBaseUrlKey = `ai.providers.${legacyProvider}.baseUrl`;
428
+ if (legacyModel && !config[providerModelKey]) {
429
+ config[providerModelKey] = legacyModel;
430
+ }
431
+ if (legacyApiKey && !config[providerApiKeyKey]) {
432
+ config[providerApiKeyKey] = legacyApiKey;
433
+ }
434
+ if (legacyBaseUrl && !config[providerBaseUrlKey]) {
435
+ config[providerBaseUrlKey] = legacyBaseUrl;
436
+ }
437
+ }
438
+ const enabledProviders = parseAiEnabledProviders(stored?.["ai.enabledProviders"]);
439
+ const derivedProviders = enabledProviders.filter(
440
+ (providerId) => hasAiProviderConfig(config, providerId)
441
+ );
442
+ for (const schema of PREFERENCE_SCHEMAS) {
443
+ if (!schema.key.startsWith("ai.providers.")) {
444
+ continue;
445
+ }
446
+ const match = /^ai\.providers\.([^.]+)\./.exec(schema.key);
447
+ const providerId = match?.[1];
448
+ if (providerId && hasAiProviderConfig(config, providerId) && !derivedProviders.includes(providerId)) {
449
+ derivedProviders.push(providerId);
450
+ }
451
+ }
452
+ const defaultProvider = typeof config["ai.defaultProvider"] === "string" && config["ai.defaultProvider"] ? String(config["ai.defaultProvider"]) : DEFAULT_AI_PROVIDER_ID;
453
+ if (!derivedProviders.includes(defaultProvider)) {
454
+ derivedProviders.unshift(defaultProvider);
455
+ }
456
+ for (const providerId of enabledProviders) {
457
+ if (!derivedProviders.includes(providerId)) {
458
+ derivedProviders.push(providerId);
459
+ }
460
+ }
461
+ config["ai.enabledProviders"] = serializeAiEnabledProviders(derivedProviders);
462
+ config["ai.defaultProvider"] = derivedProviders[0] ?? DEFAULT_AI_PROVIDER_ID;
463
+ const legacyVoiceProvider = typeof stored?.["voice.provider"] === "string" ? stored["voice.provider"] : void 0;
464
+ if (legacyVoiceProvider && typeof stored?.["voice.defaultProvider"] !== "string") {
465
+ config["voice.defaultProvider"] = legacyVoiceProvider;
466
+ }
467
+ if (legacyVoiceProvider) {
468
+ const legacyVoiceApiKey = typeof stored?.["voice.apiKey"] === "string" ? stored["voice.apiKey"] : void 0;
469
+ const legacyVoiceModel = typeof stored?.["voice.model"] === "string" ? stored["voice.model"] : void 0;
470
+ const legacyVoiceBaseUrl = typeof stored?.["voice.baseUrl"] === "string" ? stored["voice.baseUrl"] : void 0;
471
+ const legacyVoiceDeployment = typeof stored?.["voice.azureDeployment"] === "string" ? stored["voice.azureDeployment"] : void 0;
472
+ const legacyVoiceApiVersion = typeof stored?.["voice.azureApiVersion"] === "string" ? stored["voice.azureApiVersion"] : void 0;
473
+ if (legacyVoiceApiKey && !config[`voice.providers.${legacyVoiceProvider}.apiKey`]) {
474
+ config[`voice.providers.${legacyVoiceProvider}.apiKey`] = legacyVoiceApiKey;
475
+ }
476
+ if (legacyVoiceModel && !config[`voice.providers.${legacyVoiceProvider}.model`]) {
477
+ config[`voice.providers.${legacyVoiceProvider}.model`] = legacyVoiceModel;
478
+ }
479
+ if (legacyVoiceBaseUrl && !config[`voice.providers.${legacyVoiceProvider}.baseUrl`]) {
480
+ config[`voice.providers.${legacyVoiceProvider}.baseUrl`] = legacyVoiceBaseUrl;
481
+ }
482
+ if (legacyVoiceDeployment && !config[`voice.providers.${legacyVoiceProvider}.azureDeployment`]) {
483
+ config[`voice.providers.${legacyVoiceProvider}.azureDeployment`] = legacyVoiceDeployment;
484
+ }
485
+ if (legacyVoiceApiVersion && !config[`voice.providers.${legacyVoiceProvider}.azureApiVersion`]) {
486
+ config[`voice.providers.${legacyVoiceProvider}.azureApiVersion`] = legacyVoiceApiVersion;
487
+ }
488
+ }
489
+ const enabledVoiceProviders = parseVoiceEnabledProviders(stored?.["voice.enabledProviders"]);
490
+ const derivedVoiceProviders = enabledVoiceProviders.filter(
491
+ (providerId) => hasVoiceProviderConfig(config, providerId)
492
+ );
493
+ for (const schema of PREFERENCE_SCHEMAS) {
494
+ if (!schema.key.startsWith("voice.providers.")) {
495
+ continue;
496
+ }
497
+ const match = /^voice\.providers\.([^.]+)\./.exec(schema.key);
498
+ const providerId = match?.[1];
499
+ if (providerId && hasVoiceProviderConfig(config, providerId) && !derivedVoiceProviders.includes(providerId)) {
500
+ derivedVoiceProviders.push(providerId);
501
+ }
502
+ }
503
+ const defaultVoiceProvider = typeof config["voice.defaultProvider"] === "string" && config["voice.defaultProvider"] ? String(config["voice.defaultProvider"]) : DEFAULT_VOICE_PROVIDER_ID;
504
+ if (!derivedVoiceProviders.includes(defaultVoiceProvider)) {
505
+ derivedVoiceProviders.unshift(defaultVoiceProvider);
506
+ }
507
+ for (const providerId of enabledVoiceProviders) {
508
+ if (!derivedVoiceProviders.includes(providerId)) {
509
+ derivedVoiceProviders.push(providerId);
510
+ }
511
+ }
512
+ config["voice.enabledProviders"] = serializeVoiceEnabledProviders(derivedVoiceProviders);
513
+ config["voice.defaultProvider"] = derivedVoiceProviders[0] ?? DEFAULT_VOICE_PROVIDER_ID;
514
+ return config;
515
+ }
516
+ async function saveConfig(config) {
517
+ await ctx.storage.set("config", config);
518
+ }
519
+ function maskConfig(config) {
520
+ const masked = { ...config };
521
+ for (const schema of PREFERENCE_SCHEMAS) {
522
+ if (schema.sensitive && typeof masked[schema.key] === "string") {
523
+ const raw = masked[schema.key];
524
+ masked[schema.key] = raw ? maskSensitive(raw) : "";
525
+ }
526
+ }
527
+ return masked;
528
+ }
529
+ ctx.messaging.onCommand("preferences.getAll", async () => {
530
+ const config = await loadConfig();
531
+ return maskConfig(config);
532
+ });
533
+ ctx.messaging.onCommand(
534
+ "preferences.get",
535
+ async (req) => {
536
+ const config = await loadConfig();
537
+ const schema = getSchema(req.key);
538
+ let value = config[req.key] ?? defaults[req.key];
539
+ if (value === void 0) {
540
+ throw new Error(`Unknown preference key: ${req.key}`);
541
+ }
542
+ if (schema?.sensitive && typeof value === "string" && value) {
543
+ value = maskSensitive(value);
544
+ }
545
+ return { value };
546
+ }
547
+ );
548
+ ctx.messaging.onCommand(
549
+ "preferences.getRaw",
550
+ async (req) => {
551
+ const config = await loadConfig();
552
+ const value = config[req.key] ?? defaults[req.key];
553
+ if (value === void 0) {
554
+ throw new Error(`Unknown preference key: ${req.key}`);
555
+ }
556
+ return { value };
557
+ }
558
+ );
559
+ ctx.messaging.onCommand(
560
+ "preferences.set",
561
+ async (req) => {
562
+ const schema = getSchema(req.key);
563
+ if (!schema) {
564
+ throw new Error(`Unknown preference key: ${req.key}`);
565
+ }
566
+ if (typeof req.value !== schema.type) {
567
+ throw new Error(`Expected ${schema.type} for ${req.key}, got ${typeof req.value}`);
568
+ }
569
+ if (schema.type === "number" && typeof req.value === "number") {
570
+ if (schema.min !== void 0 && req.value < schema.min) {
571
+ throw new Error(`${req.key} must be >= ${schema.min}`);
572
+ }
573
+ if (schema.max !== void 0 && req.value > schema.max) {
574
+ throw new Error(`${req.key} must be <= ${schema.max}`);
575
+ }
576
+ }
577
+ if (schema.options && typeof req.value === "string") {
578
+ if (!schema.options.includes(req.value)) {
579
+ throw new Error(`${req.key} must be one of: ${schema.options.join(", ")}`);
580
+ }
581
+ }
582
+ const config = await loadConfig();
583
+ config[req.key] = req.value;
584
+ await saveConfig(config);
585
+ ctx.logger.info(`Setting updated: ${req.key}`);
586
+ ctx.messaging.emit("preferences:changed", {
587
+ key: req.key,
588
+ value: schema.sensitive ? maskSensitive(String(req.value)) : req.value,
589
+ requiresRestart: schema.requiresRestart ?? false
590
+ });
591
+ }
592
+ );
593
+ ctx.messaging.onCommand("preferences.reset", async (req) => {
594
+ const schema = getSchema(req.key);
595
+ if (!schema) {
596
+ throw new Error(`Unknown preference key: ${req.key}`);
597
+ }
598
+ const config = await loadConfig();
599
+ config[req.key] = schema.default;
600
+ await saveConfig(config);
601
+ ctx.logger.info(`Setting reset: ${req.key}`);
602
+ ctx.messaging.emit("preferences:changed", {
603
+ key: req.key,
604
+ value: schema.default,
605
+ requiresRestart: schema.requiresRestart ?? false
606
+ });
607
+ });
608
+ ctx.messaging.onCommand("preferences.resetAll", async () => {
609
+ await saveConfig({ ...defaults });
610
+ ctx.logger.info("All settings reset to defaults");
611
+ ctx.messaging.emit("preferences:resetAll", {});
612
+ });
613
+ return {};
614
+ }
615
+ function deactivate() {
616
+ }
617
+ export {
618
+ activate,
619
+ deactivate,
620
+ manifest
621
+ };
622
+ //# sourceMappingURL=backend.js.map