devsplain 2.1.0 → 2.2.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/bin/cli.js +96 -82
- package/lib/config.js +284 -207
- package/lib/llm.js +42 -13
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const { execSync } = require('child_process');
|
|
|
10
10
|
let rl;
|
|
11
11
|
let askQuestion;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Checks if the Git working tree is dirty [ds]
|
|
14
14
|
function isGitDirty() {
|
|
15
15
|
try {
|
|
16
16
|
const gitDir = execSync('git rev-parse --is-inside-work-tree', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
|
@@ -23,7 +23,7 @@ function isGitDirty() {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Checks if a line of code is inside a string literal [ds]
|
|
27
27
|
function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
28
28
|
const isPython = ext.toLowerCase() === '.py';
|
|
29
29
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
@@ -116,49 +116,55 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
116
116
|
break; // Ignore rest of line
|
|
117
117
|
}
|
|
118
118
|
} else {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
j
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
133
|
-
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
134
|
-
if (match) {
|
|
135
|
-
cppRawDelimiter = match[1];
|
|
136
|
-
inCppRawString = true;
|
|
137
|
-
j += match[0].length;
|
|
119
|
+
const isShellOrRuby = ['.sh', '.rb'].includes(ext.toLowerCase());
|
|
120
|
+
if (isShellOrRuby) {
|
|
121
|
+
if (line[j] === '#') {
|
|
122
|
+
break; // Ignore rest of line
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
if (line.slice(j, j + 2) === '//') {
|
|
126
|
+
break; // Ignore rest of line
|
|
127
|
+
}
|
|
128
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
129
|
+
inBlockJS = true;
|
|
130
|
+
blockDepthJS = 1;
|
|
131
|
+
j += 2;
|
|
138
132
|
continue;
|
|
139
133
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
134
|
+
if (ext.toLowerCase() === '.php' && line[j] === '#') {
|
|
135
|
+
break; // Ignore rest of line
|
|
136
|
+
}
|
|
137
|
+
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
138
|
+
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
139
|
+
if (match) {
|
|
140
|
+
cppRawDelimiter = match[1];
|
|
141
|
+
inCppRawString = true;
|
|
142
|
+
j += match[0].length;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (isJS && line[j] === '/') {
|
|
147
|
+
let k = j - 1;
|
|
148
|
+
while (k >= 0 && /\s/.test(line[k])) k--;
|
|
149
|
+
let isRegex = false;
|
|
150
|
+
if (k < 0) {
|
|
150
151
|
isRegex = true;
|
|
151
152
|
} else {
|
|
152
|
-
const
|
|
153
|
-
if (/(
|
|
153
|
+
const prevChar = line[k];
|
|
154
|
+
if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
|
|
154
155
|
isRegex = true;
|
|
156
|
+
} else {
|
|
157
|
+
const prefix = line.slice(0, k + 1);
|
|
158
|
+
if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
|
|
159
|
+
isRegex = true;
|
|
160
|
+
}
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
if (isRegex) {
|
|
164
|
+
inRegex = true;
|
|
165
|
+
j++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
170
|
}
|
|
@@ -230,7 +236,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
230
236
|
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
|
|
239
|
+
// Analyzes comments in a given set of code lines [ds]
|
|
234
240
|
function analyzeComments(lines, ext = '') {
|
|
235
241
|
const isPython = ext.toLowerCase() === '.py';
|
|
236
242
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
@@ -329,52 +335,59 @@ function analyzeComments(lines, ext = '') {
|
|
|
329
335
|
break;
|
|
330
336
|
}
|
|
331
337
|
} else {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
349
|
-
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
350
|
-
if (match) {
|
|
351
|
-
cppRawDelimiter = match[1];
|
|
352
|
-
inCppRawString = true;
|
|
353
|
-
j += match[0].length;
|
|
338
|
+
const isShellOrRuby = ['.sh', '.rb'].includes(ext.toLowerCase());
|
|
339
|
+
if (isShellOrRuby) {
|
|
340
|
+
if (line[j] === '#') {
|
|
341
|
+
commentStartIndex = j;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
if (line.slice(j, j + 2) === '//') {
|
|
346
|
+
commentStartIndex = j;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
350
|
+
commentStartIndex = j;
|
|
351
|
+
inBlockJS = true;
|
|
352
|
+
blockDepthJS = 1;
|
|
353
|
+
j += 2;
|
|
354
354
|
continue;
|
|
355
355
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
356
|
+
if (ext.toLowerCase() === '.php' && line[j] === '#') {
|
|
357
|
+
commentStartIndex = j;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
361
|
+
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
362
|
+
if (match) {
|
|
363
|
+
cppRawDelimiter = match[1];
|
|
364
|
+
inCppRawString = true;
|
|
365
|
+
j += match[0].length;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (isJS && line[j] === '/') {
|
|
370
|
+
let k = j - 1;
|
|
371
|
+
while (k >= 0 && /\s/.test(line[k])) k--;
|
|
372
|
+
let isRegex = false;
|
|
373
|
+
if (k < 0) {
|
|
366
374
|
isRegex = true;
|
|
367
375
|
} else {
|
|
368
|
-
const
|
|
369
|
-
if (/(
|
|
376
|
+
const prevChar = line[k];
|
|
377
|
+
if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
|
|
370
378
|
isRegex = true;
|
|
379
|
+
} else {
|
|
380
|
+
const prefix = line.slice(0, k + 1);
|
|
381
|
+
if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
|
|
382
|
+
isRegex = true;
|
|
383
|
+
}
|
|
371
384
|
}
|
|
372
385
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
386
|
+
if (isRegex) {
|
|
387
|
+
inRegex = true;
|
|
388
|
+
j++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
378
391
|
}
|
|
379
392
|
}
|
|
380
393
|
}
|
|
@@ -464,7 +477,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
464
477
|
return analysis;
|
|
465
478
|
}
|
|
466
479
|
|
|
467
|
-
|
|
480
|
+
// Splices comments into the given code data [ds]
|
|
468
481
|
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
469
482
|
const hasCRLF = data.includes('\r\n');
|
|
470
483
|
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
@@ -656,7 +669,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
656
669
|
return annotated.map(line => line.text).join(lineEnding);
|
|
657
670
|
}
|
|
658
671
|
|
|
659
|
-
|
|
672
|
+
// Runs the CLI interface for the application [ds]
|
|
660
673
|
async function runCLI() {
|
|
661
674
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
662
675
|
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
@@ -800,7 +813,6 @@ Options:
|
|
|
800
813
|
];
|
|
801
814
|
const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
|
|
802
815
|
|
|
803
|
-
/** Checks if a path is ignored based on the ignore patterns [ds] */
|
|
804
816
|
function isPathIgnored(targetPath) {
|
|
805
817
|
const filename = path.basename(targetPath);
|
|
806
818
|
for (const pattern of allIgnored) {
|
|
@@ -811,7 +823,7 @@ Options:
|
|
|
811
823
|
return false;
|
|
812
824
|
}
|
|
813
825
|
|
|
814
|
-
|
|
826
|
+
// Processes a given file path, either a file or directory [ds]
|
|
815
827
|
async function processPath(targetPath) {
|
|
816
828
|
const stats = fs.statSync(targetPath);
|
|
817
829
|
|
|
@@ -869,6 +881,7 @@ Options:
|
|
|
869
881
|
fs.renameSync(tempPath, targetPath);
|
|
870
882
|
console.log(` Successfully saved ${targetPath}`);
|
|
871
883
|
} else {
|
|
884
|
+
// Asks the user if they want to save the commented code to a file [ds]
|
|
872
885
|
console.log(` Skipped ${targetPath}`);
|
|
873
886
|
}
|
|
874
887
|
} else {
|
|
@@ -901,6 +914,7 @@ Options:
|
|
|
901
914
|
rl.close();
|
|
902
915
|
}
|
|
903
916
|
|
|
917
|
+
// Starts the CLI application if this script is run directly [ds]
|
|
904
918
|
if (require.main === module) {
|
|
905
919
|
runCLI().catch(err => {
|
|
906
920
|
console.error(err);
|
package/lib/config.js
CHANGED
|
@@ -1,207 +1,284 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const readline = require('readline');
|
|
5
|
-
const configPath = path.join(os.homedir(), '.devsplainrc');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function askSecret(query) {
|
|
9
|
-
return new Promise((resolve) => {
|
|
10
|
-
const rl = readline.createInterface({
|
|
11
|
-
input: process.stdin,
|
|
12
|
-
output: process.stdout
|
|
13
|
-
});
|
|
14
|
-
process.stdout.write(query);
|
|
15
|
-
process.stdin.setRawMode(true);
|
|
16
|
-
let key = '';
|
|
17
|
-
const onData = (char) => {
|
|
18
|
-
char = char.toString();
|
|
19
|
-
if (char === '\n' || char === '\r') {
|
|
20
|
-
process.stdin.setRawMode(false);
|
|
21
|
-
process.stdin.removeListener('data', onData);
|
|
22
|
-
process.stdout.write('\n');
|
|
23
|
-
rl.close();
|
|
24
|
-
resolve(key);
|
|
25
|
-
} else if (char === '\u0003') { // Ctrl+C
|
|
26
|
-
process.exit();
|
|
27
|
-
} else {
|
|
28
|
-
key += char;
|
|
29
|
-
process.stdout.write('*');
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
process.stdin.on('data', onData);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const configPath = path.join(os.homedir(), '.devsplainrc');
|
|
6
|
+
|
|
7
|
+
// Asks for secret input from the user [ds]
|
|
8
|
+
function askSecret(query) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout
|
|
13
|
+
});
|
|
14
|
+
process.stdout.write(query);
|
|
15
|
+
process.stdin.setRawMode(true);
|
|
16
|
+
let key = '';
|
|
17
|
+
const onData = (char) => {
|
|
18
|
+
char = char.toString();
|
|
19
|
+
if (char === '\n' || char === '\r') {
|
|
20
|
+
process.stdin.setRawMode(false);
|
|
21
|
+
process.stdin.removeListener('data', onData);
|
|
22
|
+
process.stdout.write('\n');
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(key);
|
|
25
|
+
} else if (char === '\u0003') { // Ctrl+C
|
|
26
|
+
process.exit();
|
|
27
|
+
} else {
|
|
28
|
+
key += char;
|
|
29
|
+
process.stdout.write('*');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
process.stdin.on('data', onData);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Migrates old config to the new format [ds]
|
|
37
|
+
function migrateConfig(oldConfig) {
|
|
38
|
+
if (oldConfig.provider && !oldConfig.providers) {
|
|
39
|
+
return {
|
|
40
|
+
activeProvider: oldConfig.provider,
|
|
41
|
+
providers: {
|
|
42
|
+
[oldConfig.provider]: {
|
|
43
|
+
apiKey: oldConfig.apiKey || '',
|
|
44
|
+
model: oldConfig.model || '',
|
|
45
|
+
baseUrl: oldConfig.baseUrl || null,
|
|
46
|
+
autoPrune: oldConfig.autoPrune || false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return oldConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Gets the config, either from environment variables, the config file, or by prompting the user [ds]
|
|
55
|
+
async function getConfig(forceWizard = false) {
|
|
56
|
+
if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
|
|
57
|
+
const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
|
|
58
|
+
const model = process.env.DEVSPLAIN_MODEL || (provider === 'gemini' ? 'gemini-2.0-flash' : (provider === 'claude' ? 'claude-3-5-sonnet-20240620' : 'llama-3.3-70b-versatile'));
|
|
59
|
+
const baseUrl = process.env.DEVSPLAIN_BASE_URL || (provider === 'gemini' ? null : (provider === 'claude' ? 'https://api.anthropic.com' : 'https://api.groq.com/openai'));
|
|
60
|
+
return {
|
|
61
|
+
provider,
|
|
62
|
+
apiKey: process.env.DEVSPLAIN_API_KEY || '',
|
|
63
|
+
model,
|
|
64
|
+
baseUrl
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let fileConfig = null;
|
|
69
|
+
if (fs.existsSync(configPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const rawData = fs.readFileSync(configPath, 'utf8');
|
|
72
|
+
fileConfig = migrateConfig(JSON.parse(rawData));
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// Ignored, file might be corrupted
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!fileConfig || forceWizard) {
|
|
79
|
+
let rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout
|
|
82
|
+
});
|
|
83
|
+
let askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
84
|
+
|
|
85
|
+
let config = fileConfig || { activeProvider: '', providers: {} };
|
|
86
|
+
let confirmed = false;
|
|
87
|
+
|
|
88
|
+
while (!confirmed) {
|
|
89
|
+
const savedProviders = Object.keys(config.providers);
|
|
90
|
+
let providerToConfig = null;
|
|
91
|
+
let wantToUpdate = true;
|
|
92
|
+
let isNewProvider = false;
|
|
93
|
+
|
|
94
|
+
if (savedProviders.length > 0) {
|
|
95
|
+
console.log("\nSaved Providers:");
|
|
96
|
+
savedProviders.forEach((p, i) => {
|
|
97
|
+
const isActive = config.activeProvider === p ? ' (active)' : '';
|
|
98
|
+
console.log(`${i + 1}. ${p}${isActive}`);
|
|
99
|
+
});
|
|
100
|
+
console.log(`\n${savedProviders.length + 1}. Add/Configure a different provider`);
|
|
101
|
+
|
|
102
|
+
const c = await askQuestion(`Select (1-${savedProviders.length + 1}): `);
|
|
103
|
+
const idx = parseInt(c) - 1;
|
|
104
|
+
|
|
105
|
+
if (idx >= 0 && idx < savedProviders.length) {
|
|
106
|
+
providerToConfig = savedProviders[idx];
|
|
107
|
+
const update = await askQuestion(`Do you want to update the API key or model for ${providerToConfig}? (y/N): `);
|
|
108
|
+
if (update.trim().toLowerCase() !== 'y' && update.trim().toLowerCase() !== 'yes') {
|
|
109
|
+
wantToUpdate = false;
|
|
110
|
+
}
|
|
111
|
+
} else if (idx === savedProviders.length) {
|
|
112
|
+
isNewProvider = true;
|
|
113
|
+
} else {
|
|
114
|
+
console.log("Invalid choice.");
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
isNewProvider = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let baseUrl = "";
|
|
122
|
+
let model = "";
|
|
123
|
+
let provider = "";
|
|
124
|
+
let apiKey = '';
|
|
125
|
+
let autoPrune = false;
|
|
126
|
+
|
|
127
|
+
if (wantToUpdate) {
|
|
128
|
+
if (isNewProvider || !providerToConfig) {
|
|
129
|
+
console.log("\nWhich AI Provider Do You want to use?");
|
|
130
|
+
console.log("1. Groq (Free, Fast, Llama-3)");
|
|
131
|
+
console.log("2. Gemini (Free Tier)");
|
|
132
|
+
console.log("3. OpenAI (Paid)");
|
|
133
|
+
console.log("4. Custom (Ollama, local, etc)");
|
|
134
|
+
console.log("5. Claude (Anthropic)");
|
|
135
|
+
|
|
136
|
+
const choice = await askQuestion("Select (1-5): ");
|
|
137
|
+
|
|
138
|
+
if (choice === '1') {
|
|
139
|
+
provider = 'groq';
|
|
140
|
+
baseUrl = 'https://api.groq.com/openai';
|
|
141
|
+
console.log("\nGet your free Groq key here: https://console.groq.com/keys");
|
|
142
|
+
const customModel = await askQuestion("Model name (press Enter for default 'llama-3.3-70b-versatile'): ");
|
|
143
|
+
model = customModel.trim() || 'llama-3.3-70b-versatile';
|
|
144
|
+
} else if (choice === '2') {
|
|
145
|
+
provider = 'gemini';
|
|
146
|
+
baseUrl = null;
|
|
147
|
+
console.log("\nGet your free Gemini key here: https://aistudio.google.com/apikey");
|
|
148
|
+
const customModel = await askQuestion("Model name (press Enter for default 'gemini-2.0-flash'): ");
|
|
149
|
+
model = customModel.trim() || 'gemini-2.0-flash';
|
|
150
|
+
} else if (choice === '3') {
|
|
151
|
+
provider = 'openai';
|
|
152
|
+
baseUrl = 'https://api.openai.com';
|
|
153
|
+
console.log("\nGet your OpenAI key here: https://platform.openai.com/api-keys");
|
|
154
|
+
const customModel = await askQuestion("Model name (press Enter for default 'gpt-4o'): ");
|
|
155
|
+
model = customModel.trim() || 'gpt-4o';
|
|
156
|
+
} else if (choice === '4') {
|
|
157
|
+
provider = 'custom';
|
|
158
|
+
while (true) {
|
|
159
|
+
model = (await askQuestion("Model name (e.g., llama3): ")).trim();
|
|
160
|
+
if (model) break;
|
|
161
|
+
console.log("Model name cannot be empty.");
|
|
162
|
+
}
|
|
163
|
+
while (true) {
|
|
164
|
+
baseUrl = (await askQuestion("Base URL (e.g., http://localhost:11434): ")).trim();
|
|
165
|
+
if (baseUrl) break;
|
|
166
|
+
console.log("Base URL cannot be empty.");
|
|
167
|
+
}
|
|
168
|
+
} else if (choice === '5') {
|
|
169
|
+
provider = 'claude';
|
|
170
|
+
baseUrl = 'https://api.anthropic.com';
|
|
171
|
+
console.log("\nGet your Anthropic key here: https://console.anthropic.com/settings/keys");
|
|
172
|
+
const customModel = await askQuestion("Model name (press Enter for default 'claude-3-5-sonnet-20240620'): ");
|
|
173
|
+
model = customModel.trim() || 'claude-3-5-sonnet-20240620';
|
|
174
|
+
} else {
|
|
175
|
+
console.log("Invalid choice. Please select 1, 2, 3, 4, or 5.");
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
provider = providerToConfig;
|
|
180
|
+
const old = config.providers[provider];
|
|
181
|
+
baseUrl = old.baseUrl;
|
|
182
|
+
|
|
183
|
+
let defaultModel = old.model;
|
|
184
|
+
const customModel = await askQuestion(`Model name (press Enter for default '${defaultModel}'): `);
|
|
185
|
+
model = customModel.trim() || defaultModel;
|
|
186
|
+
|
|
187
|
+
if (provider === 'custom') {
|
|
188
|
+
const customBase = await askQuestion(`Base URL (press Enter for default '${baseUrl}'): `);
|
|
189
|
+
baseUrl = customBase.trim() || baseUrl;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
while (true) {
|
|
194
|
+
const promptMsg = provider === 'custom'
|
|
195
|
+
? "Paste your API key (leave blank for local models): "
|
|
196
|
+
: "Paste your API key: ";
|
|
197
|
+
|
|
198
|
+
if (process.stdin.isTTY) {
|
|
199
|
+
rl.close();
|
|
200
|
+
apiKey = await askSecret(promptMsg);
|
|
201
|
+
|
|
202
|
+
rl = readline.createInterface({
|
|
203
|
+
input: process.stdin,
|
|
204
|
+
output: process.stdout
|
|
205
|
+
});
|
|
206
|
+
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
207
|
+
} else {
|
|
208
|
+
apiKey = await askQuestion(promptMsg);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
apiKey = apiKey.trim();
|
|
212
|
+
if (provider === 'custom' || apiKey) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
console.log(`API key is required for provider '${provider}'.`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
while (true) {
|
|
219
|
+
const pruneAns = (await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ")).trim().toLowerCase();
|
|
220
|
+
if (pruneAns === '' || pruneAns === 'n' || pruneAns === 'no') {
|
|
221
|
+
autoPrune = false;
|
|
222
|
+
break;
|
|
223
|
+
} else if (pruneAns === 'y' || pruneAns === 'yes') {
|
|
224
|
+
autoPrune = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
console.log("Invalid choice. Please enter 'y' or 'n'.");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log("\n--- Configuration Summary ---");
|
|
231
|
+
console.log(`Provider: ${provider}`);
|
|
232
|
+
console.log(`Model: ${model}`);
|
|
233
|
+
console.log(`Base URL: ${baseUrl || 'N/A'}`);
|
|
234
|
+
console.log(`API Key: ${apiKey ? apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 4)) : 'None'}`);
|
|
235
|
+
console.log(`Auto-Prune: ${autoPrune ? 'Yes' : 'No'}`);
|
|
236
|
+
console.log("-----------------------------\n");
|
|
237
|
+
|
|
238
|
+
while (true) {
|
|
239
|
+
const confirm = (await askQuestion("Does this look correct? (y/n, default: y): ")).trim().toLowerCase();
|
|
240
|
+
if (confirm === '' || confirm === 'y' || confirm === 'yes') {
|
|
241
|
+
config.activeProvider = provider;
|
|
242
|
+
config.providers[provider] = {
|
|
243
|
+
apiKey,
|
|
244
|
+
model,
|
|
245
|
+
baseUrl,
|
|
246
|
+
autoPrune
|
|
247
|
+
};
|
|
248
|
+
confirmed = true;
|
|
249
|
+
break;
|
|
250
|
+
} else if (confirm === 'n' || confirm === 'no') {
|
|
251
|
+
break; // Start over loop
|
|
252
|
+
}
|
|
253
|
+
console.log("Invalid choice. Please enter 'y' or 'n'.");
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
// User just selected an existing provider and didn't want to update it
|
|
257
|
+
config.activeProvider = providerToConfig;
|
|
258
|
+
console.log(`\nSwitched active provider to ${config.activeProvider}.`);
|
|
259
|
+
confirmed = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
rl.close();
|
|
264
|
+
|
|
265
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
266
|
+
try {
|
|
267
|
+
if (process.platform !== 'win32') {
|
|
268
|
+
fs.chmodSync(configPath, 0o600);
|
|
269
|
+
}
|
|
270
|
+
} catch (chmodErr) {}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
provider: config.activeProvider,
|
|
274
|
+
...config.providers[config.activeProvider]
|
|
275
|
+
};
|
|
276
|
+
} else {
|
|
277
|
+
return {
|
|
278
|
+
provider: fileConfig.activeProvider,
|
|
279
|
+
...fileConfig.providers[fileConfig.activeProvider]
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = { getConfig };
|
package/lib/llm.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// Asynchronously fetches data from the specified URL with a retry mechanism [ds]
|
|
2
2
|
async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000) {
|
|
3
3
|
let lastError;
|
|
4
|
+
// Loop through each attempt, up to the maximum number of retries [ds]
|
|
4
5
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
5
6
|
const controller = new AbortController();
|
|
6
7
|
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
@@ -36,16 +37,44 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
|
|
|
36
37
|
throw lastError;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
// Retrieves comments for the given code, language, and configuration [ds]
|
|
40
41
|
async function getComments(code, language, config, mode = 'default') {
|
|
41
42
|
const lines = code.split(/\r?\n/);
|
|
42
43
|
const numberedCode = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
// Extract the file extension from the language string [ds]
|
|
46
|
+
const extMatch = language.match(/\.[0-9a-z]+$/i);
|
|
47
|
+
const ext = extMatch ? extMatch[0].toLowerCase() : '';
|
|
48
|
+
const isPython = ext === '.py';
|
|
49
|
+
const isRubyOrShell = ['.rb', '.sh', '.php'].includes(ext);
|
|
50
|
+
const isHTML = ['.html', '.vue', '.svelte'].includes(ext);
|
|
51
|
+
const isSql = ext === '.sql';
|
|
52
|
+
|
|
53
|
+
// Define the single-line comment token and examples [ds]
|
|
54
|
+
let singleLineToken = '//';
|
|
55
|
+
let blockExample = '/** Calculates the total price */';
|
|
56
|
+
let inlineExample = '// Check for null values';
|
|
57
|
+
|
|
58
|
+
if (isPython || isRubyOrShell) {
|
|
59
|
+
singleLineToken = '#';
|
|
60
|
+
blockExample = '# Calculates the total price';
|
|
61
|
+
inlineExample = '# Check for null values';
|
|
62
|
+
} else if (isHTML) {
|
|
63
|
+
singleLineToken = '<!--';
|
|
64
|
+
blockExample = '<!-- Calculates the total price -->';
|
|
65
|
+
inlineExample = '<!-- Check for null values -->';
|
|
66
|
+
} else if (isSql) {
|
|
67
|
+
singleLineToken = '--';
|
|
68
|
+
blockExample = '-- Calculates the total price';
|
|
69
|
+
inlineExample = '-- Check for null values';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Provide instructions based on the mode [ds]
|
|
73
|
+
let instruction = `Provide block comments above functions and sparse inline comments for complex logic.`;
|
|
45
74
|
if (mode === 'light') {
|
|
46
|
-
instruction =
|
|
75
|
+
instruction = `Provide ONLY block comments above functions. Keep it minimal.`;
|
|
47
76
|
} else if (mode === 'full') {
|
|
48
|
-
instruction =
|
|
77
|
+
instruction = `Provide highly detailed block comments above functions, and exhaustive step-by-step inline comments explaining every conditional branch, loop, variable assignment, and logical block inside function bodies. Do not be sparse; explain the code's execution flow in detail.`;
|
|
49
78
|
}
|
|
50
79
|
|
|
51
80
|
let prompt = `
|
|
@@ -54,24 +83,24 @@ ${instruction}
|
|
|
54
83
|
|
|
55
84
|
CRITICAL RULES:
|
|
56
85
|
1. You MUST respond with ONLY a raw, valid JSON array of objects. NO markdown formatting, NO backticks, NO explanations, NO text before or after the JSON.
|
|
57
|
-
2. Each object must have exactly two properties: "line" (the integer line number where the comment should be inserted ABOVE) and "comment" (the text of the comment itself
|
|
86
|
+
2. Each object must have exactly two properties: "line" (the integer line number where the comment should be inserted ABOVE) and "comment" (the text of the comment itself).
|
|
58
87
|
3. Do NOT include the original code in your response.
|
|
59
88
|
4. If no comments are needed, return an empty array: [].
|
|
89
|
+
5. IMPORTANT: Use ONLY ${singleLineToken} for comments. DO NOT use docstrings or multi-line string literals like """ or ''' for comments.
|
|
60
90
|
|
|
61
91
|
Example Output:
|
|
62
92
|
[
|
|
63
|
-
{ "line": 4, "comment": "
|
|
64
|
-
{ "line": 12, "comment": "
|
|
93
|
+
{ "line": 4, "comment": "${blockExample}" },
|
|
94
|
+
{ "line": 12, "comment": "${inlineExample}" }
|
|
65
95
|
]
|
|
66
96
|
|
|
67
97
|
Here is the source code:
|
|
68
98
|
${numberedCode}
|
|
69
99
|
`.trim();
|
|
70
100
|
|
|
71
|
-
// Initialize text response [ds]
|
|
72
101
|
let textResponse = "";
|
|
73
102
|
|
|
74
|
-
//
|
|
103
|
+
// Handle API requests based on the provider [ds]
|
|
75
104
|
if (config.provider === 'gemini') {
|
|
76
105
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
|
|
77
106
|
let data;
|
|
@@ -94,6 +123,7 @@ ${numberedCode}
|
|
|
94
123
|
}
|
|
95
124
|
textResponse = data.candidates[0].content.parts[0].text;
|
|
96
125
|
} else if (config.provider === 'claude') {
|
|
126
|
+
// Handle Claude API requests [ds]
|
|
97
127
|
const url = `${config.baseUrl}/v1/messages`;
|
|
98
128
|
let data;
|
|
99
129
|
try {
|
|
@@ -122,7 +152,7 @@ ${numberedCode}
|
|
|
122
152
|
}
|
|
123
153
|
textResponse = data.content[0].text;
|
|
124
154
|
}
|
|
125
|
-
//
|
|
155
|
+
// Handle other API requests [ds]
|
|
126
156
|
else {
|
|
127
157
|
const url = `${config.baseUrl}/v1/chat/completions`;
|
|
128
158
|
let data;
|
|
@@ -152,7 +182,6 @@ ${numberedCode}
|
|
|
152
182
|
textResponse = data.choices[0].message.content;
|
|
153
183
|
}
|
|
154
184
|
|
|
155
|
-
// Clean up the text response [ds]
|
|
156
185
|
let cleanText = textResponse.trim();
|
|
157
186
|
const start = cleanText.indexOf('[');
|
|
158
187
|
const end = cleanText.lastIndexOf(']');
|
|
@@ -160,9 +189,9 @@ ${numberedCode}
|
|
|
160
189
|
cleanText = cleanText.substring(start, end + 1);
|
|
161
190
|
}
|
|
162
191
|
|
|
163
|
-
// Parse the response as JSON [ds]
|
|
164
192
|
let parsed;
|
|
165
193
|
try {
|
|
194
|
+
// Attempt to parse the response as JSON [ds]
|
|
166
195
|
parsed = JSON.parse(cleanText);
|
|
167
196
|
} catch (e) {
|
|
168
197
|
throw new Error(`Parsing Error: Failed to parse LLM response as JSON. Raw response was:\n${textResponse}`);
|
package/package.json
CHANGED