gaunt-sloth-assistant 0.1.2 → 0.1.4

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.
@@ -2,7 +2,7 @@ import {writeFileIfNotExistsWithMessages} from "../utils.js";
2
2
  import path from "node:path";
3
3
  import {displayWarning} from "../consoleUtils.js";
4
4
 
5
- const content = `/* eslint-disable */
5
+ const jsContent = `/* eslint-disable */
6
6
  export async function configure(importFunction, global) {
7
7
  // this is going to be imported from sloth dependencies,
8
8
  // but can potentially be pulled from global node modules or from this project
@@ -19,8 +19,29 @@ export async function configure(importFunction, global) {
19
19
  }
20
20
  `;
21
21
 
22
+ const jsonContent = `{
23
+ "llm": {
24
+ "type": "vertexai",
25
+ "model": "gemini-2.5-pro-exp-03-25",
26
+ "temperature": 0
27
+ }
28
+ }`;
29
+
22
30
  export function init(configFileName, context) {
23
31
  path.join(context.currentDir, configFileName);
32
+
33
+ // Determine which content to use based on file extension
34
+ const content = configFileName.endsWith('.json') ? jsonContent : jsContent;
35
+
24
36
  writeFileIfNotExistsWithMessages(configFileName, content);
25
37
  displayWarning("For Google VertexAI you likely to need to do `gcloud auth login` and `gcloud auth application-default login`.");
26
- }
38
+ }
39
+
40
+ // Function to process JSON config and create VertexAI LLM instance
41
+ export async function processJsonConfig(llmConfig) {
42
+ const vertexAi = await import('@langchain/google-vertexai');
43
+ return new vertexAi.ChatVertexAI({
44
+ ...llmConfig,
45
+ model: llmConfig.model || "gemini-pro"
46
+ });
47
+ }
@@ -1,23 +1,33 @@
1
1
  import chalk from 'chalk';
2
+ import {debug as systemDebug, error as systemError, log} from './systemUtils.js';
2
3
 
3
4
  // TODO it seems like commander supports coloured output, maybe dependency to chalk can be removed
4
5
 
5
6
  export function displayError (message) {
6
- console.error(chalk.red(message));
7
+ systemError(chalk.red(message));
7
8
  }
8
9
 
9
10
  export function displayWarning (message) {
10
- console.error(chalk.yellow(message));
11
+ systemError(chalk.yellow(message));
11
12
  }
12
13
 
13
14
  export function displaySuccess (message) {
14
- console.error(chalk.green(message));
15
+ systemError(chalk.green(message));
15
16
  }
16
17
 
17
18
  export function displayInfo (message) {
18
- console.error(chalk.blue(message));
19
+ systemError(chalk.blue(message));
19
20
  }
20
21
 
21
22
  export function display(message) {
22
- console.log(message);
23
+ log(message);
24
+ }
25
+
26
+ export function displayDebug(message) {
27
+ // TODO make it controlled by config
28
+ if (message?.stack) {
29
+ systemDebug(message.stack);
30
+ } else {
31
+ systemDebug(message);
32
+ }
23
33
  }
@@ -1,15 +1,10 @@
1
- import {
2
- END,
3
- MemorySaver,
4
- MessagesAnnotation,
5
- START,
6
- StateGraph,
7
- } from "@langchain/langgraph";
8
- import { writeFileSync } from "node:fs";
1
+ import {END, MemorySaver, MessagesAnnotation, START, StateGraph,} from "@langchain/langgraph";
2
+ import {writeFileSync} from "node:fs";
9
3
  import * as path from "node:path";
10
- import { slothContext } from "../config.js";
11
- import { display, displayError, displaySuccess } from "../consoleUtils.js";
12
- import { fileSafeLocalDate, toFileSafeString, ProgressIndicator, extractLastMessageContent } from "../utils.js";
4
+ import {slothContext} from "../config.js";
5
+ import {display, displayError, displaySuccess} from "../consoleUtils.js";
6
+ import {extractLastMessageContent, fileSafeLocalDate, ProgressIndicator, toFileSafeString} from "../utils.js";
7
+ import {getCurrentDir} from "../systemUtils.js";
13
8
 
14
9
  /**
15
10
  * Ask a question and get an answer from the LLM
@@ -20,7 +15,7 @@ import { fileSafeLocalDate, toFileSafeString, ProgressIndicator, extractLastMess
20
15
  export async function askQuestion(source, preamble, content) {
21
16
  const progressIndicator = new ProgressIndicator("Thinking.");
22
17
  const outputContent = await askQuestionInner(slothContext, () => progressIndicator.indicate(), preamble, content);
23
- const filePath = path.resolve(process.cwd(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
18
+ const filePath = path.resolve(getCurrentDir(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
24
19
  display(`\nwriting ${filePath}`);
25
20
  // TODO highlight LLM output with something like Prism.JS
26
21
  display('\n' + outputContent);
@@ -30,8 +25,8 @@ export async function askQuestion(source, preamble, content) {
30
25
  } catch (error) {
31
26
  displayError(`Failed to write answer to file: ${filePath}`);
32
27
  displayError(error.message);
33
- // Consider if you want to exit or just log the error
34
- // process.exit(1);
28
+ // TODO Consider if we want to exit or just log the error
29
+ // exit(1);
35
30
  }
36
31
  }
37
32
 
@@ -1,33 +1,28 @@
1
- import {
2
- END,
3
- MemorySaver,
4
- MessagesAnnotation,
5
- START,
6
- StateGraph,
7
- } from "@langchain/langgraph";
8
- import { writeFileSync } from "node:fs";
1
+ import {END, MemorySaver, MessagesAnnotation, START, StateGraph,} from "@langchain/langgraph";
2
+ import {writeFileSync} from "node:fs";
9
3
  import path from "node:path";
10
- import { initConfig, slothContext } from "../config.js";
11
- import { display, displayError, displaySuccess } from "../consoleUtils.js";
12
- import { fileSafeLocalDate, toFileSafeString, ProgressIndicator, extractLastMessageContent } from "../utils.js";
4
+ import {slothContext} from "../config.js";
5
+ import {display, displayDebug, displayError, displaySuccess} from "../consoleUtils.js";
6
+ import {extractLastMessageContent, fileSafeLocalDate, ProgressIndicator, toFileSafeString} from "../utils.js";
7
+ import {getCurrentDir, stdout} from "../systemUtils.js";
13
8
 
14
9
  export async function review(source, preamble, diff) {
15
10
  const progressIndicator = new ProgressIndicator("Reviewing.");
16
11
  const outputContent = await reviewInner(slothContext, () => progressIndicator.indicate(), preamble, diff);
17
- const filePath = path.resolve(process.cwd(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
18
- process.stdout.write("\n");
12
+ const filePath = path.resolve(getCurrentDir(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
13
+ stdout.write("\n");
19
14
  display(`writing ${filePath}`);
20
- process.stdout.write("\n");
15
+ stdout.write("\n");
21
16
  // TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
22
17
  display(outputContent);
23
18
  try {
24
19
  writeFileSync(filePath, outputContent);
25
20
  displaySuccess(`This report can be found in ${filePath}`);
26
21
  } catch (error) {
22
+ displayDebug(error);
27
23
  displayError(`Failed to write review to file: ${filePath}`);
28
- displayError(error.message);
29
24
  // Consider if you want to exit or just log the error
30
- // process.exit(1);
25
+ // exit(1);
31
26
  }
32
27
  }
33
28
 
package/src/prompt.js CHANGED
@@ -2,10 +2,11 @@ import {resolve} from "node:path";
2
2
  import {SLOTH_INTERNAL_PREAMBLE, slothContext} from "./config.js";
3
3
  import {readFileSyncWithMessages, spawnCommand} from "./utils.js";
4
4
  import { displayError } from "./consoleUtils.js";
5
+ import { exit } from "./systemUtils.js";
5
6
 
6
7
  export function readInternalPreamble() {
7
8
  const filePath = resolve(slothContext.installDir, SLOTH_INTERNAL_PREAMBLE);
8
- return readFileSyncWithMessages(filePath, "Error reading internal preamble file at:")
9
+ return readFileSyncWithMessages(filePath, "Error reading internal preamble file at:");
9
10
  }
10
11
 
11
12
  export function readPreamble(preambleFilename) {
@@ -14,7 +15,7 @@ export function readPreamble(preambleFilename) {
14
15
  filePath,
15
16
  "Error reading preamble file at:",
16
17
  "Consider running `gsloth init` to set up your project. Check `gsloth init --help` to see options."
17
- )
18
+ );
18
19
  }
19
20
 
20
21
  /**
@@ -28,6 +29,6 @@ export async function getPrDiff(pr) {
28
29
  } catch (e) {
29
30
  displayError(e.toString());
30
31
  displayError(`Failed to call "gh pr diff ${pr}", see message above for details.`);
31
- process.exit();
32
+ exit();
32
33
  }
33
34
  }
@@ -0,0 +1,19 @@
1
+ import {resolve} from "node:path";
2
+ import {slothContext} from "../config.js";
3
+ import {display} from "../consoleUtils.js";
4
+ import {readFileSyncWithMessages} from "../utils.js";
5
+
6
+ /**
7
+ * Reads the text file from current dir
8
+ * @param _ config (unused in this provider)
9
+ * @param fileName
10
+ * @returns {string} file contents
11
+ */
12
+ export function get(_, fileName) {
13
+ if (!fileName) {
14
+ return null;
15
+ }
16
+ const filePath = resolve(slothContext.currentDir, fileName);
17
+ display(`Reading file ${fileName}...`);
18
+ return readFileSyncWithMessages(filePath);
19
+ }
@@ -4,8 +4,8 @@ import {displayWarning} from "../consoleUtils.js";
4
4
  export async function get(_, pr) {
5
5
  // TODO makes sense to check if gh is available and authenticated
6
6
  if (!pr) {
7
- displayWarning("No PR provided, skipping PR diff fetching.")
7
+ displayWarning("No PR provided, skipping PR diff fetching.");
8
8
  return "";
9
9
  }
10
10
  return spawnCommand('gh', ['pr', 'diff', pr], 'Loading PR diff...', 'Loaded PR diff.');
11
- }
11
+ }
@@ -1,8 +1,9 @@
1
1
  import {display, displayWarning} from "../consoleUtils.js";
2
+ import { env } from "../systemUtils.js";
2
3
 
3
4
  export async function get(config, prId) {
4
5
  const issueData = await getJiraIssue(config, prId);
5
- return `## ${prId} Requirements - ${issueData.fields?.summary}\n\n${issueData.fields?.description}`
6
+ return `## ${prId} Requirements - ${issueData.fields?.summary}\n\n${issueData.fields?.description}`;
6
7
  }
