devsplain 1.0.0 → 1.1.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/README.md +58 -0
- package/bin/cli.js +45 -27
- package/lib/config.js +69 -35
- package/lib/llm.js +97 -53
- package/package.json +4 -6
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# devsplain
|
|
2
|
+
|
|
3
|
+
An agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using free LLMs.
|
|
4
|
+
|
|
5
|
+
Tired of writing documentation? Let AI do the explaining for you. `devsplain` reads your code and intelligently injects standard JSDoc headers and inline comments.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install it globally via npm:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g devsplain
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Simply point `devsplain` at any file you want to comment:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
devsplain src/utils.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Modes
|
|
24
|
+
|
|
25
|
+
You can control exactly how aggressive the AI is with its comments using flags:
|
|
26
|
+
|
|
27
|
+
- `--light`: Only adds JSDoc blocks above functions. Keeps the inside of your functions completely untouched.
|
|
28
|
+
- `--full`: Highly aggressive. Explains complex logic line-by-line inside your functions.
|
|
29
|
+
- **(Default)**: A balanced mix of JSDoc headers and sparse inline comments for complex logic.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
devsplain src/utils.js --light
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Samples in this Repository
|
|
38
|
+
|
|
39
|
+
To see exactly what `devsplain` can do, the source code of this very repository was commented using the tool itself (using the Groq provider):
|
|
40
|
+
|
|
41
|
+
- `bin/cli.js`: Commented using the **Default** mode.
|
|
42
|
+
- `lib/config.js`: Commented using the **--light** mode.
|
|
43
|
+
- `lib/llm.js`: Commented using the **--full** mode.
|
|
44
|
+
|
|
45
|
+
## Agent Agnostic (Bring Your Own LLM)
|
|
46
|
+
|
|
47
|
+
`devsplain` doesn't lock you into one ecosystem. On your first run, an interactive Setup Wizard will ask you which engine you want to use:
|
|
48
|
+
|
|
49
|
+
1. **Groq** (Recommended) - Instant, free Llama-3 endpoints.
|
|
50
|
+
2. **Gemini** - Google's free tier endpoints.
|
|
51
|
+
3. **OpenAI** - Standard paid ChatGPT models.
|
|
52
|
+
4. **Custom** - Point it at ANY OpenAI-compatible endpoint (e.g., local models via Ollama or LMStudio).
|
|
53
|
+
|
|
54
|
+
_(Your configuration is safely stored in `~/.devsplainrc` on your machine)._
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
package/bin/cli.js
CHANGED
|
@@ -1,59 +1,77 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
* @
|
|
5
|
-
* @
|
|
6
|
-
* @
|
|
4
|
+
* Import required modules.
|
|
5
|
+
* @module llm - Provides functionality for getting comments.
|
|
6
|
+
* @module config - Provides functionality for getting configuration.
|
|
7
|
+
* @module fs - Provides functionality for interacting with the file system.
|
|
7
8
|
*/
|
|
8
|
-
const { getComments } = require('../lib/llm.js')
|
|
9
|
-
const { getConfig } = require('../lib/config.js')
|
|
9
|
+
const { getComments } = require('../lib/llm.js');
|
|
10
|
+
const { getConfig } = require('../lib/config.js');
|
|
10
11
|
const fs = require('fs');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Get the filepath from the command line arguments.
|
|
14
15
|
* @type {string}
|
|
15
16
|
*/
|
|
16
|
-
const filepath = process.argv[2]
|
|
17
|
+
const filepath = process.argv[2];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get additional command line arguments.
|
|
21
|
+
* @type {array}
|
|
22
|
+
*/
|
|
23
|
+
const args = process.argv.slice(3);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set the default mode.
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
let mode = 'default';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check for mode flags in the command line arguments and update the mode accordingly.
|
|
33
|
+
*/
|
|
34
|
+
if (args.includes('--light')) mode = 'light';
|
|
35
|
+
if (args.includes('--full')) mode = 'full';
|
|
17
36
|
|
|
18
37
|
/**
|
|
19
|
-
*
|
|
20
|
-
* If not, it logs the usage message and exits the process.
|
|
38
|
+
* Check if a filepath was provided.
|
|
21
39
|
*/
|
|
22
40
|
if (!filepath) {
|
|
23
|
-
console.log("usage:
|
|
24
|
-
process.exit(1)
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
41
|
+
console.log("usage: devsplain <file>");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
} else {
|
|
27
44
|
/**
|
|
28
|
-
*
|
|
45
|
+
* Main execution block.
|
|
29
46
|
* @async
|
|
30
47
|
*/
|
|
31
48
|
(async () => {
|
|
32
49
|
/**
|
|
33
|
-
*
|
|
50
|
+
* Get the configuration.
|
|
34
51
|
* @type {object}
|
|
35
52
|
*/
|
|
36
53
|
const config = await getConfig();
|
|
37
|
-
|
|
54
|
+
|
|
38
55
|
/**
|
|
39
|
-
*
|
|
56
|
+
* Read the file at the specified filepath.
|
|
40
57
|
* @type {string}
|
|
41
58
|
*/
|
|
42
59
|
const data = fs.readFileSync(filepath, 'utf-8');
|
|
43
|
-
|
|
44
|
-
// Log a message to indicate that the file is being analyzed
|
|
60
|
+
|
|
45
61
|
console.log("Analyzing File...");
|
|
46
|
-
|
|
62
|
+
console.log(`Analyzing File in ${mode} mode...`);
|
|
63
|
+
|
|
47
64
|
/**
|
|
48
|
-
*
|
|
65
|
+
* Get comments for the code in the file.
|
|
49
66
|
* @type {string}
|
|
50
67
|
*/
|
|
51
|
-
const commentedCode = await getComments(data, 'javascript', config)
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
const commentedCode = await getComments(data, 'javascript', config, mode);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Write the commented code back to the file.
|
|
72
|
+
*/
|
|
54
73
|
fs.writeFileSync(filepath, commentedCode);
|
|
55
|
-
|
|
56
|
-
// Log a success message with the file path
|
|
74
|
+
|
|
57
75
|
console.log(`Successfully commented ${filepath}`);
|
|
58
76
|
})();
|
|
59
77
|
}
|
package/lib/config.js
CHANGED
|
@@ -1,36 +1,70 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const readline = require('readline');
|
|
5
|
-
const configPath = path.join(os.homedir(), '.
|
|
6
|
-
|
|
7
|
-
const rl=readline.createInterface({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const askQuestion=(query)=>new Promise((resolve)=>rl.question(query,resolve))
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const configPath = path.join(os.homedir(), '.devsplainrc');
|
|
6
|
+
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout
|
|
10
|
+
});
|
|
11
|
+
const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve))
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Retrieves the configuration for the application.
|
|
15
|
+
* If the configuration file does not exist, it will prompt the user to create one.
|
|
16
|
+
* @returns {Promise<Object>} The configuration object.
|
|
17
|
+
*/
|
|
18
|
+
async function getConfig() {
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
let baseUrl = "";
|
|
21
|
+
let model = "";
|
|
22
|
+
let provider = "";
|
|
23
|
+
console.log("Which AI Provider Do You want to use?");
|
|
24
|
+
console.log("1. Groq (Free, Fast, Llama-3)");
|
|
25
|
+
console.log("2. Gemini (Free Tier)");
|
|
26
|
+
console.log("3. OpenAI (Paid)");
|
|
27
|
+
console.log("4. Custom (Ollama, local, etc)");
|
|
28
|
+
const choice = await askQuestion("Select (1-4): ");
|
|
29
|
+
if (choice === '1') {
|
|
30
|
+
provider = 'groq',
|
|
31
|
+
model = 'llama-3.3-70b-versatile',
|
|
32
|
+
baseUrl = 'https://api.groq.com/openai',
|
|
33
|
+
console.log("\nGet your free Groq key here: https://console.groq.com/keys")
|
|
34
|
+
}
|
|
35
|
+
else if (choice === '2') {
|
|
36
|
+
provider = 'gemini';
|
|
37
|
+
model = 'gemini-2.0-flash';
|
|
38
|
+
baseUrl = null;
|
|
39
|
+
console.log("\nGet your free Gemini key here: https://aistudio.google.com/apikey")
|
|
40
|
+
}
|
|
41
|
+
else if (choice === '3') {
|
|
42
|
+
provider = 'openai';
|
|
43
|
+
model = 'gpt-4o';
|
|
44
|
+
baseUrl = 'https://api.openai.com';
|
|
45
|
+
console.log("\nGet your OpenAI key here: https://platform.openai.com/api-keys")
|
|
46
|
+
}
|
|
47
|
+
else if (choice === '4') {
|
|
48
|
+
provider = 'custom';
|
|
49
|
+
model = await askQuestion("Model name (e.g., llama3): ");
|
|
50
|
+
baseUrl = await askQuestion("Base URL (e.g., http://localhost:11434): ");
|
|
51
|
+
}
|
|
52
|
+
const apiKey = await askQuestion("Paste your API key (leave blank for local models): ");
|
|
53
|
+
rl.close()
|
|
54
|
+
const config = {
|
|
55
|
+
provider: provider,
|
|
56
|
+
apiKey: apiKey,
|
|
57
|
+
model: model,
|
|
58
|
+
baseUrl: baseUrl
|
|
59
|
+
};
|
|
60
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
61
|
+
return config
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
rl.close();
|
|
65
|
+
const rawData = fs.readFileSync(configPath, 'utf8');
|
|
66
|
+
return JSON.parse(rawData);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
36
70
|
module.exports = { getConfig };
|
package/lib/llm.js
CHANGED
|
@@ -1,53 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
97
|
+
module.exports = { getComments };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devsplain",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -28,12 +28,10 @@
|
|
|
28
28
|
],
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/mwahaj36/
|
|
32
|
-
},
|
|
31
|
+
"url": "git+https://github.com/mwahaj36/devsplain.git"},
|
|
33
32
|
"bugs": {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"homepage": "https://github.com/mwahaj36/commie#readme",
|
|
33
|
+
"url": "https://github.com/mwahaj36/devsplain/issues" },
|
|
34
|
+
"homepage": "https://github.com/mwahaj36/devsplain#readme",
|
|
37
35
|
"scripts": {
|
|
38
36
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
39
37
|
}
|