gaunt-sloth-assistant 0.1.5 → 0.2.2

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 (128) hide show
  1. package/.prettierrc.json +9 -0
  2. package/README.md +177 -158
  3. package/ROADMAP.md +1 -1
  4. package/dist/commands/askCommand.d.ts +6 -0
  5. package/dist/commands/askCommand.js +26 -0
  6. package/dist/commands/askCommand.js.map +1 -0
  7. package/dist/commands/initCommand.d.ts +6 -0
  8. package/dist/commands/initCommand.js +16 -0
  9. package/dist/commands/initCommand.js.map +1 -0
  10. package/dist/commands/reviewCommand.d.ts +3 -0
  11. package/dist/commands/reviewCommand.js +128 -0
  12. package/dist/commands/reviewCommand.js.map +1 -0
  13. package/dist/config.d.ts +80 -0
  14. package/dist/config.js +178 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/configs/anthropic.d.ts +5 -0
  17. package/{src → dist}/configs/anthropic.js +45 -48
  18. package/dist/configs/anthropic.js.map +1 -0
  19. package/dist/configs/fake.d.ts +3 -0
  20. package/{src → dist}/configs/fake.js +11 -14
  21. package/dist/configs/fake.js.map +1 -0
  22. package/dist/configs/groq.d.ts +4 -0
  23. package/{src → dist}/configs/groq.js +10 -13
  24. package/dist/configs/groq.js.map +1 -0
  25. package/dist/configs/types.d.ts +14 -0
  26. package/dist/configs/types.js +2 -0
  27. package/dist/configs/types.js.map +1 -0
  28. package/dist/configs/vertexai.d.ts +4 -0
  29. package/{src → dist}/configs/vertexai.js +44 -47
  30. package/dist/configs/vertexai.js.map +1 -0
  31. package/dist/consoleUtils.d.ts +6 -0
  32. package/{src → dist}/consoleUtils.js +10 -15
  33. package/dist/consoleUtils.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +17 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/modules/questionAnsweringModule.d.ts +18 -0
  38. package/{src → dist}/modules/questionAnsweringModule.js +72 -82
  39. package/dist/modules/questionAnsweringModule.js.map +1 -0
  40. package/dist/modules/reviewModule.d.ts +4 -0
  41. package/{src → dist}/modules/reviewModule.js +25 -35
  42. package/dist/modules/reviewModule.js.map +1 -0
  43. package/dist/modules/types.d.ts +18 -0
  44. package/dist/modules/types.js +2 -0
  45. package/dist/modules/types.js.map +1 -0
  46. package/dist/prompt.d.ts +7 -0
  47. package/dist/prompt.js +32 -0
  48. package/dist/prompt.js.map +1 -0
  49. package/dist/providers/file.d.ts +8 -0
  50. package/dist/providers/file.js +20 -0
  51. package/dist/providers/file.js.map +1 -0
  52. package/dist/providers/ghPrDiffProvider.d.ts +8 -0
  53. package/dist/providers/ghPrDiffProvider.js +16 -0
  54. package/dist/providers/ghPrDiffProvider.js.map +1 -0
  55. package/dist/providers/jiraIssueLegacyAccessTokenProvider.d.ts +8 -0
  56. package/dist/providers/jiraIssueLegacyAccessTokenProvider.js +62 -0
  57. package/dist/providers/jiraIssueLegacyAccessTokenProvider.js.map +1 -0
  58. package/dist/providers/jiraIssueLegacyProvider.d.ts +8 -0
  59. package/dist/providers/jiraIssueLegacyProvider.js +74 -0
  60. package/dist/providers/jiraIssueLegacyProvider.js.map +1 -0
  61. package/dist/providers/jiraIssueProvider.d.ts +11 -0
  62. package/dist/providers/jiraIssueProvider.js +96 -0
  63. package/dist/providers/jiraIssueProvider.js.map +1 -0
  64. package/dist/providers/text.d.ts +8 -0
  65. package/dist/providers/text.js +10 -0
  66. package/dist/providers/text.js.map +1 -0
  67. package/dist/providers/types.d.ts +21 -0
  68. package/dist/providers/types.js +2 -0
  69. package/dist/providers/types.js.map +1 -0
  70. package/dist/systemUtils.d.ts +22 -0
  71. package/dist/systemUtils.js +36 -0
  72. package/dist/systemUtils.js.map +1 -0
  73. package/dist/utils.d.ts +49 -0
  74. package/{src → dist}/utils.js +73 -60
  75. package/dist/utils.js.map +1 -0
  76. package/docs/CONFIGURATION.md +95 -6
  77. package/docs/RELEASE-HOWTO.md +1 -1
  78. package/eslint.config.js +99 -21
  79. package/index.js +10 -27
  80. package/package.json +26 -15
  81. package/src/commands/askCommand.ts +34 -0
  82. package/src/commands/initCommand.ts +19 -0
  83. package/src/commands/reviewCommand.ts +209 -0
  84. package/src/config.ts +266 -0
  85. package/src/configs/anthropic.ts +55 -0
  86. package/src/configs/fake.ts +15 -0
  87. package/src/configs/groq.ts +54 -0
  88. package/src/configs/vertexai.ts +53 -0
  89. package/src/consoleUtils.ts +33 -0
  90. package/src/index.ts +21 -0
  91. package/src/modules/questionAnsweringModule.ts +97 -0
  92. package/src/modules/reviewModule.ts +81 -0
  93. package/src/modules/types.ts +23 -0
  94. package/src/prompt.ts +39 -0
  95. package/src/providers/file.ts +24 -0
  96. package/src/providers/ghPrDiffProvider.ts +20 -0
  97. package/src/providers/jiraIssueLegacyProvider.ts +103 -0
  98. package/src/providers/jiraIssueProvider.ts +133 -0
  99. package/src/providers/text.ts +14 -0
  100. package/src/providers/types.ts +24 -0
  101. package/src/systemUtils.ts +52 -0
  102. package/src/utils.ts +225 -0
  103. package/tsconfig.json +24 -0
  104. package/vitest.config.ts +13 -0
  105. package/.eslint.config.mjs +0 -72
  106. package/.github/dependabot.yml +0 -11
  107. package/.github/workflows/ci.yml +0 -33
  108. package/spec/.gsloth.config.js +0 -22
  109. package/spec/.gsloth.config.json +0 -25
  110. package/spec/askCommand.spec.js +0 -92
  111. package/spec/config.spec.js +0 -421
  112. package/spec/initCommand.spec.js +0 -55
  113. package/spec/predefinedConfigs.spec.js +0 -100
  114. package/spec/questionAnsweringModule.spec.js +0 -137
  115. package/spec/reviewCommand.spec.js +0 -222
  116. package/spec/reviewModule.spec.js +0 -28
  117. package/spec/support/jasmine.mjs +0 -14
  118. package/src/commands/askCommand.js +0 -27
  119. package/src/commands/initCommand.js +0 -17
  120. package/src/commands/reviewCommand.js +0 -154
  121. package/src/config.js +0 -177
  122. package/src/prompt.js +0 -34
  123. package/src/providers/file.js +0 -19
  124. package/src/providers/ghPrDiffProvider.js +0 -11
  125. package/src/providers/jiraIssueLegacyAccessTokenProvider.js +0 -84
  126. package/src/providers/text.js +0 -6
  127. package/src/systemUtils.js +0 -32
  128. /package/{.gsloth.preamble.internal.md → .gsloth.backstory.md} +0 -0
