aicommit2
Version:
A Reactive CLI that generates commit messages for Git and Jujutsu with various AI
18 lines (13 loc) • 13 kB
JavaScript
import{of as L,Observable as _,Subject as T,catchError as B}from"rxjs";import"fs";import"path";import{xxh64 as P}from"@pacote/xxhash";import $ from"winston";import{A as J,l as p,D as F,g as U,d as D,s as b,h as z,e as q,E as H,f as K,i as R}from"./cli-8ee62906.mjs";const y=new Map,d=(f,r,e)=>{const s=P(0).update(r).digest("hex").substring(0,8),t=`${f}_${s}_${e}`;if(y.has(t))return y.get(t);const n=new Date,o=ee(n,f,r,e),c=`${J}/${o}`,i=$.createLogger({level:"info",format:$.format.combine($.format.timestamp({format:"YYYY-MM-DDTHH:mm:ss.SSSZ"}),$.format.printf(({timestamp:u,level:l,message:a,...m})=>m&&Object.keys(m).length>0?`[${u}] ${l}: ${a} ${JSON.stringify(m,null,2)}`:`[${u}] ${l}: ${a}`)),transports:[new $.transports.File({filename:c})]});return i.info(`=== ${f.toUpperCase()} AI SERVICE LOG ===`),i.info(`Diff Hash: ${s}`),i.info(`Request Type: ${e.toUpperCase()}`),i.info(`Start Time: ${n.toISOString()}`),i.info("=".repeat(50)),i.info(""),y.set(t,i),i},G=f=>{const r={...f},e=["authorization","x-api-key","x-goog-api-key","api-key","x-amzn-bedrock-application-key"];for(const s of e){const t=s.toLowerCase(),n=Object.keys(r).find(o=>o.toLowerCase()===t);n&&r[n]&&typeof r[n]=="string"&&(r[n].startsWith("Bearer ")?r[n]="Bearer [MASKED]":r[n]="[MASKED]")}return r},W=(f,r,e,s,t,n,o=!0)=>{if(!o)return;const c=d(e,f,r);c.info(`Making request to ${e} API with model: ${s}`),c.info(`Request URL: ${t}`),c.info("Request headers:",G(n))},Y=(f,r,e,s,t=!0)=>{if(!t)return;d(e,f,r).info("Request payload:",s)},V=(f,r,e,s,t,n=!0)=>{if(!n)return;const o=d(e,f,r);o.info("System prompt:",{prompt:s}),o.info("User prompt:",{prompt:t})},X=(f,r,e,s,t=!0)=>{if(!t)return;d(e,f,r).info("Response received:",s)},Q=(f,r,e,s,t=!0)=>{if(!t)return;d(e,f,r).error("API request failed:",s)},M=(f,r,e,s,t,n=!0)=>{if(!n)return;const o=d(e,f,r);s?o.info(`Request completed successfully in ${s}ms`):o.info("Request completed successfully"),t&&o.info("Final processed response:",{response:t}),o.info(""),o.info("=".repeat(50)),o.info(`End Time: ${new Date().toISOString()}`),o.info("=== REQUEST COMPLETED ===")},Z=(f,r,e,s,t,n,o,c=!0)=>{if(!c)return;const i=d(e,f,r);o?i.error(`Request failed after ${n}ms:`,{error:o}):(i.info(`Request completed in ${n}ms`),i.info("Response:",{response:t})),M(f,r,e,n,t,c)},ee=(f,r,e,s)=>{const{year:t,month:n,day:o,hours:c,minutes:i,seconds:u}=te(f),a=P(0).update(e).digest("hex").substring(0,8),m=r.toLowerCase().replace(/[^a-z0-9]/g,"").substring(0,20);return s==="review"?`${t}-${n}-${o}_${c}-${i}-${u}_${a}_${m}_review.log`:`${t}-${n}-${o}_${c}-${i}-${u}_${a}_${m}_commit.log`},te=f=>{const r=f.getFullYear().toString(),e=(f.getMonth()+1).toString().padStart(2,"0"),s=f.getDate().toString().padStart(2,"0"),t=f.getHours().toString().padStart(2,"0"),n=f.getMinutes().toString().padStart(2,"0"),o=f.getSeconds().toString().padStart(2,"0");return{year:r,month:e,day:s,hours:t,minutes:n,seconds:o}},w=()=>{for(const[f,r]of y.entries())try{r.close()}catch(e){console.error(`Failed to close logger ${f}:`,e)}y.clear()};process.on("exit",w),process.on("SIGINT",()=>{w(),process.exit(0)}),process.on("SIGTERM",()=>{w(),process.exit(0)});const se=["o1","o3","o4-mini","gpt-5","deepseek-v4-flash","deepseek-v4-pro","deepseek-reasoner","deepseek-r1","qwq","qwen3","phi4-mini-reasoning","smallthinker"],re=["gemini-2.5"],oe=f=>{const r=f.toLowerCase(),e=r.includes("/")&&r.split("/").pop()||r,s=e.includes(":")?e.split(":")[0]:e;return re.some(t=>s.includes(t))?!0:se.some(t=>s===t||s.startsWith(`${t}-`)||s.startsWith(`${t}.`))};class ne{constructor(){this.buffer="",this.arrayStartFound=!1,this.scanPosition=0,this.feed=r=>{this.buffer+=r;const e=[];if(!this.arrayStartFound){const s=this.buffer.indexOf("[");if(s===-1)return e;this.arrayStartFound=!0,this.scanPosition=s+1}for(;;){const s=this.buffer.indexOf("{",this.scanPosition);if(s===-1)break;const t=this.extractBalancedBraces(s);if(!t)break;this.scanPosition=s+t.length;const n=this.tryParseCommitMessage(t);n&&e.push(n)}return e},this.flush=()=>this.feed(""),this.getBuffer=()=>this.buffer,this.getUnparsedBuffer=()=>this.buffer.slice(this.scanPosition),this.extractBalancedBraces=r=>{let e=0,s=!1,t=!1;for(let n=r;n<this.buffer.length;n++){const o=this.buffer[n];if(t){t=!1;continue}if(o==="\\"&&s){t=!0;continue}if(o==='"'){s=!s;continue}if(!s&&(o==="{"&&e++,o==="}"&&e--,e===0))return this.buffer.slice(r,n+1)}return null},this.tryParseCommitMessage=r=>{try{const e=JSON.parse(r);return typeof e.subject!="string"?null:{subject:e.subject,body:typeof e.body=="string"?e.body:void 0,footer:typeof e.footer=="string"?e.footer:void 0}}catch{return null}}}}class ie{constructor(r){this.formatModelSuffix=()=>{const e=this.params.modelNameDisplay??"short",s=this.params.config.model;if(e==="none"||!s)return"";const t=Array.isArray(s)?s[0]:String(s);if(e==="full")return`/${t}`;const n=t.split("/").pop()||t;return n.length>20?`/${n.slice(0,19)}\u2026`:`/${n}`},this.handleError$=e=>{const s=this.getDetailedErrorMessage(e),t=e.status?`HTTP ${e.status}: ${s}`:s;if(this.params.config.logging){const n=this.params.stagedDiff.diff,o=this.serviceName.replace(/\[|\]/g,"").trim();Z(n,"commit",o,"Error occurred","",void 0,t)}return p.error(`${this.errorPrefix} ${t}`),e.stack&&p.error(` ${e.stack}`),e.content&&p.error(` Problematic content: ${e.content}`),e.originalError&&p.error(` Original error: ${e.originalError}`),L({name:`${this.errorPrefix} ${t}`,value:t,isError:!0,disabled:!0})},this.extractJsonObjectFromResponse=e=>{const s=e.indexOf("{");return s!==-1?this.extractBalancedJson(e,s,"{","}"):null},this.buildPromptOptions=()=>{const{systemPrompt:e,systemPromptPath:s,codeReviewPromptPath:t,locale:n,generate:o,type:c,maxLength:i,model:u}=this.params.config,l=Array.isArray(u)?u[0]||"":String(u||"");return{...F,locale:n,maxLength:i,type:c,generate:o,systemPrompt:e,systemPromptPath:s,codeReviewPromptPath:t,vcs_branch:this.params.branchName||"",isReasoning:oe(l)}},this.buildCommitPrompt=()=>U(this.buildPromptOptions()),this.buildUserPrompt=(e,s="commit")=>{const t={recentCommits:this.params.recentCommits,branchName:this.params.branchName};return D(e,s,t)},this.formatRawCommitMessage=(e,s)=>{const t=this.extractMessageAsType(e,s),n=t.subject,o=`${t.subject}${t.body?`
${t.body}`:""}${t.footer?`
${t.footer}`:""}`;return{title:n,value:o}},this.formatAsChoice=e=>({name:`${this.serviceName} ${e.title}`,short:e.title,value:this.params.config.includeBody?e.value:e.title,description:this.params.config.includeBody?e.value:"",isError:!1}),this.extractStreamPreview=e=>{const s=e.match(/"subject"\s*:\s*"((?:[^"\\]|\\.)*)(?:"|$)/),t=e.match(/"body"\s*:\s*"((?:[^"\\]|\\.)*)(?:"|$)/),n=s?s[1].replace(/\\"/g,'"').replace(/\\n/g,`
`):"",o=t?t[1].replace(/\\"/g,'"').replace(/\\n/g,`
`):"";return{subject:n,body:o}},this.createStreamingCommitMessages$=(e,s,t)=>{const n=`stream-${this.serviceName}-${crypto.randomUUID()}`;return new _(o=>{const c=new ne,i=new T;let u=0,l=!1,a=0;const m=100,A="streaming",C={name:"",value:"",streamKey:n,disabled:!0,isError:!1},I=(g=!1)=>{const h=Date.now();if(!g&&h-a<m)return;a=h;const S=c.getUnparsedBuffer();if(!S.trim())return;const{subject:v,body:x}=this.extractStreamPreview(S),O=v?`${this.serviceName} ${v}`:`${this.serviceName} Generating...`,j=x||v||"",k={name:O,short:v||"Generating...",value:`__streaming__${n}`,description:j,disabled:A,isError:!1,streamKey:n};o.next(k),l=!0},E=g=>{for(const h of g){if(u>=t)break;o.next(this.formatAsChoice(this.formatRawCommitMessage(h,s))),u++}},N=i.subscribe({next:g=>{if(u>=t)return;const h=c.feed(g);E(h),u<t&&I()},error:g=>{l&&o.next(C),o.error(g)},complete:()=>{E(c.flush()),l&&o.next(C);const g=c.getBuffer();if(u===0&&g.trim())try{const h=this.parseMessage(g,s,t);for(const S of h)o.next(this.formatAsChoice(S))}catch(h){o.error(h);return}o.complete()}});return e(i),()=>{N.unsubscribe()}}).pipe(B(this.handleError$))},this.parseCodeReview=e=>{const s=this.cleanJsonCodeBlock(e),t=this.extractJsonObjectFromResponse(s);if(!t)return p.warn(`${this.serviceName} Code review response did not contain JSON, falling back to plain text`),this.sanitizeResponse(e);const n=b(t);if(!n.ok)return p.warn(`${this.serviceName} Failed to parse code review JSON, falling back to plain text`),this.sanitizeResponse(e);const o=n.data;if(!(o&&typeof o.summary=="string"&&Array.isArray(o.items)))return p.warn(`${this.serviceName} Code review JSON missing summary or items, falling back to plain text`),this.sanitizeResponse(e);const i=o.items.filter(m=>typeof m.title=="string"&&typeof m.severity=="string"&&typeof m.description=="string"&&typeof m.category=="string"),u=i.some(m=>m.severity==="critical"),l=this.formatReviewSummaryTitle(o.summary,i),a=this.formatReviewAsMarkdown(o.summary,i,u);return this.isLoggingEnabled()&&p.info(`${this.serviceName} Parsed code review: ${i.length} items`),[{title:l,value:a}]},this.formatCodeReviewAsChoice=e=>({name:`${this.serviceName} ${e.title}`,short:e.title,value:e.value,description:e.value,isError:!1}),this.formatReviewSummaryTitle=(e,s)=>{const t=s.reduce((l,a)=>(l[a.severity]=(l[a.severity]??0)+1,l),{}),o=["critical","warning","suggestion","praise"].filter(l=>t[l]).map(l=>`${t[l]} ${l}`),c=o.length>0?` (${o.join(", ")})`:"",i=60;return`${e.length>i?`${e.slice(0,i-3)}...`:e}${c}`},this.formatReviewAsMarkdown=(e,s,t)=>{const o=[`## Summary
${e}${t?`
<!-- HAS_CRITICAL_ISSUES -->
`:""}
`],c=["critical","warning","suggestion","praise"];for(const i of c){const u=s.filter(l=>l.severity===i);if(u.length!==0){o.push(`### [${i.toUpperCase()}] ${i.charAt(0).toUpperCase()}${i.slice(1)}
`);for(const l of u){const a=l.file?` (${l.file}${l.line?`:${l.line}`:""})`:"";o.push(`- **[${l.category}]** ${l.title}${a}`),o.push(` ${l.description}`),l.suggestion&&o.push(` > Suggestion: ${l.suggestion}`),o.push("")}}}return o.join(`
`)},this.serviceName="AI",this.errorPrefix="ERROR",this.colors={primary:""},this.params=r,this.logSessionId=r.logSessionId}getProviderName(){const r=String.fromCharCode(27),e=new RegExp(`${r}\\[[0-9;]*m`,"g");return this.serviceName.replace(e,"").replace(/\[|\]/g,"").trim()}getDetailedErrorMessage(r){const e=r.message||"",s=this.getProviderName(),t=this.params.config.model?.[0],n=this.params.config.timeout,o=this.getServiceSpecificErrorMessage(r);if(o)return o;const c=r.code||(r.status?z(r.status):q(e));return c!==H.UNKNOWN?K(c,{provider:s,model:t,timeout:n}):e||"Unknown error occurred"}getServiceSpecificErrorMessage(r){return null}cleanJsonCodeBlock(r){const e=/```(?:json|JSON)?\s*([\s\S]*?)\s*```/,s=r.match(e);return s?s[1].trim():r}extractJsonFromResponse(r){const e=[],s=r.indexOf("[");s!==-1&&e.push({startIndex:s,openChar:"[",closeChar:"]"});const t=r.indexOf("{");t!==-1&&e.push({startIndex:t,openChar:"{",closeChar:"}"}),e.sort((n,o)=>n.startIndex-o.startIndex);for(const n of e){const o=this.extractBalancedJson(r,n.startIndex,n.openChar,n.closeChar);if(o&&b(o).ok)return o}return null}extractBalancedJson(r,e,s,t){let n=0,o=!1,c=!1;for(let i=e;i<r.length;i++){const u=r[i];if(c){c=!1;continue}if(u==="\\"&&o){c=!0;continue}if(u==='"'){o=!o;continue}if(!o&&(u===s&&n++,u===t&&n--,n===0))return r.slice(e,i+1)}return null}parseMessage(r,e,s){const t=this.cleanJsonCodeBlock(r),n=this.extractJsonFromResponse(t);if(!n){const a=new Error("AI response did not contain a valid JSON object or array.");throw a.name="InvalidJsonResponse",a.content=r,a}const o=b(n);if(!o.ok){const a=new Error("Failed to parse AI response as JSON");throw a.name="JsonParseError",a.content=n,a.originalError=o.error,a}const c=o.data,i=Array.isArray(c)?c:[c];if(!i.length||!i.every(a=>typeof a.subject=="string")){const a=new Error("AI response contained malformed commit message data.");throw a.name="MalformedCommitMessage",a.content=r,a}const l=i.map(a=>this.extractMessageAsType(a,e)).map(a=>({title:`${a.subject}`,value:`${a.subject}${a.body?`
${a.body}`:""}${a.footer?`
${a.footer}`:""}`})).slice(0,s);if(this.isLoggingEnabled()){const a=l.map(m=>m.title).join(", ");p.info(`${this.serviceName} Parsed ${l.length} commit messages: ${a}`)}return l}extractMessageAsType(r,e){switch(e){case"conventional":const s=/(\w+)(?:\(.*?\))?:\s*(.*)/,t=r.subject.match(s),n=t?t[0]:r.subject;return{...r,subject:this.normalizeCommitMessage(n)};case"gitmoji":const o=/:\w*:\s*(.*)/,c=r.subject.match(o),i=this.params.config.disableLowerCase??!1;return{...r,subject:c&&!i?c[0].toLowerCase():r.subject};default:return r}}normalizeCommitMessage(r){const e=/^(\w+)(\(.*?\))?:\s(.*)$/,s=r.match(e);if(s){const[,t,n,o]=s,c=this.params.config.disableLowerCase??!1,i=t.toLowerCase(),u=c?o:o.charAt(0).toLowerCase()+o.slice(1);r=`${i}${n||""}: ${u}`}return r}sanitizeResponse(r){if(typeof r=="string")try{return[{title:`${R(r)}...`,value:r}]}catch{return[]}return r.map(e=>{try{return{title:`${R(e)}...`,value:e}}catch{return{title:"",value:""}}})}isLoggingEnabled(){return this.params.config.logging&&!!this.logSessionId}}export{ie as A,V as a,Y as b,X as c,M as d,Q as e,W as l};