gaunt-sloth-assistant 0.1.5 → 0.3.0

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 (145) hide show
  1. package/{.gsloth.preamble.review.md → .gsloth.guidelines.md} +0 -8
  2. package/.gsloth.review.md +7 -0
  3. package/.prettierrc.json +9 -0
  4. package/README.md +177 -158
  5. package/ROADMAP.md +1 -1
  6. package/dist/commands/askCommand.d.ts +6 -0
  7. package/dist/commands/askCommand.js +27 -0
  8. package/dist/commands/askCommand.js.map +1 -0
  9. package/dist/commands/initCommand.d.ts +6 -0
  10. package/dist/commands/initCommand.js +16 -0
  11. package/dist/commands/initCommand.js.map +1 -0
  12. package/dist/commands/reviewCommand.d.ts +3 -0
  13. package/dist/commands/reviewCommand.js +142 -0
  14. package/dist/commands/reviewCommand.js.map +1 -0
  15. package/dist/config.d.ts +84 -0
  16. package/dist/config.js +180 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/configs/anthropic.d.ts +4 -0
  19. package/{src → dist}/configs/anthropic.js +45 -48
  20. package/dist/configs/anthropic.js.map +1 -0
  21. package/dist/configs/fake.d.ts +3 -0
  22. package/{src → dist}/configs/fake.js +11 -14
  23. package/dist/configs/fake.js.map +1 -0
  24. package/dist/configs/groq.d.ts +4 -0
  25. package/{src → dist}/configs/groq.js +10 -13
  26. package/dist/configs/groq.js.map +1 -0
  27. package/dist/configs/types.d.ts +14 -0
  28. package/dist/configs/types.js +2 -0
  29. package/dist/configs/types.js.map +1 -0
  30. package/dist/configs/vertexai.d.ts +4 -0
  31. package/{src → dist}/configs/vertexai.js +44 -47
  32. package/dist/configs/vertexai.js.map +1 -0
  33. package/dist/consoleUtils.d.ts +6 -0
  34. package/{src → dist}/consoleUtils.js +10 -15
  35. package/dist/consoleUtils.js.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +26 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/llmUtils.d.ts +4 -0
  40. package/dist/llmUtils.js +39 -0
  41. package/dist/llmUtils.js.map +1 -0
  42. package/dist/modules/questionAnsweringModule.d.ts +7 -0
  43. package/dist/modules/questionAnsweringModule.js +33 -0
  44. package/dist/modules/questionAnsweringModule.js.map +1 -0
  45. package/dist/modules/reviewModule.d.ts +1 -0
  46. package/dist/modules/reviewModule.js +29 -0
  47. package/dist/modules/reviewModule.js.map +1 -0
  48. package/dist/modules/types.d.ts +18 -0
  49. package/dist/modules/types.js +2 -0
  50. package/dist/modules/types.js.map +1 -0
  51. package/dist/prompt.d.ts +8 -0
  52. package/dist/prompt.js +45 -0
  53. package/dist/prompt.js.map +1 -0
  54. package/dist/providers/file.d.ts +8 -0
  55. package/dist/providers/file.js +20 -0
  56. package/dist/providers/file.js.map +1 -0
  57. package/dist/providers/ghPrDiffProvider.d.ts +8 -0
  58. package/dist/providers/ghPrDiffProvider.js +16 -0
  59. package/dist/providers/ghPrDiffProvider.js.map +1 -0
  60. package/dist/providers/jiraIssueLegacyAccessTokenProvider.d.ts +8 -0
  61. package/dist/providers/jiraIssueLegacyAccessTokenProvider.js +62 -0
  62. package/dist/providers/jiraIssueLegacyAccessTokenProvider.js.map +1 -0
  63. package/dist/providers/jiraIssueLegacyProvider.d.ts +8 -0
  64. package/dist/providers/jiraIssueLegacyProvider.js +74 -0
  65. package/dist/providers/jiraIssueLegacyProvider.js.map +1 -0
  66. package/dist/providers/jiraIssueProvider.d.ts +11 -0
  67. package/dist/providers/jiraIssueProvider.js +96 -0
  68. package/dist/providers/jiraIssueProvider.js.map +1 -0
  69. package/dist/providers/text.d.ts +8 -0
  70. package/dist/providers/text.js +10 -0
  71. package/dist/providers/text.js.map +1 -0
  72. package/dist/providers/types.d.ts +21 -0
  73. package/dist/providers/types.js +2 -0
  74. package/dist/providers/types.js.map +1 -0
  75. package/dist/systemUtils.d.ts +32 -0
  76. package/dist/systemUtils.js +70 -0
  77. package/dist/systemUtils.js.map +1 -0
  78. package/dist/utils.d.ts +49 -0
  79. package/dist/utils.js +192 -0
  80. package/dist/utils.js.map +1 -0
  81. package/docs/CONFIGURATION.md +99 -10
  82. package/docs/RELEASE-HOWTO.md +7 -1
  83. package/eslint.config.js +99 -21
  84. package/gth-ASK-2025-05-16T14-11-39.md +3 -0
  85. package/gth-ASK-2025-05-16T14-18-27.md +3 -0
  86. package/gth-ASK-2025-05-16T14-18-56.md +1 -0
  87. package/gth-ASK-2025-05-16T14-41-20.md +3 -0
  88. package/gth-ASK-2025-05-16T14-43-31.md +51 -0
  89. package/gth-ASK-2025-05-16T16-05-52.md +62 -0
  90. package/gth-DIFF-review-2025-05-16T16-07-53.md +56 -0
  91. package/gth-DIFF-review-2025-05-16T16-18-55.md +292 -0
  92. package/index.js +10 -27
  93. package/package.json +26 -15
  94. package/src/commands/askCommand.ts +35 -0
  95. package/src/commands/initCommand.ts +19 -0
  96. package/src/commands/reviewCommand.ts +223 -0
  97. package/src/config.ts +269 -0
  98. package/src/configs/anthropic.ts +57 -0
  99. package/src/configs/fake.ts +15 -0
  100. package/src/configs/groq.ts +54 -0
  101. package/src/configs/vertexai.ts +53 -0
  102. package/src/consoleUtils.ts +33 -0
  103. package/src/index.ts +30 -0
  104. package/src/llmUtils.ts +54 -0
  105. package/src/modules/questionAnsweringModule.ts +44 -0
  106. package/src/modules/reviewModule.ts +31 -0
  107. package/src/modules/types.ts +23 -0
  108. package/src/prompt.ts +54 -0
  109. package/src/providers/file.ts +24 -0
  110. package/src/providers/ghPrDiffProvider.ts +20 -0
  111. package/src/providers/jiraIssueLegacyProvider.ts +103 -0
  112. package/src/providers/jiraIssueProvider.ts +133 -0
  113. package/src/providers/text.ts +14 -0
  114. package/src/providers/types.ts +24 -0
  115. package/src/systemUtils.ts +90 -0
  116. package/src/utils.ts +232 -0
  117. package/tsconfig.json +24 -0
  118. package/vitest.config.ts +13 -0
  119. package/.eslint.config.mjs +0 -72
  120. package/.github/dependabot.yml +0 -11
  121. package/.github/workflows/ci.yml +0 -33
  122. package/spec/.gsloth.config.js +0 -22
  123. package/spec/.gsloth.config.json +0 -25
  124. package/spec/askCommand.spec.js +0 -92
  125. package/spec/config.spec.js +0 -421
  126. package/spec/initCommand.spec.js +0 -55
  127. package/spec/predefinedConfigs.spec.js +0 -100
  128. package/spec/questionAnsweringModule.spec.js +0 -137
  129. package/spec/reviewCommand.spec.js +0 -222
  130. package/spec/reviewModule.spec.js +0 -28
  131. package/spec/support/jasmine.mjs +0 -14
  132. package/src/commands/askCommand.js +0 -27
  133. package/src/commands/initCommand.js +0 -17
  134. package/src/commands/reviewCommand.js +0 -154
  135. package/src/config.js +0 -177
  136. package/src/modules/questionAnsweringModule.js +0 -82
  137. package/src/modules/reviewModule.js +0 -70
  138. package/src/prompt.js +0 -34
  139. package/src/providers/file.js +0 -19
  140. package/src/providers/ghPrDiffProvider.js +0 -11
  141. package/src/providers/jiraIssueLegacyAccessTokenProvider.js +0 -84
  142. package/src/providers/text.js +0 -6
  143. package/src/systemUtils.js +0 -32
  144. package/src/utils.js +0 -173
  145. /package/{.gsloth.preamble.internal.md → .gsloth.backstory.md} +0 -0
