@gaunt-sloth/core 2.0.0-alpha.3 → 2.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -7,662 +7,20 @@
7
7
  * Refer to {@link DEFAULT_CONFIG} for default configuration.
8
8
  *
9
9
  * Some config params can be overriden from command line, see {@link CommandLineConfigOverrides}
10
- */
11
- import { PROJECT_GUIDELINES, PROJECT_REVIEW_INSTRUCTIONS, USER_PROJECT_CONFIG_JS, USER_PROJECT_CONFIG_JSON, USER_PROJECT_CONFIG_MJS, } from '#src/constants.js';
12
- import { StatusLevel } from '#src/core/types.js';
13
- import { displayDebug, displayError, displayInfo, displayWarning, setConsoleLevel, } from '#src/utils/consoleUtils.js';
14
- import { getGslothConfigReadPath, importExternalFile } from '#src/utils/fileUtils.js';
15
- import { getGlobalGslothConfigReadPath } from '#src/utils/globalConfigUtils.js';
16
- import { error, exit, isTTY, setUseColour } from '#src/utils/systemUtils.js';
17
- import { existsSync, readFileSync } from 'node:fs';
18
- /**
19
- * Default per-command shell timeout (ms) when {@link GthDevToolsConfig.shell}
20
- * does not specify one. ~120s suits typical build/test/git steps without
21
- * hanging the agent forever on a stuck command.
22
- */
23
- export const SHELL_DEFAULT_TIMEOUT_MS = 120_000;
24
- /**
25
- * Default byte budget for shell output captured into the ToolMessage returned to
26
- * the model (head + tail window). ~100KB keeps a noisy log from blowing the
27
- * context window; the full output is spilled to a temp file when this is exceeded.
28
- */
29
- export const SHELL_DEFAULT_MAX_OUTPUT_BYTES = 100_000;
30
- /**
31
- * Normalize the {@link GthDevToolsConfig.shell} opt-in (bare boolean or
32
- * `{ enabled }`) to a plain boolean. Centralized so the toolkit (tool emission)
33
- * and the deep agent (interrupt wiring) agree on what "shell enabled" means.
34
- *
35
- * EXT-12 — default-resolution: an EXPLICIT value always wins (a bare boolean, or the
36
- * object form's `enabled`), so `shell: false` / `{ enabled: false }` remains a hard
37
- * escape hatch that fully disables the tool. Only when `shell` is ABSENT/undefined does
38
- * the per-mode default apply: in `code` mode the shell tool is ON by default (still
39
- * gated — the per-command approval interrupt is wired separately and is NOT bypassed by
40
- * this), and OFF everywhere else (`exec`, `ask --write`, …) to preserve prior behaviour.
41
- * The default is `code`-mode only because `code` is the interactive agentic-coding surface
42
- * where a TTY can answer the approval prompt; the absent-config default never implies yolo.
43
10
  *
44
- * @param command The active command, so the absent-config default can be scoped to `code`.
45
- * Omit (or pass a non-`code` command) to keep the historical OFF-by-default behaviour.
46
- */
47
- export function isShellToolEnabled(devTools, command) {
48
- const shell = devTools?.shell;
49
- if (typeof shell === 'boolean')
50
- return shell;
51
- if (shell && typeof shell === 'object')
52
- return shell.enabled === true;
53
- // Absent/undefined shell: ON by default for `code` mode (gated), OFF elsewhere.
54
- return command === 'code';
55
- }
56
- /**
57
- * Resolve the per-command shell timeout (ms) from config, falling back to
58
- * {@link SHELL_DEFAULT_TIMEOUT_MS}. Only the object form can override it; a bare
59
- * `shell: true` uses the default. Non-positive / non-finite values are ignored.
60
- */
61
- export function getShellTimeoutMs(devTools) {
62
- const shell = devTools?.shell;
63
- if (shell && typeof shell === 'object' && typeof shell.timeout === 'number') {
64
- if (Number.isFinite(shell.timeout) && shell.timeout > 0)
65
- return shell.timeout;
66
- }
67
- return SHELL_DEFAULT_TIMEOUT_MS;
68
- }
69
- /**
70
- * Resolve the captured-output byte budget from config, falling back to
71
- * {@link SHELL_DEFAULT_MAX_OUTPUT_BYTES}. Only the object form can override it.
72
- * Non-positive / non-finite values are ignored.
73
- */
74
- export function getShellMaxOutputBytes(devTools) {
75
- const shell = devTools?.shell;
76
- if (shell && typeof shell === 'object' && typeof shell.maxOutputBytes === 'number') {
77
- if (Number.isFinite(shell.maxOutputBytes) && shell.maxOutputBytes > 0) {
78
- return shell.maxOutputBytes;
79
- }
80
- }
81
- return SHELL_DEFAULT_MAX_OUTPUT_BYTES;
82
- }
83
- /**
84
- * Whether the EXT-9 Tier-2 scoped allow-list is active. Default `true`; only the object
85
- * form's `allowlist: false` disables it (a bare `shell: true` keeps it on). When off, the
86
- * runner prompts for every `run_shell_command` regardless of prior approvals.
87
- */
88
- export function isShellAllowlistEnabled(devTools) {
89
- const shell = devTools?.shell;
90
- if (shell && typeof shell === 'object' && shell.allowlist === false)
91
- return false;
92
- return true;
93
- }
94
- /**
95
- * Whether `always`-scoped approvals are persisted to the project allow-list file. Default
96
- * `true`; only the object form's `persistAllowlist: false` disables persistence (an
97
- * `always` decision then behaves as `session`).
98
- */
99
- export function isShellAllowlistPersisted(devTools) {
100
- const shell = devTools?.shell;
101
- if (shell && typeof shell === 'object' && shell.persistAllowlist === false)
102
- return false;
103
- return true;
104
- }
105
- /**
106
- * Whether the EXT-10 LLM-as-judge safety gate is enabled for the given dev-tools config.
107
- * Default OFF (only the object form's `judge` truthy enables it), mirroring
108
- * {@link isShellToolEnabled}. A bare `shell: true` keeps the judge OFF — it costs an LLM call
109
- * per command and must be opted into explicitly.
110
- */
111
- export function isShellJudgeEnabled(devTools) {
112
- const shell = devTools?.shell;
113
- if (!shell || typeof shell !== 'object')
114
- return false;
115
- const judge = shell.judge;
116
- if (typeof judge === 'boolean')
117
- return judge;
118
- if (judge && typeof judge === 'object')
119
- return judge.enabled === true;
120
- return false;
121
- }
122
- /**
123
- * Resolve the EXT-10 judge gate settings from a dev-tools config, applying safe defaults
124
- * (auto-approve low, do NOT block high). `enabled` reflects {@link isShellJudgeEnabled}.
125
- */
126
- export function getShellJudgeSettings(devTools) {
127
- const enabled = isShellJudgeEnabled(devTools);
128
- const shell = devTools?.shell;
129
- const judge = shell && typeof shell === 'object' && shell.judge && typeof shell.judge === 'object'
130
- ? shell.judge
131
- : undefined;
132
- return {
133
- enabled,
134
- autoApproveLow: judge?.autoApproveLow ?? true,
135
- blockHigh: judge?.blockHigh ?? false,
136
- model: judge?.model,
137
- };
138
- }
139
- /**
140
- * Resolve the {@link GthDevToolsConfig} that applies to the active command, mirroring the
141
- * per-command selection in `builtInToolsConfig.getDefaultTools` (which is what actually emits
142
- * the dev tools) and `GthDeepAgent.getEffectiveDevToolsConfig`: `exec` → `commands.exec`,
143
- * `ask --write` → `commands.ask`, `code` → `commands.code`; `undefined` elsewhere (the
144
- * toolkit is inert there). Shared in core so the runner's allow-list gate stays in lockstep
145
- * with where the shell tool is actually emitted.
146
- */
147
- export function getEffectiveDevToolsConfig(config, command) {
148
- if (!config)
149
- return undefined;
150
- const askWrite = command === 'ask' && config.askWriteMode === true;
151
- if (command === 'exec')
152
- return config.commands?.exec?.devTools;
153
- if (askWrite)
154
- return config.commands?.ask?.devTools;
155
- if (command === 'code')
156
- return config.commands?.code?.devTools;
157
- return undefined;
158
- }
159
- export const availableDefaultConfigs = [
160
- 'vertexai',
161
- 'anthropic',
162
- 'groq',
163
- 'deepseek',
164
- 'openai',
165
- 'google-genai',
166
- 'xai',
167
- 'openrouter',
168
- 'ollama',
169
- ];
170
- /**
171
- * Default config
172
- */
173
- export const DEFAULT_CONFIG = {
174
- contentSource: 'file',
175
- requirementSource: 'file',
176
- contentProvider: 'file',
177
- requirementsProvider: 'file',
178
- /**
179
- * Path to project-specific guidelines.
180
- * The default is `.gsloth.guidelines.md`; this config may be used to point Gaunt Sloth to a different file,
181
- * for example, to AGENTS.md
182
- */
183
- projectGuidelines: PROJECT_GUIDELINES,
184
- /**
185
- * Whether to include the current date in the project review instructions or not.
186
- */
187
- includeCurrentDateAfterGuidelines: false,
188
- projectReviewInstructions: PROJECT_REVIEW_INSTRUCTIONS,
189
- filesystem: 'none',
190
- debugLog: false,
191
- consoleLevel: StatusLevel.INFO, // Default to INFO level, not debug
192
- /**
193
- * Default provider for both requirements and content is GitHub.
194
- * It needs GitHub CLI (gh).
195
- *
196
- * `github` content provider uses `gh pr diff NN` internally. {@link src/providers/ghPrDiffProvider.ts!}
197
- *
198
- *
199
- * `github` requirements provider `gh issue view NN` internally
200
- */
201
- commands: {
202
- pr: {
203
- contentSource: 'github',
204
- requirementSource: 'github',
205
- contentProvider: 'github',
206
- requirementsProvider: 'github',
207
- rating: {
208
- enabled: true,
209
- passThreshold: 6,
210
- minRating: 0,
211
- maxRating: 10,
212
- errorOnReviewFail: true,
213
- },
214
- },
215
- review: {
216
- rating: {
217
- enabled: true,
218
- passThreshold: 6,
219
- minRating: 0,
220
- maxRating: 10,
221
- errorOnReviewFail: true,
222
- },
223
- },
224
- ask: {
225
- filesystem: 'read',
226
- },
227
- chat: {
228
- filesystem: 'read',
229
- },
230
- code: {
231
- filesystem: 'all',
232
- },
233
- exec: {
234
- filesystem: 'all',
235
- },
236
- api: {
237
- filesystem: 'read',
238
- port: 3000,
239
- cors: {
240
- allowOrigin: 'http://localhost:3000',
241
- allowMethods: 'POST, GET, OPTIONS',
242
- allowHeaders: 'Content-Type, Accept',
243
- },
244
- },
245
- },
246
- streamOutput: true,
247
- writeOutputToFile: true,
248
- writeBinaryOutputsToFile: true,
249
- useColour: true,
250
- streamSessionInferenceLog: true,
251
- canInterruptInferenceWithEsc: true,
252
- aiignore: {
253
- enabled: true,
254
- patterns: undefined,
255
- },
256
- };
257
- /**
258
- * Needed DEFAULT_CONFIG to be plain const to be picked up by typedoc,
259
- * this cast here is just for typecheck.
260
- */
261
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
262
- DEFAULT_CONFIG;
263
- /**
264
- * Loads the global gsloth config (if present) from the global `~/.gsloth` folder.
265
- *
266
- * Precedence support: the returned raw config is intended to act as the BASE that the
267
- * project config (and CLI overrides) merge on top of, so any value here is the lowest
268
- * user-controlled layer (still above {@link DEFAULT_CONFIG}).
269
- *
270
- * Lookup order within the global folder, first match wins:
271
- * `.gsloth.config.json` -> `.gsloth.config.js` -> `.gsloth.config.mjs`
272
- *
273
- * Absence of every variant is a no-op: returns `undefined` so behaviour is unchanged.
274
- *
275
- * NOTE: secrets (API keys) may live in this file; this function must never log its
276
- * contents. Only non-sensitive diagnostics (the resolved path / parse failure) are emitted.
277
- *
278
- * @returns The raw global config object, or `undefined` when no global config exists.
279
- */
280
- export async function loadGlobalRawConfig() {
281
- // JSON first (the must-have format).
282
- const jsonPath = getGlobalGslothConfigReadPath(USER_PROJECT_CONFIG_JSON);
283
- if (existsSync(jsonPath)) {
284
- try {
285
- return JSON.parse(readFileSync(jsonPath, 'utf8'));
286
- }
287
- catch (e) {
288
- displayDebug(e instanceof Error ? e : String(e));
289
- displayWarning(`Failed to read global config from ${jsonPath}, ignoring it.`);
290
- return undefined;
291
- }
292
- }
293
- // Then JS / MJS variants (dynamic import of a `configure()` module).
294
- for (const filename of [USER_PROJECT_CONFIG_JS, USER_PROJECT_CONFIG_MJS]) {
295
- const modulePath = getGlobalGslothConfigReadPath(filename);
296
- if (existsSync(modulePath)) {
297
- try {
298
- const imported = await importExternalFile(modulePath);
299
- const configured = await imported.configure();
300
- return configured;
301
- }
302
- catch (e) {
303
- displayDebug(e instanceof Error ? e : String(e));
304
- displayWarning(`Failed to read global config from ${modulePath}, ignoring it.`);
305
- return undefined;
306
- }
307
- }
308
- }
309
- return undefined;
310
- }
311
- /**
312
- * Deep-merges a loaded global raw config UNDER the given project raw config, so the
313
- * project config wins on conflicting keys. When no global config exists this is a no-op
314
- * and the original project config is returned unchanged.
315
- */
316
- async function applyGlobalConfigBase(projectRawConfig) {
317
- const globalRawConfig = await loadGlobalRawConfig();
318
- if (!globalRawConfig) {
319
- return projectRawConfig;
320
- }
321
- return deepMerge(globalRawConfig, projectRawConfig);
322
- }
323
- /**
324
- * Returns true when a project-level config file (json/js/mjs) exists for the given
325
- * overrides. Honours `customConfigPath` and the active identity profile so the check
326
- * matches exactly what {@link initConfig} would attempt to load.
327
- *
328
- * This is the project half of CFG-10's "is any config present?" detection; the global
329
- * half is {@link loadGlobalRawConfig} (used by {@link hasAnyConfig}).
330
- */
331
- export function hasProjectConfig(commandLineConfigOverrides) {
332
- if (commandLineConfigOverrides.customConfigPath) {
333
- return existsSync(commandLineConfigOverrides.customConfigPath);
334
- }
335
- return [USER_PROJECT_CONFIG_JSON, USER_PROJECT_CONFIG_JS, USER_PROJECT_CONFIG_MJS].some((filename) => existsSync(getGslothConfigReadPath(filename, commandLineConfigOverrides.identityProfile)));
336
- }
337
- /**
338
- * CFG-10 — true when ANY usable configuration is present, either a project config file
339
- * (json/js/mjs) or a standalone global config (`~/.gsloth/.gsloth.config.*`). When this
340
- * returns false the caller should run the first-run dialog instead of erroring.
341
- *
342
- * Reuses CFG-8's project + global detection so the two paths can never disagree.
343
- */
344
- export async function hasAnyConfig(commandLineConfigOverrides) {
345
- if (hasProjectConfig(commandLineConfigOverrides)) {
346
- return true;
347
- }
348
- return (await loadGlobalRawConfig()) !== undefined;
349
- }
350
- /**
351
- * Initialize configuration by loading from available config files
352
- * @returns The loaded GthConfig
353
- */
354
- export async function initConfig(commandLineConfigOverrides) {
355
- if (commandLineConfigOverrides.customConfigPath &&
356
- !existsSync(commandLineConfigOverrides.customConfigPath)) {
357
- throw new Error(`Provided manual config "${commandLineConfigOverrides.customConfigPath}" does not exist`);
358
- }
359
- const jsonConfigPath = commandLineConfigOverrides.customConfigPath ??
360
- getGslothConfigReadPath(USER_PROJECT_CONFIG_JSON, commandLineConfigOverrides.identityProfile);
361
- // CFG-8 — when no project config file of any format exists, fall back to a standalone
362
- // global config (loaded alone) before erroring. Project config still takes precedence:
363
- // this branch only runs when there is no project file to apply the global config under.
364
- if (!hasProjectConfig(commandLineConfigOverrides)) {
365
- const globalRawConfig = await loadGlobalRawConfig();
366
- if (globalRawConfig) {
367
- if (globalRawConfig.llm &&
368
- typeof globalRawConfig.llm === 'object' &&
369
- 'type' in globalRawConfig.llm) {
370
- // Route the global config through the same path the project JSON uses.
371
- return await tryJsonConfig(globalRawConfig, commandLineConfigOverrides);
372
- }
373
- displayError('Global configuration found but it is not in valid format. Should at least define llm.type');
374
- exit(1);
375
- // Unreachable past exit(1) in production; keeps TS happy and prevents test exit.
376
- throw new Error('Unexpected error occurred.');
377
- }
378
- }
379
- // Try loading the JSON config file first
380
- if (jsonConfigPath.endsWith('.json') && existsSync(jsonConfigPath)) {
381
- try {
382
- // TODO makes sense to employ ZOD to validate config
383
- const projectJsonConfig = JSON.parse(readFileSync(jsonConfigPath, 'utf8'));
384
- // Apply global config as the base layer (project config wins on conflicts).
385
- const jsonConfig = (await applyGlobalConfigBase(projectJsonConfig));
386
- // If the config has an LLM with a type, create the appropriate LLM instance
387
- if (jsonConfig.llm && typeof jsonConfig.llm === 'object' && 'type' in jsonConfig.llm) {
388
- return await tryJsonConfig(jsonConfig, commandLineConfigOverrides);
389
- }
390
- else {
391
- error(`${jsonConfigPath} is not in valid format. Should at least define llm.type`);
392
- exit(1);
393
- // noinspection ExceptionCaughtLocallyJS
394
- // This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
395
- // noinspection ExceptionCaughtLocallyJS
396
- throw new Error('Unexpected error occurred.');
397
- }
398
- }
399
- catch (e) {
400
- displayDebug(e instanceof Error ? e : String(e));
401
- displayError(`Failed to read config from ${USER_PROJECT_CONFIG_JSON}, will try other formats.`);
402
- // Continue to try other formats
403
- return await tryJsConfig(commandLineConfigOverrides);
404
- }
405
- }
406
- else {
407
- // JSON config not found, try JS
408
- return tryJsConfig(commandLineConfigOverrides);
409
- }
410
- }
411
- // Helper function to try loading JS config
412
- async function tryJsConfig(commandLineConfigOverrides) {
413
- const jsConfigPath = commandLineConfigOverrides.customConfigPath ??
414
- getGslothConfigReadPath(USER_PROJECT_CONFIG_JS, commandLineConfigOverrides.identityProfile);
415
- if (jsConfigPath.endsWith('.js') && existsSync(jsConfigPath)) {
416
- try {
417
- const i = await importExternalFile(jsConfigPath);
418
- const customConfig = await i.configure();
419
- const mergedWithGlobal = await applyGlobalConfigBase(customConfig);
420
- return await mergeConfig(mergedWithGlobal, commandLineConfigOverrides);
421
- }
422
- catch (e) {
423
- displayDebug(e instanceof Error ? e : String(e));
424
- displayError(`Failed to read config from ${USER_PROJECT_CONFIG_JS}, will try other formats.`);
425
- // Continue to try other formats
426
- return await tryMjsConfig(commandLineConfigOverrides);
427
- }
428
- }
429
- else {
430
- // JS config not found, try MJS
431
- return await tryMjsConfig(commandLineConfigOverrides);
432
- }
433
- }
434
- // Helper function to try loading MJS config
435
- async function tryMjsConfig(commandLineConfigOverrides) {
436
- const mjsConfigPath = commandLineConfigOverrides.customConfigPath ??
437
- getGslothConfigReadPath(USER_PROJECT_CONFIG_MJS, commandLineConfigOverrides.identityProfile);
438
- if (mjsConfigPath.endsWith('.mjs') && existsSync(mjsConfigPath)) {
439
- try {
440
- const i = await importExternalFile(mjsConfigPath);
441
- const customConfig = await i.configure();
442
- const mergedWithGlobal = await applyGlobalConfigBase(customConfig);
443
- return await mergeConfig(mergedWithGlobal, commandLineConfigOverrides);
444
- }
445
- catch (e) {
446
- displayDebug(e instanceof Error ? e : String(e));
447
- displayError(`Failed to read config from ${USER_PROJECT_CONFIG_MJS}.`);
448
- displayError(`No valid configuration found. Please create a valid configuration file.`);
449
- exit(1);
450
- }
451
- }
452
- else {
453
- // No config files found
454
- displayError('No configuration file found. Please create one of: ' +
455
- `${USER_PROJECT_CONFIG_JSON}, ${USER_PROJECT_CONFIG_JS}, or ${USER_PROJECT_CONFIG_MJS} ` +
456
- 'in your project directory.');
457
- exit(1);
458
- }
459
- // This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
460
- throw new Error('Unexpected error occurred.');
461
- }
462
- /**
463
- * Process JSON LLM config by creating the appropriate LLM instance
464
- * @param jsonConfig - The parsed JSON config
465
- * @param commandLineConfigOverrides - command line config overrides
466
- * @returns Promise<GthConfig>
467
- */
468
- export async function tryJsonConfig(jsonConfig, commandLineConfigOverrides) {
469
- try {
470
- if (jsonConfig.llm && typeof jsonConfig.llm === 'object') {
471
- // Get the type of LLM (e.g. 'vertexai', 'anthropic') - this should exist
472
- const llmType = jsonConfig.llm.type;
473
- if (!llmType) {
474
- displayError('LLM type not specified in config.');
475
- exit(1);
476
- }
477
- // Get the configuration for the specific LLM type
478
- const llmConfig = jsonConfig.llm;
479
- if (commandLineConfigOverrides.verbose) {
480
- // Necessary to avoid https://github.com/langchain-ai/langchainjs/issues/8705
481
- llmConfig.verbose = commandLineConfigOverrides.verbose;
482
- }
483
- // Import the appropriate config module
484
- const configModule = await import(`#src/providers/${llmType}.js`);
485
- if (configModule.processJsonConfig) {
486
- const llm = (await configModule.processJsonConfig(llmConfig));
487
- const mergedConfig = mergeRawConfig(jsonConfig, llm, commandLineConfigOverrides);
488
- if (configModule.postProcessJsonConfig) {
489
- return await configModule.postProcessJsonConfig(mergedConfig);
490
- }
491
- else {
492
- return await mergedConfig;
493
- }
494
- }
495
- else {
496
- displayWarning(`Config module for ${llmType} does not have processJsonConfig function.`);
497
- exit(1);
498
- }
499
- }
500
- else {
501
- displayError('No LLM configuration found in config.');
502
- exit(1);
503
- }
504
- }
505
- catch (e) {
506
- if (e instanceof Error && e.message.includes('Cannot find module')) {
507
- displayError(`LLM type '${jsonConfig.llm.type}' not supported.`);
508
- }
509
- else {
510
- displayError(`Error processing LLM config: ${e instanceof Error ? e.message : String(e)}`);
511
- }
512
- exit(1);
513
- }
514
- // This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
515
- throw new Error('Unexpected error occurred.');
516
- }
517
- /**
518
- * Deep merge two objects, with source overriding target properties
519
- * @param target - The target object with default values
520
- * @param source - The source object with user overrides
521
- * @param maxDepth - Maximum recursion depth to prevent stack overflow (default: 4)
522
- */
523
- function deepMerge(target, source, maxDepth = 4) {
524
- if (!source)
525
- return target;
526
- if (!target)
527
- return source;
528
- const result = { ...target };
529
- // Return result without merging if depth is exceeded
530
- if (maxDepth === 0)
531
- return result;
532
- for (const key in source) {
533
- const sourceValue = source[key];
534
- const targetValue = target[key];
535
- if (sourceValue &&
536
- typeof sourceValue === 'object' &&
537
- !Array.isArray(sourceValue) &&
538
- targetValue &&
539
- typeof targetValue === 'object' &&
540
- !Array.isArray(targetValue)) {
541
- // Recursively merge nested objects
542
- result[key] = deepMerge(targetValue, sourceValue, maxDepth - 1);
543
- }
544
- else if (sourceValue !== undefined) {
545
- // Override with source value if it exists
546
- result[key] = sourceValue;
547
- }
548
- }
549
- return result;
550
- }
551
- /**
552
- * Merge config with default config
553
- */
554
- async function mergeConfig(partialConfig, commandLineConfigOverrides) {
555
- const config = partialConfig;
556
- // Migrate deprecated property names
557
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
558
- const raw = config;
559
- if (raw.contentProvider && !raw.contentSource) {
560
- displayWarning('Config property "contentProvider" is deprecated. Use "contentSource" instead.');
561
- config.contentSource = raw.contentProvider;
562
- }
563
- if (raw.requirementsProvider && !raw.requirementSource) {
564
- displayWarning('Config property "requirementsProvider" is deprecated. Use "requirementSource" instead.');
565
- config.requirementSource = raw.requirementsProvider;
566
- }
567
- // Keep both old and new in sync
568
- if (config.contentSource)
569
- config.contentProvider = config.contentSource;
570
- if (config.requirementSource)
571
- config.requirementsProvider = config.requirementSource;
572
- // Migrate command-level deprecated properties
573
- if (config.commands) {
574
- for (const cmdName of ['pr', 'review']) {
575
- const cmd = config.commands[cmdName];
576
- if (cmd) {
577
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
578
- const cmdRaw = cmd;
579
- if (cmdRaw.contentProvider && !cmdRaw.contentSource) {
580
- cmd.contentSource = cmdRaw.contentProvider;
581
- }
582
- if (cmdRaw.requirementsProvider && !cmdRaw.requirementSource) {
583
- cmd.requirementSource = cmdRaw.requirementsProvider;
584
- }
585
- if (cmd.contentSource)
586
- cmd.contentProvider = cmd.contentSource;
587
- if (cmd.requirementSource)
588
- cmd.requirementsProvider = cmd.requirementSource;
589
- }
590
- }
591
- }
592
- // Deep merge command configs while preserving defaults
593
- // Type complexity from DEFAULT_CONFIG.commands 'as const' requires any cast for deep merge result
594
- const mergedCommands = {
595
- pr: deepMerge(DEFAULT_CONFIG.commands.pr, config?.commands?.pr), // eslint-disable-line @typescript-eslint/no-explicit-any
596
- review: deepMerge(DEFAULT_CONFIG.commands.review, config?.commands?.review), // eslint-disable-line @typescript-eslint/no-explicit-any
597
- code: deepMerge(DEFAULT_CONFIG.commands.code, config?.commands?.code), // eslint-disable-line @typescript-eslint/no-explicit-any
598
- exec: deepMerge(DEFAULT_CONFIG.commands.exec, config?.commands?.exec), // eslint-disable-line @typescript-eslint/no-explicit-any
599
- ask: deepMerge(DEFAULT_CONFIG.commands.ask, config?.commands?.ask), // eslint-disable-line @typescript-eslint/no-explicit-any
600
- chat: deepMerge(DEFAULT_CONFIG.commands.chat, config?.commands?.chat), // eslint-disable-line @typescript-eslint/no-explicit-any
601
- api: deepMerge(DEFAULT_CONFIG.commands.api, config?.commands?.api), // eslint-disable-line @typescript-eslint/no-explicit-any
602
- };
603
- const mergedConfig = {
604
- ...DEFAULT_CONFIG,
605
- ...config,
606
- commands: mergedCommands,
607
- };
608
- if (commandLineConfigOverrides.identityProfile !== undefined) {
609
- displayInfo(`Activating profile: ${commandLineConfigOverrides.identityProfile}`);
610
- mergedConfig.identityProfile = commandLineConfigOverrides.identityProfile.trim();
611
- }
612
- if (commandLineConfigOverrides.verbose !== undefined) {
613
- mergedConfig.llm.verbose = commandLineConfigOverrides.verbose;
614
- }
615
- if (commandLineConfigOverrides.writeOutputToFile !== undefined) {
616
- mergedConfig.writeOutputToFile = commandLineConfigOverrides.writeOutputToFile;
617
- }
618
- // Set the useColour value in systemUtils
619
- setUseColour(mergedConfig.useColour);
620
- // Set console logging level
621
- if (mergedConfig.consoleLevel !== undefined) {
622
- const resolvedConsoleLevel = resolveConsoleLevel(mergedConfig.consoleLevel);
623
- if (resolvedConsoleLevel !== undefined) {
624
- mergedConfig.consoleLevel = resolvedConsoleLevel;
625
- setConsoleLevel(resolvedConsoleLevel);
626
- }
627
- else {
628
- displayWarning(`Invalid consoleLevel "${String(mergedConfig.consoleLevel)}", using default ${StatusLevel.INFO}.`);
629
- mergedConfig.consoleLevel = StatusLevel.INFO;
630
- setConsoleLevel(StatusLevel.INFO);
631
- }
632
- }
633
- mergedConfig.canInterruptInferenceWithEsc = mergedConfig.canInterruptInferenceWithEsc && isTTY();
634
- return mergedConfig;
635
- }
636
- const CONSOLE_LEVELS_BY_NAME = {
637
- debug: StatusLevel.DEBUG,
638
- info: StatusLevel.INFO,
639
- display: StatusLevel.DISPLAY,
640
- success: StatusLevel.SUCCESS,
641
- warning: StatusLevel.WARNING,
642
- error: StatusLevel.ERROR,
643
- stream: StatusLevel.STREAM,
644
- };
645
- function resolveConsoleLevel(level) {
646
- if (typeof level === 'number') {
647
- return StatusLevel[level] !== undefined ? level : undefined;
648
- }
649
- if (typeof level === 'string') {
650
- const normalized = level.trim().toLowerCase();
651
- if (normalized in CONSOLE_LEVELS_BY_NAME) {
652
- return CONSOLE_LEVELS_BY_NAME[normalized];
653
- }
654
- const enumValue = StatusLevel[level];
655
- if (typeof enumValue === 'number') {
656
- return enumValue;
657
- }
658
- }
659
- return undefined;
660
- }
661
- /**
662
- * Merge raw with default config
663
- */
664
- async function mergeRawConfig(config, llm, commandLineConfigOverrides) {
665
- const modelDisplayName = config.llm?.model;
666
- return await mergeConfig({ ...config, llm, modelDisplayName }, commandLineConfigOverrides);
667
- }
11
+ * This module is the **public barrel** for the configuration system. The implementation
12
+ * is split into focused modules under `config/`:
13
+ * - `config/types.ts` — the configuration type surface.
14
+ * - `config/shell-policy.ts` {@link GthDevToolsConfig} + the shell/dev-tools resolvers.
15
+ * - `config/defaults.ts` — {@link DEFAULT_CONFIG}.
16
+ * - `config/loader.ts` — discovery + the layered load/merge pipeline.
17
+ * - `config/schema.ts` — the Zod schema (single source of truth) + JSON-Schema generator.
18
+ *
19
+ * Every name that was previously exported from `config.ts` is re-exported here, so the
20
+ * public import path `@gaunt-sloth/core/config.js` (and `#src/config.js`) is unchanged.
21
+ */
22
+ export * from '#src/config/types.js';
23
+ export * from '#src/config/shell-policy.js';
24
+ export * from '#src/config/defaults.js';
25
+ export * from '#src/config/loader.js';
668
26
  //# sourceMappingURL=config.js.map