gaunt-sloth-assistant 0.2.2 → 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 (57) hide show
  1. package/{.gsloth.preamble.review.md → .gsloth.guidelines.md} +0 -8
  2. package/.gsloth.review.md +7 -0
  3. package/README.md +1 -1
  4. package/dist/commands/askCommand.js +5 -4
  5. package/dist/commands/askCommand.js.map +1 -1
  6. package/dist/commands/reviewCommand.js +22 -8
  7. package/dist/commands/reviewCommand.js.map +1 -1
  8. package/dist/config.d.ts +8 -4
  9. package/dist/config.js +8 -6
  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/index.js +11 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/llmUtils.d.ts +4 -0
  18. package/dist/llmUtils.js +39 -0
  19. package/dist/llmUtils.js.map +1 -0
  20. package/dist/modules/questionAnsweringModule.d.ts +0 -11
  21. package/dist/modules/questionAnsweringModule.js +4 -43
  22. package/dist/modules/questionAnsweringModule.js.map +1 -1
  23. package/dist/modules/reviewModule.d.ts +0 -3
  24. package/dist/modules/reviewModule.js +3 -34
  25. package/dist/modules/reviewModule.js.map +1 -1
  26. package/dist/prompt.d.ts +3 -2
  27. package/dist/prompt.js +25 -12
  28. package/dist/prompt.js.map +1 -1
  29. package/dist/systemUtils.d.ts +10 -0
  30. package/dist/systemUtils.js +34 -0
  31. package/dist/systemUtils.js.map +1 -1
  32. package/dist/utils.d.ts +5 -5
  33. package/dist/utils.js +45 -39
  34. package/dist/utils.js.map +1 -1
  35. package/docs/CONFIGURATION.md +4 -4
  36. package/docs/RELEASE-HOWTO.md +6 -0
  37. package/gth-ASK-2025-05-16T14-11-39.md +3 -0
  38. package/gth-ASK-2025-05-16T14-18-27.md +3 -0
  39. package/gth-ASK-2025-05-16T14-18-56.md +1 -0
  40. package/gth-ASK-2025-05-16T14-41-20.md +3 -0
  41. package/gth-ASK-2025-05-16T14-43-31.md +51 -0
  42. package/gth-ASK-2025-05-16T16-05-52.md +62 -0
  43. package/gth-DIFF-review-2025-05-16T16-07-53.md +56 -0
  44. package/gth-DIFF-review-2025-05-16T16-18-55.md +292 -0
  45. package/package.json +1 -1
  46. package/src/commands/askCommand.ts +5 -4
  47. package/src/commands/reviewCommand.ts +23 -9
  48. package/src/config.ts +15 -12
  49. package/src/configs/anthropic.ts +5 -3
  50. package/src/configs/vertexai.ts +2 -2
  51. package/src/index.ts +12 -3
  52. package/src/llmUtils.ts +54 -0
  53. package/src/modules/questionAnsweringModule.ts +6 -59
  54. package/src/modules/reviewModule.ts +3 -53
  55. package/src/prompt.ts +32 -17
  56. package/src/systemUtils.ts +38 -0
  57. package/src/utils.ts +49 -42
@@ -1,14 +1,10 @@
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
3
  import { getCurrentDir } from '#src/systemUtils.js';
6
4
  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';
10
5
  import { writeFileSync } from 'node:fs';
11
6
  import * as path from 'node:path';
7
+ import { invoke } from '#src/llmUtils.js';
12
8
 
