devsplain 1.7.0 → 1.8.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 CHANGED
@@ -1,7 +1,10 @@
1
1
  # devsplain
2
+ An agent-agnostic CLI tool that adds JSDoc and inline comments using state-of-the-art LLMs while preserving non-comment source lines byte-for-byte through deterministic verification.
2
3
 
3
- An industrial-grade, agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using state-of-the-art LLMs, while guaranteeing code integrity with deterministic safety constraints.
4
+ ![devsplain demo](sample.gif)
4
5
 
6
+ devsplain never rewrites executable code.
7
+ If the original source cannot be reproduced exactly after comment insertion, the operation aborts.
5
8
  ---
6
9
 
7
10
  ## Key Features
@@ -35,6 +38,11 @@ Many AI code formatters rewrite your code entirely, exposing you to logic regres
35
38
  ### String Literal Guardrails
36
39
  The engine tracks lexical state across template strings, single quotes, double quotes, and multi-line docstrings (such as Python triple-quotes). Comment insertion is blocked if the target line resides within a string literal, preventing broken syntax.
37
40
 
41
+ ### The `[ds]` AI Tag Guarantee
42
+ Every single comment generated by the LLM is forcibly prefixed with a `[ds]` tag (e.g., `// [ds] This function handles...`).
43
+ This guarantees that the local `devsplain` lexer can mathematically differentiate between your human-written manual comments and the AI-generated comments.
44
+ When you run the `--clean` command, the lexer looks specifically for the `[ds]` prefix and surgically removes only the AI-generated comments, safely preserving 100% of your manual documentation.
45
+
38
46
  ### Why Not AST Verification?
39
47
 
40
48
  AST verification would require language-specific parser dependencies for every supported language.
@@ -75,6 +83,9 @@ devsplain --config
75
83
 
76
84
  Your settings are stored securely in `~/.devsplainrc` (configured with `chmod 600` on POSIX systems to restrict read access).
77
85
 
86
+ > [!NOTE]
87
+ > **API Testing Notice:** `devsplain` supports OpenAI, Anthropic, Ollama, Groq, and Gemini endpoints, but the E2E test suite has currently only been aggressively verified against the **Groq** and **Gemini** APIs. If you encounter any unexpected parsing issues or edge-case errors with other providers, please open an issue or submit a PR!
88
+
78
89
  ---
79
90
 
80
91
  ## CLI Usage & Options
package/bin/cli.js CHANGED
@@ -566,10 +566,10 @@ Options:
566
566
  if (cliProvider) {
567
567
  config.provider = cliProvider;
568
568
  if (!cliModel) {
569
- config.model = cliProvider === 'gemini' ? 'gemini-2.0-flash' : 'llama-3.3-70b-versatile';
569
+ config.model = cliProvider === 'gemini' ? 'gemini-2.0-flash' : (cliProvider === 'claude' ? 'claude-3-5-sonnet-20240620' : 'llama-3.3-70b-versatile');
570
570
  }
571
571
  if (!cliBaseUrl) {
572
- config.baseUrl = cliProvider === 'gemini' ? null : (cliProvider === 'groq' ? 'https://api.groq.com/openai' : (cliProvider === 'openai' ? 'https://api.openai.com' : ''));
572
+ config.baseUrl = cliProvider === 'gemini' ? null : (cliProvider === 'groq' ? 'https://api.groq.com/openai' : (cliProvider === 'openai' ? 'https://api.openai.com' : (cliProvider === 'claude' ? 'https://api.anthropic.com' : '')));
573
573
  }
574
574
  }
575
575
  if (cliModel) config.model = cliModel;
@@ -3,51 +3,69 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { spliceComments } = require('./cli');
5
5
 
6
- /** Attempts to execute the post-commit hook [ds] */
6
+ /** Main execution block [ds] */
7
7
  try {
8
- // Get the last commit message [ds]
8
+ // Allow users to completely bypass the AI hook using an environment variable [ds]
9
+ if (process.env.SKIP_DEVSPLAIN) {
10
+ console.log('[devsplain] SKIP_DEVSPLAIN is set. Bypassing AI generation.');
11
+ process.exit(0);
12
+ }
13
+
14
+ // Prevent the hook from firing during rebases, merges, or cherry-picks [ds]
15
+ const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
16
+ const isRebasing = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
17
+ const isMerging = fs.existsSync(path.join(gitDir, 'MERGE_HEAD'));
18
+ const isCherryPicking = fs.existsSync(path.join(gitDir, 'CHERRY_PICK_HEAD'));
19
+
20
+ if (isRebasing || isMerging || isCherryPicking) {
21
+ console.log('[devsplain] Skipping AI comment generation during git rebase/merge/cherry-pick to avoid history conflicts.');
22
+ process.exit(0);
23
+ }
24
+
25
+ /** Check if the current operation is a rebase, merge or cherry-pick [ds] */
9
26
  const lastCommitMsg = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim();
10
27
  if (lastCommitMsg === 'docs: auto-generated comments by devsplain') {
11
28
  process.exit(0);
12
29
  }
13
30
 
14
- // Get the list of changed files in the last commit [ds]
31
+ // Get the last commit message [ds]
15
32
  const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { encoding: 'utf8' }).trim();
