cloving 0.1.3 → 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/dist/cloving_gpt/index.js +14 -10
- package/dist/commands/commit.js +2 -2
- package/dist/commands/config.js +33 -6
- package/dist/commands/init.js +5 -4
- package/dist/commands/review.js +2 -2
- package/dist/commands/unit_tests.js +2 -2
- package/dist/index.js +7 -3
- package/dist/utils/command_utils.js +1 -48
- package/dist/utils/config_utils.js +89 -0
- package/package.json +3 -1
|
@@ -11,20 +11,24 @@ const claude_1 = require("./adapters/claude");
|
|
|
11
11
|
const openai_1 = require("./adapters/openai");
|
|
12
12
|
const gpt4all_1 = require("./adapters/gpt4all");
|
|
13
13
|
const ollama_1 = require("./adapters/ollama");
|
|
14
|
-
const
|
|
14
|
+
const config_utils_1 = require("../utils/config_utils");
|
|
15
15
|
class ClovingGPT {
|
|
16
16
|
constructor(options = { silent: false }) {
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const { model: partialModel } = options;
|
|
18
|
+
const config = (0, config_utils_1.getConfig)(options);
|
|
19
|
+
if (!config || !config.models) {
|
|
19
20
|
console.log('No cloving configuration found. Please run: cloving config');
|
|
20
21
|
process_1.default.exit(1);
|
|
21
22
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const primaryModel = (0, config_utils_1.getPrimaryModel)(partialModel);
|
|
24
|
+
if (!primaryModel) {
|
|
25
|
+
console.log('No primary model found in the configuration. Please run: cloving config');
|
|
26
|
+
process_1.default.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const { provider, model, config: modelConfig } = primaryModel;
|
|
29
|
+
this.apiKey = modelConfig.apiKey;
|
|
30
|
+
this.silent = options.silent || modelConfig.silent;
|
|
31
|
+
switch (provider) {
|
|
28
32
|
case 'claude':
|
|
29
33
|
this.adapter = new claude_1.ClaudeAdapter(model);
|
|
30
34
|
break;
|
|
@@ -38,7 +42,7 @@ class ClovingGPT {
|
|
|
38
42
|
this.adapter = new ollama_1.OllamaAdapter(model);
|
|
39
43
|
break;
|
|
40
44
|
default:
|
|
41
|
-
throw new Error(`Unsupported provider: ${
|
|
45
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
async askUserToConfirm(prompt, message) {
|
package/dist/commands/commit.js
CHANGED
|
@@ -8,10 +8,10 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const git_utils_1 = require("../utils/git_utils");
|
|
10
10
|
const string_utils_1 = require("../utils/string_utils");
|
|
11
|
-
const
|
|
11
|
+
const config_utils_1 = require("../utils/config_utils");
|
|
12
12
|
const cloving_gpt_1 = __importDefault(require("../cloving_gpt"));
|
|
13
13
|
const generateAndCommitMessage = async (options) => {
|
|
14
|
-
options.silent = (0,
|
|
14
|
+
options.silent = (0, config_utils_1.getConfig)(options).globalSilent || false;
|
|
15
15
|
const gpt = new cloving_gpt_1.default(options);
|
|
16
16
|
try {
|
|
17
17
|
// Generate the prompt for commit message
|
package/dist/commands/config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.config = void 0;
|
|
4
|
+
const config_utils_1 = require("../utils/config_utils");
|
|
4
5
|
const command_utils_1 = require("../utils/command_utils");
|
|
5
6
|
const config = async () => {
|
|
6
|
-
let currentConfig = (0,
|
|
7
|
+
let currentConfig = (0, config_utils_1.getConfig)({});
|
|
7
8
|
// Ensure models is initialized properly
|
|
8
9
|
if (!currentConfig.models) {
|
|
9
10
|
currentConfig.models = {};
|
|
@@ -23,17 +24,43 @@ const config = async () => {
|
|
|
23
24
|
}
|
|
24
25
|
const selectedModel = models[modelIndex];
|
|
25
26
|
const apiKey = await (0, command_utils_1.promptUser)(`Enter your API key for ${selectedModel}: `);
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
currentConfig.
|
|
27
|
+
const [provider, ...modelParts] = selectedModel.split(':');
|
|
28
|
+
const model = modelParts.join(':');
|
|
29
|
+
if (!currentConfig.models[provider]) {
|
|
30
|
+
currentConfig.models[provider] = {};
|
|
31
|
+
}
|
|
32
|
+
const setAsPrimary = await (0, command_utils_1.promptUser)('Do you want to use this model as a primary model? [Yn]: ');
|
|
33
|
+
const primary = setAsPrimary.toLowerCase() === 'y' || setAsPrimary === '';
|
|
34
|
+
const priorityNumber = await (0, command_utils_1.promptUser)('Enter the priority for this model (0-100, higher priority will be used as long as the prompt fits the context token limit): [' + (primary ? 100 : 0) + ']');
|
|
35
|
+
const priority = primary ? 100 : parseInt(priorityNumber, 10);
|
|
36
|
+
const silentResponse = await (0, command_utils_1.promptUser)('Do you want to review all prompts before they are sent to this model? [Yn]: ');
|
|
37
|
+
const silent = !(silentResponse.toLowerCase() === 'y' || silentResponse === '');
|
|
38
|
+
const trustResponse = await (0, command_utils_1.promptUser)('Do you trust this model with sensitive information? [Yn]: ');
|
|
39
|
+
const trust = trustResponse.toLowerCase() === 'y' || trustResponse === '';
|
|
40
|
+
const modelConfig = {
|
|
41
|
+
apiKey,
|
|
42
|
+
primary,
|
|
43
|
+
priority,
|
|
44
|
+
silent,
|
|
45
|
+
trust,
|
|
46
|
+
};
|
|
47
|
+
currentConfig.models[provider][model] = modelConfig;
|
|
48
|
+
if (primary) {
|
|
49
|
+
// Ensure only one primary model
|
|
50
|
+
for (const p in currentConfig.models) {
|
|
51
|
+
for (const m in currentConfig.models[p]) {
|
|
52
|
+
if (p !== provider || m !== model) {
|
|
53
|
+
currentConfig.models[p][m].primary = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
30
57
|
}
|
|
31
58
|
const anotherModel = await (0, command_utils_1.promptUser)('Do you want to configure another model? [Yn]: ');
|
|
32
59
|
if (anotherModel.toLowerCase() !== 'y' && anotherModel !== '') {
|
|
33
60
|
continueConfig = false;
|
|
34
61
|
}
|
|
35
62
|
}
|
|
36
|
-
(0,
|
|
63
|
+
(0, config_utils_1.saveConfig)(currentConfig);
|
|
37
64
|
};
|
|
38
65
|
exports.config = config;
|
|
39
66
|
exports.default = exports.config;
|
package/dist/commands/init.js
CHANGED
|
@@ -11,10 +11,11 @@ const string_utils_1 = require("../utils/string_utils");
|
|
|
11
11
|
const cloving_gpt_1 = __importDefault(require("../cloving_gpt"));
|
|
12
12
|
const ignore_1 = __importDefault(require("ignore"));
|
|
13
13
|
const string_utils_2 = require("../utils/string_utils");
|
|
14
|
+
const config_utils_1 = require("../utils/config_utils");
|
|
14
15
|
const command_utils_1 = require("../utils/command_utils");
|
|
15
16
|
// Main function for the describe command
|
|
16
17
|
const init = async (options) => {
|
|
17
|
-
options.silent = (0,
|
|
18
|
+
options.silent = (0, config_utils_1.getConfig)(options).globalSilent || false;
|
|
18
19
|
const gpt = new cloving_gpt_1.default(options);
|
|
19
20
|
const specialFileContents = (0, command_utils_1.collectSpecialFileContents)();
|
|
20
21
|
const specialFileNames = Object.keys(specialFileContents).map(file => ' - ' + file);
|
|
@@ -38,8 +39,8 @@ technologies used. This will provide better context for future Cloving requests.
|
|
|
38
39
|
`);
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
const config = (0,
|
|
42
|
-
if (!config || !config?.
|
|
42
|
+
const config = (0, config_utils_1.getConfig)(options);
|
|
43
|
+
if (!config || !config?.models) {
|
|
43
44
|
console.error('No cloving configuration found. Please run `cloving config`');
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
@@ -128,7 +129,7 @@ interface DatabaseConfig {
|
|
|
128
129
|
primary?: boolean
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
export interface
|
|
132
|
+
export interface ClovingfileConfig {
|
|
132
133
|
languages: LanguageConfig[]
|
|
133
134
|
frameworks: FrameworkConfig[]
|
|
134
135
|
testingFrameworks?: TestingFrameworkConfig[]
|
package/dist/commands/review.js
CHANGED
|
@@ -8,9 +8,9 @@ const child_process_1 = require("child_process");
|
|
|
8
8
|
const git_utils_1 = require("../utils/git_utils");
|
|
9
9
|
const string_utils_1 = require("../utils/string_utils");
|
|
10
10
|
const cloving_gpt_1 = __importDefault(require("../cloving_gpt"));
|
|
11
|
-
const
|
|
11
|
+
const config_utils_1 = require("../utils/config_utils");
|
|
12
12
|
const review = async (options) => {
|
|
13
|
-
options.silent = (0,
|
|
13
|
+
options.silent = (0, config_utils_1.getConfig)(options).globalSilent || false;
|
|
14
14
|
const gpt = new cloving_gpt_1.default(options);
|
|
15
15
|
try {
|
|
16
16
|
// Define the prompt for analysis
|
|
@@ -7,7 +7,7 @@ const fs_1 = require("fs");
|
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const git_utils_1 = require("../utils/git_utils");
|
|
9
9
|
const cloving_gpt_1 = __importDefault(require("../cloving_gpt"));
|
|
10
|
-
const
|
|
10
|
+
const config_utils_1 = require("../utils/config_utils");
|
|
11
11
|
const extractChangedFiles = (gitDiff) => {
|
|
12
12
|
const fileRegex = /diff --git a\/(.+?) b\/(.+?)\n/g;
|
|
13
13
|
const files = new Set();
|
|
@@ -72,7 +72,7 @@ const unitTests = async (options) => {
|
|
|
72
72
|
const { files } = options;
|
|
73
73
|
const gpt = new cloving_gpt_1.default(options);
|
|
74
74
|
// Read the cloving.json file
|
|
75
|
-
const config = (0,
|
|
75
|
+
const config = (0, config_utils_1.readClovingConfig)();
|
|
76
76
|
const testingDirectory = config.testingFrameworks?.find((framework) => framework.directory)?.directory;
|
|
77
77
|
if (!testingDirectory) {
|
|
78
78
|
await setupTestingPrompt(gpt, config);
|
package/dist/index.js
CHANGED
|
@@ -20,12 +20,14 @@ program
|
|
|
20
20
|
.command('commit')
|
|
21
21
|
.description('Generate a commit message and commit the changes')
|
|
22
22
|
.option('-s, --silent', 'Run the command without asking for confirmation of submitting prompts')
|
|
23
|
+
.option('-m, --model <model>', 'Select the model to use (e.g., openai, claude, ollama, ollama:llama3, claude:claude-3-5-sonnet-20240620)')
|
|
23
24
|
.action(commit_1.default);
|
|
24
25
|
program
|
|
25
26
|
.command('unit-tests')
|
|
26
|
-
.description('Generate unit tests for the
|
|
27
|
+
.description('Generate unit tests (if you don\'t specify filenames, it will generate tests for commited changes that differ from the main/master branch)')
|
|
27
28
|
.option('-s, --silent', 'Run the command without asking for confirmation of submitting prompts')
|
|
28
29
|
.option('-f, --files <filenames...>', 'Specify filenames for the unit tests')
|
|
30
|
+
.option('-m, --model <model>', 'Select the model to use (e.g., openai, claude, ollama, ollama:llama3, claude:claude-3-5-sonnet-20240620)')
|
|
29
31
|
.action(unit_tests_1.default);
|
|
30
32
|
program
|
|
31
33
|
.command('models')
|
|
@@ -33,16 +35,18 @@ program
|
|
|
33
35
|
.action(models_1.default);
|
|
34
36
|
program
|
|
35
37
|
.command('review')
|
|
36
|
-
.description('Review the changes
|
|
38
|
+
.description('Review the code for commited changes that differ from the main/master branch')
|
|
37
39
|
.option('-s, --silent', 'Run the command without asking for confirmation of submitting prompts')
|
|
40
|
+
.option('-m, --model <model>', 'Select the model to use (e.g., openai, claude, ollama, ollama:llama3, claude:claude-3-5-sonnet-20240620)')
|
|
38
41
|
.action(review_1.default);
|
|
39
42
|
program
|
|
40
43
|
.command('init')
|
|
41
44
|
.description('Setup cloving in the current project')
|
|
42
45
|
.option('-s, --silent', 'Run the command without asking for confirmation of submitting prompts')
|
|
46
|
+
.option('-m, --model <model>', 'Select the model to use (e.g., openai, claude, ollama, ollama:llama3, claude:claude-3-5-sonnet-20240620)')
|
|
43
47
|
.action(init_1.default);
|
|
44
48
|
program
|
|
45
49
|
.command('config')
|
|
46
|
-
.description('Configure cloving with your API key and
|
|
50
|
+
.description('Configure cloving with your API key and models to use')
|
|
47
51
|
.action(config_1.default);
|
|
48
52
|
program.parse(process.argv);
|
|
@@ -3,13 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.fetchModels = exports.promptUser = exports.runCommand = exports.generateFileList = exports.checkForSpecialFiles = exports.collectSpecialFileContents = exports.SPECIAL_FILES = void 0;
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const readline_1 = __importDefault(require("readline"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const os_1 = __importDefault(require("os"));
|
|
11
|
-
const path_1 = __importDefault(require("path"));
|
|
12
|
-
exports.CONFIG_PATH = path_1.default.join(os_1.default.homedir(), '.cloving.json');
|
|
13
10
|
exports.SPECIAL_FILES = [
|
|
14
11
|
'package.json',
|
|
15
12
|
'Gemfile',
|
|
@@ -122,47 +119,3 @@ const fetchModels = async () => {
|
|
|
122
119
|
}
|
|
123
120
|
};
|
|
124
121
|
exports.fetchModels = fetchModels;
|
|
125
|
-
const saveConfig = (config) => {
|
|
126
|
-
fs_1.default.writeFileSync(exports.CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
127
|
-
console.log(`Configuration saved to ${exports.CONFIG_PATH}`);
|
|
128
|
-
};
|
|
129
|
-
exports.saveConfig = saveConfig;
|
|
130
|
-
const getConfig = (options) => {
|
|
131
|
-
try {
|
|
132
|
-
if (fs_1.default.existsSync(exports.CONFIG_PATH)) {
|
|
133
|
-
const rawConfig = fs_1.default.readFileSync(exports.CONFIG_PATH, 'utf-8');
|
|
134
|
-
const config = JSON.parse(rawConfig);
|
|
135
|
-
if (options.silent) {
|
|
136
|
-
return { ...config, silent: true };
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
return config;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
console.error('Error reading configuration:', err);
|
|
145
|
-
}
|
|
146
|
-
if (process.env.CLOVING_MODEL) {
|
|
147
|
-
return {
|
|
148
|
-
models: { [`${process.env.CLOVING_MODEL}`]: process.env.CLOVING_API_KEY ?? '' },
|
|
149
|
-
primaryModel: process.env.CLOVING_MODEL,
|
|
150
|
-
silent: options.silent || false
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
return { models: {}, primaryModel: null, silent: options.silent || false };
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
exports.getConfig = getConfig;
|
|
158
|
-
const readClovingConfig = () => {
|
|
159
|
-
const configPath = path_1.default.resolve(process.cwd(), 'cloving.json');
|
|
160
|
-
if (fs_1.default.existsSync(configPath)) {
|
|
161
|
-
const configFile = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
162
|
-
return JSON.parse(configFile);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
throw new Error('cloving.json file not found');
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
exports.readClovingConfig = readClovingConfig;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getPrimaryModel = exports.saveConfig = exports.readClovingConfig = exports.getConfig = exports.CLOVINGFILE_PATH = exports.CLOVING_CONFIG_PATH = void 0;
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const ini_1 = __importDefault(require("ini"));
|
|
11
|
+
exports.CLOVING_CONFIG_PATH = path_1.default.join(os_1.default.homedir(), '.clovingconfig');
|
|
12
|
+
exports.CLOVINGFILE_PATH = 'cloving.json';
|
|
13
|
+
const getConfig = (options) => {
|
|
14
|
+
try {
|
|
15
|
+
if (fs_1.default.existsSync(exports.CLOVING_CONFIG_PATH)) {
|
|
16
|
+
const rawConfig = fs_1.default.readFileSync(exports.CLOVING_CONFIG_PATH, 'utf-8');
|
|
17
|
+
const config = ini_1.default.parse(rawConfig);
|
|
18
|
+
if (options.silent) {
|
|
19
|
+
config.globalSilent = true;
|
|
20
|
+
}
|
|
21
|
+
return config;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error('Error reading configuration:', err);
|
|
26
|
+
}
|
|
27
|
+
if (process.env.CLOVING_MODEL) {
|
|
28
|
+
const [provider, ...modelParts] = process.env.CLOVING_MODEL.split(':');
|
|
29
|
+
const model = modelParts.join(':');
|
|
30
|
+
return {
|
|
31
|
+
models: {
|
|
32
|
+
[provider]: {
|
|
33
|
+
[model]: {
|
|
34
|
+
apiKey: process.env.CLOVING_API_KEY ?? '',
|
|
35
|
+
primary: true,
|
|
36
|
+
priority: 10,
|
|
37
|
+
silent: options.silent || false,
|
|
38
|
+
trust: false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
globalSilent: options.silent || false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return { models: {}, globalSilent: options.silent || false };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
exports.getConfig = getConfig;
|
|
50
|
+
const readClovingConfig = () => {
|
|
51
|
+
if (fs_1.default.existsSync(exports.CLOVINGFILE_PATH)) {
|
|
52
|
+
const configFile = fs_1.default.readFileSync(exports.CLOVINGFILE_PATH, 'utf-8');
|
|
53
|
+
return ini_1.default.parse(configFile);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw new Error('cloving.ini file not found');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
exports.readClovingConfig = readClovingConfig;
|
|
60
|
+
const saveConfig = (config) => {
|
|
61
|
+
const iniString = ini_1.default.stringify(config);
|
|
62
|
+
fs_1.default.writeFileSync(exports.CLOVING_CONFIG_PATH, iniString);
|
|
63
|
+
console.log(`Configuration saved to ${exports.CLOVING_CONFIG_PATH}`);
|
|
64
|
+
};
|
|
65
|
+
exports.saveConfig = saveConfig;
|
|
66
|
+
const getPrimaryModel = (partialModel) => {
|
|
67
|
+
try {
|
|
68
|
+
const config = (0, exports.getConfig)({});
|
|
69
|
+
const models = [];
|
|
70
|
+
for (const provider in config.models) {
|
|
71
|
+
for (const model in config.models[provider]) {
|
|
72
|
+
if (!partialModel || `${provider}:${model}`.startsWith(partialModel)) {
|
|
73
|
+
models.push({
|
|
74
|
+
provider,
|
|
75
|
+
model,
|
|
76
|
+
config: config.models[provider][model],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
models.sort((a, b) => parseInt(`${b.config.priority}`, 10) - parseInt(`${a.config.priority}`, 10));
|
|
82
|
+
return models.length > 0 ? models[0] : null;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error('Error reading model from configuration:', err);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
exports.getPrimaryModel = getPrimaryModel;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloving",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -48,11 +48,13 @@
|
|
|
48
48
|
"eslint-plugin-n": "^17.9.0",
|
|
49
49
|
"eslint-plugin-promise": "^6.4.0",
|
|
50
50
|
"ignore": "^5.3.1",
|
|
51
|
+
"ini": "^4.1.3",
|
|
51
52
|
"nock": "^13.5.4",
|
|
52
53
|
"vite": "^5.3.3",
|
|
53
54
|
"vite-node": "^2.0.1"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
57
|
+
"@types/ini": "^4.1.1",
|
|
56
58
|
"@types/jest": "^29.5.12",
|
|
57
59
|
"@types/node": "^20.14.10",
|
|
58
60
|
"husky": "^9.0.11",
|