7
8
 
8
9
  /**
@@ -18,15 +19,23 @@ export async function get(config, prId) {
18
19
  * @throws {Error} Throws an error if the fetch fails, authentication is wrong, the issue is not found, or the response status is not OK.
19
20
  */
20
21
  async function getJiraIssue(config, jiraKey) {
21
- const { username, token, baseUrl } = config;
22
+ const {username, baseUrl} = config;
22
23
  if (!jiraKey) {
23
- displayWarning("No jiraKey provided, skipping Jira issue fetching.")
24
+ displayWarning("No jiraKey provided, skipping Jira issue fetching.");
24
25
  return "";
25
26
  }
27
+ const token = env.JIRA_LEGACY_API_TOKEN ?? config?.token;
28
+
29
+ if (!token) {
30
+ throw new Error(
31
+ 'Missing JIRA Legacy API token. ' +
32
+ 'The legacy token can be defined as JIRA_LEGACY_API_TOKEN environment variable or as "token" in config.'
33
+ );
34
+ }
26
35
 
27
36
  // Validate essential inputs
28
- if (!username || !token || !baseUrl) {
29
- throw new Error('Missing required parameters in config (username, token, baseUrl) or missing jiraKey.');
37
+ if (!username || !baseUrl) {
38
+ throw new Error('Missing required parameters in config: username or baseUrl');
30
39
  }
31
40
 
32
41
  // Ensure baseUrl doesn't end with a slash to avoid double slashes in the URL
@@ -49,33 +58,27 @@ async function getJiraIssue(config, jiraKey) {
49
58
 
50
59
  display(`Fetching Jira issue: ${apiUrl}`);
51
60
 
52
- try {
53
- const response = await fetch(apiUrl, {
54
- method: 'GET',
55
- headers: headers,
56
- });
61
+ const response = await fetch(apiUrl, {
62
+ method: 'GET',
63
+ headers: headers,
64
+ });
57
65
 
58
- // Check if the response status code indicates success (e.g., 200 OK)
59
- if (!response.ok) {
60
- let errorBody = 'Could not read error body.';
61
- try {
62
- // Attempt to get more details from the response body for non-OK statuses
63
- errorBody = await response.text();
64
- } catch (e) {
65
- // Silent fail - we already have a generic error message
66
- }
67
- // Throw a detailed error including status, status text, URL, and body if available
68
- throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}. URL: ${apiUrl}. Response Body: ${errorBody}`);
66
+ // Check if the response status code indicates success (e.g., 200 OK)
67
+ if (!response.ok) {
68
+ let errorBody = 'Could not read error body.';
69
+ try {
70
+ // Attempt to get more details from the response body for non-OK statuses
71
+ errorBody = await response.text();
72
+ // eslint-disable-next-line no-unused-vars
73
+ } catch (e) {
74
+ // Silent fail - we already have a generic error message
69
75
  }
76
+ // Throw a detailed error including status, status text, URL, and body if available
77
+ throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}. URL: ${apiUrl}. Response Body: ${errorBody}`);
78
+ }
70
79
 
71
- // Parse the JSON response body if the request was successful
72
- const issueData = await response.json();
73
- return issueData;
80
+ // Parse the JSON response body if the request was successful
81
+ const issueData = await response.json();
82
+ return issueData;
74
83
 
75
- } catch (error) {
76
- // Handle network errors (e.g., DNS resolution failure, connection refused)
77
- // or errors thrown from the non-OK response check above
78
- // Re-throw the error so the caller can handle it appropriately
79
- throw error;
80
- }
81
84
  }
