gaunt-sloth-assistant 0.1.2 → 0.1.4
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/.github/workflows/ci.yml +33 -0
- package/README.md +59 -33
- package/ROADMAP.md +20 -20
- package/UX-RESEARCH.md +1 -1
- package/docs/CONFIGURATION.md +96 -6
- package/docs/DEVELOPMENT.md +12 -0
- package/eslint.config.js +38 -0
- package/index.js +4 -2
- package/package.json +7 -3
- package/spec/.gsloth.config.js +1 -1
- package/spec/.gsloth.config.json +25 -0
- package/spec/askCommand.spec.js +39 -5
- package/spec/config.spec.js +421 -0
- package/spec/initCommand.spec.js +3 -2
- package/spec/predefinedConfigs.spec.js +100 -0
- package/spec/questionAnsweringModule.spec.js +14 -14
- package/spec/reviewCommand.spec.js +86 -8
- package/spec/reviewModule.spec.js +7 -1
- package/src/commands/askCommand.js +5 -4
- package/src/commands/initCommand.js +2 -1
- package/src/commands/reviewCommand.js +19 -12
- package/src/config.js +139 -25
- package/src/configs/anthropic.js +28 -5
- package/src/configs/fake.js +15 -0
- package/src/configs/groq.js +27 -4
- package/src/configs/vertexai.js +23 -2
- package/src/consoleUtils.js +15 -5
- package/src/modules/questionAnsweringModule.js +9 -14
- package/src/modules/reviewModule.js +11 -16
- package/src/prompt.js +4 -3
- package/src/providers/file.js +19 -0
- package/src/providers/ghPrDiffProvider.js +2 -2
- package/src/providers/jiraIssueLegacyAccessTokenProvider.js +33 -30
- package/src/providers/text.js +1 -1
- package/src/systemUtils.js +32 -0
- package/src/utils.js +49 -13
package/src/configs/vertexai.js
CHANGED
@@ -2,7 +2,7 @@ import {writeFileIfNotExistsWithMessages} from "../utils.js";
|
|
2
2
|
import path from "node:path";
|
3
3
|
import {displayWarning} from "../consoleUtils.js";
|
4
4
|
|
5
|
-
const
|
5
|
+
const jsContent = `/* eslint-disable */
|
6
6
|
export async function configure(importFunction, global) {
|
7
7
|
// this is going to be imported from sloth dependencies,
|
8
8
|
// but can potentially be pulled from global node modules or from this project
|
@@ -19,8 +19,29 @@ export async function configure(importFunction, global) {
|
|
19
19
|
}
|
20
20
|
`;
|
21
21
|
|
22
|
+
const jsonContent = `{
|
23
|
+
"llm": {
|
24
|
+
"type": "vertexai",
|
25
|
+
"model": "gemini-2.5-pro-exp-03-25",
|
26
|
+
"temperature": 0
|
27
|
+
}
|
28
|
+
}`;
|
29
|
+
|
22
30
|
export function init(configFileName, context) {
|
23
31
|
path.join(context.currentDir, configFileName);
|
32
|
+
|
33
|
+
// Determine which content to use based on file extension
|
34
|
+
const content = configFileName.endsWith('.json') ? jsonContent : jsContent;
|
35
|
+
|
24
36
|
writeFileIfNotExistsWithMessages(configFileName, content);
|
25
37
|
displayWarning("For Google VertexAI you likely to need to do `gcloud auth login` and `gcloud auth application-default login`.");
|
26
|
-
}
|
38
|
+
}
|
39
|
+
|
40
|
+
// Function to process JSON config and create VertexAI LLM instance
|
41
|
+
export async function processJsonConfig(llmConfig) {
|
42
|
+
const vertexAi = await import('@langchain/google-vertexai');
|
43
|
+
return new vertexAi.ChatVertexAI({
|
44
|
+
...llmConfig,
|
45
|
+
model: llmConfig.model || "gemini-pro"
|
46
|
+
});
|
47
|
+
}
|
package/src/consoleUtils.js
CHANGED
@@ -1,23 +1,33 @@
|
|
1
1
|
import chalk from 'chalk';
|
2
|
+
import {debug as systemDebug, error as systemError, log} from './systemUtils.js';
|
2
3
|
|
3
4
|
// TODO it seems like commander supports coloured output, maybe dependency to chalk can be removed
|
4
5
|
|
5
6
|
export function displayError (message) {
|
6
|
-
|
7
|
+
systemError(chalk.red(message));
|
7
8
|
}
|
8
9
|
|
9
10
|
export function displayWarning (message) {
|
10
|
-
|
11
|
+
systemError(chalk.yellow(message));
|
11
12
|
}
|
12
13
|
|
13
14
|
export function displaySuccess (message) {
|
14
|
-
|
15
|
+
systemError(chalk.green(message));
|
15
16
|
}
|
16
17
|
|
17
18
|
export function displayInfo (message) {
|
18
|
-
|
19
|
+
systemError(chalk.blue(message));
|
19
20
|
}
|
20
21
|
|
21
22
|
export function display(message) {
|
22
|
-
|
23
|
+
log(message);
|
24
|
+
}
|
25
|
+
|
26
|
+
export function displayDebug(message) {
|
27
|
+
// TODO make it controlled by config
|
28
|
+
if (message?.stack) {
|
29
|
+
systemDebug(message.stack);
|
30
|
+
} else {
|
31
|
+
systemDebug(message);
|
32
|
+
}
|
23
33
|
}
|
@@ -1,15 +1,10 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
MemorySaver,
|
4
|
-
MessagesAnnotation,
|
5
|
-
START,
|
6
|
-
StateGraph,
|
7
|
-
} from "@langchain/langgraph";
|
8
|
-
import { writeFileSync } from "node:fs";
|
1
|
+
import {END, MemorySaver, MessagesAnnotation, START, StateGraph,} from "@langchain/langgraph";
|
2
|
+
import {writeFileSync} from "node:fs";
|
9
3
|
import * as path from "node:path";
|
10
|
-
import {
|
11
|
-
import {
|
12
|
-
import { fileSafeLocalDate,
|
4
|
+
import {slothContext} from "../config.js";
|
5
|
+
import {display, displayError, displaySuccess} from "../consoleUtils.js";
|
6
|
+
import {extractLastMessageContent, fileSafeLocalDate, ProgressIndicator, toFileSafeString} from "../utils.js";
|
7
|
+
import {getCurrentDir} from "../systemUtils.js";
|
13
8
|
|
14
9
|
/**
|
15
10
|
* Ask a question and get an answer from the LLM
|
@@ -20,7 +15,7 @@ import { fileSafeLocalDate, toFileSafeString, ProgressIndicator, extractLastMess
|
|
20
15
|
export async function askQuestion(source, preamble, content) {
|
21
16
|
const progressIndicator = new ProgressIndicator("Thinking.");
|
22
17
|
const outputContent = await askQuestionInner(slothContext, () => progressIndicator.indicate(), preamble, content);
|
23
|
-
const filePath = path.resolve(
|
18
|
+
const filePath = path.resolve(getCurrentDir(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
|
24
19
|
display(`\nwriting ${filePath}`);
|
25
20
|
// TODO highlight LLM output with something like Prism.JS
|
26
21
|
display('\n' + outputContent);
|
@@ -30,8 +25,8 @@ export async function askQuestion(source, preamble, content) {
|
|
30
25
|
} catch (error) {
|
31
26
|
displayError(`Failed to write answer to file: ${filePath}`);
|
32
27
|
displayError(error.message);
|
33
|
-
// Consider if
|
34
|
-
//
|
28
|
+
// TODO Consider if we want to exit or just log the error
|
29
|
+
// exit(1);
|
35
30
|
}
|
36
31
|
}
|
37
32
|
|
@@ -1,33 +1,28 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
MemorySaver,
|
4
|
-
MessagesAnnotation,
|
5
|
-
START,
|
6
|
-
StateGraph,
|
7
|
-
} from "@langchain/langgraph";
|
8
|
-
import { writeFileSync } from "node:fs";
|
1
|
+
import {END, MemorySaver, MessagesAnnotation, START, StateGraph,} from "@langchain/langgraph";
|
2
|
+
import {writeFileSync} from "node:fs";
|
9
3
|
import path from "node:path";
|
10
|
-
import {
|
11
|
-
import {
|
12
|
-
import { fileSafeLocalDate,
|
4
|
+
import {slothContext} from "../config.js";
|
5
|
+
import {display, displayDebug, displayError, displaySuccess} from "../consoleUtils.js";
|
6
|
+
import {extractLastMessageContent, fileSafeLocalDate, ProgressIndicator, toFileSafeString} from "../utils.js";
|
7
|
+
import {getCurrentDir, stdout} from "../systemUtils.js";
|
13
8
|
|
14
9
|
export async function review(source, preamble, diff) {
|
15
10
|
const progressIndicator = new ProgressIndicator("Reviewing.");
|
16
11
|
const outputContent = await reviewInner(slothContext, () => progressIndicator.indicate(), preamble, diff);
|
17
|
-
const filePath = path.resolve(
|
18
|
-
|
12
|
+
const filePath = path.resolve(getCurrentDir(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
|
13
|
+
stdout.write("\n");
|
19
14
|
display(`writing ${filePath}`);
|
20
|
-
|
15
|
+
stdout.write("\n");
|
21
16
|
// TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
|
22
17
|
display(outputContent);
|
23
18
|
try {
|
24
19
|
writeFileSync(filePath, outputContent);
|
25
20
|
displaySuccess(`This report can be found in ${filePath}`);
|
26
21
|
} catch (error) {
|
22
|
+
displayDebug(error);
|
27
23
|
displayError(`Failed to write review to file: ${filePath}`);
|
28
|
-
displayError(error.message);
|
29
24
|
// Consider if you want to exit or just log the error
|
30
|
-
//
|
25
|
+
// exit(1);
|
31
26
|
}
|
32
27
|
}
|
33
28
|
|
package/src/prompt.js
CHANGED
@@ -2,10 +2,11 @@ import {resolve} from "node:path";
|
|
2
2
|
import {SLOTH_INTERNAL_PREAMBLE, slothContext} from "./config.js";
|
3
3
|
import {readFileSyncWithMessages, spawnCommand} from "./utils.js";
|
4
4
|
import { displayError } from "./consoleUtils.js";
|
5
|
+
import { exit } from "./systemUtils.js";
|
5
6
|
|
6
7
|
export function readInternalPreamble() {
|
7
8
|
const filePath = resolve(slothContext.installDir, SLOTH_INTERNAL_PREAMBLE);
|
8
|
-
return readFileSyncWithMessages(filePath, "Error reading internal preamble file at:")
|
9
|
+
return readFileSyncWithMessages(filePath, "Error reading internal preamble file at:");
|
9
10
|
}
|
10
11
|
|
11
12
|
export function readPreamble(preambleFilename) {
|
@@ -14,7 +15,7 @@ export function readPreamble(preambleFilename) {
|
|
14
15
|
filePath,
|
15
16
|
"Error reading preamble file at:",
|
16
17
|
"Consider running `gsloth init` to set up your project. Check `gsloth init --help` to see options."
|
17
|
-
)
|
18
|
+
);
|
18
19
|
}
|
19
20
|
|
20
21
|
/**
|
@@ -28,6 +29,6 @@ export async function getPrDiff(pr) {
|
|
28
29
|
} catch (e) {
|
29
30
|
displayError(e.toString());
|
30
31
|
displayError(`Failed to call "gh pr diff ${pr}", see message above for details.`);
|
31
|
-
|
32
|
+
exit();
|
32
33
|
}
|
33
34
|
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import {resolve} from "node:path";
|
2
|
+
import {slothContext} from "../config.js";
|
3
|
+
import {display} from "../consoleUtils.js";
|
4
|
+
import {readFileSyncWithMessages} from "../utils.js";
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Reads the text file from current dir
|
8
|
+
* @param _ config (unused in this provider)
|
9
|
+
* @param fileName
|
10
|
+
* @returns {string} file contents
|
11
|
+
*/
|
12
|
+
export function get(_, fileName) {
|
13
|
+
if (!fileName) {
|
14
|
+
return null;
|
15
|
+
}
|
16
|
+
const filePath = resolve(slothContext.currentDir, fileName);
|
17
|
+
display(`Reading file ${fileName}...`);
|
18
|
+
return readFileSyncWithMessages(filePath);
|
19
|
+
}
|
@@ -4,8 +4,8 @@ import {displayWarning} from "../consoleUtils.js";
|
|
4
4
|
export async function get(_, pr) {
|
5
5
|
// TODO makes sense to check if gh is available and authenticated
|
6
6
|
if (!pr) {
|
7
|
-
displayWarning("No PR provided, skipping PR diff fetching.")
|
7
|
+
displayWarning("No PR provided, skipping PR diff fetching.");
|
8
8
|
return "";
|
9
9
|
}
|
10
10
|
return spawnCommand('gh', ['pr', 'diff', pr], 'Loading PR diff...', 'Loaded PR diff.');
|
11
|
-
}
|
11
|
+
}
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import {display, displayWarning} from "../consoleUtils.js";
|
2
|
+
import { env } from "../systemUtils.js";
|
2
3
|
|
3
4
|
export async function get(config, prId) {
|
4
5
|
const issueData = await getJiraIssue(config, prId);
|
5
|
-
return `## ${prId} Requirements - ${issueData.fields?.summary}\n\n${issueData.fields?.description}
|
6
|
+
return `## ${prId} Requirements - ${issueData.fields?.summary}\n\n${issueData.fields?.description}`;
|
6
7
|
}
|
7
8
|
|
8
9
|
/**
|
@@ -18,15 +19,23 @@ export async function get(config, prId) {
|
|
18
19
|
* @throws {Error} Throws an error if the fetch fails, authentication is wrong, the issue is not found, or the response status is not OK.
|
19
20
|
*/
|
20
21
|
async function getJiraIssue(config, jiraKey) {
|
21
|
-
const {
|
22
|
+
const {username, baseUrl} = config;
|
22
23
|
if (!jiraKey) {
|
23
|
-
displayWarning("No jiraKey provided, skipping Jira issue fetching.")
|
24
|
+
displayWarning("No jiraKey provided, skipping Jira issue fetching.");
|
24
25
|
return "";
|
25
26
|
}
|
27
|
+
const token = env.JIRA_LEGACY_API_TOKEN ?? config?.token;
|
28
|
+
|
29
|
+
if (!token) {
|
30
|
+
throw new Error(
|
31
|
+
'Missing JIRA Legacy API token. ' +
|
32
|
+
'The legacy token can be defined as JIRA_LEGACY_API_TOKEN environment variable or as "token" in config.'
|
33
|
+
);
|
34
|
+
}
|
26
35
|
|
27
36
|
// Validate essential inputs
|
28
|
-
if (!username || !
|
29
|
-
throw new Error('Missing required parameters in config
|
37
|
+
if (!username || !baseUrl) {
|
38
|
+
throw new Error('Missing required parameters in config: username or baseUrl');
|
30
39
|
}
|
31
40
|
|
32
41
|
// Ensure baseUrl doesn't end with a slash to avoid double slashes in the URL
|
@@ -49,33 +58,27 @@ async function getJiraIssue(config, jiraKey) {
|
|
49
58
|
|
50
59
|
display(`Fetching Jira issue: ${apiUrl}`);
|
51
60
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
});
|
61
|
+
const response = await fetch(apiUrl, {
|
62
|
+
method: 'GET',
|
63
|
+
headers: headers,
|
64
|
+
});
|
57
65
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
// Throw a detailed error including status, status text, URL, and body if available
|
68
|
-
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}. URL: ${apiUrl}. Response Body: ${errorBody}`);
|
66
|
+
// Check if the response status code indicates success (e.g., 200 OK)
|
67
|
+
if (!response.ok) {
|
68
|
+
let errorBody = 'Could not read error body.';
|
69
|
+
try {
|
70
|
+
// Attempt to get more details from the response body for non-OK statuses
|
71
|
+
errorBody = await response.text();
|
72
|
+
// eslint-disable-next-line no-unused-vars
|
73
|
+
} catch (e) {
|
74
|
+
// Silent fail - we already have a generic error message
|
69
75
|
}
|
76
|
+
// Throw a detailed error including status, status text, URL, and body if available
|
77
|
+
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}. URL: ${apiUrl}. Response Body: ${errorBody}`);
|
78
|
+
}
|
70
79
|
|
71
|
-
|
72
|
-
|
73
|
-
|
80
|
+
// Parse the JSON response body if the request was successful
|
81
|
+
const issueData = await response.json();
|
82
|
+
return issueData;
|
74
83
|
|
75
|
-
} catch (error) {
|
76
|
-
// Handle network errors (e.g., DNS resolution failure, connection refused)
|
77
|
-
// or errors thrown from the non-OK response check above
|
78
|
-
// Re-throw the error so the caller can handle it appropriately
|
79
|
-
throw error;
|
80
|
-
}
|
81
84
|
}
|
package/src/providers/text.js
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
/**
|
2
|
+
* This file contains all system functions and objects that are globally available
|
3
|
+
* but not imported directly, such as process.stdin, process.stdout, process.argv,
|
4
|
+
* process.env, process.cwd(), process.exit(), etc.
|
5
|
+
*
|
6
|
+
* By centralizing these in one file, we improve testability and make it easier
|
7
|
+
* to mock these dependencies in tests.
|
8
|
+
*/
|
9
|
+
|
10
|
+
const innerState = {
|
11
|
+
installDir: undefined
|
12
|
+
};
|
13
|
+
|
14
|
+
/* eslint-disable no-undef */
|
15
|
+
// Process-related functions and objects
|
16
|
+
export const getCurrentDir = () => process.cwd();
|
17
|
+
export const getInstallDir = () => innerState.installDir;
|
18
|
+
export const exit = (code) => process.exit(code);
|
19
|
+
export const stdin = process.stdin;
|
20
|
+
export const stdout = process.stdout;
|
21
|
+
export const argv = process.argv;
|
22
|
+
export const env = process.env;
|
23
|
+
|
24
|
+
export const setInstallDir = (dir) => innerState.installDir = dir;
|
25
|
+
|
26
|
+
// Console-related functions
|
27
|
+
export const log = (message) => console.log(message);
|
28
|
+
export const error = (message) => console.error(message);
|
29
|
+
export const warn = (message) => console.warn(message);
|
30
|
+
export const info = (message) => console.info(message);
|
31
|
+
export const debug = (message) => console.debug(message);
|
32
|
+
/* eslint-enable no-undef */
|
package/src/utils.js
CHANGED
@@ -3,6 +3,8 @@ import {existsSync, readFileSync, writeFileSync} from "node:fs";
|
|
3
3
|
import {slothContext} from "./config.js";
|
4
4
|
import {resolve} from "node:path";
|
5
5
|
import {spawn} from "node:child_process";
|
6
|
+
import {exit, stdin, stdout, argv} from "./systemUtils.js";
|
7
|
+
import url from "node:url";
|
6
8
|
|
7
9
|
export function toFileSafeString(string) {
|
8
10
|
return string.replace(/[^A-Za-z0-9]/g, '-');
|
@@ -45,27 +47,28 @@ export function readFileSyncWithMessages(filePath, errorMessageIn, noFileMessage
|
|
45
47
|
} else {
|
46
48
|
displayError(error.message);
|
47
49
|
}
|
48
|
-
|
50
|
+
exit(1); // Exit gracefully after error
|
49
51
|
}
|
50
52
|
}
|
51
53
|
|
52
54
|
export function readStdin(program) {
|
53
55
|
return new Promise((resolve) => {
|
54
|
-
|
56
|
+
// TODO use progress indicator here
|
57
|
+
if(stdin.isTTY) {
|
55
58
|
program.parseAsync().then(resolve);
|
56
59
|
} else {
|
57
60
|
// Support piping diff into gsloth
|
58
|
-
|
59
|
-
|
61
|
+
stdout.write('reading STDIN.');
|
62
|
+
stdin.on('readable', function() {
|
60
63
|
const chunk = this.read();
|
61
|
-
|
64
|
+
stdout.write('.');
|
62
65
|
if (chunk !== null) {
|
63
66
|
slothContext.stdin += chunk;
|
64
67
|
}
|
65
68
|
});
|
66
|
-
|
67
|
-
|
68
|
-
program.parseAsync(
|
69
|
+
stdin.on('end', function() {
|
70
|
+
stdout.write('.\n');
|
71
|
+
program.parseAsync(argv).then(resolve);
|
69
72
|
});
|
70
73
|
}
|
71
74
|
});
|
@@ -76,17 +79,17 @@ export async function spawnCommand(command, args, progressMessage, successMessag
|
|
76
79
|
// TODO use progress indicator
|
77
80
|
const out = {stdout: '', stderr: ''};
|
78
81
|
const spawned = spawn(command, args);
|
79
|
-
spawned.stdout.on('data', async (stdoutChunk
|
82
|
+
spawned.stdout.on('data', async (stdoutChunk) => {
|
80
83
|
display(progressMessage);
|
81
84
|
out.stdout += stdoutChunk.toString();
|
82
85
|
});
|
83
86
|
spawned.stderr.on('data', (err) => {
|
84
87
|
display(progressMessage);
|
85
88
|
out.stderr += err.toString();
|
86
|
-
})
|
89
|
+
});
|
87
90
|
spawned.on('error', (err) => {
|
88
91
|
reject(err.toString());
|
89
|
-
})
|
92
|
+
});
|
90
93
|
spawned.on('close', (code) => {
|
91
94
|
if (code === 0) {
|
92
95
|
display(successMessage);
|
@@ -115,10 +118,10 @@ export class ProgressIndicator {
|
|
115
118
|
|
116
119
|
indicate() {
|
117
120
|
if (this.hasBeenCalled) {
|
118
|
-
|
121
|
+
stdout.write('.');
|
119
122
|
} else {
|
120
123
|
this.hasBeenCalled = true;
|
121
|
-
|
124
|
+
stdout.write(this.initialMessage);
|
122
125
|
}
|
123
126
|
}
|
124
127
|
|
@@ -135,3 +138,36 @@ export function extractLastMessageContent(output) {
|
|
135
138
|
}
|
136
139
|
return output.messages[output.messages.length - 1].content;
|
137
140
|
}
|
141
|
+
|
142
|
+
/**
|
143
|
+
* Dynamically imports a module from a file path from the outside of the installation dir
|
144
|
+
* @param {string} filePath - The path to the file to import
|
145
|
+
* @returns {Promise} A promise that resolves to the imported module
|
146
|
+
*/
|
147
|
+
export function importExternalFile(filePath) {
|
148
|
+
const configFileUrl = url.pathToFileURL(filePath);
|
149
|
+
return import(configFileUrl);
|
150
|
+
}
|
151
|
+
|
152
|
+
/**
|
153
|
+
* Alias for importExternalFile for backward compatibility with tests
|
154
|
+
* @param {string} filePath - The path to the file to import
|
155
|
+
* @returns {Promise} A promise that resolves to the imported module
|
156
|
+
*/
|
157
|
+
export const importFromFilePath = importExternalFile;
|
158
|
+
|
159
|
+
/**
|
160
|
+
* Reads multiple files from the current directory and returns their contents
|
161
|
+
* @param {string[]} fileNames - Array of file names to read
|
162
|
+
* @returns {string} Combined content of all files with proper formatting
|
163
|
+
*/
|
164
|
+
export function readMultipleFilesFromCurrentDir(fileNames) {
|
165
|
+
if (!Array.isArray(fileNames)) {
|
166
|
+
return readFileFromCurrentDir(fileNames);
|
167
|
+
}
|
168
|
+
|
169
|
+
return fileNames.map(fileName => {
|
170
|
+
const content = readFileFromCurrentDir(fileName);
|
171
|
+
return `${fileName}:\n\`\`\`\n${content}\n\`\`\``;
|
172
|
+
}).join('\n\n');
|
173
|
+
}
|