gaunt-sloth-assistant 0.0.4 → 0.0.8

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.
File without changes
File without changes
File without changes
File without changes
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -76,7 +76,7 @@ Make sure you edit `.gsloth.config.js` and set up your key.
76
76
 
77
77
  ### Further configuration
78
78
 
79
- Currently only vertexai and anthropic can be configured with `gsloth init`.
79
+ Currently vertexai, anthropic and groq can be configured with `gsloth init`.
80
80
 
81
81
  Populate `.gsloth.preamble.review.md` with your project details and quality requirements.
82
82
  Proper preamble is a paramount for good inference.
@@ -98,7 +98,7 @@ export async function configure(importFunction, global) {
98
98
  const anthropic = await importFunction('@langchain/anthropic');
99
99
  return {
100
100
  llm: new anthropic.ChatAnthropic({
101
- apiKey: "sk-ant-api03--YOURAPIHASH", // You should put your API hash here
101
+ apiKey: process.env.ANTHROPIC_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
102
102
  model: "claude-3-5-sonnet-20241022"
103
103
  })
104
104
  };
@@ -127,6 +127,23 @@ export async function configure(importFunction, global) {
127
127
  }
128
128
  ```
129
129
 
130
+ **Example of .gsloth.config.js for Groq**
131
+ VertexAI usually needs `gcloud auth application-default login`
132
+ (or both `gcloud auth login` and `gcloud auth application-default login`) and does not need any separate API keys.
133
+ ```javascript
134
+ export async function configure(importFunction, global) {
135
+ // this is going to be imported from sloth dependencies,
136
+ // but can potentially be pulled from global node modules or from this project
137
+ const groq = await importFunction('@langchain/groq');
138
+ return {
139
+ llm: new groq.ChatGroq({
140
+ model: "deepseek-r1-distill-llama-70b", // Check other models available
141
+ apiKey: process.env.GROQ_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
142
+ })
143
+ };
144
+ }
145
+ ```
146
+
130
147
  The configure function should simply return instance of langchain [chat model](https://v03.api.js.langchain.com/classes/_langchain_core.language_models_chat_models.BaseChatModel.html).
131
148
  See [Langchain documentation](https://js.langchain.com/docs/tutorials/llm_chain/) for more details.
132
149
 
@@ -0,0 +1,18 @@
1
+ Make sure `npm config set git-tag-version true`
2
+
3
+ ```shell
4
+ npm version patch
5
+ git push
6
+ git push --tags
7
+ ```
8
+
9
+ Note the release version and do
10
+ ```shell
11
+ gh release create --generate-notes
12
+ ```
13
+
14
+ Publish to NPM
15
+ ```shell
16
+ npm login
17
+ npm publish
18
+ ```
package/ROADMAP.md CHANGED
@@ -5,6 +5,8 @@
5
5
  Doing the following below and making it work stably should be sufficient to call it version 1.
6
6
 
7
7
  ### Add tests and gain reasonable coverage
8
+ ### Configure eslint for code quality checks
9
+ ### Automate release process
8
10
  ### Add project init command
9
11
  Add a command to init certain model in certain project, for example `gsloth init gemini`
10
12
  or `gsloth init` and select one of the provided options.
package/index.js CHANGED
@@ -39,7 +39,7 @@ program.command('pr')
39
39
  displayError('`gsloth pr` does not expect stdin, use `gsloth review` instead');
40
40
  return;
41
41
  }
42
- displayInfo('Starting review of PR', pr);
42
+ displayInfo(`Starting review of PR ${pr}`);
43
43
  const diff = await getPrDiff(pr);
44
44
  const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
45
45
  const content = [diff];
@@ -55,7 +55,7 @@ program.command('review')
55
55
  .option('-f, --file <file>', 'Input file. Context of this file will be added BEFORE the diff')
56
56
  // TODO add option consuming extra message as argument
57
57
  .action(async (options) => {
58
- if (!slothContext.stdin || options.file) {
58
+ if (!slothContext.stdin && !options.file) {
59
59
  displayError('gsloth review expects stdin with github diff stdin or a file');
60
60
  return
61
61
  }
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "gaunt-sloth-assistant",
3
- "version": "0.0.4",
3
+ "version": "0.0.8",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "Andrew Kondratev",
7
7
  "type": "module",
8
8
  "main": "index.js",
9
+ "repository": "github:andruhon/gaunt-sloth-assistant",
10
+ "engines": {
11
+ "node": ">=22.0.0",
12
+ "npm": ">=10.9.0"
13
+ },
9
14
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1",
11
- "test-run": "node --trace-deprecation index.js ask \"status check\""
15
+ "test": "jasmine"
12
16
  },
13
17
  "bin": {
14
18
  "gsloth": "index.js"
@@ -18,10 +22,15 @@
18
22
  "@langchain/anthropic": "^0.3.17",
19
23
  "@langchain/core": "^0.3.43",
20
24
  "@langchain/google-vertexai": "^0.2.3",
25
+ "@langchain/groq": "^0.2.2",
21
26
  "@langchain/langgraph": "^0.2.64",
22
27
  "@types/node": "^22.14.1",
23
28
  "chalk": "^5.4.1",
24
29
  "commander": "^13.1.0",
25
30
  "uuid": "^11.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "jasmine": "^5.6.0",
34
+ "jest": "^29.7.0"
26
35
  }
27
36
  }
@@ -0,0 +1,22 @@
1
+ import { reviewInner } from '../src/codeReview.js';
2
+ import { slothContext } from '../src/config.js';
3
+ import { FakeListChatModel } from "@langchain/core/utils/testing";
4
+
5
+ describe('codeReview', () => {
6
+
7
+ it('should invoke LLM', async () => {
8
+ // Setup mock for slothContext
9
+ const testContext = {...slothContext,
10
+ config: {
11
+ llm: new FakeListChatModel({
12
+ responses: ["First LLM message", "Second LLM message"],
13
+ })
14
+ }
15
+ };
16
+
17
+ // Test the function
18
+ const output = await reviewInner(testContext, () => {}, 'test-preamble', 'test-diff');
19
+ expect(output).toBe("First LLM message");
20
+ });
21
+
22
+ });
@@ -0,0 +1,14 @@
1
+ export default {
2
+ spec_dir: "spec",
3
+ spec_files: [
4
+ "**/*[sS]pec.?(m)js"
5
+ ],
6
+ helpers: [
7
+ "helpers/**/*.?(m)js"
8
+ ],
9
+ env: {
10
+ stopSpecOnExpectationFailure: false,
11
+ random: true,
12
+ forbidDuplicateNames: true
13
+ }
14
+ }
package/src/codeReview.js CHANGED
@@ -7,17 +7,36 @@ import {
7
7
  } from "@langchain/langgraph";
8
8
  import { writeFileSync } from "node:fs";
9
9
  import path from "node:path";
10
- import {initConfig, slothContext} from "./config.js";
10
+ import { initConfig, slothContext } from "./config.js";
11
11
  import { display, displayError, displaySuccess } from "./consoleUtils.js";
12
- import { fileSafeLocalDate, toFileSafeString } from "./utils.js";
13
-
14
- await initConfig();
12
+ import { fileSafeLocalDate, toFileSafeString, ProgressIndicator } from "./utils.js";
15
13
 
16
14
  export async function review(source, preamble, diff) {
15
+ await initConfig();
16
+ const progressIndicator = new ProgressIndicator("Reviewing.");
17
+ const outputContent = await reviewInner(slothContext, () => progressIndicator.indicate(), preamble, diff);
18
+ const filePath = path.resolve(process.cwd(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
19
+ process.stdout.write("\n");
20
+ display(`writing ${filePath}`);
21
+ process.stdout.write("\n");
22
+ // TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
23
+ display(outputContent);
24
+ try {
25
+ writeFileSync(filePath, outputContent);
26
+ displaySuccess(`This report can be found in ${filePath}`);
27
+ } catch (error) {
28
+ displayError(`Failed to write review to file: ${filePath}`);
29
+ displayError(error.message);
30
+ // Consider if you want to exit or just log the error
31
+ // process.exit(1);
32
+ }
33
+ }
34
+
35
+ export async function reviewInner(context, indicateProgress, preamble, diff) {
17
36
  // This node receives the current state (messages) and invokes the LLM
18
37
  const callModel = async (state) => {
19
38
  // state.messages will contain the list including the system preamble and user diff
20
- const response = await slothContext.config.llm.invoke(state.messages);
39
+ const response = await context.config.llm.invoke(state.messages);
21
40
  // MessagesAnnotation expects the node to return the new message(s) to be added to the state.
22
41
  // Wrap the response in an array if it's a single message object.
23
42
  return { messages: response };
@@ -48,24 +67,11 @@ export async function review(source, preamble, diff) {
48
67
  },
49
68
  ];
50
69
 
51
- process.stdout.write("Reviewing.");
52
- const progress = setInterval(() => process.stdout.write('.'), 1000);
53
- const output = await app.invoke({messages}, slothContext.session);
54
- const filePath = path.resolve(process.cwd(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
55
- display(`writing ${filePath}`);
56
- // FIXME this looks ugly, there should be other way
57
- const outputContent = output.messages[output.messages.length - 1].content;
70
+ indicateProgress();
71
+ // TODO create proper progress indicator for async tasks.
72
+ const progress = setInterval(() => indicateProgress(), 1000);
73
+ const output = await app.invoke({messages}, context.session);
58
74
  clearInterval(progress);
59
- console.log('');
60
- // TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
61
- display(outputContent);
62
- try {
63
- writeFileSync(filePath, outputContent);
64
- displaySuccess(`This report can be found in ${filePath}`);
65
- } catch (error) {
66
- displayError(`Failed to write review to file: ${filePath}`);
67
- displayError(error.message);
68
- // Consider if you want to exit or just log the error
69
- // process.exit(1);
70
- }
75
+ // FIXME this looks ugly, there should be other way
76
+ return output.messages[output.messages.length - 1].content;
71
77
  }
package/src/config.js CHANGED
@@ -1,16 +1,14 @@
1
- import path, {dirname} from "node:path";
1
+ import path from "node:path";
2
2
  import url from "node:url";
3
- import {v4 as uuidv4} from "uuid";
4
- import {display, displayError, displayInfo, displaySuccess, displayWarning} from "./consoleUtils.js";
5
- import {fileURLToPath} from "url";
6
- import {write, writeFileSync, existsSync} from "node:fs";
7
- import {writeFileIfNotExistsWithMessages} from "./utils.js";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import { displayError, displayInfo, displayWarning } from "./consoleUtils.js";
5
+ import { writeFileIfNotExistsWithMessages } from "./utils.js";
8
6
 
9
7
  export const USER_PROJECT_CONFIG_FILE = '.gsloth.config.js'
10
8
  export const SLOTH_INTERNAL_PREAMBLE = '.gsloth.preamble.internal.md';
11
9
  export const USER_PROJECT_REVIEW_PREAMBLE = '.gsloth.preamble.review.md';
12
10
 
13
- export const availableDefaultConfigs = ['vertexai', 'anthropic'];
11
+ export const availableDefaultConfigs = ['vertexai', 'anthropic', 'groq'];
14
12
 
15
13
  export const slothContext = {
16
14
  /**
@@ -29,10 +27,17 @@ export const slothContext = {
29
27
  };
30
28
 
31
29
  export async function initConfig() {
32
- const configFileUrl = url.pathToFileURL(path.join(process.cwd(), USER_PROJECT_CONFIG_FILE));
33
- const {configure} = await import(configFileUrl);
34
- const config = await configure((module) => import(module));
35
- slothContext.config = {...config};
30
+ const configFileUrl = url.pathToFileURL(path.join(process.cwd(), USER_PROJECT_CONFIG_FILE));
31
+ return import(configFileUrl)
32
+ .then((i) => i.configure((module) => import(module)))
33
+ .then((config) => {
34
+ slothContext.config = {...config};
35
+ })
36
+ .catch((e) => {
37
+ console.log(e);
38
+ displayError(`Failed to read config, make sure ${configFileUrl} contains valid JavaScript.`);
39
+ process.exit();
40
+ });
36
41
  }
37
42
 
38
43
  export async function createProjectConfig(configType) {
@@ -11,7 +11,7 @@ export async function configure(importFunction, global) {
11
11
  const anthropic = await importFunction('@langchain/anthropic');
12
12
  return {
13
13
  llm: new anthropic.ChatAnthropic({
14
- apiKey: "sk-ant-api--YOUR_API_HASH", // You should put your API hash here
14
+ apiKey: process.env.ANTHROPIC_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
15
15
  model: "claude-3-5-sonnet-20241022" // Don't forget to check new models availability.
16
16
  })
17
17
  };
@@ -0,0 +1,25 @@
1
+ import {writeFileIfNotExistsWithMessages} from "../utils.js";
2
+ import path from "node:path";
3
+ import {displayInfo, displayWarning} from "../consoleUtils.js";
4
+ import {USER_PROJECT_CONFIG_FILE} from "../config.js";
5
+
6
+ const content = `/* eslint-disable */
7
+ export async function configure(importFunction, global) {
8
+ // this is going to be imported from sloth dependencies,
9
+ // but can potentially be pulled from global node modules or from this project
10
+ const groq = await importFunction('@langchain/groq');
11
+ return {
12
+ llm: new groq.ChatGroq({
13
+ model: "deepseek-r1-distill-llama-70b", // Check other models available
14
+ apiKey: process.env.GROQ_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
15
+ })
16
+ };
17
+ }
18
+ `;
19
+
20
+ export function init(configFileName, context) {
21
+ path.join(context.currentDir, configFileName);
22
+ writeFileIfNotExistsWithMessages(configFileName, content);
23
+ displayInfo(`You can define GROQ_API_KEY environment variable with your Groq API key and it will work with default model.`);
24
+ displayWarning(`You need to edit your ${USER_PROJECT_CONFIG_FILE} to to configure model.`);
25
+ }
File without changes
File without changes
package/src/prompt.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {resolve} from "node:path";
2
2
  import {SLOTH_INTERNAL_PREAMBLE, slothContext} from "./config.js";
3
3
  import {readFileSyncWithMessages, spawnCommand} from "./utils.js";
4
+ import { displayError } from "./consoleUtils.js";
4
5
 
5
6
  export function readInternalPreamble() {
6
7
  const filePath = resolve(slothContext.installDir, SLOTH_INTERNAL_PREAMBLE);
@@ -18,8 +19,15 @@ export function readPreamble(preambleFilename) {
18
19
 
19
20
  /**
20
21
  * This function expects https://cli.github.com/ to be installed and authenticated.
22
+ * It does something like `gh pr diff 42`
21
23
  */
22
24
  export async function getPrDiff(pr) {
23
25
  // TODO makes sense to check if gh is available and authenticated
24
- return spawnCommand('gh', ['pr', 'diff', pr], 'Loading PR diff...', 'Loaded PR diff.');
26
+ try {
27
+ return await spawnCommand('gh', ['pr', 'diff', pr], 'Loading PR diff...', 'Loaded PR diff.');
28
+ } catch (e) {
29
+ displayError(e.toString());
30
+ displayError(`Failed to call "gh pr diff ${pr}", see message above for details.`);
31
+ process.exit();
32
+ }
25
33
  }
File without changes
package/src/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {display, displayError, displaySuccess, displayWarning} from "./consoleUtils.js";
2
2
  import {existsSync, readFileSync, writeFileSync} from "node:fs";
3
- import {slothContext, USER_PROJECT_REVIEW_PREAMBLE} from "./config.js";
3
+ import {slothContext} from "./config.js";
4
4
  import {resolve} from "node:path";
5
5
  import {spawn} from "node:child_process";
6
6
 
@@ -63,7 +63,7 @@ export function readStdin(program) {
63
63
  }
64
64
  });
65
65
  process.stdin.on('end', function() {
66
- console.log('');
66
+ process.stdout.write('.\n');
67
67
  program.parse(process.argv);
68
68
  });
69
69
  }
@@ -71,18 +71,27 @@ export function readStdin(program) {
71
71
 
72
72
  export async function spawnCommand(command, args, progressMessage, successMessage) {
73
73
  return new Promise((resolve, reject) => {
74
- const out = {stdout: ''};
74
+ const out = {stdout: '', stderr: ''};
75
75
  const spawned = spawn(command, args);
76
- spawned.stdout.on('data', async (stdoutChunk) => {
76
+ spawned.stdout.on('data', async (stdoutChunk, dd) => {
77
77
  display(progressMessage);
78
78
  out.stdout += stdoutChunk.toString();
79
79
  });
80
+ spawned.stderr.on('data', (err) => {
81
+ displayError(progressMessage);
82
+ out.stderr += err.toString();
83
+ })
80
84
  spawned.on('error', (err) => {
81
- reject(err);
85
+ reject(err.toString());
82
86
  })
83
87
  spawned.on('close', (code) => {
84
- display(successMessage);
85
- resolve(out.stdout);
88
+ if (code === 0) {
89
+ display(successMessage);
90
+ resolve(out.stdout);
91
+ } else {
92
+ displayError(`Failed to spawn command with code ${code}`);
93
+ reject(out.stdout + ' ' + out.stderr);
94
+ }
86
95
  });
87
96
  });
88
97
  }
@@ -92,3 +101,22 @@ export function getSlothVersion() {
92
101
  const projectJson = readFileSync(jsonPath, { encoding: 'utf8' });
93
102
  return JSON.parse(projectJson).version;
94
103
  }
104
+
105
+
106
+ export class ProgressIndicator {
107
+
108
+ constructor(initialMessage) {
109
+ this.hasBeenCalled = false;
110
+ this.initialMessage = initialMessage;
111
+ }
112
+
113
+ indicate() {
114
+ if (this.hasBeenCalled) {
115
+ process.stdout.write('.');
116
+ } else {
117
+ this.hasBeenCalled = true;
118
+ process.stdout.write(this.initialMessage);
119
+ }
120
+ }
121
+
122
+ }
package/testMessage.txt CHANGED
File without changes