gaunt-sloth-assistant 0.2.2 → 0.3.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 (55) hide show
  1. package/.gsloth.guidelines.md +127 -0
  2. package/.gsloth.review.md +13 -0
  3. package/README.md +1 -1
  4. package/dist/commands/askCommand.js +4 -4
  5. package/dist/commands/askCommand.js.map +1 -1
  6. package/dist/commands/reviewCommand.js +20 -8
  7. package/dist/commands/reviewCommand.js.map +1 -1
  8. package/dist/config.d.ts +8 -4
  9. package/dist/config.js +49 -19
  10. package/dist/config.js.map +1 -1
  11. package/dist/configs/anthropic.d.ts +2 -3
  12. package/dist/configs/anthropic.js.map +1 -1
  13. package/dist/configs/vertexai.d.ts +2 -2
  14. package/dist/configs/vertexai.js.map +1 -1
  15. package/dist/filePathUtils.d.ts +25 -0
  16. package/dist/filePathUtils.js +66 -0
  17. package/dist/filePathUtils.js.map +1 -0
  18. package/dist/index.js +11 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/llmUtils.d.ts +4 -0
  21. package/dist/llmUtils.js +39 -0
  22. package/dist/llmUtils.js.map +1 -0
  23. package/dist/modules/questionAnsweringModule.d.ts +0 -11
  24. package/dist/modules/questionAnsweringModule.js +8 -47
  25. package/dist/modules/questionAnsweringModule.js.map +1 -1
  26. package/dist/modules/reviewModule.d.ts +0 -3
  27. package/dist/modules/reviewModule.js +8 -38
  28. package/dist/modules/reviewModule.js.map +1 -1
  29. package/dist/prompt.d.ts +3 -2
  30. package/dist/prompt.js +28 -12
  31. package/dist/prompt.js.map +1 -1
  32. package/dist/systemUtils.d.ts +10 -0
  33. package/dist/systemUtils.js +34 -0
  34. package/dist/systemUtils.js.map +1 -1
  35. package/dist/utils.d.ts +15 -5
  36. package/dist/utils.js +74 -47
  37. package/dist/utils.js.map +1 -1
  38. package/docs/CONFIGURATION.md +25 -4
  39. package/docs/RELEASE-HOWTO.md +13 -5
  40. package/package.json +1 -1
  41. package/src/commands/askCommand.ts +4 -4
  42. package/src/commands/reviewCommand.ts +21 -9
  43. package/src/config.ts +59 -28
  44. package/src/configs/anthropic.ts +5 -3
  45. package/src/configs/vertexai.ts +2 -2
  46. package/src/filePathUtils.ts +79 -0
  47. package/src/index.ts +12 -3
  48. package/src/llmUtils.ts +54 -0
  49. package/src/modules/questionAnsweringModule.ts +10 -66
  50. package/src/modules/reviewModule.ts +8 -60
  51. package/src/prompt.ts +35 -17
  52. package/src/systemUtils.ts +38 -0
  53. package/src/utils.ts +82 -50
  54. package/.gsloth.preamble.review.md +0 -125
  55. package/UX-RESEARCH.md +0 -78
@@ -1,19 +1,40 @@
1
1
  # Configuration
2
2
 
3
- Populate `.gsloth.preamble.review.md` with your project details and quality requirements.
3
+ Populate `.gsloth.guidelines.md` with your project details and quality requirements.
4
4
  Proper preamble is a paramount for good inference.
5
- Check [.gsloth.preamble.review.md](../.gsloth.preamble.review.md) for example.
5
+ Check [.gsloth.guidelines.md](../.gsloth.guidelines.md) for example.
6
6
 
7
7
  Your project should have the following files in order for gsloth to function:
8
8
  - Configuration file (one of):
9
9
  - `.gsloth.config.js` (JavaScript module)
10
10
  - `.gsloth.config.json` (JSON file)
11
11
  - `.gsloth.config.mjs` (JavaScript module with explicit module extension)
12
- - `.gsloth.preamble.review.md`
12
+ - `.gsloth.guidelines.md`
13
13
 
