gaunt-sloth-assistant 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/{.gsloth.preamble.review.md → .gsloth.guidelines.md} +0 -8
  2. package/.gsloth.review.md +7 -0
  3. package/.prettierrc.json +9 -0
  4. package/README.md +177 -158
  5. package/ROADMAP.md +1 -1
  6. package/dist/commands/askCommand.d.ts +6 -0
  7. package/dist/commands/askCommand.js +27 -0
  8. package/dist/commands/askCommand.js.map +1 -0
  9. package/dist/commands/initCommand.d.ts +6 -0
  10. package/dist/commands/initCommand.js +16 -0
  11. package/dist/commands/initCommand.js.map +1 -0
  12. package/dist/commands/reviewCommand.d.ts +3 -0
  13. package/dist/commands/reviewCommand.js +142 -0
  14. package/dist/commands/reviewCommand.js.map +1 -0
  15. package/dist/config.d.ts +84 -0
  16. package/dist/config.js +180 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/configs/anthropic.d.ts +4 -0
  19. package/{src → dist}/configs/anthropic.js +45 -48
  20. package/dist/configs/anthropic.js.map +1 -0
  21. package/dist/configs/fake.d.ts +3 -0
  22. package/{src → dist}/configs/fake.js +11 -14
  23. package/dist/configs/fake.js.map +1 -0
  24. package/dist/configs/groq.d.ts +4 -0
  25. package/{src → dist}/configs/groq.js +10 -13
  26. package/dist/configs/groq.js.map +1 -0
  27. package/dist/configs/types.d.ts +14 -0
  28. package/dist/configs/types.js +2 -0
  29. package/dist/configs/types.js.map +1 -0
  30. package/dist/configs/vertexai.d.ts +4 -0
  31. package/{src → dist}/configs/vertexai.js +44 -47
  32. package/dist/configs/vertexai.js.map +1 -0
  33. package/dist/consoleUtils.d.ts +6 -0
  34. package/{src → dist}/consoleUtils.js +10 -15
  35. package/dist/consoleUtils.js.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +26 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/llmUtils.d.ts +4 -0
  40. package/dist/llmUtils.js +39 -0
  41. package/dist/llmUtils.js.map +1 -0
  42. package/dist/modules/questionAnsweringModule.d.ts +7 -0
  43. package/dist/modules/questionAnsweringModule.js +33 -0
  44. package/dist/modules/questionAnsweringModule.js.map +1 -0
  45. package/dist/modules/reviewModule.d.ts +1 -0
  46. package/dist/modules/reviewModule.js +29 -0
  47. package/dist/modules/reviewModule.js.map +1 -0
  48. package/dist/modules/types.d.ts +18 -0
  49. package/dist/modules/types.js +2 -0
  50. package/dist/modules/types.js.map +1 -0
  51. package/dist/prompt.d.ts +8 -0
  52. package/dist/prompt.js +45 -0
  53. package/dist/prompt.js.map +1 -0
  54. package/dist/providers/file.d.ts +8 -0
  55. package/dist/providers/file.js +20 -0
  56. package/dist/providers/file.js.map +1 -0
  57. package/dist/providers/ghPrDiffProvider.d.ts +8 -0
  58. package/dist/providers/ghPrDiffProvider.js +16 -0
  59. package/dist/providers/ghPrDiffProvider.js.map +1 -0
  60. package/dist/providers/jiraIssueLegacyAccessTokenProvider.d.ts +8 -0
  61. package/dist/providers/jiraIssueLegacyAccessTokenProvider.js +62 -0
  62. package/dist/providers/jiraIssueLegacyAccessTokenProvider.js.map +1 -0
  63. package/dist/providers/jiraIssueLegacyProvider.d.ts +8 -0
  64. package/dist/providers/jiraIssueLegacyProvider.js +74 -0
  65. package/dist/providers/jiraIssueLegacyProvider.js.map +1 -0
  66. package/dist/providers/jiraIssueProvider.d.ts +11 -0
  67. package/dist/providers/jiraIssueProvider.js +96 -0
  68. package/dist/providers/jiraIssueProvider.js.map +1 -0
  69. package/dist/providers/text.d.ts +8 -0
  70. package/dist/providers/text.js +10 -0
  71. package/dist/providers/text.js.map +1 -0
  72. package/dist/providers/types.d.ts +21 -0
  73. package/dist/providers/types.js +2 -0
  74. package/dist/providers/types.js.map +1 -0
  75. package/dist/systemUtils.d.ts +32 -0
  76. package/dist/systemUtils.js +70 -0
  77. package/dist/systemUtils.js.map +1 -0
  78. package/dist/utils.d.ts +49 -0
  79. package/dist/utils.js +192 -0
  80. package/dist/utils.js.map +1 -0
  81. package/docs/CONFIGURATION.md +99 -10
  82. package/docs/RELEASE-HOWTO.md +7 -1
  83. package/eslint.config.js +99 -21
  84. package/gth-ASK-2025-05-16T14-11-39.md +3 -0
  85. package/gth-ASK-2025-05-16T14-18-27.md +3 -0
  86. package/gth-ASK-2025-05-16T14-18-56.md +1 -0
  87. package/gth-ASK-2025-05-16T14-41-20.md +3 -0
  88. package/gth-ASK-2025-05-16T14-43-31.md +51 -0
  89. package/gth-ASK-2025-05-16T16-05-52.md +62 -0
  90. package/gth-DIFF-review-2025-05-16T16-07-53.md +56 -0
  91. package/gth-DIFF-review-2025-05-16T16-18-55.md +292 -0
  92. package/index.js +10 -27
  93. package/package.json +26 -15
  94. package/src/commands/askCommand.ts +35 -0
  95. package/src/commands/initCommand.ts +19 -0
  96. package/src/commands/reviewCommand.ts +223 -0
  97. package/src/config.ts +269 -0
  98. package/src/configs/anthropic.ts +57 -0
  99. package/src/configs/fake.ts +15 -0
  100. package/src/configs/groq.ts +54 -0
  101. package/src/configs/vertexai.ts +53 -0
  102. package/src/consoleUtils.ts +33 -0
  103. package/src/index.ts +30 -0
  104. package/src/llmUtils.ts +54 -0
  105. package/src/modules/questionAnsweringModule.ts +44 -0
  106. package/src/modules/reviewModule.ts +31 -0
  107. package/src/modules/types.ts +23 -0
  108. package/src/prompt.ts +54 -0
  109. package/src/providers/file.ts +24 -0
  110. package/src/providers/ghPrDiffProvider.ts +20 -0
  111. package/src/providers/jiraIssueLegacyProvider.ts +103 -0
  112. package/src/providers/jiraIssueProvider.ts +133 -0
  113. package/src/providers/text.ts +14 -0
  114. package/src/providers/types.ts +24 -0
  115. package/src/systemUtils.ts +90 -0
  116. package/src/utils.ts +232 -0
  117. package/tsconfig.json +24 -0
  118. package/vitest.config.ts +13 -0
  119. package/.eslint.config.mjs +0 -72
  120. package/.github/dependabot.yml +0 -11
  121. package/.github/workflows/ci.yml +0 -33
  122. package/spec/.gsloth.config.js +0 -22
  123. package/spec/.gsloth.config.json +0 -25
  124. package/spec/askCommand.spec.js +0 -92
  125. package/spec/config.spec.js +0 -421
  126. package/spec/initCommand.spec.js +0 -55
  127. package/spec/predefinedConfigs.spec.js +0 -100
  128. package/spec/questionAnsweringModule.spec.js +0 -137
  129. package/spec/reviewCommand.spec.js +0 -222
  130. package/spec/reviewModule.spec.js +0 -28
  131. package/spec/support/jasmine.mjs +0 -14
  132. package/src/commands/askCommand.js +0 -27
  133. package/src/commands/initCommand.js +0 -17
  134. package/src/commands/reviewCommand.js +0 -154
  135. package/src/config.js +0 -177
  136. package/src/modules/questionAnsweringModule.js +0 -82
  137. package/src/modules/reviewModule.js +0 -70
  138. package/src/prompt.js +0 -34
  139. package/src/providers/file.js +0 -19
  140. package/src/providers/ghPrDiffProvider.js +0 -11
  141. package/src/providers/jiraIssueLegacyAccessTokenProvider.js +0 -84
  142. package/src/providers/text.js +0 -6
  143. package/src/systemUtils.js +0 -32
  144. package/src/utils.js +0 -173
  145. /package/{.gsloth.preamble.internal.md → .gsloth.backstory.md} +0 -0