@@ -3,4 +3,4 @@
3
3
  */
4
4
  export async function get(_, text) {
5
5
  return text;
6
- }
6
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * This file contains all system functions and objects that are globally available
3
+ * but not imported directly, such as process.stdin, process.stdout, process.argv,
4
+ * process.env, process.cwd(), process.exit(), etc.
5
+ *
6
+ * By centralizing these in one file, we improve testability and make it easier
7
+ * to mock these dependencies in tests.
8
+ */
9
+
10
+ const innerState = {
11
+ installDir: undefined
12
+ };
13
+
14
+ /* eslint-disable no-undef */
15
+ // Process-related functions and objects
16
+ export const getCurrentDir = () => process.cwd();
17
+ export const getInstallDir = () => innerState.installDir;
18
+ export const exit = (code) => process.exit(code);
19
+ export const stdin = process.stdin;
20
+ export const stdout = process.stdout;
21
+ export const argv = process.argv;
22
+ export const env = process.env;
23
+
24
+ export const setInstallDir = (dir) => innerState.installDir = dir;
25
+
26
+ // Console-related functions
27
+ export const log = (message) => console.log(message);
28
+ export const error = (message) => console.error(message);
29
+ export const warn = (message) => console.warn(message);
30
+ export const info = (message) => console.info(message);
31
+ export const debug = (message) => console.debug(message);
32
+ /* eslint-enable no-undef */
package/src/utils.js CHANGED
@@ -3,6 +3,8 @@ import {existsSync, readFileSync, writeFileSync} from "node:fs";
3
3
  import {slothContext} from "./config.js";
