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/LICENSE +21 -0
- package/README.md +124 -27
- package/bin/cli.js +591 -76
- package/bin/post-commit.js +96 -0
- package/bin/setup-hook.js +79 -0
- package/lib/config.js +124 -69
- package/lib/llm.js +221 -96
- package/package.json +43 -38
package/lib/llm.js
CHANGED
|
@@ -1,97 +1,222 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @param {
|
|
5
|
-
* @param {
|
|
6
|
-
* @param {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|