main.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. /*
  2. * Copyright (C) 2024-present Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. // METADATA // {"ai-params":{"service":"claude"},"comment-verbosity": "high","ai-commented":{"service":"claude"}}
  20. const enq = require('enquirer');
  21. const wrap = require('word-wrap');
  22. const dedent = require('dedent');
  23. const axios = require('axios');
  24. const { walk, EXCLUDE_LISTS } = require('../file-walker/test');
  25. const https = require('https');
  26. const fs = require('fs');
  27. const path_ = require('path');
  28. const FILE_EXCLUDES = [
  29. /^\.git/,
  30. /^node_modules\//,
  31. /\/node_modules$/,
  32. /^node_modules$/,
  33. /package-lock\.json/,
  34. /^src\/dev-center\/js/,
  35. /src\/backend\/src\/public\/assets/,
  36. /^src\/gui\/src\/lib/,
  37. /^eslint\.config\.js$/,
  38. ];
  39. const models_to_try = [
  40. {
  41. service: 'openai-completion',
  42. model: 'gpt-4o',
  43. },
  44. {
  45. service: 'claude',
  46. },
  47. {
  48. service: 'xai',
  49. },
  50. // === Models that didn't work for this ===
  51. // Sometimes outputs source lines despite "no surrounding text" instruction
  52. // {
  53. // service: 'openai-completion',
  54. // model: 'gpt-4o-mini',
  55. // },
  56. // Was the first to break a source file
  57. // {
  58. // service: 'together-ai',
  59. // model: 'meta-llama/Meta-Llama-3-70B-Instruct-Turbo',
  60. // },
  61. // Occasionally fails spectacularly
  62. // {
  63. // service: 'mistral',
  64. // model: 'mistral-large-latest',
  65. // }
  66. ];
  67. const axi = axios.create({
  68. httpsAgent: new https.Agent({
  69. rejectUnauthorized: false
  70. })
  71. });
  72. const cwd = process.cwd();
  73. const context = {};
  74. context.config = JSON.parse(
  75. fs.readFileSync('config.json')
  76. );
  77. /**
  78. * @class AI
  79. * @description A class that handles interactions with the Puter API for AI-powered chat completions.
  80. * This class provides an interface to make requests to the Puter chat completion service,
  81. * handling authentication and message formatting. It supports various AI models through
  82. * the puter-chat-completion driver interface.
  83. */
  84. class AI {
  85. constructor (context) {
  86. //
  87. }
  88. /**
  89. * Sends a chat completion request to the Puter API and returns the response message.
  90. *
  91. * @param {Object} params - The parameters for the completion request
  92. * @param {Array} params.messages - Array of message objects to send to the API
  93. * @param {Object} params.driver_params - Additional parameters for the driver interface
  94. * @returns {Promise<Object>} The response message from the API
  95. *
  96. * Makes a POST request to the configured API endpoint with the provided messages and
  97. * driver parameters. Authenticates using the configured auth token and returns the
  98. * message content from the response.
  99. */
  100. async complete ({ messages, driver_params }) {
  101. const response = await axi.post(`${context.config.api_url}/drivers/call`, {
  102. interface: 'puter-chat-completion',
  103. method: 'complete',
  104. ...driver_params,
  105. args: {
  106. messages,
  107. },
  108. }, {
  109. headers: {
  110. "Content-Type": "application/json",
  111. Origin: 'https://puter.local',
  112. Authorization: `Bearer ${context.config.auth_token}`,
  113. },
  114. });
  115. return response.data.result.message;
  116. }
  117. }
  118. const ai_message_to_lines = text => {
  119. // Extract text content from message object, handling various formats
  120. while ( typeof text === 'object' ) {
  121. if ( Array.isArray(text) ) text = text[0];
  122. else if ( text.content ) text = text.content;
  123. else if ( text.text ) text = text.text;
  124. else {
  125. console.log('Invalid message object', text);
  126. throw new Error('Invalid message object');
  127. }
  128. }
  129. return text.split('\n');
  130. }
  131. /**
  132. * @class JavascriptFileProcessor
  133. * @description A class responsible for processing JavaScript source files to identify and extract
  134. * various code definitions and structures. It analyzes the file content line by line using
  135. * configurable pattern matchers to detect classes, methods, functions, control structures,
  136. * and constants. The processor maintains context and parameters for consistent processing
  137. * across multiple files.
  138. */
  139. class JavascriptFileProcessor {
  140. constructor (context, parameters) {
  141. this.context = context;
  142. this.parameters = parameters;
  143. }
  144. process (lines) {
  145. const definitions = [];
  146. // Collect definitions by iterating through each line
  147. for ( let i = 0 ; i < lines.length ; i++ ) {
  148. const line = lines[i];
  149. // Iterate through each line in the file
  150. for ( const matcher of this.parameters.definition_matchers ) {
  151. const match = matcher.pattern.exec(line);
  152. console.log('match object', match);
  153. // Check if there is a match for any of the definition patterns
  154. if ( match ) {
  155. definitions.push({
  156. ...matcher.handler(match),
  157. line: i,
  158. });
  159. break;
  160. }
  161. }
  162. }
  163. return { definitions };
  164. }
  165. }
  166. const js_processor = new JavascriptFileProcessor(context, {
  167. definition_matchers: [
  168. // {
  169. // name: 'require',
  170. // pattern: /const (\w+) = require\(['"](.+)['"]\);/,
  171. // handler: (match) => {
  172. // const [ , name, path ] = match;
  173. // return {
  174. // type: 'require',
  175. // name,
  176. // path,
  177. // };
  178. // }
  179. // },
  180. {
  181. name: 'class',
  182. pattern: /class (\w+)(?: extends (.+))? {/,
  183. handler: (match) => {
  184. const [ , name, parent ] = match;
  185. return {
  186. type: 'class',
  187. name,
  188. parent,
  189. };
  190. }
  191. },
  192. {
  193. name: 'if',
  194. pattern: /^\s*if\s*\(.*\)\s*{/,
  195. /**
  196. * Matches code patterns against a line to identify if it's an if statement
  197. * @param {string} line - The line of code to check
  198. * @returns {Object} Returns an object with type: 'if' if pattern matches
  199. * @description Identifies if statements by matching the pattern /^\s*if\s*\(.*\)\s*{/
  200. * This handles basic if statement syntax with optional whitespace and any condition
  201. * within the parentheses
  202. */
  203. handler: () => {
  204. return { type: 'if' };
  205. }
  206. },
  207. {
  208. name: 'while',
  209. pattern: /^\s*while\s*\(.*\)\s*{/,
  210. /**
  211. * Matches lines that begin with a while loop structure.
  212. * @param {void} - Takes no parameters
  213. * @returns {Object} Returns an object with type: 'while' to indicate this is a while loop definition
  214. * @description Used by the definition matcher system to identify while loop structures in code.
  215. * The pattern looks for lines that start with optional whitespace, followed by 'while',
  216. * followed by parentheses containing any characters, and ending with an opening curly brace.
  217. */
  218. handler: () => {
  219. return { type: 'while' };
  220. }
  221. },
  222. {
  223. name: 'for',
  224. pattern: /^\s*for\s*\(.*\)\s*{/,
  225. /**
  226. * Matches for loop patterns in code and returns a 'for' type definition.
  227. * Used by the JavascriptFileProcessor to identify for loop structures.
  228. * @returns {Object} An object with type 'for' indicating a for loop was found
  229. */
  230. handler: () => {
  231. return { type: 'for' };
  232. }
  233. },
  234. {
  235. name: 'method',
  236. pattern: /^\s*async .*\(.*\).*{/,
  237. handler: (match) => {
  238. const [ , name ] = match;
  239. return {
  240. async: true,
  241. type: 'method',
  242. name,
  243. };
  244. }
  245. },
  246. {
  247. name: 'method',
  248. pattern: /^\s*[A-Za-z_\$]+.*\(\).*{/,
  249. handler: (match) => {
  250. const [ , name ] = match;
  251. // Extract method name from match array and handle special cases for 'if' and 'while'
  252. if ( name === 'if' ) {
  253. return { type: 'if' };
  254. }
  255. // Check if the name is 'while' and return appropriate type
  256. if ( name === 'while' ) {
  257. return { type: 'while' };
  258. }
  259. return {
  260. type: 'method',
  261. name,
  262. };
  263. }
  264. },
  265. {
  266. name: 'function',
  267. pattern: /^\s*function .*\(.*\).*{/,
  268. handler: (match) => {
  269. const [ , name ] = match;
  270. return {
  271. type: 'function',
  272. scope: 'function',
  273. name,
  274. };
  275. }
  276. },
  277. {
  278. name: 'function',
  279. pattern: /const [A-Za-z_]+\s*=\s*\(.*\)\s*=>\s*{/,
  280. handler: (match) => {
  281. const [ , name, args ] = match;
  282. return {
  283. type: 'function',
  284. scope: 'lexical',
  285. name,
  286. args: (args ?? '').split(',').map(arg => arg.trim()),
  287. };
  288. }
  289. },
  290. {
  291. name: 'const',
  292. // pattern to match only uppercase-lettered variable names
  293. pattern: /const ([A-Z_]+) = (.+);/,
  294. handler: (match) => {
  295. const [ , name, value ] = match;
  296. return {
  297. type: 'const',
  298. name,
  299. value,
  300. };
  301. }
  302. }
  303. ],
  304. });
  305. /**
  306. * Creates a limited view of the code file by showing specific ranges around key lines.
  307. * Takes an array of lines and key places (anchors with context ranges) and returns
  308. * a formatted string showing relevant code sections with line numbers and descriptions.
  309. * Merges overlapping ranges to avoid duplication.
  310. * @param {string[]} lines - Array of code lines from the file
  311. * @param {Object[]} key_places - Array of objects defining important locations and context
  312. * @returns {string} Formatted string containing the limited code view
  313. */
  314. const create_limited_view = (lines, key_places) => {
  315. // Sort key places by starting line
  316. key_places.sort((a, b) => {
  317. const a_start = Math.max(0, a.anchor - a.lines_above);
  318. const b_start = Math.max(0, b.anchor - b.lines_above);
  319. return a_start - b_start;
  320. });
  321. const visible_ranges = [];
  322. // Create visible ranges for each key place
  323. // Create visible ranges for each key place in the limited view
  324. for ( const key_place of key_places ) {
  325. const anchor = key_place.anchor;
  326. const lines_above = key_place.lines_above;
  327. const lines_below = key_place.lines_below;
  328. const start = Math.max(0, anchor - lines_above);
  329. const end = Math.min(lines.length, anchor + lines_below);
  330. visible_ranges.push({
  331. anchor: key_place.anchor,
  332. comment: key_place.comment,
  333. start,
  334. end,
  335. });
  336. }
  337. // Merge overlapping visible ranges
  338. const merged_ranges = [];
  339. // Iterate through each visible range and merge overlapping ones
  340. for ( const range of visible_ranges ) {
  341. range.comments = [{
  342. anchor: range.anchor,
  343. text: range.comment
  344. }];
  345. // If no merged ranges exist yet, add this range as the first one
  346. if ( ! merged_ranges.length ) {
  347. merged_ranges.push(range);
  348. continue;
  349. }
  350. const last_range = merged_ranges[merged_ranges.length - 1];
  351. // Check if the current range overlaps with the last range in merged_ranges
  352. if ( last_range.end >= range.start ) {
  353. last_range.end = Math.max(last_range.end, range.end);
  354. last_range.comments.push({
  355. anchor: range.anchor,
  356. text: range.comment
  357. });
  358. } else {
  359. merged_ranges.push(range);
  360. }
  361. }
  362. // Create the limited view, adding line numbers and comments
  363. let limited_view = '';
  364. let previous_visible_range = null;
  365. // Iterate through visible ranges and add line numbers and comments
  366. for ( let i = 0 ; i < lines.length ; i++ ) {
  367. const line = lines[i];
  368. let visible_range = null;
  369. if ( i === 22 ) debugger;
  370. // Iterate through merged ranges to find which range contains the current line
  371. for ( const range of merged_ranges ) {
  372. // Check if current line is within any of the merged ranges
  373. if ( i >= range.start && i < range.end ) {
  374. visible_range = range;
  375. break;
  376. }
  377. }
  378. // console.log('visible_range', visible_range, i);
  379. // Check if this line is visible in the current range
  380. if ( visible_range === null ) {
  381. continue;
  382. }
  383. // Check if visible range is different from previous range
  384. if ( visible_range !== previous_visible_range ) {
  385. if ( i !== 0 ) limited_view += '\n';
  386. // Check if we're starting a new visible range and add appropriate header
  387. if ( visible_range.comments.length === 1 ) {
  388. const comment = visible_range.comments[0];
  389. limited_view += `window around line ${comment.anchor}: ${comment.text}\n`;
  390. } else {
  391. limited_view += `window around lines ${visible_range.comments.length} key lines:\n`;
  392. // Iterate through visible range comments and add them to the limited view
  393. for ( const comment of visible_range.comments ) {
  394. limited_view += `- line ${comment.anchor}: ${comment.text}\n`;
  395. }
  396. }
  397. }
  398. previous_visible_range = visible_range;
  399. limited_view += `${i + 1}: ${line}\n`;
  400. }
  401. return limited_view;
  402. };
  403. /**
  404. * Inject comments into lines
  405. * @param {*} lines - Array of original file lines
  406. * @param {*} comments - Array of comment objects
  407. *
  408. * Comment object structure:
  409. * {
  410. * position: 0, // Position in lines array
  411. * lines: [ 'comment line 1', 'comment line 2', ... ]
  412. * }
  413. */
  414. /**
  415. * Injects comments into an array of code lines at specified positions
  416. * @param {string[]} lines - Array of original file lines
  417. * @param {Object[]} comments - Array of comment objects specifying where and what to inject
  418. * @param {number} comments[].position - Line number where comment should be inserted
  419. * @param {string[]} comments[].lines - Array of comment text lines to insert
  420. */
  421. const inject_comments = (lines, comments) => {
  422. // Sort comments in reverse order
  423. comments.sort((a, b) => b.position - a.position);
  424. // Inject comments into lines
  425. // Inject comments into lines array based on comment objects
  426. for ( const comment of comments ) {
  427. // AI might have been stupid and added a comment above a blank line,
  428. // despite that we told it not to do that. So we need to adjust the position.
  429. // Adjust comment position if it would be above a blank line
  430. while ( comment.position < lines.length && ! lines[comment.position].trim() ) {
  431. comment.position++;
  432. }
  433. const indentation = lines[comment.position].match(/^\s*/)[0];
  434. console.log('????', comment.position, lines[comment.position], '|' + indentation + '|');
  435. const comment_lines = comment.lines.map(line => `${indentation}${line}`);
  436. lines.splice(comment.position, 0, ...comment_lines);
  437. // If the first line of the comment lines starts with '/*`, ensure there is
  438. // a blank line above it.
  439. // Check if comment starts with '/*' to ensure proper spacing above JSDoc comments
  440. if ( comment_lines[0].trim().startsWith('/*') ) {
  441. // Check if comment starts with JSDoc style to add blank line above
  442. if ( comment.position > 0 && lines[comment.position - 1].trim() === '' ) {
  443. lines.splice(comment.position, 0, '');
  444. }
  445. }
  446. }
  447. }
  448. const textutil = {};
  449. textutil.format = text => {
  450. return wrap(dedent(text), {
  451. width: 80,
  452. indent: '| '
  453. });
  454. };
  455. context.ai = new AI(context);
  456. /**
  457. * Creates a new AI instance for handling chat completions
  458. * @param {Object} context - The application context object
  459. * @description Initializes an AI instance that interfaces with the Puter chat completion API.
  460. * The AI instance is used to generate comments and other text responses through the
  461. * chat completion interface.
  462. */
  463. const main = async () => {
  464. // const message = await context.ai.complete({
  465. // messages: [
  466. // {
  467. // role: 'user',
  468. // content: `
  469. // Introduce yourself as the Puter Comment Writer. You are an AI that will
  470. // write comments in code files. A file walker will be used to iterate over
  471. // the source files and present them one at a time, and the user will accept,
  472. // reject, or request edits interactively. For each new file, a clean AI
  473. // context will be created.
  474. // `.trim()
  475. // }
  476. // ]
  477. // });
  478. // const intro = message.content;
  479. const intro = textutil.format(`
  480. Hello! I am the Puter Comment Writer, an AI designed to enhance your code files with meaningful comments. As you walk through your source files, I will provide insights, explanations, and clarifications tailored to the specific content of each file. You can choose to accept my comments, request edits for more clarity or detail, or even reject them if they don't meet your needs. Each time we move to a new file, I'll start fresh with a clean context, ready to help you improve your code documentation. Let's get started!
  481. `);
  482. console.log(intro);
  483. console.log(`Enter a path relative to: ${process.cwd()}`);
  484. console.log('arg?', process.argv[2]);
  485. let rootpath = process.argv[2] ? { path: process.argv[2] } : await enq.prompt({
  486. type: 'input',
  487. name: 'path',
  488. message: 'Enter path:'
  489. });
  490. rootpath = path_.resolve(rootpath.path);
  491. console.log('rootpath:', rootpath);
  492. const walk_iter = walk({
  493. excludes: FILE_EXCLUDES,
  494. }, rootpath);
  495. let i = 0, limit = undefined;
  496. for await ( const value of walk_iter ) {
  497. if ( limit !== undefined && i >= limit ) break;
  498. i++;
  499. // Exit after processing 12 files
  500. if ( value.is_dir ) {
  501. console.log('directory:', value.path);
  502. continue;
  503. }
  504. // Check if file is not a JavaScript file and skip it
  505. if ( ! value.name.endsWith('.js') ) {
  506. continue;
  507. }
  508. console.log('file:', value.path);
  509. const lines = fs.readFileSync(value.path, 'utf8').split('\n');
  510. let metadata, has_metadata_line = false;
  511. // Check if metadata line exists and parse it
  512. if ( lines[0].startsWith('// METADATA // ') ) {
  513. has_metadata_line = true;
  514. metadata = JSON.parse(lines[0].slice('// METADATA // '.length));
  515. // Check if metadata exists and has been parsed from the first line
  516. if ( metadata['ai-commented'] ) {
  517. console.log('File was already commented by AI; skipping...');
  518. continue;
  519. }
  520. } else metadata = {};
  521. let refs = null;
  522. // Check if there are any references in the metadata
  523. if ( metadata['ai-refs'] ) {
  524. const relative_file_paths = metadata['ai-refs'];
  525. // name of file is the key, value is the contents
  526. const references = {};
  527. let n = 0;
  528. // Iterate through each relative file path in the metadata
  529. for ( const relative_file_path of relative_file_paths ) {
  530. n++;
  531. const full_path = path_.join(path_.dirname(value.path), relative_file_path);
  532. const ref_text = fs.readFileSync(full_path, 'utf8');
  533. references[relative_file_path] = ref_text;
  534. }
  535. // Check if there are any references in the metadata and process them
  536. if ( n === 1 ) {
  537. refs = dedent(`
  538. The following documentation contains relevant information about the code.
  539. The code will follow after this documentation.
  540. `);
  541. refs += '\n\n' + dedent(references[Object.keys(references)[0]]);
  542. } else if ( n > 2 ) {
  543. refs = dedent(`
  544. The following documentation contains relevant information about the code.
  545. The code will follow after a number of documentation files.
  546. `);
  547. // Iterate through each key in the references object
  548. for ( const key of Object.keys(references) ) {
  549. refs += '\n\n' + dedent(references[key]);
  550. }
  551. }
  552. }
  553. const action = limit === undefined ? await enq.prompt({
  554. type: 'select',
  555. name: 'action',
  556. message: 'Select action:',
  557. choices: [
  558. 'generate',
  559. 'skip',
  560. 'all',
  561. 'limit',
  562. 'exit',
  563. ]
  564. }) : 'generate';
  565. // const action = 'generate';
  566. // Check if user wants to exit the program
  567. if ( action.action === 'exit' ) {
  568. break;
  569. }
  570. // Skip if user chose to exit
  571. if ( action.action === 'skip' ) {
  572. continue;
  573. }
  574. if ( action.action === 'limit' ) {
  575. limit = await enq.prompt({
  576. type: 'input',
  577. name: 'limit',
  578. message: 'Enter limit:'
  579. });
  580. i = 1;
  581. limit = Number(limit.limit);
  582. }
  583. if ( action.action === 'all' ) {
  584. i = 1;
  585. limit = Infinity;
  586. }
  587. const { definitions } = js_processor.process(lines);
  588. const key_places = [];
  589. key_places.push({
  590. anchor: 0,
  591. lines_above: 2,
  592. lines_below: 200,
  593. comment: `Top of file: ${value.path}`
  594. });
  595. key_places.push({
  596. anchor: lines.length - 1,
  597. lines_above: 200,
  598. lines_below: 2,
  599. comment: `Bottom of ${value.name}`
  600. });
  601. // Iterate through each definition and add comments based on its type
  602. for ( const definition of definitions ) {
  603. key_places.push({
  604. anchor: definition.line,
  605. lines_above: 40,
  606. lines_below: 40,
  607. comment: `${definition.type}.`
  608. });
  609. }
  610. let limited_view = create_limited_view(lines, key_places);
  611. console.log('--- view ---');
  612. console.log(limited_view);
  613. const comments = [];
  614. // comments.push({
  615. // position: 0,
  616. // });
  617. // for ( const definition of definitions ) {
  618. // comments.push({
  619. // position: definition.line,
  620. // definition,
  621. // });
  622. // }
  623. // This was worth a try but the LLM is very bad at this
  624. /*
  625. const message = await context.ai.complete({
  626. messages: [
  627. {
  628. role: 'user',
  629. content: dedent(`
  630. Respond with comma-separated numbers only, with no surrounding text.
  631. Please write the numbers of the lines above which a comment should be added.
  632. Do not include numbers of lines that are blank, already have comments, or are part of a comment.
  633. Prefer comment locations in a higher level scope, such as a classes, function definitions and class methods,
  634. `).trim() + '\n\n' + limited_view
  635. }
  636. ]
  637. });
  638. const numbers = message.content.split(',').map(n => Number(n));
  639. // Iterate through each number in the array of line numbers
  640. for ( const n of numbers ) {
  641. // Check if the line number is valid and not NaN before adding comment
  642. if ( Number.isNaN(n) ) {
  643. console.log('Invalid number:', n);
  644. continue;
  645. }
  646. comments.push({
  647. position: n - 1,
  648. });
  649. }
  650. */
  651. // Iterate through each definition to add comments
  652. for ( const def of definitions ) {
  653. console.log('def?', def);
  654. let instruction = '';
  655. // Check if the line starts with an if statement and has curly braces
  656. if ( def.type === 'class' ) {
  657. instruction = dedent(`
  658. Since the comment is going above a class definition, please write a JSDoc style comment.
  659. Make the comment as descriptive as possible, including the class name and its purpose.
  660. `);
  661. }
  662. // Check if comment is for an if/while/for control structure
  663. if ( def.type === 'if' || def.type === 'while' || def.type === 'for' ) {
  664. if ( metadata['comment-verbosity'] !== 'high' ) continue;
  665. instruction = dedent(`
  666. Since the comment is going above a control structure, please write a short concise comment.
  667. The comment should be only one or two lines long, and should use line comments.
  668. `);
  669. }
  670. // Check if comment is going above a method definition
  671. if ( def.type === 'method' ) {
  672. instruction = dedent(`
  673. Since the comment is going above a method, please write a JSDoc style comment.
  674. The comment should include a short concise description of the method's purpose,
  675. notes about its behavior, and any parameters or return values.
  676. `);
  677. }
  678. // Check if comment is for a constant definition and set appropriate instruction
  679. if ( def.type === 'const' ) {
  680. instruction = dedent(`
  681. Since the comment is going above a constant definition, please write a comment that explains
  682. the purpose of the constant and how it is used in the code.
  683. The comment should be only one or two lines long, and should use line comments.
  684. `);
  685. }
  686. comments.push({
  687. position: def.line,
  688. instruction: instruction,
  689. });
  690. }
  691. const driver_params = metadata['ai-params'] ??
  692. models_to_try[Math.floor(Math.random() * models_to_try.length)];
  693. // Iterate through each comment object to add comments to the code
  694. for ( const comment of comments ) {
  695. // This doesn't work very well yet
  696. /*
  697. const ranges_message = await context.ai.complete({
  698. messages: [
  699. {
  700. role: 'user',
  701. content: dedent(`
  702. Please only respond with comma-separated number ranges in this format with no surrounding text:
  703. 11-21, 25-30, 35-40
  704. You may also respond with "none".
  705. A comment will be added above line ${comment.position} in the code which follows.
  706. You are seeing a limited view of the code that includes chunks around interesting lines.
  707. Please specify ranges of lines that might provide useful context for this comment.
  708. Do not include in any range lines which are already visible in the limited view.
  709. Avoid specifying more than a couple hundred lines.
  710. `).trim() + '\n\n' + limited_view
  711. }
  712. ]
  713. });
  714. // Check if the comment lines start with '/*' and ensure there's a blank line above it
  715. if ( ranges_message.content.trim() !== 'none' ) {
  716. const ranges = ranges_message.content.split(',').map(range => {
  717. const [ start, end ] = range.split('-').map(n => Number(n));
  718. return { start, end };
  719. });
  720. // Iterate through ranges and add key places for each range
  721. for ( const range of ranges ) {
  722. key_places.push({
  723. anchor: range.start,
  724. lines_above: 0,
  725. lines_below: range.end - range.start,
  726. comment: `Requested range by AI agent: ${range.start}-${range.end}`
  727. });
  728. }
  729. limited_view = create_limited_view(lines, key_places);
  730. console.log('--- updated view ---');
  731. console.log(limited_view);
  732. }
  733. */
  734. const prompt =
  735. dedent(`
  736. Please write a comment to be added above line ${comment.position}.
  737. Do not write any surrounding text; just the comment itself.
  738. Please include comment markers. If the comment is on a class, function, or method, please use jsdoc style.
  739. The code is written in JavaScript.
  740. `).trim() +
  741. (refs ? '\n\n' + dedent(refs) : '') +
  742. (comment.instruction ? '\n\n' + dedent(comment.instruction) : '') +
  743. '\n\n' + limited_view
  744. ;
  745. // console.log('prompt:', prompt);
  746. const message = await context.ai.complete({
  747. messages: [
  748. {
  749. role: 'user',
  750. content: prompt
  751. }
  752. ],
  753. driver_params,
  754. });
  755. console.log('message:', message);
  756. comment.lines = ai_message_to_lines(message.content);
  757. // Remove leading and trailing blank lines
  758. // Remove leading and trailing blank lines from comment lines array
  759. while ( comment.lines.length && ! comment.lines[0].trim() ) {
  760. comment.lines.shift();
  761. }
  762. // Remove trailing blank lines from comment lines array
  763. while ( comment.lines.length && ! comment.lines[comment.lines.length - 1].trim() ) {
  764. comment.lines.pop();
  765. }
  766. // Remove leading "```" or "```<language>" lines
  767. // Remove leading "```" or "```<language>" lines
  768. if ( comment.lines[0].startsWith('```') ) {
  769. comment.lines.shift();
  770. }
  771. // Remove trailing "```" lines
  772. // Remove trailing "```" lines if present
  773. if ( comment.lines[comment.lines.length - 1].startsWith('```') ) {
  774. comment.lines.pop();
  775. }
  776. comment.lines = dedent(comment.lines.join('\n')).split('\n');
  777. }
  778. inject_comments(lines, comments);
  779. console.log('--- lines ---');
  780. console.log(lines);
  781. // Check if file has metadata line and remove it before adding new metadata
  782. if ( has_metadata_line ) {
  783. lines.shift();
  784. }
  785. lines.unshift('// METADATA // ' + JSON.stringify({
  786. ...metadata,
  787. 'ai-commented': driver_params,
  788. }));
  789. // Write the modified file
  790. fs.writeFileSync(value.path, lines.join('\n'));
  791. }
  792. };
  793. main();