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 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 || (provider === 'gemini' ? 'gemini-2.0-flash' : (provider === 'claude' ? 'claude-3-5-sonnet-20240620' : 'llama-3.3-70b-versatile'));
65
- const baseUrl = process.env.DEVSPLAIN_BASE_URL || (provider === 'gemini' ? null : (provider === 'claude' ? 'https://api.anthropic.com' : 'https://api.groq.com/openai'));
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-5): ");
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 5.");
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
- try {
95
- return await taskFn(item);
96
- } catch (err) {
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
- const tracked = task.then(
116
- val => { executing.delete(tracked); return val; },
117
- err => { executing.delete(tracked); throw err; }
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 fetchFromProvider(prompt, config);
438
+ const [textResponse] = await runWithConcurrency([prompt], p => fetchFromProvider(p, config));
437
439
  return parseAndValidate(textResponse, mode);
438
440
  }
439
441
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
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",