13
9
  /**
14
10
  * Ask a question and get an answer from the LLM
@@ -22,18 +18,19 @@ export async function askQuestion(
22
18
  content: string
23
19
  ): Promise<void> {
24
20
  const progressIndicator = new ProgressIndicator('Thinking.');
25
- const outputContent = await askQuestionInner(
26
- slothContext,
27
- () => progressIndicator.indicate(),
21
+ const outputContent = await invoke(
22
+ slothContext.config.llm,
23
+ slothContext.session,
28
24
  preamble,
29
25
  content
30
26
  );
27
+ progressIndicator.stop();
31
28
  const filePath = path.resolve(
32
29
  getCurrentDir(),
33
30
  toFileSafeString(source) + '-' + fileSafeLocalDate() + '.md'
34
31
  );
35
32
  display(`\nwriting ${filePath}`);
36
- // TODO highlight LLM output with something like Prism.JS
33
+ // TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
37
34
  display('\n' + outputContent);
38
35
  try {
39
36
  writeFileSync(filePath, outputContent);
@@ -45,53 +42,3 @@ export async function askQuestion(
45
42
  // exit(1);
46
43
  }
47
44
  }
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
- }
@@ -1,23 +1,15 @@
1
- import type { SlothContext } from '#src/config.js';
2
1
  import { slothContext } from '#src/config.js';
3
2
  import { display, displayDebug, displayError, displaySuccess } from '#src/consoleUtils.js';
4
- import type { Message, ProgressCallback, State } from '#src/modules/types.js';
5
3
  import { getCurrentDir, stdout } from '#src/systemUtils.js';
6
4
  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';
10
5
  import { writeFileSync } from 'node:fs';
11
6
  import path from 'node:path';
7
+ import { invoke } from '#src/llmUtils.js';
12
8
 
13
9
  export async function review(source: string, preamble: string, diff: string): Promise<void> {
14
10
  const progressIndicator = new ProgressIndicator('Reviewing.');
15
- const outputContent = await reviewInner(
16
- slothContext,
17
- () => progressIndicator.indicate(),
18
- preamble,
19
- diff
20
- );
11
+ const outputContent = await invoke(slothContext.config.llm, slothContext.session, preamble, diff);
12
+ progressIndicator.stop();
21
13
  const filePath = path.resolve(
22
14
  getCurrentDir(),
23
15
  toFileSafeString(source) + '-' + fileSafeLocalDate() + '.md'
@@ -37,45 +29,3 @@ export async function review(source: string, preamble: string, diff: string): Pr
37
29
  // exit(1);
38
30
  }
39
31
  }
40
-
41
- export async function reviewInner(
42
- context: SlothContext,
43
- indicateProgress: ProgressCallback,
44
- preamble: string,
45
- diff: string
46
- ): Promise<string> {
47
- // This node receives the current state (messages) and invokes the LLM
48
- const callModel = async (state: State): Promise<{ messages: AIMessageChunk }> => {
49
- // state.messages will contain the list including the system preamble and user diff
50
- const response = await (context.config.llm as BaseChatModel).invoke(state.messages);
51
- // MessagesAnnotation expects the node to return the new message(s) to be added to the state.
52
- // Wrap the response in an array if it's a single message object.
53
- return { messages: response };
54
- };
55
-
56
- // Define the graph structure with MessagesAnnotation state
57
- const workflow = new StateGraph(MessagesAnnotation)
58
- // Define the node and edge
59
- .addNode('model', callModel)
60
- .addEdge(START, 'model') // Start at the 'model' node
61
- .addEdge('model', END); // End after the 'model' node completes
62
-
63
- // Set up memory (optional but good practice for potential future multi-turn interactions)
64
- const memory = new MemorySaver(); // TODO extract to config
65
-
66
- // Compile the workflow into a runnable app
67
- const app = workflow.compile({ checkpointer: memory });
68
-
69
- // Construct the initial the messages including the preamble as a system message
70
- const messages: Message[] = [new SystemMessage(preamble), new HumanMessage(diff)];
71
-
72
- indicateProgress();
73
- // TODO create proper progress indicator for async tasks.
74
- const progress = setInterval(() => indicateProgress(), 1000);
75
- const output = await app.invoke({ messages }, context.session);
76
- clearInterval(progress);
77
- const lastMessage = output.messages[output.messages.length - 1];
78
- return typeof lastMessage.content === 'string'
79
- ? lastMessage.content
80
- : JSON.stringify(lastMessage.content);
81
- }
package/src/prompt.ts CHANGED
@@ -1,25 +1,40 @@
1
- import { resolve } from 'node:path';
2
- import { GSLOTH_BACKSTORY } from '#src/config.js';
3
- import { readFileSyncWithMessages, spawnCommand } from '#src/utils.js';
1
+ import {
2
+ readFileFromCurrentDir,
3
+ readFileFromCurrentOrInstallDir,
4
+ spawnCommand,
5
+ } from '#src/utils.js';
4
6
  import { displayError } from '#src/consoleUtils.js';
5
- import { exit, getCurrentDir, getInstallDir } from '#src/systemUtils.js';
7
+ import { exit } from '#src/systemUtils.js';
8
+ import { GSLOTH_BACKSTORY } from '#src/config.js';
6
9
 
7
- export function readInternalPreamble(): string {
8
- const installDir = getInstallDir();
9
- const filePath = resolve(installDir, GSLOTH_BACKSTORY);
10
- return readFileSyncWithMessages(filePath, 'Error reading internal preamble file at:') || '';
10
+ export function readBackstory(): string {
11
+ return readFileFromCurrentOrInstallDir(GSLOTH_BACKSTORY, true);
11
12
  }
12
13
 
13
- export function readPreamble(preambleFilename: string): string {
14
- const currentDir = getCurrentDir();
15
- const filePath = resolve(currentDir, preambleFilename);
16
- return (
17
- readFileSyncWithMessages(
18
- filePath,
19
- 'Error reading preamble file at:',
14
+ export function readGuidelines(guidelinesFilename: string): string {
15
+ try {
16
+ return readFileFromCurrentDir(guidelinesFilename);
17
+ } catch (error) {
18
+ displayError(
20
19
  'Consider running `gsloth init` to set up your project. Check `gsloth init --help` to see options.'
21
- ) || ''
22
- );
20
+ );
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ export function readReviewInstructions(reviewInstructions: string): string {
26
+ return readConfigPromptFile(reviewInstructions);
27
+ }
28
+
29
+ function readConfigPromptFile(guidelinesFilename: string): string {
30
+ try {
31
+ return readFileFromCurrentOrInstallDir(guidelinesFilename);
32
+ } catch (error) {
33
+ displayError(
34
+ 'Consider running `gsloth init` to set up your project. Check `gsloth init --help` to see options.'
35
+ );
36
+ throw error;
37
+ }
23
38
  }
24
39
 
25
40
  /**
@@ -1,5 +1,7 @@
1
1
  import { dirname, resolve } from 'node:path';
2
2
  import { fileURLToPath } from 'url';
3
+ import { Command } from 'commander';
4
+ import { ProgressIndicator } from '#src/utils.js';
3
5
 
4
6
  /**
5
7
  * This file contains all system functions and objects that are globally available
@@ -12,10 +14,12 @@ import { fileURLToPath } from 'url';
12
14
 
13
15
  interface InnerState {
14
16
  installDir: string | null | undefined;
17
+ stringFromStdin: string;
15
18
  }
16
19
 
17
20
  const innerState: InnerState = {
18
21
  installDir: undefined,
22
+ stringFromStdin: '',
19
23
  };
20
24
 
21
25
  // Process-related functions and objects
@@ -26,6 +30,12 @@ export const getInstallDir = (): string => {
26
30
  }
27
31
  throw new Error('Install directory not set');
28
32
  };
33
+ /**
34
+ * Cached string from stdin. Should only be called after readStdin completes execution.
35
+ */
36
+ export const getStringFromStdin = (): string => {
37
+ return innerState.stringFromStdin;
38
+ };
29
39
  export const exit = (code?: number): never => process.exit(code || 0);
