gennady 0.2.2 β†’ 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  πŸ€– Gennadyᡇᡉᡗᡃ πŸ—―οΈ
2
2
  -----------------
3
- **Gen**erate **N**ext-level **A**utomated **D**escription **Y**ntelligence.
3
+ **GEN**erate **N**ext-level **A**utomated **D**escription **Y**ntelligence.
4
4
 
5
5
  ```bash
6
6
  npx gennady
@@ -8,7 +8,9 @@ npx gennady
8
8
 
9
9
  ---
10
10
 
11
- ### Setup Local LLM
11
+ ## Setup LLM
12
+
13
+ ### Local
12
14
 
13
15
  ```sh
14
16
  brew install ollama
@@ -18,6 +20,22 @@ ollama serve
18
20
 
19
21
  ---
20
22
 
23
+ ### External
24
+
25
+ Create `~/.gennadyrc` file:
26
+
27
+ ```json
28
+ [
29
+ {
30
+ "url": "https://api.openai.com/v1/chat/completions",
31
+ "key": "...",
32
+ "model": "gpt-4o"
33
+ }
34
+ ]
35
+ ```
36
+
37
+ ---
38
+
21
39
  ### Usage
22
40
 
23
41
  ```bash
package/cli/gennady.js CHANGED
@@ -16,8 +16,9 @@ const PROMPTS_DIR = join(
16
16
  const params = parseArgs(process.argv, {
17
17
  mode: ['mode', 'm'],
18
18
  oneline: ['short', 'one', 'o'],
19
- reviewerModel: ['model'],
19
+ model: ['model'],
20
20
  targetBranch: ['branch', 'b'],
21
+ apiUrl: ['api', 'apiUrl'],
21
22
  });
22
23
 
23
24
  const commit = new CommitGen({
@@ -28,7 +29,9 @@ const commit = new CommitGen({
28
29
  translatePromptTemplate: readFileSync(join(PROMPTS_DIR, 'translate-prompt.md')).toString(),
29
30
  });
30
31
 
31
- console.info(`πŸ€–`, style.whiteBright.bold(`GENNADY`), `(${style.cyan(commit.reviewerModel)} β†’ ${style.yellow(commit.mode)})`, `πŸ—―οΈ`);
32
+ console.info(`πŸ€–`, style.whiteBright.bold(`GENNADY`), `(${style.cyan(commit.model)} β†’ ${style.yellow(commit.mode)})`, `πŸ—―οΈ`);
33
+ console.info(style.gray(`-`.repeat(30)));
34
+ console.info(`- url: ${style.blue(commit.apiUrl)}`);
32
35
  console.info(style.gray(`-`.repeat(30)));
33
36
 
34
37
  const msg = await commit.generate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gennady",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "author": "Konstantin Lebedev <ibnrubaxa@gmail.com>",
5
5
  "description": "Gennady β€” Generate Next-level Automated Description Yntelligence",
6
6
  "keywords": [
@@ -1,26 +1,62 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import { getGitCommitCount, getGitDiff } from '../git/git-cmd.js';
2
4
  import { parseGitDiff } from '../git/git-diff.js';
3
5
  import { style } from '../utils/style.js';
4
6
 
7
+ const DEFAULT_MODEL = 'llama3:8b';
8
+ const DEFAULT_API_URL = 'http://127.0.0.1:11434/api/generate';
9
+
10
+ const GENNADY_RC_FILENAME = '.gennadyrc';
11
+
5
12
  export class CommitGen {
13
+ api;
14
+ apiList = [];
15
+
6
16
  constructor(init) {
7
17
  this.init = {
8
18
  mode: 'auto',
9
19
  oneline: false,
10
- reviewerModel: 'llama3:8b',
11
20
  targetBranch: undefined,
12
21
 
13
- maxInputTokens: init.maxInputTokens || 4000,
14
- ollamaUrl: init.ollamaUrl || 'http://127.0.0.1:11434/api/generate',
15
22
  logger: console,
23
+ maxInputTokens: init.maxInputTokens || 4000,
16
24
 
17
25
  basePromptTemplate: undefined,
18
26
  formatOnelinePromptTemplate: undefined,
19
27
  formatDetailedPromptTemplate: undefined,
20
28
  translatePromptTemplate: undefined,
21
29
 
30
+ timeout: 120,
31
+
22
32
  ...init,
23
33
  };
34
+
35
+ // Try parse rc files
36
+ [
37
+ join(process.cwd(), GENNADY_RC_FILENAME),
38
+ join(process.env.HOME, GENNADY_RC_FILENAME),
39
+ ].find((file) => {
40
+ try {
41
+ if (existsSync(file)) {
42
+ const items = JSON.parse(readFileSync(file).toString());
43
+ if (Array.isArray(items)) {
44
+ this.apiList.push(...items);
45
+ } else {
46
+ this.logger.warn(`Invalid "${file}" config:`, items);
47
+ }
48
+ }
49
+ } catch (err) {
50
+ this.logger.error(`Parse "${file}" error:`, err);
51
+ }
52
+ });
53
+
54
+ // Default API
55
+ this.apiList[this.init.apiUrl ? 'unshift' : 'push']({
56
+ url: this.init.apiUrl || DEFAULT_API_URL,
57
+ key: this.init.apiKey,
58
+ model: this.init.model || DEFAULT_MODEL,
59
+ });
24
60
  }
25
61
 
26
62
  get logger() {
@@ -31,8 +67,12 @@ export class CommitGen {
31
67
  return this.init.mode;
32
68
  }
33
69
 
34
- get reviewerModel() {
35
- return this.init.reviewerModel;
70
+ get model() {
71
+ return this.api?.model || this.apiList[0].model;
72
+ }
73
+
74
+ get apiUrl() {
75
+ return this.api?.url || this.apiList[0].url;
36
76
  }
37
77
 
38
78
  get targetBranch() {
@@ -45,25 +85,97 @@ export class CommitGen {
45
85
  .join('\n\n');
46
86
  }
47
87
 
88
+ async getApi() {
89
+ if (!this.api) {
90
+ // By default
91
+ this.api = {url: DEFAULT_API_URL, model: DEFAULT_MODEL};
92
+
93
+ for (const api of this.apiList) {
94
+ try {
95
+ const resp = await fetch(api.url, {method: 'HEAD', timeout: 1000});
96
+ if (resp.status >= 200 && resp.status < 500) {
97
+ this.api = api;
98
+ return api;
99
+ }
100
+ } catch {}
101
+ }
102
+ }
103
+
104
+ return this.api;
105
+ }
106
+
48
107
  async fetchPrompt(prompt, context) {
49
108
  try {
50
- const req = await fetch(this.init.ollamaUrl, {
51
- method: 'POST',
52
- headers: {'Content-Type': 'application/json'},
53
- body: JSON.stringify({
54
- model: this.init.reviewerModel,
55
- stream: false,
56
- prompt,
57
- context,
58
- }),
59
- });
60
- const data = await req.json();
61
- return data.response || '';
109
+ const api = await this.getApi();
110
+ if (api.url.includes('completions')) {
111
+ return await this.callCompletionsApi(api, prompt, context);
112
+ }
113
+
114
+ return await this.callGenerateApi(api, prompt, context);
62
115
  } catch (e) {
63
- this.logger.error(`Failed to generate ollama response:`, e);
116
+ this.logger.error(`Failed to generate LLM response:`, e);
64
117
  return '';
65
118
  }
66
119
  }
120
+
121
+ async callGenerateApi(api, prompt, context) {
122
+ const req = await fetch(api.url, {
123
+ method: 'POST',
124
+ headers: {'Content-Type': 'application/json'},
125
+ body: JSON.stringify({
126
+ model: api.model,
127
+ stream: false,
128
+ prompt,
129
+ context,
130
+ }),
131
+ });
132
+ const data = await req.json();
133
+ return data.response || '';
134
+ }
135
+
136
+ async callCompletionsApi(api, prompt, context) {
137
+ const messages = [];
138
+
139
+ if (context) {
140
+ messages.push({ role: 'system', content: context });
141
+ }
142
+
143
+ messages.push({ role: 'user', content: prompt });
144
+
145
+ const req = await fetch(api.url, {
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ 'Authorization': `Bearer ${api.key}`,
150
+ },
151
+ body: JSON.stringify({
152
+ model: api.model,
153
+ messages: messages,
154
+ temperature: 0.1,
155
+ stream: false,
156
+ timeout: this.init.timeout,
157
+ }),
158
+ });
159
+
160
+ if (!req.ok) {
161
+ let errorBody = '';
162
+ try {
163
+ errorBody = await req.text();
164
+ } catch (e) { /* ignore */ }
165
+
166
+ throw new Error(`LLM completions request failed with status ${req.status}: ${errorBody}`);
167
+ }
168
+
169
+ const data = await req.json();
170
+
171
+ if (data.choices && data.choices.length > 0 && data.choices[0].message) {
172
+ return data.choices[0].message.content || '';
173
+ } else {
174
+ this.logger.warn(style.yellow('LLM completions response structure unexpected:'), data);
175
+ return '';
176
+ }
177
+ }
178
+
67
179
  async generateCommitMessage(prompt) {
68
180
  const text = await this.fetchPrompt(prompt);
69
181
  return text;
@@ -1,4 +1,4 @@
1
- You are {languages} expert tasked with writing a git commit subject and description based on code changes.
1
+ You are {languages} expert tasked with writing a git commit subject and description based on code changes. Think of 3 options and choose the best one.
2
2
 
3
3
  **Subject:**
4
4
  - Clearly summarize the key changes in one concise yet informative sentence.
@@ -8,4 +8,5 @@ You are a {languages} expert tasked with writing a git commit subject and descri
8
8
 
9
9
  **Extremely important:**
10
10
  - The output must contain only commit message inside `<message>` and `</message>`.
11
- - Do not add unnecessary words and markup, strictly follow the output format.
11
+ - Do not add unnecessary words and markup, strictly follow the output format.
12
+ - The commit message of changes should be no more than 20 tokens.
@@ -9,7 +9,9 @@ export const parseArgs = (argv, schema) => {
9
9
 
10
10
  for (const [optionKey, aliases] of Object.entries(schema)) {
11
11
  if (aliases.includes(key)) {
12
- params[optionKey] = value || true;
12
+ params[optionKey] = value
13
+ ? value.replace(/^"|"$/g, '')
14
+ : true;
13
15
  break;
14
16
  }
15
17
  }