14
- > Gaunt Sloth currently only functions from the directory which has one of the configuration files and `.gsloth.preamble.review.md`.
14
+ > Gaunt Sloth currently only functions from the directory which has one of the configuration files and `.gsloth.guidelines.md`.
15
15
  > Global configuration to invoke gsloth anywhere is in [ROADMAP](../ROADMAP.md).
16
16
 
17
+ ## Using .gsloth Directory
18
+
19
+ For a tidier project structure, you can create a `.gsloth` directory in your project root. When this directory exists, gsloth will:
20
+
21
+ 1. Write all output files (like responses from commands) to the `.gsloth` directory instead of the project root
22
+ 2. Look for configuration files in `.gsloth/.gsloth-settings/` subdirectory
23
+
24
+ Example directory structure when using the `.gsloth` directory:
25
+
26
+ ```
27
+ .gsloth/.gsloth-settings/.gsloth-config.json
28
+ .gsloth/.gsloth-settings/.gsloth.guidelines.md
29
+ .gsloth/.gsloth-settings/.gsloth.review.md
30
+ .gsloth/gth_2025-05-18_09-34-38_ASK.md
31
+ .gsloth/gth_2025-05-18_22-09-00_PR-22.md
32
+ ```
33
+
34
+ If the `.gsloth` directory doesn't exist, gsloth will continue writing all files to the project root directory as it did previously.
35
+
36
+ **Note:** When initializing a project with an existing `.gsloth` directory, the configuration files will be created in the `.gsloth/.gsloth-settings` directory automatically. There is no automated migration for existing configurations - if you create a `.gsloth` directory after initialization, you'll need to manually move your configuration files into the `.gsloth/.gsloth-settings` directory.
37
+
17
38
  ## Config initialization
18
39
  Configuration can be created with `gsloth init [vendor]` command.
19
40
  Currently, vertexai, anthropic and groq can be configured with `gsloth init [vendor]`.
@@ -1,26 +1,34 @@
1
- Make sure `npm config set git-tag-version true`
1
+ Make sure `npm config set git-tag-version true`
2
2
 
3
3
  For patch, e.g., from 0.0.8 to 0.0.9
4
4
  ```shell
5
- npm version patch
5
+ npm version patch -m "Release notes"
6
6
  git push
7
7
  git push --tags
8
8
  ```
9
9
 
10
10
  For minor, e.g., from 0.0.8 to 0.1.0
11
11
  ```shell
12
- npm version minor
12
+ npm version minor -m "Release notes"
13
13
  git push
14
14
  git push --tags
15
15
  ```
16
+ Type `\` and then Enter to type new line in message.
16
17
 
17
- Note the release version and do
18
+ Note the release version and do
18
19
  ```shell
19
- gh release create --notes "notes"
20
+ gh release create --notes-from-tag
20
21
  ```
22
+ Alternatively `gh release create --notes "notes"`
21
23
 
22
24
  Publish to NPM
23
25
  ```shell
24
26
  npm login
25
27
  npm publish
28
+ ```
29
+
30
+ Delete incidental remote and local tag
31
+ ```shell
32
+ git tag -d v0.3.0
33
+ git push --delete origin v0.3.0
26
34
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaunt-sloth-assistant",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "Andrew Kondratev",
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
- import { readInternalPreamble } from '#src/prompt.js';
2
+ import { readBackstory, readGuidelines } from '#src/prompt.js';
3
3
  import { readMultipleFilesFromCurrentDir } from '#src/utils.js';
4
- import { initConfig } from '#src/config.js';
4
+ import { initConfig, slothContext } from '#src/config.js';
5
5
 
6
6
  interface AskCommandOptions {
7
7
  file?: string[];
@@ -22,13 +22,13 @@ export function askCommand(program: Command): void {
22
22
  )
23
23
  // TODO add option consuming extra message as argument
24
24
  .action(async (message: string, options: AskCommandOptions) => {
25
- const preamble = [readInternalPreamble()];
25
+ const preamble = [readBackstory(), readGuidelines(slothContext.config.projectGuidelines)];
26
26
  const content = [message];
27
27
  if (options.file) {
28
28
  content.push(readMultipleFilesFromCurrentDir(options.file));
29
29
  }
30
30
  await initConfig();
31
31
  const { askQuestion } = await import('#src/modules/questionAnsweringModule.js');
32
- await askQuestion('sloth-ASK', preamble.join('\n'), content.join('\n'));
32
+ await askQuestion('ASK', preamble.join('\n'), content.join('\n'));
33
33
  });
34
34
  }
