2ndopinion-cli 0.1.0

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 (3) hide show
  1. package/README.md +54 -0
  2. package/dist/index.js +8 -0
  3. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # 2ndOpinion
2
+
3
+ **Get a 2nd AI opinion on your code changes.** 2ndOpinion is a CLI tool that analyzes your git diffs using Claude, GPT-4, or Gemini and returns risk analysis, alternative approaches, and a merge recommendation — right in your terminal.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g 2ndopinion-cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ 2ndopinion signup # Create an account
15
+ 2ndopinion # Analyze current git changes
16
+ 2ndopinion --llm claude # Choose a specific LLM
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ | Command | Description |
22
+ |---------|-------------|
23
+ | `2ndopinion` | Analyze current changes (staged + unstaged) |
24
+ | `2ndopinion watch` | Continuous monitoring on file changes |
25
+ | `2ndopinion --llm claude\|gpt4\|gemini` | Choose which LLM to use |
26
+ | `2ndopinion signup` | Create a new account |
27
+ | `2ndopinion login` | Log in to your account |
28
+ | `2ndopinion status` | Check usage and subscription |
29
+ | `2ndopinion upgrade` | Upgrade your plan |
30
+ | `2ndopinion config --llm <name>` | Set default LLM |
31
+
32
+ ## Watch Mode
33
+
34
+ Run `2ndopinion watch` in a second terminal alongside your editor or AI coding tool. It monitors your working directory for file changes, automatically detects new git diffs, and streams analysis results as you code — no manual re-runs needed.
35
+
36
+ ```bash
37
+ # Terminal 1: write code
38
+ # Terminal 2: continuous review
39
+ 2ndopinion watch
40
+ ```
41
+
42
+ ## Pricing
43
+
44
+ | Plan | Analyses/month | Price |
45
+ |------|---------------|-------|
46
+ | Free | 5 | $0 |
47
+ | Pro | 100 | $10/mo |
48
+ | Unlimited | Unlimited | $25/mo |
49
+
50
+ Full docs at [get2ndopinion.dev](https://get2ndopinion.dev)
51
+
52
+ ## License
53
+
54
+ Proprietary. All rights reserved.
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import Qt from'simple-git';import Zt from'conf';import jt,{AxiosError}from'axios';import {render,useApp,Box,Text,useInput}from'ink';import {jsx,jsxs}from'react/jsx-runtime';import {useState,useEffect,useRef,useCallback}from'react';import ao from'ink-spinner';import rt from'ink-text-input';import {watch}from'chokidar';import {createHash}from'crypto';import Wt from'open';import {Command}from'commander';var qe=Object.defineProperty;var Kt=Object.getOwnPropertyDescriptor;var Ht=Object.getOwnPropertyNames;var Yt=Object.prototype.hasOwnProperty;var C=(e,t)=>()=>(e&&(t=e(e=0)),t);var Q=(e,t)=>{for(var o in t)qe(e,o,{get:t[o],enumerable:true});},Jt=(e,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of Ht(t))!Yt.call(e,i)&&i!==o&&qe(e,i,{get:()=>t[i],enumerable:!(n=Kt(t,i))||n.enumerable});return e};var $=e=>Jt(qe({},"__esModule",{value:true}),e);async function Fe(){try{return await Se.checkIsRepo()}catch{return false}}async function Re(){try{await Se.version();}catch{throw new Error("GIT_NOT_INSTALLED")}if(!await Fe())throw new Error("NOT_GIT_REPO");let t=await Se.diff(["--cached"]),o=await Se.diff(),n=t.trim().length>0,i=o.trim().length>0;if(!n&&!i)throw new Error("NO_CHANGES");let r=[];return n&&r.push(`=== STAGED CHANGES ===
3
+ ${t.trim()}`),i&&r.push(`=== UNSTAGED CHANGES ===
4
+ ${o.trim()}`),r.join(`
5
+
6
+ `)}var Se,Ae=C(()=>{Se=Qt();});function k(){return Z.get("token")}function Le(e){Z.set("token",e);}function Ie(){return Z.get("apiUrl")}function Tt(e){Z.set("apiUrl",e);}function j(){return Z.get("defaultLlm")||"gpt4"}function ht(e){Z.set("defaultLlm",e);}function Oe(){return Z.get("email")}function _e(e){Z.set("email",e);}var Z,q=C(()=>{Z=new Zt({projectName:"2ndopinion",schema:{token:{type:"string",default:""},apiUrl:{type:"string",default:"http://localhost:3000"},defaultLlm:{type:"string",default:"gpt4"},email:{type:"string",default:""}}});});var wt={};Q(wt,{analyze:()=>we,createCheckout:()=>He,getUsage:()=>ee,login:()=>Ke,openPortal:()=>Ye,setDebug:()=>to,signup:()=>Ve});function to(e){yt=e;}function fe(){let e=jt.create({baseURL:Ie(),timeout:45e3,headers:{"Content-Type":"application/json"}});return yt&&(e.interceptors.request.use(t=>{let o=t.data?JSON.stringify(t.data):"(none)",n=t.data?Buffer.byteLength(o):0;return console.error(`[DEBUG] ${t.method?.toUpperCase()} ${t.baseURL}${t.url}`),console.error(`[DEBUG] Body size: ${(n/1024).toFixed(1)}KB`),t._startTime=Date.now(),t}),e.interceptors.response.use(t=>{let o=Date.now()-(t.config._startTime||Date.now());return console.error(`[DEBUG] Response: ${t.status} (${o}ms)`),console.error(`[DEBUG] Data: ${JSON.stringify(t.data).slice(0,500)}`),t},t=>{if(t.config){let o=Date.now()-(t.config._startTime||Date.now());console.error(`[DEBUG] Error: ${t.response?.status||t.code} (${o}ms)`);}return Promise.reject(t)})),e}function Ne(){let e=k();if(!e)throw new Error("NOT_LOGGED_IN");return {Authorization:`Bearer ${e}`}}function xe(e){if(e instanceof AxiosError){if(!e.response)throw e.code==="ECONNABORTED"||e.message?.includes("timeout")?new Error("TIMEOUT"):e.code==="ECONNREFUSED"?new Error("CONNECTION_REFUSED"):new Error("NETWORK_ERROR");if(e.response.status===401)throw new Error("NOT_LOGGED_IN");if(e.response.status===429){let o=e.response.data;if(o?.retryAfter)throw new Error("RATE_LIMIT");let n=o?.usage?.used??"?",i=o?.usage?.limit??"?";throw new Error(`LIMIT_REACHED:${n}/${i}`)}if(e.response.status>=500)throw new Error("SERVER_ERROR");let t=e.response.data?.error||e.message;throw new Error(t)}throw e}async function Ve(e,t){try{return (await fe().post("/api/auth/signup",{email:e,password:t})).data}catch(o){return xe(o)}}async function Ke(e,t){try{return (await fe().post("/api/auth/login",{email:e,password:t})).data}catch(o){return xe(o)}}async function we(e,t,o,n="manual"){try{return (await fe().post("/api/analyze",{diff:e,context:t,llm:o,mode:n},{headers:Ne()})).data}catch(i){return xe(i)}}async function ee(){try{return (await fe().get("/api/usage",{headers:Ne()})).data}catch(e){return xe(e)}}async function He(e){try{return (await fe().post("/api/checkout",{tier:e},{headers:Ne()})).data}catch(t){return xe(t)}}async function Ye(){try{return (await fe().post("/api/portal",{},{headers:Ne()})).data}catch(e){return xe(e)}}var yt,te=C(()=>{q();yt=false;});function P(e){return oo[e]||e}function Ue(e){let t=Buffer.byteLength(e,"utf-8"),o=Math.round(t/1024);if(t<=Je)return {diff:e,truncated:false,originalKB:o};let n=e.slice(0,Je),i=n.lastIndexOf(`
7
+ `);return i>Je*.8&&(n=n.slice(0,i)),{diff:n,truncated:true,originalKB:o}}var oo,Je,oe=C(()=>{oo={claude:"Claude Sonnet 4",gpt4:"GPT-5.2",gemini:"Gemini 3 Flash"};Je=50*1024;});function no({risk:e}){return e.level==="high"?jsxs(Box,{flexDirection:"column",children:[jsxs(Box,{children:[jsx(Text,{bold:true,color:"red",children:"[!] HIGH "}),jsx(Text,{bold:true,color:"red",children:e.description})]}),e.lineNumbers&&jsx(Box,{marginLeft:10,children:jsxs(Text,{dimColor:true,children:["Lines: ",e.lineNumbers]})})]}):e.level==="medium"?jsxs(Box,{flexDirection:"column",children:[jsxs(Box,{children:[jsx(Text,{color:"yellow",children:"[*] MEDIUM "}),jsx(Text,{color:"yellow",children:e.description})]}),e.lineNumbers&&jsx(Box,{marginLeft:10,children:jsxs(Text,{dimColor:true,children:["Lines: ",e.lineNumbers]})})]}):jsxs(Box,{flexDirection:"column",children:[jsxs(Box,{children:[jsx(Text,{dimColor:true,color:"green",children:"[~] LOW "}),jsx(Text,{dimColor:true,children:e.description})]}),e.lineNumbers&&jsx(Box,{marginLeft:10,children:jsxs(Text,{dimColor:true,children:["Lines: ",e.lineNumbers]})})]})}function io({rec:e}){return e==="accept"?jsxs(Box,{borderStyle:"round",borderColor:"green",paddingX:2,flexDirection:"column",children:[jsx(Text,{bold:true,color:"green",children:"RECOMMENDATION: ACCEPT"}),jsx(Text,{color:"green",children:"These changes look good to go."})]}):e==="review"?jsxs(Box,{borderStyle:"round",borderColor:"yellow",paddingX:2,flexDirection:"column",children:[jsx(Text,{bold:true,color:"yellow",children:"RECOMMENDATION: REVIEW"}),jsx(Text,{color:"yellow",children:"A human should check these changes before merging."})]}):jsxs(Box,{borderStyle:"round",borderColor:"red",paddingX:2,flexDirection:"column",children:[jsx(Text,{bold:true,color:"red",children:"RECOMMENDATION: REJECT"}),jsx(Text,{color:"red",children:"Do not merge without fixing the issues above."})]})}function be({analysis:e,usage:t,llm:o,mode:n="manual"}){let i=ro[t.tier]||t.tier,r=t.limit===1/0?"unlimited":String(t.limit);return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsxs(Box,{borderStyle:"double",borderColor:"cyan",paddingX:2,flexDirection:"column",children:[jsx(Text,{bold:true,color:"cyan",children:"2ndOpinion Analysis"}),jsxs(Text,{dimColor:true,children:["Model: ",P(o)," | Mode: ",n==="watch"?"Watch":"Manual"]})]}),jsxs(Box,{flexDirection:"column",marginTop:1,children:[jsx(Text,{bold:true,color:"white",children:" SUMMARY"}),jsx(Box,{borderStyle:"round",borderColor:"gray",paddingX:2,marginTop:0,children:jsx(Text,{wrap:"wrap",children:e.summary})})]}),jsxs(Box,{flexDirection:"column",marginTop:1,children:[jsxs(Text,{bold:true,color:"white",children:[" RISKS (",e.risks.length,")"]}),jsx(Box,{borderStyle:"round",borderColor:"gray",paddingX:2,marginTop:0,flexDirection:"column",children:e.risks.length===0?jsx(Text,{color:"green",children:"No risks identified. Looking good!"}):e.risks.map((s,c)=>jsx(Box,{marginTop:c>0?1:0,children:jsx(no,{risk:s})},c))})]}),jsxs(Box,{flexDirection:"column",marginTop:1,children:[jsx(Text,{bold:true,color:"white",children:" ALTERNATIVE APPROACH"}),jsx(Box,{borderStyle:"round",borderColor:"gray",paddingX:2,marginTop:0,children:jsx(Text,{wrap:"wrap",children:e.alternative})})]}),jsx(Box,{marginTop:1,children:jsx(io,{rec:e.recommendation})}),jsx(Box,{marginTop:1,children:jsxs(Text,{dimColor:true,children:["Usage: ",t.used,"/",r," requests this month (",i,")"]})})]})}var ro,Qe=C(()=>{oe();ro={free:"Free",pro_100:"Pro 100",pro_unlimited:"Pro Unlimited"};});function tt({llm:e}){let[t]=useState(()=>Math.floor(Math.random()*bt.length));return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsxs(Box,{children:[jsx(Text,{color:"cyan",children:jsx(ao,{type:"dots"})}),jsxs(Text,{children:[" Analyzing changes with ",P(e),"..."]})]}),jsx(Box,{marginTop:1,children:jsx(Text,{dimColor:true,children:bt[t]})})]})}var bt,Et=C(()=>{oe();bt=["Tip: Use --llm claude to switch LLM providers","Tip: Stage specific files with git add for focused analysis","Tip: Run 2ndopinion watch for continuous monitoring","Tip: Use 2ndopinion status to check your usage","Tip: Smaller, focused diffs get better analysis","Tip: Pro plans support up to unlimited requests/month"];});function F({error:e}){if(e==="NOT_LOGGED_IN")return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"red",children:"Session expired. Please log in again."}),jsx(Box,{marginTop:1,children:jsxs(Text,{children:["Run: ",jsx(Text,{bold:true,color:"cyan",children:"2ndopinion login"})]})})]});if(e.startsWith("LIMIT_REACHED")){let t=e.split(":")[1]||"?/?";return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsxs(Text,{bold:true,color:"red",children:["Monthly limit reached (",t,")."]}),jsx(Box,{marginTop:1,children:jsxs(Text,{children:["Upgrade: ",jsx(Text,{bold:true,color:"cyan",children:"2ndopinion upgrade"})]})})]})}return e==="RATE_LIMIT"?jsx(Box,{flexDirection:"column",paddingX:1,children:jsx(Text,{bold:true,color:"yellow",children:"Slow down! Wait a minute before the next request."})}):e==="TIMEOUT"?jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"red",children:"Request timed out."}),jsx(Box,{marginTop:1,children:jsx(Text,{dimColor:true,children:"The AI is taking longer than usual. Try again."})})]}):e==="CONNECTION_REFUSED"?jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"red",children:"Cannot connect to 2ndOpinion servers."}),jsx(Box,{marginTop:1,children:jsx(Text,{dimColor:true,children:"Check your internet connection."})})]}):e==="NETWORK_ERROR"?jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"red",children:"Could not reach 2ndOpinion servers."}),jsx(Box,{marginTop:1,children:jsx(Text,{dimColor:true,children:"Check your internet connection and try again."})})]}):e==="SERVER_ERROR"?jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"red",children:"Something went wrong on our end."}),jsx(Box,{marginTop:1,children:jsx(Text,{dimColor:true,children:"Please try again in a moment."})})]}):e==="GIT_NOT_INSTALLED"?jsx(Box,{flexDirection:"column",paddingX:1,children:jsx(Text,{bold:true,color:"red",children:"Git is not installed or not in PATH."})}):e==="NOT_GIT_REPO"?jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"red",children:"This directory is not a git repository."}),jsx(Box,{marginTop:1,children:jsx(Text,{dimColor:true,children:"Navigate to a project directory and try again."})})]}):e==="NO_CHANGES"?jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"yellow",children:"No changes detected."}),jsx(Box,{marginTop:1,children:jsxs(Text,{dimColor:true,children:["Stage your changes first: ",jsxs(Text,{bold:true,children:["git add ","<files>"]})]})})]}):jsx(Box,{flexDirection:"column",paddingX:1,children:jsxs(Text,{bold:true,color:"red",children:["Error: ",e]})})}var Ee=C(()=>{});function ot({onChoice:e}){let{exit:t}=useApp(),[o,n]=useState(0);return useInput((i,r)=>{if(i==="1"){e("signup");return}if(i==="2"){e("login");return}if(i==="q"||r.escape){t();return}if(r.upArrow||r.downArrow){n(s=>s===0?1:0);return}r.return&&e(o===0?"signup":"login");}),jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsxs(Box,{borderStyle:"double",borderColor:"cyan",paddingX:2,flexDirection:"column",children:[jsx(Text,{bold:true,color:"cyan",children:"Welcome to 2ndOpinion!"}),jsx(Text,{children:"Get a second AI opinion on your code changes."})]}),jsxs(Box,{marginTop:1,flexDirection:"column",children:[jsx(Text,{bold:true,children:"Let's get you set up:"}),jsxs(Box,{marginTop:1,flexDirection:"column",children:[jsxs(Box,{children:[jsxs(Text,{color:o===0?"cyan":void 0,bold:o===0,children:[o===0?"> ":" ","[1] Sign up"]}),jsx(Text,{dimColor:true,children:" (new account)"})]}),jsxs(Box,{children:[jsxs(Text,{color:o===1?"cyan":void 0,bold:o===1,children:[o===1?"> ":" ","[2] Log in"]}),jsx(Text,{dimColor:true,children:" (existing account)"})]})]})]}),jsx(Box,{marginTop:1,children:jsx(Text,{dimColor:true,children:"Use arrow keys or press 1/2, then Enter. [q] to quit."})})]})}var Bt=C(()=>{});var nt={};Q(nt,{runSignup:()=>xo});function fo(){let{exit:e}=useApp(),[t,o]=useState("email"),[n,i]=useState(""),[r,s]=useState(""),[c,l]=useState(""),[a,m]=useState("");async function U(w,Te){o("submitting");try{let Y=await Ve(w,Te);Le(Y.token),_e(Y.user.email),o("done"),setTimeout(()=>e(),1500);}catch(Y){m(Y.message||"Signup failed"),o("error"),setTimeout(()=>e(),100);}}return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"cyan",children:"2ndOpinion Signup"}),t==="email"&&jsxs(Box,{marginTop:1,children:[jsx(Text,{children:"Email: "}),jsx(rt,{value:n,onChange:i,onSubmit:w=>{w.trim()&&(i(w.trim()),o("password"));}})]}),t!=="email"&&jsxs(Box,{marginTop:1,children:[jsx(Text,{color:"green",children:"+ "}),jsxs(Text,{dimColor:true,children:["Email: ",n]})]}),t==="password"&&jsxs(Box,{children:[jsx(Text,{children:"Password: "}),jsx(rt,{value:r,onChange:s,mask:"*",onSubmit:w=>{w.trim()&&(s(w.trim()),o("confirm"));}})]}),(t==="confirm"||t==="submitting"||t==="done")&&jsxs(Box,{children:[jsx(Text,{color:"green",children:"+ "}),jsx(Text,{dimColor:true,children:"Password: ********"})]}),t==="confirm"&&jsxs(Box,{children:[jsx(Text,{children:"Confirm password: "}),jsx(rt,{value:c,onChange:l,mask:"*",onSubmit:w=>{if(w.trim()!==r){m("Passwords do not match"),o("error"),setTimeout(()=>e(),100);return}U(n,w.trim());}})]}),t==="submitting"&&jsxs(Box,{marginTop:1,children:[jsx(Text,{color:"cyan",children:jsx(ao,{type:"dots"})}),jsx(Text,{children:" Creating your account..."})]}),t==="done"&&jsxs(Box,{flexDirection:"column",marginTop:1,children:[jsx(Text,{bold:true,color:"green",children:"+ Welcome to 2ndOpinion!"}),jsx(Box,{marginTop:1,children:jsxs(Text,{children:["Signed up as ",jsx(Text,{bold:true,children:n}),". You have 5 free requests/month."]})}),jsxs(Box,{marginTop:1,borderStyle:"round",borderColor:"cyan",paddingX:2,flexDirection:"column",children:[jsx(Text,{bold:true,children:"Getting started:"}),jsxs(Text,{children:["Make some code changes, then run ",jsx(Text,{bold:true,color:"cyan",children:"2ndopinion"})," to get your first analysis."]}),jsx(Text,{dimColor:true,children:"Both staged and unstaged changes are analyzed."})]})]}),t==="error"&&jsx(Box,{marginTop:1,children:jsxs(Text,{bold:true,color:"red",children:["x Signup failed: ",a]})})]})}function xo(){let{waitUntilExit:e}=render(jsx(fo,{}));e().then(()=>process.exit(0));}var it=C(()=>{te();q();});var st={};Q(st,{runLogin:()=>bo});function wo(){let{exit:e}=useApp(),[t,o]=useState("email"),[n,i]=useState(""),[r,s]=useState(""),[c,l]=useState("");async function a(m,U){o("submitting");try{let w=await Ke(m,U);Le(w.token),_e(w.user.email),o("done"),setTimeout(()=>e(),500);}catch(w){l(w.message||"Login failed"),o("error"),setTimeout(()=>e(),100);}}return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"cyan",children:"2ndOpinion Login"}),t==="email"&&jsxs(Box,{marginTop:1,children:[jsx(Text,{children:"Email: "}),jsx(rt,{value:n,onChange:i,onSubmit:m=>{m.trim()&&(i(m.trim()),o("password"));}})]}),t!=="email"&&jsxs(Box,{marginTop:1,children:[jsx(Text,{color:"green",children:"+ "}),jsxs(Text,{dimColor:true,children:["Email: ",n]})]}),t==="password"&&jsxs(Box,{children:[jsx(Text,{children:"Password: "}),jsx(rt,{value:r,onChange:s,mask:"*",onSubmit:m=>{m.trim()&&a(n,m.trim());}})]}),t==="submitting"&&jsxs(Box,{marginTop:1,children:[jsx(Text,{color:"cyan",children:jsx(ao,{type:"dots"})}),jsx(Text,{children:" Authenticating..."})]}),t==="done"&&jsx(Box,{marginTop:1,children:jsxs(Text,{bold:true,color:"green",children:["+ Logged in as ",n]})}),t==="error"&&jsx(Box,{marginTop:1,children:jsxs(Text,{bold:true,color:"red",children:["x Login failed: ",c]})})]})}function bo(){let{waitUntilExit:e}=render(jsx(wo,{}));e().then(()=>process.exit(0));}var at=C(()=>{te();q();});var At={};Q(At,{runAnalyze:()=>Co});function Bo({llm:e}){let[t,o]=useState("checking"),[n,i]=useState(null),[r,s]=useState(""),[c,l]=useState("");return useEffect(()=>{let a=k();o(a?"loading":"welcome");},[]),useEffect(()=>{if(t!=="loading")return;let a=false;async function m(){let U;try{U=await Re();}catch(le){o("error"),s(le.message||"Failed to read git diff");return}let{diff:w,truncated:Te,originalKB:Y}=Ue(U);Te&&l(`Large diff detected (${Y}KB). Sending first 50KB for analysis.`);try{let le=await we(w,"",e,"manual");a||(i(le),o("done"));}catch(le){a||(o("error"),s(le.message||"Analysis failed"));}}return m(),()=>{a=true;}},[t,e]),t==="checking"?null:t==="welcome"?jsx(ot,{onChoice:a=>{if(a==="signup"){let{runSignup:m}=(it(),$(nt));m();}else {let{runLogin:m}=(at(),$(st));m();}}}):t==="loading"?jsxs(Box,{flexDirection:"column",children:[c&&jsx(Box,{paddingX:1,children:jsx(Text,{color:"yellow",children:c})}),jsx(tt,{llm:e})]}):t==="error"?jsx(F,{error:r}):n?jsxs(Box,{flexDirection:"column",children:[c&&jsx(Box,{paddingX:1,marginBottom:1,children:jsx(Text,{color:"yellow",children:c})}),jsx(be,{analysis:n.analysis,usage:n.usage,llm:e})]}):null}function Co(e){let t=e.llm||j(),{waitUntilExit:o}=render(jsx(Bo,{llm:t}));o().then(()=>process.exit(0));}var Lt=C(()=>{Ae();te();q();oe();Qe();Et();Ee();Bt();});var It={};Q(It,{runStatus:()=>Io});function Lo(){let[e,t]=useState("loading"),[o,n]=useState(null),[i,r]=useState("");if(useEffect(()=>{async function s(){if(!k()){t("error"),r("NOT_LOGGED_IN");return}try{let l=await ee();n(l),t("done");}catch(l){t("error"),r(l.message||"Failed to fetch usage");}}s();},[]),e==="loading")return jsxs(Box,{paddingX:1,children:[jsx(Text,{color:"cyan",children:jsx(ao,{type:"dots"})}),jsx(Text,{children:" Fetching account info..."})]});if(e==="error")return jsx(F,{error:i});if(o){let s=Oe(),c=new Date(o.resetsAt).toLocaleDateString(),l=o.tier!=="free",a=Ao[o.tier]||o.tier,m=j();return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Box,{borderStyle:"double",borderColor:"cyan",paddingX:2,flexDirection:"column",children:jsx(Text,{bold:true,color:"cyan",children:"2ndOpinion Status"})}),jsxs(Box,{marginTop:1,flexDirection:"column",children:[jsxs(Text,{children:["Account: ",jsx(Text,{bold:true,children:s||"unknown"})]}),jsxs(Text,{children:["Tier: ",jsx(Text,{bold:true,children:a})]}),jsxs(Text,{children:["Usage: ",jsxs(Text,{bold:true,children:[o.used,"/",o.limit===1/0?"\u221E":o.limit]})," requests this month"]}),jsxs(Text,{children:["Resets: ",jsx(Text,{bold:true,children:c})]}),jsxs(Text,{children:["Default LLM: ",jsx(Text,{bold:true,children:P(m)})]})]}),jsx(Box,{marginTop:1,children:l?jsxs(Text,{dimColor:true,children:["Manage billing: ",jsx(Text,{bold:true,children:"2ndopinion upgrade"})]}):jsxs(Text,{dimColor:true,children:["Upgrade your plan: ",jsx(Text,{bold:true,children:"2ndopinion upgrade"})]})})]})}return null}function Io(){let{waitUntilExit:e}=render(jsx(Lo,{}));e().then(()=>process.exit(0));}var Ao,Ot=C(()=>{te();q();oe();Ee();Ao={free:"Free",pro_100:"Pro 100",pro_unlimited:"Pro Unlimited"};});function ko(e){return createHash("md5").update(e).digest("hex")}function Po(e){return e.split(`
8
+ `).filter(o=>{if(!o.startsWith("+")&&!o.startsWith("-")||o.startsWith("+++")||o.startsWith("---"))return false;let n=o.slice(1).trim();return !(n===""||n.startsWith("//")||n.startsWith("#")||n.startsWith("*"))}).length<No}function Nt(e){let t="",o=null,n=0,i=false;async function r(){if(i)return;let l=Date.now()-n;if(n>0&&l<_t){o&&clearTimeout(o),o=setTimeout(r,_t-l);return}let a;try{a=await Re();}catch{return}if(Po(a))return;let m=ko(a);m!==t&&(t=m,n=Date.now(),e.onDiff(a));}let s=watch(".git/index",{usePolling:false,ignoreInitial:true,awaitWriteFinish:{stabilityThreshold:500}});return s.on("change",()=>{o&&clearTimeout(o),o=setTimeout(r,Uo);}),s.on("error",c=>{e.onError(c.message||"File watcher error");}),()=>{i=true,o&&clearTimeout(o),s.close();}}var No,Uo,_t,Ut=C(()=>{Ae();No=3,Uo=2e3,_t=5e3;});function Go(e){let t=Math.floor((Date.now()-e.getTime())/1e3);if(t<5)return "just now";if(t<60)return `${t}s ago`;let o=Math.floor(t/60);return o<60?`${o}m ago`:`${Math.floor(o/60)}h ago`}function kt(e){if(!e)return "";let t=e.analysis.recommendation.toUpperCase(),o=e.analysis.risks.length,n=e.analysis.risks.filter(r=>r.level==="high").length,i=`Last result: ${t}`;return o>0?(i+=` (${o} risk${o>1?"s":""}`,n>0&&(i+=`, ${n} high`),i+=")"):i+=" (no risks)",i}function ct({llm:e,state:t,result:o,error:n,lastCheck:i,analysisCount:r,riskCount:s,usage:c}){let l="\u2501".repeat(50);return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsxs(Box,{children:[jsx(Text,{bold:true,color:"cyan",children:"2ndOpinion Watch Mode "}),jsx(Text,{bold:true,color:"black",backgroundColor:"green",children:" ACTIVE "})]}),jsx(Text,{dimColor:true,children:l}),jsxs(Box,{flexDirection:"column",marginY:1,children:[t==="idle"&&jsxs(Box,{flexDirection:"column",children:[jsx(Text,{dimColor:true,children:"Watching for git changes..."}),i&&jsxs(Text,{dimColor:true,children:["Last check: ",Go(i)]}),r>0&&jsxs(Text,{dimColor:true,children:["Session: ",r," analys",r===1?"is":"es"," | ",s," risk",s!==1?"s":""," found"]}),o&&jsx(Text,{dimColor:true,children:kt(o)})]}),t==="analyzing"&&jsxs(Box,{flexDirection:"column",children:[jsxs(Box,{children:[jsx(Text,{color:"cyan",children:jsx(ao,{type:"dots"})}),jsxs(Text,{children:[" Analyzing changes with ",P(e),"..."]})]}),r>0&&jsxs(Text,{dimColor:true,children:["Session: ",r," analys",r===1?"is":"es"," so far"]})]}),t==="error"&&jsxs(Box,{flexDirection:"column",children:[jsxs(Text,{color:"red",children:["Error: ",n]}),o&&jsx(Text,{dimColor:true,children:kt(o)})]}),t==="paused"&&jsx(Text,{color:"yellow",bold:true,children:n}),t==="done"&&!o&&jsx(Text,{dimColor:true,children:"Waiting for changes..."})]}),t==="done"&&o&&jsxs(Box,{flexDirection:"column",children:[jsx(be,{analysis:o.analysis,usage:o.usage,llm:e,mode:"watch"}),jsx(Box,{marginTop:1,children:jsx(Text,{dimColor:true,children:"Watching for more changes..."})})]}),jsx(Text,{dimColor:true,children:l}),jsx(Box,{children:jsxs(Text,{dimColor:true,children:["Usage: ",c?`${c.used}/${c.limit===1/0?"\u221E":c.limit}`:"..."," | LLM: ",P(e)," | Analyzed: ",r," | Risks: ",s," | Ctrl+C to stop"]})})]})}var Pt=C(()=>{Qe();oe();});var Mt={};Q(Mt,{runWatch:()=>Yo});function Ko(e){return Math.min(Fo*Math.pow(2,e-1),Vo)}function Ho({llm:e}){let{exit:t}=useApp(),[o,n]=useState(""),[i,r]=useState("idle"),[s,c]=useState(null),[l,a]=useState(""),[m,U]=useState(null),[w,Te]=useState(0),[Y,le]=useState(0),[Vt,dt]=useState(null),ce=useRef(null),$e=useRef(null),De=useRef(0),ze=useRef(""),ve=useCallback(async ue=>{r("analyzing"),a(""),ze.current=ue;let{diff:he,truncated:pt,originalKB:gt}=Ue(ue);pt&&console.error(`[watch] Large diff detected (${gt}KB). Sending first 50KB.`);try{let J=await we(he,"",e,"watch");c(J),dt({used:J.usage.used,limit:J.usage.limit}),U(new Date),Te(b=>b+1),le(b=>b+J.analysis.risks.length),r("done"),De.current=0;}catch(J){let b=J.message||"Analysis failed";if(b.startsWith("LIMIT_REACHED")){r("error"),a(b),ce.current&&ce.current();return}if(b==="NOT_LOGGED_IN"){r("error"),a(b),ce.current&&ce.current();return}De.current+=1;let ft=De.current;if(ft>=qo){r("paused"),a("Multiple failures. Press Enter to retry or Ctrl+C to quit.");return}let xt=Ko(ft),ye=Math.round(xt/1e3);r("error"),a(b==="NETWORK_ERROR"||b==="CONNECTION_REFUSED"?`Backend unreachable. Retrying in ${ye}s...`:b==="TIMEOUT"?`Request timed out. Retrying in ${ye}s...`:b==="RATE_LIMIT"?`Rate limited. Retrying in ${ye}s...`:b==="SERVER_ERROR"?`Server error. Retrying in ${ye}s...`:`${b} \u2014 Retrying in ${ye}s...`),$e.current=setTimeout(()=>{r("idle");},xt);}},[e]);return useInput((ue,he)=>{i==="paused"&&he.return&&(De.current=0,r("idle"),ze.current&&ve(ze.current));}),useEffect(()=>{let ue=false;async function he(){if(!k()){n("NOT_LOGGED_IN");return}if(!await Fe()){n("NOT_GIT_REPO");return}try{let b=await ee();ue||dt({used:b.used,limit:b.limit});}catch{}if(ue)return;let J=Nt({onDiff:ve,onError:b=>{r("error"),a(b);}});ce.current=J;}return he(),()=>{ue=true,ce.current&&ce.current(),$e.current&&clearTimeout($e.current);}},[ve]),o?jsx(F,{error:o}):jsx(ct,{llm:e,state:i,result:s,error:l,lastCheck:m,analysisCount:w,riskCount:Y,usage:Vt})}function Yo(e){let t=e.llm||j(),{waitUntilExit:o}=render(jsx(Ho,{llm:t}));o().then(()=>{process.exit(0);});}var qo,Fo,Vo,Gt=C(()=>{q();Ae();te();Ut();oe();Pt();Ee();qo=5,Fo=5e3,Vo=6e4;});var zt={};Q(zt,{runUpgrade:()=>or});function er(e){return e===1/0?"\u221E":String(e)}function tr(){let{exit:e}=useApp(),[t,o]=useState({phase:"loading"});useEffect(()=>{if(!k()){o({phase:"error",message:"NOT_LOGGED_IN"});return}ee().then(s=>{o({phase:"menu",used:s.used,limit:s.limit,tier:s.tier,resetsAt:s.resetsAt,selected:0});}).catch(s=>{o({phase:"error",message:s.message||"Failed to fetch usage"});});},[]),useEffect(()=>{if(t.phase!=="polling")return;let r=t.targetTier,s=Date.now(),c=12e4,l=setInterval(async()=>{let a=Date.now()-s;if(a>=c){clearInterval(l),o({phase:"error",message:"Timed out waiting for payment confirmation. If you completed payment, run `2ndopinion status` to check."});return}try{(await ee()).tier===r?(clearInterval(l),o({phase:"success",newTier:r})):o(U=>U.phase==="polling"?{...U,elapsed:Math.floor(a/1e3)}:U);}catch{}},3e3);return ()=>clearInterval(l)},[t.phase==="polling"?t.targetTier:null]),useEffect(()=>{if(t.phase==="success"||t.phase==="cancelled"){let r=setTimeout(()=>e(),500);return ()=>clearTimeout(r)}if(t.phase==="error"){let r=setTimeout(()=>e(),100);return ()=>clearTimeout(r)}},[t.phase,e]);let n=useCallback(async r=>{o({phase:"opening"});try{let{checkoutUrl:s}=await He(r);await Wt(s),o({phase:"polling",targetTier:r,elapsed:0});}catch(s){o({phase:"error",message:s.message||"Failed to create checkout session"});}},[]),i=useCallback(async()=>{o({phase:"opening"});try{let{portalUrl:r}=await Ye();await Wt(r),o({phase:"cancelled"});}catch(r){o({phase:"error",message:r.message||"Failed to open billing portal"});}},[]);if(useInput((r,s)=>{if(t.phase!=="menu")return;let c=t.tier!=="free";if(r==="q"||s.escape){o({phase:"cancelled"});return}if(c){(r==="1"||s.return)&&i();return}if(r==="1"){n("pro_100");return}if(r==="2"){n("pro_unlimited");return}if(s.upArrow||s.downArrow){o(l=>l.phase==="menu"?{...l,selected:l.selected===0?1:0}:l);return}if(s.return){let l=t.selected===0?"pro_100":"pro_unlimited";n(l);}}),t.phase==="loading")return jsx(Box,{paddingX:1,children:jsx(Text,{color:"cyan",children:"Loading account info..."})});if(t.phase==="error")return jsx(F,{error:t.message});if(t.phase==="cancelled")return jsx(Box,{paddingX:1,children:jsx(Text,{dimColor:true,children:"Cancelled."})});if(t.phase==="opening")return jsx(Box,{paddingX:1,children:jsx(Text,{color:"cyan",children:"Opening in your browser..."})});if(t.phase==="polling")return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{color:"cyan",children:"Opening Stripe checkout in your browser... Complete payment there."}),jsx(Box,{marginTop:1,children:jsxs(Text,{dimColor:true,children:["Waiting for payment confirmation... (",t.elapsed,"s)"]})})]});if(t.phase==="success"){let r=$t[t.newTier]||t.newTier,s=t.newTier==="pro_100"?"100":"unlimited";return jsx(Box,{paddingX:1,children:jsxs(Text,{color:"green",children:["\u{1F389}"," Upgraded to ",r,"! You now have ",s," requests/month."]})})}if(t.phase==="menu"){let{used:r,limit:s,tier:c,selected:l}=t,a=$t[c]||c,m=c!=="free";return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"cyan",children:"2ndOpinion Upgrade"}),jsx(Box,{marginTop:1,children:jsxs(Text,{children:["Your Plan: ",jsx(Text,{bold:true,children:a})," (",r,"/",er(s)," requests used this month)"]})}),m?jsxs(Box,{flexDirection:"column",marginTop:1,children:[jsx(Text,{children:"You're on a paid plan."}),jsx(Box,{marginTop:1,children:jsxs(Text,{children:["Select: ",jsx(Text,{bold:true,color:"cyan",children:"[1] Manage subscription"})," ",jsx(Text,{dimColor:true,children:"[q] Cancel"})]})})]}):jsxs(Box,{flexDirection:"column",marginTop:1,children:[jsx(Text,{bold:true,children:"Upgrade Options:"}),jsxs(Box,{marginTop:1,flexDirection:"column",children:[jsxs(Box,{children:[jsxs(Text,{color:l===0?"cyan":void 0,bold:l===0,children:[l===0?">":" "," Pro 100"]}),jsx(Text,{dimColor:true,children:" 100 requests/month "}),jsx(Text,{color:"green",children:"$10/mo"})]}),jsxs(Box,{children:[jsxs(Text,{color:l===1?"cyan":void 0,bold:l===1,children:[l===1?">":" "," Pro Unlimited"]}),jsx(Text,{dimColor:true,children:" Unlimited requests "}),jsx(Text,{color:"green",children:"$25/mo"})]})]}),jsx(Box,{marginTop:1,children:jsxs(Text,{children:["Select: ",jsx(Text,{bold:true,children:"[1]"})," Pro 100"," ",jsx(Text,{bold:true,children:"[2]"})," Pro Unlimited"," ",jsx(Text,{dimColor:true,children:"[q] Cancel"})]})})]})]})}return null}function or(){let{waitUntilExit:e}=render(jsx(tr,{}));e().then(()=>process.exit(0));}var $t,vt=C(()=>{te();q();Ee();$t={free:"Free",pro_100:"Pro 100",pro_unlimited:"Pro Unlimited"};});var qt={};Q(qt,{runConfig:()=>ar});function sr({llm:e,apiUrl:t}){let[o,n]=useState([]);useEffect(()=>{let a=[];e&&(["claude","gpt4","gemini"].includes(e)?(ht(e),a.push(`Default LLM set to ${P(e)} (${e})`)):a.push(`Invalid LLM "${e}". Must be one of: claude, gpt4, gemini`)),t&&(Tt(t),a.push(`API URL set to ${t}`)),n(a);},[e,t]);let i=Oe(),r=k(),s=j(),c=Ie();return jsxs(Box,{flexDirection:"column",paddingX:1,children:[jsx(Text,{bold:true,color:"cyan",children:"2ndOpinion Config"}),jsxs(Box,{marginTop:1,flexDirection:"column",children:[jsxs(Text,{children:["Account: ",jsx(Text,{bold:true,children:!!r?i||"logged in":"not logged in"})]}),jsxs(Text,{children:["Default LLM: ",jsx(Text,{bold:true,children:P(s)})," ",jsxs(Text,{dimColor:true,children:["(",s,")"]})]}),jsxs(Text,{children:["API URL: ",jsx(Text,{bold:true,children:c})]})]}),o.length>0&&jsx(Box,{marginTop:1,flexDirection:"column",children:o.map((a,m)=>jsxs(Text,{color:"green",children:["+ ",a]},m))}),o.length===0&&jsxs(Box,{marginTop:1,flexDirection:"column",children:[jsx(Text,{dimColor:true,children:"Options:"}),jsx(Text,{dimColor:true,children:" --llm claude|gpt4|gemini Set default LLM"}),jsxs(Text,{dimColor:true,children:[" --api-url ","<url>"," Set API URL"]})]})]})}function ar(e){let{waitUntilExit:t}=render(jsx(sr,{llm:e.llm,apiUrl:e.apiUrl}));t().then(()=>process.exit(0));}var Ft=C(()=>{q();oe();});var v=new Command;v.name("2ndopinion").description("Get a second AI opinion on your code changes").version("0.1.0").option("--debug","Show debug info (API requests, timing, raw diffs)");v.command("analyze",{isDefault:true}).description("Analyze git changes (staged + unstaged)").option("-l, --llm <provider>","LLM to use: claude, gpt4, gemini").action(e=>{ge();let{runAnalyze:t}=(Lt(),$(At));t(e);});v.command("login").description("Log in to your 2ndOpinion account").action(()=>{ge();let{runLogin:e}=(at(),$(st));e();});v.command("signup").description("Create a new 2ndOpinion account").action(()=>{ge();let{runSignup:e}=(it(),$(nt));e();});v.command("status").description("Show current account info and usage").action(()=>{ge();let{runStatus:e}=(Ot(),$(It));e();});v.command("watch").description("Watch for git changes and analyze continuously").option("-l, --llm <provider>","LLM to use: claude, gpt4, gemini").action(e=>{ge();let{runWatch:t}=(Gt(),$(Mt));t(e);});v.command("upgrade").description("Upgrade your plan or manage your subscription").action(()=>{ge();let{runUpgrade:e}=(vt(),$(zt));e();});v.command("config").description("Show or update CLI configuration").option("-l, --llm <provider>","Set default LLM: claude, gpt4, gemini").option("--api-url <url>","Set API URL").action(e=>{ge();let{runConfig:t}=(Ft(),$(qt));t(e);});function ge(){if(v.opts().debug){let{setDebug:t}=(te(),$(wt));t(true);}}v.parse();
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "2ndopinion-cli",
3
+ "version": "0.1.0",
4
+ "description": "Get a 2nd AI opinion on your code changes",
5
+ "type": "module",
6
+ "bin": {
7
+ "2ndopinion": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "keywords": [
14
+ "code-review",
15
+ "ai",
16
+ "llm",
17
+ "git",
18
+ "cli",
19
+ "developer-tools"
20
+ ],
21
+ "author": "bdubtronux",
22
+ "license": "UNLICENSED",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/bdubtronux/2ndopinion"
26
+ },
27
+ "homepage": "https://get2ndopinion.dev",
28
+ "bugs": "https://github.com/bdubtronux/2ndopinion/issues",
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "dependencies": {
33
+ "axios": "^1.7.0",
34
+ "chalk": "^4.1.2",
35
+ "chokidar": "^3.6.0",
36
+ "commander": "^12.1.0",
37
+ "conf": "^10.2.0",
38
+ "ink": "^4.4.1",
39
+ "ink-spinner": "^5.0.0",
40
+ "ink-text-input": "^5.0.1",
41
+ "open": "^11.0.0",
42
+ "react": "^18.2.0",
43
+ "simple-git": "^3.27.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.17.0",
47
+ "@types/react": "^18.2.0",
48
+ "tsup": "^8.0.0",
49
+ "typescript": "^5.7.0",
50
+ "@2ndopinion/shared": "0.1.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "dev": "tsup --watch"
55
+ }
56
+ }