devsplain 1.2.0 → 1.5.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/lib/llm.js CHANGED
@@ -1,140 +1,222 @@
1
- /**
2
- * This function generates commented code based on the given parameters.
3
- * It sends a request to the specified AI provider with the provided code, language, and configuration.
4
- * The function returns the commented code as a string.
5
- *
6
- * @param {string} code - The code that needs to be commented.
7
- * @param {string} language - The programming language of the code.
8
- * @param {object} config - The configuration object containing the provider and API details.
9
- * @param {string} [mode='default'] - The mode of commenting. Default modes are 'default', 'light', 'full', and 'clean'.
10
- * @returns {Promise<string>} The commented code.
11
- */
12
- async function getComments(code, language, config, mode = 'default') {
13
- // Initialize the instruction based on the mode
14
- let instruction = "Add JSDoc/docstrings block above functions. Do NOT add inline comments unless the logic is extremely complex or highly non-obvious. Keep the code clean and readable.";
15
-
16
- // Check if the mode is 'light' and update the instruction accordingly
17
- if (mode === 'light') {
18
- // In 'light' mode, only JSDoc comments are added above functions
19
- instruction = "Add ONLY JSDoc/docstrings above functions. Do NOT add any inline comments inside the functions. Keep it extremely minimal.";
20
- }
21
- // Check if the mode is 'full' and update the instruction accordingly
22
- else if (mode === 'full') {
23
- // In 'full' mode, detailed JSDoc comments and inline comments are added
24
- instruction = "Add highly detailed JSDoc/docstrings above functions, and add detailed inline comments explaining almost every single line of logic. Do NOT add comments inside string literals, template literals, or multiline strings.";
25
- }
26
- // Check if the mode is 'clean' and update the instruction accordingly
27
- else if (mode === 'clean') {
28
- // In 'clean' mode, all comments are removed from the code
29
- instruction = "Remove ALL comments (both block/JSDoc and inline comments) from this code. Return only the raw, uncommented code. Do NOT alter the code logic or formatting.";
30
- }
31
-
32
- // Initialize the prompt based on the mode
33
- let prompt = "";
34
- // Check if the mode is 'clean' and update the prompt accordingly
35
- if (mode === 'clean') {
36
- // In 'clean' mode, the prompt asks to remove all comments
37
- prompt = `
38
- Remove ALL comments (both block/JSDoc and inline comments) from the following ${language} code.
39
- Return ONLY the raw, uncommented code. NO MARKDOWN. NO EXPLANATION. NO PERSONAL MESSAGE.
40
- Do NOT alter the code logic or formatting in any way.
41
- ${code}
42
- `;
43
- } else {
44
- // In other modes, the prompt asks to add comments to the code
45
- prompt = `
46
- Add comments to the code in this file (${language}). ${instruction}
47
- If the code already contains comments, completely REPLACE them with your own. Do not leave duplicate or messy comments behind.
48
- Return ONLY the commented code. NO MARKDOWN. NO EXPLANATION. NO PERSONAL MESSAGE.
49
- Do NOT alter, refactor, or format the existing code in any way. Only add comments. IF ANYTHING WRONG HIGHLIGHT IN COMMENT BUT DO NOT CHANGE
50
- ${code}
51
- `;
52
- }
53
-
54
- // Check if the provider is 'gemini' and proceed accordingly
55
- if (config.provider === 'gemini') {
56
- // Construct the URL for the API request
57
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
58
- let data;
59
-
60
- try {
61
- // Send a POST request to the API with the prompt
62
- const response = await fetch(url, {
63
- method: 'POST',
64
- headers: {
65
- 'Content-Type': 'application/json'
66
- },
67
- body: JSON.stringify({
68
- "contents": [{ "parts": [{ "text": prompt }] }]
69
- })
70
- });
71
- // Parse the response as JSON
72
- data = await response.json();
73
- } catch (error) {
74
- // Log any network errors and exit the process
75
- console.log("\n Network Error: Could not connect to the AI provider. Check your internet or API url.");
76
- process.exit(1);
77
- }
78
-
79
- // Check if there is an error in the API response
80
- if (data.error) {
81
- // Log the API error and exit the process
82
- console.error("\n API Error:", data.error.message);
83
- process.exit(1);
84
- }
85
-
86
- // Extract the generated text from the API response
87
- let text = data.candidates[0].content.parts[0].text;
88
- // Remove any unnecessary code blocks and trim the text
89
- text = text.replace(/^```[\w]*\n/m, '').replace(/```$/m, '').trim();
90
- // Return the generated text
91
- return text;
92
- }
93
- // If the provider is not 'gemini', proceed with the default API request
94
- else {
95
- // Construct the URL for the API request
96
- const url = `${config.baseUrl}/v1/chat/completions`;
97
- let data;
98
-
99
- try {
100
- // Send a POST request to the API with the prompt
101
- const response = await fetch(url, {
102
- method: 'POST',
103
- headers: {
104
- 'Content-Type': 'application/json',
105
- 'Authorization': `Bearer ${config.apiKey}`
106
- },
107
- body: JSON.stringify({
108
- "model": config.model,
109
- "messages": [{
110
- "role": "user",
111
- "content": prompt
112
- }]
113
- })
114
- });
115
- // Parse the response as JSON
116
- data = await response.json();
117
- } catch (error) {
118
- // Log any network errors and exit the process
119
- console.log("\n Network Error: Could not connect to the AI provider. Check your internet or API url.");
120
- process.exit(1);
121
- }
122
-
123
- // Check if there is an error in the API response
124
- if (data.error) {
125
- // Log the API error and exit the process
126
- console.error("\n API Error:", data.error.message);
127
- process.exit(1);
128
- }
129
-
130
- // Extract the generated text from the API response
131
- let text = data.choices[0].message.content;
132
- // Remove any unnecessary code blocks and trim the text
133
- text = text.replace(/^```[\w]*\n/m, '').replace(/```$/m, '').trim();
134
- // Return the generated text
135
- return text;
136
- }
137
- }
138
-
139
- // Export the getComments function as a module
1
+ /**
2
+ * Fetches a URL with retry logic, timeout handling, and exponential backoff.
3
+ * @param {string} url - The URL to fetch.
4
+ * @param {object} options - Fetch options.
5
+ * @param {number} maxRetries - Maximum number of retries.
6
+ * @param {number} initialDelay - Initial delay in ms for backoff.
7
+ */
8
+ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000) {
9
+ let lastError;
10
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
11
+ // AbortController to handle request timeouts
12
+ const controller = new AbortController();
13
+ const timeoutId = setTimeout(() => controller.abort(), 15000);
14
+ try {
15
+ const response = await fetch(url, {
16
+ ...options,
17
+ signal: controller.signal
18
+ });
19
+ clearTimeout(timeoutId);
20
+ // If response is valid, return immediately
21
+ if (response.ok) {
22
+ return response;
23
+ }
24
+ // Retry on rate limit (429) or server-side errors (500-599)
25
+ if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
26
+ lastError = new Error(`HTTP Error ${response.status}: ${response.statusText}`);
27
+ } else {
28
+ return response;
29
+ }
30
+ } catch (err) {
31
+ clearTimeout(timeoutId);
32
+ // Handle specific timeout case separately
33
+ if (err.name === 'AbortError') {
34
+ lastError = new Error("Request timed out after 15 seconds");
35
+ } else {
36
+ lastError = err;
37
+ }
38
+ }
39
+
40
+ if (attempt < maxRetries - 1) {
41
+ // Calculate exponential backoff delay
42
+ const backoffDelay = initialDelay * Math.pow(2, attempt);
43
+ console.warn(`[devsplain] AI request failed. Retrying in ${backoffDelay}ms... (Attempt ${attempt + 1}/${maxRetries})`);
44
+ await new Promise(resolve => setTimeout(resolve, backoffDelay));
45
+ }
46
+ }
47
+ throw lastError;
48
+ }
49
+
50
+ /**
51
+ * Generates documentation for code by sending it to an LLM provider.
52
+ * @param {string} code - The source code to document.
53
+ * @param {string} language - The programming language.
54
+ * @param {object} config - Provider configuration (apiKey, model, etc.).
55
+ * @param {string} mode - Operation mode (default, clean, light, full).
56
+ */
57
+ async function getComments(code, language, config, mode = 'default') {
58
+ // Split into lines and prepend line numbers for LLM context
59
+ const lines = code.split(/\r?\n/);
60
+ const numberedCode = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
61
+
62
+ let prompt = "";
63
+ if (mode === 'clean') {
64
+ prompt = `
65
+ You are a code documentation scrubber. Analyze the following ${language} code which has line numbers prepended to it.
66
+ Your goal is to identify all lines containing comments (both block/JSDoc and inline comments) and return their line numbers to delete them.
67
+
68
+ CRITICAL RULES:
69
+ 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.
70
+ 2. Each object must have exactly two properties: "line" (the integer line number of the comment line to delete) and "action" (which must be the string "delete").
71
+ 3. Do NOT include the original code in your response.
72
+ 4. If no comments are found, return an empty array: [].
73
+ 5. For block or JSDoc comments (e.g., starting with /* and ending with */), you MUST identify and return the line numbers of ALL lines in that block, including the opening /*, all intermediate lines, and the closing */. Do NOT leave trailing comment delimiters behind.
74
+
75
+ Example Output:
76
+ [
77
+ { "line": 4, "action": "delete" },
78
+ { "line": 5, "action": "delete" }
79
+ ]
80
+
81
+ Here is the source code:
82
+ ${numberedCode}
83
+ `.trim();
84
+ } else {
85
+ let instruction = "Provide JSDoc/docstrings block comments above functions and sparse inline comments for complex logic.";
86
+ if (mode === 'light') {
87
+ instruction = "Provide ONLY JSDoc/docstrings above functions. Keep it minimal.";
88
+ } else if (mode === 'full') {
89
+ instruction = "Provide highly detailed JSDoc/docstrings above functions, and exhaustive step-by-step inline comments (using standard comment syntax like // or #) 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.";
90
+ }
91
+
92
+ prompt = `
93
+ You are a code documentation engine. Analyze the following ${language} code which has line numbers prepended to it.
94
+ ${instruction}
95
+
96
+ CRITICAL RULES:
97
+ 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.
98
+ 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, including standard comment syntax like // or /** */).
99
+ 3. Do NOT include the original code in your response.
100
+ 4. If no comments are needed, return an empty array: [].
101
+
102
+ Example Output:
103
+ [
104
+ { "line": 4, "comment": "/** Calculates the total price */" },
105
+ { "line": 12, "comment": "// Check for null values" }
106
+ ]
107
+
108
+ Here is the source code:
109
+ ${numberedCode}
110
+ `.trim();
111
+ }
112
+
113
+ let textResponse = "";
114
+
115
+ // Branching logic for different LLM providers
116
+ if (config.provider === 'gemini') {
117
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
118
+ let data;
119
+ try {
120
+ const response = await fetchWithRetry(url, {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json'
124
+ },
125
+ body: JSON.stringify({
126
+ "contents": [{ "parts": [{ "text": prompt }] }]
127
+ })
128
+ });
129
+ data = await response.json();
130
+ } catch (error) {
131
+ throw new Error("Network Error: Could not connect to the AI provider. Check your internet or API url.");
132
+ }
133
+ if (data.error) {
134
+ throw new Error(`API Error: ${data.error.message}`);
135
+ }
136
+ textResponse = data.candidates[0].content.parts[0].text;
137
+ }
138
+ else {
139
+ const url = `${config.baseUrl}/v1/chat/completions`;
140
+ let data;
141
+
142
+ try {
143
+ const response = await fetchWithRetry(url, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ 'Authorization': `Bearer ${config.apiKey}`
148
+ },
149
+ body: JSON.stringify({
150
+ "model": config.model,
151
+ "messages": [{
152
+ "role": "user",
153
+ "content": prompt
154
+ }]
155
+ })
156
+ });
157
+ data = await response.json();
158
+ } catch (error) {
159
+ throw new Error("Network Error: Could not connect to the AI provider. Check your internet or API url.");
160
+ }
161
+ if (data.error) {
162
+ throw new Error(`API Error: ${data.error.message}`);
163
+ }
164
+ textResponse = data.choices[0].message.content;
165
+ }
166
+
167
+ // Extract JSON array from LLM response text
168
+ let cleanText = textResponse.trim();
169
+ const start = cleanText.indexOf('[');
170
+ const end = cleanText.lastIndexOf(']');
171
+ if (start !== -1 && end !== -1 && end >= start) {
172
+ cleanText = cleanText.substring(start, end + 1);
173
+ }
174
+
175
+ let parsed;
176
+ // Validate response format and schema integrity
177
+ try {
178
+ parsed = JSON.parse(cleanText);
179
+ } catch (e) {
180
+ throw new Error(`Parsing Error: Failed to parse LLM response as JSON. Raw response was:\n${textResponse}`);
181
+ }
182
+
183
+ if (!Array.isArray(parsed)) {
184
+ throw new Error("Schema Error: LLM response is not a JSON array.");
185
+ }
186
+
187
+ for (const item of parsed) {
188
+ if (typeof item !== 'object' || item === null) {
189
+ throw new Error("Schema Error: Array elements must be objects.");
190
+ }
191
+ if (!Number.isInteger(item.line) || item.line <= 0) {
192
+ throw new Error("Schema Error: 'line' must be a positive integer.");
193
+ }
194
+
195
+ if (mode === 'clean') {
196
+ if (item.action !== 'delete') {
197
+ throw new Error("Schema Error: 'action' must be 'delete' in clean mode.");
198
+ }
199
+ } else {
200
+ if (typeof item.comment !== 'string') {
201
+ throw new Error("Schema Error: 'comment' must be a string.");
202
+ }
203
+
204
+ const trimmedComment = item.comment.trim();
205
+ // Sanity check for valid comment syntax
206
+ const startsWithCommentMarker =
207
+ trimmedComment.startsWith('//') ||
208
+ trimmedComment.startsWith('/*') ||
209
+ trimmedComment.startsWith('#') ||
210
+ trimmedComment.startsWith('<!--') ||
211
+ trimmedComment.startsWith('--');
212
+
213
+ if (!startsWithCommentMarker) {
214
+ throw new Error(`Security Error: Comment on line ${item.line} does not start with a valid comment character sequence. Rejected: ${trimmedComment}`);
215
+ }
216
+ }
217
+ }
218
+
219
+ return parsed;
220
+ }
221
+
140
222
  module.exports = { getComments };
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "devsplain",
3
- "version": "1.2.0",
4
- "description": "An agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using free LLMs.",
5
- "author": "mwahaj36",
6
- "license": "MIT",
7
- "main": "bin/cli.js",
8
- "type": "commonjs",
9
- "bin": {
10
- "devsplain": "./bin/cli.js"
11
- },
12
- "files": [
13
- "bin",
14
- "lib"
15
- ],
16
- "engines": {
17
- "node": ">=18.0.0"
18
- },
19
- "keywords": [
20
- "cli",
21
- "ai",
22
- "comments",
23
- "jsdoc",
24
- "documentation",
25
- "gemini",
26
- "groq",
27
- "llm"
28
- ],
29
- "repository": {
30
- "type": "git",
31
- "url": "git+https://github.com/mwahaj36/devsplain.git"
32
- },
33
- "bugs": {
34
- "url": "https://github.com/mwahaj36/devsplain/issues"
35
- },
36
- "homepage": "https://github.com/mwahaj36/devsplain#readme",
37
- "scripts": {
38
- "test": "jest"
39
- },
40
- "devDependencies": {
41
- "jest": "^30.4.2"
42
- }
43
- }
1
+ {
2
+ "name": "devsplain",
3
+ "version": "1.5.1",
4
+ "description": "An agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using free LLMs.",
5
+ "author": "mwahaj36",
6
+ "license": "MIT",
7
+ "main": "bin/cli.js",
8
+ "type": "commonjs",
9
+ "bin": {
10
+ "devsplain": "bin/cli.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "lib"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "keywords": [
20
+ "cli",
21
+ "ai",
22
+ "comments",
23
+ "jsdoc",
24
+ "documentation",
25
+ "gemini",
26
+ "groq",
27
+ "llm"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/mwahaj36/devsplain.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/mwahaj36/devsplain/issues"
35
+ },
36
+ "homepage": "https://github.com/mwahaj36/devsplain#readme",
37
+ "scripts": {
38
+ "test": "jest"
39
+ },
40
+ "devDependencies": {
41
+ "jest": "^30.4.2"
42
+ }
43
+ }