@@ -1,9 +1,10 @@
1
1
  import { Command, Option } from 'commander';
2
- import { USER_PROJECT_REVIEW_PREAMBLE } from '#src/config.js';
3
- import { readInternalPreamble, readPreamble } from '#src/prompt.js';
2
+ import type { SlothContext } from '#src/config.js';
3
+ import { slothContext } from '#src/config.js';
4
+ import { readBackstory, readGuidelines, readReviewInstructions } from '#src/prompt.js';
4
5
  import { readMultipleFilesFromCurrentDir } from '#src/utils.js';
5
6
  import { displayError } from '#src/consoleUtils.js';
6
- import type { SlothContext } from '#src/config.js';
7
+ import { getStringFromStdin } from '#src/systemUtils.js';
7
8
 
8
9
  /**
9
10
  * Requirements providers. Expected to be in `.providers/` dir
@@ -72,7 +73,11 @@ export function reviewCommand(program: Command, context: SlothContext): void {
72
73
  .action(async (contentId: string | undefined, options: ReviewCommandOptions) => {
73
74
  const { initConfig } = await import('#src/config.js');
74
75
  await initConfig();
75
- const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
76
+ const systemMessage = [
77
+ readBackstory(),
78
+ readGuidelines(slothContext.config.projectGuidelines),
79
+ readReviewInstructions(slothContext.config.projectReviewInstructions),
80
+ ];
76
81
  const content: string[] = [];
77
82
  const requirementsId = options.requirements;
78
83
  const requirementsProvider =
@@ -98,14 +103,15 @@ export function reviewCommand(program: Command, context: SlothContext): void {
98
103
  if (options.file) {
99
104
  content.push(readMultipleFilesFromCurrentDir(options.file));
100
105
  }
101
- if (context.stdin) {
102
- content.push(context.stdin);
106
+ let stringFromStdin = getStringFromStdin();
107
+ if (stringFromStdin) {
108
+ content.push(stringFromStdin);
103
109
  }
104
110
  if (options.message) {
105
111
  content.push(options.message);
106
112
  }
107
113
  const { review } = await import('#src/modules/reviewModule.js');
108
- await review('sloth-DIFF-review', preamble.join('\n'), content.join('\n'));
114
+ await review('REVIEW', systemMessage.join('\n'), content.join('\n'));
109
115
  });
110
116
 
111
117
  program
@@ -134,7 +140,11 @@ export function reviewCommand(program: Command, context: SlothContext): void {
134
140
  const { initConfig } = await import('#src/config.js');
135
141
  await initConfig();
136
142
 
137
- const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
143
+ const systemMessage = [
144
+ readBackstory(),
145
+ readGuidelines(slothContext.config.projectGuidelines),
146
+ readReviewInstructions(slothContext.config.projectReviewInstructions),
147
+ ];
138
148
  const content: string[] = [];
139
149
  const requirementsProvider =
140
150
  options.requirementsProvider ??
@@ -157,7 +167,9 @@ export function reviewCommand(program: Command, context: SlothContext): void {
157
167
  content.push(await get(null, prId));
158
168
 
159
169
  const { review } = await import('#src/modules/reviewModule.js');
160
- await review(`sloth-PR-${prId}-review`, preamble.join('\n'), content.join('\n'));
170
+ // TODO consider including requirements id
171
+ // TODO sanitize prId
172
+ await review(`PR-${prId}`, systemMessage.join('\n'), content.join('\n'));
161
173
  });
162
174
 
163
175
  async function getRequirementsFromProvider(
package/src/config.ts CHANGED
@@ -1,15 +1,17 @@
1
- import path from 'node:path/posix';
2
1
  import { v4 as uuidv4 } from 'uuid';
3
2
  import { displayDebug, displayError, displayInfo, displayWarning } from '#src/consoleUtils.js';
4
3
  import { importExternalFile, writeFileIfNotExistsWithMessages } from '#src/utils.js';
5
4
  import { existsSync, readFileSync } from 'node:fs';
6
- import { error, exit, getCurrentDir } from '#src/systemUtils.js';
7
- import { LanguageModelLike } from '@langchain/core/language_models/base';
5
+ import { error, exit } from '#src/systemUtils.js';
6
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
7
+ import { getGslothConfigWritePath, getGslothConfigReadPath } from '#src/filePathUtils.js';
8
8
 
9
9
  export interface SlothConfig extends BaseSlothConfig {
10
- llm: LanguageModelLike; // FIXME this is still bad keeping instance in config is probably not best choice
10
+ llm: BaseChatModel; // FIXME this is still bad keeping instance in config is probably not best choice
11
11
  contentProvider: string;
12
12
  requirementsProvider: string;
13
+ projectGuidelines: string;
14
+ projectReviewInstructions: string;
13
15
  commands: {
14
16
  pr: {
15
17
  contentProvider: string;
@@ -31,6 +33,8 @@ interface BaseSlothConfig {
31
33
  llm: unknown;
32
34
  contentProvider?: string;
33
35
  requirementsProvider?: string;
36
+ projectGuidelines?: string;
37
+ projectReviewInstructions?: string;
34
38
  commands?: {
35
39
  pr: {
36
40
  contentProvider: string;
@@ -53,7 +57,6 @@ interface BaseSlothConfig {
53
57
  */
