@vybestack/llxprt-code-settings 0.10.0-nightly.260613.1adad3b34

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 (44) hide show
  1. package/dist/.last_build +0 -0
  2. package/dist/index.d.ts +8 -0
  3. package/dist/index.js +9 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/__tests__/SettingsService.test.d.ts +12 -0
  6. package/dist/src/__tests__/SettingsService.test.js +231 -0
  7. package/dist/src/__tests__/SettingsService.test.js.map +1 -0
  8. package/dist/src/__tests__/settingsRegistry.test.d.ts +15 -0
  9. package/dist/src/__tests__/settingsRegistry.test.js +471 -0
  10. package/dist/src/__tests__/settingsRegistry.test.js.map +1 -0
  11. package/dist/src/__tests__/settingsServiceInstance.test.d.ts +13 -0
  12. package/dist/src/__tests__/settingsServiceInstance.test.js +60 -0
  13. package/dist/src/__tests__/settingsServiceInstance.test.js.map +1 -0
  14. package/dist/src/index.d.ts +15 -0
  15. package/dist/src/index.js +14 -0
  16. package/dist/src/index.js.map +1 -0
  17. package/dist/src/profiles/ProfileManager.d.ts +91 -0
  18. package/dist/src/profiles/ProfileManager.js +328 -0
  19. package/dist/src/profiles/ProfileManager.js.map +1 -0
  20. package/dist/src/profiles/__tests__/ProfileManager.test.d.ts +12 -0
  21. package/dist/src/profiles/__tests__/ProfileManager.test.js +294 -0
  22. package/dist/src/profiles/__tests__/ProfileManager.test.js.map +1 -0
  23. package/dist/src/profiles/types.d.ts +196 -0
  24. package/dist/src/profiles/types.js +48 -0
  25. package/dist/src/profiles/types.js.map +1 -0
  26. package/dist/src/settings/SettingsService.d.ts +59 -0
  27. package/dist/src/settings/SettingsService.js +289 -0
  28. package/dist/src/settings/SettingsService.js.map +1 -0
  29. package/dist/src/settings/settingsRegistry.d.ts +72 -0
  30. package/dist/src/settings/settingsRegistry.js +1469 -0
  31. package/dist/src/settings/settingsRegistry.js.map +1 -0
  32. package/dist/src/settings/settingsServiceInstance.d.ts +21 -0
  33. package/dist/src/settings/settingsServiceInstance.js +34 -0
  34. package/dist/src/settings/settingsServiceInstance.js.map +1 -0
  35. package/dist/src/storage/Storage.d.ts +8 -0
  36. package/dist/src/storage/Storage.js +9 -0
  37. package/dist/src/storage/Storage.js.map +1 -0
  38. package/dist/src/storage/__tests__/Storage.test.d.ts +11 -0
  39. package/dist/src/storage/__tests__/Storage.test.js +160 -0
  40. package/dist/src/storage/__tests__/Storage.test.js.map +1 -0
  41. package/dist/src/types.d.ts +16 -0
  42. package/dist/src/types.js +16 -0
  43. package/dist/src/types.js.map +1 -0
  44. package/package.json +66 -0
