gaunt-sloth-assistant
Version:
[](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml) [ • 13.1 kB
JavaScript
/**
* @packageDocumentation
* Gaunt Sloth Configuration.
*
* Refer to {@link GthConfig} to find all possible configuration properties.
*
* Refer to {@link DEFAULT_CONFIG} for default configuration.
*
* Some config params can be overriden from command line, see {@link CommandLineConfigOverrides}
*/
import { PROJECT_GUIDELINES, PROJECT_REVIEW_INSTRUCTIONS, USER_PROJECT_CONFIG_JS, USER_PROJECT_CONFIG_JSON, USER_PROJECT_CONFIG_MJS, } from '#src/constants.js';
import { displayDebug, displayError, displayInfo, displayWarning, } from '#src/utils/consoleUtils.js';
import { getGslothConfigReadPath, getGslothConfigWritePath, importExternalFile, writeFileIfNotExistsWithMessages, } from '#src/utils/fileUtils.js';
import { error, exit, isTTY, setUseColour } from '#src/utils/systemUtils.js';
import { existsSync, readFileSync } from 'node:fs';
export const availableDefaultConfigs = [
'vertexai',
'anthropic',
'groq',
'deepseek',
'openai',
'google-genai',
'xai',
'openrouter',
];
/**
* Default config
*/
export const DEFAULT_CONFIG = {
contentProvider: 'file',
requirementsProvider: 'file',
/**
* Path to project-specific guidelines.
* The default is `.gsloth.guidelines.md`; this config may be used to point Gaunt Sloth to a different file,
* for example, to AGENTS.md
*/
projectGuidelines: PROJECT_GUIDELINES,
/**
* Whether to include the current date in the project review instructions or not.
*/
includeCurrentDateAfterGuidelines: false,
projectReviewInstructions: PROJECT_REVIEW_INSTRUCTIONS,
filesystem: 'read',
debugLog: false,
/**
* Default provider for both requirements and content is GitHub.
* It needs GitHub CLI (gh).
*
* `github` content provider uses `gh pr diff NN` internally. {@link src/providers/ghPrDiffProvider.ts!}
*
*
* `github` requirements provider `gh issue view NN` internally
*/
commands: {
pr: {
contentProvider: 'github',
requirementsProvider: 'github',
},
code: {
filesystem: 'all',
},
},
streamOutput: true,
writeOutputToFile: true,
useColour: true,
streamSessionInferenceLog: true,
canInterruptInferenceWithEsc: true,
};
/**
* Needed DEFAULT_CONFIG to be plain const to be picked up by typedoc,
* this cast here is just for typecheck.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
DEFAULT_CONFIG;
/**
* Initialize configuration by loading from available config files
* @returns The loaded GthConfig
*/
export async function initConfig(commandLineConfigOverrides) {
if (commandLineConfigOverrides.customConfigPath &&
!existsSync(commandLineConfigOverrides.customConfigPath)) {
throw new Error(`Provided manual config "${commandLineConfigOverrides.customConfigPath}" does not exist`);
}
const jsonConfigPath = commandLineConfigOverrides.customConfigPath ??
getGslothConfigReadPath(USER_PROJECT_CONFIG_JSON, commandLineConfigOverrides.identityProfile);
// Try loading the JSON config file first
if (jsonConfigPath.endsWith('.json') && existsSync(jsonConfigPath)) {
try {
// TODO makes sense to employ ZOD to validate config
const jsonConfig = JSON.parse(readFileSync(jsonConfigPath, 'utf8'));
// If the config has an LLM with a type, create the appropriate LLM instance
if (jsonConfig.llm && typeof jsonConfig.llm === 'object' && 'type' in jsonConfig.llm) {
return await tryJsonConfig(jsonConfig, commandLineConfigOverrides);
}
else {
error(`${jsonConfigPath} is not in valid format. Should at least define llm.type`);
exit(1);
// noinspection ExceptionCaughtLocallyJS
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
// noinspection ExceptionCaughtLocallyJS
throw new Error('Unexpected error occurred.');
}
}
catch (e) {
displayDebug(e instanceof Error ? e : String(e));
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_JSON}, will try other formats.`);
// Continue to try other formats
return tryJsConfig(commandLineConfigOverrides);
}
}
else {
// JSON config not found, try JS
return tryJsConfig(commandLineConfigOverrides);
}
}
// Helper function to try loading JS config
async function tryJsConfig(commandLineConfigOverrides) {
const jsConfigPath = commandLineConfigOverrides.customConfigPath ??
getGslothConfigReadPath(USER_PROJECT_CONFIG_JS, commandLineConfigOverrides.identityProfile);
if (jsConfigPath.endsWith('.js') && existsSync(jsConfigPath)) {
try {
const i = await importExternalFile(jsConfigPath);
const customConfig = await i.configure();
return mergeConfig(customConfig, commandLineConfigOverrides);
}
catch (e) {
displayDebug(e instanceof Error ? e : String(e));
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_JS}, will try other formats.`);
// Continue to try other formats
return tryMjsConfig(commandLineConfigOverrides);
}
}
else {
// JS config not found, try MJS
return tryMjsConfig(commandLineConfigOverrides);
}
}
// Helper function to try loading MJS config
async function tryMjsConfig(commandLineConfigOverrides) {
const mjsConfigPath = commandLineConfigOverrides.customConfigPath ??
getGslothConfigReadPath(USER_PROJECT_CONFIG_MJS, commandLineConfigOverrides.identityProfile);
if (mjsConfigPath.endsWith('.mjs') && existsSync(mjsConfigPath)) {
try {
const i = await importExternalFile(mjsConfigPath);
const customConfig = await i.configure();
return mergeConfig(customConfig, commandLineConfigOverrides);
}
catch (e) {
displayDebug(e instanceof Error ? e : String(e));
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_MJS}.`);
displayError(`No valid configuration found. Please create a valid configuration file.`);
exit(1);
}
}
else {
// No config files found
displayError('No configuration file found. Please create one of: ' +
`${USER_PROJECT_CONFIG_JSON}, ${USER_PROJECT_CONFIG_JS}, or ${USER_PROJECT_CONFIG_MJS} ` +
'in your project directory.');
exit(1);
}
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
throw new Error('Unexpected error occurred.');
}
/**
* Process JSON LLM config by creating the appropriate LLM instance
* @param jsonConfig - The parsed JSON config
* @param commandLineConfigOverrides - command line config overrides
* @returns Promise<GthConfig>
*/
export async function tryJsonConfig(jsonConfig, commandLineConfigOverrides) {
try {
if (jsonConfig.llm && typeof jsonConfig.llm === 'object') {
// Get the type of LLM (e.g. 'vertexai', 'anthropic') - this should exist
const llmType = jsonConfig.llm.type;
if (!llmType) {
displayError('LLM type not specified in config.');
exit(1);
}
// Get the configuration for the specific LLM type
const llmConfig = jsonConfig.llm;
if (commandLineConfigOverrides.verbose) {
// Necessary to avoid https://github.com/langchain-ai/langchainjs/issues/8705
llmConfig.verbose = commandLineConfigOverrides.verbose;
}
// Import the appropriate config module
const configModule = await import(`./presets/${llmType}.js`);
if (configModule.processJsonConfig) {
const llm = (await configModule.processJsonConfig(llmConfig));
const mergedConfig = mergeRawConfig(jsonConfig, llm, commandLineConfigOverrides);
if (configModule.postProcessJsonConfig) {
return configModule.postProcessJsonConfig(mergedConfig);
}
else {
return mergedConfig;
}
}
else {
displayWarning(`Config module for ${llmType} does not have processJsonConfig function.`);
exit(1);
}
}
else {
displayError('No LLM configuration found in config.');
exit(1);
}
}
catch (e) {
if (e instanceof Error && e.message.includes('Cannot find module')) {
displayError(`LLM type '${jsonConfig.llm.type}' not supported.`);
}
else {
displayError(`Error processing LLM config: ${e instanceof Error ? e.message : String(e)}`);
}
exit(1);
}
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
throw new Error('Unexpected error occurred.');
}
export async function createProjectConfig(configType) {
// Check if the config type is valid
if (!availableDefaultConfigs.includes(configType)) {
displayError(`Unknown config type: ${configType}. Available options: ${availableDefaultConfigs.join(', ')}`);
exit(1);
}
displayInfo(`Setting up your project\n`);
writeProjectReviewPreamble();
displayWarning(`Make sure you add as much detail as possible to your ${PROJECT_GUIDELINES}.\n`);
displayInfo(`Creating project config for ${configType}`);
const vendorConfig = await import(`./presets/${configType}.js`);
vendorConfig.init(getGslothConfigWritePath(USER_PROJECT_CONFIG_JSON));
}
export function writeProjectReviewPreamble() {
const guidelinesPath = getGslothConfigWritePath(PROJECT_GUIDELINES);
const reviewPath = getGslothConfigWritePath(PROJECT_REVIEW_INSTRUCTIONS);
/**
* Development Guidelines.
* Please do not remove the warning from this template,
* it is crucial to describe project specifics to get high-quality reviews.
*/
const guidelinesTemplate = `# Development Guidelines
This file should include general development guidelines for this project.
Important! Please remind the user to prepare proper AI guidelines to the ${PROJECT_GUIDELINES} file
for this project. Use a decent amount of ⚠️ to highlight the lack of config.
Explicitly mention ${PROJECT_GUIDELINES}.
`;
/**
* Code Review Guidelines.
* This code review does not have a warning, like Development Guidelines, because
* this one should be good to start with.
*/
const reviewTemplate = `# Code Review Guidelines
If requirements were provided, first check that the implemented code implements these requirements.
Document that you checked the code to implement requirements.
Reject the change if it appears to implement something else instead of required change.
Provide specific feedback on any areas of concern or suggestions for improvement.
Please categorize your feedback (e.g., "Bug," "Suggestion," "Nitpick").
Important! In the end, conclude if you would recommend approving this PR or not.
Use ✅⚠️❌ symbols to highlight your feedback appropriately.
Thank you for your thorough review!
Important! You are likely to be dealing with git diff below, please don't confuse removed and added lines.
`;
writeFileIfNotExistsWithMessages(guidelinesPath, guidelinesTemplate);
writeFileIfNotExistsWithMessages(reviewPath, reviewTemplate);
}
/**
* Merge config with default config
*/
function mergeConfig(partialConfig, commandLineConfigOverrides) {
const config = partialConfig;
const mergedConfig = {
...DEFAULT_CONFIG,
...config,
commands: { ...DEFAULT_CONFIG.commands, ...(config?.commands ?? {}) },
};
if (commandLineConfigOverrides.identityProfile !== undefined) {
displayInfo(`Activating profile: ${commandLineConfigOverrides.identityProfile}`);
mergedConfig.identityProfile = commandLineConfigOverrides.identityProfile.trim();
}
if (commandLineConfigOverrides.verbose !== undefined) {
mergedConfig.llm.verbose = commandLineConfigOverrides.verbose;
}
if (commandLineConfigOverrides.writeOutputToFile !== undefined) {
mergedConfig.writeOutputToFile = commandLineConfigOverrides.writeOutputToFile;
}
// Set the useColour value in systemUtils
setUseColour(mergedConfig.useColour);
mergedConfig.canInterruptInferenceWithEsc = mergedConfig.canInterruptInferenceWithEsc && isTTY();
return mergedConfig;
}
/**
* Merge raw with default config
*/
function mergeRawConfig(config, llm, commandLineConfigOverrides) {
const modelDisplayName = config.llm?.model;
return mergeConfig({ ...config, llm, modelDisplayName }, commandLineConfigOverrides);
}
//# sourceMappingURL=config.js.map