@@ -0,0 +1,33 @@
1
+ import chalk from 'chalk';
2
+ import { debug as systemDebug, error as systemError, log } from '#src/systemUtils.js';
3
+
4
+ // TODO it seems like commander supports coloured output, maybe dependency to chalk can be removed
5
+
6
+ export function displayError(message: string): void {
7
+ systemError(chalk.red(message));
8
+ }
9
+
10
+ export function displayWarning(message: string): void {
11
+ systemError(chalk.yellow(message));
12
+ }
13
+
14
+ export function displaySuccess(message: string): void {
15
+ systemError(chalk.green(message));
16
+ }
17
+
18
+ export function displayInfo(message: string): void {
19
+ systemError(chalk.blue(message));
20
+ }
21
+
22
+ export function display(message: string): void {
23
+ log(message);
24
+ }
25
+
26
+ export function displayDebug(message: string | Error | undefined): void {
27
+ // TODO make it controlled by config
28
+ if (message instanceof Error) {
29
+ systemDebug(message.stack || '');
30
+ } else if (message !== undefined) {
31
+ systemDebug(message);
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { Command } from 'commander';
2
+ import { askCommand } from '#src/commands/askCommand.js';
3
+ import { initCommand } from '#src/commands/initCommand.js';
4
+ import { reviewCommand } from '#src/commands/reviewCommand.js';
5
+ import { slothContext } from '#src/config.js';
6
+ import { getSlothVersion } from '#src/utils.js';
7
+ import { argv, readStdin } from '#src/systemUtils.js';
8
+ import { setVerbose } from '#src/llmUtils.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('gsloth')
14
+ .description('Gaunt Sloth Assistant reviewing your PRs')
15
+ .version(getSlothVersion())
16
+ .option('--verbose', 'Print entire prompt sent to LLM.');
17
+
18
+ // Parse global options before binding any commands
19
+ program.parseOptions(argv);
20
+ if (program.getOptionValue('verbose')) {
21
+ // Set global prompt debug
22
+ setVerbose(true);
23
+ }
24
+
25
+ initCommand(program);
26
+ reviewCommand(program, slothContext);
27
+ askCommand(program);
28
+ // TODO add general interactive chat command
29
+
30
+ await readStdin(program);
@@ -0,0 +1,54 @@
1
+ import type { Message, State } from '#src/modules/types.js';
2
+ import { AIMessageChunk, HumanMessage, SystemMessage } from '@langchain/core/messages';
3
+ import { BaseChatModel } from '@langchain/core/language_models/chat_models';
4
+ import { END, MemorySaver, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
5
+ import { BaseLanguageModelCallOptions } from '@langchain/core/language_models/base';
6
+
7
+ const llmGlobalSettings = {
8
+ verbose: false,
9
+ };
10
+
11
+ export async function invoke(
12
+ llm: BaseChatModel,
13
+ options: Partial<BaseLanguageModelCallOptions>,
14
+ systemMessage: string,
15
+ prompt: string
16
+ ): Promise<string> {
17
+ if (llmGlobalSettings.verbose) {
18
+ llm.verbose = true;
19
+ }
20
+ // This node receives the current state (messages) and invokes the LLM
21
+ const callModel = async (state: State): Promise<{ messages: AIMessageChunk }> => {
22
+ // state.messages will contain the list including the system systemMessage and user diff
23
+ const response = await (llm as BaseChatModel).invoke(state.messages);
24
+ // MessagesAnnotation expects the node to return the new message(s) to be added to the state.
25
+ // Wrap the response in an array if it's a single message object.
26
+ return { messages: response };
27
+ };
28
+
29
+ // Define the graph structure with MessagesAnnotation state
30
+ const workflow = new StateGraph(MessagesAnnotation)
31
+ // Define the node and edge
32
+ .addNode('model', callModel)
33
+ .addEdge(START, 'model') // Start at the 'model' node
34
+ .addEdge('model', END); // End after the 'model' node completes
35
+
36
+ // Set up memory (optional but good practice for potential future multi-turn interactions)
37
+ const memory = new MemorySaver();
38
+
39
+ // Compile the workflow into a runnable app
40
+ const app = workflow.compile({ checkpointer: memory });
41
+
42
+ // Construct the initial the messages including the systemMessage as a system message
43
+ const messages: Message[] = [new SystemMessage(systemMessage), new HumanMessage(prompt)];
44
+
45
+ const output = await app.invoke({ messages }, options);
46
+ const lastMessage = output.messages[output.messages.length - 1];
47
+ return typeof lastMessage.content === 'string'
48
+ ? lastMessage.content
49
+ : JSON.stringify(lastMessage.content);
50
+ }
51
+
52
+ export function setVerbose(debug: boolean) {
53
+ llmGlobalSettings.verbose = debug;
54
+ }
@@ -0,0 +1,44 @@
1
+ import { slothContext } from '#src/config.js';
2
+ import { display, displayError, displaySuccess } from '#src/consoleUtils.js';
3
+ import { getCurrentDir } from '#src/systemUtils.js';
4
+ import { fileSafeLocalDate, ProgressIndicator, toFileSafeString } from '#src/utils.js';
5
+ import { writeFileSync } from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import { invoke } from '#src/llmUtils.js';
8
+
9
+ /**
10
+ * Ask a question and get an answer from the LLM
11
+ * @param source - The source of the question (used for file naming)
12
+ * @param preamble - The preamble to send to the LLM
13
+ * @param content - The content of the question
14
+ */
15
+ export async function askQuestion(
16
+ source: string,
17
+ preamble: string,
18
+ content: string
19
+ ): Promise<void> {
20
+ const progressIndicator = new ProgressIndicator('Thinking.');
21
+ const outputContent = await invoke(
22
+ slothContext.config.llm,
23
+ slothContext.session,
24
+ preamble,
25
+ content
26
+ );
27
+ progressIndicator.stop();
28
+ const filePath = path.resolve(
29
+ getCurrentDir(),
30
+ toFileSafeString(source) + '-' + fileSafeLocalDate() + '.md'
31
+ );
32
+ display(`\nwriting ${filePath}`);
33
+ // TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
34
+ display('\n' + outputContent);
35
+ try {
36
+ writeFileSync(filePath, outputContent);
37
+ displaySuccess(`This report can be found in ${filePath}`);
38
+ } catch (error) {
39
+ displayError(`Failed to write answer to file: ${filePath}`);
40
+ displayError(error instanceof Error ? error.message : String(error));
41
+ // TODO Consider if we want to exit or just log the error
42
+ // exit(1);
43
+ }
44
+ }
@@ -0,0 +1,31 @@
1
+ import { slothContext } from '#src/config.js';
2
+ import { display, displayDebug, displayError, displaySuccess } from '#src/consoleUtils.js';
3
+ import { getCurrentDir, stdout } from '#src/systemUtils.js';
4
+ import { fileSafeLocalDate, ProgressIndicator, toFileSafeString } from '#src/utils.js';
5
+ import { writeFileSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { invoke } from '#src/llmUtils.js';
8
+
9
+ export async function review(source: string, preamble: string, diff: string): Promise<void> {
10
+ const progressIndicator = new ProgressIndicator('Reviewing.');
11
+ const outputContent = await invoke(slothContext.config.llm, slothContext.session, preamble, diff);
12
+ progressIndicator.stop();
13
+ const filePath = path.resolve(
14
+ getCurrentDir(),
15
+ toFileSafeString(source) + '-' + fileSafeLocalDate() + '.md'
16
+ );
17
+ stdout.write('\n');
18
+ display(`writing ${filePath}`);
19
+ stdout.write('\n');
20
+ // TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
21
+ display(outputContent);
22
+ try {
23
+ writeFileSync(filePath, outputContent);
24
+ displaySuccess(`This report can be found in ${filePath}`);
25
+ } catch (error) {
26
+ displayDebug(error instanceof Error ? error : String(error));
27
+ displayError(`Failed to write review to file: ${filePath}`);
28
+ // Consider if you want to exit or just log the error
29
+ // exit(1);
30
+ }
31
+ }
@@ -0,0 +1,23 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+
3
+ export type Message = BaseMessage;
4
+
5
+ export interface State {
6
+ messages: Message[];
7
+ }
8
+
9
+ export interface ProgressCallback {
10
+ (): void;
11
+ }
12
+
13
+ export interface ReviewOptions {
14
+ source: string;
15
+ preamble: string;
16
+ diff: string;
17
+ }
18
+
19
+ export interface QuestionOptions {
20
+ source: string;
21
+ preamble: string;
22
+ content: string;
23
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,54 @@
1
+ import {
2
+ readFileFromCurrentDir,
3
+ readFileFromCurrentOrInstallDir,
4
+ spawnCommand,
5
+ } from '#src/utils.js';
6
+ import { displayError } from '#src/consoleUtils.js';
7
+ import { exit } from '#src/systemUtils.js';
8
+ import { GSLOTH_BACKSTORY } from '#src/config.js';
9
+
10
+ export function readBackstory(): string {
11
+ return readFileFromCurrentOrInstallDir(GSLOTH_BACKSTORY, true);
12
+ }
13
+
14
+ export function readGuidelines(guidelinesFilename: string): string {
15
+ try {
16
+ return readFileFromCurrentDir(guidelinesFilename);
17
+ } catch (error) {
18
+ displayError(
19
+ 'Consider running `gsloth init` to set up your project. Check `gsloth init --help` to see options.'
20
+ );
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ export function readReviewInstructions(reviewInstructions: string): string {
26
+ return readConfigPromptFile(reviewInstructions);
27
+ }
28
+
29
+ function readConfigPromptFile(guidelinesFilename: string): string {
30
+ try {
31
+ return readFileFromCurrentOrInstallDir(guidelinesFilename);
32
+ } catch (error) {
33
+ displayError(
34
+ 'Consider running `gsloth init` to set up your project. Check `gsloth init --help` to see options.'
35
+ );
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * This function expects https://cli.github.com/ to be installed and authenticated.
42
+ * It does something like `gh pr diff 42`
43
+ */
44
+ export async function getPrDiff(pr: string): Promise<string> {
45
+ // TODO makes sense to check if gh is available and authenticated
46
+ try {
47
+ return await spawnCommand('gh', ['pr', 'diff', pr], 'Loading PR diff...', 'Loaded PR diff.');
48
+ } catch (e) {
49
+ displayError(e instanceof Error ? e.toString() : String(e));
50
+ displayError(`Failed to call "gh pr diff ${pr}", see message above for details.`);
51
+ exit();
52
+ return ''; // This line will never be reached due to exit()
53
+ }
54
+ }
@@ -0,0 +1,24 @@
1
+ import { resolve } from 'node:path';
2
+ import { display } from '#src/consoleUtils.js';
3
+ import { readFileSyncWithMessages } from '#src/utils.js';
4
+ import { getCurrentDir } from '#src/systemUtils.js';
5
+ import type { ProviderConfig } from './types.js';
6
+
7
+ /**
8
+ * Reads the text file from current dir
9
+ * @param _ config (unused in this provider)
10
+ * @param fileName
11
+ * @returns file contents
12
+ */
13
+ export async function get(
14
+ _: ProviderConfig | null,
15
+ fileName: string | undefined
16
+ ): Promise<string | null> {
17
+ if (!fileName) {
18
+ return null;
19
+ }
20
+ const currentDir = getCurrentDir();
21
+ const filePath = resolve(currentDir, fileName);
22
+ display(`Reading file ${fileName}...`);
23
+ return readFileSyncWithMessages(filePath);
24
+ }
@@ -0,0 +1,20 @@
1
+ import { displayWarning } from '#src/consoleUtils.js';
2
+ import { execAsync } from '#src/utils.js';
3
+ import type { ProviderConfig } from './types.js';
4
+
5
+ /**
6
+ * Gets PR diff using gh command line tool
7
+ * @param _ config (unused in this provider)
8
+ * @param pr PR number
9
+ * @returns PR diff
10
+ */
11
+ export async function get(
12
+ _: ProviderConfig | null,
13
+ pr: string | undefined
14
+ ): Promise<string | null> {
15
+ if (!pr) {
16
+ displayWarning('No PR provided');
17
+ return null;
18
+ }
19
+ return execAsync(`gh pr diff ${pr}`);
20
+ }
@@ -0,0 +1,103 @@
1
+ import { display, displayError, displayWarning } from '#src/consoleUtils.js';
2
+ import { env } from '#src/systemUtils.js';
3
+ import type { JiraLegacyConfig } from '#src/providers/types.js';
4
+
5
+ interface JiraIssueResponse {
6
+ fields: {
7
+ summary: string;
8
+ description: string;
9
+ [key: string]: unknown;
10
+ };
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ /**
15
+ * Gets Jira issue using Atlassian REST API v2 with unscoped API token (aka Legacy Token)
16
+ * @param config Jira configuration
17
+ * @param issueId Jira issue ID
18
+ * @returns Jira issue content
19
+ */
20
+ export async function get(
21
+ config: Partial<JiraLegacyConfig>,
22
+ issueId: string | undefined
23
+ ): Promise<string | null> {
24
+ if (!config) {
25
+ displayWarning('No Jira config provided');
26
+ return null;
27
+ }
28
+ if (!issueId) {
29
+ displayWarning('No issue ID provided');
30
+ return null;
31
+ }
32
+ if (!config.baseUrl) {
33
+ displayWarning('No Jira base URL provided');
34
+ return null;
35
+ }
36
+
37
+ // Get username from environment variable or config
38
+ const username = env.JIRA_USERNAME || config.username;
39
+ if (!username) {
40
+ throw new Error(
41
+ 'Missing JIRA username. The username can be defined as JIRA_USERNAME environment variable or as "username" in config.'
42
+ );
43
+ }
44
+
45
+ // Get token from environment variable or config
46
+ const token = env.JIRA_LEGACY_API_TOKEN || config.token;
47
+ if (!token) {
48
+ throw new Error(
49
+ 'Missing JIRA Legacy API token. The legacy token can be defined as JIRA_LEGACY_API_TOKEN environment variable or as "token" in config.'
50
+ );
51
+ }
52
+
53
+ try {
54
+ const issue = await getJiraIssue(
55
+ {
56
+ ...(config as JiraLegacyConfig),
57
+ username,
58
+ token,
59
+ },
60
+ issueId
61
+ );
62
+ if (!issue) {
63
+ return null;
64
+ }
65
+
66
+ const summary = issue.fields.summary;
67
+ const description = issue.fields.description;
68
+
69
+ return `Jira Issue: ${issueId}\nSummary: ${summary}\n\nDescription:\n${description}`;
70
+ } catch (error) {
71
+ displayError(
72
+ `Failed to get Jira issue: ${error instanceof Error ? error.message : String(error)}`
73
+ );
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Helper function to get Jira issue details
80
+ * @param config Jira configuration
81
+ * @param issueId Jira issue ID
82
+ * @returns Jira issue response
83
+ */
84
+ async function getJiraIssue(config: JiraLegacyConfig, issueId: string): Promise<JiraIssueResponse> {
85
+ const auth = Buffer.from(`${config.username}:${config.token}`).toString('base64');
86
+ const url = `${config.baseUrl}${issueId}`;
87
+ if (config.displayUrl) {
88
+ display(`Loading Jira issue ${config.displayUrl}${issueId}`);
89
+ }
90
+ display(`Retrieving jira from api ${url.replace(/^https?:\/\//, '')}`);
91
+ const response = await fetch(url, {
92
+ headers: {
93
+ Authorization: `Basic ${auth}`,
94
+ Accept: 'application/json',
95
+ },
96
+ });
97
+
98
+ if (!response.ok) {
99
+ throw new Error(`Failed to fetch Jira issue from ${url} with status: ${response.statusText}`);
100
+ }
101
+
102
+ return response.json();
103
+ }
@@ -0,0 +1,133 @@
1
+ import { display, displayError, displayWarning } from '#src/consoleUtils.js';
2
+ import { env } from '#src/systemUtils.js';
3
+ import type { JiraConfig } from './types.js';
4
+
5
+ interface JiraIssueResponse {
6
+ fields: {
7
+ summary: string;
8
+ description: string;
9
+ [key: string]: unknown;
10
+ };
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ /**
15
+ * Gets Jira issue using Atlassian REST API v3 with Personal Access Token
16
+ *
17
+ * TODO we need to figure out how would this work with public jira.
18
+ *
19
+ * @param config Jira configuration
20
+ * @param issueId Jira issue ID
21
+ * @returns Jira issue content
22
+ */
23
+ export async function get(
24
+ config: Partial<JiraConfig> | null,
25
+ issueId: string | undefined
26
+ ): Promise<string | null> {
27
+ if (!config) {
28
+ displayWarning('No Jira config provided');
29
+ return null;
30
+ }
31
+ if (!issueId) {
32
+ displayWarning('No issue ID provided');
33
+ return null;
34
+ }
35
+
36
+ // Get username from environment variable or config
37
+ const username = env.JIRA_USERNAME || config.username;
38
+ if (!username) {
39
+ throw new Error(
40
+ 'Missing JIRA username. The username can be defined as JIRA_USERNAME environment variable or as "username" in config.'
41
+ );
42
+ }
43
+
44
+ // Get token from environment variable or config
45
+ const token = env.JIRA_API_PAT_TOKEN || config.token;
46
+ if (!token) {
47
+ throw new Error(
48
+ 'Missing JIRA PAT token. The token can be defined as JIRA_API_PAT_TOKEN environment variable or as "token" in config.'
49
+ );
50
+ }
51
+
52
+ // Get cloud ID from environment variable or config
53
+ const cloudId = env.JIRA_CLOUD_ID || config.cloudId;
54
+ if (!cloudId) {
55
+ throw new Error(
56
+ 'Missing JIRA Cloud ID. The Cloud ID can be defined as JIRA_CLOUD_ID environment variable or as "cloudId" in config.'
57
+ );
58
+ }
59
+
60
+ try {
61
+ const issue = await getJiraIssue(
62
+ {
63
+ ...config,
64
+ username,
65
+ token,
66
+ cloudId,
67
+ },
68
+ issueId
69
+ );
70
+ if (!issue) {
71
+ return null;
72
+ }
73
+
74
+ const summary = issue.fields.summary;
75
+ const description = issue.fields.description;
76
+
77
+ return `Jira Issue: ${issueId}\nSummary: ${summary}\n\nDescription:\n${description}`;
78
+ } catch (error) {
79
+ displayError(
80
+ `Failed to get Jira issue: ${error instanceof Error ? error.message : String(error)}`
81
+ );
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Helper function to get Jira issue details using Atlassian REST API v3
88
+ * @param config Jira configuration
89
+ * @param jiraKey Jira issue ID
90
+ * @returns Jira issue response
91
+ */
92
+ async function getJiraIssue(config: JiraConfig, jiraKey: string): Promise<JiraIssueResponse> {
93
+ // Jira Cloud ID can be found by authenticated user at https://company.atlassian.net/_edge/tenant_info
94
+
95
+ // 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:
96
+ // either Classic (RECOMMENDED) read:jira-work
97
+ // 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
98
+ const apiUrl = `https://api.atlassian.com/ex/jira/${config.cloudId}/rest/api/3/issue/${jiraKey}`;
99
+ if (config.displayUrl) {
100
+ display(`Loading Jira issue ${config.displayUrl}${jiraKey}`);
101
+ }
102
+ display(`Retrieving jira from api ${apiUrl.replace(/^https?:\/\//, '')}`);
103
+
104
+ // Encode credentials for Basic Authentication header
105
+ const credentials = `${config.username}:${config.token}`;
106
+ const encodedCredentials = Buffer.from(credentials).toString('base64');
107
+ const authHeader = `Basic ${encodedCredentials}`;
108
+
109
+ // Define request headers
110
+ const headers = {
111
+ Authorization: authHeader,
112
+ Accept: 'application/json; charset=utf-8',
113
+ 'Accept-Language': 'en-US,en;q=0.9', // Prevents errors in other languages
114
+ };
115
+
116
+ const response = await fetch(apiUrl, {
117
+ method: 'GET',
118
+ headers: headers,
119
+ });
120
+
121
+ if (!response?.ok) {
122
+ try {
123
+ const errorData = await response.json();
124
+ throw new Error(
125
+ `Failed to fetch Jira issue: ${response.statusText} - ${JSON.stringify(errorData)}`
126
+ );
127
+ } catch (_e) {
128
+ throw new Error(`Failed to fetch Jira issue: ${response?.statusText}`);
129
+ }
130
+ }
131
+
132
+ return response.json();
133
+ }
@@ -0,0 +1,14 @@
1
+ import type { ProviderConfig } from './types.js';
2
+
3
+ /**
4
+ * Returns the provided text as is
5
+ * @param _ config (unused in this provider)
6
+ * @param text Text to return
7
+ * @returns The provided text
8
+ */
9
+ export async function get(
10
+ _: ProviderConfig | null,
11
+ text: string | undefined
12
+ ): Promise<string | null> {
13
+ return text ?? null;
14
+ }
@@ -0,0 +1,24 @@
1
+ export interface ProviderConfig {
2
+ username?: string;
3
+ token?: string;
4
+ baseUrl?: string;
5
+ [key: string]: unknown;
6
+ }
7
+
8
+ export interface JiraLegacyConfig extends ProviderConfig {
9
+ username: string;
10
+ baseUrl: string;
11
+ displayUrl?: string;
12
+ token: string;
13
+ }
14
+
15
+ export interface JiraConfig extends ProviderConfig {
16
+ cloudId: string;
17
+ username: string;
18
+ displayUrl?: string;
19
+ token: string;
20
+ }
21
+
22
+ export interface Provider {
23
+ get: (config: ProviderConfig | null, id: string | undefined) => Promise<string | null>;
24
+ }
@@ -0,0 +1,90 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'url';
3
+ import { Command } from 'commander';
4
+ import { ProgressIndicator } from '#src/utils.js';
5
+
6
+ /**
7
+ * This file contains all system functions and objects that are globally available
8
+ * but not imported directly, such as process.stdin, process.stdout, process.argv,
9
+ * process.env, process.cwd(), process.exit(), etc.
10
+ *
11
+ * By centralizing these in one file, we improve testability and make it easier
12
+ * to mock these dependencies in tests.
13
+ */
14
+
15
+ interface InnerState {
16
+ installDir: string | null | undefined;
17
+ stringFromStdin: string;
18
+ }
19
+
20
+ const innerState: InnerState = {
21
+ installDir: undefined,
22
+ stringFromStdin: '',
23
+ };
24
+
25
+ // Process-related functions and objects
26
+ export const getCurrentDir = (): string => process.cwd();
27
+ export const getInstallDir = (): string => {
28
+ if (innerState.installDir) {
29
+ return innerState.installDir;
30
+ }
31
+ throw new Error('Install directory not set');
32
+ };
33
+ /**
34
+ * Cached string from stdin. Should only be called after readStdin completes execution.
35
+ */
36
+ export const getStringFromStdin = (): string => {
37
+ return innerState.stringFromStdin;
38
+ };
39
+ export const exit = (code?: number): never => process.exit(code || 0);
40
+ export const stdin = process.stdin;
41
+ export const stdout = process.stdout;
42
+ export const argv = process.argv;
43
+ export const env = process.env;
44
+
45
+ // noinspection JSUnusedGlobalSymbols
46
+ /**
47
+ * Provide the path to the entry point of the application.
48
+ * This is used to set the install directory.
49
+ * This is called from index.js root entry point.
50
+ */
51
+ export const setEntryPoint = (indexJs: string): void => {
52
+ const filePath = fileURLToPath(indexJs);
53
+ const dirPath = dirname(filePath);
54
+ innerState.installDir = resolve(dirPath);
55
+ };
56
+
57
+ /**
58
+ * Asynchronously reads the stdin and stores it as a string,
59
+ * it can later be retrieved with getStringFromStdin.
60
+ */
61
+ export function readStdin(program: Command): Promise<void> {
62
+ return new Promise((resolvePromise) => {
63
+ if (stdin.isTTY) {
64
+ program.parseAsync().then(() => resolvePromise());
65
+ } else {
66
+ // Support piping diff into gsloth
67
+ const progressIndicator = new ProgressIndicator('reading STDIN', true);
68
+
69
+ stdin.on('readable', function (this: NodeJS.ReadStream) {
70
+ const chunk = this.read();
71
+ progressIndicator.indicate();
72
+ if (chunk !== null) {
73
+ const chunkStr = chunk.toString('utf8');
74
+ innerState.stringFromStdin = innerState.stringFromStdin + chunkStr;
75
+ }
76
+ });
77
+
78
+ stdin.on('end', function () {
79
+ program.parseAsync(argv).then(() => resolvePromise());
80
+ });
81
+ }
82
+ });
83
+ }
84
+
85
+ // Console-related functions
86
+ export const log = (message: string): void => console.log(message);
87
+ export const error = (message: string): void => console.error(message);
88
+ export const warn = (message: string): void => console.warn(message);
89
+ export const info = (message: string): void => console.info(message);
90
+ export const debug = (message: string): void => console.debug(message);