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.
Files changed (46) hide show
  1. package/.gsloth.config_.json +2 -2
  2. package/.gsloth.config_claude.mjs +18 -0
  3. package/.gsloth.config_vertex.json +6 -0
  4. package/README.md +1 -1
  5. package/assets/release-notes/v0_7_3.md +64 -0
  6. package/dist/builtInToolsConfig.d.ts +6 -0
  7. package/dist/builtInToolsConfig.js +73 -0
  8. package/dist/builtInToolsConfig.js.map +1 -0
  9. package/dist/commands/prCommand.js +14 -1
  10. package/dist/commands/prCommand.js.map +1 -1
  11. package/dist/config.d.ts +18 -7
  12. package/dist/config.js +1 -41
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/Invocation.js +5 -2
  15. package/dist/core/Invocation.js.map +1 -1
  16. package/dist/helpers/jira/jiraClient.d.ts +4 -0
  17. package/dist/helpers/jira/jiraClient.js +75 -0
  18. package/dist/helpers/jira/jiraClient.js.map +1 -0
  19. package/dist/helpers/jira/jiraLogWork.d.ts +2 -0
  20. package/dist/helpers/jira/jiraLogWork.js +53 -0
  21. package/dist/helpers/jira/jiraLogWork.js.map +1 -0
  22. package/dist/providers/jiraIssueProvider.js +9 -52
  23. package/dist/providers/jiraIssueProvider.js.map +1 -1
  24. package/dist/tools/gthJiraLogWorkTool.d.ts +3 -0
  25. package/dist/tools/gthJiraLogWorkTool.js +37 -0
  26. package/dist/tools/gthJiraLogWorkTool.js.map +1 -0
  27. package/dist/tools/gthStatusUpdateTool.d.ts +2 -0
  28. package/dist/tools/gthStatusUpdateTool.js +16 -0
  29. package/dist/tools/gthStatusUpdateTool.js.map +1 -0
  30. package/docs/DEVELOPMENT.md +26 -0
  31. package/package.json +1 -1
  32. package/src/builtInToolsConfig.ts +99 -0
  33. package/src/commands/prCommand.ts +28 -3
  34. package/src/config.ts +20 -62
  35. package/src/core/Invocation.ts +7 -2
  36. package/src/helpers/jira/jiraClient.ts +99 -0
  37. package/src/helpers/jira/jiraLogWork.ts +81 -0
  38. package/src/providers/jiraIssueProvider.ts +13 -70
  39. package/src/tools/gthJiraLogWorkTool.ts +51 -0
  40. package/src/tools/gthStatusUpdateTool.ts +18 -0
  41. package/vitest-it.config.ts +1 -1
  42. package/vitest.config.ts +1 -0
  43. package/dist/tools/statusUpdate.d.ts +0 -1
  44. package/dist/tools/statusUpdate.js +0 -12
  45. package/dist/tools/statusUpdate.js.map +0 -1
  46. package/src/tools/statusUpdate.ts +0 -15
@@ -1,6 +1,5 @@
1
1
  import { display, displayError, displayWarning } from '#src/consoleUtils.js';
2
- import { env } from '#src/systemUtils.js';
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
- // Get username from environment variable or config
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(config, jiraKey) {
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, read:avatar:jira, read:issue:jira, read:status:jira, read:user:jira, read:field-configuration:jira
72
- const apiUrl = `https://api.atlassian.com/ex/jira/${config.cloudId}/rest/api/2/issue/${jiraKey}`;
73
- if (config.displayUrl) {
74
- display(`Loading Jira issue ${config.displayUrl}${jiraKey}`);
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
- // Encode credentials for Basic Authentication header
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":"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"}
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,3 @@
1
+ import { type StructuredToolInterface } from '@langchain/core/tools';
2
+ import { SlothConfig } from '#src/config.js';
3
+ export declare function get(config: SlothConfig): StructuredToolInterface<import("@langchain/core/tools").ToolSchemaBase, any, any>;
@@ -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,2 @@
1
+ import { SlothConfig } from '#src/config.js';
2
+ export declare function get(_: SlothConfig): import("@langchain/core/tools").DynamicTool<void>;
@@ -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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaunt-sloth-assistant",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "Andrew Kondratev",
@@ -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 { getCurrentDir } from '#src/systemUtils.js';
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
- }
@@ -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 { getDefaultTools, SlothConfig } from '#src/config.js';
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(this.config.filesystem || 'none');
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
+ }