30
40
  export const stdin = process.stdin;
31
41
  export const stdout = process.stdout;
@@ -44,6 +54,34 @@ export const setEntryPoint = (indexJs: string): void => {
44
54
  innerState.installDir = resolve(dirPath);
45
55
  };
46
56
 
57
+ /**
58
+ * Asynchronously reads the stdin and stores it as a string,
59
+ * it can later be retrieved with getStringFromStdin.
60
+ */
61
+ export function readStdin(program: Command): Promise<void> {
62
+ return new Promise((resolvePromise) => {
63
+ if (stdin.isTTY) {
64
+ program.parseAsync().then(() => resolvePromise());
65
+ } else {
66
+ // Support piping diff into gsloth
67
+ const progressIndicator = new ProgressIndicator('reading STDIN', true);
68
+
69
+ stdin.on('readable', function (this: NodeJS.ReadStream) {
70
+ const chunk = this.read();
71
+ progressIndicator.indicate();
72
+ if (chunk !== null) {
73
+ const chunkStr = chunk.toString('utf8');
74
+ innerState.stringFromStdin = innerState.stringFromStdin + chunkStr;
75
+ }
76
+ });
77
+
78
+ stdin.on('end', function () {
79
+ program.parseAsync(argv).then(() => resolvePromise());
80
+ });
81
+ }
82
+ });
83
+ }
84
+
47
85
  // Console-related functions
48
86
  export const log = (message: string): void => console.log(message);
49
87
  export const error = (message: string): void => console.error(message);
package/src/utils.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { display, displayError, displaySuccess, displayWarning } from '#src/consoleUtils.js';
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { SlothConfig, slothContext } from '#src/config.js';
3
+ import { SlothConfig } from '#src/config.js';
4
4
  import { resolve } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
- import { exit, stdin, stdout, argv, getCurrentDir, getInstallDir } from '#src/systemUtils.js';
6
+ import { getCurrentDir, getInstallDir, stdout } from '#src/systemUtils.js';
7
7
  import url from 'node:url';
8
- import { Command } from 'commander';
9
8
 
