devsplain 1.1.0 → 1.5.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/llm.js CHANGED
@@ -1,97 +1,222 @@
1
- /**
2
- * Asynchronously retrieves comments for a given piece of code.
3
- *
4
- * @param {string} code The code for which to retrieve comments.
5
- * @param {string} language The programming language of the code.
6
- * @param {object} config An object containing configuration settings for the API request.
7
- * @param {string} [mode='default'] The mode in which to retrieve comments. Can be 'light', 'full', or 'default'.
8
- * @returns {Promise<string>} A promise that resolves to the commented code.
9
- */
10
- async function getComments(code, language, config, mode = 'default') {
11
- // First, we need to determine the instruction based on the mode
12
- 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.";
13
- // If the mode is 'light', we update the instruction accordingly
14
- if (mode === 'light') {
15
- instruction = "Add ONLY JSDoc/docstrings above functions. Do NOT add any inline comments inside the functions. Keep it extremely minimal.";
16
- }
17
- // If the mode is 'full', we update the instruction to include detailed inline comments
18
- else if (mode === 'full') {
19
- instruction = "Add highly detailed JSDoc/docstrings above functions, and add detailed inline comments explaining almost every single line of logic.";
20
- }
21
-
22
- // Now, we create a prompt based on the language and instruction
23
- const prompt = `
24
- // This is a prompt to add comments to the provided code
25
- Add comments to this ${language} code. ${instruction}
26
- // The commented code should be returned without any markdown, explanations, or personal messages
27
- Return ONLY the commented code. NO MARKDOWN. NO EXPLANATION. NO PERSONAL MESSAGE.
28
- ${code}
29
- `;
30
-
31
- // Next, we check the provider in the config to determine which API to use
32
- if (config.provider === 'gemini') {
33
- // If the provider is 'gemini', we use the Google API
34
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
35
- // We make a POST request to the API with the prompt
36
- const response = await fetch(url, {
37
- method: 'POST',
38
- // We set the content type to application/json
39
- headers: { 'Content-Type': 'application/json' },
40
- // We stringified the prompt as JSON
41
- body: JSON.stringify({
42
- "contents": [{ "parts": [{ "text": prompt }] }]
43
- })
44
- });
45
-
46
- // We parse the response as JSON
47
- const data = await response.json();
48
- // For debugging purposes, we log the API response
49
- console.log("DEBUG API RESPONSE:", data);
50
-
51
- // If there's an error in the response, we log the error and exit the process
52
- if (data.error) {
53
- console.error("\n API Error:", data.error.message);
54
- process.exit(1);
55
- }
56
-
57
- // We extract the commented code from the response
58
- let text = data.candidates[0].content.parts[0].text;
59
- // We remove any unnecessary characters from the commented code
60
- text = text.replace(/^```[\w]*\n/m, '').replace(/```$/m, '').trim();
61
- // Finally, we return the commented code
62
- return text;
63
- }
64
- // If the provider is not 'gemini', we use a different API
65
- else {
66
- const url = `${config.baseUrl}/v1/chat/completions`;
67
- // We make a POST request to the API with the prompt
68
- const response = await fetch(url, {
69
- method: 'POST',
70
- // We set the content type to application/json and include the API key in the authorization header
71
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}` },
72
- // We stringified the prompt as JSON
73
- body: JSON.stringify({ "model": config.model, "messages": [{ "role": "user", "content": prompt }] })
74
- });
75
-
76
- // We parse the response as JSON
77
- const data = await response.json();
78
- // For debugging purposes, we log the API response
79
- console.log("DEBUG API RESPONSE:", data);
80
-
81
- // If there's an error in the response, we log the error and exit the process
82
- if (data.error) {
83
- console.error("\n API Error:", data.error.message);
84
- process.exit(1);
85
- }
86
-
87
- // We extract the commented code from the response
88
- let text = data.choices[0].message.content;
89
- // We remove any unnecessary characters from the commented code
90
- text = text.replace(/^```[\w]*\n/m, '').replace(/```$/m, '').trim();
91
- // Finally, we return the commented code
92
- return text;
93
- }
94
- }
95
-
96
- // We export the getComments function
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
+
97
222
  module.exports = { getComments };
package/package.json CHANGED
@@ -1,38 +1,43 @@
1
- {
2
- "name": "devsplain",
3
- "version": "1.1.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
- "bugs": {
33
- "url": "https://github.com/mwahaj36/devsplain/issues" },
34
- "homepage": "https://github.com/mwahaj36/devsplain#readme",
35
- "scripts": {
36
- "test": "echo \"Error: no test specified\" && exit 1"
37
- }
38
- }
1
+ {
2
+ "name": "devsplain",
3
+ "version": "1.5.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
+ }