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.
Files changed (135) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/.gsloth.backstory.md +0 -0
  3. package/.gsloth.guidelines.md +0 -0
  4. package/.gsloth.review.md +0 -0
  5. package/.gsloth.system.md +10 -0
  6. package/.prettierrc.json +0 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +0 -0
  9. package/README.md +11 -1
  10. package/ROADMAP.md +0 -0
  11. package/assets/gaunt-sloth-logo.png +0 -0
  12. package/assets/release-notes/v0_4_0.md +0 -0
  13. package/assets/release-notes/v0_5_0.md +10 -0
  14. package/assets/release-notes/v0_5_1.md +47 -0
  15. package/dist/commands/askCommand.d.ts +0 -0
  16. package/dist/commands/askCommand.js +9 -5
  17. package/dist/commands/askCommand.js.map +1 -1
  18. package/dist/commands/commandUtils.d.ts +25 -0
  19. package/dist/commands/commandUtils.js +48 -0
  20. package/dist/commands/commandUtils.js.map +1 -0
  21. package/dist/commands/initCommand.d.ts +0 -0
  22. package/dist/commands/initCommand.js +0 -0
  23. package/dist/commands/initCommand.js.map +0 -0
  24. package/dist/commands/prCommand.d.ts +2 -0
  25. package/dist/commands/prCommand.js +52 -0
  26. package/dist/commands/prCommand.js.map +1 -0
  27. package/dist/commands/reviewCommand.d.ts +1 -2
  28. package/dist/commands/reviewCommand.js +17 -98
  29. package/dist/commands/reviewCommand.js.map +1 -1
  30. package/dist/config.d.ts +22 -41
  31. package/dist/config.js +107 -88
  32. package/dist/config.js.map +1 -1
  33. package/dist/configs/anthropic.d.ts +0 -0
  34. package/dist/configs/anthropic.js +0 -0
  35. package/dist/configs/anthropic.js.map +0 -0
  36. package/dist/configs/fake.d.ts +0 -0
  37. package/dist/configs/fake.js +0 -0
  38. package/dist/configs/fake.js.map +0 -0
  39. package/dist/configs/groq.d.ts +0 -0
  40. package/dist/configs/groq.js +0 -0
  41. package/dist/configs/groq.js.map +0 -0
  42. package/dist/configs/vertexai.d.ts +0 -0
  43. package/dist/configs/vertexai.js +0 -0
  44. package/dist/configs/vertexai.js.map +0 -0
  45. package/dist/consoleUtils.d.ts +0 -0
  46. package/dist/consoleUtils.js +0 -0
  47. package/dist/consoleUtils.js.map +0 -0
  48. package/dist/constants.d.ts +7 -0
  49. package/dist/constants.js +8 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/filePathUtils.d.ts +0 -0
  52. package/dist/filePathUtils.js +0 -0
  53. package/dist/filePathUtils.js.map +0 -0
  54. package/dist/index.d.ts +0 -0
  55. package/dist/index.js +4 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/llmUtils.d.ts +2 -2
  58. package/dist/llmUtils.js +128 -28
  59. package/dist/llmUtils.js.map +1 -1
  60. package/dist/modules/questionAnsweringModule.d.ts +2 -1
  61. package/dist/modules/questionAnsweringModule.js +7 -8
  62. package/dist/modules/questionAnsweringModule.js.map +1 -1
  63. package/dist/modules/reviewModule.d.ts +2 -1
  64. package/dist/modules/reviewModule.js +7 -11
  65. package/dist/modules/reviewModule.js.map +1 -1
  66. package/dist/modules/types.d.ts +0 -0
  67. package/dist/modules/types.js +0 -0
  68. package/dist/modules/types.js.map +0 -0
  69. package/dist/prompt.d.ts +1 -0
  70. package/dist/prompt.js +4 -1
  71. package/dist/prompt.js.map +1 -1
  72. package/dist/providers/file.d.ts +0 -0
  73. package/dist/providers/file.js +0 -0
  74. package/dist/providers/file.js.map +0 -0
  75. package/dist/providers/ghIssueProvider.d.ts +0 -0
  76. package/dist/providers/ghIssueProvider.js +3 -1
  77. package/dist/providers/ghIssueProvider.js.map +1 -1
  78. package/dist/providers/ghPrDiffProvider.d.ts +0 -0
  79. package/dist/providers/ghPrDiffProvider.js +3 -1
  80. package/dist/providers/ghPrDiffProvider.js.map +1 -1
  81. package/dist/providers/jiraIssueLegacyProvider.d.ts +0 -0
  82. package/dist/providers/jiraIssueLegacyProvider.js +0 -0
  83. package/dist/providers/jiraIssueLegacyProvider.js.map +0 -0
  84. package/dist/providers/jiraIssueProvider.d.ts +0 -0
  85. package/dist/providers/jiraIssueProvider.js +3 -1
  86. package/dist/providers/jiraIssueProvider.js.map +1 -1
  87. package/dist/providers/text.d.ts +0 -0
  88. package/dist/providers/text.js +0 -0
  89. package/dist/providers/text.js.map +0 -0
  90. package/dist/providers/types.d.ts +0 -0
  91. package/dist/providers/types.js +0 -0
  92. package/dist/providers/types.js.map +0 -0
  93. package/dist/systemUtils.d.ts +0 -0
  94. package/dist/systemUtils.js +0 -0
  95. package/dist/systemUtils.js.map +0 -0
  96. package/dist/utils.d.ts +0 -0
  97. package/dist/utils.js +0 -0
  98. package/dist/utils.js.map +0 -0
  99. package/docs/CONFIGURATION.md +60 -26
  100. package/docs/DEVELOPMENT.md +0 -0
  101. package/docs/RELEASE-HOWTO.md +0 -0
  102. package/eslint.config.js +0 -0
  103. package/maintenance/doc-maintenance.md +0 -0
  104. package/package.json +11 -7
  105. package/src/commands/askCommand.ts +9 -5
  106. package/src/commands/commandUtils.ts +77 -0
  107. package/src/commands/initCommand.ts +0 -0
  108. package/src/commands/prCommand.ts +93 -0
  109. package/src/commands/reviewCommand.ts +33 -155
  110. package/src/config.ts +128 -128
  111. package/src/configs/anthropic.ts +0 -0
  112. package/src/configs/fake.ts +0 -0
  113. package/src/configs/groq.ts +0 -0
  114. package/src/configs/vertexai.ts +0 -0
  115. package/src/consoleUtils.ts +0 -0
  116. package/src/constants.ts +7 -0
  117. package/src/filePathUtils.ts +0 -0
  118. package/src/index.ts +4 -2
  119. package/src/llmUtils.ts +149 -36
  120. package/src/modules/questionAnsweringModule.ts +9 -13
  121. package/src/modules/reviewModule.ts +14 -11
  122. package/src/modules/types.ts +0 -0
  123. package/src/prompt.ts +5 -1
  124. package/src/providers/file.ts +0 -0
  125. package/src/providers/ghIssueProvider.ts +3 -1
  126. package/src/providers/ghPrDiffProvider.ts +3 -1
  127. package/src/providers/jiraIssueLegacyProvider.ts +0 -0
  128. package/src/providers/jiraIssueProvider.ts +5 -1
  129. package/src/providers/text.ts +0 -0
  130. package/src/providers/types.ts +0 -0
  131. package/src/systemUtils.ts +0 -0
  132. package/src/utils.ts +0 -0
  133. package/tsconfig.json +0 -0
  134. package/vitest-it.config.ts +0 -0
  135. 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 { getGslothConfigWritePath, getGslothConfigReadPath } from '#src/filePathUtils.js';
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
- commands: {
16
- pr: {
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: string;
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
- * @deprecated
101
- * this object has blurred responsibility lines and bad name.
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 const slothContext = {
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
- // Helper function to try loading JS config
139
- async function tryJsConfig(): Promise<void> {
140
- if (existsSync(jsConfigPath)) {
141
- return importExternalFile(jsConfigPath)
142
- .then((i: { configure: (module: string) => Promise<Partial<SlothConfig>> }) =>
143
- i.configure(jsConfigPath)
144
- )
145
- .then((config) => {
146
- slothContext.config = { ...slothContext.config, ...config };
147
- })
148
- .catch((e) => {
149
- displayDebug(e instanceof Error ? e : String(e));
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
- // Helper function to try loading MJS config
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
- // Process JSON LLM config by creating the appropriate LLM instance
191
- export async function tryJsonConfig(jsonConfig: RawSlothConfig): Promise<void> {
192
- const llmConfig = jsonConfig?.llm;
193
- const llmType = llmConfig?.type?.toLowerCase();
194
-
195
- // Check if the LLM type is in availableDefaultConfigs
196
- if (!llmType || !availableDefaultConfigs.includes(llmType as ConfigType)) {
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
- `Unsupported LLM type: ${llmType}. Available types are: ${availableDefaultConfigs.join(', ')}`
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
- // Import the appropriate config module based on the LLM type
206
- try {
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
- slothContext.config = { ...slothContext.config, ...jsonConfig, llm };
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
- } catch (importError) {
216
- displayDebug(importError instanceof Error ? importError : String(importError));
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 (error) {
221
- displayDebug(error instanceof Error ? error : String(error));
222
- displayError(`Error creating LLM instance for type ${llmType}.`);
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
- displayInfo(`Setting up your project\n`);
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
- `Unsupported config type: ${configType}. Available types are: ${availableDefaultConfigs.join(', ')}`
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), slothContext);
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
- * @deprecated test only
292
- * TODO should be gone together with slothContext itself
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
- export function reset() {
295
- Object.keys(slothContext).forEach((key) => {
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
  }
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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';
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 { slothContext } from '#src/config.js';
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, slothContext);
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, State } from '#src/modules/types.js';
2
- import { AIMessageChunk, HumanMessage, SystemMessage } from '@langchain/core/messages';
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 { END, MemorySaver, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
5
- import { BaseLanguageModelCallOptions } from '@langchain/core/language_models/base';
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
- // This node receives the current state (messages) and invokes the LLM
21
- const callModel = async (state: State): Promise<{ messages: AIMessageChunk }> => {
22
- // state.messages will contain the list including the system systemMessage and user diff
23
- const response = await (llm as BaseChatModel).invoke(state.messages);
24
- // MessagesAnnotation expects the node to return the new message(s) to be added to the state.
25
- // Wrap the response in an array if it's a single message object.
26
- return { messages: response };
27
- };
28
-
29
- // Define the graph structure with MessagesAnnotation state
30
- const workflow = new StateGraph(MessagesAnnotation)
31
- // Define the node and edge
32
- .addNode('model', callModel)
33
- .addEdge(START, 'model') // Start at the 'model' node
34
- .addEdge('model', END); // End after the 'model' node completes
35
-
36
- // Set up memory (optional but good practice for potential future multi-turn interactions)
37
- const memory = new MemorySaver();
38
-
39
- // Compile the workflow into a runnable app
40
- const app = workflow.compile({ checkpointer: memory });
41
-
42
- // Construct the initial the messages including the systemMessage as a system message
43
- const messages: Message[] = [new SystemMessage(systemMessage), new HumanMessage(prompt)];
44
-
45
- const output = await app.invoke({ messages }, options);
46
- const lastMessage = output.messages[output.messages.length - 1];
47
- return typeof lastMessage.content === 'string'
48
- ? lastMessage.content
49
- : JSON.stringify(lastMessage.content);
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 { slothContext } from '#src/config.js';
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
- slothContext.config.llm,
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
- display(`\nwriting ${filePath}`);
30
- // TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
31
- display('\n' + outputContent);
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}`);