@@ -0,0 +1,223 @@
1
+ import { Command, Option } from 'commander';
2
+ import type { SlothContext } from '#src/config.js';
3
+ import { slothContext } from '#src/config.js';
4
+ import { readBackstory, readGuidelines, readReviewInstructions } from '#src/prompt.js';
5
+ import { readMultipleFilesFromCurrentDir } from '#src/utils.js';
6
+ import { displayError } from '#src/consoleUtils.js';
7
+ import { getStringFromStdin } from '#src/systemUtils.js';
8
+
9
+ /**
10
+ * Requirements providers. Expected to be in `.providers/` dir
11
+ */
12
+ const REQUIREMENTS_PROVIDERS = {
13
+ 'jira-legacy': 'jiraIssueLegacyProvider.js',
14
+ jira: 'jiraIssueProvider.js',
15
+ text: 'text.js',
16
+ file: 'file.js',
17
+ } as const;
18
+
19
+ type RequirementsProviderType = keyof typeof REQUIREMENTS_PROVIDERS;
20
+
21
+ /**
22
+ * Content providers. Expected to be in `.providers/` dir
23
+ */
24
+ const CONTENT_PROVIDERS = {
25
+ gh: 'ghPrDiffProvider.js',
26
+ text: 'text.js',
27
+ file: 'file.js',
28
+ } as const;
29
+
30
+ type ContentProviderType = keyof typeof CONTENT_PROVIDERS;
31
+
32
+ interface ReviewCommandOptions {
33
+ file?: string[];
34
+ requirements?: string;
35
+ requirementsProvider?: RequirementsProviderType;
36
+ contentProvider?: ContentProviderType;
37
+ message?: string;
38
+ }
39
+
40
+ interface PrCommandOptions {
41
+ file?: string[];
42
+ requirementsProvider?: RequirementsProviderType;
43
+ }
44
+
45
+ export function reviewCommand(program: Command, context: SlothContext): void {
46
+ program
47
+ .command('review')
48
+ .description('Review provided diff or other content')
49
+ .argument(
50
+ '[contentId]',
51
+ 'Optional content ID argument to retrieve content with content provider'
52
+ )
53
+ .alias('r')
54
+ // TODO add provider to get results of git --no-pager diff
55
+ .option(
56
+ '-f, --file [files...]',
57
+ 'Input files. Content of these files will be added BEFORE the diff, but after requirements'
58
+ )
59
+ // TODO figure out what to do with this (we probably want to merge it with requirementsId)?
60
+ .option('-r, --requirements <requirements>', 'Requirements for this review.')
61
+ .addOption(
62
+ new Option(
63
+ '-p, --requirements-provider <requirementsProvider>',
64
+ 'Requirements provider for this review.'
65
+ ).choices(Object.keys(REQUIREMENTS_PROVIDERS))
66
+ )
67
+ .addOption(
68
+ new Option('--content-provider <contentProvider>', 'Content provider').choices(
69
+ Object.keys(CONTENT_PROVIDERS)
70
+ )
71
+ )
72
+ .option('-m, --message <message>', 'Extra message to provide just before the content')
73
+ .action(async (contentId: string | undefined, options: ReviewCommandOptions) => {
74
+ const { initConfig } = await import('#src/config.js');
75
+ await initConfig();
76
+ const systemMessage = [
77
+ readBackstory(),
78
+ readGuidelines(slothContext.config.projectGuidelines),
79
+ readReviewInstructions(slothContext.config.projectReviewInstructions),
80
+ ];
81
+ const content: string[] = [];
82
+ const requirementsId = options.requirements;
83
+ const requirementsProvider =
84
+ options.requirementsProvider ??
85
+ (context.config?.review?.requirementsProvider as RequirementsProviderType | undefined) ??
86
+ (context.config?.requirementsProvider as RequirementsProviderType | undefined);
87
+ const contentProvider =
88
+ options.contentProvider ??
89
+ (context.config?.review?.contentProvider as ContentProviderType | undefined) ??
90
+ (context.config?.contentProvider as ContentProviderType | undefined);
91
+
92
+ // TODO consider calling these in parallel
93
+ const requirements = await getRequirementsFromProvider(requirementsProvider, requirementsId);
94
+ if (requirements) {
95
+ content.push(requirements);
96
+ }
97
+
98
+ const providedContent = await getContentFromProvider(contentProvider, contentId);
99
+ if (providedContent) {
100
+ content.push(providedContent);
101
+ }
102
+
103
+ if (options.file) {
104
+ content.push(readMultipleFilesFromCurrentDir(options.file));
105
+ }
106
+ let stringFromStdin = getStringFromStdin();
107
+ if (stringFromStdin) {
108
+ content.push(stringFromStdin);
109
+ }
110
+ if (options.message) {
111
+ content.push(options.message);
112
+ }
113
+ const { review } = await import('#src/modules/reviewModule.js');
114
+ // TODO make the prefix configurable
115
+ await review('gth-DIFF-review', systemMessage.join('\n'), content.join('\n'));
116
+ });
117
+
118
+ program
119
+ .command('pr')
120
+ .description(
121
+ 'Review provided Pull Request in current directory. ' +
122
+ 'This command is similar to `review`, but default content provider is `gh`. ' +
123
+ '(assuming that GH cli is installed and authenticated for current project'
124
+ )
125
+ .argument('<prId>', 'Pull request ID to review.')
126
+ .argument(
127
+ '[requirementsId]',
128
+ 'Optional requirements ID argument to retrieve requirements with requirements provider'
129
+ )
130
+ .addOption(
131
+ new Option(
132
+ '-p, --requirements-provider <requirementsProvider>',
133
+ 'Requirements provider for this review.'
134
+ ).choices(Object.keys(REQUIREMENTS_PROVIDERS))
135
+ )
136
+ .option(
137
+ '-f, --file [files...]',
138
+ 'Input files. Content of these files will be added BEFORE the diff, but after requirements'
139
+ )
140
+ .action(async (prId: string, requirementsId: string | undefined, options: PrCommandOptions) => {
141
+ const { initConfig } = await import('#src/config.js');
142
+ await initConfig();
143
+
144
+ const systemMessage = [
145
+ readBackstory(),
146
+ readGuidelines(slothContext.config.projectGuidelines),
147
+ readReviewInstructions(slothContext.config.projectReviewInstructions),
148
+ ];
149
+ const content: string[] = [];
150
+ const requirementsProvider =
151
+ options.requirementsProvider ??
152
+ (context.config?.pr?.requirementsProvider as RequirementsProviderType | undefined) ??
153
+ (context.config?.requirementsProvider as RequirementsProviderType | undefined);
154
+
155
+ // Handle requirements
156
+ const requirements = await getRequirementsFromProvider(requirementsProvider, requirementsId);
157
+ if (requirements) {
158
+ content.push(requirements);
159
+ }
160
+
161
+ if (options.file) {
162
+ content.push(readMultipleFilesFromCurrentDir(options.file));
163
+ }
164
+
165
+ // Get PR diff using the 'gh' provider
166
+ const providerPath = `#src/providers/${CONTENT_PROVIDERS['gh']}`;
167
+ const { get } = await import(providerPath);
168
+ content.push(await get(null, prId));
169
+
170
+ const { review } = await import('#src/modules/reviewModule.js');
171
+ // TODO make the prefix configurable
172
+ // TODO consider including requirements id
173
+ // TODO sanitize prId
174
+ await review(`gth-PR-${prId}-review`, systemMessage.join('\n'), content.join('\n'));
175
+ });
176
+
177
+ async function getRequirementsFromProvider(
178
+ requirementsProvider: RequirementsProviderType | undefined,
179
+ requirementsId: string | undefined
180
+ ): Promise<string> {
181
+ return getFromProvider(
182
+ requirementsProvider,
183
+ requirementsId,
184
+ (context.config?.requirementsProviderConfig ?? {})[requirementsProvider as string],
185
+ REQUIREMENTS_PROVIDERS
186
+ );
187
+ }
188
+
189
+ async function getContentFromProvider(
190
+ contentProvider: ContentProviderType | undefined,
191
+ contentId: string | undefined
192
+ ): Promise<string> {
193
+ return getFromProvider(
194
+ contentProvider,
195
+ contentId,
196
+ (context.config?.contentProviderConfig ?? {})[contentProvider as string],
197
+ CONTENT_PROVIDERS
198
+ );
199
+ }
200
+
201
+ async function getFromProvider(
202
+ provider: RequirementsProviderType | ContentProviderType | undefined,
203
+ id: string | undefined,
204
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
+ config: any,
206
+ legitPredefinedProviders: typeof REQUIREMENTS_PROVIDERS | typeof CONTENT_PROVIDERS
207
+ ): Promise<string> {
208
+ if (typeof provider === 'string') {
209
+ // Use one of the predefined providers
210
+ if (legitPredefinedProviders[provider as keyof typeof legitPredefinedProviders]) {
211
+ const providerPath = `#src/providers/${legitPredefinedProviders[provider as keyof typeof legitPredefinedProviders]}`;
212
+ const { get } = await import(providerPath);
213
+ return await get(config, id);
214
+ } else {
215
+ displayError(`Unknown provider: ${provider}. Continuing without it.`);
216
+ }
217
+ } else if (typeof provider === 'function') {
218
+ // Type assertion to handle function call
219
+ return await (provider as (id: string | undefined) => Promise<string>)(id);
220
+ }
221
+ return '';
222
+ }
223
+ }
package/src/config.ts ADDED
@@ -0,0 +1,269 @@
1
+ import path from 'node:path/posix';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { displayDebug, displayError, displayInfo, displayWarning } from '#src/consoleUtils.js';
4
+ import { importExternalFile, writeFileIfNotExistsWithMessages } from '#src/utils.js';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { error, exit, getCurrentDir } from '#src/systemUtils.js';
7
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
8
+
9
+ export interface SlothConfig extends BaseSlothConfig {
10
+ llm: BaseChatModel; // FIXME this is still bad keeping instance in config is probably not best choice
11
+ contentProvider: string;
12
+ requirementsProvider: string;
13
+ projectGuidelines: string;
14
+ projectReviewInstructions: string;
15
+ commands: {
16
+ pr: {
17
+ contentProvider: string;
18
+ };
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Raw, unprocessed sloth config
24
+ */
25
+ export interface RawSlothConfig extends BaseSlothConfig {
26
+ llm: LLMConfig;
27
+ }
28
+
29
+ /**
30
+ * Do not export this one
31
+ */
32
+ interface BaseSlothConfig {
33
+ llm: unknown;
34
+ contentProvider?: string;
35
+ requirementsProvider?: string;
36
+ projectGuidelines?: string;
37
+ projectReviewInstructions?: string;
38
+ commands?: {
39
+ pr: {
40
+ contentProvider: string;
41
+ };
42
+ };
43
+ review?: {
44
+ requirementsProvider?: string;
45
+ contentProvider?: string;
46
+ };
47
+ pr?: {
48
+ requirementsProvider?: string;
49
+ };
50
+ requirementsProviderConfig?: Record<string, unknown>;
51
+ contentProviderConfig?: Record<string, unknown>;
52
+ }
53
+
54
+ /**
55
+ * @deprecated
56
+ * this object has blurred responsibility lines and bad name.
57
+ */
58
+ export interface SlothContext {
59
+ config: SlothConfig;
60
+ session: {
61
+ configurable: {
62
+ thread_id: string;
63
+ };
64
+ };
65
+ }
66
+
67
+ export interface LLMConfig extends Record<string, unknown> {
68
+ type: string;
69
+ model: string;
70
+ }
71
+
72
+ export const USER_PROJECT_CONFIG_JS = '.gsloth.config.js';
73
+ export const USER_PROJECT_CONFIG_JSON = '.gsloth.config.json';
74
+ export const USER_PROJECT_CONFIG_MJS = '.gsloth.config.mjs';
75
+ export const GSLOTH_BACKSTORY = '.gsloth.backstory.md';
76
+ export const PROJECT_GUIDELINES = '.gsloth.guidelines.md';
77
+ export const PROJECT_REVIEW_INSTRUCTIONS = '.gsloth.review.md';
78
+
79
+ export const availableDefaultConfigs = ['vertexai', 'anthropic', 'groq'] as const;
80
+ export type ConfigType = (typeof availableDefaultConfigs)[number];
81
+
82
+ export const DEFAULT_CONFIG: Partial<SlothConfig> = {
83
+ llm: undefined,
84
+ contentProvider: 'file',
85
+ requirementsProvider: 'file',
86
+ projectGuidelines: PROJECT_GUIDELINES,
87
+ projectReviewInstructions: PROJECT_REVIEW_INSTRUCTIONS,
88
+ commands: {
89
+ pr: {
90
+ contentProvider: 'gh',
91
+ },
92
+ },
93
+ };
94
+
95
+ /**
96
+ * @deprecated
97
+ * this object has blurred responsibility lines and bad name.
98
+ * TODO this should be reworked to something more robust
99
+ */
100
+ export const slothContext = {
101
+ config: DEFAULT_CONFIG,
102
+ stdin: '',
103
+ session: { configurable: { thread_id: uuidv4() } },
104
+ } as Partial<SlothContext> as SlothContext;
105
+
106
+ export async function initConfig(): Promise<void> {
107
+ const currentDir = getCurrentDir();
108
+ const jsonConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_JSON);
109
+ const jsConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_JS);
110
+ const mjsConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_MJS);
111
+
112
+ // Try loading JSON config file first
113
+ if (existsSync(jsonConfigPath)) {
114
+ try {
115
+ const jsonConfig = JSON.parse(readFileSync(jsonConfigPath, 'utf8')) as RawSlothConfig;
116
+ // If the config has an LLM with a type, create the appropriate LLM instance
117
+ if (jsonConfig.llm && typeof jsonConfig.llm === 'object' && 'type' in jsonConfig.llm) {
118
+ await tryJsonConfig(jsonConfig);
119
+ } else {
120
+ error(`${jsonConfigPath} is not in valid format. Should at least define llm.type`);
121
+ exit(1);
122
+ }
123
+ } catch (e) {
124
+ displayDebug(e instanceof Error ? e : String(e));
125
+ displayError(
126
+ `Failed to read config from ${USER_PROJECT_CONFIG_JSON}, will try other formats.`
127
+ );
128
+ // Continue to try other formats
129
+ return tryJsConfig();
130
+ }
131
+ } else {
132
+ // JSON config not found, try JS
133
+ return tryJsConfig();
134
+ }
135
+
136
+ // Helper function to try loading JS config
137
+ async function tryJsConfig(): Promise<void> {
138
+ if (existsSync(jsConfigPath)) {
139
+ return importExternalFile(jsConfigPath)
140
+ .then((i: { configure: (module: string) => Promise<Partial<SlothConfig>> }) =>
141
+ i.configure(jsConfigPath)
142
+ )
143
+ .then((config) => {
144
+ slothContext.config = { ...slothContext.config, ...config };
145
+ })
146
+ .catch((e) => {
147
+ displayDebug(e instanceof Error ? e : String(e));
148
+ displayError(
149
+ `Failed to read config from ${USER_PROJECT_CONFIG_JS}, will try other formats.`
150
+ );
151
+ // Continue to try other formats
152
+ return tryMjsConfig();
153
+ });
154
+ } else {
155
+ // JS config not found, try MJS
156
+ return tryMjsConfig();
157
+ }
158
+ }
159
+
160
+ // Helper function to try loading MJS config
161
+ async function tryMjsConfig(): Promise<void> {
162
+ if (existsSync(mjsConfigPath)) {
163
+ return importExternalFile(mjsConfigPath)
164
+ .then((i: { configure: (module: string) => Promise<Partial<SlothConfig>> }) =>
165
+ i.configure(mjsConfigPath)
166
+ )
167
+ .then((config) => {
168
+ slothContext.config = { ...slothContext.config, ...config };
169
+ })
170
+ .catch((e) => {
171
+ displayDebug(e instanceof Error ? e : String(e));
172
+ displayError(`Failed to read config from ${USER_PROJECT_CONFIG_MJS}.`);
173
+ displayError(`No valid configuration found. Please create a valid configuration file.`);
174
+ exit();
175
+ });
176
+ } else {
177
+ // No config files found
178
+ displayError(
179
+ 'No configuration file found. Please create one of: ' +
180
+ `${USER_PROJECT_CONFIG_JSON}, ${USER_PROJECT_CONFIG_JS}, or ${USER_PROJECT_CONFIG_MJS} ` +
181
+ 'in your project directory.'
182
+ );
183
+ exit();
184
+ }
185
+ }
186
+ }
187
+
188
+ // Process JSON LLM config by creating the appropriate LLM instance
189
+ export async function tryJsonConfig(jsonConfig: RawSlothConfig): Promise<void> {
190
+ const llmConfig = jsonConfig?.llm;
191
+ const llmType = llmConfig?.type?.toLowerCase();
192
+
193
+ // Check if the LLM type is in availableDefaultConfigs
194
+ if (!llmType || !availableDefaultConfigs.includes(llmType as ConfigType)) {
195
+ displayError(
196
+ `Unsupported LLM type: ${llmType}. Available types are: ${availableDefaultConfigs.join(', ')}`
197
+ );
198
+ exit(1);
199
+ return;
200
+ }
201
+
202
+ try {
203
+ // Import the appropriate config module based on the LLM type
204
+ try {
205
+ const configModule = await import(`./configs/${llmType}.js`);
206
+ if (configModule.processJsonConfig) {
207
+ const llm = (await configModule.processJsonConfig(llmConfig)) as BaseChatModel;
208
+ slothContext.config = { ...slothContext.config, ...jsonConfig, llm };
209
+ } else {
210
+ displayWarning(`Config module for ${llmType} does not have processJsonConfig function.`);
211
+ exit(1);
212
+ }
213
+ } catch (importError) {
214
+ displayDebug(importError instanceof Error ? importError : String(importError));
215
+ displayWarning(`Could not import config module for ${llmType}.`);
216
+ exit(1);
217
+ }
218
+ } catch (error) {
219
+ displayDebug(error instanceof Error ? error : String(error));
220
+ displayError(`Error creating LLM instance for type ${llmType}.`);
221
+ exit(1);
222
+ }
223
+ }
224
+
225
+ export async function createProjectConfig(configType: string): Promise<void> {
226
+ displayInfo(`Setting up your project\n`);
227
+ writeProjectReviewPreamble();
228
+ displayWarning(`Make sure you add as much detail as possible to your ${PROJECT_GUIDELINES}.\n`);
229
+
230
+ // Check if the config type is in availableDefaultConfigs
231
+ if (!availableDefaultConfigs.includes(configType as ConfigType)) {
232
+ displayError(
233
+ `Unsupported config type: ${configType}. Available types are: ${availableDefaultConfigs.join(', ')}`
234
+ );
235
+ exit(1);
236
+ return;
237
+ }
238
+
239
+ displayInfo(`Creating project config for ${configType}`);
240
+ const vendorConfig = await import(`./configs/${configType}.js`);
241
+ vendorConfig.init(USER_PROJECT_CONFIG_JSON, slothContext);
242
+ }
243
+
244
+ export function writeProjectReviewPreamble(): void {
245
+ const currentDir = getCurrentDir();
246
+ const reviewPreamblePath = path.join(currentDir, PROJECT_GUIDELINES);
247
+ writeFileIfNotExistsWithMessages(
248
+ reviewPreamblePath,
249
+ 'You are doing generic code review.\n' +
250
+ ' Important! Please remind user to prepare proper AI preamble in' +
251
+ PROJECT_GUIDELINES +
252
+ ' for this project. Use decent amount of ⚠️ to highlight lack of config.' +
253
+ ' Explicitly mention `' +
254
+ PROJECT_GUIDELINES +
255
+ '`.'
256
+ );
257
+ }
258
+
259
+ /**
260
+ * @deprecated test only
261
+ * TODO should be gone together with slothContext itself
262
+ */
263
+ export function reset() {
264
+ Object.keys(slothContext).forEach((key) => {
265
+ delete (slothContext as unknown as Record<string, unknown>)[key];
266
+ });
267
+ slothContext.config = DEFAULT_CONFIG as SlothConfig;
268
+ slothContext.session = { configurable: { thread_id: uuidv4() } };
269
+ }
@@ -0,0 +1,57 @@
1
+ import path from 'node:path';
2
+ import { displayWarning } from '#src/consoleUtils.js';
3
+ import { env, getCurrentDir } from '#src/systemUtils.js';
4
+ import { writeFileIfNotExistsWithMessages } from '#src/utils.js';
5
+ import type { AnthropicInput } from '@langchain/anthropic';
6
+ import type {
7
+ BaseChatModel,
8
+ BaseChatModelParams,
9
+ } from '@langchain/core/language_models/chat_models';
10
+
11
+ // Function to process JSON config and create Anthropic LLM instance
12
+ export async function processJsonConfig(
13
+ llmConfig: AnthropicInput & BaseChatModelParams
14
+ ): Promise<BaseChatModel> {
15
+ const anthropic = await import('@langchain/anthropic');
16
+ // Use environment variable if available, otherwise use the config value
17
+ const anthropicApiKey = env.ANTHROPIC_API_KEY || llmConfig.apiKey;
18
+ return new anthropic.ChatAnthropic({
19
+ ...llmConfig,
20
+ apiKey: anthropicApiKey,
21
+ model: llmConfig.model || 'claude-3-7-sonnet-20250219',
22
+ });
23
+ }
24
+
25
+ const jsContent = `/* eslint-disable */
26
+ export async function configure(importFunction, global) {
27
+ // this is going to be imported from sloth dependencies,
28
+ // but can potentially be pulled from global node modules or from this project
29
+ // At a moment only google-vertexai and anthropic packaged with Sloth, but you can install support for any other langchain llms
30
+ const anthropic = await importFunction('@langchain/anthropic');
31
+ return {
32
+ llm: new anthropic.ChatAnthropic({
33
+ apiKey: process.env.ANTHROPIC_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
34
+ model: "claude-3-7-sonnet-20250219" // Don't forget to check new models availability.
35
+ })
36
+ };
37
+ }
38
+ `;
39
+
40
+ const jsonContent = `{
41
+ "llm": {
42
+ "type": "anthropic",
43
+ "apiKey": "your-api-key-here",
44
+ "model": "claude-3-7-sonnet-20250219"
45
+ }
46
+ }`;
47
+
48
+ export function init(configFileName: string): void {
49
+ const currentDir = getCurrentDir();
50
+ path.join(currentDir, configFileName);
51
+
52
+ // Determine which content to use based on file extension
53
+ const content = configFileName.endsWith('.json') ? jsonContent : jsContent;
54
+
55
+ writeFileIfNotExistsWithMessages(configFileName, content);
56
+ displayWarning(`You need to update your ${configFileName} to add your Anthropic API key.`);
57
+ }
@@ -0,0 +1,15 @@
1
+ import { BaseChatModel } from '@langchain/core/language_models/chat_models';
2
+ import { displayWarning } from '#src/consoleUtils.js';
3
+ import type { FakeChatInput } from '@langchain/core/utils/testing';
4
+
5
+ // Function to process JSON config and create Fake LLM instance for testing
6
+ export async function processJsonConfig(llmConfig: FakeChatInput): Promise<BaseChatModel | null> {
7
+ if (llmConfig.responses) {
8
+ const test = await import('@langchain/core/utils/testing');
9
+ return new test.FakeListChatModel(llmConfig);
10
+ }
11
+ displayWarning("Fake LLM requires 'responses' array in config");
12
+ return null;
13
+ }
14
+
15
+ // No init function needed for fake LLM as it's only used for testing
@@ -0,0 +1,54 @@
1
+ import path from 'node:path';
2
+ import { displayInfo, displayWarning } from '#src/consoleUtils.js';
3
+ import { env, getCurrentDir } from '#src/systemUtils.js';
4
+ import { writeFileIfNotExistsWithMessages } from '#src/utils.js';
5
+ import { BaseChatModel } from '@langchain/core/language_models/chat_models';
6
+ import { ChatGroqInput } from '@langchain/groq';
7
+
8
+ // Function to process JSON config and create Groq LLM instance
9
+ export async function processJsonConfig(llmConfig: ChatGroqInput): Promise<BaseChatModel> {
10
+ const groq = await import('@langchain/groq');
11
+ // Use environment variable if available, otherwise use the config value
12
+ const groqApiKey = env.GROQ_API_KEY || llmConfig.apiKey;
13
+ return new groq.ChatGroq({
14
+ ...llmConfig,
15
+ apiKey: groqApiKey,
16
+ model: llmConfig.model || 'deepseek-r1-distill-llama-70b',
17
+ });
18
+ }
19
+
20
+ const jsContent = `/* eslint-disable */
21
+ export async function configure(importFunction, global) {
22
+ // this is going to be imported from sloth dependencies,
23
+ // but can potentially be pulled from global node modules or from this project
24
+ const groq = await importFunction('@langchain/groq');
25
+ return {
26
+ llm: new groq.ChatGroq({
27
+ model: "deepseek-r1-distill-llama-70b", // Check other models available
28
+ apiKey: process.env.GROQ_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
29
+ })
30
+ };
31
+ }
32
+ `;
33
+
34
+ const jsonContent = `{
35
+ "llm": {
36
+ "type": "groq",
37
+ "model": "deepseek-r1-distill-llama-70b",
38
+ "apiKey": "your-api-key-here"
39
+ }
40
+ }`;
41
+
42
+ export function init(configFileName: string): void {
43
+ const currentDir = getCurrentDir();
44
+ path.join(currentDir, configFileName);
45
+
46
+ // Determine which content to use based on file extension
47
+ const content = configFileName.endsWith('.json') ? jsonContent : jsContent;
48
+
49
+ writeFileIfNotExistsWithMessages(configFileName, content);
50
+ displayInfo(
51
+ `You can define GROQ_API_KEY environment variable with your Groq API key and it will work with default model.`
52
+ );
53
+ displayWarning(`You need to edit your ${configFileName} to configure model.`);
54
+ }
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import { displayWarning } from '#src/consoleUtils.js';
3
+ import { getCurrentDir } from '#src/systemUtils.js';
4
+ import { writeFileIfNotExistsWithMessages } from '#src/utils.js';
5
+ import { ChatVertexAIInput } from '@langchain/google-vertexai';
6
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
7
+
8
+ const jsContent = `/* eslint-disable */
9
+ export async function configure(importFunction, global) {
10
+ // this is going to be imported from sloth dependencies,
11
+ // but can potentially be pulled from global node modules or from this project
12
+ const vertexAi = await importFunction('@langchain/google-vertexai');
13
+ return {
14
+ llm: new vertexAi.ChatVertexAI({
15
+ model: "gemini-2.5-pro-preview-05-06", // Consider checking for latest recommended model versions
16
+ // temperature: 0,
17
+ // Other parameters might be relevant depending on Vertex AI API updates
18
+ // The project is not in the interface, but it is in documentation
19
+ // project: 'your-cool-gcloud-project'
20
+ })
21
+ }
22
+ }
23
+ `;
24
+
25
+ const jsonContent = `{
26
+ "llm": {
27
+ "type": "vertexai",
28
+ "model": "gemini-2.5-pro-preview-05-06",
29
+ "temperature": 0
30
+ }
31
+ }`;
32
+
33
+ export function init(configFileName: string): void {
34
+ const currentDir = getCurrentDir();
35
+ path.join(currentDir, configFileName);
36
+
37
+ // Determine which content to use based on file extension
38
+ const content = configFileName.endsWith('.json') ? jsonContent : jsContent;
39
+
40
+ writeFileIfNotExistsWithMessages(configFileName, content);
41
+ displayWarning(
42
+ 'For Google VertexAI you likely to need to do `gcloud auth login` and `gcloud auth application-default login`.'
43
+ );
44
+ }
45
+
46
+ // Function to process JSON config and create VertexAI LLM instance
47
+ export async function processJsonConfig(llmConfig: ChatVertexAIInput): Promise<BaseChatModel> {
48
+ const vertexAi = await import('@langchain/google-vertexai');
49
+ return new vertexAi.ChatVertexAI({
50
+ ...llmConfig,
51
+ model: llmConfig.model || 'gemini-2.5-pro-preview-05-06',
52
+ });
53
+ }