devsplain 2.1.1 → 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/lib/config.js +284 -207
- package/package.json +1 -1
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/package.json
CHANGED