4
4
  import {resolve} from "node:path";
5
5
  import {spawn} from "node:child_process";
6
+ import {exit, stdin, stdout, argv} from "./systemUtils.js";
7
+ import url from "node:url";
6
8
 
7
9
  export function toFileSafeString(string) {
8
10
  return string.replace(/[^A-Za-z0-9]/g, '-');
@@ -45,27 +47,28 @@ export function readFileSyncWithMessages(filePath, errorMessageIn, noFileMessage
45
47
  } else {
46
48
  displayError(error.message);
47
49
  }
48
- process.exit(1); // Exit gracefully after error
50
+ exit(1); // Exit gracefully after error
49
51
  }
50
52
  }
51
53
 
52
54
  export function readStdin(program) {
53
55
  return new Promise((resolve) => {
54
- if(process.stdin.isTTY) {
56
+ // TODO use progress indicator here
57
+ if(stdin.isTTY) {
55
58
  program.parseAsync().then(resolve);
56
59
  } else {
57
60
  // Support piping diff into gsloth
58
- process.stdout.write('reading STDIN.');
59
- process.stdin.on('readable', function() {
61
+ stdout.write('reading STDIN.');
62
+ stdin.on('readable', function() {
60
63
  const chunk = this.read();
61
- process.stdout.write('.');
64
+ stdout.write('.');
62
65
  if (chunk !== null) {
63
66
  slothContext.stdin += chunk;
64
67
  }
65
68
  });
66
- process.stdin.on('end', function() {
67
- process.stdout.write('.\n');
68
- program.parseAsync(process.argv).then(resolve);
69
+ stdin.on('end', function() {
70
+ stdout.write('.\n');
71
+ program.parseAsync(argv).then(resolve);
69
72
  });
70
73
  }
71
74
  });
@@ -76,17 +79,17 @@ export async function spawnCommand(command, args, progressMessage, successMessag
76
79
  // TODO use progress indicator
77
80
  const out = {stdout: '', stderr: ''};
78
81
  const spawned = spawn(command, args);
79
- spawned.stdout.on('data', async (stdoutChunk, dd) => {
82
+ spawned.stdout.on('data', async (stdoutChunk) => {
80
83
  display(progressMessage);
81
84
  out.stdout += stdoutChunk.toString();
82
85
  });
83
86
  spawned.stderr.on('data', (err) => {
84
87
  display(progressMessage);
85
88
  out.stderr += err.toString();
86
- })
89
+ });
87
90
  spawned.on('error', (err) => {
88
91
  reject(err.toString());
89
- })
92
+ });
90
93
  spawned.on('close', (code) => {
91
94
  if (code === 0) {
92
95
  display(successMessage);
@@ -115,10 +118,10 @@ export class ProgressIndicator {
115
118
 
116
119
  indicate() {
117
120
  if (this.hasBeenCalled) {
118
- process.stdout.write('.');
121
+ stdout.write('.');
119
122
  } else {
120
123
  this.hasBeenCalled = true;
121
- process.stdout.write(this.initialMessage);
124
+ stdout.write(this.initialMessage);
122
125
  }
123
126
  }
124
127
 
@@ -135,3 +138,36 @@ export function extractLastMessageContent(output) {
135
138
  }
136
139
  return output.messages[output.messages.length - 1].content;
137
140
  }
141
+
142
+ /**
143
+ * Dynamically imports a module from a file path from the outside of the installation dir
144
+ * @param {string} filePath - The path to the file to import
145
+ * @returns {Promise} A promise that resolves to the imported module
146
+ */
147
+ export function importExternalFile(filePath) {
148
+ const configFileUrl = url.pathToFileURL(filePath);
149
+ return import(configFileUrl);
150
+ }
151
+
152
+ /**
153
+ * Alias for importExternalFile for backward compatibility with tests
154
+ * @param {string} filePath - The path to the file to import
155
+ * @returns {Promise} A promise that resolves to the imported module
156
+ */
157
+ export const importFromFilePath = importExternalFile;
158
+
159
+ /**
160
+ * Reads multiple files from the current directory and returns their contents
161
+ * @param {string[]} fileNames - Array of file names to read
162
+ * @returns {string} Combined content of all files with proper formatting
163
+ */
164
+ export function readMultipleFilesFromCurrentDir(fileNames) {
165
+ if (!Array.isArray(fileNames)) {
166
+ return readFileFromCurrentDir(fileNames);
167
+ }
168
+
169
+ return fileNames.map(fileName => {
170
+ const content = readFileFromCurrentDir(fileName);
171
+ return `${fileName}:\n\`\`\`\n${content}\n\`\`\``;
172
+ }).join('\n\n');
173
+ }