aicommit2 2.5.5 → 2.5.7

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.
Files changed (32) hide show
  1. package/README.md +79 -1
  2. package/dist/ai.service-da8345b1.mjs +17 -0
  3. package/dist/{anthropic.service-d0af34bb.mjs → anthropic.service-bff34bb1.mjs} +1 -1
  4. package/dist/bedrock.service-419a856c.mjs +1 -0
  5. package/dist/cli-83e0874c.mjs +273 -0
  6. package/dist/cli.mjs +1 -1
  7. package/dist/codestral.service-59a12e78.mjs +1 -0
  8. package/dist/cohere.service-80e89971.mjs +1 -0
  9. package/dist/{deep-seek.service-f1fce159.mjs → deep-seek.service-86125741.mjs} +1 -1
  10. package/dist/{gemini.service-ea4399b1.mjs → gemini.service-2c3b0cd4.mjs} +1 -1
  11. package/dist/github-models.service-37847903.mjs +5 -0
  12. package/dist/{groq.service-b7d23bbc.mjs → groq.service-5cf5b8c9.mjs} +1 -1
  13. package/dist/hugging-face.service-966de0cf.mjs +2 -0
  14. package/dist/mistral.service-3389f888.mjs +1 -0
  15. package/dist/ollama.service-c5b469be.mjs +1 -0
  16. package/dist/{openai-8b372df6.mjs → openai-3092588f.mjs} +1 -1
  17. package/dist/{openai-compatible.service-bf183fc9.mjs → openai-compatible.service-599ec5d0.mjs} +1 -1
  18. package/dist/openai.service-df984ad9.mjs +1 -0
  19. package/dist/openrouter.service-107eec99.mjs +1 -0
  20. package/dist/perplexity.service-eda88944.mjs +1 -0
  21. package/package.json +1 -1
  22. package/dist/ai.service-6f818099.mjs +0 -11
  23. package/dist/bedrock.service-7f01f1d1.mjs +0 -1
  24. package/dist/cli-9533dfd6.mjs +0 -297
  25. package/dist/codestral.service-ccd13cd7.mjs +0 -1
  26. package/dist/cohere.service-e72f068e.mjs +0 -1
  27. package/dist/github-models.service-16ce699f.mjs +0 -5
  28. package/dist/hugging-face.service-760afbf2.mjs +0 -2
  29. package/dist/mistral.service-fe74f317.mjs +0 -1
  30. package/dist/ollama.service-3312d7f0.mjs +0 -1
  31. package/dist/openai.service-1d3ec4cc.mjs +0 -1
  32. package/dist/perplexity.service-85ac5631.mjs +0 -1
