main.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. // METADATA // {"ai-params":{"service":"claude"},"comment-verbosity": "high","ai-commented":{"service":"claude"}}
  2. const enq = require('enquirer');
  3. const wrap = require('word-wrap');
  4. const dedent = require('dedent');
  5. const axios = require('axios');
  6. const { walk, EXCLUDE_LISTS } = require('../file-walker/test');
  7. const https = require('https');
  8. const fs = require('fs');
  9. const path_ = require('path');
  10. const FILE_EXCLUDES = [
  11. /^\.git/,
  12. /^node_modules\//,
  13. /\/node_modules$/,
  14. /^node_modules$/,
  15. /package-lock\.json/,
  16. /^src\/dev-center\/js/,
  17. /src\/backend\/src\/public\/assets/,
  18. /^src\/gui\/src\/lib/,
  19. /^eslint\.config\.js$/,
  20. ];
  21. const models_to_try = [
  22. {
  23. service: 'openai-completion',
  24. model: 'gpt-4o-mini',
  25. },
  26. {
  27. service: 'openai-completion',
  28. model: 'gpt-4o',
  29. },
  30. {
  31. service: 'claude',
  32. },
  33. {
  34. service: 'xai',
  35. },
  36. // llama broke code - that's a "one strike you're out" situation
  37. // {
  38. // service: 'together-ai',
  39. // model: 'meta-llama/Meta-Llama-3-70B-Instruct-Turbo',
  40. // },
  41. {
  42. service: 'mistral',
  43. model: 'mistral-large-latest',
  44. }
  45. ];
  46. const axi = axios.create({
  47. httpsAgent: new https.Agent({
  48. rejectUnauthorized: false
  49. })
  50. });
  51. const cwd = process.cwd();
  52. const context = {};
  53. context.config = JSON.parse(
  54. fs.readFileSync('config.json')
  55. );
  56. class AI {
  57. constructor (context) {
  58. //
  59. }
  60. async complete ({ messages, driver_params }) {
  61. const response = await axi.post(`${context.config.api_url}/drivers/call`, {
  62. interface: 'puter-chat-completion',
  63. method: 'complete',
  64. ...driver_params,
  65. args: {
  66. messages,
  67. },
  68. }, {
  69. headers: {
  70. "Content-Type": "application/json",
  71. Origin: 'https://puter.local',
  72. Authorization: `Bearer ${context.config.auth_token}`,
  73. },
  74. });
  75. return response.data.result.message;
  76. }
  77. }
  78. const ai_message_to_lines = text => {
  79. while ( typeof text === 'object' ) {
  80. if ( Array.isArray(text) ) text = text[0];
  81. else if ( text.content ) text = text.content;
  82. else if ( text.text ) text = text.text;
  83. else {
  84. console.log('Invalid message object', text);
  85. throw new Error('Invalid message object');
  86. }
  87. }
  88. return text.split('\n');
  89. }
  90. class CommentWriter {
  91. //
  92. }
  93. class JavascriptFileProcessor {
  94. constructor (context, parameters) {
  95. this.context = context;
  96. this.parameters = parameters;
  97. }
  98. process (lines) {
  99. const definitions = [];
  100. for ( let i = 0 ; i < lines.length ; i++ ) {
  101. const line = lines[i];
  102. for ( const matcher of this.parameters.definition_matchers ) {
  103. const match = matcher.pattern.exec(line);
  104. console.log('match object', match);
  105. if ( match ) {
  106. definitions.push({
  107. ...matcher.handler(match),
  108. line: i,
  109. });
  110. break;
  111. }
  112. }
  113. }
  114. return { definitions };
  115. }
  116. }
  117. const js_processor = new JavascriptFileProcessor(context, {
  118. definition_matchers: [
  119. // {
  120. // name: 'require',
  121. // pattern: /const (\w+) = require\(['"](.+)['"]\);/,
  122. // handler: (match) => {
  123. // const [ , name, path ] = match;
  124. // return {
  125. // type: 'require',
  126. // name,
  127. // path,
  128. // };
  129. // }
  130. // },
  131. {
  132. name: 'class',
  133. pattern: /class (\w+)(?: extends (.+))? {/,
  134. handler: (match) => {
  135. const [ , name, parent ] = match;
  136. return {
  137. type: 'class',
  138. name,
  139. parent,
  140. };
  141. }
  142. },
  143. {
  144. name: 'if',
  145. pattern: /^\s*if\s*\(.*\)\s*{/,
  146. handler: () => {
  147. return { type: 'if' };
  148. }
  149. },
  150. {
  151. name: 'while',
  152. pattern: /^\s*while\s*\(.*\)\s*{/,
  153. handler: () => {
  154. return { type: 'while' };
  155. }
  156. },
  157. {
  158. name: 'for',
  159. pattern: /^\s*for\s*\(.*\)\s*{/,
  160. handler: () => {
  161. return { type: 'for' };
  162. }
  163. },
  164. {
  165. name: 'method',
  166. pattern: /^\s*async .*\(.*\).*{/,
  167. handler: (match) => {
  168. const [ , name ] = match;
  169. return {
  170. async: true,
  171. type: 'method',
  172. name,
  173. };
  174. }
  175. },
  176. {
  177. name: 'method',
  178. pattern: /^\s*[A-Za-z_\$]+.*\(\).*{/,
  179. handler: (match) => {
  180. const [ , name ] = match;
  181. if ( name === 'if' ) {
  182. return { type: 'if' };
  183. }
  184. if ( name === 'while' ) {
  185. return { type: 'while' };
  186. }
  187. return {
  188. type: 'method',
  189. name,
  190. };
  191. }
  192. },
  193. {
  194. name: 'function',
  195. pattern: /^\s*function .*\(.*\).*{/,
  196. handler: (match) => {
  197. const [ , name ] = match;
  198. return {
  199. type: 'function',
  200. scope: 'function',
  201. name,
  202. };
  203. }
  204. },
  205. {
  206. name: 'function',
  207. pattern: /const [A-Za-z_]+\s*=\s*\(.*\)\s*=>\s*{/,
  208. handler: (match) => {
  209. const [ , name, args ] = match;
  210. return {
  211. type: 'function',
  212. scope: 'lexical',
  213. name,
  214. args: (args ?? '').split(',').map(arg => arg.trim()),
  215. };
  216. }
  217. },
  218. {
  219. name: 'const',
  220. // pattern to match only uppercase-lettered variable names
  221. pattern: /const ([A-Z_]+) = (.+);/,
  222. handler: (match) => {
  223. const [ , name, value ] = match;
  224. return {
  225. type: 'const',
  226. name,
  227. value,
  228. };
  229. }
  230. }
  231. ],
  232. });
  233. const create_limited_view = (lines, key_places) => {
  234. // Sort key places by starting line
  235. key_places.sort((a, b) => {
  236. const a_start = Math.max(0, a.anchor - a.lines_above);
  237. const b_start = Math.max(0, b.anchor - b.lines_above);
  238. return a_start - b_start;
  239. });
  240. const visible_ranges = [];
  241. // Create visible ranges for each key place
  242. for ( const key_place of key_places ) {
  243. const anchor = key_place.anchor;
  244. const lines_above = key_place.lines_above;
  245. const lines_below = key_place.lines_below;
  246. const start = Math.max(0, anchor - lines_above);
  247. const end = Math.min(lines.length, anchor + lines_below);
  248. visible_ranges.push({
  249. anchor: key_place.anchor,
  250. comment: key_place.comment,
  251. start,
  252. end,
  253. });
  254. }
  255. // Merge overlapping visible ranges
  256. const merged_ranges = [];
  257. for ( const range of visible_ranges ) {
  258. range.comments = [{
  259. anchor: range.anchor,
  260. text: range.comment
  261. }];
  262. if ( ! merged_ranges.length ) {
  263. merged_ranges.push(range);
  264. continue;
  265. }
  266. const last_range = merged_ranges[merged_ranges.length - 1];
  267. if ( last_range.end >= range.start ) {
  268. last_range.end = Math.max(last_range.end, range.end);
  269. last_range.comments.push({
  270. anchor: range.anchor,
  271. text: range.comment
  272. });
  273. } else {
  274. merged_ranges.push(range);
  275. }
  276. }
  277. // Create the limited view, adding line numbers and comments
  278. let limited_view = '';
  279. let previous_visible_range = null;
  280. for ( let i = 0 ; i < lines.length ; i++ ) {
  281. const line = lines[i];
  282. let visible_range = null;
  283. if ( i === 22 ) debugger;
  284. for ( const range of merged_ranges ) {
  285. if ( i >= range.start && i < range.end ) {
  286. visible_range = range;
  287. break;
  288. }
  289. }
  290. // console.log('visible_range', visible_range, i);
  291. if ( visible_range === null ) {
  292. continue;
  293. }
  294. if ( visible_range !== previous_visible_range ) {
  295. if ( i !== 0 ) limited_view += '\n';
  296. if ( visible_range.comments.length === 1 ) {
  297. const comment = visible_range.comments[0];
  298. limited_view += `window around line ${comment.anchor}: ${comment.text}\n`;
  299. } else {
  300. limited_view += `window around lines ${visible_range.comments.length} key lines:\n`;
  301. for ( const comment of visible_range.comments ) {
  302. limited_view += `- line ${comment.anchor}: ${comment.text}\n`;
  303. }
  304. }
  305. }
  306. previous_visible_range = visible_range;
  307. limited_view += `${i + 1}: ${line}\n`;
  308. }
  309. return limited_view;
  310. };
  311. /**
  312. * Inject comments into lines
  313. * @param {*} lines - Array of original file lines
  314. * @param {*} comments - Array of comment objects
  315. *
  316. * Comment object structure:
  317. * {
  318. * position: 0, // Position in lines array
  319. * lines: [ 'comment line 1', 'comment line 2', ... ]
  320. * }
  321. */
  322. const inject_comments = (lines, comments) => {
  323. // Sort comments in reverse order
  324. comments.sort((a, b) => b.position - a.position);
  325. // Inject comments into lines
  326. for ( const comment of comments ) {
  327. // AI might have been stupid and added a comment above a blank line,
  328. // despite that we told it not to do that. So we need to adjust the position.
  329. while ( comment.position < lines.length && ! lines[comment.position].trim() ) {
  330. comment.position++;
  331. }
  332. const indentation = lines[comment.position].match(/^\s*/)[0];
  333. console.log('????', comment.position, lines[comment.position], '|' + indentation + '|');
  334. const comment_lines = comment.lines.map(line => `${indentation}${line}`);
  335. lines.splice(comment.position, 0, ...comment_lines);
  336. // If the first line of the comment lines starts with '/*`, ensure there is
  337. // a blank line above it.
  338. if ( comment_lines[0].trim().startsWith('/*') ) {
  339. if ( comment.position > 0 && lines[comment.position - 1].trim() === '' ) {
  340. lines.splice(comment.position, 0, '');
  341. }
  342. }
  343. }
  344. }
  345. const textutil = {};
  346. textutil.format = text => {
  347. return wrap(dedent(text), {
  348. width: 80,
  349. indent: '| '
  350. });
  351. };
  352. context.ai = new AI(context);
  353. const main = async () => {
  354. // const message = await context.ai.complete({
  355. // messages: [
  356. // {
  357. // role: 'user',
  358. // content: `
  359. // Introduce yourself as the Puter Comment Writer. You are an AI that will
  360. // write comments in code files. A file walker will be used to iterate over
  361. // the source files and present them one at a time, and the user will accept,
  362. // reject, or request edits interactively. For each new file, a clean AI
  363. // context will be created.
  364. // `.trim()
  365. // }
  366. // ]
  367. // });
  368. // const intro = message.content;
  369. const intro = textutil.format(`
  370. 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!
  371. `);
  372. console.log(intro);
  373. console.log(`Enter a path relative to: ${process.cwd()}`);
  374. console.log('arg?', process.argv[2]);
  375. let rootpath = process.argv[2] ? { path: process.argv[2] } : await enq.prompt({
  376. type: 'input',
  377. name: 'path',
  378. message: 'Enter path:'
  379. });
  380. rootpath = path_.resolve(rootpath.path);
  381. console.log('rootpath:', rootpath);
  382. const walk_iter = walk({
  383. excludes: FILE_EXCLUDES,
  384. }, rootpath);
  385. let i = 0;
  386. for await ( const value of walk_iter ) {
  387. i++;
  388. if ( i == 12 ) process.exit(0);
  389. if ( value.is_dir ) {
  390. console.log('directory:', value.path);
  391. continue;
  392. }
  393. if ( ! value.name.endsWith('.js') ) {
  394. continue;
  395. }
  396. console.log('file:', value.path);
  397. const lines = fs.readFileSync(value.path, 'utf8').split('\n');
  398. let metadata, has_metadata_line = false;
  399. if ( lines[0].startsWith('// METADATA // ') ) {
  400. has_metadata_line = true;
  401. metadata = JSON.parse(lines[0].slice('// METADATA // '.length));
  402. if ( metadata['ai-commented'] ) {
  403. console.log('File was already commented by AI; skipping...');
  404. continue;
  405. }
  406. }
  407. let refs = null;
  408. if ( metadata['ai-refs'] ) {
  409. const relative_file_paths = metadata['ai-refs'];
  410. // name of file is the key, value is the contents
  411. const references = {};
  412. let n = 0;
  413. for ( const relative_file_path of relative_file_paths ) {
  414. n++;
  415. const full_path = path_.join(path_.dirname(value.path), relative_file_path);
  416. const ref_text = fs.readFileSync(full_path, 'utf8');
  417. references[relative_file_path] = ref_text;
  418. }
  419. if ( n === 1 ) {
  420. refs = dedent(`
  421. The following documentation contains relevant information about the code.
  422. The code will follow after this documentation.
  423. `);
  424. refs += '\n\n' + dedent(references[Object.keys(references)[0]]);
  425. } else if ( n > 2 ) {
  426. refs = dedent(`
  427. The following documentation contains relevant information about the code.
  428. The code will follow after a number of documentation files.
  429. `);
  430. for ( const key of Object.keys(references) ) {
  431. refs += '\n\n' + dedent(references[key]);
  432. }
  433. }
  434. }
  435. const action = await enq.prompt({
  436. type: 'select',
  437. name: 'action',
  438. message: 'Select action:',
  439. choices: [
  440. 'generate',
  441. 'skip',
  442. 'exit',
  443. ]
  444. })
  445. // const action = 'generate';
  446. if ( action.action === 'exit' ) {
  447. break;
  448. }
  449. if ( action.action === 'skip' ) {
  450. continue;
  451. }
  452. const { definitions } = js_processor.process(lines);
  453. const key_places = [];
  454. key_places.push({
  455. anchor: 0,
  456. lines_above: 2,
  457. lines_below: 200,
  458. comment: `Top of file: ${value.path}`
  459. });
  460. key_places.push({
  461. anchor: lines.length - 1,
  462. lines_above: 200,
  463. lines_below: 2,
  464. comment: `Bottom of ${value.name}`
  465. });
  466. for ( const definition of definitions ) {
  467. key_places.push({
  468. anchor: definition.line,
  469. lines_above: 40,
  470. lines_below: 40,
  471. comment: `${definition.type}.`
  472. });
  473. }
  474. let limited_view = create_limited_view(lines, key_places);
  475. console.log('--- view ---');
  476. console.log(limited_view);
  477. const comments = [];
  478. // comments.push({
  479. // position: 0,
  480. // });
  481. // for ( const definition of definitions ) {
  482. // comments.push({
  483. // position: definition.line,
  484. // definition,
  485. // });
  486. // }
  487. // This was worth a try but the LLM is very bad at this
  488. /*
  489. const message = await context.ai.complete({
  490. messages: [
  491. {
  492. role: 'user',
  493. content: dedent(`
  494. Respond with comma-separated numbers only, with no surrounding text.
  495. Please write the numbers of the lines above which a comment should be added.
  496. Do not include numbers of lines that are blank, already have comments, or are part of a comment.
  497. Prefer comment locations in a higher level scope, such as a classes, function definitions and class methods,
  498. `).trim() + '\n\n' + limited_view
  499. }
  500. ]
  501. });
  502. const numbers = message.content.split(',').map(n => Number(n));
  503. for ( const n of numbers ) {
  504. if ( Number.isNaN(n) ) {
  505. console.log('Invalid number:', n);
  506. continue;
  507. }
  508. comments.push({
  509. position: n - 1,
  510. });
  511. }
  512. */
  513. for ( const def of definitions ) {
  514. console.log('def?', def);
  515. let instruction = '';
  516. if ( def.type === 'class' ) {
  517. instruction = dedent(`
  518. Since the comment is going above a class definition, please write a JSDoc style comment.
  519. Make the comment as descriptive as possible, including the class name and its purpose.
  520. `);
  521. }
  522. if ( def.type === 'if' || def.type === 'while' || def.type === 'for' ) {
  523. if ( metadata['comment-verbosity'] !== 'high' ) continue;
  524. instruction = dedent(`
  525. Since the comment is going above a control structure, please write a short concise comment.
  526. The comment should be only one or two lines long, and should use line comments.
  527. `);
  528. }
  529. if ( def.type === 'method' ) {
  530. instruction = dedent(`
  531. Since the comment is going above a method, please write a JSDoc style comment.
  532. The comment should include a short concise description of the method's purpose,
  533. notes about its behavior, and any parameters or return values.
  534. `);
  535. }
  536. if ( def.type === 'const' ) {
  537. instruction = dedent(`
  538. Since the comment is going above a constant definition, please write a comment that explains
  539. the purpose of the constant and how it is used in the code.
  540. The comment should be only one or two lines long, and should use line comments.
  541. `);
  542. }
  543. comments.push({
  544. position: def.line,
  545. instruction: instruction,
  546. });
  547. }
  548. const driver_params = metadata['ai-params'] ??
  549. models_to_try[Math.floor(Math.random() * models_to_try.length)];
  550. for ( const comment of comments ) {
  551. // This doesn't work very well yet
  552. /*
  553. const ranges_message = await context.ai.complete({
  554. messages: [
  555. {
  556. role: 'user',
  557. content: dedent(`
  558. Please only respond with comma-separated number ranges in this format with no surrounding text:
  559. 11-21, 25-30, 35-40
  560. You may also respond with "none".
  561. A comment will be added above line ${comment.position} in the code which follows.
  562. You are seeing a limited view of the code that includes chunks around interesting lines.
  563. Please specify ranges of lines that might provide useful context for this comment.
  564. Do not include in any range lines which are already visible in the limited view.
  565. Avoid specifying more than a couple hundred lines.
  566. `).trim() + '\n\n' + limited_view
  567. }
  568. ]
  569. });
  570. if ( ranges_message.content.trim() !== 'none' ) {
  571. const ranges = ranges_message.content.split(',').map(range => {
  572. const [ start, end ] = range.split('-').map(n => Number(n));
  573. return { start, end };
  574. });
  575. for ( const range of ranges ) {
  576. key_places.push({
  577. anchor: range.start,
  578. lines_above: 0,
  579. lines_below: range.end - range.start,
  580. comment: `Requested range by AI agent: ${range.start}-${range.end}`
  581. });
  582. }
  583. limited_view = create_limited_view(lines, key_places);
  584. console.log('--- updated view ---');
  585. console.log(limited_view);
  586. }
  587. */
  588. const prompt =
  589. dedent(`
  590. Please write a comment to be added above line ${comment.position}.
  591. Do not write any surrounding text; just the comment itself.
  592. Please include comment markers. If the comment is on a class, function, or method, please use jsdoc style.
  593. The code is written in JavaScript.
  594. `).trim() +
  595. (refs ? '\n\n' + dedent(refs) : '') +
  596. (comment.instruction ? '\n\n' + dedent(comment.instruction) : '') +
  597. '\n\n' + limited_view
  598. ;
  599. // console.log('prompt:', prompt);
  600. const message = await context.ai.complete({
  601. messages: [
  602. {
  603. role: 'user',
  604. content: prompt
  605. }
  606. ],
  607. driver_params,
  608. });
  609. console.log('message:', message);
  610. comment.lines = ai_message_to_lines(message.content);
  611. // Remove leading and trailing blank lines
  612. while ( comment.lines.length && ! comment.lines[0].trim() ) {
  613. comment.lines.shift();
  614. }
  615. while ( comment.lines.length && ! comment.lines[comment.lines.length - 1].trim() ) {
  616. comment.lines.pop();
  617. }
  618. // Remove leading "```" or "```<language>" lines
  619. if ( comment.lines[0].startsWith('```') ) {
  620. comment.lines.shift();
  621. }
  622. // Remove trailing "```" lines
  623. if ( comment.lines[comment.lines.length - 1].startsWith('```') ) {
  624. comment.lines.pop();
  625. }
  626. comment.lines = dedent(comment.lines.join('\n')).split('\n');
  627. }
  628. inject_comments(lines, comments);
  629. console.log('--- lines ---');
  630. console.log(lines);
  631. if ( has_metadata_line ) {
  632. lines.shift();
  633. }
  634. lines.unshift('// METADATA // ' + JSON.stringify({
  635. ...metadata,
  636. 'ai-commented': driver_params,
  637. }));
  638. // Write the modified file
  639. fs.writeFileSync(value.path, lines.join('\n'));
  640. }
  641. };
  642. main();