gaunt-sloth-assistant 0.7.2 → 0.7.3
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.
- package/.gsloth.config_.json +2 -2
- package/.gsloth.config_claude.mjs +18 -0
- package/.gsloth.config_vertex.json +6 -0
- package/README.md +1 -1
- package/assets/release-notes/v0_7_3.md +64 -0
- package/dist/builtInToolsConfig.d.ts +6 -0
- package/dist/builtInToolsConfig.js +73 -0
- package/dist/builtInToolsConfig.js.map +1 -0
- package/dist/commands/prCommand.js +14 -1
- package/dist/commands/prCommand.js.map +1 -1
- package/dist/config.d.ts +18 -7
- package/dist/config.js +1 -41
- package/dist/config.js.map +1 -1
- package/dist/core/Invocation.js +5 -2
- package/dist/core/Invocation.js.map +1 -1
- package/dist/helpers/jira/jiraClient.d.ts +4 -0
- package/dist/helpers/jira/jiraClient.js +75 -0
- package/dist/helpers/jira/jiraClient.js.map +1 -0
- package/dist/helpers/jira/jiraLogWork.d.ts +2 -0
- package/dist/helpers/jira/jiraLogWork.js +53 -0
- package/dist/helpers/jira/jiraLogWork.js.map +1 -0
- package/dist/providers/jiraIssueProvider.js +9 -52
- package/dist/providers/jiraIssueProvider.js.map +1 -1
- package/dist/tools/gthJiraLogWorkTool.d.ts +3 -0
- package/dist/tools/gthJiraLogWorkTool.js +37 -0
- package/dist/tools/gthJiraLogWorkTool.js.map +1 -0
- package/dist/tools/gthStatusUpdateTool.d.ts +2 -0
- package/dist/tools/gthStatusUpdateTool.js +16 -0
- package/dist/tools/gthStatusUpdateTool.js.map +1 -0
- package/docs/DEVELOPMENT.md +26 -0
- package/package.json +1 -1
- package/src/builtInToolsConfig.ts +99 -0
- package/src/commands/prCommand.ts +28 -3
- package/src/config.ts +20 -62
- package/src/core/Invocation.ts +7 -2
- package/src/helpers/jira/jiraClient.ts +99 -0
- package/src/helpers/jira/jiraLogWork.ts +81 -0
- package/src/providers/jiraIssueProvider.ts +13 -70
- package/src/tools/gthJiraLogWorkTool.ts +51 -0
- package/src/tools/gthStatusUpdateTool.ts +18 -0
- package/vitest-it.config.ts +1 -1
- package/vitest.config.ts +1 -0
- package/dist/tools/statusUpdate.d.ts +0 -1
- package/dist/tools/statusUpdate.js +0 -12
- package/dist/tools/statusUpdate.js.map +0 -1
- package/src/tools/statusUpdate.ts +0 -15
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { display, displayError, displayWarning } from '#src/consoleUtils.js';
|
|
2
|
-
import {
|
|
3
|
-
import { ProgressIndicator } from '#src/utils.js';
|
|
2
|
+
import { getJiraCredentials, jiraRequest } from '#src/helpers/jira/jiraClient.js';
|
|
4
3
|
/**
|
|
5
4
|
* Gets Jira issue using Atlassian REST API v3 with Personal Access Token
|
|
6
5
|
*
|
|
@@ -19,28 +18,9 @@ export async function get(config, issueId) {
|
|
|
19
18
|
displayWarning('No issue ID provided');
|
|
20
19
|
return null;
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
const username = env.JIRA_USERNAME || config.username;
|
|
24
|
-
if (!username) {
|
|
25
|
-
throw new Error('Missing JIRA username. The username can be defined as JIRA_USERNAME environment variable or as "username" in config.');
|
|
26
|
-
}
|
|
27
|
-
// Get token from environment variable or config
|
|
28
|
-
const token = env.JIRA_API_PAT_TOKEN || config.token;
|
|
29
|
-
if (!token) {
|
|
30
|
-
throw new Error('Missing JIRA PAT token. The token can be defined as JIRA_API_PAT_TOKEN environment variable or as "token" in config.');
|
|
31
|
-
}
|
|
32
|
-
// Get cloud ID from environment variable or config
|
|
33
|
-
const cloudId = env.JIRA_CLOUD_ID || config.cloudId;
|
|
34
|
-
if (!cloudId) {
|
|
35
|
-
throw new Error('Missing JIRA Cloud ID. The Cloud ID can be defined as JIRA_CLOUD_ID environment variable or as "cloudId" in config.');
|
|
36
|
-
}
|
|
21
|
+
const credentials = getJiraCredentials(config);
|
|
37
22
|
try {
|
|
38
|
-
const issue = await getJiraIssue(
|
|
39
|
-
...config,
|
|
40
|
-
username,
|
|
41
|
-
token,
|
|
42
|
-
cloudId,
|
|
43
|
-
}, issueId);
|
|
23
|
+
const issue = await getJiraIssue(credentials, issueId);
|
|
44
24
|
if (!issue) {
|
|
45
25
|
return null;
|
|
46
26
|
}
|
|
@@ -63,43 +43,20 @@ export async function get(config, issueId) {
|
|
|
63
43
|
* @param jiraKey Jira issue ID
|
|
64
44
|
* @returns Jira issue response
|
|
65
45
|
*/
|
|
66
|
-
async function getJiraIssue(
|
|
46
|
+
async function getJiraIssue(credentials, jiraKey) {
|
|
67
47
|
// Jira Cloud ID can be found by authenticated user at https://company.atlassian.net/_edge/tenant_info
|
|
68
48
|
// According to doc https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get permissions to read this resource:
|
|
69
49
|
// https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issues/#api-rest-api-2-issue-issueidorkey-get
|
|
70
50
|
// either Classic (RECOMMENDED) read:jira-work
|
|
71
|
-
// or Granular read:issue-meta:jira, read:issue-security-level:jira, read:issue.vote:jira, read:issue.changelog:jira,
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
display(`Loading Jira issue ${
|
|
51
|
+
// or Granular read:issue-meta:jira, read:issue-security-level:jira, read:issue.vote:jira, read:issue.changelog:jira,
|
|
52
|
+
// read:avatar:jira, read:issue:jira, read:status:jira, read:user:jira, read:field-configuration:jira
|
|
53
|
+
if (credentials.displayUrl) {
|
|
54
|
+
display(`Loading Jira issue ${credentials.displayUrl}${jiraKey}`);
|
|
75
55
|
}
|
|
76
56
|
// This filter will be necessary for V3: `&expand=renderedFields` to convert ADF to HTML
|
|
77
57
|
const filters = '?fields=summary,description'; // Limit JSON to summary and description
|
|
78
|
-
|
|
79
|
-
const credentials = `${config.username}:${config.token}`;
|
|
80
|
-
const encodedCredentials = Buffer.from(credentials).toString('base64');
|
|
81
|
-
const authHeader = `Basic ${encodedCredentials}`;
|
|
82
|
-
// Define request headers
|
|
83
|
-
const headers = {
|
|
84
|
-
Authorization: authHeader,
|
|
85
|
-
Accept: 'application/json; charset=utf-8',
|
|
86
|
-
'Accept-Language': 'en-US,en;q=0.9', // Prevents errors in other languages
|
|
87
|
-
};
|
|
88
|
-
const progressIndicator = new ProgressIndicator(`Retrieving jira from api ${apiUrl.replace(/^https?:\/\//, '')}`);
|
|
89
|
-
const response = await fetch(apiUrl + filters, {
|
|
58
|
+
return jiraRequest(credentials, `/rest/api/2/issue/${jiraKey}${filters}`, {
|
|
90
59
|
method: 'GET',
|
|
91
|
-
headers: headers,
|
|
92
60
|
});
|
|
93
|
-
progressIndicator.stop();
|
|
94
|
-
if (!response?.ok) {
|
|
95
|
-
try {
|
|
96
|
-
const errorData = await response.json();
|
|
97
|
-
throw new Error(`Failed to fetch Jira issue: ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
98
|
-
}
|
|
99
|
-
catch (_e) {
|
|
100
|
-
throw new Error(`Failed to fetch Jira issue: ${response?.statusText}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return response.json();
|
|
104
61
|
}
|
|
105
62
|
//# sourceMappingURL=jiraIssueProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jiraIssueProvider.js","sourceRoot":"","sources":["../../src/providers/jiraIssueProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"jiraIssueProvider.js","sourceRoot":"","sources":["../../src/providers/jiraIssueProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE7E,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAYlF;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,GAAG,CACvB,MAAkC,EAClC,OAA2B;IAE3B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,cAAc,CAAC,yBAAyB,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,cAAc,CAAC,sBAAsB,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,WAAW,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAE/C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC;QACrC,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC;QAE7C,OAAO,eAAe,OAAO,cAAc,OAAO,qBAAqB,WAAW,EAAE,CAAC;IACvF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,YAAY,CACV,6BAA6B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACtF,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,YAAY,CACzB,WAAsF,EACtF,OAAe;IAEf,sGAAsG;IAEtG,0KAA0K;IAC1K,sHAAsH;IACtH,8CAA8C;IAC9C,qHAAqH;IACrH,qGAAqG;IAErG,IAAI,WAAW,CAAC,UAAU,EAAE,CAAC;QAC3B,OAAO,CAAC,sBAAsB,WAAW,CAAC,UAAU,GAAG,OAAO,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,wFAAwF;IACxF,MAAM,OAAO,GAAG,6BAA6B,CAAC,CAAC,wCAAwC;IAEvF,OAAO,WAAW,CAAoB,WAAW,EAAE,qBAAqB,OAAO,GAAG,OAAO,EAAE,EAAE;QAC3F,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { displayWarning } from '#src/consoleUtils.js';
|
|
4
|
+
import jiraLogWork from '#src/helpers/jira/jiraLogWork.js';
|
|
5
|
+
// Define the input schema for the tool
|
|
6
|
+
const gthJiraLogWorkSchema = z.object({
|
|
7
|
+
jiraId: z.string().describe('The Jira issue ID (e.g., "PROJ-123")'),
|
|
8
|
+
timeInSeconds: z.number().describe('Time spent in seconds'),
|
|
9
|
+
comment: z.string().optional().describe('Work log comment'),
|
|
10
|
+
startedAt: z.string().optional().describe('ISO 8601 date string for when work started'),
|
|
11
|
+
});
|
|
12
|
+
const toolDefinition = {
|
|
13
|
+
name: 'gth_jira_log_work',
|
|
14
|
+
description: `Gaunt Sloth Jira Log Work Tool. Log work time to a Jira issue. Requires Jira configuration with credentials.
|
|
15
|
+
Example: gth_jira_log_work({ jiraId: "PROJ-123", timeInSeconds: 3600, comment: "Implemented feature X" })`,
|
|
16
|
+
schema: gthJiraLogWorkSchema,
|
|
17
|
+
};
|
|
18
|
+
function getToolImpl(config) {
|
|
19
|
+
let toolImpl = async ({ jiraId, timeInSeconds, comment = 'Work logged', startedAt, }) => {
|
|
20
|
+
const jiraConfig = config || {};
|
|
21
|
+
const startDate = startedAt ? new Date(startedAt) : new Date();
|
|
22
|
+
return await jiraLogWork(jiraConfig, jiraId, timeInSeconds, comment, startDate);
|
|
23
|
+
};
|
|
24
|
+
return tool(toolImpl, toolDefinition);
|
|
25
|
+
}
|
|
26
|
+
// Export a default instance that uses environment variables
|
|
27
|
+
export function get(config) {
|
|
28
|
+
if (!config.prebuiltToolsConfig?.jira && config.requirementsProviderConfig?.jira) {
|
|
29
|
+
displayWarning('config.prebuiltToolsConfig.jira is not defined. Using config.requirementsProviderConfig.jira.');
|
|
30
|
+
}
|
|
31
|
+
let jiraConfig = config.prebuiltToolsConfig?.jira || config.requirementsProviderConfig?.jira;
|
|
32
|
+
if (!jiraConfig) {
|
|
33
|
+
throw new Error('gth_jira_log_work is added to preBuiltTools, but no Jira config is provided.');
|
|
34
|
+
}
|
|
35
|
+
return getToolImpl(jiraConfig);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=gthJiraLogWorkTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gthJiraLogWorkTool.js","sourceRoot":"","sources":["../../src/tools/gthJiraLogWorkTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,WAAW,MAAM,kCAAkC,CAAC;AAI3D,uCAAuC;AACvC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sCAAsC,CAAC;IACnE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,uBAAuB,CAAC;IAC3D,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IAC3D,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4CAA4C,CAAC;CACxF,CAAC,CAAC;AAEH,MAAM,cAAc,GAAG;IACrB,IAAI,EAAE,mBAAmB;IACzB,WAAW,EAAE;0GAC2F;IACxG,MAAM,EAAE,oBAAoB;CAC7B,CAAC;AAEF,SAAS,WAAW,CAAC,MAA4B;IAC/C,IAAI,QAAQ,GAAG,KAAK,EAAE,EACpB,MAAM,EACN,aAAa,EACb,OAAO,GAAG,aAAa,EACvB,SAAS,GAC4B,EAAmB,EAAE;QAC1D,MAAM,UAAU,GAAG,MAAM,IAAI,EAAE,CAAC;QAEhC,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAE/D,OAAO,MAAM,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IAClF,CAAC,CAAC;IACF,OAAO,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;AACxC,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,GAAG,CAAC,MAAmB;IACrC,IAAI,CAAC,MAAM,CAAC,mBAAmB,EAAE,IAAI,IAAI,MAAM,CAAC,0BAA0B,EAAE,IAAI,EAAE,CAAC;QACjF,cAAc,CACZ,+FAA+F,CAChG,CAAC;IACJ,CAAC;IACD,IAAI,UAAU,GAAG,MAAM,CAAC,mBAAmB,EAAE,IAAI,IAAI,MAAM,CAAC,0BAA0B,EAAE,IAAI,CAAC;IAC7F,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAC;IAClG,CAAC;IACD,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { display } from '#src/consoleUtils.js';
|
|
5
|
+
const toolDefinition = {
|
|
6
|
+
name: 'gth_status_update',
|
|
7
|
+
description: `Gaunt Sloth Status Update Tool. Use this tool to update status in the console. Example: gth_status_update "Working on something important", be brief, feel free to use emojis. Update after using tools.`,
|
|
8
|
+
schema: z.string(),
|
|
9
|
+
};
|
|
10
|
+
const toolImpl = (s) => {
|
|
11
|
+
display(chalk.grey(s));
|
|
12
|
+
};
|
|
13
|
+
export function get(_) {
|
|
14
|
+
return tool(toolImpl, toolDefinition);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=gthStatusUpdateTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gthStatusUpdateTool.js","sourceRoot":"","sources":["../../src/tools/gthStatusUpdateTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAG/C,MAAM,cAAc,GAAG;IACrB,IAAI,EAAE,mBAAmB;IACzB,WAAW,EAAE,0MAA0M;IACvN,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CACnB,CAAC;AACF,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAQ,EAAE;IACnC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,UAAU,GAAG,CAAC,CAAc;IAChC,OAAO,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;AACxC,CAAC"}
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -18,3 +18,29 @@ Running unit tests:
|
|
|
18
18
|
```shell
|
|
19
19
|
npm run test
|
|
20
20
|
```
|
|
21
|
+
|
|
22
|
+
## Running tests on both Windows and WSL
|
|
23
|
+
|
|
24
|
+
If you first installed deps on Windows and then running tests on WSL
|
|
25
|
+
or vice versa you're likely to experience an error like
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Error: Cannot find module @rollup/rollup-linux-x64-gnu. npm has a bug related to optional dependencies
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
or
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
'tsc' is not recognized as an internal or external command, operable program or batch file.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Simply install that missing *optional* rollup dependency or TypeScript without saving it,
|
|
38
|
+
like that:
|
|
39
|
+
|
|
40
|
+
`npm install @rollup/rollup-linux-x64-gnu --no-save`
|
|
41
|
+
|
|
42
|
+
or
|
|
43
|
+
|
|
44
|
+
`npm install typescript --no-save`
|
|
45
|
+
|
|
46
|
+
This will allow switching between environments and run tests in both.
|
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import GthFileSystemToolkit from '#src/tools/GthFileSystemToolkit.js';
|
|
2
|
+
import { StructuredToolInterface } from '@langchain/core/tools';
|
|
3
|
+
import { SlothConfig } from '#src/config.js';
|
|
4
|
+
import { displayWarning } from '#src/consoleUtils.js';
|
|
5
|
+
import { getCurrentDir } from '#src/systemUtils.js';
|
|
6
|
+
|
|
7
|
+
const AVAILABLE_BUILT_IN_TOOLS = {
|
|
8
|
+
gth_status_update: '#src/tools/gthStatusUpdateTool.js',
|
|
9
|
+
gth_jira_log_work: '#src/tools/gthJiraLogWorkTool.js',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get default tools based on filesystem and built-in tools configuration
|
|
14
|
+
*/
|
|
15
|
+
export async function getDefaultTools(config: SlothConfig): Promise<StructuredToolInterface[]> {
|
|
16
|
+
const filesystemTools = filterFilesystemTools(
|
|
17
|
+
new GthFileSystemToolkit([getCurrentDir()]),
|
|
18
|
+
config.filesystem
|
|
19
|
+
);
|
|
20
|
+
const builtInTools = await getBuiltInTools(config);
|
|
21
|
+
|
|
22
|
+
return [...filesystemTools, ...builtInTools];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Filter filesystem tools based on configuration
|
|
27
|
+
*/
|
|
28
|
+
function filterFilesystemTools(
|
|
29
|
+
toolkit: GthFileSystemToolkit,
|
|
30
|
+
filesystemConfig: string[] | 'all' | 'read' | 'none'
|
|
31
|
+
): StructuredToolInterface[] {
|
|
32
|
+
if (filesystemConfig === 'all') {
|
|
33
|
+
return toolkit.getTools();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (filesystemConfig === 'none') {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (filesystemConfig === 'read') {
|
|
41
|
+
// Read-only: only allow read operations
|
|
42
|
+
return toolkit.getFilteredTools(['read']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!Array.isArray(filesystemConfig)) {
|
|
46
|
+
return toolkit.getTools();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (filesystemConfig.includes('all')) {
|
|
50
|
+
return toolkit.getTools();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle an array of specific tool names or 'read'/'all'
|
|
54
|
+
const allowedTools: StructuredToolInterface[] = filesystemConfig.includes('read')
|
|
55
|
+
? toolkit.getFilteredTools(['read'])
|
|
56
|
+
: [];
|
|
57
|
+
|
|
58
|
+
// Also allow specific tool names
|
|
59
|
+
const allowedToolNames = new Set(
|
|
60
|
+
filesystemConfig.filter((name) => name !== 'read' && name !== 'all')
|
|
61
|
+
);
|
|
62
|
+
const specificNamedTools = toolkit.getTools().filter((tool) => {
|
|
63
|
+
return tool.name && allowedToolNames.has(tool.name);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Combine and deduplicate
|
|
67
|
+
const allAllowedTools = [...allowedTools, ...specificNamedTools];
|
|
68
|
+
return allAllowedTools.filter(
|
|
69
|
+
(tool, index, arr) => arr.findIndex((t) => t.name === tool.name) === index
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get built-in tools based on configuration
|
|
75
|
+
*/
|
|
76
|
+
async function getBuiltInTools(config: SlothConfig): Promise<StructuredToolInterface[]> {
|
|
77
|
+
const tools: StructuredToolInterface[] = [];
|
|
78
|
+
|
|
79
|
+
if (!config.builtInTools) {
|
|
80
|
+
return tools;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const toolName of config.builtInTools) {
|
|
84
|
+
if (toolName in AVAILABLE_BUILT_IN_TOOLS) {
|
|
85
|
+
try {
|
|
86
|
+
const tool = await import(
|
|
87
|
+
AVAILABLE_BUILT_IN_TOOLS[toolName as keyof typeof AVAILABLE_BUILT_IN_TOOLS]
|
|
88
|
+
);
|
|
89
|
+
tools.push(tool.get(config));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
displayWarning(`Failed to load built-in tool '${toolName}': ${error}`);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
displayWarning(`Unknown built-in tool: ${toolName}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return tools;
|
|
99
|
+
}
|
|
@@ -7,16 +7,19 @@ import {
|
|
|
7
7
|
} from '#src/prompt.js';
|
|
8
8
|
import { readMultipleFilesFromCurrentDir } from '#src/utils.js';
|
|
9
9
|
import {
|
|
10
|
-
REQUIREMENTS_PROVIDERS,
|
|
11
10
|
CONTENT_PROVIDERS,
|
|
12
|
-
type RequirementsProviderType,
|
|
13
|
-
getRequirementsFromProvider,
|
|
14
11
|
type ContentProviderType,
|
|
12
|
+
getRequirementsFromProvider,
|
|
13
|
+
REQUIREMENTS_PROVIDERS,
|
|
14
|
+
type RequirementsProviderType,
|
|
15
15
|
} from './commandUtils.js';
|
|
16
|
+
import jiraLogWork from '#src/helpers/jira/jiraLogWork.js';
|
|
17
|
+
import { JiraConfig } from '#src/providers/types.js';
|
|
16
18
|
|
|
17
19
|
interface PrCommandOptions {
|
|
18
20
|
file?: string[];
|
|
19
21
|
requirementsProvider?: RequirementsProviderType;
|
|
22
|
+
message?: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function prCommand(program: Command): void {
|
|
@@ -42,6 +45,7 @@ export function prCommand(program: Command): void {
|
|
|
42
45
|
'-f, --file [files...]',
|
|
43
46
|
'Input files. Content of these files will be added BEFORE the diff, but after requirements'
|
|
44
47
|
)
|
|
48
|
+
.option('-m, --message <message>', 'Extra message to provide just before the content')
|
|
45
49
|
.action(async (prId: string, requirementsId: string | undefined, options: PrCommandOptions) => {
|
|
46
50
|
const { initConfig } = await import('#src/config.js');
|
|
47
51
|
const config = await initConfig(); // Initialize and get config
|
|
@@ -85,9 +89,30 @@ export function prCommand(program: Command): void {
|
|
|
85
89
|
const { get } = await import(providerPath);
|
|
86
90
|
content.push(await get(null, prId));
|
|
87
91
|
|
|
92
|
+
if (options.message) {
|
|
93
|
+
content.push(options.message);
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
const { review } = await import('#src/modules/reviewModule.js');
|
|
89
97
|
// TODO consider including requirements id
|
|
90
98
|
// TODO sanitize prId
|
|
91
99
|
await review(`PR-${prId}`, systemMessage.join('\n'), content.join('\n'), config, 'pr');
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
requirementsId &&
|
|
103
|
+
(config.commands?.pr?.requirementsProvider ?? config.requirementsProvider) === 'jira' &&
|
|
104
|
+
config.commands?.pr?.logWorkForReviewInSeconds
|
|
105
|
+
) {
|
|
106
|
+
// TODO we need to figure out some sort of post-processors
|
|
107
|
+
let jiraConfig =
|
|
108
|
+
config.prebuiltToolsConfig?.jira ||
|
|
109
|
+
(config.requirementsProviderConfig?.jira as JiraConfig);
|
|
110
|
+
await jiraLogWork(
|
|
111
|
+
jiraConfig,
|
|
112
|
+
requirementsId,
|
|
113
|
+
config.commands?.pr?.logWorkForReviewInSeconds,
|
|
114
|
+
'code review'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
92
117
|
});
|
|
93
118
|
}
|
package/src/config.ts
CHANGED
|
@@ -13,8 +13,7 @@ import {
|
|
|
13
13
|
USER_PROJECT_CONFIG_JSON,
|
|
14
14
|
USER_PROJECT_CONFIG_MJS,
|
|
15
15
|
} from '#src/constants.js';
|
|
16
|
-
import {
|
|
17
|
-
import GthFileSystemToolkit from '#src/tools/GthFileSystemToolkit.js';
|
|
16
|
+
import { JiraConfig } from '#src/providers/types.js';
|
|
18
17
|
|
|
19
18
|
export interface SlothConfig extends BaseSlothConfig {
|
|
20
19
|
llm: BaseChatModel; // FIXME this is still bad keeping instance in config is probably not best choice
|
|
@@ -24,6 +23,7 @@ export interface SlothConfig extends BaseSlothConfig {
|
|
|
24
23
|
projectReviewInstructions: string;
|
|
25
24
|
streamOutput: boolean;
|
|
26
25
|
filesystem: string[] | 'all' | 'read' | 'none';
|
|
26
|
+
builtInTools?: string[];
|
|
27
27
|
tools?: StructuredToolInterface[] | BaseToolkit[];
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -45,32 +45,46 @@ interface BaseSlothConfig {
|
|
|
45
45
|
projectReviewInstructions?: string;
|
|
46
46
|
streamOutput?: boolean;
|
|
47
47
|
filesystem?: string[] | 'all' | 'read' | 'none';
|
|
48
|
+
prebuiltToolsConfig?: PreBuiltToolsConfig;
|
|
49
|
+
customToolsConfig?: CustomToolsConfig;
|
|
50
|
+
requirementsProviderConfig?: Record<string, unknown>;
|
|
51
|
+
contentProviderConfig?: Record<string, unknown>;
|
|
52
|
+
mcpServers?: Record<string, Connection>;
|
|
53
|
+
builtInTools?: string[];
|
|
48
54
|
commands?: {
|
|
49
55
|
pr?: {
|
|
50
56
|
contentProvider?: string;
|
|
51
57
|
requirementsProvider?: string;
|
|
52
58
|
filesystem?: string[] | 'all' | 'read' | 'none';
|
|
59
|
+
builtInTools?: string[];
|
|
60
|
+
logWorkForReviewInSeconds?: number;
|
|
53
61
|
};
|
|
54
62
|
review?: {
|
|
55
63
|
requirementsProvider?: string;
|
|
56
64
|
contentProvider?: string;
|
|
57
65
|
filesystem?: string[] | 'all' | 'read' | 'none';
|
|
66
|
+
builtInTools?: string[];
|
|
58
67
|
};
|
|
59
68
|
ask?: {
|
|
60
69
|
filesystem?: string[] | 'all' | 'read' | 'none';
|
|
70
|
+
builtInTools?: string[];
|
|
61
71
|
};
|
|
62
72
|
chat?: {
|
|
63
73
|
filesystem?: string[] | 'all' | 'read' | 'none';
|
|
74
|
+
builtInTools?: string[];
|
|
64
75
|
};
|
|
65
76
|
code?: {
|
|
66
77
|
filesystem?: string[] | 'all' | 'read' | 'none';
|
|
78
|
+
builtInTools?: string[];
|
|
67
79
|
};
|
|
68
80
|
};
|
|
69
|
-
requirementsProviderConfig?: Record<string, unknown>;
|
|
70
|
-
contentProviderConfig?: Record<string, unknown>;
|
|
71
|
-
mcpServers?: Record<string, Connection>;
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
export type CustomToolsConfig = Record<string, object>;
|
|
84
|
+
export type PreBuiltToolsConfig = {
|
|
85
|
+
jira: JiraConfig;
|
|
86
|
+
};
|
|
87
|
+
|
|
74
88
|
export interface LLMConfig extends Record<string, unknown> {
|
|
75
89
|
type: string;
|
|
76
90
|
model: string;
|
|
@@ -118,6 +132,7 @@ export async function initConfig(): Promise<SlothConfig> {
|
|
|
118
132
|
exit(1);
|
|
119
133
|
// noinspection ExceptionCaughtLocallyJS
|
|
120
134
|
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
|
|
135
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
121
136
|
throw new Error('Unexpected error occurred.');
|
|
122
137
|
}
|
|
123
138
|
} catch (e) {
|
|
@@ -303,60 +318,3 @@ function mergeConfig(partialConfig: Partial<SlothConfig>): SlothConfig {
|
|
|
303
318
|
function mergeRawConfig(config: RawSlothConfig, llm: BaseChatModel): SlothConfig {
|
|
304
319
|
return mergeConfig({ ...config, llm });
|
|
305
320
|
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Filter filesystem tools based on configuration
|
|
309
|
-
*/
|
|
310
|
-
function filterFilesystemTools(
|
|
311
|
-
toolkit: GthFileSystemToolkit,
|
|
312
|
-
filesystemConfig: string[] | 'all' | 'read' | 'none'
|
|
313
|
-
): StructuredToolInterface[] {
|
|
314
|
-
if (filesystemConfig === 'all') {
|
|
315
|
-
return toolkit.getTools();
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (filesystemConfig === 'none') {
|
|
319
|
-
return [];
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (filesystemConfig === 'read') {
|
|
323
|
-
// Read-only: only allow read operations
|
|
324
|
-
return toolkit.getFilteredTools(['read']);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (!Array.isArray(filesystemConfig)) {
|
|
328
|
-
return toolkit.getTools();
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (filesystemConfig.includes('all')) {
|
|
332
|
-
return toolkit.getTools();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Handle an array of specific tool names or 'read'/'all'
|
|
336
|
-
const allowedTools: StructuredToolInterface[] = filesystemConfig.includes('read')
|
|
337
|
-
? toolkit.getFilteredTools(['read'])
|
|
338
|
-
: [];
|
|
339
|
-
|
|
340
|
-
// Also allow specific tool names
|
|
341
|
-
const allowedToolNames = new Set(
|
|
342
|
-
filesystemConfig.filter((name) => name !== 'read' && name !== 'all')
|
|
343
|
-
);
|
|
344
|
-
const specificNamedTools = toolkit.getTools().filter((tool) => {
|
|
345
|
-
return tool.name && allowedToolNames.has(tool.name);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Combine and deduplicate
|
|
349
|
-
const allAllowedTools = [...allowedTools, ...specificNamedTools];
|
|
350
|
-
return allAllowedTools.filter(
|
|
351
|
-
(tool, index, arr) => arr.findIndex((t) => t.name === tool.name) === index
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Get default tools based on filesystem configuration
|
|
357
|
-
*/
|
|
358
|
-
export function getDefaultTools(
|
|
359
|
-
filesystemConfig: string[] | 'all' | 'read' | 'none' = 'none'
|
|
360
|
-
): StructuredToolInterface[] {
|
|
361
|
-
return [...filterFilesystemTools(new GthFileSystemToolkit([getCurrentDir()]), filesystemConfig)];
|
|
362
|
-
}
|
package/src/core/Invocation.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Message } from '#src/modules/types.js';
|
|
2
2
|
import { AIMessage, isAIMessage } from '@langchain/core/messages';
|
|
3
|
-
import {
|
|
3
|
+
import { SlothConfig } from '#src/config.js';
|
|
4
4
|
import type { Connection } from '@langchain/mcp-adapters';
|
|
5
5
|
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
|
|
6
6
|
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
@@ -10,6 +10,7 @@ import { type RunnableConfig } from '@langchain/core/runnables';
|
|
|
10
10
|
import { ToolCall } from '@langchain/core/messages/tool';
|
|
11
11
|
import { GthCommand, StatusLevel } from '#src/core/types.js';
|
|
12
12
|
import { BaseToolkit, StructuredToolInterface } from '@langchain/core/tools';
|
|
13
|
+
import { getDefaultTools } from '#src/builtInToolsConfig.js';
|
|
13
14
|
|
|
14
15
|
export type StatusUpdateCallback = (level: StatusLevel, message: string) => void;
|
|
15
16
|
|
|
@@ -43,7 +44,7 @@ export class Invocation {
|
|
|
43
44
|
this.mcpClient = this.getMcpClient(this.config);
|
|
44
45
|
|
|
45
46
|
// Get default filesystem tools (filtered based on config)
|
|
46
|
-
const defaultTools = getDefaultTools(
|
|
47
|
+
const defaultTools = await getDefaultTools(config);
|
|
47
48
|
|
|
48
49
|
// Get user config tools
|
|
49
50
|
const flattenedConfigTools = this.extractAndFlattenTools(this.config.tools || []);
|
|
@@ -81,6 +82,10 @@ export class Invocation {
|
|
|
81
82
|
command && config.commands?.[command]?.filesystem !== undefined
|
|
82
83
|
? config.commands[command].filesystem!
|
|
83
84
|
: config.filesystem,
|
|
85
|
+
builtInTools:
|
|
86
|
+
command && config.commands?.[command]?.builtInTools !== undefined
|
|
87
|
+
? config.commands[command].builtInTools!
|
|
88
|
+
: config.builtInTools,
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { env } from '#src/systemUtils.js';
|
|
2
|
+
import type { JiraConfig } from '#src/providers/types.js';
|
|
3
|
+
import { ProgressIndicator } from '#src/utils.js';
|
|
4
|
+
|
|
5
|
+
export function getJiraCredentials(config: Partial<JiraConfig> | null): JiraConfig {
|
|
6
|
+
if (!config) {
|
|
7
|
+
throw new Error('No Jira config provided');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const username = env.JIRA_USERNAME || config.username;
|
|
11
|
+
if (!username) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'Missing JIRA username. The username can be defined as JIRA_USERNAME environment variable or as "username" in config.'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const token = env.JIRA_API_PAT_TOKEN || config.token;
|
|
18
|
+
if (!token) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'Missing JIRA PAT token. The token can be defined as JIRA_API_PAT_TOKEN environment variable or as "token" in config.'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cloudId = env.JIRA_CLOUD_ID || config.cloudId;
|
|
25
|
+
if (!cloudId) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Missing JIRA Cloud ID. The Cloud ID can be defined as JIRA_CLOUD_ID environment variable or as "cloudId" in config.'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
username,
|
|
33
|
+
token,
|
|
34
|
+
cloudId,
|
|
35
|
+
displayUrl: config.displayUrl,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getJiraHeaders(config: JiraConfig): Record<string, string> {
|
|
40
|
+
const credentials = `${config.username}:${config.token}`;
|
|
41
|
+
const encodedCredentials = Buffer.from(credentials).toString('base64');
|
|
42
|
+
const authHeader = `Basic ${encodedCredentials}`;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
Authorization: authHeader,
|
|
46
|
+
Accept: 'application/json; charset=utf-8',
|
|
47
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function jiraRequest<T>(
|
|
53
|
+
config: JiraConfig,
|
|
54
|
+
endpoint: string,
|
|
55
|
+
options: RequestInit = {},
|
|
56
|
+
showProgress = true
|
|
57
|
+
): Promise<T> {
|
|
58
|
+
const apiUrl = `https://api.atlassian.com/ex/jira/${config.cloudId}${endpoint}`;
|
|
59
|
+
const headers = getJiraHeaders(config);
|
|
60
|
+
|
|
61
|
+
let progressIndicator: ProgressIndicator | undefined;
|
|
62
|
+
if (showProgress) {
|
|
63
|
+
progressIndicator = new ProgressIndicator(
|
|
64
|
+
`${options.method || 'GET'} ${apiUrl.replace(/^https?:\/\//, '')}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(apiUrl, {
|
|
70
|
+
...options,
|
|
71
|
+
headers: {
|
|
72
|
+
...headers,
|
|
73
|
+
...options.headers,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (progressIndicator) {
|
|
78
|
+
progressIndicator.stop();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
let errorMessage = `Failed to fetch from Jira: ${response.statusText}`;
|
|
83
|
+
try {
|
|
84
|
+
const errorData = await response.json();
|
|
85
|
+
errorMessage += ` - ${JSON.stringify(errorData)}`;
|
|
86
|
+
} catch {
|
|
87
|
+
// If we can't parse JSON error, use the basic message
|
|
88
|
+
}
|
|
89
|
+
throw new Error(errorMessage);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return response.json() as Promise<T>;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (progressIndicator) {
|
|
95
|
+
progressIndicator.stop();
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|