16
33
  if (!changedFilesStr) {
17
34
  process.exit(0);
18
35
  }
19
36
  const changedFiles = changedFilesStr.split(/\r?\n/);
20
37
 
21
- /** Defines a list of valid file extensions for commenting [ds] */
38
+ /** List of valid file extensions for commenting [ds] */
22
39
  const validExtensions = [
23
40
  '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
24
41
  '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
25
42
  '.swift', '.kt', '.dart', '.sh'
26
43
  ];
27
44
 
28
- /** Filters the changed files based on valid extensions and existence [ds] */
45
+ /** Filter files to be commented [ds] */
29
46
  const filesToComment = changedFiles.filter(file => {
30
47
  const ext = path.extname(file).toLowerCase();
31
48
  const isIgnored = file.includes('node_modules/') || file.includes('tests/') || file.includes('__tests__/') || file.includes('fixtures/');
32
49
  return validExtensions.includes(ext) && fs.existsSync(file) && !isIgnored;
33
50
  });
34
51
 
52
+ // Check if there are any files to comment [ds]
35
53
  if (filesToComment.length === 0) {
36
54
  process.exit(0);
37
55
  }
38
56
 
39
- // Log the number of files to be commented [ds]
40
57
  console.log(`[devsplain] Found ${filesToComment.length} file(s) in the last commit to auto-comment.`);
41
58
 
42
- // Parse command-line arguments for commenting mode [ds]
59
+ // Parse command line arguments for mode flag [ds]
43
60
  const args = process.argv.slice(2);
44
61
  let modeFlag = '';
45
62
  if (args.includes('--light')) modeFlag = ' --light';
46
63
  if (args.includes('--full')) modeFlag = ' --full';
47
64
 
48
65
  let commentedAny = false;
66
+ const successfullyCommentedFiles = [];
49
67
 
