devsplain 2.3.0 → 2.3.1
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 +2 -2
- package/bin/cli.js +3 -3
- package/lib/config.js +27 -4
- package/lib/llm.js +27 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Unlike interactive AI editors, `devsplain` is designed for batch documentation p
|
|
|
18
18
|
- **Comment Preservation & Tagging**: AI-generated comments are tagged with `[ds]`. Your manually written comments are safe and will never be touched by the engine.
|
|
19
19
|
- **Local Deterministic Scrubber**: The `--clean` flag strips AI-generated `[ds]` comments locally using a deterministic lexical state machine—no LLM calls, API keys, or internet required.
|
|
20
20
|
- **Git Hook Automation**: Supports an automated two-commit Git hook workflow (`pre-commit` for quality, `post-commit` for auto-generated documentation commits) that prevents recursive commit loops.
|
|
21
|
-
- **Bring Your Own LLM**: Native setup wizard for Groq, Gemini, OpenAI, or any OpenAI-compatible API endpoint (like Ollama or LMStudio).
|
|
21
|
+
- **Bring Your Own LLM**: Native setup wizard for Groq, Gemini, OpenAI, Claude, DeepSeek, or any OpenAI-compatible API endpoint (like Ollama or LMStudio).
|
|
22
22
|
- **Exponential Backoff**: Resilient AI request handler that automatically retries rate-limited requests with exponential backoff.
|
|
23
23
|
- **Headless & Override Control**: Configure via environment variables or override global config settings dynamically on the fly with command-line flags.
|
|
24
24
|
|
|
@@ -109,7 +109,7 @@ devsplain <file-or-directory> [options]
|
|
|
109
109
|
| `--force` | Bypasses the safety block check that prevents running `devsplain` on a dirty Git working tree. |
|
|
110
110
|
| `--clean` | Scrubber mode. Deterministically removes only devsplain-generated comments tagged with `[ds]`, preserving your manual comments. |
|
|
111
111
|
| `--prune` | Destructive scrubber mode. Removes ALL comments and docstrings from source files, including your own manual comments. |
|
|
112
|
-
| `--provider <name>`| Temporary one-off override for the AI provider (`gemini`, `groq`, `openai`, `custom`) for this command run only (does not modify the saved config file). |
|
|
112
|
+
| `--provider <name>`| Temporary one-off override for the AI provider (`gemini`, `groq`, `openai`, `claude`, `deepseek`, `custom`) for this command run only (does not modify the saved config file). |
|
|
113
113
|
| `--model <name>` | Temporary one-off override for the model name for this command run only. |
|
|
114
114
|
| `--api-key <key>` | Temporary one-off override for the API key for this command run only. |
|
|
115
115
|
| `--base-url <url>` | Temporary one-off override for the API base URL for this command run only. |
|
package/bin/cli.js
CHANGED
|
@@ -766,7 +766,7 @@ Options:
|
|
|
766
766
|
--force Bypass the dirty Git tree safety check
|
|
767
767
|
--clean Scrub only devsplain-generated [ds] comments
|
|
768
768
|
--prune Destructively scrub ALL comments from files
|
|
769
|
-
--provider <name> Override AI provider (gemini, groq, openai, custom)
|
|
769
|
+
--provider <name> Override AI provider (gemini, groq, openai, claude, deepseek, custom)
|
|
770
770
|
--model <name> Override AI model name
|
|
771
771
|
--api-key <key> Override API key for the provider
|
|
772
772
|
--base-url <url> Override base URL for custom APIs
|
|
@@ -862,10 +862,10 @@ Options:
|
|
|
862
862
|
if (cliProvider) {
|
|
863
863
|
config.provider = cliProvider;
|
|
864
864
|
if (!cliModel) {
|
|
865
|
-
config.model = cliProvider === 'gemini' ? 'gemini-2.0-flash' : (cliProvider === 'claude' ? 'claude-3-5-sonnet-20240620' : 'llama-3.3-70b-versatile');
|
|
865
|
+
config.model = cliProvider === 'gemini' ? 'gemini-2.0-flash' : (cliProvider === 'claude' ? 'claude-3-5-sonnet-20240620' : (cliProvider === 'deepseek' ? 'deepseek-chat' : (cliProvider === 'openai' ? 'gpt-4o' : 'llama-3.3-70b-versatile')));
|
|
866
866
|
}
|
|
867
867
|
if (!cliBaseUrl) {
|
|
868
|
-
config.baseUrl = cliProvider === 'gemini' ? null : (cliProvider === 'groq' ? 'https://api.groq.com/openai' : (cliProvider === 'openai' ? 'https://api.openai.com' : (cliProvider === 'claude' ? 'https://api.anthropic.com' : '')));
|
|
868
|
+
config.baseUrl = cliProvider === 'gemini' ? null : (cliProvider === 'groq' ? 'https://api.groq.com/openai' : (cliProvider === 'openai' ? 'https://api.openai.com' : (cliProvider === 'claude' ? 'https://api.anthropic.com' : (cliProvider === 'deepseek' ? 'https://api.deepseek.com' : ''))));
|
|
869
869
|
}
|
|
870
870
|
}
|
|
871
871
|
if (cliModel) config.model = cliModel;
|
package/lib/config.js
CHANGED
|
@@ -61,8 +61,24 @@ function migrateConfig(oldConfig) {
|
|
|
61
61
|
async function getConfig(forceWizard = false) {
|
|
62
62
|
if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
|
|
63
63
|
const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
|
|
64
|
-
const model = process.env.DEVSPLAIN_MODEL || (
|
|
65
|
-
|
|
64
|
+
const model = process.env.DEVSPLAIN_MODEL || (
|
|
65
|
+
provider === 'gemini' ? 'gemini-2.0-flash' : (
|
|
66
|
+
provider === 'claude' ? 'claude-3-5-sonnet-20240620' : (
|
|
67
|
+
provider === 'deepseek' ? 'deepseek-chat' : (
|
|
68
|
+
provider === 'openai' ? 'gpt-4o' : 'llama-3.3-70b-versatile'
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
const baseUrl = process.env.DEVSPLAIN_BASE_URL || (
|
|
74
|
+
provider === 'gemini' ? null : (
|
|
75
|
+
provider === 'claude' ? 'https://api.anthropic.com' : (
|
|
76
|
+
provider === 'deepseek' ? 'https://api.deepseek.com' : (
|
|
77
|
+
provider === 'openai' ? 'https://api.openai.com' : 'https://api.groq.com/openai'
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
);
|
|
66
82
|
return {
|
|
67
83
|
provider,
|
|
68
84
|
apiKey: process.env.DEVSPLAIN_API_KEY || '',
|
|
@@ -136,8 +152,9 @@ async function getConfig(forceWizard = false) {
|
|
|
136
152
|
console.log("3. OpenAI (Paid)");
|
|
137
153
|
console.log("4. Custom (Ollama, local, etc)");
|
|
138
154
|
console.log("5. Claude (Anthropic)");
|
|
155
|
+
console.log("6. DeepSeek (deepseek-chat, deepseek-reasoner)");
|
|
139
156
|
|
|
140
|
-
const choice = await askQuestion("Select (1-
|
|
157
|
+
const choice = await askQuestion("Select (1-6): ");
|
|
141
158
|
|
|
142
159
|
if (choice === '1') {
|
|
143
160
|
provider = 'groq';
|
|
@@ -175,8 +192,14 @@ async function getConfig(forceWizard = false) {
|
|
|
175
192
|
console.log("\nGet your Anthropic key here: https://console.anthropic.com/settings/keys");
|
|
176
193
|
const customModel = await askQuestion("Model name (press Enter for default 'claude-3-5-sonnet-20240620'): ");
|
|
177
194
|
model = customModel.trim() || 'claude-3-5-sonnet-20240620';
|
|
195
|
+
} else if (choice === '6') {
|
|
196
|
+
provider = 'deepseek';
|
|
197
|
+
baseUrl = 'https://api.deepseek.com';
|
|
198
|
+
console.log("\nGet your DeepSeek key here: https://platform.deepseek.com/api_keys");
|
|
199
|
+
const customModel = await askQuestion("Model name (press Enter for default 'deepseek-chat'): ");
|
|
200
|
+
model = customModel.trim() || 'deepseek-chat';
|
|
178
201
|
} else {
|
|
179
|
-
console.log("Invalid choice. Please select 1, 2, 3, 4, or
|
|
202
|
+
console.log("Invalid choice. Please select 1, 2, 3, 4, 5, or 6.");
|
|
180
203
|
continue;
|
|
181
204
|
}
|
|
182
205
|
} else {
|
package/lib/llm.js
CHANGED
|
@@ -90,38 +90,40 @@ async function runWithConcurrency(items, taskFn) {
|
|
|
90
90
|
|
|
91
91
|
for (let i = 0; i < items.length; i++) {
|
|
92
92
|
const item = items[i];
|
|
93
|
+
|
|
94
|
+
while (executing.size >= _concurrencyLimit) {
|
|
95
|
+
await Promise.race(executing);
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
const task = (async () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// On 429, downshift to serial and retry the item after backoff [ds]
|
|
98
|
-
if (err.isRateLimit) {
|
|
99
|
-
if (!_hitRateLimit) {
|
|
100
|
-
_hitRateLimit = true;
|
|
101
|
-
_concurrencyLimit = 1;
|
|
102
|
-
console.warn(`[devsplain] Rate limit hit — switching to serial mode.`);
|
|
103
|
-
}
|
|
104
|
-
// Wait for all in-flight tasks to settle before retrying [ds]
|
|
105
|
-
await Promise.allSettled([...executing]);
|
|
106
|
-
const backoff = 2000 + Math.random() * 1000;
|
|
107
|
-
console.warn(`[devsplain] Backing off for ${Math.round(backoff)}ms...`);
|
|
108
|
-
await new Promise(r => setTimeout(r, backoff));
|
|
99
|
+
const maxRetries = 3;
|
|
100
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
101
|
+
try {
|
|
109
102
|
return await taskFn(item);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err.isRateLimit && attempt < maxRetries - 1) {
|
|
105
|
+
if (!_hitRateLimit) {
|
|
106
|
+
_hitRateLimit = true;
|
|
107
|
+
_concurrencyLimit = 1;
|
|
108
|
+
console.warn(`[devsplain] Rate limit hit — switching to serial mode.`);
|
|
109
|
+
}
|
|
110
|
+
const backoff = (process.env.NODE_ENV === 'test' ? 50 : 2000) * Math.pow(2, attempt) + Math.random() * 500;
|
|
111
|
+
console.warn(`[devsplain] Backing off for ${Math.round(backoff)}ms...`);
|
|
112
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
110
116
|
}
|
|
111
|
-
throw err;
|
|
112
117
|
}
|
|
113
118
|
})();
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
120
|
+
let tracked;
|
|
121
|
+
tracked = task.finally(() => {
|
|
122
|
+
executing.delete(tracked);
|
|
123
|
+
});
|
|
124
|
+
tracked.catch(() => {});
|
|
119
125
|
executing.add(tracked);
|
|
120
126
|
results.push(tracked);
|
|
121
|
-
|
|
122
|
-
if (executing.size >= _concurrencyLimit) {
|
|
123
|
-
await Promise.race(executing);
|
|
124
|
-
}
|
|
125
127
|
}
|
|
126
128
|
return Promise.all(results);
|
|
127
129
|
}
|
|
@@ -433,7 +435,7 @@ async function getComments(code, language, config, mode = 'default') {
|
|
|
433
435
|
if (lines.length <= CHUNK_THRESHOLD) {
|
|
434
436
|
const numberedCode = lines.map((line, i) => `${i + 1}: ${line}`).join('\n');
|
|
435
437
|
const prompt = buildPrompt(numberedCode, language, mode);
|
|
436
|
-
const textResponse = await
|
|
438
|
+
const [textResponse] = await runWithConcurrency([prompt], p => fetchFromProvider(p, config));
|
|
437
439
|
return parseAndValidate(textResponse, mode);
|
|
438
440
|
}
|
|
439
441
|
|
package/package.json
CHANGED