54
58
  export interface SlothContext {
55
59
  config: SlothConfig;
56
- stdin: string;
57
60
  session: {
58
61
  configurable: {
59
62
  thread_id: string;
@@ -70,7 +73,8 @@ export const USER_PROJECT_CONFIG_JS = '.gsloth.config.js';
70
73
  export const USER_PROJECT_CONFIG_JSON = '.gsloth.config.json';
71
74
  export const USER_PROJECT_CONFIG_MJS = '.gsloth.config.mjs';
72
75
  export const GSLOTH_BACKSTORY = '.gsloth.backstory.md';
73
- export const USER_PROJECT_REVIEW_PREAMBLE = '.gsloth.preamble.review.md';
76
+ export const PROJECT_GUIDELINES = '.gsloth.guidelines.md';
77
+ export const PROJECT_REVIEW_INSTRUCTIONS = '.gsloth.review.md';
74
78
 
75
79
  export const availableDefaultConfigs = ['vertexai', 'anthropic', 'groq'] as const;
76
80
  export type ConfigType = (typeof availableDefaultConfigs)[number];
@@ -79,6 +83,8 @@ export const DEFAULT_CONFIG: Partial<SlothConfig> = {
79
83
  llm: undefined,
80
84
  contentProvider: 'file',
81
85
  requirementsProvider: 'file',
86
+ projectGuidelines: PROJECT_GUIDELINES,
87
+ projectReviewInstructions: PROJECT_REVIEW_INSTRUCTIONS,
82
88
  commands: {
83
89
  pr: {
84
90
  contentProvider: 'gh',
@@ -98,10 +104,9 @@ export const slothContext = {
98
104
  } as Partial<SlothContext> as SlothContext;
99
105
 
100
106
  export async function initConfig(): Promise<void> {
101
- const currentDir = getCurrentDir();
102
- const jsonConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_JSON);
103
- const jsConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_JS);
104
- const mjsConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_MJS);
107
+ const jsonConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_JSON);
108
+ const jsConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_JS);
109
+ const mjsConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_MJS);
105
110
 
106
111
  // Try loading JSON config file first
107
112
  if (existsSync(jsonConfigPath)) {
@@ -198,7 +203,7 @@ export async function tryJsonConfig(jsonConfig: RawSlothConfig): Promise<void> {
198
203
  try {
199
204
  const configModule = await import(`./configs/${llmType}.js`);
200
205
  if (configModule.processJsonConfig) {
201
- const llm = (await configModule.processJsonConfig(llmConfig)) as LanguageModelLike;
206
+ const llm = (await configModule.processJsonConfig(llmConfig)) as BaseChatModel;
202
207
  slothContext.config = { ...slothContext.config, ...jsonConfig, llm };
203
208
  } else {
204
209
  displayWarning(`Config module for ${llmType} does not have processJsonConfig function.`);
@@ -219,9 +224,7 @@ export async function tryJsonConfig(jsonConfig: RawSlothConfig): Promise<void> {
219
224
  export async function createProjectConfig(configType: string): Promise<void> {
220
225
  displayInfo(`Setting up your project\n`);
221
226
  writeProjectReviewPreamble();
222
- displayWarning(
223
- `Make sure you add as much detail as possible to your ${USER_PROJECT_REVIEW_PREAMBLE}.\n`
224
- );
227
+ displayWarning(`Make sure you add as much detail as possible to your ${PROJECT_GUIDELINES}.\n`);
225
228
 
226
229
  // Check if the config type is in availableDefaultConfigs
227
230
  if (!availableDefaultConfigs.includes(configType as ConfigType)) {
@@ -234,22 +237,51 @@ export async function createProjectConfig(configType: string): Promise<void> {
234
237
 
235
238
  displayInfo(`Creating project config for ${configType}`);
236
239
  const vendorConfig = await import(`./configs/${configType}.js`);
237
- vendorConfig.init(USER_PROJECT_CONFIG_JSON, slothContext);
240
+ vendorConfig.init(getGslothConfigWritePath(USER_PROJECT_CONFIG_JSON), slothContext);
238
241
  }
239
242
 
240
243
  export function writeProjectReviewPreamble(): void {
241
- const currentDir = getCurrentDir();
242
- const reviewPreamblePath = path.join(currentDir, USER_PROJECT_REVIEW_PREAMBLE);
243
- writeFileIfNotExistsWithMessages(
244
- reviewPreamblePath,
245
- 'You are doing generic code review.\n' +
246
- ' Important! Please remind user to prepare proper AI preamble in' +
247
- USER_PROJECT_REVIEW_PREAMBLE +
248
- ' for this project. Use decent amount of ⚠️ to highlight lack of config.' +
249
- ' Explicitly mention `' +
250
- USER_PROJECT_REVIEW_PREAMBLE +
251
- '`.'
252
- );
244
+ const guidelinesPath = getGslothConfigWritePath(PROJECT_GUIDELINES);
245
+ const reviewPath = getGslothConfigWritePath(PROJECT_REVIEW_INSTRUCTIONS);
246
+
247
+ /**
248
+ * Development Guidelines.
249
+ * Please do not remove the warning from this template,
250
+ * it is crucial to describe project specifics to get high-quality reviews.
251
+ */
252
+ const guidelinesTemplate = `# Development Guidelines
253
+
254
+ This file should include general development guidelines for this project.
255
+
256
+ Important! Please remind the user to prepare proper AI guidelines to the ${PROJECT_GUIDELINES} file
257
+ for this project. Use a decent amount of ⚠️ to highlight the lack of config.
258
+ Explicitly mention ${PROJECT_GUIDELINES}.
259
+ `;
260
+
261
+ /**
262
+ * Code Review Guidelines.
263
+ * This code review does not have a warning, like Development Guidelines, because
264
+ * this one should be good to start with.
265
+ */
266
+ const reviewTemplate = `# Code Review Guidelines
267
+
268
+ If requirements were provided, first check that the implemented code implements these requirements.
269
+ Document that you checked the code to implement requirements.
270
+ Reject the change if it appears to implement something else instead of required change.
271
+
272
+ Provide specific feedback on any areas of concern or suggestions for improvement.
273
+ Please categorize your feedback (e.g., "Bug," "Suggestion," "Nitpick").
274
+
275
+ Important! In the end, conclude if you would recommend approving this PR or not.
276
+ Use ✅⚠️❌ symbols to highlight your feedback appropriately.
277
+
278
+ Thank you for your thorough review!
279
+
280
+ Important! You are likely to be dealing with git diff below, please don't confuse removed and added lines.
281
+ `;
282
+
283
+ writeFileIfNotExistsWithMessages(guidelinesPath, guidelinesTemplate);
284
+ writeFileIfNotExistsWithMessages(reviewPath, reviewTemplate);
253
285
  }
254
286
 
255
287
  /**
@@ -261,6 +293,5 @@ export function reset() {
261
293
  delete (slothContext as unknown as Record<string, unknown>)[key];
262
294
  });
263
295
  slothContext.config = DEFAULT_CONFIG as SlothConfig;
264
- slothContext.stdin = '';
265
296
  slothContext.session = { configurable: { thread_id: uuidv4() } };
266
297
  }
@@ -2,14 +2,16 @@ import path from 'node:path';
2
2
  import { displayWarning } from '#src/consoleUtils.js';
3
3
  import { env, getCurrentDir } from '#src/systemUtils.js';
4
4
  import { writeFileIfNotExistsWithMessages } from '#src/utils.js';
5
- import { LanguageModelLike } from '@langchain/core/language_models/base';
6
5
  import type { AnthropicInput } from '@langchain/anthropic';
7
- import type { BaseChatModelParams } from '@langchain/core/language_models/chat_models';
6
+ import type {
7
+ BaseChatModel,
8
+ BaseChatModelParams,
9
+ } from '@langchain/core/language_models/chat_models';
8
10
 
9
11
  // Function to process JSON config and create Anthropic LLM instance
10
12
  export async function processJsonConfig(
11
13
  llmConfig: AnthropicInput & BaseChatModelParams
12
- ): Promise<LanguageModelLike> {
14
+ ): Promise<BaseChatModel> {
13
15
  const anthropic = await import('@langchain/anthropic');
14
16
  // Use environment variable if available, otherwise use the config value
15
17
  const anthropicApiKey = env.ANTHROPIC_API_KEY || llmConfig.apiKey;
@@ -1,9 +1,9 @@
1
- import { LanguageModelLike } from '@langchain/core/language_models/base';
2
1
  import path from 'node:path';
3
2
  import { displayWarning } from '#src/consoleUtils.js';
4
3
  import { getCurrentDir } from '#src/systemUtils.js';
5
4
  import { writeFileIfNotExistsWithMessages } from '#src/utils.js';
6
5
  import { ChatVertexAIInput } from '@langchain/google-vertexai';
6
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
7
7
 
8
8
  const jsContent = `/* eslint-disable */
9
9
  export async function configure(importFunction, global) {
@@ -44,7 +44,7 @@ export function init(configFileName: string): void {
44
44
  }
45
45
 
46
46
  // Function to process JSON config and create VertexAI LLM instance
47
- export async function processJsonConfig(llmConfig: ChatVertexAIInput): Promise<LanguageModelLike> {
47
+ export async function processJsonConfig(llmConfig: ChatVertexAIInput): Promise<BaseChatModel> {
48
48
  const vertexAi = await import('@langchain/google-vertexai');
49
49
  return new vertexAi.ChatVertexAI({
50
50
  ...llmConfig,
@@ -0,0 +1,79 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { getCurrentDir } from '#src/systemUtils.js';
5
+
6
+ const GSLOTH_DIR = '.gsloth';
7
+ const GSLOTH_SETTINGS_DIR = '.gsloth-settings';
8
+
9
+ /**
10
+ * Checks if .gsloth directory exists in the project root
11
+ * @returns Boolean indicating whether .gsloth directory exists
12
+ */
13
+ export function gslothDirExists(): boolean {
14
+ const currentDir = getCurrentDir();
15
+ const gslothDirPath = resolve(currentDir, GSLOTH_DIR);
16
+ return existsSync(gslothDirPath);
17
+ }
18
+
19
+ /**
20
+ * Gets the path where gsloth should write files based on .gsloth directory existence
21
+ * @param filename The filename to append to the path
22
+ * @returns The resolved path where the file should be written
23
+ */
24
+ export function getGslothFilePath(filename: string): string {
25
+ const currentDir = getCurrentDir();
26
+
27
+ if (gslothDirExists()) {
28
+ const gslothDirPath = resolve(currentDir, GSLOTH_DIR);
29
+ return resolve(gslothDirPath, filename);
30
+ }
31
+
32
+ return resolve(currentDir, filename);
33
+ }
34
+
35
+ /**
36
+ * Gets the path where gsloth should write configuration files based on .gsloth directory existence.
37
+ * The main difference from {@link #getGslothConfigReadPath} is that this getGslothConfigWritePath
38
+ * method creates internal settings directory if it does not exist.
39
+ * @param filename The configuration filename
40
+ * @returns The resolved path where the configuration file should be written
41
+ */
42
+ export function getGslothConfigWritePath(filename: string): string {
43
+ const currentDir = getCurrentDir();
44
+
45
+ if (gslothDirExists()) {
46
+ const gslothDirPath = resolve(currentDir, GSLOTH_DIR);
47
+ const gslothSettingsPath = resolve(gslothDirPath, GSLOTH_SETTINGS_DIR);
48
+
49
+ // Create .gsloth-settings directory if it doesn't exist
50
+ if (!existsSync(gslothSettingsPath)) {
51
+ mkdirSync(gslothSettingsPath, { recursive: true });
52
+ }
53
+
54
+ return resolve(gslothSettingsPath, filename);
55
+ }
56
+
57
+ return resolve(currentDir, filename);
58
+ }
59
+
60
+ /**
61
+ * Gets the path where gsloth should look for configuration files based on .gsloth directory existence
62
+ * @param filename The configuration filename to look for
63
+ * @returns The resolved path where the configuration file should be found
64
+ */
65
+ export function getGslothConfigReadPath(filename: string): string {
66
+ const currentDir = getCurrentDir();
67
+
68
+ if (gslothDirExists()) {
69
+ const gslothDirPath = resolve(currentDir, GSLOTH_DIR);
70
+ const gslothSettingsPath = resolve(gslothDirPath, GSLOTH_SETTINGS_DIR);
71
+ const configPath = resolve(gslothSettingsPath, filename);
72
+
73
+ if (existsSync(configPath)) {
74
+ return configPath;
75
+ }
76
+ }
77
+
78
+ return resolve(currentDir, filename);
79
+ }
package/src/index.ts CHANGED
@@ -3,19 +3,28 @@ 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
5
  import { slothContext } from '#src/config.js';
6
- import { getSlothVersion, readStdin } from '#src/utils.js';
6
+ import { getSlothVersion } from '#src/utils.js';
7
+ import { argv, readStdin } from '#src/systemUtils.js';
8
+ import { setVerbose } from '#src/llmUtils.js';
7
9
 
8
10
  const program = new Command();
9
11
 
10
12
  program
11
13
  .name('gsloth')
12
14
  .description('Gaunt Sloth Assistant reviewing your PRs')
13
- .version(getSlothVersion());
15
+ .version(getSlothVersion())
16
+ .option('--verbose', 'Print entire prompt sent to LLM.');
17
+
18
+ // Parse global options before binding any commands
19
+ program.parseOptions(argv);
20
+ if (program.getOptionValue('verbose')) {
21
+ // Set global prompt debug
22
+ setVerbose(true);
23
+ }
14
24
 
15
25
  initCommand(program);
16
26
  reviewCommand(program, slothContext);
17
27
  askCommand(program);
18
-
19
28
  // TODO add general interactive chat command
20
29
 
21
30
  await readStdin(program);
@@ -0,0 +1,54 @@
1
+ import type { Message, State } from '#src/modules/types.js';
2
+ import { AIMessageChunk, HumanMessage, SystemMessage } from '@langchain/core/messages';
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';
6
+
7
+ const llmGlobalSettings = {
8
+ verbose: false,
9
+ };
10
+
11
+ export async function invoke(
12
+ llm: BaseChatModel,
13
+ options: Partial<BaseLanguageModelCallOptions>,
14
+ systemMessage: string,
15
+ prompt: string
16
+ ): Promise<string> {
17
+ if (llmGlobalSettings.verbose) {
18
+ llm.verbose = true;
19
+ }
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);
50
+ }
51
+
52
+ export function setVerbose(debug: boolean) {
53
+ llmGlobalSettings.verbose = debug;
54
+ }
@@ -1,14 +1,9 @@
1
- import type { SlothContext } from '#src/config.js';
2
1
  import { slothContext } from '#src/config.js';
3
2
  import { display, displayError, displaySuccess } from '#src/consoleUtils.js';
4
- import type { Message, ProgressCallback, State } from '#src/modules/types.js';
5
- import { getCurrentDir } from '#src/systemUtils.js';
6
- import { fileSafeLocalDate, ProgressIndicator, toFileSafeString } from '#src/utils.js';
7
- import { BaseChatModel } from '@langchain/core/language_models/chat_models';
8
- import { AIMessageChunk, HumanMessage, SystemMessage } from '@langchain/core/messages';
9
- import { END, MemorySaver, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
3
+ import { getGslothFilePath } from '#src/filePathUtils.js';
4
+ import { generateStandardFileName, ProgressIndicator } from '#src/utils.js';
10
5
  import { writeFileSync } from 'node:fs';
11
- import * as path from 'node:path';
6
+ import { invoke } from '#src/llmUtils.js';
12
7
 
13
8
  /**
14
9
  * Ask a question and get an answer from the LLM
@@ -22,18 +17,17 @@ export async function askQuestion(
22
17
  content: string
23
18
  ): Promise<void> {
24
19
  const progressIndicator = new ProgressIndicator('Thinking.');
25
- const outputContent = await askQuestionInner(
26
- slothContext,
27
- () => progressIndicator.indicate(),
20
+ const outputContent = await invoke(
21
+ slothContext.config.llm,
22
+ slothContext.session,
28
23
  preamble,
29
24
  content
30
25
  );
31
- const filePath = path.resolve(
32
- getCurrentDir(),
33
- toFileSafeString(source) + '-' + fileSafeLocalDate() + '.md'
34
- );
26
+ progressIndicator.stop();
27
+ const filename = generateStandardFileName(source);
28
+ const filePath = getGslothFilePath(filename);
35
29
  display(`\nwriting ${filePath}`);
36
- // TODO highlight LLM output with something like Prism.JS
30
+ // TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
37
31
  display('\n' + outputContent);
38
32
  try {
39
33
  writeFileSync(filePath, outputContent);
@@ -45,53 +39,3 @@ export async function askQuestion(
45
39
  // exit(1);
46
40
  }
47
41
  }
48
-
49
- /**
50
- * Inner function to ask a question and get an answer from the LLM
51
- * @param context - The context object
52
- * @param indicateProgress - Function to indicate progress
53
- * @param preamble - The preamble to send to the LLM
54
- * @param content - The content of the question
55
- * @returns The answer from the LLM
56
- */
57
- export async function askQuestionInner(
58
- context: SlothContext,
59
- indicateProgress: ProgressCallback,
60
- preamble: string,
61
- content: string
62
- ): Promise<string> {
63
- // This node receives the current state (messages) and invokes the LLM
64
- const callModel = async (state: State): Promise<{ messages: AIMessageChunk }> => {
65
- // state.messages will contain the list including the system preamble and user diff
66
- const response = await (context.config.llm as BaseChatModel).invoke(state.messages);
67
- // MessagesAnnotation expects the node to return the new message(s) to be added to the state.
68
- // Wrap the response in an array if it's a single message object.
69
- return { messages: response };
70
- };
71
-
72
- // Define the graph structure with MessagesAnnotation state
73
- const workflow = new StateGraph(MessagesAnnotation)
74
- // Define the node and edge
75
- .addNode('model', callModel)
76
- .addEdge(START, 'model') // Start at the 'model' node
77
- .addEdge('model', END); // End after the 'model' node completes
78
-
79
- // Set up memory (optional but good practice for potential future multi-turn interactions)
80
- const memory = new MemorySaver();
81
-
82
- // Compile the workflow into a runnable app
83
- const app = workflow.compile({ checkpointer: memory });
84
-
85
- // Construct the initial the messages including the preamble as a system message
86
- const messages: Message[] = [new SystemMessage(preamble), new HumanMessage(content)];
87
-
88
- indicateProgress();
89
- // TODO create proper progress indicator for async tasks.
90
- const progress = setInterval(() => indicateProgress(), 1000);
91
- const output = await app.invoke({ messages }, context.session);
92
- clearInterval(progress);
93
- const lastMessage = output.messages[output.messages.length - 1];
94
- return typeof lastMessage.content === 'string'
95
- ? lastMessage.content
96
- : JSON.stringify(lastMessage.content);
97
- }