50
- /** Iterates through the files to be commented and attempts to comment each one [ds] */
68
+ /** Iterate over files to comment [ds] */
51
69
  for (const file of filesToComment) {
52
70
  try {
53
71
  const ext = path.extname(file).toLowerCase();
@@ -61,7 +79,7 @@ try {
61
79
  } catch (prevErr) {
62
80
  }
63
81
 
64
- // Check if the file has been modified beyond just comments [ds]
82
+ // Compare file content before and after the last commit [ds]
65
83
  if (contentPrev) {
66
84
  const cleanHead = spliceComments(contentHead, [], 'prune', ext);
67
85
  const cleanPrev = spliceComments(contentPrev, [], 'prune', ext);
@@ -73,26 +91,36 @@ try {
73
91
  } catch (cleanErr) {
74
92
  }
75
93
 
76
- // Attempt to comment the file using the cli script [ds]
94
+ // Comment the file using the CLI [ds]
77
95
  console.log(`[devsplain] Automatically commenting file: ${file}`);
78
96
  try {
79
97
  const cliPath = path.join(__dirname, 'cli.js');
80
98
  execSync(`node "${cliPath}" "${file}" --force${modeFlag}`, { stdio: 'inherit' });
81
99
  commentedAny = true;
100
+ successfullyCommentedFiles.push(file);
82
101
  } catch (err) {
83
102
  console.warn(`[devsplain] Warning: Failed to comment ${file}: ${err.message}`);
84
103
  }
85
104
  }
86
105
 
87
- /** If any files were commented, stage and commit the changes [ds] */
106
+ /** Stage and commit auto-generated comments if any [ds] */
88
107
  if (commentedAny) {
89
- const status = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
90
- if (status.length > 0) {
108
+ // Only stage the exact files that the AI touched to avoid accidentally committing unstaged work [ds]
109
+ for (const file of successfullyCommentedFiles) {
110
+ try {
111
+ execSync(`git add "${file}"`);
112
+ } catch (addErr) {}
113
+ }
114
+
115
+ // Check if there are actually staged changes now [ds]
116
+ const stagedChanges = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
117
+ if (stagedChanges.length > 0) {
91
118
  console.log('[devsplain] Staging and committing auto-generated comments...');
92
- execSync('git commit -am "docs: auto-generated comments by devsplain" --no-verify', { stdio: 'inherit' });
119
+ execSync('git commit -m "docs: auto-generated comments by devsplain" --no-verify', { stdio: 'inherit' });
93
120
  console.log('[devsplain] Comments committed successfully! Rollback via: git reset --hard HEAD~1');
94
121
  }
95
122
  }
123
+ /** Catch and log any errors [ds] */
96
124
  } catch (e) {
97
125
  console.warn(`[devsplain] Warning: post-commit hook script failed: ${e.message}`);
98
126
  }
package/lib/config.js CHANGED
@@ -8,8 +8,8 @@ const configPath = path.join(os.homedir(), '.devsplainrc');
8
8
  async function getConfig(forceWizard = false) {
9
9
  if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
10
10
  const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
11
- const model = process.env.DEVSPLAIN_MODEL || (provider === 'gemini' ? 'gemini-2.0-flash' : 'llama-3.3-70b-versatile');
12
- const baseUrl = process.env.DEVSPLAIN_BASE_URL || (provider === 'gemini' ? null : 'https://api.groq.com/openai');
11
+ const model = process.env.DEVSPLAIN_MODEL || (provider === 'gemini' ? 'gemini-2.0-flash' : (provider === 'claude' ? 'claude-3-5-sonnet-20240620' : 'llama-3.3-70b-versatile'));
12
+ const baseUrl = process.env.DEVSPLAIN_BASE_URL || (provider === 'gemini' ? null : (provider === 'claude' ? 'https://api.anthropic.com' : 'https://api.groq.com/openai'));
13
13
  return {
14
14
  provider,
15
15
  apiKey: process.env.DEVSPLAIN_API_KEY || '',
@@ -41,9 +41,10 @@ async function getConfig(forceWizard = false) {
41
41
  console.log("2. Gemini (Free Tier)");
42
42
  console.log("3. OpenAI (Paid)");
43
43
  console.log("4. Custom (Ollama, local, etc)");
44
+ console.log("5. Claude (Anthropic)");
44
45
 
45
46
  // Get user's selected AI provider option [ds]
46
- const choice = await askQuestion("Select (1-4): ");
47
+ const choice = await askQuestion("Select (1-5): ");
47
48
 
48
49
  // Handle selected AI provider option [ds]
49
50
  if (choice === '1') {
@@ -68,8 +69,14 @@ async function getConfig(forceWizard = false) {
68
69
  provider = 'custom';
69
70
  model = await askQuestion("Model name (e.g., llama3): ");
70
71
  baseUrl = await askQuestion("Base URL (e.g., http://localhost:11434): ");
72
+ } else if (choice === '5') {
73
+ provider = 'claude';
74
+ baseUrl = 'https://api.anthropic.com';
75
+ console.log("\nGet your Anthropic key here: https://console.anthropic.com/settings/keys");
76
+ const customModel = await askQuestion("Model name (press Enter for default 'claude-3-5-sonnet-20240620'): ");
77
+ model = customModel.trim() || 'claude-3-5-sonnet-20240620';
71
78
  } else {
72
- console.log("Invalid choice. Please select 1, 2, 3, or 4.");
79
+ console.log("Invalid choice. Please select 1, 2, 3, 4, or 5.");
73
80
  continue;
74
81
  }
75
82
 
package/lib/llm.js CHANGED
@@ -117,8 +117,36 @@ ${numberedCode}
117
117
  throw new Error(`API Error: ${data.error.message}`);
118
118
  }
119
119
  textResponse = data.candidates[0].content.parts[0].text;
120
+ } else if (config.provider === 'claude') {
121
+ const url = `${config.baseUrl}/v1/messages`;
122
+ let data;
123
+ try {
124
+ const response = await fetchWithRetry(url, {
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': 'application/json',
128
+ 'x-api-key': config.apiKey,
129
+ 'anthropic-version': '2023-06-01'
130
+ },
131
+ body: JSON.stringify({
132
+ "model": config.model,
133
+ "max_tokens": 8192,
134
+ "messages": [{
135
+ "role": "user",
136
+ "content": prompt
137
+ }]
138
+ })
139
+ });
140
+ data = await response.json();
141
+ } catch (error) {
142
+ throw new Error("Network Error: Could not connect to the AI provider. Check your internet or API url.");
143
+ }
144
+ if (data.error) {
145
+ throw new Error(`API Error: ${data.error.message}`);
146
+ }
147
+ textResponse = data.content[0].text;
120
148
  }
121
- // Otherwise, use a different provider [ds]
149
+ // Otherwise, use an OpenAI-compatible provider [ds]
122
150
  else {
123
151
  const url = `${config.baseUrl}/v1/chat/completions`;
124
152
  let data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",