devsplain 2.0.1 → 2.1.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/bin/cli.js CHANGED
@@ -10,7 +10,7 @@ const { execSync } = require('child_process');
10
10
  let rl;
11
11
  let askQuestion;
12
12
 
13
- /** Checks if the Git repository is dirty [ds] */
13
+ // Checks if the Git working tree is dirty [ds]
14
14
  function isGitDirty() {
15
15
  try {
16
16
  const gitDir = execSync('git rev-parse --is-inside-work-tree', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
@@ -23,10 +23,13 @@ function isGitDirty() {
23
23
  return false;
24
24
  }
25
25
 
26
- /** Checks if a line of code is inside a string [ds] */
26
+ // Checks if a line of code is inside a string literal [ds]
27
27
  function isLineInsideString(lines, targetLineIndex, ext = '') {
28
28
  const isPython = ext.toLowerCase() === '.py';
29
29
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
30
+ const isRustOrSwift = ['.rs', '.swift'].includes(ext.toLowerCase());
31
+ const isCpp = ['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp'].includes(ext.toLowerCase());
32
+ const isJS = ['.js', '.jsx', '.ts', '.tsx'].includes(ext.toLowerCase());
30
33
  let inBacktick = false;
31
34
  let inTripleDouble = false;
32
35
  let inTripleSingle = false;
@@ -34,19 +37,55 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
34
37
  let inDouble = false;
35
38
  let inBlockJS = false;
36
39
  let inBlockHTML = false;
40
+ let blockDepthJS = 0;
41
+ let inCppRawString = false;
42
+ let cppRawDelimiter = '';
43
+ let inRegex = false;
37
44
  for (let i = 0; i < targetLineIndex; i++) {
38
45
  const line = lines[i];
39
46
  let j = 0;
40
47
  while (j < line.length) {
41
48
  if (inBlockJS) {
49
+ if (line.slice(j, j + 2) === '/*') {
50
+ if (isRustOrSwift) blockDepthJS++;
51
+ j += 2;
52
+ continue;
53
+ }
42
54
  if (line.slice(j, j + 2) === '*/') {
43
- inBlockJS = false;
55
+ if (isRustOrSwift && blockDepthJS > 1) {
56
+ blockDepthJS--;
57
+ } else {
58
+ inBlockJS = false;
59
+ blockDepthJS = 0;
60
+ }
44
61
  j += 2;
45
62
  continue;
46
63
  }
47
64
  j++;
48
65
  continue;
49
66
  }
67
+ if (inCppRawString) {
68
+ if (line.slice(j, j + 2 + cppRawDelimiter.length) === ')' + cppRawDelimiter + '"') {
69
+ inCppRawString = false;
70
+ j += 2 + cppRawDelimiter.length;
71
+ continue;
72
+ }
73
+ j++;
74
+ continue;
75
+ }
76
+ if (inRegex) {
77
+ let escaped = false;
78
+ let k = j - 1;
79
+ while (k >= 0 && line[k] === '\\') {
80
+ escaped = !escaped;
81
+ k--;
82
+ }
83
+ if (line[j] === '/' && !escaped) {
84
+ inRegex = false;
85
+ }
86
+ j++;
87
+ continue;
88
+ }
50
89
  if (inBlockHTML) {
51
90
  if (line.slice(j, j + 3) === '-->') {
52
91
  inBlockHTML = false;
@@ -65,7 +104,6 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
65
104
  } else if (isHTML) {
66
105
  if (line.slice(j, j + 4) === '<!--') {
67
106
  inBlockHTML = true;
68
- // Check if current character is a backtick [ds]
69
107
  j += 4;
70
108
  continue;
71
109
  }
@@ -78,17 +116,56 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
78
116
  break; // Ignore rest of line
79
117
  }
80
118
  } else {
81
- if (line.slice(j, j + 2) === '//') {
82
- break; // Ignore rest of line
83
- }
84
- if (line.slice(j, j + 2) === '/*') {
85
- inBlockJS = true;
86
- j += 2;
87
- continue;
88
- }
89
- const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
90
- if (isShellOrRuby && line[j] === '#') {
91
- break; // Ignore rest of line
119
+ const isShellOrRuby = ['.sh', '.rb'].includes(ext.toLowerCase());
120
+ if (isShellOrRuby) {
121
+ if (line[j] === '#') {
122
+ break; // Ignore rest of line
123
+ }
124
+ } else {
125
+ if (line.slice(j, j + 2) === '//') {
126
+ break; // Ignore rest of line
127
+ }
128
+ if (line.slice(j, j + 2) === '/*') {
129
+ inBlockJS = true;
130
+ blockDepthJS = 1;
131
+ j += 2;
132
+ continue;
133
+ }
134
+ if (ext.toLowerCase() === '.php' && line[j] === '#') {
135
+ break; // Ignore rest of line
136
+ }
137
+ if (isCpp && line[j] === 'R' && line[j+1] === '"') {
138
+ const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
139
+ if (match) {
140
+ cppRawDelimiter = match[1];
141
+ inCppRawString = true;
142
+ j += match[0].length;
143
+ continue;
144
+ }
145
+ }
146
+ if (isJS && line[j] === '/') {
147
+ let k = j - 1;
148
+ while (k >= 0 && /\s/.test(line[k])) k--;
149
+ let isRegex = false;
150
+ if (k < 0) {
151
+ isRegex = true;
152
+ } else {
153
+ const prevChar = line[k];
154
+ if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
155
+ isRegex = true;
156
+ } else {
157
+ const prefix = line.slice(0, k + 1);
158
+ if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
159
+ isRegex = true;
160
+ }
161
+ }
162
+ }
163
+ if (isRegex) {
164
+ inRegex = true;
165
+ j++;
166
+ continue;
167
+ }
168
+ }
92
169
  }
93
170
  }
94
171
  }
@@ -125,7 +202,6 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
125
202
  }
126
203
  }
127
204
  if (!inBacktick) {
128
- // Check if current character is a double quote [ds]
129
205
  if (line[j] === '"' && !inSingle) {
130
206
  let escaped = false;
131
207
  let k = j - 1;
@@ -157,13 +233,16 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
157
233
  inDouble = false;
158
234
  }
159
235
  }
160
- return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
236
+ return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
161
237
  }
162
238
 
163
- /** Analyzes comments in a given list of lines [ds] */
239
+ // Analyzes comments in a given set of code lines [ds]
164
240
  function analyzeComments(lines, ext = '') {
165
241
  const isPython = ext.toLowerCase() === '.py';
166
242
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
243
+ const isRustOrSwift = ['.rs', '.swift'].includes(ext.toLowerCase());
244
+ const isCpp = ['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp'].includes(ext.toLowerCase());
245
+ const isJS = ['.js', '.jsx', '.ts', '.tsx'].includes(ext.toLowerCase());
167
246
  const analysis = [];
168
247
  let inBacktick = false;
169
248
  let inTripleDouble = false;
@@ -172,6 +251,10 @@ function analyzeComments(lines, ext = '') {
172
251
  let inDouble = false;
173
252
  let inBlockJS = false;
174
253
  let inBlockHTML = false;
254
+ let blockDepthJS = 0;
255
+ let inCppRawString = false;
256
+ let cppRawDelimiter = '';
257
+ let inRegex = false;
175
258
  for (let i = 0; i < lines.length; i++) {
176
259
  const line = lines[i];
177
260
  let commentStartIndex = -1;
@@ -179,14 +262,46 @@ function analyzeComments(lines, ext = '') {
179
262
  let j = 0;
180
263
  while (j < line.length) {
181
264
  if (inBlockJS) {
265
+ if (line.slice(j, j + 2) === '/*') {
266
+ if (isRustOrSwift) blockDepthJS++;
267
+ j += 2;
268
+ continue;
269
+ }
182
270
  if (line.slice(j, j + 2) === '*/') {
183
- inBlockJS = false;
271
+ if (isRustOrSwift && blockDepthJS > 1) {
272
+ blockDepthJS--;
273
+ } else {
274
+ inBlockJS = false;
275
+ blockDepthJS = 0;
276
+ }
184
277
  j += 2;
185
278
  continue;
186
279
  }
187
280
  j++;
188
281
  continue;
189
282
  }
283
+ if (inCppRawString) {
284
+ if (line.slice(j, j + 2 + cppRawDelimiter.length) === ')' + cppRawDelimiter + '"') {
285
+ inCppRawString = false;
286
+ j += 2 + cppRawDelimiter.length;
287
+ continue;
288
+ }
289
+ j++;
290
+ continue;
291
+ }
292
+ if (inRegex) {
293
+ let escaped = false;
294
+ let k = j - 1;
295
+ while (k >= 0 && line[k] === '\\') {
296
+ escaped = !escaped;
297
+ k--;
298
+ }
299
+ if (line[j] === '/' && !escaped) {
300
+ inRegex = false;
301
+ }
302
+ j++;
303
+ continue;
304
+ }
190
305
  if (inBlockHTML) {
191
306
  if (line.slice(j, j + 3) === '-->') {
192
307
  inBlockHTML = false;
@@ -220,20 +335,60 @@ function analyzeComments(lines, ext = '') {
220
335
  break;
221
336
  }
222
337
  } else {
223
- if (line.slice(j, j + 2) === '//') {
224
- commentStartIndex = j;
225
- break;
226
- }
227
- if (line.slice(j, j + 2) === '/*') {
228
- commentStartIndex = j;
229
- inBlockJS = true;
230
- j += 2;
231
- continue;
232
- }
233
- const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
234
- if (isShellOrRuby && line[j] === '#') {
235
- commentStartIndex = j;
236
- break;
338
+ const isShellOrRuby = ['.sh', '.rb'].includes(ext.toLowerCase());
339
+ if (isShellOrRuby) {
340
+ if (line[j] === '#') {
341
+ commentStartIndex = j;
342
+ break;
343
+ }
344
+ } else {
345
+ if (line.slice(j, j + 2) === '//') {
346
+ commentStartIndex = j;
347
+ break;
348
+ }
349
+ if (line.slice(j, j + 2) === '/*') {
350
+ commentStartIndex = j;
351
+ inBlockJS = true;
352
+ blockDepthJS = 1;
353
+ j += 2;
354
+ continue;
355
+ }
356
+ if (ext.toLowerCase() === '.php' && line[j] === '#') {
357
+ commentStartIndex = j;
358
+ break;
359
+ }
360
+ if (isCpp && line[j] === 'R' && line[j+1] === '"') {
361
+ const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
362
+ if (match) {
363
+ cppRawDelimiter = match[1];
364
+ inCppRawString = true;
365
+ j += match[0].length;
366
+ continue;
367
+ }
368
+ }
369
+ if (isJS && line[j] === '/') {
370
+ let k = j - 1;
371
+ while (k >= 0 && /\s/.test(line[k])) k--;
372
+ let isRegex = false;
373
+ if (k < 0) {
374
+ isRegex = true;
375
+ } else {
376
+ const prevChar = line[k];
377
+ if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
378
+ isRegex = true;
379
+ } else {
380
+ const prefix = line.slice(0, k + 1);
381
+ if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
382
+ isRegex = true;
383
+ }
384
+ }
385
+ }
386
+ if (isRegex) {
387
+ inRegex = true;
388
+ j++;
389
+ continue;
390
+ }
391
+ }
237
392
  }
238
393
  }
239
394
  }
@@ -281,7 +436,6 @@ function analyzeComments(lines, ext = '') {
281
436
  inDouble = !inDouble;
282
437
  }
283
438
  }
284
- // Check if current character is a single quote [ds]
285
439
  else if (line[j] === "'" && !inDouble) {
286
440
  let escaped = false;
287
441
  let k = j - 1;
@@ -323,7 +477,7 @@ function analyzeComments(lines, ext = '') {
323
477
  return analysis;
324
478
  }
325
479
 
326
- /** Splices comments into the given data [ds] */
480
+ // Splices comments into the given code data [ds]
327
481
  function spliceComments(data, comments, mode = 'default', ext = '') {
328
482
  const hasCRLF = data.includes('\r\n');
329
483
  const lineEnding = hasCRLF ? '\r\n' : '\n';
@@ -372,7 +526,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
372
526
  continue;
373
527
  }
374
528
 
375
- // Prune mode: remove all comments from the line [ds]
376
529
  if (mode === 'prune') {
377
530
  if (lineAnalysis.isPureComment) {
378
531
  finalDeletions.add(lineNum);
@@ -447,7 +600,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
447
600
  const indentation = indentMatch ? indentMatch[1] : '';
448
601
 
449
602
  const commentLines = c.comment.split(/\r?\n/).map((line, idx) => {
450
- // Add the comment to the list of comments [ds]
451
603
  let trimmed = line.trimStart();
452
604
  if (!trimmed) return '';
453
605
 
@@ -479,7 +631,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
479
631
  const filteredText = filtered.map(line => line.text);
480
632
  const filteredIndices = filtered.map(line => line.originalIndex);
481
633
 
482
- // Check if the filtered text is equal to the original text [ds]
483
634
  const textEqual = filteredText.every((text, idx) => {
484
635
  const origIdx = filteredIndices[idx];
485
636
  const originalLine = originalLines[origIdx];
@@ -518,7 +669,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
518
669
  return annotated.map(line => line.text).join(lineEnding);
519
670
  }
520
671
 
521
- /** Runs the CLI interface [ds] */
672
+ // Runs the CLI interface for the application [ds]
522
673
  async function runCLI() {
523
674
  rl = readline.createInterface({ input: process.stdin, output: process.stdout });
524
675
  askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
@@ -662,7 +813,6 @@ Options:
662
813
  ];
663
814
  const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
664
815
 
665
- /** Checks if a given path is ignored [ds] */
666
816
  function isPathIgnored(targetPath) {
667
817
  const filename = path.basename(targetPath);
668
818
  for (const pattern of allIgnored) {
@@ -673,7 +823,7 @@ Options:
673
823
  return false;
674
824
  }
675
825
 
676
- /** Processes a given path [ds] */
826
+ // Processes a given file path, either a file or directory [ds]
677
827
  async function processPath(targetPath) {
678
828
  const stats = fs.statSync(targetPath);
679
829
 
@@ -731,6 +881,7 @@ Options:
731
881
  fs.renameSync(tempPath, targetPath);
732
882
  console.log(` Successfully saved ${targetPath}`);
733
883
  } else {
884
+ // Asks the user if they want to save the commented code to a file [ds]
734
885
  console.log(` Skipped ${targetPath}`);
735
886
  }
736
887
  } else {
@@ -763,6 +914,7 @@ Options:
763
914
  rl.close();
764
915
  }
765
916
 
917
+ // Starts the CLI application if this script is run directly [ds]
766
918
  if (require.main === module) {
767
919
  runCLI().catch(err => {
768
920
  console.error(err);
package/lib/config.js CHANGED
@@ -4,7 +4,7 @@ const os = require('os');
4
4
  const readline = require('readline');
5
5
  const configPath = path.join(os.homedir(), '.devsplainrc');
6
6
 
7
- /** Prompts the user for a secret input, hiding the input from the console [ds] */
7
+ /** Asks the user for a secret input (e.g., an API key) without echoing the input to the console. [ds] */
8
8
  function askSecret(query) {
9
9
  return new Promise((resolve) => {
10
10
  const rl = readline.createInterface({
@@ -33,7 +33,7 @@ function askSecret(query) {
33
33
  });
34
34
  }
35
35
 
36
- /** Retrieves the configuration, either from environment variables or by prompting the user [ds] */
36
+ /** Attempts to fetch the configuration settings, either from environment variables or by prompting the user. [ds] */
37
37
  async function getConfig(forceWizard = false) {
38
38
  if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
39
39
  const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
@@ -46,8 +46,8 @@ async function getConfig(forceWizard = false) {
46
46
  baseUrl
47
47
  };
48
48
  }
49
+ // If the configuration file doesn't exist, or if we're forcing the wizard, let's prompt the user for configuration. [ds]
49
50
 
50
- // If the configuration file does not exist or forceWizard is true, prompt the user to configure [ds]
51
51
  if (!fs.existsSync(configPath) || forceWizard) {
52
52
  let rl = readline.createInterface({
53
53
  input: process.stdin,
@@ -58,13 +58,13 @@ async function getConfig(forceWizard = false) {
58
58
  let config = null;
59
59
  let confirmed = false;
60
60
 
61
- // Continuously prompt the user until the configuration is confirmed [ds]
61
+ // Keep prompting the user until they confirm their configuration settings. [ds]
62
62
  while (!confirmed) {
63
63
  let baseUrl = "";
64
64
  let model = "";
65
65
  let provider = "";
66
66
 
67
- // Display the list of available AI providers [ds]
67
+ // Display the list of available AI providers to the user. [ds]
68
68
  console.log("\nWhich AI Provider Do You want to use?");
69
69
  console.log("1. Groq (Free, Fast, Llama-3)");
70
70
  console.log("2. Gemini (Free Tier)");
@@ -115,7 +115,7 @@ async function getConfig(forceWizard = false) {
115
115
  continue;
116
116
  }
117
117
 
118
- // Prompt the user for an API key [ds]
118
+ // Get the API key from the user. [ds]
119
119
  let apiKey = '';
120
120
  while (true) {
121
121
  const promptMsg = provider === 'custom'
@@ -142,7 +142,7 @@ async function getConfig(forceWizard = false) {
142
142
  console.log(`API key is required for provider '${provider}'.`);
143
143
  }
144
144
 
145
- // Ask the user if they want to enable auto-pruning of existing comments [ds]
145
+ // Ask the user if they want to enable auto-pruning of existing human/AI comments. [ds]
146
146
  let autoPrune = false;
147
147
  while (true) {
148
148
  const pruneAns = (await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ")).trim().toLowerCase();
@@ -156,7 +156,7 @@ async function getConfig(forceWizard = false) {
156
156
  console.log("Invalid choice. Please enter 'y' or 'n'.");
157
157
  }
158
158
 
159
- // Display a summary of the configuration [ds]
159
+ // Display a summary of the user's configuration settings. [ds]
160
160
  console.log("\n--- Configuration Summary ---");
161
161
  console.log(`Provider: ${provider}`);
162
162
  console.log(`Model: ${model}`);
@@ -165,7 +165,7 @@ async function getConfig(forceWizard = false) {
165
165
  console.log(`Auto-Prune: ${autoPrune ? 'Yes' : 'No'}`);
166
166
  console.log("-----------------------------\n");
167
167
 
168
- // Confirm the configuration with the user [ds]
168
+ // Confirm the configuration settings with the user. [ds]
169
169
  while (true) {
170
170
  const confirm = (await askQuestion("Does this look correct? (y/n, default: y): ")).trim().toLowerCase();
171
171
  if (confirm === '' || confirm === 'y' || confirm === 'yes') {
@@ -187,10 +187,9 @@ async function getConfig(forceWizard = false) {
187
187
 
188
188
  rl.close();
189
189
 
190
- // Write the configuration to the config file [ds]
190
+ // Write the configuration settings to the config file. [ds]
191
191
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
192
192
  try {
193
- // Set the permissions of the config file to prevent other users from reading it [ds]
194
193
  if (process.platform !== 'win32') {
195
194
  fs.chmodSync(configPath, 0o600);
196
195
  }
@@ -204,4 +203,5 @@ async function getConfig(forceWizard = false) {
204
203
  }
205
204
  }
206
205
 
206
+ // Export the getConfig function for use in other modules. [ds]
207
207
  module.exports = { getConfig };
package/lib/llm.js CHANGED
@@ -1,6 +1,7 @@
1
- /** Fetches a URL with retry functionality [ds] */
1
+ // Asynchronously fetches data from the specified URL with a retry mechanism [ds]
2
2
  async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000) {
3
3
  let lastError;
4
+ // Loop through each attempt, up to the maximum number of retries [ds]
4
5
  for (let attempt = 0; attempt < maxRetries; attempt++) {
5
6
  const controller = new AbortController();
6
7
  const timeoutId = setTimeout(() => controller.abort(), 15000);
@@ -36,66 +37,70 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
36
37
  throw lastError;
37
38
  }
38
39
 
39
- /** Gets comments for the given code [ds] */
40
+ // Retrieves comments for the given code, language, and configuration [ds]
40
41
  async function getComments(code, language, config, mode = 'default') {
41
42
  const lines = code.split(/\r?\n/);
42
43
  const numberedCode = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
43
44
 
44
- let prompt = "";
45
- if (mode === 'clean') {
46
- prompt = `
47
- You are a code documentation scrubber. Analyze the following ${language} code which has line numbers prepended to it.
48
- Your goal is to identify all lines containing comments (both block/JSDoc and inline comments) and return their line numbers to delete them.
49
-
50
- CRITICAL RULES:
51
- 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.
52
- 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").
53
- 3. Do NOT include the original code in your response.
54
- 4. If no comments are found, return an empty array: [].
55
- 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.
56
-
57
- Example Output:
58
- [
59
- { "line": 4, "action": "delete" },
60
- { "line": 5, "action": "delete" }
61
- ]
45
+ // Extract the file extension from the language string [ds]
46
+ const extMatch = language.match(/\.[0-9a-z]+$/i);
47
+ const ext = extMatch ? extMatch[0].toLowerCase() : '';
48
+ const isPython = ext === '.py';
49
+ const isRubyOrShell = ['.rb', '.sh', '.php'].includes(ext);
50
+ const isHTML = ['.html', '.vue', '.svelte'].includes(ext);
51
+ const isSql = ext === '.sql';
52
+
53
+ // Define the single-line comment token and examples [ds]
54
+ let singleLineToken = '//';
55
+ let blockExample = '/** Calculates the total price */';
56
+ let inlineExample = '// Check for null values';
57
+
58
+ if (isPython || isRubyOrShell) {
59
+ singleLineToken = '#';
60
+ blockExample = '# Calculates the total price';
61
+ inlineExample = '# Check for null values';
62
+ } else if (isHTML) {
63
+ singleLineToken = '<!--';
64
+ blockExample = '<!-- Calculates the total price -->';
65
+ inlineExample = '<!-- Check for null values -->';
66
+ } else if (isSql) {
67
+ singleLineToken = '--';
68
+ blockExample = '-- Calculates the total price';
69
+ inlineExample = '-- Check for null values';
70
+ }
62
71
 
63
- Here is the source code:
64
- ${numberedCode}
65
- `.trim();
66
- } else {
67
- let instruction = "Provide JSDoc/docstrings block comments above functions and sparse inline comments for complex logic.";
68
- if (mode === 'light') {
69
- instruction = "Provide ONLY JSDoc/docstrings above functions. Keep it minimal.";
70
- } else if (mode === 'full') {
71
- 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.";
72
- }
72
+ // Provide instructions based on the mode [ds]
73
+ let instruction = `Provide block comments above functions and sparse inline comments for complex logic.`;
74
+ if (mode === 'light') {
75
+ instruction = `Provide ONLY block comments above functions. Keep it minimal.`;
76
+ } else if (mode === 'full') {
77
+ instruction = `Provide highly detailed block comments above functions, and exhaustive step-by-step inline comments 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.`;
78
+ }
73
79
 
74
- prompt = `
80
+ let prompt = `
75
81
  You are a code documentation engine. Analyze the following ${language} code which has line numbers prepended to it.
76
82
  ${instruction}
77
83
 
78
84
  CRITICAL RULES:
79
85
  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.
80
- 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 /** */).
86
+ 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).
81
87
  3. Do NOT include the original code in your response.
82
88
  4. If no comments are needed, return an empty array: [].
89
+ 5. IMPORTANT: Use ONLY ${singleLineToken} for comments. DO NOT use docstrings or multi-line string literals like """ or ''' for comments.
83
90
 
84
91
  Example Output:
85
92
  [
86
- { "line": 4, "comment": "/** Calculates the total price */" },
87
- { "line": 12, "comment": "// Check for null values" }
93
+ { "line": 4, "comment": "${blockExample}" },
94
+ { "line": 12, "comment": "${inlineExample}" }
88
95
  ]
89
96
 
90
97
  Here is the source code:
91
98
  ${numberedCode}
92
- `.trim();
93
- }
99
+ `.trim();
94
100
 
95
- // Initialize text response [ds]
96
101
  let textResponse = "";
97
102
 
98
- // Check if provider is Gemini [ds]
103
+ // Handle API requests based on the provider [ds]
99
104
  if (config.provider === 'gemini') {
100
105
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
101
106
  let data;
@@ -118,6 +123,7 @@ ${numberedCode}
118
123
  }
119
124
  textResponse = data.candidates[0].content.parts[0].text;
120
125
  } else if (config.provider === 'claude') {
126
+ // Handle Claude API requests [ds]
121
127
  const url = `${config.baseUrl}/v1/messages`;
122
128
  let data;
123
129
  try {
@@ -146,7 +152,7 @@ ${numberedCode}
146
152
  }
147
153
  textResponse = data.content[0].text;
148
154
  }
149
- // Otherwise, use an OpenAI-compatible provider [ds]
155
+ // Handle other API requests [ds]
150
156
  else {
151
157
  const url = `${config.baseUrl}/v1/chat/completions`;
152
158
  let data;
@@ -176,7 +182,6 @@ ${numberedCode}
176
182
  textResponse = data.choices[0].message.content;
177
183
  }
178
184
 
179
- // Clean up the text response [ds]
180
185
  let cleanText = textResponse.trim();
181
186
  const start = cleanText.indexOf('[');
182
187
  const end = cleanText.lastIndexOf(']');
@@ -184,9 +189,9 @@ ${numberedCode}
184
189
  cleanText = cleanText.substring(start, end + 1);
185
190
  }
186
191
 
187
- // Parse the response as JSON [ds]
188
192
  let parsed;
189
193
  try {
194
+ // Attempt to parse the response as JSON [ds]
190
195
  parsed = JSON.parse(cleanText);
191
196
  } catch (e) {
192
197
  throw new Error(`Parsing Error: Failed to parse LLM response as JSON. Raw response was:\n${textResponse}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
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",