10
9
  export function toFileSafeString(string: string): string {
11
10
  return string.replace(/[^A-Za-z0-9]/g, '-');
@@ -24,10 +23,34 @@ export function fileSafeLocalDate(): string {
24
23
  export function readFileFromCurrentDir(fileName: string): string {
25
24
  const currentDir = getCurrentDir();
26
25
  const filePath = resolve(currentDir, fileName);
27
- display(`Reading file ${fileName}...`);
26
+ display(`Reading file ${filePath}...`);
28
27
  return readFileSyncWithMessages(filePath);
29
28
  }
30
29
 
30
+ export function readFileFromCurrentOrInstallDir(filePath: string, silentCurrent?: boolean): string {
31
+ const currentDir = getCurrentDir();
32
+ const currentFilePath = resolve(currentDir, filePath);
33
+ if (!silentCurrent) {
34
+ display(`Reading file ${currentFilePath}...`);
35
+ }
36
+
37
+ try {
38
+ return readFileSync(currentFilePath, { encoding: 'utf8' });
39
+ } catch (_error) {
40
+ if (!silentCurrent) {
41
+ display(`The ${currentFilePath} not found or can\'t be read, trying install directory...`);
42
+ }
43
+ const installDir = getInstallDir();
44
+ const installFilePath = resolve(installDir, filePath);
45
+ try {
46
+ return readFileSync(installFilePath, { encoding: 'utf8' });
47
+ } catch (readFromInstallDirError) {
48
+ displayError(`The ${installFilePath} not found or can\'t be read.`);
49
+ throw readFromInstallDirError;
50
+ }
51
+ }
52
+ }
53
+
31
54
  export function writeFileIfNotExistsWithMessages(filePath: string, content: string): void {
32
55
  display(`checking ${filePath} existence`);
33
56
  if (!existsSync(filePath)) {
@@ -53,36 +76,10 @@ export function readFileSyncWithMessages(
53
76
  } else {
54
77
  displayError((error as Error).message);
55
78
  }
56
- exit(1); // Exit gracefully after error
57
- throw error; // This line will never be reached due to exit(1), but satisfies TypeScript
79
+ throw error;
58
80
  }
59
81
  }
60
82
 
61
- export function readStdin(program: Command): Promise<void> {
62
- return new Promise((resolvePromise) => {
63
- if (stdin.isTTY) {
64
- program.parseAsync().then(() => resolvePromise());
65
- } else {
66
- // Support piping diff into gsloth
67
- const progressIndicator = new ProgressIndicator('reading STDIN');
68
- progressIndicator.indicate();
69
-
70
- stdin.on('readable', function (this: NodeJS.ReadStream) {
71
- const chunk = this.read();
72
- progressIndicator.indicate();
73
- if (chunk !== null) {
74
- const chunkStr = chunk.toString('utf8');
75
- (slothContext as { stdin: string }).stdin = slothContext.stdin + chunkStr;
76
- }
77
- });
78
-
79
- stdin.on('end', function () {
80
- program.parseAsync(argv).then(() => resolvePromise());
81
- });
82
- }
83
- });
84
- }
85
-
86
83
  interface SpawnOutput {
87
84
  stdout: string;
88
85
  stderr: string;
@@ -95,7 +92,7 @@ export async function spawnCommand(
95
92
  successMessage: string
96
93
  ): Promise<string> {
97
94
  return new Promise((resolve, reject) => {
98
- const progressIndicator = new ProgressIndicator(progressMessage);
95
+ const progressIndicator = new ProgressIndicator(progressMessage, true);
99
96
  const out: SpawnOutput = { stdout: '', stderr: '' };
100
97
  const spawned = spawn(command, args);
101
98
 
@@ -134,21 +131,31 @@ export function getSlothVersion(): string {
134
131
  }
135
132
 
136
133
  export class ProgressIndicator {
137
- private hasBeenCalled: boolean;
138
- private initialMessage: string;
134
+ private interval: number | undefined = undefined;
139
135
 
140
- constructor(initialMessage: string) {
141
- this.hasBeenCalled = false;
142
- this.initialMessage = initialMessage;
136
+ constructor(initialMessage: string, manual?: boolean) {
137
+ stdout.write(initialMessage);
138
+ if (!manual) {
139
+ this.interval = setInterval(this.indicateInner, 1000) as unknown as number;
140
+ }
141
+ }
142
+
143
+ private indicateInner(): void {
144
+ stdout.write('.');
143
145
  }
144
146
 
145
147
  indicate(): void {
146
- if (this.hasBeenCalled) {
147
- stdout.write('.');
148
- } else {
149
- this.hasBeenCalled = true;
150
- stdout.write(this.initialMessage);
148
+ if (this.interval) {
149
+ throw new Error('ProgressIndicator.indicate only to be called in manual mode');
150
+ }
151
+ this.indicateInner();
152
+ }
153
+
154
+ stop(): void {
155
+ if (this.interval) {
156
+ clearInterval(this.interval);
151
157
  }
158
+ stdout.write('\n');
152
159
  }
153
160
  }
154
161