@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/defaults.d.ts +84 -0
- package/dist/config/defaults.js +97 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loader.d.ts +88 -0
- package/dist/config/loader.js +604 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +471 -0
- package/dist/config/schema.js +301 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/shell-policy.d.ts +212 -0
- package/dist/config/shell-policy.js +142 -0
- package/dist/config/shell-policy.js.map +1 -0
- package/dist/config/types.d.ts +453 -0
- package/dist/config/types.js +12 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config.d.ts +18 -821
- package/dist/config.js +15 -657
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.js +9 -1
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/utils/fileUtils.d.ts +4 -1
- package/dist/utils/fileUtils.js +19 -10
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/systemUtils.d.ts +31 -0
- package/dist/utils/systemUtils.js +38 -0
- package/dist/utils/systemUtils.js.map +1 -1
- package/package.json +8 -5
- package/schema/gsloth-config.schema.json +1548 -0
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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|