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.
Files changed (2) hide show
  1. package/lib/config.js +284 -207
  2. 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
- /** Asks the user for a secret input (e.g., an API key) without echoing the input to the console. [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
- /** Attempts to fetch the configuration settings, either from environment variables or by prompting the user. [ds] */
37
- async function getConfig(forceWizard = false) {
38
- if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
39
- const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
40
- const model = process.env.DEVSPLAIN_MODEL || (provider === 'gemini' ? 'gemini-2.0-flash' : (provider === 'claude' ? 'claude-3-5-sonnet-20240620' : 'llama-3.3-70b-versatile'));
41
- const baseUrl = process.env.DEVSPLAIN_BASE_URL || (provider === 'gemini' ? null : (provider === 'claude' ? 'https://api.anthropic.com' : 'https://api.groq.com/openai'));
42
- return {
43
- provider,
44
- apiKey: process.env.DEVSPLAIN_API_KEY || '',
45
- model,
46
- baseUrl
47
- };
48
- }
49
- // If the configuration file doesn't exist, or if we're forcing the wizard, let's prompt the user for configuration. [ds]
50
-
51
- if (!fs.existsSync(configPath) || forceWizard) {
52
- let rl = readline.createInterface({
53
- input: process.stdin,
54
- output: process.stdout
55
- });
56
- let askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
57
-
58
- let config = null;
59
- let confirmed = false;
60
-
61
- // Keep prompting the user until they confirm their configuration settings. [ds]
62
- while (!confirmed) {
63
- let baseUrl = "";
64
- let model = "";
65
- let provider = "";
66
-
67
- // Display the list of available AI providers to the user. [ds]
68
- console.log("\nWhich AI Provider Do You want to use?");
69
- console.log("1. Groq (Free, Fast, Llama-3)");
70
- console.log("2. Gemini (Free Tier)");
71
- console.log("3. OpenAI (Paid)");
72
- console.log("4. Custom (Ollama, local, etc)");
73
- console.log("5. Claude (Anthropic)");
74
-
75
- const choice = await askQuestion("Select (1-5): ");
76
-
77
- if (choice === '1') {
78
- provider = 'groq';
79
- baseUrl = 'https://api.groq.com/openai';
80
- console.log("\nGet your free Groq key here: https://console.groq.com/keys");
81
- const customModel = await askQuestion("Model name (press Enter for default 'llama-3.3-70b-versatile'): ");
82
- model = customModel.trim() || 'llama-3.3-70b-versatile';
83
- } else if (choice === '2') {
84
- provider = 'gemini';
85
- baseUrl = null;
86
- console.log("\nGet your free Gemini key here: https://aistudio.google.com/apikey");
87
- const customModel = await askQuestion("Model name (press Enter for default 'gemini-2.0-flash'): ");
88
- model = customModel.trim() || 'gemini-2.0-flash';
89
- } else if (choice === '3') {
90
- provider = 'openai';
91
- baseUrl = 'https://api.openai.com';
92
- console.log("\nGet your OpenAI key here: https://platform.openai.com/api-keys");
93
- const customModel = await askQuestion("Model name (press Enter for default 'gpt-4o'): ");
94
- model = customModel.trim() || 'gpt-4o';
95
- } else if (choice === '4') {
96
- provider = 'custom';
97
- while (true) {
98
- model = (await askQuestion("Model name (e.g., llama3): ")).trim();
99
- if (model) break;
100
- console.log("Model name cannot be empty.");
101
- }
102
- while (true) {
103
- baseUrl = (await askQuestion("Base URL (e.g., http://localhost:11434): ")).trim();
104
- if (baseUrl) break;
105
- console.log("Base URL cannot be empty.");
106
- }
107
- } else if (choice === '5') {
108
- provider = 'claude';
109
- baseUrl = 'https://api.anthropic.com';
110
- console.log("\nGet your Anthropic key here: https://console.anthropic.com/settings/keys");
111
- const customModel = await askQuestion("Model name (press Enter for default 'claude-3-5-sonnet-20240620'): ");
112
- model = customModel.trim() || 'claude-3-5-sonnet-20240620';
113
- } else {
114
- console.log("Invalid choice. Please select 1, 2, 3, 4, or 5.");
115
- continue;
116
- }
117
-
118
- // Get the API key from the user. [ds]
119
- let apiKey = '';
120
- while (true) {
121
- const promptMsg = provider === 'custom'
122
- ? "Paste your API key (leave blank for local models): "
123
- : "Paste your API key: ";
124
-
125
- if (process.stdin.isTTY) {
126
- rl.close();
127
- apiKey = await askSecret(promptMsg);
128
-
129
- rl = readline.createInterface({
130
- input: process.stdin,
131
- output: process.stdout
132
- });
133
- askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
134
- } else {
135
- apiKey = await askQuestion(promptMsg);
136
- }
137
-
138
- apiKey = apiKey.trim();
139
- if (provider === 'custom' || apiKey) {
140
- break;
141
- }
142
- console.log(`API key is required for provider '${provider}'.`);
143
- }
144
-
145
- // Ask the user if they want to enable auto-pruning of existing human/AI comments. [ds]
146
- let autoPrune = false;
147
- while (true) {
148
- const pruneAns = (await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ")).trim().toLowerCase();
149
- if (pruneAns === '' || pruneAns === 'n' || pruneAns === 'no') {
150
- autoPrune = false;
151
- break;
152
- } else if (pruneAns === 'y' || pruneAns === 'yes') {
153
- autoPrune = true;
154
- break;
155
- }
156
- console.log("Invalid choice. Please enter 'y' or 'n'.");
157
- }
158
-
159
- // Display a summary of the user's configuration settings. [ds]
160
- console.log("\n--- Configuration Summary ---");
161
- console.log(`Provider: ${provider}`);
162
- console.log(`Model: ${model}`);
163
- console.log(`Base URL: ${baseUrl || 'N/A'}`);
164
- console.log(`API Key: ${apiKey ? apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 4)) : 'None'}`);
165
- console.log(`Auto-Prune: ${autoPrune ? 'Yes' : 'No'}`);
166
- console.log("-----------------------------\n");
167
-
168
- // Confirm the configuration settings with the user. [ds]
169
- while (true) {
170
- const confirm = (await askQuestion("Does this look correct? (y/n, default: y): ")).trim().toLowerCase();
171
- if (confirm === '' || confirm === 'y' || confirm === 'yes') {
172
- config = {
173
- provider,
174
- apiKey,
175
- model,
176
- baseUrl,
177
- autoPrune
178
- };
179
- confirmed = true;
180
- break;
181
- } else if (confirm === 'n' || confirm === 'no') {
182
- break;
183
- }
184
- console.log("Invalid choice. Please enter 'y' or 'n'.");
185
- }
186
- }
187
-
188
- rl.close();
189
-
190
- // Write the configuration settings to the config file. [ds]
191
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
192
- try {
193
- if (process.platform !== 'win32') {
194
- fs.chmodSync(configPath, 0o600);
195
- }
196
- } catch (chmodErr) {
197
- }
198
-
199
- return config;
200
- } else {
201
- const rawData = fs.readFileSync(configPath, 'utf8');
202
- return JSON.parse(rawData);
203
- }
204
- }
205
-
206
- // Export the getConfig function for use in other modules. [ds]
207
- module.exports = { getConfig };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "An agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using free LLMs.",
5
5
  "author": "mwahaj36",
6
6
  "license": "MIT",