package/README.md CHANGED
@@ -38,6 +38,7 @@ ______________________________________________________________________
38
38
  - [General Settings](#general-settings)
39
39
  - [Logging](#logging)
40
40
  - [Custom Prompt Template](#custom-prompt-template)
41
+ - [Code Review](#code-review)
41
42
  - [Watch Commit Mode](#watch-commit-mode)
42
43
  - [Upgrading](#upgrading)
43
44
  - [Contributing](#contributing)
@@ -78,6 +79,7 @@ _aicommit2_ automatically generates commit messages using AI. It supports [Git](
78
79
  - **[Multi-AI Support](#cloud-ai-services)**: Integrates with OpenAI, Anthropic Claude, Google Gemini, Mistral AI, Cohere, Groq, Ollama and more
79
80
  - **[OpenAI API Compatibility](docs/providers/compatible.md)**: Support for any service that implements the OpenAI API specification
80
81
  - **[Reactive CLI](#usage)**: Enables simultaneous requests to multiple AIs and selection of the best commit message
82
+ - **[Code Review](#code-review)**: AI-powered structured code review with severity levels before committing
81
83
  - **[Git Hook Integration](#git-hooks)**: Can be used as a prepare-commit-msg hook
82
84
  - **[Custom Prompt](#custom-prompt-template)**: Supports user-defined system prompt templates
83
85
 
@@ -86,6 +88,7 @@ _aicommit2_ automatically generates commit messages using AI. It supports [Git](
86
88
  | Provider | Default Model | Documentation |
87
89
  |----------|---------------|---------------|
88
90
  | OpenAI | `gpt-4o-mini` | [Guide](docs/providers/openai.md) |
91
+ | OpenRouter | `openrouter/auto` | [Guide](docs/providers/openrouter.md) |
89
92
  | Anthropic | `claude-sonnet-4-20250514` | [Guide](docs/providers/anthropic.md) |
90
93
  | Gemini | `gemini-3-flash-preview` | [Guide](docs/providers/gemini.md) |
91
94
  | Mistral | `mistral-small-latest` | [Guide](docs/providers/mistral.md) |
@@ -747,6 +750,31 @@ key=$OPENAI_API_KEY
747
750
  url=${CUSTOM_API_URL}/v1
748
751
  ```
749
752
 
753
+ OpenRouter example:
754
+
755
+ ```ini
756
+ logging=true
757
+ generate=1
758
+ locale=ru
759
+ maxTokens=4096
760
+ temperature=0.2
761
+
762
+ [OPENROUTER]
763
+ envKey=OPENROUTER_BASE_TOKEN
764
+ model=stepfun/step-3.5-flash:free
765
+ url=https://openrouter.ai
766
+ path=/api/v1/chat/completions
767
+ systemPromptPath=prompts/aicommit_prompt.txt
768
+ responseFormat.type=json_object
769
+ provider.allow_fallbacks=true
770
+ provider.require_parameters=false
771
+ ```
772
+
773
+ If `systemPromptPath` is relative, it is resolved relative to the config file
774
+ location.
775
+ Nested OpenRouter objects such as `responseFormat` and `provider` can be written
776
+ directly in `config.ini` using dotted keys, or set with JSON via `aicommit2 config set`.
777
+
750
778
  ### Reading and Setting Configuration
751
779
 
752
780
  - READ: `aicommit2 config get [<key> [<key> ...]]`
@@ -887,7 +915,8 @@ For detailed information about all available settings, see the [General Settings
887
915
  | `temperature` | Model's creativity (0.0 - 2.0) | 0.7 |
888
916
  | `maxTokens` | Maximum number of tokens to generate | 1024 |
889
917
  | `includeBody` | Whether the commit message includes body | false |
890
- | `codeReview` | Enable automated code review | false |
918
+ | `codeReview` | Enable automated code review before commit | false |
919
+ | `codeReviewPromptPath` | Path to custom code review prompt file | - |
891
920
  | `autoCopy` | Auto-copy commit message to clipboard (commits normally) | false |
892
921
  | `useStats` | Enable usage statistics tracking | true |
893
922
  | `statsDays` | Days to retain statistics data (auto-cleanup) | 30 |
@@ -1059,6 +1088,55 @@ The response should be valid JSON that can be parsed without errors.
1059
1088
 
1060
1089
  This ensures that the output is consistently formatted as a JSON array, regardless of the custom template used.
1061
1090
 
1091
+ ## Code Review
1092
+
1093
+ aicommit2 includes an AI-powered code review feature that analyzes your staged changes before generating commit messages. When enabled, it provides structured feedback with severity levels and actionable suggestions.
1094
+
1095
+ ### Enable Code Review
1096
+
1097
+ ```bash
1098
+ # Enable globally
1099
+ aicommit2 config set codeReview=true
1100
+
1101
+ # Or enable for specific providers only
1102
+ aicommit2 config set OPENAI.codeReview=true
1103
+ aicommit2 config set ANTHROPIC.codeReview=true
1104
+ ```
1105
+
1106
+ ### How It Works
1107
+
1108
+ When `codeReview` is enabled, the commit flow becomes:
1109
+
1110
+ 1. **Stage changes** (`git add`)
1111
+ 2. **Run aicommit2** — code review runs automatically before commit message generation
1112
+ 3. **Review results** — AI analyzes the diff and returns structured feedback
1113
+ 4. **Confirm or abort** — choose to continue with the commit or fix issues first
1114
+ 5. **Generate commit messages** — proceeds as normal after confirmation
1115
+
1116
+ ### Structured Review Output
1117
+
1118
+ Reviews are organized by severity and category:
1119
+
1120
+ - **Severity levels**: `critical`, `warning`, `suggestion`, `praise`
1121
+ - **Categories**: `bug`, `security`, `performance`, `style`, `maintainability`, `other`
1122
+
1123
+ Each review item includes a title, description, file reference, and concrete suggestion for improvement. When critical issues are found, the confirmation prompt defaults to "No" to encourage fixing before committing.
1124
+
1125
+ ### Custom Review Prompt
1126
+
1127
+ You can customize the code review prompt using a template file:
1128
+
1129
+ ```bash
1130
+ aicommit2 config set codeReviewPromptPath="/path/to/review-prompt.txt"
1131
+ aicommit2 config set OPENAI.codeReviewPromptPath="/path/to/another-prompt.txt"
1132
+ ```
1133
+
1134
+ The template supports the same `{locale}`, `{type}`, `{generate}`, `{maxLength}` placeholders as the commit prompt.
1135
+
1136
+ > **NOTE**: When using a custom review prompt, the response format is plain text (not structured JSON). The structured severity/category output is only available with the default prompt.
1137
+
1138
+ > **WARNING**: Code review runs **in addition to** commit message generation, which means **API token usage roughly doubles** per commit. If multiple providers have `codeReview` enabled, each provider performs its own review. Monitor your token usage carefully, especially with large diffs.
1139
+
1062
1140
  ## Watch Commit Mode
1063
1141
 
1064
1142
  ![watch-commit-gif](https://github.com/tak-bro/aicommit2/blob/main/img/watch-commit-min.gif?raw=true)
@@ -0,0 +1,17 @@
1
+ import{of as L,Observable as _,Subject as J,catchError as B}from"rxjs";import"fs";import"path";import{xxh64 as M}from"@pacote/xxhash";import $ from"winston";import{A as T,l as p,D as F,g as D,s as b,h as U,d as z,E as H,e as q,f as P}from"./cli-83e0874c.mjs";const y=new Map,d=(f,s,e)=>{const o=M(0).update(s).digest("hex").substring(0,8),r=`${f}_${o}_${e}`;if(y.has(r))return y.get(r);const n=new Date,t=Z(n,f,s,e),c=`${T}/${t}`,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: ${o}`),i.info(`Request Type: ${e.toUpperCase()}`),i.info(`Start Time: ${n.toISOString()}`),i.info("=".repeat(50)),i.info(""),y.set(r,i),i},K=f=>{const s={...f},e=["authorization","x-api-key","x-goog-api-key","api-key","x-amzn-bedrock-application-key"];for(const o of e){const r=o.toLowerCase(),n=Object.keys(s).find(t=>t.toLowerCase()===r);n&&s[n]&&typeof s[n]=="string"&&(s[n].startsWith("Bearer ")?s[n]="Bearer [MASKED]":s[n]="[MASKED]")}return s},Y=(f,s,e,o,r,n,t=!0)=>{if(!t)return;const c=d(e,f,s);c.info(`Making request to ${e} API with model: ${o}`),c.info(`Request URL: ${r}`),c.info("Request headers:",K(n))},V=(f,s,e,o,r=!0)=>{if(!r)return;d(e,f,s).info("Request payload:",o)},W=(f,s,e,o,r,n=!0)=>{if(!n)return;const t=d(e,f,s);t.info("System prompt:",{prompt:o}),t.info("User prompt:",{prompt:r})},G=(f,s,e,o,r=!0)=>{if(!r)return;d(e,f,s).info("Response received:",o)},Q=(f,s,e,o,r=!0)=>{if(!r)return;d(e,f,s).error("API request failed:",o)},R=(f,s,e,o,r,n=!0)=>{if(!n)return;const t=d(e,f,s);o?t.info(`Request completed successfully in ${o}ms`):t.info("Request completed successfully"),r&&t.info("Final processed response:",{response:r}),t.info(""),t.info("=".repeat(50)),t.info(`End Time: ${new Date().toISOString()}`),t.info("=== REQUEST COMPLETED ===")},X=(f,s,e,o,r,n,t,c=!0)=>{if(!c)return;const i=d(e,f,s);t?i.error(`Request failed after ${n}ms:`,{error:t}):(i.info(`Request completed in ${n}ms`),i.info("Response:",{response:r})),R(f,s,e,n,r,c)},Z=(f,s,e,o)=>{const{year:r,month:n,day:t,hours:c,minutes:i,seconds:u}=ee(f),a=M(0).update(e).digest("hex").substring(0,8),m=s.toLowerCase().replace(/[^a-z0-9]/g,"").substring(0,20);return o==="review"?`${r}-${n}-${t}_${c}-${i}-${u}_${a}_${m}_review.log`:`${r}-${n}-${t}_${c}-${i}-${u}_${a}_${m}_commit.log`},ee=f=>{const s=f.getFullYear().toString(),e=(f.getMonth()+1).toString().padStart(2,"0"),o=f.getDate().toString().padStart(2,"0"),r=f.getHours().toString().padStart(2,"0"),n=f.getMinutes().toString().padStart(2,"0"),t=f.getSeconds().toString().padStart(2,"0");return{year:s,month:e,day:o,hours:r,minutes:n,seconds:t}},w=()=>{for(const[f,s]of y.entries())try{s.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)});class te{constructor(){this.buffer="",this.arrayStartFound=!1,this.scanPosition=0,this.feed=s=>{this.buffer+=s;const e=[];if(!this.arrayStartFound){const o=this.buffer.indexOf("[");if(o===-1)return e;this.arrayStartFound=!0,this.scanPosition=o+1}for(;;){const o=this.buffer.indexOf("{",this.scanPosition);if(o===-1)break;const r=this.extractBalancedBraces(o);if(!r)break;this.scanPosition=o+r.length;const n=this.tryParseCommitMessage(r);n&&e.push(n)}return e},this.flush=()=>this.feed(""),this.getBuffer=()=>this.buffer,this.getUnparsedBuffer=()=>this.buffer.slice(this.scanPosition),this.extractBalancedBraces=s=>{let e=0,o=!1,r=!1;for(let n=s;n<this.buffer.length;n++){const t=this.buffer[n];if(r){r=!1;continue}if(t==="\\"&&o){r=!0;continue}if(t==='"'){o=!o;continue}if(!o&&(t==="{"&&e++,t==="}"&&e--,e===0))return this.buffer.slice(s,n+1)}return null},this.tryParseCommitMessage=s=>{try{const e=JSON.parse(s);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 se{constructor(s){this.handleError$=e=>{const o=this.getDetailedErrorMessage(e),r=e.status?`HTTP ${e.status}: ${o}`:o;if(this.params.config.logging){const n=this.params.stagedDiff.diff,t=this.serviceName.replace(/\[|\]/g,"").trim();X(n,"commit",t,"Error occurred","",void 0,r)}return p.error(`${this.errorPrefix} ${r}`),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} ${r}`,value:r,isError:!0,disabled:!0})},this.extractJsonObjectFromResponse=e=>{const o=e.indexOf("{");return o!==-1?this.extractBalancedJson(e,o,"{","}"):null},this.buildCommitPrompt=()=>{const{systemPrompt:e,systemPromptPath:o,codeReviewPromptPath:r,locale:n,generate:t,type:c,maxLength:i}=this.params.config,u={...F,locale:n,maxLength:i,type:c,generate:t,systemPrompt:e,systemPromptPath:o,codeReviewPromptPath:r,vcs_branch:this.params.branchName||""};return D(u)},this.formatRawCommitMessage=(e,o)=>{const r=this.extractMessageAsType(e,o),n=r.subject,t=`${r.subject}${r.body?`
2
+
3
+ ${r.body}`:""}${r.footer?`
4
+
5
+ ${r.footer}`:""}`;return{title:n,value:t}},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 o=e.match(/"subject"\s*:\s*"((?:[^"\\]|\\.)*)(?:"|$)/),r=e.match(/"body"\s*:\s*"((?:[^"\\]|\\.)*)(?:"|$)/),n=o?o[1].replace(/\\"/g,'"').replace(/\\n/g,`
6
+ `):"",t=r?r[1].replace(/\\"/g,'"').replace(/\\n/g,`
7
+ `):"";return{subject:n,body:t}},this.createStreamingCommitMessages$=(e,o,r)=>{const n=`stream-${this.serviceName}-${Date.now()}`;return new _(t=>{const c=new te,i=new J;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 v=c.getUnparsedBuffer();if(!v.trim())return;const{subject:S,body:j}=this.extractStreamPreview(v),O=S?`${this.serviceName} ${S}`:`${this.serviceName} Generating...`,N=j||S||"",k={name:O,short:S||"Generating...",value:`__streaming__${n}`,description:N,disabled:A,isError:!1,streamKey:n};t.next(k),l=!0},E=g=>{for(const h of g){if(u>=r)break;t.next(this.formatAsChoice(this.formatRawCommitMessage(h,o))),u++}},x=i.subscribe({next:g=>{if(u>=r)return;const h=c.feed(g);E(h),u<r&&I()},error:g=>{l&&t.next(C),t.error(g)},complete:()=>{E(c.flush()),l&&t.next(C);const g=c.getBuffer();if(u===0&&g.trim())try{const h=this.parseMessage(g,o,r);for(const v of h)t.next(this.formatAsChoice(v))}catch(h){t.error(h);return}t.complete()}});return e(i),()=>{x.unsubscribe()}}).pipe(B(this.handleError$))},this.parseCodeReview=e=>{const o=this.cleanJsonCodeBlock(e),r=this.extractJsonObjectFromResponse(o);if(!r)return p.warn(`${this.serviceName} Code review response did not contain JSON, falling back to plain text`),this.sanitizeResponse(e);const n=b(r);if(!n.ok)return p.warn(`${this.serviceName} Failed to parse code review JSON, falling back to plain text`),this.sanitizeResponse(e);const t=n.data;if(!(t&&typeof t.summary=="string"&&Array.isArray(t.items)))return p.warn(`${this.serviceName} Code review JSON missing summary or items, falling back to plain text`),this.sanitizeResponse(e);const i=t.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(t.summary,i),a=this.formatReviewAsMarkdown(t.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,o)=>{const r=o.reduce((l,a)=>(l[a.severity]=(l[a.severity]??0)+1,l),{}),t=["critical","warning","suggestion","praise"].filter(l=>r[l]).map(l=>`${r[l]} ${l}`),c=t.length>0?` (${t.join(", ")})`:"",i=60;return`${e.length>i?`${e.slice(0,i-3)}...`:e}${c}`},this.formatReviewAsMarkdown=(e,o,r)=>{const t=[`## Summary
8
+ ${e}${r?`
9
+ <!-- HAS_CRITICAL_ISSUES -->
10
+ `:""}
11
+ `],c=["critical","warning","suggestion","praise"];for(const i of c){const u=o.filter(l=>l.severity===i);if(u.length!==0){t.push(`### [${i.toUpperCase()}] ${i.charAt(0).toUpperCase()}${i.slice(1)}
12
+ `);for(const l of u){const a=l.file?` (${l.file}${l.line?`:${l.line}`:""})`:"";t.push(`- **[${l.category}]** ${l.title}${a}`),t.push(` ${l.description}`),l.suggestion&&t.push(` > Suggestion: ${l.suggestion}`),t.push("")}}}return t.join(`
13
+ `)},this.serviceName="AI",this.errorPrefix="ERROR",this.colors={primary:""},this.params=s,this.logSessionId=s.logSessionId}getProviderName(){const s=String.fromCharCode(27),e=new RegExp(`${s}\\[[0-9;]*m`,"g");return this.serviceName.replace(e,"").replace(/\[|\]/g,"").trim()}getDetailedErrorMessage(s){const e=s.message||"",o=this.getProviderName(),r=this.params.config.model?.[0],n=this.params.config.timeout,t=this.getServiceSpecificErrorMessage(s);if(t)return t;const c=s.code||(s.status?U(s.status):z(e));return c!==H.UNKNOWN?q(c,{provider:o,model:r,timeout:n}):e||"Unknown error occurred"}getServiceSpecificErrorMessage(s){return null}cleanJsonCodeBlock(s){const e=/```(?:json|JSON)?\s*([\s\S]*?)\s*```/,o=s.match(e);return o?o[1].trim():s}extractJsonFromResponse(s){const e=[],o=s.indexOf("[");o!==-1&&e.push({startIndex:o,openChar:"[",closeChar:"]"});const r=s.indexOf("{");r!==-1&&e.push({startIndex:r,openChar:"{",closeChar:"}"}),e.sort((n,t)=>n.startIndex-t.startIndex);for(const n of e){const t=this.extractBalancedJson(s,n.startIndex,n.openChar,n.closeChar);if(t&&b(t).ok)return t}return null}extractBalancedJson(s,e,o,r){let n=0,t=!1,c=!1;for(let i=e;i<s.length;i++){const u=s[i];if(c){c=!1;continue}if(u==="\\"&&t){c=!0;continue}if(u==='"'){t=!t;continue}if(!t&&(u===o&&n++,u===r&&n--,n===0))return s.slice(e,i+1)}return null}parseMessage(s,e,o){const r=this.cleanJsonCodeBlock(s),n=this.extractJsonFromResponse(r);if(!n){const a=new Error("AI response did not contain a valid JSON object or array.");throw a.name="InvalidJsonResponse",a.content=s,a}const t=b(n);if(!t.ok){const a=new Error("Failed to parse AI response as JSON");throw a.name="JsonParseError",a.content=n,a.originalError=t.error,a}const c=t.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=s,a}const l=i.map(a=>this.extractMessageAsType(a,e)).map(a=>({title:`${a.subject}`,value:`${a.subject}${a.body?`
14
+
15
+ ${a.body}`:""}${a.footer?`
16
+
17
+ ${a.footer}`:""}`})).slice(0,o);if(this.isLoggingEnabled()){const a=l.map(m=>m.title).join(", ");p.info(`${this.serviceName} Parsed ${l.length} commit messages: ${a}`)}return l}extractMessageAsType(s,e){switch(e){case"conventional":const o=/(\w+)(?:\(.*?\))?:\s*(.*)/,r=s.subject.match(o),n=r?r[0]:s.subject;return{...s,subject:this.normalizeCommitMessage(n)};case"gitmoji":const t=/:\w*:\s*(.*)/,c=s.subject.match(t),i=this.params.config.disableLowerCase??!1;return{...s,subject:c&&!i?c[0].toLowerCase():s.subject};default:return s}}normalizeCommitMessage(s){const e=/^(\w+)(\(.*?\))?:\s(.*)$/,o=s.match(e);if(o){const[,r,n,t]=o,c=this.params.config.disableLowerCase??!1,i=r.toLowerCase(),u=c?t:t.charAt(0).toLowerCase()+t.slice(1);s=`${i}${n||""}: ${u}`}return s}sanitizeResponse(s){if(typeof s=="string")try{return[{title:`${P(s)}...`,value:s}]}catch{return[]}return s.map(e=>{try{return{title:`${P(e)}...`,value:e}}catch{return{title:"",value:""}}})}isLoggingEnabled(){return this.params.config.logging&&!!this.logSessionId}}export{se as A,W as a,V as b,G as c,R as d,Q as e,Y as l};
@@ -1 +1 @@
1
- import j from"@anthropic-ai/sdk";import S from"chalk";import{concatMap as $,from as b,map as D,catchError as R}from"rxjs";import{fromPromise as U}from"rxjs/internal/observable/innerFrom";import{A as B,l as F,a as O,b as L,c as N,d as T,e as z}from"./ai.service-6f818099.mjs";import{D as W,g as H,b as G,k as K}from"./cli-9533dfd6.mjs";import"fs";import"path";import"@pacote/xxhash";import"winston";import"cleye";import"module";import"crypto";import"os";import"node:buffer";import"node:path";import"node:child_process";import"node:process";import"child_process";import"node:url";import"node:os";import"assert";import"events";import"node:fs";import"buffer";import"stream";import"util";import"node:util";import"inquirer";import"fs/promises";import"readline";import"figlet";import"gradient-string";import"ora";import"inquirer-reactive-list-prompt";import"winston-daily-rotate-file";import"axios";import"url";import"node:fs/promises";import"chokidar";import"rxjs/operators";const J=["claude-4","claude-haiku-4","claude-sonnet-4","claude-opus-4"],X=x=>{const e=x.toLowerCase();return J.some(t=>e===t||e.startsWith(`${t}-`)||e.startsWith(`${t}.`))},Q=10*60*1e3;class V extends B{constructor(e){super(e),this.params=e,this.generateStreamingCommitMessage$=()=>{const{generate:t,type:o}=this.params.config;return this.createStreamingCommitMessages$(n=>{this.streamChunks(n).catch(c=>n.error(c))},o,t)},this.streamChunks=async t=>{const o=this.params.stagedDiff.diff,{systemPrompt:n,systemPromptPath:c,codeReviewPromptPath:s,logging:i,temperature:P,locale:l,generate:d,type:y,maxLength:v,maxTokens:k,topP:p,model:m}=this.params.config,u={...W,locale:l,maxLength:v,type:y,generate:d,systemPrompt:n,systemPromptPath:c,codeReviewPromptPath:s,vcs_branch:this.params.branchName||""},h=H(u),M=K(o,"commit"),C=`${this.params.config.url||"https://api.anthropic.com"}/v1/messages`,I={"Content-Type":"application/json","x-api-key":this.params.config.key,"anthropic-version":"2023-06-01"};F(o,"commit","Anthropic",m,C,I,i),O(o,"commit","Anthropic",h,M,i);const g=X(m),f={max_tokens:k,temperature:P,system:h,messages:[{role:"user",content:M}],model:m,stream:!0,...g?{}:{top_p:p}};L(o,"commit","Anthropic",f,i);const a=Date.now();let A="";try{const r=this.anthropic.messages.stream(f);r.on("text",_=>{A+=_,t.next(_)});const w=await r.finalMessage(),Y=Date.now()-a;N(o,"commit","Anthropic",w,i),T(o,"commit","Anthropic",Y,A,i),t.complete()}catch(r){z(o,"commit","Anthropic",r,i),t.error(r)}},this.colors={primary:"#AE5630",secondary:"#fff"},this.serviceName=S.bgHex(this.colors.primary).hex(this.colors.secondary).bold("[Anthropic]"),this.errorPrefix=S.red.bold("[Anthropic]"),this.anthropic=new j({apiKey:this.params.config.key,...this.params.config.timeout>Q&&{timeout:this.params.config.timeout}})}getServiceSpecificErrorMessage(e){const t=e.message||"";return t.includes("API key")||t.includes("api_key")?"Invalid API key. Check your Anthropic API key in configuration":t.includes("quota")||t.includes("usage")?"API quota exceeded. Check your Anthropic usage limits":t.includes("model_not_found")||t.includes("Model not found")||/model.*does not exist/i.test(t)?"Model not found or not accessible. Check if the Claude model name is correct":t.includes("403")||t.includes("Forbidden")?"Access denied. Your API key may not have permission for this Claude model":t.includes("404")||t.includes("Not Found")?"Model or endpoint not found. Check your Claude model configuration":t.includes("500")||t.includes("Internal Server Error")?"Anthropic server error. Try again later":null}generateCommitMessage$(){return this.params.config.stream||!1?this.generateStreamingCommitMessage$():U(this.generateMessage("commit")).pipe($(t=>b(t)),D(this.formatAsChoice),R(this.handleError$))}generateCodeReview$(){return U(this.generateMessage("review")).pipe($(e=>b(e)),D(e=>({name:`${this.serviceName} ${e.title}`,short:e.title,value:e.value,description:e.value,isError:!1})),R(this.handleError$))}async generateMessage(e){const t=this.params.stagedDiff.diff,{systemPrompt:o,systemPromptPath:n,codeReviewPromptPath:c,logging:s,temperature:i,locale:P,generate:l,type:d,maxLength:y,maxTokens:v,topP:k,model:p}=this.params.config,m={...W,locale:P,maxLength:y,type:d,generate:l,systemPrompt:o,systemPromptPath:n,codeReviewPromptPath:c,vcs_branch:this.params.branchName||""},u=e==="review"?G(m):H(m),h=K(t,e),E=`${this.params.config.url||"https://api.anthropic.com"}/v1/messages`,C={"Content-Type":"application/json","x-api-key":this.params.config.key,"anthropic-version":"2023-06-01"};F(t,e,"Anthropic",p,E,C,s),O(t,e,"Anthropic",u,h,s);const I=X(p),g={max_tokens:v,temperature:i,system:u,messages:[{role:"user",content:h}],model:p,...I?{}:{top_p:k}};L(t,e,"Anthropic",g,s);const f=Date.now();try{const a=await this.anthropic.messages.create(g),A=Date.now()-f;N(t,e,"Anthropic",a,s);const r=a.content.map(({text:w})=>w).join("");return T(t,e,"Anthropic",A,r,s),e==="review"?this.sanitizeResponse(r):this.parseMessage(r,d,l)}catch(a){throw z(t,e,"Anthropic",a,s),a}}}export{V as AnthropicService};
1
+ import j from"@anthropic-ai/sdk";import S from"chalk";import{concatMap as b,from as D,map as R,catchError as $}from"rxjs";import{fromPromise as U}from"rxjs/internal/observable/innerFrom";import{A as B,l as F,a as O,b as L,c as T,d as N,e as W}from"./ai.service-da8345b1.mjs";import{D as z,g as H,b as G,k as K}from"./cli-83e0874c.mjs";import"fs";import"path";import"@pacote/xxhash";import"winston";import"cleye";import"module";import"crypto";import"os";import"node:buffer";import"node:path";import"node:child_process";import"node:process";import"child_process";import"node:url";import"node:os";import"assert";import"events";import"node:fs";import"buffer";import"stream";import"util";import"node:util";import"inquirer";import"fs/promises";import"readline";import"figlet";import"gradient-string";import"ora";import"inquirer-reactive-list-prompt";import"winston-daily-rotate-file";import"axios";import"url";import"node:fs/promises";import"chokidar";import"rxjs/operators";const J=["claude-4","claude-haiku-4","claude-sonnet-4","claude-opus-4"],X=x=>{const e=x.toLowerCase();return J.some(t=>e===t||e.startsWith(`${t}-`)||e.startsWith(`${t}.`))},Q=10*60*1e3;class V extends B{constructor(e){super(e),this.params=e,this.generateStreamingCommitMessage$=()=>{const{generate:t,type:o}=this.params.config;return this.createStreamingCommitMessages$(n=>{this.streamChunks(n).catch(c=>n.error(c))},o,t)},this.streamChunks=async t=>{const o=this.params.stagedDiff.diff,{systemPrompt:n,systemPromptPath:c,codeReviewPromptPath:s,logging:i,temperature:P,locale:l,generate:d,type:y,maxLength:C,maxTokens:k,topP:p,model:m}=this.params.config,g={...z,locale:l,maxLength:C,type:y,generate:d,systemPrompt:n,systemPromptPath:c,codeReviewPromptPath:s,vcs_branch:this.params.branchName||""},h=H(g),v=K(o,"commit"),M=`${this.params.config.url||"https://api.anthropic.com"}/v1/messages`,w={"Content-Type":"application/json","x-api-key":this.params.config.key,"anthropic-version":"2023-06-01"};F(o,"commit","Anthropic",m,M,w,i),O(o,"commit","Anthropic",h,v,i);const u=X(m),f={max_tokens:k,temperature:P,system:h,messages:[{role:"user",content:v}],model:m,stream:!0,...u?{}:{top_p:p}};L(o,"commit","Anthropic",f,i);const a=Date.now();let A="";try{const r=this.anthropic.messages.stream(f);r.on("text",E=>{A+=E,t.next(E)});const I=await r.finalMessage(),Y=Date.now()-a;T(o,"commit","Anthropic",I,i),N(o,"commit","Anthropic",Y,A,i),t.complete()}catch(r){W(o,"commit","Anthropic",r,i),t.error(r)}},this.colors={primary:"#AE5630",secondary:"#fff"},this.serviceName=S.bgHex(this.colors.primary).hex(this.colors.secondary).bold("[Anthropic]"),this.errorPrefix=S.red.bold("[Anthropic]"),this.anthropic=new j({apiKey:this.params.config.key,...this.params.config.timeout>Q&&{timeout:this.params.config.timeout}})}getServiceSpecificErrorMessage(e){const t=e.message||"";return t.includes("API key")||t.includes("api_key")?"Invalid API key. Check your Anthropic API key in configuration":t.includes("quota")||t.includes("usage")?"API quota exceeded. Check your Anthropic usage limits":t.includes("model_not_found")||t.includes("Model not found")||/model.*does not exist/i.test(t)?"Model not found or not accessible. Check if the Claude model name is correct":t.includes("403")||t.includes("Forbidden")?"Access denied. Your API key may not have permission for this Claude model":t.includes("404")||t.includes("Not Found")?"Model or endpoint not found. Check your Claude model configuration":t.includes("500")||t.includes("Internal Server Error")?"Anthropic server error. Try again later":null}generateCommitMessage$(){return this.params.config.stream||!1?this.generateStreamingCommitMessage$():U(this.generateMessage("commit")).pipe(b(t=>D(t)),R(this.formatAsChoice),$(this.handleError$))}generateCodeReview$(){return U(this.generateMessage("review")).pipe(b(e=>D(e)),R(this.formatCodeReviewAsChoice),$(this.handleError$))}async generateMessage(e){const t=this.params.stagedDiff.diff,{systemPrompt:o,systemPromptPath:n,codeReviewPromptPath:c,logging:s,temperature:i,locale:P,generate:l,type:d,maxLength:y,maxTokens:C,topP:k,model:p}=this.params.config,m={...z,locale:P,maxLength:y,type:d,generate:l,systemPrompt:o,systemPromptPath:n,codeReviewPromptPath:c,vcs_branch:this.params.branchName||""},g=e==="review"?G(m):H(m),h=K(t,e),_=`${this.params.config.url||"https://api.anthropic.com"}/v1/messages`,M={"Content-Type":"application/json","x-api-key":this.params.config.key,"anthropic-version":"2023-06-01"};F(t,e,"Anthropic",p,_,M,s),O(t,e,"Anthropic",g,h,s);const w=X(p),u={max_tokens:C,temperature:i,system:g,messages:[{role:"user",content:h}],model:p,...w?{}:{top_p:k}};L(t,e,"Anthropic",u,s);const f=Date.now();try{const a=await this.anthropic.messages.create(u),A=Date.now()-f;T(t,e,"Anthropic",a,s);const r=a.content.map(({text:I})=>I).join("");return N(t,e,"Anthropic",A,r,s),e==="review"?this.parseCodeReview(r):this.parseMessage(r,d,l)}catch(a){throw W(t,e,"Anthropic",a,s),a}}}export{V as AnthropicService};
@@ -0,0 +1 @@
1
+ import F from"https";import h from"chalk";import{concatMap as $,from as T,map as x,catchError as L}from"rxjs";import{fromPromise as W}from"rxjs/internal/observable/innerFrom";import{A as q,l as Y,a as H,b as z,d as V,e as _,c as K}from"./ai.service-da8345b1.mjs";import{D as J,b as j,g as Q,p as O,s as X,k as Z}from"./cli-83e0874c.mjs";import"fs";import"path";import"@pacote/xxhash";import"winston";import"cleye";import"module";import"crypto";import"os";import"node:buffer";import"node:path";import"node:child_process";import"node:process";import"child_process";import"node:url";import"node:os";import"assert";import"events";import"node:fs";import"buffer";import"stream";import"util";import"node:util";import"inquirer";import"fs/promises";import"readline";import"figlet";import"gradient-string";import"ora";import"inquirer-reactive-list-prompt";import"winston-daily-rotate-file";import"axios";import"url";import"node:fs/promises";import"chokidar";import"rxjs/operators";const g="Bedrock",S={MISSING_DEPENDENCY:"MissingDependencyError",MISSING_REGION:"MissingRegionError",MISSING_MODEL_ID:"MissingModelIdError",MISSING_API_KEY:"MissingApiKeyError",MISSING_APPLICATION_KEY:"MissingApplicationKeyError",INVALID_RESPONSE:"InvalidResponseError",EMPTY_RESPONSE:"EmptyResponseError"},i=f=>typeof f=="string"&&f.length>0;let v=null,M=null;const G=f=>{const e=new Error('Amazon Bedrock support requires "@aws-sdk/client-bedrock-runtime" and "@aws-sdk/credential-providers". Install them with `pnpm add @aws-sdk/client-bedrock-runtime @aws-sdk/credential-providers`.');return e.name=S.MISSING_DEPENDENCY,e.originalError=f,e},ee=async()=>{if(v)return v;try{return v=await import("@aws-sdk/client-bedrock-runtime"),v}catch(f){throw v=null,G(f)}},U=async()=>{if(M)return M;try{return M=await import("@aws-sdk/credential-providers"),M}catch(f){throw M=null,G(f)}};class oe extends q{constructor(e){super(e),this.params=e,this.credentialCache=void 0,this.credentialCacheTimestamp=0,this.CREDENTIAL_CACHE_TTL=5*60*1e3,this.bedrockConfig=this.params.config,this.colors={primary:"#232F3E",secondary:"#FF9900"},this.serviceName=h.bgHex(this.colors.primary).hex(this.colors.secondary).bold(`[${g}]`),this.errorPrefix=h.red.bold(`[${g}]`),this.validateConfiguration()}validateConfiguration(){const e=this.bedrockConfig;if(!i(e.model)){const t=new Error("Model ID or inference profile ARN is required.");throw t.name=S.MISSING_MODEL_ID,t}if(!this.getRegion()){const t=new Error("AWS region is required. Configure BEDROCK.region or set AWS_REGION/AWS_DEFAULT_REGION.");throw t.name=S.MISSING_REGION,t}const r=i(e.key),o=this.canUseAwsSdk();if(!r&&!o){const t=new Error("Authentication required: Configure AWS credentials (profile, access keys, IAM role) or API key (BEDROCK.key).");throw t.name=S.MISSING_API_KEY,t}if(r&&!o&&!this.getRegion()&&!i(e.applicationBaseUrl)){const t=new Error("Bearer token authentication requires region or applicationBaseUrl to construct endpoint.");throw t.name=S.MISSING_REGION,t}}canUseAwsSdk(){const e=this.bedrockConfig,r=i(e.profile)||i(process.env.AWS_PROFILE),o=i(e.accessKeyId)&&i(e.secretAccessKey)||i(process.env.AWS_ACCESS_KEY_ID)&&i(process.env.AWS_SECRET_ACCESS_KEY);return r||o}determineAuthMethod(){const e=i(this.bedrockConfig.key);if(this.canUseAwsSdk())return"aws-sdk";if(e)return"bearer-token";throw new Error("No authentication method configured")}getServiceSpecificErrorMessage(e){const r=e?.name||e.code,o=e.message||"";switch(r){case"UnrecognizedClientException":case"InvalidSignatureException":return"Authentication with AWS failed. Check your IAM credentials or Bedrock API key settings.";case"AccessDeniedException":return"Access denied. Ensure the IAM principal or application key has permission to invoke the Bedrock resource.";case"ValidationException":return"Invalid request for the selected Bedrock model. Verify the model ID and payload.";case"ResourceNotFoundException":return"The specified Bedrock model, endpoint, or inference profile could not be found.";case"ThrottlingException":return"Request throttled by Bedrock. Reduce request rate or check service quotas."}return o.includes("Region")?"AWS region is required for Bedrock. Configure BEDROCK.region or set AWS_REGION/AWS_DEFAULT_REGION.":null}generateCommitMessage$(){return W(this.generateMessage("commit")).pipe($(e=>T(e)),x(this.formatAsChoice),L(this.handleError$))}generateCodeReview$(){return W(this.generateMessage("review")).pipe($(e=>T(e)),x(this.formatCodeReviewAsChoice),L(this.handleError$))}async generateMessage(e){const r=this.params.stagedDiff.diff,o=this.bedrockConfig,t=o.model,{logging:c,inferenceParameters:n}=o,s={...J,locale:o.locale,maxLength:o.maxLength,type:o.type,generate:o.generate,systemPrompt:o.systemPrompt,systemPromptPath:o.systemPromptPath,codeReviewPromptPath:o.codeReviewPromptPath},a=e==="review"?j(s):Q(s),m=Z(r,e),y={region:this.getRegion(),profile:o.profile,modelId:t},w=`https://bedrock-runtime.${this.getRegion()||"unknown"}.amazonaws.com/model/${encodeURIComponent(t)}/converse`;Y(r,e,g,t,w,y,c),H(r,e,g,a,m,c);const R={modelId:t,systemPrompt:a,userPrompt:m,...n&&Object.keys(n).length>0&&{inferenceConfig:n}};z(r,e,g,R,c);const u=Date.now();try{const l=this.determineAuthMethod();O()&&(console.log(h.cyan(`[Bedrock] Authentication method: ${l}`)),console.log(h.cyan(`[Bedrock] Model ID: ${t}`)),console.log(h.cyan(`[Bedrock] Region: ${this.getRegion()}`)),console.log(h.cyan(`[Bedrock] Has AWS credentials: ${this.canUseAwsSdk()}`)),console.log(h.cyan(`[Bedrock] Has API key: ${i(o.key)}`)));const E=l==="bearer-token"?await this.invokeWithBearerToken({model:t,systemPrompt:a,userPrompt:m,logging:c,requestType:e,diff:r,inferenceConfig:n}):await this.invokeWithAwsSdk({model:t,systemPrompt:a,userPrompt:m,logging:c,requestType:e,diff:r,inferenceConfig:n}),I=Date.now()-u;return V(r,e,g,I,E,c),e==="review"?this.parseCodeReview(E):this.parseMessage(E,o.type,o.generate)}catch(l){throw _(r,e,g,l,c),l instanceof Error&&(l.status=l?.status||l?.$metadata?.httpStatusCode),l}}async invokeWithAwsSdk(e){const r=this.getRegion(),{model:o,systemPrompt:t,userPrompt:c,inferenceConfig:n,logging:s,requestType:a,diff:m}=e,{BedrockRuntimeClient:y,ConverseCommand:w}=await ee(),R=this.bedrockConfig,u={region:r,requestHandler:{requestTimeout:R.timeout||12e4}},l=await this.resolveCredentials();l&&(u.credentials=l);const E=new y(u),I=new w({modelId:o,messages:[{role:"user",content:[{text:c}]}],...t?{system:[{text:t}]}:{},...n&&Object.keys(n).length>0&&{inferenceConfig:n}});O()&&console.log(h.cyan(`[Bedrock] Sending ConverseCommand with modelId: ${o}`));let C;try{C=await E.send(I),K(m,a,g,C,s)}catch(d){throw O()&&(console.error(h.red(`[Bedrock] AWS SDK Error: ${d.name}`)),console.error(h.red(`[Bedrock] Error message: ${d.message}`)),d.$metadata&&(console.error(h.red(`[Bedrock] Request ID: ${d.$metadata.requestId}`)),console.error(h.red(`[Bedrock] HTTP Status: ${d.$metadata.httpStatusCode}`))),d.$fault&&console.error(h.red(`[Bedrock] Fault: ${d.$fault}`))),d}const A=C.output?.message?.content?.[0]?.text||"";if(!A){const d=new Error("No text content found in Bedrock response.");throw d.name=S.EMPTY_RESPONSE,d.content=C,d}return A}invokeWithBearerToken(e){const{model:r,systemPrompt:o,userPrompt:t,inferenceConfig:c,logging:n,requestType:s,diff:a}=e,m=this.bedrockConfig,y=this.getRegion(),w=encodeURIComponent(r),R=m.applicationBaseUrl||`https://bedrock-runtime.${y}.amazonaws.com/model/${w}/converse`,u=new URL(R),l={modelId:r,messages:[{role:"user",content:[{text:t}]}],...c&&Object.keys(c).length>0&&{inferenceConfig:c}};o&&(l.system=[{text:o}]);const E=JSON.stringify(l),I={"Content-Type":"application/json","Content-Length":Buffer.byteLength(E).toString()};return i(m.key)&&(I.Authorization=`Bearer ${m.key}`),i(m.applicationInferenceProfileArn)&&(I["x-amzn-bedrock-inference-profile-arn"]=m.applicationInferenceProfileArn),i(m.applicationEndpointId)&&(I["x-amzn-bedrock-endpoint-id"]=m.applicationEndpointId),new Promise((C,A)=>{const d=F.request({method:"POST",protocol:u.protocol,hostname:u.hostname,port:u.port,path:u.pathname+u.search,headers:I,timeout:m.timeout},k=>{const P=[];k.on("data",N=>P.push(N)),k.on("end",()=>{const N=Buffer.concat(P).toString("utf8");if(k.statusCode&&k.statusCode>=400){const p=new Error(`Bedrock application endpoint responded with status ${k.statusCode}.`);return p.status=k.statusCode,p.content=N,_(a,s,g,p,n),A(p)}const D=X(N);if(!D.ok){const p=new Error("Failed to parse Bedrock application response as JSON. The Bedrock Converse API should always return valid JSON.");return p.name=S.INVALID_RESPONSE,p.content=N,_(a,s,g,p,n),A(p)}const B=D.data;K(a,s,g,B,n);const b=B.output?.message?.content?.[0]?.text||"";if(!b){const p=new Error("No text content found in Bedrock response.");return p.name=S.EMPTY_RESPONSE,p.content=B,_(a,s,g,p,n),A(p)}C(b)})});d.on("error",k=>{const P=k;_(a,s,g,P,n),A(P)}),d.write(E),d.end()})}getRegion(){return this.bedrockConfig.region||process.env.AWS_REGION||process.env.AWS_DEFAULT_REGION||""}async resolveCredentials(){const e=Date.now();if(this.credentialCache&&e-this.credentialCacheTimestamp<this.CREDENTIAL_CACHE_TTL)return this.credentialCache;const r=this.bedrockConfig,o=r.profile,t=r.accessKeyId,c=r.secretAccessKey,n=r.sessionToken;let s;if(i(o)){const{fromIni:a}=await U();s=a({profile:o})}else if(i(t)&&i(c))s=async()=>({accessKeyId:t,secretAccessKey:c,sessionToken:n||process.env.AWS_SESSION_TOKEN||void 0});else if(process.env.AWS_PROFILE){const{fromIni:a}=await U();s=a({profile:process.env.AWS_PROFILE})}return s&&(this.credentialCache=s,this.credentialCacheTimestamp=e),s}}export{oe as BedrockService};