gennady 0.2.2 β 0.5.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.
- package/README.md +107 -10
- package/cli/cmd/cat.js +26 -0
- package/cli/cmd/commit.js +83 -0
- package/cli/cmd/review.js +58 -0
- package/cli/gennady.js +19 -40
- package/index.js +2 -0
- package/package.json +5 -3
- package/src/ai/ai-core.js +187 -0
- package/src/cat-gen/cat-gen.js +53 -0
- package/src/commit-gen/commit-gen.js +38 -71
- package/src/git/git-core.js +69 -0
- package/src/git/git-diff.js +36 -13
- package/src/prompts/commit/commit-base-prompt.md +38 -0
- package/src/prompts/commit/commit-format-detailed-prompt.md +29 -0
- package/src/prompts/commit/commit-format-oneline-prompt.md +19 -0
- package/src/prompts/commit/commit-translate-prompt.md +15 -0
- package/src/prompts/index.js +10 -0
- package/src/prompts/review/review-base-prompt.md +46 -0
- package/src/review-gen/__fixture__/review-gen-fixture.md +110 -0
- package/src/review-gen/review-gen.js +119 -0
- package/src/review-gen/review-gen.test.js +79 -0
- package/src/review-gen/specs/js/Function.prototype.json +162 -0
- package/src/review-gen/specs/js/Global.json +219 -0
- package/src/review-gen/specs/js/JSON.json +85 -0
- package/src/review-gen/specs/js/Object.json +626 -0
- package/src/review-gen/specs/js/Object.prototype.json +337 -0
- package/src/review-gen/specs/js/Storage.json +97 -0
- package/src/utils/parse-args.js +7 -3
- package/src/utils/style.js +12 -3
- package/src/git/git-cmd.js +0 -32
- package/src/prompts/base-prompt.md +0 -27
- package/src/prompts/format-detailed-prompt.md +0 -16
- package/src/prompts/format-oneline-prompt.md +0 -11
- package/src/prompts/translate-prompt.md +0 -11
package/README.md
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
π€ Gennadyα΅α΅α΅α΅ π―οΈ
|
|
2
2
|
-----------------
|
|
3
|
-
**
|
|
3
|
+
**GEN**erate **N**ext-level **A**utomated **D**escription **Y**ntelligence.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
# Commit message
|
|
6
7
|
npx gennady
|
|
8
|
+
|
|
9
|
+
# Code review for critical issues
|
|
10
|
+
npx gennady review
|
|
11
|
+
|
|
12
|
+
# Quickly display the contents
|
|
13
|
+
npx gennady cat <path1> <path2> ...
|
|
7
14
|
```
|
|
8
15
|
|
|
9
16
|
---
|
|
10
17
|
|
|
11
|
-
###
|
|
18
|
+
### β¨ Features
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
ollama serve
|
|
17
|
-
```
|
|
20
|
+
- π€ [**Commit Message**](#-commit-messages): Automatically generate clear, descriptive git commit messages from your staged changes.
|
|
21
|
+
- π [**review**](#-review): Instantly review your staged git changes for critical issues (logic, runtime, security).
|
|
22
|
+
- π± [**cat**](#-cat): Quickly display the contents of multiple files or directories at once, filtered by allowed extensions (default: .js, .ts, .tsx).
|
|
18
23
|
|
|
19
24
|
---
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
## π Usage Overview
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
Gennady provides several main CLI commands:
|
|
29
|
+
- `npx gennady` β Generate commit messages from your staged git changes.
|
|
30
|
+
- `npx gennady cat <path1> <path2> ...` β Display the contents of one or more files or directories, filtered by allowed extensions.
|
|
31
|
+
- `npx gennady review` β Review your staged git changes for critical issues.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## π€ Commit Messages
|
|
36
|
+
|
|
37
|
+
```sh
|
|
24
38
|
# Basic usage
|
|
25
39
|
npx gennady
|
|
26
40
|
|
|
@@ -32,4 +46,87 @@ npx gennady --mode=detailed
|
|
|
32
46
|
|
|
33
47
|
# Generate detailed commit message relative to the target branch
|
|
34
48
|
npx gennady --branch=develop
|
|
35
|
-
```
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Options
|
|
52
|
+
| Option | Alias(es) | Description |
|
|
53
|
+
|-------------------|------------------|----------------------------------------------|
|
|
54
|
+
| `--mode` | `-m` | Set the mode (`auto`, `oneline`, `detailed`) |
|
|
55
|
+
| `--oneline` | `--short`, `-o` | Generate a one-line commit message |
|
|
56
|
+
| `--model` | | Specify the AI model |
|
|
57
|
+
| `--branch` | `-b` | Target branch for diff |
|
|
58
|
+
| `--apply` | | Immediately apply the generated commit message to git |
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
#### What Happens?
|
|
62
|
+
- Gennady analyzes your staged changes.
|
|
63
|
+
- It generates a commit message.
|
|
64
|
+
- If your system language isn't English, it translates the message for you.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## π review
|
|
70
|
+
|
|
71
|
+
Review your staged git changes for critical issues.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx gennady review
|
|
75
|
+
|
|
76
|
+
# Review changes relative to a specific branch
|
|
77
|
+
npx gennady review --branch=develop
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### What Happens?
|
|
81
|
+
- Gennady analyzes your staged changes.
|
|
82
|
+
- It checks only the lines added or modified in your diff for critical issues (logic, runtime, and security errors).
|
|
83
|
+
- If no critical issues are found, it outputs `GOOD`.
|
|
84
|
+
- If issues are found, they are listed in a clear, structured format.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## π± cat
|
|
89
|
+
|
|
90
|
+
Display the contents of files or directories (with filtering for allowed extensions).
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npx gennady cat ./src/
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Output
|
|
97
|
+
- Shows file contents with headers per file.
|
|
98
|
+
- Hints for copying output without color codes.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Setup LLM
|
|
103
|
+
|
|
104
|
+
### Local
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
brew install ollama
|
|
108
|
+
ollama pull llama3:8b
|
|
109
|
+
ollama serve
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### External
|
|
115
|
+
|
|
116
|
+
Create `~/.gennadyrc` configuration file:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
[
|
|
120
|
+
{
|
|
121
|
+
"url": "https://api.openai.com/v1/chat/completions",
|
|
122
|
+
"key": "...",
|
|
123
|
+
"model": "gpt-3.5-turbo-0125"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## π Happy Coding with Gennady!
|
|
131
|
+
|
|
132
|
+
> Made with π€ by Konstantin Lebedev
|
package/cli/cmd/cat.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { catGen } from '../../src/cat-gen/cat-gen.js';
|
|
5
|
+
import { style } from '../../src/utils/style.js';
|
|
6
|
+
import { parseArgs } from '../../src/utils/parse-args.js';
|
|
7
|
+
|
|
8
|
+
//
|
|
9
|
+
// π± CAT-GEN
|
|
10
|
+
//
|
|
11
|
+
const args = parseArgs(process.argv);
|
|
12
|
+
|
|
13
|
+
if (args._.length === 0) {
|
|
14
|
+
console.error(style.yellow('Usage: npx gennady cat <path1> <path2> ...'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
catGen(args._).forEach(({ relativePath, content }) => {
|
|
19
|
+
console.log(style.blue(`#### ${relativePath}`));
|
|
20
|
+
console.log(content);
|
|
21
|
+
console.log('');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log(style.green(`^`.repeat(40)));
|
|
25
|
+
console.log(style.italic.gray(`Hint: npx gennady cat ${process.argv.slice(3).join(' ')} --plain | pbcopy`));
|
|
26
|
+
console.log('');
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { CommitGen } from '../../src/commit-gen/commit-gen.js';
|
|
8
|
+
import { parseArgs } from '../../src/utils/parse-args.js';
|
|
9
|
+
import { style } from '../../src/utils/style.js';
|
|
10
|
+
import { getSysLang } from '../../src/utils/language.js';
|
|
11
|
+
|
|
12
|
+
//
|
|
13
|
+
// π€ COMMIT-GEN π¬
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
const CACHE_PATH = path.join(os.homedir(), '.gennady_commit_cache.json');
|
|
17
|
+
const PROJECT_KEY = process.cwd();
|
|
18
|
+
const COMMIT_CACHE = readCache();
|
|
19
|
+
|
|
20
|
+
const params = parseArgs(process.argv, {
|
|
21
|
+
apply: ['apply'],
|
|
22
|
+
mode: ['mode', 'm'],
|
|
23
|
+
oneline: ['short', 'one', 'o'],
|
|
24
|
+
model: ['model'],
|
|
25
|
+
targetBranch: ['branch', 'b'],
|
|
26
|
+
apiUrl: ['api', 'apiUrl'],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const commitGen = new CommitGen(params);
|
|
30
|
+
|
|
31
|
+
console.info(
|
|
32
|
+
'π€',
|
|
33
|
+
style.whiteBright.bold('GENNADY'),
|
|
34
|
+
`(${style.cyan(commitGen.model)} β ${style.yellow(commitGen.mode)})`,
|
|
35
|
+
'π―οΈ',
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
console.info(style.gray('-'.repeat(40)));
|
|
39
|
+
console.info(`- url: ${style.blue(commitGen.apiUrl)}`);
|
|
40
|
+
console.info(style.gray('-'.repeat(40)));
|
|
41
|
+
|
|
42
|
+
const commitMessage = params.apply && COMMIT_CACHE[PROJECT_KEY] || await commitGen.generate();
|
|
43
|
+
if (!commitMessage) {
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (params.apply) {
|
|
48
|
+
// APPLY COMMIT
|
|
49
|
+
execSync(
|
|
50
|
+
`git commit -am "${commitMessage.replace(/"/g, '\"')}"`,
|
|
51
|
+
{stdio: 'inherit'},
|
|
52
|
+
);
|
|
53
|
+
saveCache({ [PROJECT_KEY]: undefined });
|
|
54
|
+
} else {
|
|
55
|
+
// GENERATE COMMIT
|
|
56
|
+
console.info('-'.repeat(40), '\n');
|
|
57
|
+
console.info(style.whiteBright(commitMessage), '\n');
|
|
58
|
+
console.info('^'.repeat(40), '\n');
|
|
59
|
+
|
|
60
|
+
const lang = getSysLang();
|
|
61
|
+
if (lang !== 'en') {
|
|
62
|
+
console.info(await commitGen.translate(commitMessage, lang), '\n');
|
|
63
|
+
console.info('^'.repeat(40), '\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(style.italic.gray(`Hint: npx gennady ${process.argv.slice(3).join(' ')} --apply`));
|
|
67
|
+
console.log('');
|
|
68
|
+
|
|
69
|
+
saveCache({ [PROJECT_KEY]: commitMessage });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readCache() {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
|
|
75
|
+
} catch {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function saveCache(patch) {
|
|
81
|
+
const next = { ...COMMIT_CACHE, ...patch };
|
|
82
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(next, null, 2));
|
|
83
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { getGitDiffInfo } from '../../src/git/git-core.js';
|
|
4
|
+
import { ReviewGen } from '../../src/review-gen/review-gen.js';
|
|
5
|
+
import { parseArgs } from '../../src/utils/parse-args.js';
|
|
6
|
+
import { style } from '../../src/utils/style.js';
|
|
7
|
+
|
|
8
|
+
//
|
|
9
|
+
// π REVIEW-GEN
|
|
10
|
+
//
|
|
11
|
+
|
|
12
|
+
const params = parseArgs(process.argv, {
|
|
13
|
+
branch: ['branch', 'b'],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const review = new ReviewGen();
|
|
17
|
+
|
|
18
|
+
console.info(
|
|
19
|
+
`π€`,
|
|
20
|
+
style.whiteBright.bold(`GENNADY`),
|
|
21
|
+
`(${style.cyan(review.ai.model)})`,
|
|
22
|
+
`π`,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
console.info(style.gray(`-`.repeat(40)));
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
parsedCodeDiff,
|
|
29
|
+
parsedCodeTokens,
|
|
30
|
+
programmingLanguages,
|
|
31
|
+
parsedCodeChunkMaxTokens,
|
|
32
|
+
} = getGitDiffInfo(params.branch);
|
|
33
|
+
|
|
34
|
+
console.info(`- Tokens: ${style.bold.cyanBright(parsedCodeTokens)} ${style.gray(`(max per file: ${parsedCodeChunkMaxTokens})`)}`);
|
|
35
|
+
console.info(`- Languages: ${style.yellow(programmingLanguages)}`);
|
|
36
|
+
|
|
37
|
+
if (parsedCodeDiff.length === 0) {
|
|
38
|
+
console.info(`No changes detected, skipping review.`);
|
|
39
|
+
console.info(style.italic.gray(`Hint: git add`));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const batches = review.ai.createPromptsBatchesByDiff(parsedCodeDiff);
|
|
44
|
+
|
|
45
|
+
console.info(`- Queue: ${style.bold.cyan(batches.length)}`);
|
|
46
|
+
console.info(style.gray(`-`.repeat(40)));
|
|
47
|
+
|
|
48
|
+
const startGenTime = performance.now();
|
|
49
|
+
const results = await Promise.all(batches.map(async (batch) => {
|
|
50
|
+
console.info(`- Task:`, batch.tokens, batch.languages);
|
|
51
|
+
return await review.generate(batch.diff, batch.languages);
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
console.info(style.gray(`-`.repeat(40)));
|
|
55
|
+
console.info(`- Generation time: ${style.blueBright(((performance.now() - startGenTime) / 1000).toFixed(2))}s`);
|
|
56
|
+
console.info(style.gray(`-`.repeat(40)));
|
|
57
|
+
|
|
58
|
+
console.info(results.join('\n\n'));
|
package/cli/gennady.js
CHANGED
|
@@ -1,45 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
|
|
3
|
+
switch(process.argv[2]) {
|
|
4
|
+
//
|
|
5
|
+
// π± CAT-GEN
|
|
6
|
+
//
|
|
7
|
+
case 'cat':
|
|
8
|
+
import('./cmd/cat.js');
|
|
9
|
+
break;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
//
|
|
12
|
+
// π REVIEW-GEN
|
|
13
|
+
//
|
|
14
|
+
case 'review':
|
|
15
|
+
import('./cmd/review.js');
|
|
16
|
+
break;
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const commit = new CommitGen({
|
|
24
|
-
...params,
|
|
25
|
-
basePromptTemplate: readFileSync(join(PROMPTS_DIR, 'base-prompt.md')).toString(),
|
|
26
|
-
formatOnelinePromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-oneline-prompt.md')).toString(),
|
|
27
|
-
formatDetailedPromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-detailed-prompt.md')).toString(),
|
|
28
|
-
translatePromptTemplate: readFileSync(join(PROMPTS_DIR, 'translate-prompt.md')).toString(),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
console.info(`π€`, style.whiteBright.bold(`GENNADY`), `(${style.cyan(commit.reviewerModel)} β ${style.yellow(commit.mode)})`, `π―οΈ`);
|
|
32
|
-
console.info(style.gray(`-`.repeat(30)));
|
|
33
|
-
|
|
34
|
-
const msg = await commit.generate();
|
|
35
|
-
if (msg) {
|
|
36
|
-
console.info(`-`.repeat(40), '\n');
|
|
37
|
-
console.info(style.whiteBright(msg), '\n');
|
|
38
|
-
console.info(`^`.repeat(40), '\n');
|
|
39
|
-
|
|
40
|
-
const lang = getSysLang();
|
|
41
|
-
if (lang !== 'en') {
|
|
42
|
-
console.info(await commit.translate(msg, lang), '\n');
|
|
43
|
-
console.info(`^`.repeat(40), '\n');
|
|
44
|
-
}
|
|
18
|
+
//
|
|
19
|
+
// π€ COMMIT-GEN π¬
|
|
20
|
+
//
|
|
21
|
+
default:
|
|
22
|
+
import('./cmd/commit.js');
|
|
23
|
+
break;
|
|
45
24
|
}
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gennady",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"author": "Konstantin Lebedev <ibnrubaxa@gmail.com>",
|
|
5
5
|
"description": "Gennady β Generate Next-level Automated Description Yntelligence",
|
|
6
6
|
"keywords": [
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"local",
|
|
11
11
|
"commit",
|
|
12
12
|
"generator",
|
|
13
|
-
"gennady"
|
|
13
|
+
"gennady",
|
|
14
|
+
"review",
|
|
15
|
+
"code-review"
|
|
14
16
|
],
|
|
15
17
|
"license": "MIT",
|
|
16
18
|
"repository": "https://github.com/rubaxa/gennady",
|
|
@@ -18,6 +20,6 @@
|
|
|
18
20
|
"bin": "./cli/gennady.js",
|
|
19
21
|
"main": "index.js",
|
|
20
22
|
"scripts": {
|
|
21
|
-
"test": "
|
|
23
|
+
"test": "node --test"
|
|
22
24
|
}
|
|
23
25
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MODEL = 'llama3:8b';
|
|
5
|
+
const DEFAULT_API_URL = 'http://127.0.0.1:11434/api/generate';
|
|
6
|
+
|
|
7
|
+
const GENNADY_RC_FILENAME = '.gennadyrc';
|
|
8
|
+
|
|
9
|
+
export class AiCore {
|
|
10
|
+
api = undefined;
|
|
11
|
+
apiList = [];
|
|
12
|
+
|
|
13
|
+
constructor(init) {
|
|
14
|
+
this.init = {
|
|
15
|
+
logger: console,
|
|
16
|
+
timeout: 120,
|
|
17
|
+
maxInputTokens: init.maxInputTokens || 4000,
|
|
18
|
+
...init,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Try parse rc files
|
|
22
|
+
[
|
|
23
|
+
join(process.cwd(), GENNADY_RC_FILENAME),
|
|
24
|
+
join(process.env.HOME, GENNADY_RC_FILENAME),
|
|
25
|
+
].find((file) => {
|
|
26
|
+
try {
|
|
27
|
+
if (existsSync(file)) {
|
|
28
|
+
const items = JSON.parse(readFileSync(file).toString());
|
|
29
|
+
if (Array.isArray(items)) {
|
|
30
|
+
this.apiList.push(...items);
|
|
31
|
+
} else {
|
|
32
|
+
this.logger.warn(`Invalid "${file}" config:`, items);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
this.logger.error(`Parse "${file}" error:`, err);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Default API
|
|
41
|
+
this.apiList[this.init.apiUrl ? 'unshift' : 'push']({
|
|
42
|
+
url: this.init.url || DEFAULT_API_URL,
|
|
43
|
+
key: this.init.key,
|
|
44
|
+
model: this.init.model || DEFAULT_MODEL,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get model() {
|
|
49
|
+
return this.api?.model || this.apiList[0].model;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get apiUrl() {
|
|
53
|
+
return this.api?.url || this.apiList[0].url;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get maxInputTokens() {
|
|
57
|
+
return this.init.maxInputTokens;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get logger() {
|
|
61
|
+
return this.init.logger;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
createPromptsBatchesByDiff(parseDiff) {
|
|
65
|
+
const maxChunkTokens = parseDiff.at(-1)?.tokens || 0
|
|
66
|
+
|
|
67
|
+
if (maxChunkTokens > this.maxInputTokens) {
|
|
68
|
+
this.logger.error(`TODO: Diff is too large`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const batches = parseDiff.reduce((acc, file) => {
|
|
73
|
+
if (!acc[0] || acc[0].tokens + file.tokens > this.maxInputTokens) {
|
|
74
|
+
acc.unshift({tokens: 0, diff: '', languages: []});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fileDiff = file.diff.hunks.flatMap(h => h.changes).join('\n');
|
|
78
|
+
if (fileDiff.trim()) {
|
|
79
|
+
acc[0].tokens += file.tokens;
|
|
80
|
+
acc[0].diff += `### File **${file.filename}**:\n${fileDiff}\n\n`;
|
|
81
|
+
|
|
82
|
+
if (!acc[0].languages.includes(file.programmingLanguage)) {
|
|
83
|
+
acc[0].languages.push(file.programmingLanguage);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return acc;
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
return batches;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async generate(prompt, context) {
|
|
94
|
+
try {
|
|
95
|
+
const api = await this._getApi();
|
|
96
|
+
if (api.url.includes('completions')) {
|
|
97
|
+
return await this._callCompletionsApi(api, prompt, context);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return await this._callGenerateApi(api, prompt, context);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
this.logger.error(`Failed to generate LLM response:`, e);
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _getApi() {
|
|
108
|
+
if (!this.api) {
|
|
109
|
+
// By default
|
|
110
|
+
this.api = {url: DEFAULT_API_URL, model: DEFAULT_MODEL};
|
|
111
|
+
|
|
112
|
+
for (const api of this.apiList) {
|
|
113
|
+
try {
|
|
114
|
+
const ctrl = new AbortController();
|
|
115
|
+
|
|
116
|
+
setTimeout(() => ctrl.abort(new Error('Module timeout')), 500);
|
|
117
|
+
|
|
118
|
+
const resp = await fetch(api.url, {method: 'HEAD', signal: ctrl.signal});
|
|
119
|
+
if (resp.status >= 200 && resp.status < 500) {
|
|
120
|
+
this.api = api;
|
|
121
|
+
return api;
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return this.api;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async _callGenerateApi(api, prompt, context) {
|
|
131
|
+
const req = await fetch(api.url, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {'Content-Type': 'application/json'},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
model: api.model,
|
|
136
|
+
stream: false,
|
|
137
|
+
prompt,
|
|
138
|
+
context,
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
const data = await req.json();
|
|
142
|
+
return data.response || '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async _callCompletionsApi(api, prompt, context) {
|
|
146
|
+
const messages = [];
|
|
147
|
+
|
|
148
|
+
if (context) {
|
|
149
|
+
messages.push({ role: 'system', content: context });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
messages.push({ role: 'user', content: prompt });
|
|
153
|
+
|
|
154
|
+
const req = await fetch(api.url, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
'Authorization': `Bearer ${api.key}`,
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
model: api.model,
|
|
162
|
+
messages: messages,
|
|
163
|
+
temperature: 0.1,
|
|
164
|
+
stream: false,
|
|
165
|
+
timeout: this.init.timeout,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!req.ok) {
|
|
170
|
+
let errorBody = '';
|
|
171
|
+
try {
|
|
172
|
+
errorBody = await req.text();
|
|
173
|
+
} catch (e) { /* ignore */ }
|
|
174
|
+
|
|
175
|
+
throw new Error(`LLM completions request failed with status ${req.status}: ${errorBody}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const data = await req.json();
|
|
179
|
+
|
|
180
|
+
if (data.choices && data.choices.length > 0 && data.choices[0].message) {
|
|
181
|
+
return data.choices[0].message.content || '';
|
|
182
|
+
} else {
|
|
183
|
+
this.logger.warn(style.yellow('LLM completions response structure unexpected:'), data);
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_EXTENSIONS = ['.js', '.ts', '.tsx'];
|
|
5
|
+
|
|
6
|
+
export const catGen = (paths, extensions = DEFAULT_EXTENSIONS) => {
|
|
7
|
+
const extSet = new Set(extensions);
|
|
8
|
+
const results = [];
|
|
9
|
+
|
|
10
|
+
const process = (inputPath) => {
|
|
11
|
+
const absoluteInputPath = path.resolve(inputPath);
|
|
12
|
+
if (!fs.existsSync(absoluteInputPath)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const initialStats = fs.statSync(absoluteInputPath);
|
|
17
|
+
const basePath = initialStats.isDirectory()
|
|
18
|
+
? absoluteInputPath
|
|
19
|
+
: path.dirname(absoluteInputPath);
|
|
20
|
+
|
|
21
|
+
const walk = (currentPath) => {
|
|
22
|
+
const stats = fs.statSync(currentPath);
|
|
23
|
+
|
|
24
|
+
if (stats.isFile()) {
|
|
25
|
+
const ext = path.extname(currentPath).toLowerCase();
|
|
26
|
+
if (extSet.has(ext)) {
|
|
27
|
+
const relativePath = path.relative(basePath, currentPath);
|
|
28
|
+
|
|
29
|
+
const content = fs.readFileSync(currentPath, 'utf8');
|
|
30
|
+
results.push({
|
|
31
|
+
basePath,
|
|
32
|
+
currentPath,
|
|
33
|
+
relativePath,
|
|
34
|
+
content,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
} else if (stats.isDirectory()) {
|
|
38
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const fullEntryPath = path.join(currentPath, entry.name);
|
|
41
|
+
walk(fullEntryPath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
walk(absoluteInputPath);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
(paths || []).forEach(process);
|
|
50
|
+
|
|
51
|
+
return results;
|
|
52
|
+
};
|
|
53
|
+
|