gaunt-sloth-assistant 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +15 -0
- package/.gsloth.backstory.md +0 -0
- package/.gsloth.guidelines.md +0 -0
- package/.gsloth.review.md +0 -0
- package/.gsloth.system.md +10 -0
- package/.prettierrc.json +0 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +0 -0
- package/README.md +11 -1
- package/ROADMAP.md +0 -0
- package/assets/gaunt-sloth-logo.png +0 -0
- package/assets/release-notes/v0_4_0.md +0 -0
- package/assets/release-notes/v0_5_0.md +10 -0
- package/assets/release-notes/v0_5_1.md +47 -0
- package/dist/commands/askCommand.d.ts +0 -0
- package/dist/commands/askCommand.js +9 -5
- package/dist/commands/askCommand.js.map +1 -1
- package/dist/commands/commandUtils.d.ts +25 -0
- package/dist/commands/commandUtils.js +48 -0
- package/dist/commands/commandUtils.js.map +1 -0
- package/dist/commands/initCommand.d.ts +0 -0
- package/dist/commands/initCommand.js +0 -0
- package/dist/commands/initCommand.js.map +0 -0
- package/dist/commands/prCommand.d.ts +2 -0
- package/dist/commands/prCommand.js +52 -0
- package/dist/commands/prCommand.js.map +1 -0
- package/dist/commands/reviewCommand.d.ts +1 -2
- package/dist/commands/reviewCommand.js +17 -98
- package/dist/commands/reviewCommand.js.map +1 -1
- package/dist/config.d.ts +22 -41
- package/dist/config.js +107 -88
- package/dist/config.js.map +1 -1
- package/dist/configs/anthropic.d.ts +0 -0
- package/dist/configs/anthropic.js +0 -0
- package/dist/configs/anthropic.js.map +0 -0
- package/dist/configs/fake.d.ts +0 -0
- package/dist/configs/fake.js +0 -0
- package/dist/configs/fake.js.map +0 -0
- package/dist/configs/groq.d.ts +0 -0
- package/dist/configs/groq.js +0 -0
- package/dist/configs/groq.js.map +0 -0
- package/dist/configs/vertexai.d.ts +0 -0
- package/dist/configs/vertexai.js +0 -0
- package/dist/configs/vertexai.js.map +0 -0
- package/dist/consoleUtils.d.ts +0 -0
- package/dist/consoleUtils.js +0 -0
- package/dist/consoleUtils.js.map +0 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/filePathUtils.d.ts +0 -0
- package/dist/filePathUtils.js +0 -0
- package/dist/filePathUtils.js.map +0 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/llmUtils.d.ts +2 -2
- package/dist/llmUtils.js +128 -28
- package/dist/llmUtils.js.map +1 -1
- package/dist/modules/questionAnsweringModule.d.ts +2 -1
- package/dist/modules/questionAnsweringModule.js +7 -8
- package/dist/modules/questionAnsweringModule.js.map +1 -1
- package/dist/modules/reviewModule.d.ts +2 -1
- package/dist/modules/reviewModule.js +7 -11
- package/dist/modules/reviewModule.js.map +1 -1
- package/dist/modules/types.d.ts +0 -0
- package/dist/modules/types.js +0 -0
- package/dist/modules/types.js.map +0 -0
- package/dist/prompt.d.ts +1 -0
- package/dist/prompt.js +4 -1
- package/dist/prompt.js.map +1 -1
- package/dist/providers/file.d.ts +0 -0
- package/dist/providers/file.js +0 -0
- package/dist/providers/file.js.map +0 -0
- package/dist/providers/ghIssueProvider.d.ts +0 -0
- package/dist/providers/ghIssueProvider.js +3 -1
- package/dist/providers/ghIssueProvider.js.map +1 -1
- package/dist/providers/ghPrDiffProvider.d.ts +0 -0
- package/dist/providers/ghPrDiffProvider.js +3 -1
- package/dist/providers/ghPrDiffProvider.js.map +1 -1
- package/dist/providers/jiraIssueLegacyProvider.d.ts +0 -0
- package/dist/providers/jiraIssueLegacyProvider.js +0 -0
- package/dist/providers/jiraIssueLegacyProvider.js.map +0 -0
- package/dist/providers/jiraIssueProvider.d.ts +0 -0
- package/dist/providers/jiraIssueProvider.js +3 -1
- package/dist/providers/jiraIssueProvider.js.map +1 -1
- package/dist/providers/text.d.ts +0 -0
- package/dist/providers/text.js +0 -0
- package/dist/providers/text.js.map +0 -0
- package/dist/providers/types.d.ts +0 -0
- package/dist/providers/types.js +0 -0
- package/dist/providers/types.js.map +0 -0
- package/dist/systemUtils.d.ts +0 -0
- package/dist/systemUtils.js +0 -0
- package/dist/systemUtils.js.map +0 -0
- package/dist/utils.d.ts +0 -0
- package/dist/utils.js +0 -0
- package/dist/utils.js.map +0 -0
- package/docs/CONFIGURATION.md +60 -26
- package/docs/DEVELOPMENT.md +0 -0
- package/docs/RELEASE-HOWTO.md +0 -0
- package/eslint.config.js +0 -0
- package/maintenance/doc-maintenance.md +0 -0
- package/package.json +11 -7
- package/src/commands/askCommand.ts +9 -5
- package/src/commands/commandUtils.ts +77 -0
- package/src/commands/initCommand.ts +0 -0
- package/src/commands/prCommand.ts +93 -0
- package/src/commands/reviewCommand.ts +33 -155
- package/src/config.ts +128 -128
- package/src/configs/anthropic.ts +0 -0
- package/src/configs/fake.ts +0 -0
- package/src/configs/groq.ts +0 -0
- package/src/configs/vertexai.ts +0 -0
- package/src/consoleUtils.ts +0 -0
- package/src/constants.ts +7 -0
- package/src/filePathUtils.ts +0 -0
- package/src/index.ts +4 -2
- package/src/llmUtils.ts +149 -36
- package/src/modules/questionAnsweringModule.ts +9 -13
- package/src/modules/reviewModule.ts +14 -11
- package/src/modules/types.ts +0 -0
- package/src/prompt.ts +5 -1
- package/src/providers/file.ts +0 -0
- package/src/providers/ghIssueProvider.ts +3 -1
- package/src/providers/ghPrDiffProvider.ts +3 -1
- package/src/providers/jiraIssueLegacyProvider.ts +0 -0
- package/src/providers/jiraIssueProvider.ts +5 -1
- package/src/providers/text.ts +0 -0
- package/src/providers/types.ts +0 -0
- package/src/systemUtils.ts +0 -0
- package/src/utils.ts +0 -0
- package/tsconfig.json +0 -0
- package/vitest-it.config.ts +0 -0
- package/vitest.config.ts +0 -0
package/src/config.ts
CHANGED
@@ -1,10 +1,17 @@
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid';
|
2
1
|
import { displayDebug, displayError, displayInfo, displayWarning } from '#src/consoleUtils.js';
|
3
2
|
import { importExternalFile, writeFileIfNotExistsWithMessages } from '#src/utils.js';
|
4
3
|
import { existsSync, readFileSync } from 'node:fs';
|
5
4
|
import { error, exit } from '#src/systemUtils.js';
|
6
5
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
7
|
-
import {
|
6
|
+
import { getGslothConfigReadPath, getGslothConfigWritePath } from '#src/filePathUtils.js';
|
7
|
+
import type { Connection } from '@langchain/mcp-adapters';
|
8
|
+
import {
|
9
|
+
USER_PROJECT_CONFIG_JS,
|
10
|
+
USER_PROJECT_CONFIG_JSON,
|
11
|
+
USER_PROJECT_CONFIG_MJS,
|
12
|
+
PROJECT_GUIDELINES,
|
13
|
+
PROJECT_REVIEW_INSTRUCTIONS,
|
14
|
+
} from '#src/constants.js';
|
8
15
|
|
9
16
|
export interface SlothConfig extends BaseSlothConfig {
|
10
17
|
llm: BaseChatModel; // FIXME this is still bad keeping instance in config is probably not best choice
|
@@ -12,16 +19,8 @@ export interface SlothConfig extends BaseSlothConfig {
|
|
12
19
|
requirementsProvider: string;
|
13
20
|
projectGuidelines: string;
|
14
21
|
projectReviewInstructions: string;
|
15
|
-
|
16
|
-
|
17
|
-
contentProvider: string;
|
18
|
-
requirementsProvider?: string;
|
19
|
-
};
|
20
|
-
review?: {
|
21
|
-
requirementsProvider?: string;
|
22
|
-
contentProvider?: string;
|
23
|
-
};
|
24
|
-
};
|
22
|
+
streamOutput: boolean;
|
23
|
+
filesystem: string[] | 'all' | 'none';
|
25
24
|
}
|
26
25
|
|
27
26
|
/**
|
@@ -40,31 +39,26 @@ interface BaseSlothConfig {
|
|
40
39
|
requirementsProvider?: string;
|
41
40
|
projectGuidelines?: string;
|
42
41
|
projectReviewInstructions?: string;
|
42
|
+
streamOutput?: boolean;
|
43
|
+
filesystem?: string[] | 'all' | 'none';
|
43
44
|
commands?: {
|
44
|
-
pr
|
45
|
-
contentProvider
|
45
|
+
pr?: {
|
46
|
+
contentProvider?: string;
|
46
47
|
requirementsProvider?: string;
|
48
|
+
filesystem?: string[] | 'all' | 'none';
|
47
49
|
};
|
48
50
|
review?: {
|
49
51
|
requirementsProvider?: string;
|
50
52
|
contentProvider?: string;
|
53
|
+
filesystem?: string[] | 'all' | 'none';
|
54
|
+
};
|
55
|
+
ask?: {
|
56
|
+
filesystem?: string[] | 'all' | 'none';
|
51
57
|
};
|
52
58
|
};
|
53
59
|
requirementsProviderConfig?: Record<string, unknown>;
|
54
60
|
contentProviderConfig?: Record<string, unknown>;
|
55
|
-
|
56
|
-
|
57
|
-
/**
|
58
|
-
* @deprecated
|
59
|
-
* this object has blurred responsibility lines and bad name.
|
60
|
-
*/
|
61
|
-
export interface SlothContext {
|
62
|
-
config: SlothConfig;
|
63
|
-
session: {
|
64
|
-
configurable: {
|
65
|
-
thread_id: string;
|
66
|
-
};
|
67
|
-
};
|
61
|
+
mcpServers?: Record<string, Connection>;
|
68
62
|
}
|
69
63
|
|
70
64
|
export interface LLMConfig extends Record<string, unknown> {
|
@@ -72,13 +66,6 @@ export interface LLMConfig extends Record<string, unknown> {
|
|
72
66
|
model: string;
|
73
67
|
}
|
74
68
|
|
75
|
-
export const USER_PROJECT_CONFIG_JS = '.gsloth.config.js';
|
76
|
-
export const USER_PROJECT_CONFIG_JSON = '.gsloth.config.json';
|
77
|
-
export const USER_PROJECT_CONFIG_MJS = '.gsloth.config.mjs';
|
78
|
-
export const GSLOTH_BACKSTORY = '.gsloth.backstory.md';
|
79
|
-
export const PROJECT_GUIDELINES = '.gsloth.guidelines.md';
|
80
|
-
export const PROJECT_REVIEW_INSTRUCTIONS = '.gsloth.review.md';
|
81
|
-
|
82
69
|
export const availableDefaultConfigs = ['vertexai', 'anthropic', 'groq'] as const;
|
83
70
|
export type ConfigType = (typeof availableDefaultConfigs)[number];
|
84
71
|
|
@@ -88,6 +75,16 @@ export const DEFAULT_CONFIG: Partial<SlothConfig> = {
|
|
88
75
|
requirementsProvider: 'file',
|
89
76
|
projectGuidelines: PROJECT_GUIDELINES,
|
90
77
|
projectReviewInstructions: PROJECT_REVIEW_INSTRUCTIONS,
|
78
|
+
streamOutput: true,
|
79
|
+
filesystem: [
|
80
|
+
'read_file',
|
81
|
+
'read_multiple_files',
|
82
|
+
'list_directory',
|
83
|
+
'directory_tree',
|
84
|
+
'search_files',
|
85
|
+
'get_file_info',
|
86
|
+
'list_allowed_directories',
|
87
|
+
],
|
91
88
|
commands: {
|
92
89
|
pr: {
|
93
90
|
contentProvider: 'github', // gh pr diff NN
|
@@ -97,30 +94,26 @@ export const DEFAULT_CONFIG: Partial<SlothConfig> = {
|
|
97
94
|
};
|
98
95
|
|
99
96
|
/**
|
100
|
-
*
|
101
|
-
*
|
102
|
-
* TODO this should be reworked to something more robust
|
97
|
+
* Initialize configuration by loading from available config files
|
98
|
+
* @returns The loaded SlothConfig
|
103
99
|
*/
|
104
|
-
export
|
105
|
-
config: DEFAULT_CONFIG,
|
106
|
-
session: { configurable: { thread_id: uuidv4() } },
|
107
|
-
} as Partial<SlothContext> as SlothContext;
|
108
|
-
|
109
|
-
export async function initConfig(): Promise<void> {
|
100
|
+
export async function initConfig(): Promise<SlothConfig> {
|
110
101
|
const jsonConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_JSON);
|
111
|
-
const jsConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_JS);
|
112
|
-
const mjsConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_MJS);
|
113
102
|
|
114
|
-
// Try loading JSON config file first
|
103
|
+
// Try loading the JSON config file first
|
115
104
|
if (existsSync(jsonConfigPath)) {
|
116
105
|
try {
|
106
|
+
// TODO makes sense to employ ZOD to validate config
|
117
107
|
const jsonConfig = JSON.parse(readFileSync(jsonConfigPath, 'utf8')) as RawSlothConfig;
|
118
108
|
// If the config has an LLM with a type, create the appropriate LLM instance
|
119
109
|
if (jsonConfig.llm && typeof jsonConfig.llm === 'object' && 'type' in jsonConfig.llm) {
|
120
|
-
await tryJsonConfig(jsonConfig);
|
110
|
+
return await tryJsonConfig(jsonConfig);
|
121
111
|
} else {
|
122
112
|
error(`${jsonConfigPath} is not in valid format. Should at least define llm.type`);
|
123
113
|
exit(1);
|
114
|
+
// noinspection ExceptionCaughtLocallyJS
|
115
|
+
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
|
116
|
+
throw new Error('Unexpected error occurred.');
|
124
117
|
}
|
125
118
|
} catch (e) {
|
126
119
|
displayDebug(e instanceof Error ? e : String(e));
|
@@ -134,113 +127,113 @@ export async function initConfig(): Promise<void> {
|
|
134
127
|
// JSON config not found, try JS
|
135
128
|
return tryJsConfig();
|
136
129
|
}
|
130
|
+
}
|
137
131
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
displayError(
|
151
|
-
`Failed to read config from ${USER_PROJECT_CONFIG_JS}, will try other formats.`
|
152
|
-
);
|
153
|
-
// Continue to try other formats
|
154
|
-
return tryMjsConfig();
|
155
|
-
});
|
156
|
-
} else {
|
157
|
-
// JS config not found, try MJS
|
132
|
+
// Helper function to try loading JS config
|
133
|
+
async function tryJsConfig(): Promise<SlothConfig> {
|
134
|
+
const jsConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_JS);
|
135
|
+
if (existsSync(jsConfigPath)) {
|
136
|
+
try {
|
137
|
+
const i = await importExternalFile(jsConfigPath);
|
138
|
+
const customConfig = await i.configure(jsConfigPath);
|
139
|
+
return mergeConfig(customConfig) as SlothConfig;
|
140
|
+
} catch (e) {
|
141
|
+
displayDebug(e instanceof Error ? e : String(e));
|
142
|
+
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_JS}, will try other formats.`);
|
143
|
+
// Continue to try other formats
|
158
144
|
return tryMjsConfig();
|
159
145
|
}
|
160
|
-
}
|
161
|
-
|
162
|
-
|
163
|
-
async function tryMjsConfig(): Promise<void> {
|
164
|
-
if (existsSync(mjsConfigPath)) {
|
165
|
-
return importExternalFile(mjsConfigPath)
|
166
|
-
.then((i: { configure: (module: string) => Promise<Partial<SlothConfig>> }) =>
|
167
|
-
i.configure(mjsConfigPath)
|
168
|
-
)
|
169
|
-
.then((config) => {
|
170
|
-
slothContext.config = { ...slothContext.config, ...config };
|
171
|
-
})
|
172
|
-
.catch((e) => {
|
173
|
-
displayDebug(e instanceof Error ? e : String(e));
|
174
|
-
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_MJS}.`);
|
175
|
-
displayError(`No valid configuration found. Please create a valid configuration file.`);
|
176
|
-
exit();
|
177
|
-
});
|
178
|
-
} else {
|
179
|
-
// No config files found
|
180
|
-
displayError(
|
181
|
-
'No configuration file found. Please create one of: ' +
|
182
|
-
`${USER_PROJECT_CONFIG_JSON}, ${USER_PROJECT_CONFIG_JS}, or ${USER_PROJECT_CONFIG_MJS} ` +
|
183
|
-
'in your project directory.'
|
184
|
-
);
|
185
|
-
exit();
|
186
|
-
}
|
146
|
+
} else {
|
147
|
+
// JS config not found, try MJS
|
148
|
+
return tryMjsConfig();
|
187
149
|
}
|
188
150
|
}
|
189
151
|
|
190
|
-
//
|
191
|
-
|
192
|
-
const
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
152
|
+
// Helper function to try loading MJS config
|
153
|
+
async function tryMjsConfig(): Promise<SlothConfig> {
|
154
|
+
const mjsConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_MJS);
|
155
|
+
if (existsSync(mjsConfigPath)) {
|
156
|
+
try {
|
157
|
+
const i = await importExternalFile(mjsConfigPath);
|
158
|
+
const customConfig = await i.configure(mjsConfigPath);
|
159
|
+
return mergeConfig(customConfig) as SlothConfig;
|
160
|
+
} catch (e) {
|
161
|
+
displayDebug(e instanceof Error ? e : String(e));
|
162
|
+
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_MJS}.`);
|
163
|
+
displayError(`No valid configuration found. Please create a valid configuration file.`);
|
164
|
+
exit(1);
|
165
|
+
}
|
166
|
+
} else {
|
167
|
+
// No config files found
|
197
168
|
displayError(
|
198
|
-
|
169
|
+
'No configuration file found. Please create one of: ' +
|
170
|
+
`${USER_PROJECT_CONFIG_JSON}, ${USER_PROJECT_CONFIG_JS}, or ${USER_PROJECT_CONFIG_MJS} ` +
|
171
|
+
'in your project directory.'
|
199
172
|
);
|
200
173
|
exit(1);
|
201
|
-
return;
|
202
174
|
}
|
175
|
+
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
|
176
|
+
throw new Error('Unexpected error occurred.');
|
177
|
+
}
|
203
178
|
|
179
|
+
/**
|
180
|
+
* Process JSON LLM config by creating the appropriate LLM instance
|
181
|
+
* @param jsonConfig - The parsed JSON config
|
182
|
+
* @returns Promise<SlothConfig>
|
183
|
+
*/
|
184
|
+
export async function tryJsonConfig(jsonConfig: RawSlothConfig): Promise<SlothConfig> {
|
204
185
|
try {
|
205
|
-
|
206
|
-
|
186
|
+
if (jsonConfig.llm && typeof jsonConfig.llm === 'object') {
|
187
|
+
// Get the type of LLM (e.g., 'vertexai', 'anthropic') - this should exist
|
188
|
+
const llmType = (jsonConfig.llm as LLMConfig).type;
|
189
|
+
if (!llmType) {
|
190
|
+
displayError('LLM type not specified in config.');
|
191
|
+
exit(1);
|
192
|
+
}
|
193
|
+
|
194
|
+
// Get the configuration for the specific LLM type
|
195
|
+
const llmConfig = jsonConfig.llm;
|
196
|
+
// Import the appropriate config module
|
207
197
|
const configModule = await import(`./configs/${llmType}.js`);
|
208
198
|
if (configModule.processJsonConfig) {
|
209
199
|
const llm = (await configModule.processJsonConfig(llmConfig)) as BaseChatModel;
|
210
|
-
|
200
|
+
return mergeRawConfig(jsonConfig, llm);
|
211
201
|
} else {
|
212
202
|
displayWarning(`Config module for ${llmType} does not have processJsonConfig function.`);
|
213
203
|
exit(1);
|
214
204
|
}
|
215
|
-
}
|
216
|
-
|
217
|
-
displayWarning(`Could not import config module for ${llmType}.`);
|
205
|
+
} else {
|
206
|
+
displayError('No LLM configuration found in config.');
|
218
207
|
exit(1);
|
219
208
|
}
|
220
|
-
} catch (
|
221
|
-
|
222
|
-
|
209
|
+
} catch (e) {
|
210
|
+
if (e instanceof Error && e.message.includes('Cannot find module')) {
|
211
|
+
displayError(`LLM type '${(jsonConfig.llm as LLMConfig).type}' not supported.`);
|
212
|
+
} else {
|
213
|
+
displayError(`Error processing LLM config: ${e instanceof Error ? e.message : String(e)}`);
|
214
|
+
}
|
223
215
|
exit(1);
|
224
216
|
}
|
217
|
+
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
|
218
|
+
throw new Error('Unexpected error occurred.');
|
225
219
|
}
|
226
220
|
|
227
221
|
export async function createProjectConfig(configType: string): Promise<void> {
|
228
|
-
|
229
|
-
writeProjectReviewPreamble();
|
230
|
-
displayWarning(`Make sure you add as much detail as possible to your ${PROJECT_GUIDELINES}.\n`);
|
231
|
-
|
232
|
-
// Check if the config type is in availableDefaultConfigs
|
222
|
+
// Check if the config type is valid
|
233
223
|
if (!availableDefaultConfigs.includes(configType as ConfigType)) {
|
234
224
|
displayError(
|
235
|
-
`
|
225
|
+
`Unknown config type: ${configType}. Available options: ${availableDefaultConfigs.join(', ')}`
|
236
226
|
);
|
237
227
|
exit(1);
|
238
|
-
return;
|
239
228
|
}
|
240
229
|
|
230
|
+
displayInfo(`Setting up your project\n`);
|
231
|
+
writeProjectReviewPreamble();
|
232
|
+
displayWarning(`Make sure you add as much detail as possible to your ${PROJECT_GUIDELINES}.\n`);
|
233
|
+
|
241
234
|
displayInfo(`Creating project config for ${configType}`);
|
242
235
|
const vendorConfig = await import(`./configs/${configType}.js`);
|
243
|
-
vendorConfig.init(getGslothConfigWritePath(USER_PROJECT_CONFIG_JSON)
|
236
|
+
vendorConfig.init(getGslothConfigWritePath(USER_PROJECT_CONFIG_JSON));
|
244
237
|
}
|
245
238
|
|
246
239
|
export function writeProjectReviewPreamble(): void {
|
@@ -288,13 +281,20 @@ Important! You are likely to be dealing with git diff below, please don't confus
|
|
288
281
|
}
|
289
282
|
|
290
283
|
/**
|
291
|
-
*
|
292
|
-
|
284
|
+
* Merge config with default config
|
285
|
+
*/
|
286
|
+
function mergeConfig(partialConfig: Partial<SlothConfig>): SlothConfig {
|
287
|
+
const config = partialConfig as SlothConfig;
|
288
|
+
return {
|
289
|
+
...DEFAULT_CONFIG,
|
290
|
+
...config,
|
291
|
+
commands: { ...DEFAULT_CONFIG.commands, ...(config?.commands ?? {}) },
|
292
|
+
};
|
293
|
+
}
|
294
|
+
|
295
|
+
/**
|
296
|
+
* Merge raw with default config
|
293
297
|
*/
|
294
|
-
|
295
|
-
|
296
|
-
delete (slothContext as unknown as Record<string, unknown>)[key];
|
297
|
-
});
|
298
|
-
slothContext.config = DEFAULT_CONFIG as SlothConfig;
|
299
|
-
slothContext.session = { configurable: { thread_id: uuidv4() } };
|
298
|
+
function mergeRawConfig(config: RawSlothConfig, llm: BaseChatModel): SlothConfig {
|
299
|
+
return mergeConfig({ ...config, llm });
|
300
300
|
}
|
package/src/configs/anthropic.ts
CHANGED
File without changes
|
package/src/configs/fake.ts
CHANGED
File without changes
|
package/src/configs/groq.ts
CHANGED
File without changes
|
package/src/configs/vertexai.ts
CHANGED
File without changes
|
package/src/consoleUtils.ts
CHANGED
File without changes
|
package/src/constants.ts
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
export const USER_PROJECT_CONFIG_JS = '.gsloth.config.js';
|
2
|
+
export const USER_PROJECT_CONFIG_JSON = '.gsloth.config.json';
|
3
|
+
export const USER_PROJECT_CONFIG_MJS = '.gsloth.config.mjs';
|
4
|
+
export const GSLOTH_BACKSTORY = '.gsloth.backstory.md';
|
5
|
+
export const PROJECT_GUIDELINES = '.gsloth.guidelines.md';
|
6
|
+
export const PROJECT_REVIEW_INSTRUCTIONS = '.gsloth.review.md';
|
7
|
+
export const GSLOTH_SYSTEM_PROMPT = '.gsloth.system.md';
|
package/src/filePathUtils.ts
CHANGED
File without changes
|
package/src/index.ts
CHANGED
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
2
2
|
import { askCommand } from '#src/commands/askCommand.js';
|
3
3
|
import { initCommand } from '#src/commands/initCommand.js';
|
4
4
|
import { reviewCommand } from '#src/commands/reviewCommand.js';
|
5
|
-
import {
|
5
|
+
import { prCommand } from '#src/commands/prCommand.js';
|
6
6
|
import { getSlothVersion } from '#src/utils.js';
|
7
7
|
import { argv, readStdin } from '#src/systemUtils.js';
|
8
8
|
import { setVerbose } from '#src/llmUtils.js';
|
@@ -22,8 +22,10 @@ if (program.getOptionValue('verbose')) {
|
|
22
22
|
setVerbose(true);
|
23
23
|
}
|
24
24
|
|
25
|
+
// Initialize all commands - they will handle their own config loading
|
25
26
|
initCommand(program);
|
26
|
-
reviewCommand(program
|
27
|
+
reviewCommand(program);
|
28
|
+
prCommand(program);
|
27
29
|
askCommand(program);
|
28
30
|
// TODO add general interactive chat command
|
29
31
|
|
package/src/llmUtils.ts
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
-
import type { Message
|
2
|
-
import {
|
1
|
+
import type { Message } from '#src/modules/types.js';
|
2
|
+
import { HumanMessage, isAIMessage, SystemMessage } from '@langchain/core/messages';
|
3
3
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
4
|
-
import {
|
5
|
-
import {
|
4
|
+
import { SlothConfig } from '#src/config.js';
|
5
|
+
import type { Connection } from '@langchain/mcp-adapters';
|
6
|
+
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
|
7
|
+
import { display, displayError, displayInfo, displayWarning } from '#src/consoleUtils.js';
|
8
|
+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
9
|
+
import { getCurrentDir, stdout } from '#src/systemUtils.js';
|
10
|
+
import type { StructuredToolInterface } from '@langchain/core/tools';
|
11
|
+
import { ProgressIndicator } from '#src/utils.js';
|
6
12
|
|
7
13
|
const llmGlobalSettings = {
|
8
14
|
verbose: false,
|
@@ -10,45 +16,152 @@ const llmGlobalSettings = {
|
|
10
16
|
|
11
17
|
export async function invoke(
|
12
18
|
llm: BaseChatModel,
|
13
|
-
options: Partial<BaseLanguageModelCallOptions>,
|
14
19
|
systemMessage: string,
|
15
|
-
prompt: string
|
20
|
+
prompt: string,
|
21
|
+
config: SlothConfig,
|
22
|
+
command?: 'ask' | 'pr' | 'review'
|
16
23
|
): Promise<string> {
|
24
|
+
try {
|
25
|
+
if (config.streamOutput && config.llm._llmType() === 'anthropic') {
|
26
|
+
displayWarning('To avoid known bug with Anthropic forcing streamOutput to false');
|
27
|
+
config.streamOutput = false;
|
28
|
+
}
|
29
|
+
} catch {}
|
17
30
|
if (llmGlobalSettings.verbose) {
|
18
31
|
llm.verbose = true;
|
19
32
|
}
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
const
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
//
|
40
|
-
const
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
:
|
33
|
+
|
34
|
+
// Merge command-specific filesystem config if provided
|
35
|
+
let effectiveConfig = config;
|
36
|
+
if (command && config.commands?.[command]?.filesystem !== undefined) {
|
37
|
+
effectiveConfig = {
|
38
|
+
...config,
|
39
|
+
filesystem: config.commands[command].filesystem!,
|
40
|
+
};
|
41
|
+
}
|
42
|
+
|
43
|
+
const client = getClient(effectiveConfig);
|
44
|
+
|
45
|
+
const allTools = (await client?.getTools()) ?? [];
|
46
|
+
const tools = filterTools(allTools, effectiveConfig.filesystem || 'none');
|
47
|
+
|
48
|
+
if (allTools.length > 0) {
|
49
|
+
displayInfo(`Loaded ${tools.length} tools.`);
|
50
|
+
}
|
51
|
+
|
52
|
+
// Create the React agent
|
53
|
+
const agent = createReactAgent({
|
54
|
+
llm,
|
55
|
+
tools,
|
56
|
+
});
|
57
|
+
|
58
|
+
// Run the agent
|
59
|
+
try {
|
60
|
+
const messages: Message[] = [new SystemMessage(systemMessage), new HumanMessage(prompt)];
|
61
|
+
display(`Connecting to LLM...`);
|
62
|
+
const output = { aiMessage: '' };
|
63
|
+
if (!config.streamOutput) {
|
64
|
+
const progress = new ProgressIndicator('Thinking.');
|
65
|
+
try {
|
66
|
+
const response = await agent.invoke({ messages });
|
67
|
+
output.aiMessage = response.messages[response.messages.length - 1].content as string;
|
68
|
+
const toolNames = response.messages
|
69
|
+
.filter((msg: any) => msg.tool_calls && msg.tool_calls.length > 0)
|
70
|
+
.flatMap((msg: any) => msg.tool_calls.map((tc: any) => tc.name));
|
71
|
+
if (toolNames.length > 0) {
|
72
|
+
displayInfo(`\nUsed tools: ${toolNames.join(', ')}`);
|
73
|
+
}
|
74
|
+
} catch (e) {
|
75
|
+
displayWarning(`Something went wrong ${(e as Error).message}`);
|
76
|
+
} finally {
|
77
|
+
progress.stop();
|
78
|
+
}
|
79
|
+
display(output.aiMessage);
|
80
|
+
} else {
|
81
|
+
const stream = await agent.stream({ messages }, { streamMode: 'messages' });
|
82
|
+
|
83
|
+
for await (const [chunk, _metadata] of stream) {
|
84
|
+
if (isAIMessage(chunk)) {
|
85
|
+
stdout.write(chunk.content as string, 'utf-8');
|
86
|
+
output.aiMessage += chunk.content;
|
87
|
+
let toolCalls = chunk.tool_calls;
|
88
|
+
if (toolCalls && toolCalls.length > 0) {
|
89
|
+
const suffix = toolCalls.length > 1 ? 's' : '';
|
90
|
+
const toolCallsString = toolCalls.map((t) => t?.name).join(', ');
|
91
|
+
displayInfo(`Using tool${suffix} ${toolCallsString}`);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
return output.aiMessage;
|
98
|
+
} catch (error) {
|
99
|
+
if (error instanceof Error) {
|
100
|
+
if (error?.name === 'ToolException') {
|
101
|
+
displayError(`Tool execution failed: ${error.message}`);
|
102
|
+
}
|
103
|
+
}
|
104
|
+
throw error;
|
105
|
+
} finally {
|
106
|
+
if (client) {
|
107
|
+
await client.close();
|
108
|
+
}
|
109
|
+
}
|
50
110
|
}
|
51
111
|
|
52
112
|
export function setVerbose(debug: boolean) {
|
53
113
|
llmGlobalSettings.verbose = debug;
|
54
114
|
}
|
115
|
+
|
116
|
+
function filterTools(
|
117
|
+
tools: StructuredToolInterface[],
|
118
|
+
filesystemConfig: string[] | 'all' | 'none'
|
119
|
+
) {
|
120
|
+
if (filesystemConfig === 'all' || !Array.isArray(filesystemConfig)) {
|
121
|
+
return tools;
|
122
|
+
}
|
123
|
+
|
124
|
+
// Create set of allowed tool names with mcp__filesystem__ prefix
|
125
|
+
const allowedToolNames = new Set(
|
126
|
+
filesystemConfig.map((shortName) => `mcp__filesystem__${shortName}`)
|
127
|
+
);
|
128
|
+
|
129
|
+
return tools.filter((tool) => {
|
130
|
+
// Allow non-filesystem tools and only allowed filesystem tools
|
131
|
+
return !tool.name.startsWith('mcp__filesystem__') || allowedToolNames.has(tool.name);
|
132
|
+
});
|
133
|
+
}
|
134
|
+
|
135
|
+
function getClient(config: SlothConfig) {
|
136
|
+
const defaultServers: Record<string, Connection> = {};
|
137
|
+
|
138
|
+
// Add filesystem server if configured
|
139
|
+
if (config.filesystem && config.filesystem !== 'none') {
|
140
|
+
const filesystemConfig: Connection = {
|
141
|
+
transport: 'stdio' as const,
|
142
|
+
command: 'npx',
|
143
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', getCurrentDir()],
|
144
|
+
};
|
145
|
+
|
146
|
+
defaultServers.filesystem = filesystemConfig;
|
147
|
+
}
|
148
|
+
|
149
|
+
// Merge with user's mcpServers
|
150
|
+
const mcpServers = { ...defaultServers, ...(config.mcpServers || {}) };
|
151
|
+
|
152
|
+
// If user provided their own filesystem config, it overrides default
|
153
|
+
if (config.mcpServers?.filesystem) {
|
154
|
+
mcpServers.filesystem = config.mcpServers.filesystem;
|
155
|
+
}
|
156
|
+
|
157
|
+
if (Object.keys(mcpServers).length > 0) {
|
158
|
+
return new MultiServerMCPClient({
|
159
|
+
throwOnLoadError: true,
|
160
|
+
prefixToolNameWithServerName: true,
|
161
|
+
additionalToolNamePrefix: 'mcp',
|
162
|
+
mcpServers,
|
163
|
+
});
|
164
|
+
} else {
|
165
|
+
return null;
|
166
|
+
}
|
167
|
+
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import type { SlothConfig } from '#src/config.js';
|
2
2
|
import { display, displayError, displaySuccess } from '#src/consoleUtils.js';
|
3
3
|
import { getGslothFilePath } from '#src/filePathUtils.js';
|
4
4
|
import { generateStandardFileName, ProgressIndicator } from '#src/utils.js';
|
@@ -14,21 +14,17 @@ import { invoke } from '#src/llmUtils.js';
|
|
14
14
|
export async function askQuestion(
|
15
15
|
source: string,
|
16
16
|
preamble: string,
|
17
|
-
content: string
|
17
|
+
content: string,
|
18
|
+
config: SlothConfig
|
18
19
|
): Promise<void> {
|
19
|
-
const progressIndicator = new ProgressIndicator('Thinking.');
|
20
|
-
const outputContent = await invoke(
|
21
|
-
|
22
|
-
slothContext.session,
|
23
|
-
preamble,
|
24
|
-
content
|
25
|
-
);
|
26
|
-
progressIndicator.stop();
|
20
|
+
const progressIndicator = config.streamOutput ? undefined : new ProgressIndicator('Thinking.');
|
21
|
+
const outputContent = await invoke(config.llm, preamble, content, config, 'ask');
|
22
|
+
progressIndicator?.stop();
|
27
23
|
const filename = generateStandardFileName(source);
|
28
24
|
const filePath = getGslothFilePath(filename);
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
if (!config.streamOutput) {
|
26
|
+
display('\n' + outputContent);
|
27
|
+
}
|
32
28
|
try {
|
33
29
|
writeFileSync(filePath, outputContent);
|
34
30
|
displaySuccess(`This report can be found in ${filePath}`);
|