@@ -62,7 +62,7 @@ JSON configuration is simpler but less flexible than JavaScript configuration. I
62
62
  {
63
63
  "llm": {
64
64
  "type": "vertexai",
65
- "model": "gemini-2.5-pro-exp-03-25",
65
+ "model": "gemini-2.5-pro-preview-05-06",
66
66
  "temperature": 0
67
67
  }
68
68
  }
@@ -109,7 +109,7 @@ export async function configure(importFunction, global) {
109
109
  const vertexAi = await importFunction('@langchain/google-vertexai');
110
110
  return {
111
111
  llm: new vertexAi.ChatVertexAI({
112
- model: "gemini-2.5-pro-exp-03-25", // Consider checking for latest recommended model versions
112
+ model: "gemini-2.5-pro-preview-05-06", // Consider checking for latest recommended model versions
113
113
  // API Key from AI Studio should also work
114
114
  temperature: 0,
115
115
  //// Other parameters might be relevant depending on Vertex AI API updates.
@@ -146,16 +146,106 @@ See [Langchain documentation](https://js.langchain.com/docs/tutorials/llm_chain/
146
146
 
147
147
  ### JIRA
148
148
 
149
+ Gaunt Sloth supports two methods to integrate with JIRA:
150
+
151
+ #### 1. Modern Jira REST API (Scoped Token)
152
+
153
+ This method uses the Atlassian REST API v3 with a Personal Access Token (PAT). It requires your Atlassian Cloud ID.
154
+
155
+ **Prerequisites:**
156
+
157
+ 1. **Cloud ID**: You can find your Cloud ID by visiting `https://yourcompany.atlassian.net/_edge/tenant_info` while authenticated.
158
+
159
+ 2. **Personal Access Token (PAT)**: Create a PAT with the appropriate permissions from `Atlassian Account Settings -> Security -> Create and manage API tokens -> [Create API token with scopes]`.
160
+ - For issue access, the recommended permission is `read:jira-work` (classic)
161
+ - Alternatively granular access would require: `read:issue-meta:jira`, `read:issue-security-level:jira`, `read:issue.vote:jira`, `read:issue.changelog:jira`, `read:avatar:jira`, `read:issue:jira`, `read:status:jira`, `read:user:jira`, `read:field-configuration:jira`
162
+
163
+ Refer to JIRA API documentation for more details [https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get)
164
+
165
+ **Environment Variables Support:**
166
+
167
+ For better security, you can set the JIRA username, token, and cloud ID using environment variables instead of placing them in the configuration file:
168
+
169
+ - `JIRA_USERNAME`: Your JIRA username (e.g., `user@yourcompany.com`).
170
+ - `JIRA_API_PAT_TOKEN`: Your JIRA Personal Access Token with scopes.
171
+ - `JIRA_CLOUD_ID`: Your Atlassian Cloud ID.
172
+
173
+ If these environment variables are set, they will take precedence over the values in the configuration file.
174
+
175
+ JSON:
176
+
177
+ ```json
178
+ {
179
+ "llm": {"type": "vertexai", "model": "gemini-2.5-pro-preview-05-06"},
180
+ "requirementsProvider": "jira",
181
+ "requirementsProviderConfig": {
182
+ "jira": {
183
+ "username": "username@yourcompany.com",
184
+ "token": "YOUR_JIRA_PAT_TOKEN",
185
+ "cloudId": "YOUR_ATLASSIAN_CLOUD_ID"
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ Optionally displayUrl can be defined to have a clickable link in the output:
192
+
193
+ ```json
194
+ {
195
+ "llm": {"type": "vertexai", "model": "gemini-2.5-pro-preview-05-06"},
196
+ "requirementsProvider": "jira",
197
+ "requirementsProviderConfig": {
198
+ "jira": {
199
+ "displayUrl": "https://yourcompany.atlassian.net/browse/"
200
+ }
201
+ }
202
+ }
203
+ ```
204
+
205
+ JavaScript:
206
+
207
+ ```javascript
208
+ export async function configure(importFunction, global) {
209
+ const vertexAi = await importFunction('@langchain/google-vertexai');
210
+ return {
211
+ llm: new vertexAi.ChatVertexAI({
212
+ model: "gemini-2.5-pro-preview-05-06"
213
+ }),
214
+ requirementsProvider: 'jira',
215
+ requirementsProviderConfig: {
216
+ 'jira': {
217
+ username: 'username@yourcompany.com', // Your Jira username/email
218
+ token: 'YOUR_JIRA_PAT_TOKEN', // Your Personal Access Token
219
+ cloudId: 'YOUR_ATLASSIAN_CLOUD_ID' // Your Atlassian Cloud ID
220
+ }
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ #### 2. Legacy Jira REST API (Unscoped Token)
227
+
228
+ This uses the Unscoped API token (Aka Legacy API token) method with REST API v2.
229
+
230
+ A legacy token can be acquired from `Atlassian Account Settings -> Security -> Create and manage API tokens -> [Create API token without scopes]`.
231
+
149
232
  Example configuration setting up JIRA integration using a legacy API token for both `review` and `pr` commands.
150
233
  Make sure you use your actual company domain in `baseUrl` and your personal legacy `token`.
151
234
 
152
- A legacy token can be acquired from `Atlassian Account Settings -> Security -> Create and manage API tokens`.
235
+ **Environment Variables Support:**
236
+
237
+ For better security, you can set the JIRA username and token using environment variables instead of placing them in the configuration file:
238
+
239
+ - `JIRA_USERNAME`: Your JIRA username (e.g., `user@yourcompany.com`).
240
+ - `JIRA_LEGACY_API_TOKEN`: Your JIRA legacy API token.
241
+
242
+ If these environment variables are set, they will take precedence over the values in the configuration file.
153
243
 
154
244
  JSON:
155
245
 
156
246
  ```json
157
247
  {
158
- "llm": {"type": "vertexai", "model": "gemini-2.5-pro-exp-03-25"},
248
+ "llm": {"type": "vertexai", "model": "gemini-2.5-pro-preview-05-06"},
159
249
  "requirementsProvider": "jira-legacy",
160
250
  "requirementsProviderConfig": {
161
251
  "jira-legacy": {
@@ -174,7 +264,7 @@ export async function configure(importFunction, global) {
174
264
  const vertexAi = await importFunction('@langchain/google-vertexai');
175
265
  return {
176
266
  llm: new vertexAi.ChatVertexAI({
177
- model: "gemini-2.5-pro-exp-03-25"
267
+ model: "gemini-2.5-pro-preview-05-06"
178
268
  }),
179
269
  requirementsProvider: 'jira-legacy',
180
270
  requirementsProviderConfig: {
@@ -187,4 +277,3 @@ export async function configure(importFunction, global) {
187
277
  }
188
278
  }
189
279
  ```
190
-
@@ -16,7 +16,7 @@ git push --tags
16
16
 
17
17
  Note the release version and do
18
18
  ```shell
19
- gh release create --generate-notes
19
+ gh release create --notes "notes"
20
20
  ```
21
21
 
22
22
  Publish to NPM
package/eslint.config.js CHANGED
@@ -1,38 +1,116 @@
1
- import js from "@eslint/js";
2
- import { defineConfig } from "eslint/config";
3
- import jasmine from "eslint-plugin-jasmine"
4
- import globals from "globals";
1
+ import jsConfig from '@eslint/js';
2
+ import { defineConfig } from 'eslint/config';
3
+ import globals from 'globals';
4
+ import tseslint from '@typescript-eslint/eslint-plugin';
5
+ import tsParser from '@typescript-eslint/parser';
6
+ import prettierConfig from 'eslint-config-prettier';
7
+ import prettierPlugin from 'eslint-plugin-prettier';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
5
10
 
6
- const { setTimeout, setInterval, clearInterval, Buffer, fetch } = globals.node;
11
+ // Get the directory path of the current module
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ // Global ignores applied to all configurations
16
+ const globalIgnores = ['**/node_modules/**', '**/dist/**', 'coverage/**', '.git/**'];
7
17
 
8
18
  export default defineConfig([
19
+ // Ignore files config - applies first
20
+ {
21
+ ignores: globalIgnores,
22
+ },
23
+ // Base configuration for all JavaScript files
9
24
  {
10
- files: ["src/**/*.js"],
25
+ files: ['**/*.js'],
26
+ languageOptions: {
27
+ ecmaVersion: 2022,
28
+ sourceType: 'module',
29
+ globals: {
30
+ ...globals.node,
31
+ },
32
+ },
11
33
  plugins: {
12
- js,
34
+ prettier: prettierPlugin,
13
35
  },
14
- extends: ["js/recommended"],
15
36
  rules: {
16
- semi: "error",
17
- "eol-last": "error"
37
+ ...jsConfig.configs.recommended.rules,
38
+ ...prettierConfig.rules,
39
+ semi: 'error',
40
+ 'eol-last': 'error',
41
+ 'prettier/prettier': 'error',
42
+ 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
18
43
  },
44
+ },
45
+ // Source TypeScript files
46
+ {
47
+ files: ['src/**/*.ts'],
19
48
  languageOptions: {
49
+ parser: tsParser,
50
+ parserOptions: {
51
+ ecmaVersion: 2022,
52
+ sourceType: 'module',
53
+ project: path.resolve(__dirname, 'tsconfig.json'),
54
+ },
20
55
  globals: {
21
- setTimeout,
22
- setInterval,
23
- clearInterval,
24
- Buffer,
25
- fetch
56
+ ...globals.node,
26
57
  },
27
58
  },
59
+ plugins: {
60
+ '@typescript-eslint': tseslint,
61
+ prettier: prettierPlugin,
62
+ },
63
+ rules: {
64
+ ...tseslint.configs.recommended.rules,
65
+ ...prettierConfig.rules,
66
+ semi: 'error',
67
+ 'eol-last': 'error',
68
+ 'prettier/prettier': 'error',
69
+ '@typescript-eslint/explicit-function-return-type': 'off',
70
+ '@typescript-eslint/no-explicit-any': 'warn',
71
+ '@typescript-eslint/no-unused-vars': [
72
+ 'error',
73
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
74
+ ],
75
+ },
28
76
  },
77
+ // Test TypeScript files with separate project reference
29
78
  {
30
- files: ["spec/**/*.js"],
31
- plugins: { jasmine },
32
- extends: ["jasmine/recommended"],
79
+ files: ['spec/**/*.ts', 'vitest.config.ts'],
80
+ languageOptions: {
81
+ parser: tsParser,
82
+ parserOptions: {
83
+ ecmaVersion: 2022,
84
+ sourceType: 'module',
85
+ // No project needed for tests to avoid parser errors
86
+ },
87
+ globals: {
88
+ ...globals.node,
89
+ describe: 'readonly',
90
+ it: 'readonly',
91
+ expect: 'readonly',
92
+ beforeEach: 'readonly',
93
+ afterEach: 'readonly',
94
+ vi: 'readonly',
95
+ },
96
+ },
97
+ plugins: {
98
+ '@typescript-eslint': tseslint,
99
+ prettier: prettierPlugin,
100
+ },
33
101
  rules: {
34
- semi: "error",
35
- "eol-last": "error",
36
- }
102
+ ...tseslint.configs.recommended.rules,
103
+ ...prettierConfig.rules,
104
+ semi: 'error',
105
+ 'eol-last': 'error',
106
+ 'prettier/prettier': 'error',
107
+ '@typescript-eslint/explicit-function-return-type': 'off',
108
+ '@typescript-eslint/no-explicit-any': 'off', // Allow any in test files
109
+ '@typescript-eslint/no-unused-vars': [
110
+ 'error',
111
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
112
+ ], // Error for unused vars in tests
113
+ 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], // For .js test files
114
+ },
37
115
  },
38
116
  ]);
package/index.js CHANGED
@@ -1,31 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { dirname } from 'node:path';
4
- import { fileURLToPath } from "url";
5
- import { reviewCommand } from "./src/commands/reviewCommand.js";
6
- import { initCommand } from "./src/commands/initCommand.js";
7
- import { askCommand } from "./src/commands/askCommand.js";
8
- import { slothContext } from "./src/config.js";
9
- import { getSlothVersion, readStdin } from "./src/utils.js";
10
- import {getCurrentDir, getInstallDir, setInstallDir} from "./src/systemUtils.js";
11
2
 
12
- const program = new Command();
3
+ // This is a minimalistic entry point that sets the installDir in systemUtils
4
+ // and delegates to the compiled TypeScript code in dist/index.js
5
+ import { setEntryPoint } from './dist/systemUtils.js';
13
6
 
14
- setInstallDir(dirname(fileURLToPath(import.meta.url)));
15
- slothContext.currentDir = getCurrentDir();
16
- slothContext.installDir = getInstallDir();
7
+ // Set the installation directory in systemUtils
8
+ setEntryPoint(import.meta.url);
17
9
 
18
- program
19
- .name('gsloth')
20
- .description('Gaunt Sloth Assistant reviewing your PRs')
21
- .version(getSlothVersion());
22
-
23
- initCommand(program, slothContext);
24
-
25
- reviewCommand(program, slothContext)
26
-
27
- askCommand(program, slothContext);
28
-
29
- // TODO add general interactive chat command
30
-
31
- await readStdin(program);
10
+ // Import and run the compiled TypeScript code
11
+ import('./dist/index.js').catch((err) => {
12
+ console.error('Failed to load application:', err);
13
+ process.exit(1);
14
+ });
package/package.json CHANGED
@@ -1,41 +1,52 @@
1
1
  {
2
2
  "name": "gaunt-sloth-assistant",
3
- "version": "0.1.5",
3
+ "version": "0.2.2",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "Andrew Kondratev",
7
7
  "type": "module",
8
- "main": "index.js",
8
+ "main": "dist/index.js",
9
9
  "repository": "github:andruhon/gaunt-sloth-assistant",
10
10
  "engines": {
11
11
  "node": ">=22.0.0",
12
12
  "npm": ">=10.9.0"
13
13
  },
14
14
  "scripts": {
15
- "test": "jasmine",
16
- "lint": "eslint"
15
+ "build": "tsc",
16
+ "test": "npm run build &&vitest run",
17
+ "lint": "eslint . --ext .js,.ts",
18
+ "format": "prettier --write 'src/**/*.{js,ts}'",
19
+ "prepare": "npm run build"
17
20
  },
18
21
  "bin": {
19
22
  "gsloth": "index.js",
20
23
  "gth": "index.js"
21
24
  },
22
25
  "dependencies": {
23
- "@langchain/anthropic": "^0.3.18",
24
- "@langchain/core": "^0.3.45",
25
- "@langchain/google-vertexai": "^0.2.4",
26
+ "@langchain/anthropic": "^0.3.20",
27
+ "@langchain/core": "^0.3.55",
28
+ "@langchain/google-vertexai": "^0.2.8",
26
29
  "@langchain/groq": "^0.2.2",
27
- "@langchain/langgraph": "^0.2.65",
28
- "@types/node": "^22.14.1",
30
+ "@langchain/langgraph": "^0.2.71",
29
31
  "chalk": "^5.4.1",
30
32
  "commander": "^13.1.0",
31
33
  "uuid": "^11.1.0"
32
34
  },
33
35
  "devDependencies": {
34
- "@eslint/js": "^9.25.1",
35
- "eslint": "^9.25.1",
36
- "eslint-plugin-jasmine": "^4.2.2",
37
- "globals": "^16.0.0",
38
- "jasmine": "^5.6.0",
39
- "testdouble": "^3.20.2"
36
+ "@eslint/js": "^9.26.0",
37
+ "@types/node": "^22.15.17",
38
+ "@types/uuid": "^10.0.0",
39
+ "@typescript-eslint/eslint-plugin": "^8.32.0",
40
+ "@typescript-eslint/parser": "^8.32.0",
41
+ "eslint": "^9.26.0",
42
+ "eslint-config-prettier": "^9.1.0",
43
+ "eslint-plugin-prettier": "^5.1.3",
44
+ "globals": "^16.1.0",
45
+ "prettier": "3.5.3",
46
+ "typescript": "^5.4.2",
47
+ "vitest": "^3.1.3"
48
+ },
49
+ "imports": {
50
+ "#src/*.js": "./dist/*.js"
40
51
  }
41
52
  }
@@ -0,0 +1,34 @@
1
+ import { Command } from 'commander';
2
+ import { readInternalPreamble } from '#src/prompt.js';
3
+ import { readMultipleFilesFromCurrentDir } from '#src/utils.js';
4
+ import { initConfig } from '#src/config.js';
5
+
6
+ interface AskCommandOptions {
7
+ file?: string[];
8
+ }
9
+
10
+ /**
11
+ * Adds the ask command to the program
12
+ * @param program - The commander program
13
+ */
14
+ export function askCommand(program: Command): void {
15
+ program
16
+ .command('ask')
17
+ .description('Ask a question')
18
+ .argument('<message>', 'A message')
19
+ .option(
20
+ '-f, --file [files...]',
21
+ 'Input files. Content of these files will be added BEFORE the message'
22
+ )
23
+ // TODO add option consuming extra message as argument
24
+ .action(async (message: string, options: AskCommandOptions) => {
25
+ const preamble = [readInternalPreamble()];
26
+ const content = [message];
27
+ if (options.file) {
28
+ content.push(readMultipleFilesFromCurrentDir(options.file));
29
+ }
30
+ await initConfig();
31
+ const { askQuestion } = await import('#src/modules/questionAnsweringModule.js');
32
+ await askQuestion('sloth-ASK', preamble.join('\n'), content.join('\n'));
33
+ });
34
+ }
@@ -0,0 +1,19 @@
1
+ import type { ConfigType } from '#src/config.js';
2
+ import { availableDefaultConfigs, createProjectConfig } from '#src/config.js';
3
+ import { Argument, Command } from 'commander';
4
+
5
+ /**
6
+ * Adds the init command to the program
7
+ * @param program - The commander program
8
+ */
9
+ export function initCommand(program: Command): void {
10
+ program
11
+ .command('init')
12
+ .description(
13
+ 'Initialize the Gaunt Sloth Assistant in your project. This will write necessary config files.'
14
+ )
15
+ .addArgument(new Argument('<type>', 'Config type').choices(availableDefaultConfigs))
16
+ .action(async (config: ConfigType) => {
17
+ await createProjectConfig(config);
18
+ });
19
+ }
@@ -0,0 +1,209 @@
1
+ import { Command, Option } from 'commander';
2
+ import { USER_PROJECT_REVIEW_PREAMBLE } from '#src/config.js';
3
+ import { readInternalPreamble, readPreamble } from '#src/prompt.js';
4
+ import { readMultipleFilesFromCurrentDir } from '#src/utils.js';
5
+ import { displayError } from '#src/consoleUtils.js';
6
+ import type { SlothContext } from '#src/config.js';
7
+
8
+ /**
9
+ * Requirements providers. Expected to be in `.providers/` dir
10
+ */
11
+ const REQUIREMENTS_PROVIDERS = {
12
+ 'jira-legacy': 'jiraIssueLegacyProvider.js',
13
+ jira: 'jiraIssueProvider.js',
14
+ text: 'text.js',
15
+ file: 'file.js',
16
+ } as const;
17
+
18
+ type RequirementsProviderType = keyof typeof REQUIREMENTS_PROVIDERS;
19
+
20
+ /**
21
+ * Content providers. Expected to be in `.providers/` dir
22
+ */
23
+ const CONTENT_PROVIDERS = {
24
+ gh: 'ghPrDiffProvider.js',
25
+ text: 'text.js',
26
+ file: 'file.js',
27
+ } as const;
28
+
29
+ type ContentProviderType = keyof typeof CONTENT_PROVIDERS;
30
+
31
+ interface ReviewCommandOptions {
32
+ file?: string[];
33
+ requirements?: string;
34
+ requirementsProvider?: RequirementsProviderType;
35
+ contentProvider?: ContentProviderType;
36
+ message?: string;
37
+ }
38
+
39
+ interface PrCommandOptions {
40
+ file?: string[];
41
+ requirementsProvider?: RequirementsProviderType;
42
+ }
43
+
44
+ export function reviewCommand(program: Command, context: SlothContext): void {
45
+ program
46
+ .command('review')
47
+ .description('Review provided diff or other content')
48
+ .argument(
49
+ '[contentId]',
50
+ 'Optional content ID argument to retrieve content with content provider'
51
+ )
52
+ .alias('r')
53
+ // TODO add provider to get results of git --no-pager diff
54
+ .option(
55
+ '-f, --file [files...]',
56
+ 'Input files. Content of these files will be added BEFORE the diff, but after requirements'
57
+ )
58
+ // TODO figure out what to do with this (we probably want to merge it with requirementsId)?
59
+ .option('-r, --requirements <requirements>', 'Requirements for this review.')
60
+ .addOption(
61
+ new Option(
62
+ '-p, --requirements-provider <requirementsProvider>',
63
+ 'Requirements provider for this review.'
64
+ ).choices(Object.keys(REQUIREMENTS_PROVIDERS))
65
+ )
66
+ .addOption(
67
+ new Option('--content-provider <contentProvider>', 'Content provider').choices(
68
+ Object.keys(CONTENT_PROVIDERS)
69
+ )
70
+ )
71
+ .option('-m, --message <message>', 'Extra message to provide just before the content')
72
+ .action(async (contentId: string | undefined, options: ReviewCommandOptions) => {
73
+ const { initConfig } = await import('#src/config.js');
74
+ await initConfig();
75
+ const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
76
+ const content: string[] = [];
77
+ const requirementsId = options.requirements;
78
+ const requirementsProvider =
79
+ options.requirementsProvider ??
80
+ (context.config?.review?.requirementsProvider as RequirementsProviderType | undefined) ??
81
+ (context.config?.requirementsProvider as RequirementsProviderType | undefined);
82
+ const contentProvider =
83
+ options.contentProvider ??
84
+ (context.config?.review?.contentProvider as ContentProviderType | undefined) ??
85
+ (context.config?.contentProvider as ContentProviderType | undefined);
86
+
87
+ // TODO consider calling these in parallel
88
+ const requirements = await getRequirementsFromProvider(requirementsProvider, requirementsId);
89
+ if (requirements) {
90
+ content.push(requirements);
91
+ }
92
+
93
+ const providedContent = await getContentFromProvider(contentProvider, contentId);
94
+ if (providedContent) {
95
+ content.push(providedContent);
96
+ }
97
+
98
+ if (options.file) {
99
+ content.push(readMultipleFilesFromCurrentDir(options.file));
100
+ }
101
+ if (context.stdin) {
102
+ content.push(context.stdin);
103
+ }
104
+ if (options.message) {
105
+ content.push(options.message);
106
+ }
107
+ const { review } = await import('#src/modules/reviewModule.js');
108
+ await review('sloth-DIFF-review', preamble.join('\n'), content.join('\n'));
109
+ });
110
+
111
+ program
112
+ .command('pr')
113
+ .description(
114
+ 'Review provided Pull Request in current directory. ' +
115
+ 'This command is similar to `review`, but default content provider is `gh`. ' +
116
+ '(assuming that GH cli is installed and authenticated for current project'
117
+ )
118
+ .argument('<prId>', 'Pull request ID to review.')
119
+ .argument(
120
+ '[requirementsId]',
121
+ 'Optional requirements ID argument to retrieve requirements with requirements provider'
122
+ )
123
+ .addOption(
124
+ new Option(
125
+ '-p, --requirements-provider <requirementsProvider>',
126
+ 'Requirements provider for this review.'
127
+ ).choices(Object.keys(REQUIREMENTS_PROVIDERS))
128
+ )
129
+ .option(
130
+ '-f, --file [files...]',
131
+ 'Input files. Content of these files will be added BEFORE the diff, but after requirements'
132
+ )
133
+ .action(async (prId: string, requirementsId: string | undefined, options: PrCommandOptions) => {
134
+ const { initConfig } = await import('#src/config.js');
135
+ await initConfig();
136
+
137
+ const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
138
+ const content: string[] = [];
139
+ const requirementsProvider =
140
+ options.requirementsProvider ??
141
+ (context.config?.pr?.requirementsProvider as RequirementsProviderType | undefined) ??
142
+ (context.config?.requirementsProvider as RequirementsProviderType | undefined);
143
+
144
+ // Handle requirements
145
+ const requirements = await getRequirementsFromProvider(requirementsProvider, requirementsId);
146
+ if (requirements) {
147
+ content.push(requirements);
148
+ }
149
+
150
+ if (options.file) {
151
+ content.push(readMultipleFilesFromCurrentDir(options.file));
152
+ }
153
+
154
+ // Get PR diff using the 'gh' provider
155
+ const providerPath = `#src/providers/${CONTENT_PROVIDERS['gh']}`;
156
+ const { get } = await import(providerPath);
157
+ content.push(await get(null, prId));
158
+
159
+ const { review } = await import('#src/modules/reviewModule.js');
160
+ await review(`sloth-PR-${prId}-review`, preamble.join('\n'), content.join('\n'));
161
+ });
162
+
163
+ async function getRequirementsFromProvider(
164
+ requirementsProvider: RequirementsProviderType | undefined,
165
+ requirementsId: string | undefined
166
+ ): Promise<string> {
167
+ return getFromProvider(
168
+ requirementsProvider,
169
+ requirementsId,
170
+ (context.config?.requirementsProviderConfig ?? {})[requirementsProvider as string],
171
+ REQUIREMENTS_PROVIDERS
172
+ );
173
+ }
174
+
175
+ async function getContentFromProvider(
176
+ contentProvider: ContentProviderType | undefined,
177
+ contentId: string | undefined
178
+ ): Promise<string> {
179
+ return getFromProvider(
180
+ contentProvider,
181
+ contentId,
182
+ (context.config?.contentProviderConfig ?? {})[contentProvider as string],
183
+ CONTENT_PROVIDERS
184
+ );
185
+ }
186
+
187
+ async function getFromProvider(
188
+ provider: RequirementsProviderType | ContentProviderType | undefined,
189
+ id: string | undefined,
190
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
191
+ config: any,
192
+ legitPredefinedProviders: typeof REQUIREMENTS_PROVIDERS | typeof CONTENT_PROVIDERS
193
+ ): Promise<string> {
194
+ if (typeof provider === 'string') {
195
+ // Use one of the predefined providers
196
+ if (legitPredefinedProviders[provider as keyof typeof legitPredefinedProviders]) {
197
+ const providerPath = `#src/providers/${legitPredefinedProviders[provider as keyof typeof legitPredefinedProviders]}`;
198
+ const { get } = await import(providerPath);
199
+ return await get(config, id);
200
+ } else {
201
+ displayError(`Unknown provider: ${provider}. Continuing without it.`);
202
+ }
203
+ } else if (typeof provider === 'function') {
204
+ // Type assertion to handle function call
205
+ return await (provider as (id: string | undefined) => Promise<string>)(id);
206
+ }
207
+ return '';
208
+ }
209
+ }