@@ -0,0 +1,1469 @@
1
+ /**
2
+ * @plan PLAN-20260608-ISSUE1588.P05
3
+ *
4
+ * Settings registry — migrated from core.
5
+ * Explicit temporary duplicate; core copy remains until P09.
6
+ * Settings-owned: does NOT import core compression types.
7
+ */
8
+ /* eslint-disable max-lines -- Phase 5: legacy core boundary retained while larger decomposition continues. */
9
+ const COMPRESSION_STRATEGIES = [
10
+ 'middle-out',
11
+ 'top-down-truncation',
12
+ 'one-shot',
13
+ 'high-density',
14
+ ];
15
+ const ALIAS_NORMALIZATION_RULES = {
16
+ 'max-tokens': 'max_tokens',
17
+ maxTokens: 'max_tokens',
18
+ 'response-format': 'response_format',
19
+ responseFormat: 'response_format',
20
+ 'tool-choice': 'tool_choice',
21
+ toolChoice: 'tool_choice',
22
+ 'disabled-tools': 'tools.disabled',
23
+ };
24
+ const HEADER_PRESERVE_SET = new Set([
25
+ 'user-agent',
26
+ 'content-type',
27
+ 'authorization',
28
+ 'x-api-key',
29
+ ]);
30
+ export const SETTINGS_REGISTRY = [
31
+ {
32
+ key: 'auth-key',
33
+ aliases: ['apiKey', 'api-key'],
34
+ category: 'provider-config',
35
+ description: 'Provider API authentication key',
36
+ type: 'string',
37
+ persistToProfile: true,
38
+ },
39
+ {
40
+ key: 'auth-keyfile',
41
+ aliases: ['apiKeyfile', 'api-keyfile'],
42
+ category: 'provider-config',
43
+ description: 'Path to file containing API key',
44
+ type: 'string',
45
+ persistToProfile: true,
46
+ },
47
+ {
48
+ key: 'auth-key-name',
49
+ category: 'provider-config',
50
+ description: 'Name of a saved API key in the keyring (resolved via /key save)',
51
+ type: 'string',
52
+ persistToProfile: true,
53
+ },
54
+ {
55
+ key: 'base-url',
56
+ category: 'provider-config',
57
+ description: 'Provider API base URL',
58
+ type: 'string',
59
+ persistToProfile: true,
60
+ },
61
+ {
62
+ key: 'sandbox-base-url',
63
+ category: 'provider-config',
64
+ description: 'Base URL override used when running inside a container sandbox (Docker/Podman)',
65
+ type: 'string',
66
+ persistToProfile: true,
67
+ },
68
+ {
69
+ key: 'requires-auth',
70
+ category: 'provider-config',
71
+ description: 'Whether the provider requires API key authentication (set to false for local providers)',
72
+ type: 'boolean',
73
+ persistToProfile: true,
74
+ },
75
+ {
76
+ key: 'model',
77
+ category: 'provider-config',
78
+ description: 'Default model name',
79
+ type: 'string',
80
+ persistToProfile: true,
81
+ },
82
+ {
83
+ key: 'defaultModel',
84
+ category: 'provider-config',
85
+ description: 'Fallback model if primary unavailable',
86
+ type: 'string',
87
+ persistToProfile: true,
88
+ },
89
+ {
90
+ key: 'enabled',
91
+ category: 'provider-config',
92
+ description: 'Enable/disable provider',
93
+ type: 'boolean',
94
+ persistToProfile: true,
95
+ },
96
+ {
97
+ key: 'toolFormat',
98
+ aliases: ['tool-format'],
99
+ category: 'provider-config',
100
+ description: 'Tool format preference',
101
+ type: 'enum',
102
+ enumValues: [
103
+ 'auto',
104
+ 'openai',
105
+ 'anthropic',
106
+ 'qwen',
107
+ 'kimi',
108
+ 'hermes',
109
+ 'xml',
110
+ 'deepseek',
111
+ 'gemma',
112
+ 'llama',
113
+ ],
114
+ persistToProfile: true,
115
+ },
116
+ {
117
+ key: 'toolFormatOverride',
118
+ aliases: ['tool-format-override'],
119
+ category: 'provider-config',
120
+ description: 'Force specific tool format',
121
+ type: 'enum',
122
+ enumValues: [
123
+ 'auto',
124
+ 'openai',
125
+ 'anthropic',
126
+ 'qwen',
127
+ 'kimi',
128
+ 'hermes',
129
+ 'xml',
130
+ 'deepseek',
131
+ 'gemma',
132
+ 'llama',
133
+ ],
134
+ persistToProfile: true,
135
+ },
136
+ {
137
+ key: 'api-version',
138
+ category: 'cli-behavior',
139
+ description: 'API version to use',
140
+ type: 'string',
141
+ persistToProfile: true,
142
+ },
143
+ {
144
+ key: 'reasoning.enabled',
145
+ category: 'model-behavior',
146
+ description: 'Enable thinking/reasoning for models that support it',
147
+ type: 'boolean',
148
+ persistToProfile: true,
149
+ completionOptions: [
150
+ { value: 'true', description: 'Enable thinking' },
151
+ { value: 'false', description: 'Disable thinking' },
152
+ ],
153
+ },
154
+ {
155
+ key: 'reasoning.effort',
156
+ category: 'model-behavior',
157
+ description: 'How much the model should think before responding (minimal/low/medium/high/xhigh)',
158
+ type: 'enum',
159
+ enumValues: ['minimal', 'low', 'medium', 'high', 'xhigh'],
160
+ persistToProfile: true,
161
+ },
162
+ {
163
+ key: 'reasoning.maxTokens',
164
+ category: 'model-behavior',
165
+ description: 'Maximum token budget for reasoning',
166
+ type: 'number',
167
+ persistToProfile: true,
168
+ },
169
+ {
170
+ key: 'reasoning.budgetTokens',
171
+ category: 'model-behavior',
172
+ description: 'Token budget for reasoning (Anthropic-specific)',
173
+ type: 'number',
174
+ persistToProfile: true,
175
+ },
176
+ {
177
+ key: 'reasoning.adaptiveThinking',
178
+ category: 'model-behavior',
179
+ description: 'Enable adaptive thinking for Anthropic Opus 4.6+ (true/false)',
180
+ type: 'boolean',
181
+ persistToProfile: true,
182
+ },
183
+ {
184
+ key: 'reasoning.includeInResponse',
185
+ category: 'cli-behavior',
186
+ description: 'Show thinking blocks in UI output',
187
+ type: 'boolean',
188
+ persistToProfile: true,
189
+ },
190
+ {
191
+ key: 'reasoning.includeInContext',
192
+ category: 'cli-behavior',
193
+ description: 'Keep thinking in conversation history',
194
+ type: 'boolean',
195
+ persistToProfile: true,
196
+ },
197
+ {
198
+ key: 'reasoning.stripFromContext',
199
+ category: 'cli-behavior',
200
+ description: 'Remove thinking blocks from context (all/allButLast/none)',
201
+ type: 'enum',
202
+ enumValues: ['all', 'allButLast', 'none'],
203
+ persistToProfile: true,
204
+ },
205
+ {
206
+ key: 'reasoning.format',
207
+ category: 'cli-behavior',
208
+ description: 'API format for reasoning (native/field)',
209
+ type: 'enum',
210
+ enumValues: ['native', 'field'],
211
+ persistToProfile: true,
212
+ },
213
+ {
214
+ key: 'reasoning.summary',
215
+ category: 'model-behavior',
216
+ description: 'OpenAI Responses API reasoning summary mode (auto/concise/detailed/none)',
217
+ type: 'enum',
218
+ enumValues: ['auto', 'concise', 'detailed', 'none'],
219
+ persistToProfile: true,
220
+ },
221
+ {
222
+ key: 'text.verbosity',
223
+ category: 'model-behavior',
224
+ description: 'OpenAI Responses API text verbosity for thinking output (low/medium/high)',
225
+ type: 'enum',
226
+ enumValues: ['low', 'medium', 'high'],
227
+ persistToProfile: true,
228
+ },
229
+ {
230
+ key: 'prompt-caching',
231
+ category: 'model-behavior',
232
+ description: 'Enable prompt caching (off/5m/1h/24h)',
233
+ type: 'enum',
234
+ enumValues: ['off', '5m', '1h', '24h'],
235
+ persistToProfile: true,
236
+ },
237
+ {
238
+ key: 'rate-limit-throttle',
239
+ category: 'model-behavior',
240
+ description: 'Enable proactive rate limit throttling (on/off)',
241
+ type: 'enum',
242
+ enumValues: ['on', 'off'],
243
+ persistToProfile: true,
244
+ },
245
+ {
246
+ key: 'rate-limit-throttle-threshold',
247
+ category: 'model-behavior',
248
+ description: 'Percentage threshold for rate limit throttling (1-100)',
249
+ type: 'number',
250
+ persistToProfile: true,
251
+ },
252
+ {
253
+ key: 'rate-limit-max-wait',
254
+ category: 'model-behavior',
255
+ description: 'Maximum wait time in milliseconds for rate limit throttling',
256
+ type: 'number',
257
+ persistToProfile: true,
258
+ },
259
+ {
260
+ key: 'shell-replacement',
261
+ category: 'cli-behavior',
262
+ description: 'Command substitution mode for shell tool',
263
+ type: 'string',
264
+ enumValues: ['allowlist', 'all', 'none', 'true', 'false'],
265
+ persistToProfile: true,
266
+ },
267
+ {
268
+ key: 'streaming',
269
+ category: 'cli-behavior',
270
+ description: 'Enable/disable streaming (enabled/disabled)',
271
+ type: 'enum',
272
+ enumValues: ['enabled', 'disabled'],
273
+ persistToProfile: true,
274
+ completionOptions: [
275
+ { value: 'enabled', description: 'Enable streaming' },
276
+ { value: 'disabled', description: 'Disable streaming' },
277
+ ],
278
+ parse: (raw) => {
279
+ if (raw === 'true')
280
+ return 'enabled';
281
+ if (raw === 'false')
282
+ return 'disabled';
283
+ return raw;
284
+ },
285
+ validate: (value) => {
286
+ const validModes = ['enabled', 'disabled'];
287
+ if (typeof value === 'string' && validModes.includes(value)) {
288
+ return { success: true, value };
289
+ }
290
+ return {
291
+ success: false,
292
+ message: `Invalid streaming mode '${String(value)}'. Valid modes are: ${validModes.join(', ')}`,
293
+ };
294
+ },
295
+ },
296
+ {
297
+ key: 'context-limit',
298
+ category: 'cli-behavior',
299
+ description: 'Maximum number of tokens for the context window',
300
+ type: 'number',
301
+ hint: 'positive integer (e.g., 100000)',
302
+ persistToProfile: true,
303
+ validate: (value) => {
304
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
305
+ return { success: true, value };
306
+ }
307
+ return {
308
+ success: false,
309
+ message: 'context-limit must be a positive integer (e.g., 100000)',
310
+ };
311
+ },
312
+ },
313
+ {
314
+ key: 'compression-threshold',
315
+ category: 'cli-behavior',
316
+ description: 'Fraction of context limit that triggers compression (0.0-1.0)',
317
+ type: 'number',
318
+ hint: 'decimal between 0 and 1 (e.g., 0.7)',
319
+ persistToProfile: true,
320
+ validate: (value) => {
321
+ if (typeof value === 'number' && value <= 1) {
322
+ return { success: true, value };
323
+ }
324
+ return {
325
+ success: false,
326
+ message: 'compression-threshold must be a decimal between 0 and 1 (e.g., 0.7 for 70%)',
327
+ };
328
+ },
329
+ },
330
+ {
331
+ key: 'tool-output-max-items',
332
+ category: 'cli-behavior',
333
+ description: 'Maximum number of items/files/matches returned by tools',
334
+ type: 'number',
335
+ persistToProfile: true,
336
+ validate: (value) => {
337
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
338
+ return { success: true, value };
339
+ }
340
+ return {
341
+ success: false,
342
+ message: 'tool-output-max-items must be a positive integer',
343
+ };
344
+ },
345
+ },
346
+ {
347
+ key: 'file-read-max-lines',
348
+ category: 'cli-behavior',
349
+ description: 'Default maximum lines to read from text files when no explicit limit is provided (default: 2000)',
350
+ type: 'number',
351
+ persistToProfile: true,
352
+ validate: (value) => {
353
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
354
+ return { success: true, value };
355
+ }
356
+ return {
357
+ success: false,
358
+ message: 'file-read-max-lines must be a positive integer',
359
+ };
360
+ },
361
+ },
362
+ {
363
+ key: 'tool-output-max-tokens',
364
+ category: 'cli-behavior',
365
+ description: 'Maximum tokens in tool output',
366
+ type: 'number',
367
+ persistToProfile: true,
368
+ },
369
+ {
370
+ key: 'tool-output-truncate-mode',
371
+ category: 'cli-behavior',
372
+ description: 'How to handle exceeding limits (warn/truncate/sample)',
373
+ type: 'enum',
374
+ enumValues: ['warn', 'truncate', 'sample'],
375
+ persistToProfile: true,
376
+ },
377
+ {
378
+ key: 'tool-output-item-size-limit',
379
+ category: 'cli-behavior',
380
+ description: 'Maximum size per item/file in bytes',
381
+ type: 'number',
382
+ persistToProfile: true,
383
+ },
384
+ {
385
+ key: 'max-prompt-tokens',
386
+ category: 'cli-behavior',
387
+ description: 'Maximum tokens allowed in any prompt sent to LLM',
388
+ type: 'number',
389
+ persistToProfile: true,
390
+ },
391
+ {
392
+ key: 'maxTurnsPerPrompt',
393
+ category: 'cli-behavior',
394
+ description: 'Maximum number of turns allowed per prompt before stopping (default: -1 for unlimited)',
395
+ type: 'number',
396
+ persistToProfile: true,
397
+ default: -1,
398
+ validate: (value) => {
399
+ if (typeof value === 'number' &&
400
+ Number.isInteger(value) &&
401
+ (value === -1 || value > 0)) {
402
+ return { success: true, value };
403
+ }
404
+ return {
405
+ success: false,
406
+ message: 'maxTurnsPerPrompt must be a positive integer or -1 for unlimited',
407
+ };
408
+ },
409
+ },
410
+ {
411
+ key: 'loopDetectionEnabled',
412
+ category: 'cli-behavior',
413
+ description: 'Enable/disable all loop detection mechanisms (true/false)',
414
+ type: 'boolean',
415
+ persistToProfile: true,
416
+ default: true,
417
+ },
418
+ {
419
+ key: 'toolCallLoopThreshold',
420
+ category: 'cli-behavior',
421
+ description: 'Number of consecutive identical tool calls before triggering loop detection (default: 50, -1 = unlimited)',
422
+ type: 'number',
423
+ persistToProfile: true,
424
+ default: 50,
425
+ validate: (value) => {
426
+ if (typeof value === 'number' &&
427
+ Number.isInteger(value) &&
428
+ (value === -1 || value > 0)) {
429
+ return { success: true, value };
430
+ }
431
+ return {
432
+ success: false,
433
+ message: 'toolCallLoopThreshold must be a positive integer or -1 for unlimited',
434
+ };
435
+ },
436
+ },
437
+ {
438
+ key: 'contentLoopThreshold',
439
+ category: 'cli-behavior',
440
+ description: 'Number of content chunk repetitions before triggering loop detection (default: 50, -1 = unlimited)',
441
+ type: 'number',
442
+ persistToProfile: true,
443
+ default: 50,
444
+ validate: (value) => {
445
+ if (typeof value === 'number' &&
446
+ Number.isInteger(value) &&
447
+ (value === -1 || value > 0)) {
448
+ return { success: true, value };
449
+ }
450
+ return {
451
+ success: false,
452
+ message: 'contentLoopThreshold must be a positive integer or -1 for unlimited',
453
+ };
454
+ },
455
+ },
456
+ {
457
+ key: 'retries',
458
+ category: 'cli-behavior',
459
+ description: 'Maximum number of retry attempts for API calls',
460
+ type: 'number',
461
+ persistToProfile: true,
462
+ },
463
+ {
464
+ key: 'retrywait',
465
+ category: 'cli-behavior',
466
+ description: 'Initial delay in milliseconds between retry attempts',
467
+ type: 'number',
468
+ persistToProfile: true,
469
+ },
470
+ {
471
+ key: 'auth-retry-timeout',
472
+ category: 'cli-behavior',
473
+ description: 'Timeout in milliseconds for mid-turn OAuth reauthentication attempts',
474
+ type: 'number',
475
+ hint: 'positive integer in milliseconds (default: 30000)',
476
+ persistToProfile: true,
477
+ default: 30000,
478
+ validate: (value) => {
479
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
480
+ return { success: true, value };
481
+ }
482
+ return {
483
+ success: false,
484
+ message: 'auth-retry-timeout must be a positive integer in milliseconds (e.g., 30000)',
485
+ };
486
+ },
487
+ },
488
+ {
489
+ key: 'socket-timeout',
490
+ category: 'cli-behavior',
491
+ description: 'Request timeout in milliseconds for local AI servers',
492
+ type: 'number',
493
+ hint: 'positive integer in milliseconds (e.g., 60000)',
494
+ persistToProfile: true,
495
+ },
496
+ {
497
+ key: 'socket-keepalive',
498
+ category: 'cli-behavior',
499
+ description: 'Enable TCP keepalive for local AI server connections',
500
+ type: 'boolean',
501
+ persistToProfile: true,
502
+ },
503
+ {
504
+ key: 'socket-nodelay',
505
+ category: 'cli-behavior',
506
+ description: 'Enable TCP_NODELAY for local AI servers',
507
+ type: 'boolean',
508
+ persistToProfile: true,
509
+ },
510
+ {
511
+ key: 'emojifilter',
512
+ category: 'cli-behavior',
513
+ description: 'Emoji filter mode (allowed/auto/warn/error)',
514
+ type: 'enum',
515
+ enumValues: ['allowed', 'auto', 'warn', 'error'],
516
+ persistToProfile: true,
517
+ parse: (raw) => raw.toLowerCase(),
518
+ },
519
+ {
520
+ key: 'dumponerror',
521
+ category: 'cli-behavior',
522
+ description: 'Dump API request body to ~/.llxprt/dumps/ on errors (enabled/disabled)',
523
+ type: 'enum',
524
+ enumValues: ['enabled', 'disabled'],
525
+ persistToProfile: true,
526
+ },
527
+ {
528
+ key: 'dumpcontext',
529
+ category: 'cli-behavior',
530
+ description: 'Control context dumping (now/status/on/error/off)',
531
+ type: 'enum',
532
+ enumValues: ['now', 'status', 'on', 'error', 'off'],
533
+ persistToProfile: true,
534
+ },
535
+ {
536
+ key: 'authOnly',
537
+ category: 'cli-behavior',
538
+ description: 'Force OAuth authentication only',
539
+ type: 'boolean',
540
+ persistToProfile: true,
541
+ },
542
+ {
543
+ key: 'auth.noBrowser',
544
+ category: 'cli-behavior',
545
+ description: 'Skip automatic browser OAuth flow and prompt for manual code entry',
546
+ type: 'boolean',
547
+ default: false,
548
+ persistToProfile: true,
549
+ completionOptions: [
550
+ { value: 'true', description: 'Force manual OAuth code entry' },
551
+ { value: 'false', description: 'Allow automatic browser launch' },
552
+ ],
553
+ },
554
+ {
555
+ key: 'todo-continuation',
556
+ category: 'cli-behavior',
557
+ description: 'Enable todo continuation mode',
558
+ type: 'boolean',
559
+ persistToProfile: true,
560
+ },
561
+ {
562
+ key: 'tools.disabled',
563
+ aliases: ['disabled-tools'],
564
+ category: 'cli-behavior',
565
+ description: 'Disabled tools list',
566
+ type: 'string-array',
567
+ persistToProfile: true,
568
+ },
569
+ {
570
+ key: 'tools.allowed',
571
+ category: 'cli-behavior',
572
+ description: 'Allowed tools list',
573
+ type: 'string-array',
574
+ persistToProfile: true,
575
+ },
576
+ {
577
+ key: 'stream-options',
578
+ category: 'cli-behavior',
579
+ description: 'Stream options for OpenAI API',
580
+ type: 'json',
581
+ persistToProfile: true,
582
+ },
583
+ {
584
+ key: 'include-folder-structure',
585
+ category: 'cli-behavior',
586
+ description: 'Include folder structure in system prompts',
587
+ type: 'boolean',
588
+ persistToProfile: true,
589
+ },
590
+ {
591
+ key: 'enable-tool-prompts',
592
+ category: 'cli-behavior',
593
+ description: 'Load tool-specific prompts from ~/.llxprt/prompts/tools/**',
594
+ type: 'boolean',
595
+ persistToProfile: true,
596
+ },
597
+ {
598
+ key: 'model.canSaveCore',
599
+ category: 'cli-behavior',
600
+ description: 'Allow the model to save core (system) memories via save_memory tool. ' +
601
+ 'WARNING: Unsafe — the model can override your directives when this is enabled.',
602
+ type: 'boolean',
603
+ default: false,
604
+ persistToProfile: false,
605
+ completionOptions: [
606
+ {
607
+ value: 'true',
608
+ description: 'Enable (unsafe: model can override your system directives)',
609
+ },
610
+ { value: 'false', description: 'Disable (default, recommended)' },
611
+ ],
612
+ },
613
+ {
614
+ key: 'model.allMemoriesAreCore',
615
+ category: 'cli-behavior',
616
+ description: 'Load LLXPRT.md files as part of the system prompt instead of user context. ' +
617
+ 'Useful for models that strictly follow system directives.',
618
+ type: 'boolean',
619
+ default: false,
620
+ persistToProfile: true,
621
+ completionOptions: [
622
+ {
623
+ value: 'true',
624
+ description: 'Load LLXPRT.md as system directives',
625
+ },
626
+ {
627
+ value: 'false',
628
+ description: 'Load LLXPRT.md as user context (default)',
629
+ },
630
+ ],
631
+ },
632
+ {
633
+ key: 'task-default-timeout-seconds',
634
+ category: 'cli-behavior',
635
+ description: 'Default timeout in seconds for task tool executions',
636
+ type: 'number',
637
+ persistToProfile: true,
638
+ validate: (value) => {
639
+ if (typeof value === 'number' && (value === -1 || value > 0)) {
640
+ return { success: true, value };
641
+ }
642
+ return {
643
+ success: false,
644
+ message: 'task-default-timeout-seconds must be a positive number in seconds or -1 for unlimited',
645
+ };
646
+ },
647
+ },
648
+ {
649
+ key: 'task-max-timeout-seconds',
650
+ category: 'cli-behavior',
651
+ description: 'Maximum allowed timeout in seconds for task tool executions',
652
+ type: 'number',
653
+ persistToProfile: true,
654
+ validate: (value) => {
655
+ if (typeof value === 'number' && (value === -1 || value > 0)) {
656
+ return { success: true, value };
657
+ }
658
+ return {
659
+ success: false,
660
+ message: 'task-max-timeout-seconds must be a positive number in seconds or -1 for unlimited',
661
+ };
662
+ },
663
+ },
664
+ {
665
+ // @plan PLAN-20260130-ASYNCTASK.P21
666
+ // @requirement REQ-ASYNC-012
667
+ key: 'task-max-async',
668
+ category: 'cli-behavior',
669
+ description: 'Maximum concurrent async tasks. Default 5, use -1 for unlimited.',
670
+ type: 'number',
671
+ persistToProfile: true,
672
+ validate: (value) => {
673
+ if (
674
+ // eslint-disable-next-line sonarjs/expression-complexity -- Existing structure is intentionally preserved; refactoring this boundary is outside the lint slice.
675
+ typeof value === 'number' &&
676
+ Number.isInteger(value) &&
677
+ (value === -1 || (value >= 1 && value <= 100))) {
678
+ return { success: true, value };
679
+ }
680
+ return {
681
+ success: false,
682
+ message: 'task-max-async must be -1 (unlimited) or an integer between 1 and 100',
683
+ };
684
+ },
685
+ },
686
+ {
687
+ key: 'subagents.async.enabled',
688
+ category: 'cli-behavior',
689
+ description: 'Enable async subagents for this profile.',
690
+ type: 'boolean',
691
+ default: true,
692
+ persistToProfile: true,
693
+ completionOptions: [
694
+ { value: 'true', description: 'Enable async subagents' },
695
+ { value: 'false', description: 'Disable async subagents' },
696
+ ],
697
+ },
698
+ {
699
+ key: 'shell-default-timeout-seconds',
700
+ category: 'cli-behavior',
701
+ description: 'Default timeout in seconds for shell command executions',
702
+ type: 'number',
703
+ persistToProfile: true,
704
+ validate: (value) => {
705
+ if (typeof value === 'number' && (value === -1 || value > 0)) {
706
+ return { success: true, value };
707
+ }
708
+ return {
709
+ success: false,
710
+ message: 'shell-default-timeout-seconds must be a positive number in seconds or -1 for unlimited',
711
+ };
712
+ },
713
+ },
714
+ {
715
+ key: 'shell-max-timeout-seconds',
716
+ category: 'cli-behavior',
717
+ description: 'Maximum allowed timeout in seconds for shell command executions',
718
+ type: 'number',
719
+ persistToProfile: true,
720
+ validate: (value) => {
721
+ if (typeof value === 'number' && (value === -1 || value > 0)) {
722
+ return { success: true, value };
723
+ }
724
+ return {
725
+ success: false,
726
+ message: 'shell-max-timeout-seconds must be a positive number in seconds or -1 for unlimited',
727
+ };
728
+ },
729
+ },
730
+ {
731
+ key: 'shell-inactivity-timeout-seconds',
732
+ category: 'cli-behavior',
733
+ description: 'Inactivity timeout in seconds for shell commands. Kills commands that produce no output for this duration. Resets on each output event.',
734
+ type: 'number',
735
+ persistToProfile: true,
736
+ validate: (value) => {
737
+ if (typeof value === 'number' && (value === -1 || value > 0)) {
738
+ return { success: true, value };
739
+ }
740
+ return {
741
+ success: false,
742
+ message: 'shell-inactivity-timeout-seconds must be a positive number in seconds or -1 for unlimited',
743
+ };
744
+ },
745
+ },
746
+ {
747
+ key: 'temperature',
748
+ category: 'model-param',
749
+ description: 'Sampling temperature',
750
+ type: 'number',
751
+ persistToProfile: true,
752
+ },
753
+ {
754
+ key: 'max_tokens',
755
+ aliases: ['max-tokens', 'maxTokens'],
756
+ category: 'model-param',
757
+ description: 'Maximum tokens to generate',
758
+ type: 'number',
759
+ persistToProfile: true,
760
+ },
761
+ {
762
+ key: 'max_output_tokens',
763
+ aliases: ['max-output-tokens'],
764
+ category: 'model-param',
765
+ description: 'Maximum output tokens (Gemini native param)',
766
+ type: 'number',
767
+ persistToProfile: true,
768
+ },
769
+ {
770
+ key: 'maxOutputTokens',
771
+ aliases: ['max-output'],
772
+ category: 'cli-behavior',
773
+ description: 'Maximum output tokens (generic, translated by provider)',
774
+ type: 'number',
775
+ persistToProfile: true,
776
+ },
777
+ {
778
+ key: 'top_p',
779
+ category: 'model-param',
780
+ description: 'Nucleus sampling',
781
+ type: 'number',
782
+ persistToProfile: true,
783
+ },
784
+ {
785
+ key: 'top_k',
786
+ category: 'model-param',
787
+ description: 'Top-k sampling',
788
+ type: 'number',
789
+ persistToProfile: true,
790
+ },
791
+ {
792
+ key: 'frequency_penalty',
793
+ category: 'model-param',
794
+ description: 'Frequency penalty',
795
+ type: 'number',
796
+ persistToProfile: true,
797
+ },
798
+ {
799
+ key: 'presence_penalty',
800
+ category: 'model-param',
801
+ description: 'Presence penalty',
802
+ type: 'number',
803
+ persistToProfile: true,
804
+ },
805
+ {
806
+ key: 'seed',
807
+ category: 'model-param',
808
+ providers: ['openai', 'openaivercel'],
809
+ description: 'Random seed for deterministic sampling (OpenAI only)',
810
+ type: 'number',
811
+ persistToProfile: true,
812
+ },
813
+ {
814
+ key: 'stop',
815
+ category: 'model-param',
816
+ description: 'Stop sequences',
817
+ type: 'string-array',
818
+ persistToProfile: true,
819
+ },
820
+ {
821
+ key: 'response_format',
822
+ aliases: ['response-format', 'responseFormat'],
823
+ category: 'model-param',
824
+ description: 'Response format (e.g., json_object)',
825
+ type: 'json',
826
+ persistToProfile: true,
827
+ },
828
+ {
829
+ key: 'logit_bias',
830
+ category: 'model-param',
831
+ description: 'Token bias',
832
+ type: 'json',
833
+ persistToProfile: true,
834
+ },
835
+ {
836
+ key: 'tool_choice',
837
+ aliases: ['tool-choice', 'toolChoice'],
838
+ category: 'model-param',
839
+ description: 'Tool choice strategy (auto/required/none)',
840
+ type: 'string',
841
+ persistToProfile: true,
842
+ },
843
+ {
844
+ key: 'reasoning',
845
+ category: 'model-param',
846
+ providers: ['openai', 'openaivercel', 'openai-responses'],
847
+ description: 'Reasoning configuration object (OpenAI)',
848
+ type: 'json',
849
+ persistToProfile: false,
850
+ normalize: (value) => {
851
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
852
+ return undefined;
853
+ }
854
+ const sanitized = {};
855
+ const INTERNAL_KEYS = new Set([
856
+ 'enabled',
857
+ 'includeInContext',
858
+ 'includeInResponse',
859
+ 'format',
860
+ 'stripFromContext',
861
+ ]);
862
+ for (const [k, v] of Object.entries(value)) {
863
+ if (v !== undefined && v !== null && !INTERNAL_KEYS.has(k)) {
864
+ sanitized[k] = v;
865
+ }
866
+ }
867
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
868
+ },
869
+ },
870
+ {
871
+ key: 'custom-headers',
872
+ category: 'custom-header',
873
+ description: 'Custom HTTP headers as JSON object',
874
+ type: 'json',
875
+ persistToProfile: true,
876
+ },
877
+ {
878
+ key: 'user-agent',
879
+ aliases: ['User-Agent'],
880
+ category: 'custom-header',
881
+ description: 'User-Agent header override',
882
+ type: 'string',
883
+ persistToProfile: true,
884
+ },
885
+ {
886
+ key: 'GOOGLE_CLOUD_PROJECT',
887
+ category: 'provider-config',
888
+ description: 'Google Cloud project ID',
889
+ type: 'string',
890
+ persistToProfile: true,
891
+ },
892
+ {
893
+ key: 'GOOGLE_CLOUD_LOCATION',
894
+ category: 'provider-config',
895
+ description: 'Google Cloud location/region',
896
+ type: 'string',
897
+ persistToProfile: true,
898
+ },
899
+ // Load balancer settings (Issue #489)
900
+ {
901
+ key: 'tpm_threshold',
902
+ category: 'cli-behavior',
903
+ description: 'Minimum tokens per minute before triggering failover (positive integer, load balancer only)',
904
+ type: 'number',
905
+ persistToProfile: true,
906
+ validate: (value) => {
907
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
908
+ return { success: true, value };
909
+ }
910
+ return {
911
+ success: false,
912
+ message: 'tpm_threshold must be a positive integer',
913
+ };
914
+ },
915
+ },
916
+ {
917
+ key: 'timeout_ms',
918
+ category: 'cli-behavior',
919
+ description: 'Maximum request duration in milliseconds before timeout (positive integer, load balancer only)',
920
+ type: 'number',
921
+ persistToProfile: true,
922
+ validate: (value) => {
923
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
924
+ return { success: true, value };
925
+ }
926
+ return {
927
+ success: false,
928
+ message: 'timeout_ms must be a positive integer',
929
+ };
930
+ },
931
+ },
932
+ {
933
+ key: 'circuit_breaker_enabled',
934
+ category: 'cli-behavior',
935
+ description: 'Enable circuit breaker pattern for failing backends (true/false, load balancer only)',
936
+ type: 'boolean',
937
+ persistToProfile: true,
938
+ validate: (value) => {
939
+ if (value === true || value === false) {
940
+ return { success: true, value };
941
+ }
942
+ return {
943
+ success: false,
944
+ message: `circuit_breaker_enabled must be either 'true' or 'false'`,
945
+ };
946
+ },
947
+ },
948
+ {
949
+ key: 'circuit_breaker_failure_threshold',
950
+ category: 'cli-behavior',
951
+ description: 'Number of failures before opening circuit (positive integer, default: 3, load balancer only)',
952
+ type: 'number',
953
+ persistToProfile: true,
954
+ validate: (value) => {
955
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
956
+ return { success: true, value };
957
+ }
958
+ return {
959
+ success: false,
960
+ message: 'circuit_breaker_failure_threshold must be a positive integer',
961
+ };
962
+ },
963
+ },
964
+ {
965
+ key: 'circuit_breaker_failure_window_ms',
966
+ category: 'cli-behavior',
967
+ description: 'Time window for counting failures in milliseconds (positive integer, default: 60000, load balancer only)',
968
+ type: 'number',
969
+ persistToProfile: true,
970
+ validate: (value) => {
971
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
972
+ return { success: true, value };
973
+ }
974
+ return {
975
+ success: false,
976
+ message: 'circuit_breaker_failure_window_ms must be a positive integer',
977
+ };
978
+ },
979
+ },
980
+ {
981
+ key: 'circuit_breaker_recovery_timeout_ms',
982
+ category: 'cli-behavior',
983
+ description: 'Cooldown period before retrying after circuit opens in milliseconds (positive integer, default: 30000, load balancer only)',
984
+ type: 'number',
985
+ persistToProfile: true,
986
+ validate: (value) => {
987
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
988
+ return { success: true, value };
989
+ }
990
+ return {
991
+ success: false,
992
+ message: 'circuit_breaker_recovery_timeout_ms must be a positive integer',
993
+ };
994
+ },
995
+ },
996
+ /** @plan PLAN-20260211-COMPRESSION.P12 */
997
+ {
998
+ key: 'compression.strategy',
999
+ category: 'cli-behavior',
1000
+ description: 'Compression strategy to use (middle-out or top-down-truncation)',
1001
+ type: 'enum',
1002
+ enumValues: [...COMPRESSION_STRATEGIES],
1003
+ default: 'middle-out',
1004
+ persistToProfile: true,
1005
+ },
1006
+ /** @plan PLAN-20260211-COMPRESSION.P12 */
1007
+ {
1008
+ key: 'compression.profile',
1009
+ category: 'cli-behavior',
1010
+ description: 'Profile name for compression LLM calls',
1011
+ type: 'string',
1012
+ persistToProfile: true,
1013
+ },
1014
+ /**
1015
+ * @plan PLAN-20260211-HIGHDENSITY.P15
1016
+ * @requirement REQ-HD-009.1, REQ-HD-009.2, REQ-HD-009.3, REQ-HD-009.4
1017
+ * @pseudocode settings-factory.md lines 14-51
1018
+ */
1019
+ {
1020
+ key: 'compression.density.readWritePruning',
1021
+ category: 'cli-behavior',
1022
+ description: 'Enable READ→WRITE pair pruning in high-density strategy',
1023
+ type: 'boolean',
1024
+ default: true,
1025
+ persistToProfile: true,
1026
+ },
1027
+ {
1028
+ key: 'compression.density.fileDedupe',
1029
+ category: 'cli-behavior',
1030
+ description: 'Enable duplicate @ file inclusion deduplication',
1031
+ type: 'boolean',
1032
+ default: true,
1033
+ persistToProfile: true,
1034
+ },
1035
+ {
1036
+ key: 'compression.density.recencyPruning',
1037
+ category: 'cli-behavior',
1038
+ description: 'Enable tool result recency pruning (keep last N per tool type)',
1039
+ type: 'boolean',
1040
+ default: false,
1041
+ persistToProfile: true,
1042
+ },
1043
+ {
1044
+ key: 'compression.density.recencyRetention',
1045
+ category: 'cli-behavior',
1046
+ description: 'Number of recent results to keep per tool type',
1047
+ type: 'number',
1048
+ default: 3,
1049
+ persistToProfile: true,
1050
+ validate: (value) => {
1051
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 1) {
1052
+ return { success: true, value };
1053
+ }
1054
+ return {
1055
+ success: false,
1056
+ message: 'compression.density.recencyRetention must be a positive integer (>= 1)',
1057
+ };
1058
+ },
1059
+ },
1060
+ {
1061
+ key: 'compression.density.compressHeadroom',
1062
+ category: 'cli-behavior',
1063
+ description: 'Headroom multiplier for compression target tokens (0 < value <= 1)',
1064
+ type: 'number',
1065
+ default: 0.6,
1066
+ persistToProfile: true,
1067
+ validate: (value) => {
1068
+ if (typeof value === 'number' && value > 0 && value <= 1) {
1069
+ return { success: true, value };
1070
+ }
1071
+ return {
1072
+ success: false,
1073
+ message: 'compression.density.compressHeadroom must be a number > 0 and <= 1',
1074
+ };
1075
+ },
1076
+ },
1077
+ {
1078
+ key: 'compression.density.optimizeThreshold',
1079
+ category: 'cli-behavior',
1080
+ description: 'Context usage threshold (0-1) for when density optimization runs. If not set, uses the compression strategy default (e.g., 0.9 for high-density).',
1081
+ type: 'number',
1082
+ default: undefined,
1083
+ persistToProfile: true,
1084
+ validate: (value) => {
1085
+ if (value === undefined || value === null) {
1086
+ return { success: true, value: undefined };
1087
+ }
1088
+ if (typeof value === 'number' && value <= 1) {
1089
+ return { success: true, value };
1090
+ }
1091
+ return {
1092
+ success: false,
1093
+ message: 'compression.density.optimizeThreshold must be a number >= 0 and <= 1',
1094
+ };
1095
+ },
1096
+ },
1097
+ {
1098
+ key: 'auth.noBrowser',
1099
+ category: 'cli-behavior',
1100
+ description: 'Skip automatic browser OAuth flow and use manual code entry',
1101
+ type: 'boolean',
1102
+ default: false,
1103
+ persistToProfile: true,
1104
+ completionOptions: [
1105
+ { value: 'true', description: 'Disable browser auto-launch for OAuth' },
1106
+ { value: 'false', description: 'Allow browser auto-launch for OAuth' },
1107
+ ],
1108
+ },
1109
+ {
1110
+ key: 'stream-idle-timeout-ms',
1111
+ category: 'cli-behavior',
1112
+ description: 'Stream idle timeout in milliseconds. Disabled by default (0). Set to a positive number of milliseconds to enable the watchdog.',
1113
+ type: 'number',
1114
+ persistToProfile: true,
1115
+ validate: (value) => {
1116
+ if (typeof value === 'number' && Number.isFinite(value)) {
1117
+ return { success: true, value };
1118
+ }
1119
+ return {
1120
+ success: false,
1121
+ message: 'stream-idle-timeout-ms must be a finite number (use 0 or negative to disable)',
1122
+ };
1123
+ },
1124
+ },
1125
+ ];
1126
+ function isPlainObject(value) {
1127
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1128
+ }
1129
+ export function resolveAlias(key) {
1130
+ const normalizedAlias = ALIAS_NORMALIZATION_RULES[key];
1131
+ if (typeof normalizedAlias === 'string') {
1132
+ return normalizedAlias;
1133
+ }
1134
+ for (const spec of SETTINGS_REGISTRY) {
1135
+ if (spec.aliases?.includes(key) === true) {
1136
+ return spec.key;
1137
+ }
1138
+ }
1139
+ for (const spec of SETTINGS_REGISTRY) {
1140
+ if (spec.key === key) {
1141
+ return key;
1142
+ }
1143
+ }
1144
+ const lowerKey = key.toLowerCase();
1145
+ if (HEADER_PRESERVE_SET.has(lowerKey)) {
1146
+ return key;
1147
+ }
1148
+ return key.replace(/-/g, '_');
1149
+ }
1150
+ export function getSettingSpec(key) {
1151
+ // Direct canonical-key match first (fast path)
1152
+ const direct = SETTINGS_REGISTRY.find((s) => s.key === key);
1153
+ if (direct) {
1154
+ return direct;
1155
+ }
1156
+ // Resolve alias to canonical key and look up the spec
1157
+ const resolved = resolveAlias(key);
1158
+ if (resolved !== key) {
1159
+ return SETTINGS_REGISTRY.find((s) => s.key === resolved);
1160
+ }
1161
+ return undefined;
1162
+ }
1163
+ export function normalizeSetting(key, value) {
1164
+ const resolvedKey = resolveAlias(key);
1165
+ const spec = SETTINGS_REGISTRY.find((s) => s.key === resolvedKey);
1166
+ if (spec?.normalize) {
1167
+ return spec.normalize(value);
1168
+ }
1169
+ // Reasoning spec already has normalize, but keep fallback for safety
1170
+ // until all reasoning normalization is verified through spec.normalize
1171
+ return value;
1172
+ }
1173
+ const INTERNAL_SETTINGS_KEYS = new Set([
1174
+ 'activeProvider',
1175
+ 'currentProfile',
1176
+ 'tools',
1177
+ ]);
1178
+ /** Extract custom headers from both global and provider-level settings. */
1179
+ function extractCustomHeaders(mixed, providerOverrides) {
1180
+ const customHeaders = {};
1181
+ if (isPlainObject(mixed['custom-headers'])) {
1182
+ const globalHeaders = mixed['custom-headers'];
1183
+ for (const [headerName, headerValue] of Object.entries(globalHeaders)) {
1184
+ if (typeof headerValue === 'string') {
1185
+ customHeaders[headerName] = headerValue;
1186
+ }
1187
+ }
1188
+ }
1189
+ if (isPlainObject(providerOverrides['custom-headers'])) {
1190
+ const providerHeaders = providerOverrides['custom-headers'];
1191
+ for (const [headerName, headerValue] of Object.entries(providerHeaders)) {
1192
+ if (typeof headerValue === 'string') {
1193
+ customHeaders[headerName] = headerValue;
1194
+ }
1195
+ }
1196
+ }
1197
+ return customHeaders;
1198
+ }
1199
+ /** Flatten provider overrides and reasoning sub-keys into a merged settings object. */
1200
+ function mergeProviderSettings(mixed, providerOverrides) {
1201
+ const merged = { ...mixed, ...providerOverrides };
1202
+ if (isPlainObject(merged['reasoning'])) {
1203
+ const reasoningObj = merged['reasoning'];
1204
+ for (const [subKey, subValue] of Object.entries(reasoningObj)) {
1205
+ const fullKey = `reasoning.${subKey}`;
1206
+ if (!(fullKey in merged)) {
1207
+ merged[fullKey] = subValue;
1208
+ }
1209
+ }
1210
+ }
1211
+ return merged;
1212
+ }
1213
+ /** Categorize a single setting entry into the appropriate bucket. */
1214
+ function categorizeSettingEntry(rawKey, value, providerName, buckets) {
1215
+ if (value === undefined || value === null)
1216
+ return;
1217
+ if (typeof value === 'object' &&
1218
+ !Array.isArray(value) &&
1219
+ rawKey === providerName) {
1220
+ return;
1221
+ }
1222
+ if (rawKey === 'custom-headers') {
1223
+ return;
1224
+ }
1225
+ if (INTERNAL_SETTINGS_KEYS.has(rawKey)) {
1226
+ buckets.cliSettings[rawKey] = value;
1227
+ return;
1228
+ }
1229
+ const resolvedKey = resolveAlias(rawKey);
1230
+ const normalizedValue = normalizeSetting(resolvedKey, value);
1231
+ if (normalizedValue === undefined)
1232
+ return;
1233
+ const spec = getSettingSpec(resolvedKey);
1234
+ if (!spec) {
1235
+ // Unknown settings default to model-param (pass-through to API).
1236
+ buckets.modelParams[resolvedKey] = normalizedValue;
1237
+ return;
1238
+ }
1239
+ if (spec.category === 'model-param' &&
1240
+ spec.providers &&
1241
+ providerName &&
1242
+ !spec.providers.includes(providerName)) {
1243
+ return;
1244
+ }
1245
+ switch (spec.category) {
1246
+ case 'provider-config':
1247
+ break;
1248
+ case 'cli-behavior':
1249
+ buckets.cliSettings[resolvedKey] = normalizedValue;
1250
+ break;
1251
+ case 'model-behavior':
1252
+ buckets.modelBehavior[resolvedKey] = normalizedValue;
1253
+ break;
1254
+ case 'model-param':
1255
+ buckets.modelParams[resolvedKey] = normalizedValue;
1256
+ break;
1257
+ case 'custom-header':
1258
+ if (typeof normalizedValue === 'string') {
1259
+ buckets.customHeaders[resolvedKey] = normalizedValue;
1260
+ }
1261
+ break;
1262
+ default:
1263
+ break;
1264
+ }
1265
+ }
1266
+ export function separateSettings(mixed, providerName) {
1267
+ const cliSettings = {};
1268
+ const modelBehavior = {};
1269
+ const modelParams = {};
1270
+ let providerOverrides = {};
1271
+ if (providerName && isPlainObject(mixed[providerName])) {
1272
+ providerOverrides = mixed[providerName];
1273
+ }
1274
+ const customHeaders = extractCustomHeaders(mixed, providerOverrides);
1275
+ const mergedProviderSettings = mergeProviderSettings(mixed, providerOverrides);
1276
+ const buckets = { cliSettings, modelBehavior, modelParams, customHeaders };
1277
+ for (const [rawKey, value] of Object.entries(mergedProviderSettings)) {
1278
+ categorizeSettingEntry(rawKey, value, providerName, buckets);
1279
+ }
1280
+ return { cliSettings, modelBehavior, modelParams, customHeaders };
1281
+ }
1282
+ export function validateSetting(key, value) {
1283
+ const resolved = resolveAlias(key);
1284
+ const spec = getSettingSpec(resolved);
1285
+ if (!spec) {
1286
+ // Unknown settings are allowed — they pass through as model-params
1287
+ return { success: true, value };
1288
+ }
1289
+ if (spec.validate) {
1290
+ return spec.validate(value);
1291
+ }
1292
+ // Auto-validate enum types if no custom validator
1293
+ if (spec.type === 'enum' && spec.enumValues) {
1294
+ const strValue = typeof value === 'string' ? value.toLowerCase() : value;
1295
+ if (typeof strValue !== 'string' || !spec.enumValues.includes(strValue)) {
1296
+ return {
1297
+ success: false,
1298
+ message: `${spec.key} must be one of: ${spec.enumValues.join(', ')}`,
1299
+ };
1300
+ }
1301
+ return { success: true, value: strValue };
1302
+ }
1303
+ // Auto-validate boolean types
1304
+ if (spec.type === 'boolean') {
1305
+ if (typeof value !== 'boolean') {
1306
+ return {
1307
+ success: false,
1308
+ message: `${spec.key} must be either 'true' or 'false'`,
1309
+ };
1310
+ }
1311
+ return { success: true, value };
1312
+ }
1313
+ // Auto-validate number types
1314
+ if (spec.type === 'number') {
1315
+ if (typeof value !== 'number' || Number.isNaN(value)) {
1316
+ return {
1317
+ success: false,
1318
+ message: `${spec.key} must be a number`,
1319
+ };
1320
+ }
1321
+ return { success: true, value };
1322
+ }
1323
+ return { success: true, value };
1324
+ }
1325
+ export function parseSetting(key, raw) {
1326
+ const resolved = resolveAlias(key);
1327
+ const spec = getSettingSpec(resolved);
1328
+ // If spec has a custom parser, use it
1329
+ if (spec?.parse) {
1330
+ return spec.parse(raw);
1331
+ }
1332
+ // Only apply type coercion when spec explicitly indicates the type
1333
+ // This prevents converting enum/string values like "true" to boolean true
1334
+ if (spec?.type === 'number') {
1335
+ const num = Number(raw);
1336
+ if (!Number.isNaN(num)) {
1337
+ return num;
1338
+ }
1339
+ }
1340
+ if (spec?.type === 'boolean') {
1341
+ if (raw.toLowerCase() === 'true') {
1342
+ return true;
1343
+ }
1344
+ if (raw.toLowerCase() === 'false') {
1345
+ return false;
1346
+ }
1347
+ }
1348
+ // For unknown settings (no spec) or string/enum types, try JSON parse or return raw
1349
+ try {
1350
+ return JSON.parse(raw);
1351
+ }
1352
+ catch {
1353
+ return raw;
1354
+ }
1355
+ }
1356
+ export function getProfilePersistableKeys() {
1357
+ return SETTINGS_REGISTRY.filter((s) => s.persistToProfile).map((s) => s.key);
1358
+ }
1359
+ export function getSettingHelp() {
1360
+ const help = {};
1361
+ for (const spec of SETTINGS_REGISTRY) {
1362
+ help[spec.key] = spec.description;
1363
+ }
1364
+ return help;
1365
+ }
1366
+ export function getCompletionOptions() {
1367
+ return SETTINGS_REGISTRY.filter((s) => s.completionOptions ?? s.enumValues).map((s) => ({
1368
+ key: s.key,
1369
+ options: s.completionOptions ?? s.enumValues?.map((v) => ({ value: v })),
1370
+ }));
1371
+ }
1372
+ export function getAllSettingKeys() {
1373
+ return SETTINGS_REGISTRY.map((s) => s.key);
1374
+ }
1375
+ export function getValidationHelp(key) {
1376
+ const resolved = resolveAlias(key);
1377
+ const spec = getSettingSpec(resolved);
1378
+ if (!spec) {
1379
+ return undefined;
1380
+ }
1381
+ let help = spec.description;
1382
+ if (spec.hint) {
1383
+ help += ` (${spec.hint})`;
1384
+ }
1385
+ if (spec.enumValues) {
1386
+ help += ` Valid values: ${spec.enumValues.join(', ')}`;
1387
+ }
1388
+ return help;
1389
+ }
1390
+ export function getAutocompleteSuggestions(key) {
1391
+ const resolved = resolveAlias(key);
1392
+ const spec = getSettingSpec(resolved);
1393
+ if (!spec) {
1394
+ return undefined;
1395
+ }
1396
+ if (spec.completionOptions) {
1397
+ return spec.completionOptions;
1398
+ }
1399
+ if (spec.enumValues) {
1400
+ return spec.enumValues.map((v) => ({ value: v }));
1401
+ }
1402
+ return undefined;
1403
+ }
1404
+ function collectProviderConfigKeys() {
1405
+ const keys = [];
1406
+ for (const spec of SETTINGS_REGISTRY) {
1407
+ if (spec.category === 'provider-config') {
1408
+ keys.push(spec.key);
1409
+ if (spec.aliases) {
1410
+ keys.push(...spec.aliases);
1411
+ }
1412
+ }
1413
+ }
1414
+ return keys;
1415
+ }
1416
+ export function getProtectedSettingKeys() {
1417
+ const keys = collectProviderConfigKeys();
1418
+ keys.push('provider', 'currentProfile');
1419
+ return keys;
1420
+ }
1421
+ export function getProviderConfigKeys() {
1422
+ return collectProviderConfigKeys();
1423
+ }
1424
+ function deriveHintFromSpec(spec) {
1425
+ if (spec.hint) {
1426
+ return spec.hint;
1427
+ }
1428
+ if (spec.type === 'boolean') {
1429
+ return 'true or false';
1430
+ }
1431
+ if (spec.type === 'number') {
1432
+ return 'number';
1433
+ }
1434
+ if (spec.type === 'enum' && spec.enumValues) {
1435
+ return spec.enumValues.join(', ');
1436
+ }
1437
+ if (spec.type === 'json') {
1438
+ return 'JSON object';
1439
+ }
1440
+ if (spec.type === 'string-array') {
1441
+ return 'comma-separated list';
1442
+ }
1443
+ return 'value';
1444
+ }
1445
+ /* eslint-enable max-lines -- Phase 5: legacy core boundary retained while larger decomposition continues. */
1446
+ export function getDirectSettingSpecs() {
1447
+ const specs = [];
1448
+ for (const spec of SETTINGS_REGISTRY) {
1449
+ if (spec.category === 'model-param' ||
1450
+ spec.category === 'custom-header' ||
1451
+ spec.category === 'provider-config') {
1452
+ continue;
1453
+ }
1454
+ const hint = deriveHintFromSpec(spec);
1455
+ const options = spec.completionOptions ??
1456
+ spec.enumValues?.map((v) => ({ value: v })) ??
1457
+ (spec.type === 'boolean'
1458
+ ? [{ value: 'true' }, { value: 'false' }]
1459
+ : undefined);
1460
+ specs.push({
1461
+ value: spec.key,
1462
+ hint,
1463
+ description: spec.description,
1464
+ options,
1465
+ });
1466
+ }
1467
+ return specs;
1468
+ }
1469
+ //# sourceMappingURL=settingsRegistry.js.map