UNPKG

ccrotate

Version:

A simple CLI tool to manage and rotate multiple Claude Code accounts, helping you bypass rate limits

9 lines (8 loc) 20.1 kB
#!/usr/bin/env node import{Command as ge}from"commander";import C from"chalk";import l from"fs";import b from"path";import $ from"os";import{spawn as pe,execSync as fe}from"child_process";import M from"chalk";import Y from"prompts";var v=class{constructor(e){this.ccrotate=e}async execute(e=!1){let t=this.ccrotate.getCurrentAccount(),o=this.ccrotate.loadProfiles();if(o[t.email]&&!e&&!(await Y({type:"confirm",name:"overwrite",message:`Account ${t.email} already exists. Overwrite?`,initial:!1})).overwrite){console.log(M.yellow("Operation cancelled."));return}o[t.email]={credentials:t.credentials,userId:t.userId,oauthAccount:t.oauthAccount,lastUsed:new Date().toISOString()},this.ccrotate.saveProfiles(o),console.log(M.green(`\u2713 Account ${t.email} saved successfully.`))}};import X from"react";import{render as Z}from"ink";import u from"react";import{Box as g,Text as f}from"ink";var Q=({accounts:i,currentEmail:e})=>{if(i.length===0)return u.createElement(g,{flexDirection:"column",marginTop:1,marginBottom:1},u.createElement(f,{color:"yellow"},"No saved accounts found."),u.createElement(f,{color:"blue"},"Please login with claude-code and run `ccrotate snap` to add your first account."));let t=u.createElement(g,null,u.createElement(g,{width:"3%"},u.createElement(f,{bold:!0,color:"gray"},"#")),u.createElement(g,{width:"3%"},u.createElement(f,{bold:!0,color:"gray"},"\u2605")),u.createElement(g,{width:"35%"},u.createElement(f,{bold:!0,color:"gray"},"Email")),u.createElement(g,{width:"25%"},u.createElement(f,{bold:!0,color:"gray"},"Last Used")),u.createElement(g,{width:"34%"},u.createElement(f,{bold:!0,color:"gray"},"Expires At (KST)"))),o=u.createElement(g,null,u.createElement(f,{color:"gray"},"\u2500".repeat(80))),s=i.map((c,r)=>{let n=c.email===e,a=n?"\u2605":" ",d=n?"green":"gray",h=n?"green":"white";return u.createElement(g,{key:c.email},u.createElement(g,{width:"3%"},u.createElement(f,{color:"gray"},r+1)),u.createElement(g,{width:"3%"},u.createElement(f,{color:d},a)),u.createElement(g,{width:"35%"},u.createElement(f,{color:h},c.email)),u.createElement(g,{width:"25%"},u.createElement(f,{color:"gray"},c.lastUsed)),u.createElement(g,{width:"34%"},u.createElement(f,{color:"yellow"},c.expiresAt)))});return u.createElement(g,{flexDirection:"column",marginTop:1},u.createElement(f,{bold:!0,color:"white"},"\u{1F4CB} Saved Accounts"),u.createElement(f,null," "),t,o,...s,u.createElement(f,null," "))},W=Q;function z(i){if(!i)return"Unknown";try{let e=new Date(i);if(isNaN(e.getTime()))return"Invalid";let t=new Date(e.getTime()+540*60*1e3),o=t.getUTCFullYear(),s=String(t.getUTCMonth()+1).padStart(2,"0"),c=String(t.getUTCDate()).padStart(2,"0"),r=String(t.getUTCHours()).padStart(2,"0"),n=String(t.getUTCMinutes()).padStart(2,"0"),a=String(t.getUTCSeconds()).padStart(2,"0");return`${o}-${s}-${c} ${r}:${n}:${a}`}catch{return"Invalid"}}var O=class{constructor(e){this.ccrotate=e}async execute(){let e=this.ccrotate.loadProfiles(),t=Object.keys(e),o;try{o=this.ccrotate.getCurrentAccount().email}catch{o=null}let s=t.map(r=>{let n=e[r],a=new Date(n.lastUsed).toLocaleDateString(),d="Unknown";try{n.credentials&&n.credentials.claudeAiOauth&&n.credentials.claudeAiOauth.expiresAt&&(d=z(n.credentials.claudeAiOauth.expiresAt))}catch{d="Invalid"}return{email:r,lastUsed:a,expiresAt:d}}),c=X.createElement(W,{accounts:s,currentEmail:o});Z(c)}};import F from"chalk";var k=class{constructor(e){this.ccrotate=e}async execute(e){let t=this.ccrotate.loadProfiles();if(!t[e])throw new Error(`Account ${e} not found. Run 'ccrotate list' to see available accounts.`);let o=t[e];this.ccrotate.writeClaudeFiles(o),t[e].lastUsed=new Date().toISOString(),this.ccrotate.saveProfiles(t),console.log(F.green(`\u2713 Switched to account: ${e}`)),console.log(F.blue(` \u{1F4A1} Next steps:`)),console.log(F.gray(" \u2022 Restart claude-code to apply account changes")),console.log(F.gray(" \u2022 To resume previous conversation: Use")+F.cyan(" /resume")+F.gray(" command"))}};import S from"chalk";var U=class{constructor(e){this.ccrotate=e}async execute(){let e=this.ccrotate.loadProfiles(),t=Object.keys(e);if(t.length===0)throw new Error("No saved accounts found. Please add accounts first using `ccrotate snap`.");if(t.length===1){console.log(S.yellow("Only one account available. Nothing to switch to."));return}let o;try{o=this.ccrotate.getCurrentAccount().email}catch{o=null}let s=0;o&&e[o]&&(s=(t.indexOf(o)+1)%t.length);let c=t[s];console.log(S.blue(`Switching: ${o||"unknown"} -> ${c}`));let r=e[c];this.ccrotate.writeClaudeFiles(r),e[c].lastUsed=new Date().toISOString(),this.ccrotate.saveProfiles(e),console.log(S.green(`\u2713 Switched to account: ${c}`)),console.log(S.blue(` \u{1F4A1} Next steps:`)),console.log(S.gray(" \u2022 Restart claude-code to apply account changes")),console.log(S.gray(" \u2022 To resume previous conversation: Use")+S.cyan(" /resume")+S.gray(" command"))}};import B from"chalk";import ee from"prompts";var T=class{constructor(e){this.ccrotate=e}async execute(e){let t=this.ccrotate.loadProfiles();if(!t[e])throw new Error(`Account ${e} not found.`);if(!(await ee({type:"confirm",name:"remove",message:`Are you sure you want to remove account ${e}?`,initial:!1})).remove){console.log(B.yellow("Operation cancelled."));return}delete t[e],this.ccrotate.saveProfiles(t),console.log(B.green(`\u2713 Account ${e} removed successfully.`))}};import se from"react";import{render as ce}from"ink";import m,{useState as te,useEffect as re}from"react";import{Box as y,Text as w}from"ink";var oe=({accounts:i,onTestAccount:e,onComplete:t})=>{let[o,s]=te(()=>i.map(r=>({email:r.email,status:"pending",result:"...",credentialsUpdated:!1})));re(()=>{(async()=>{for(let n of i){s(a=>a.map(d=>d.email===n.email?{...d,status:"testing"}:d));try{let a=await e(n.email);s(d=>d.map(h=>h.email===n.email?{...h,status:a.status,result:a.response.substring(0,150),credentialsUpdated:a.credentialsUpdated||!1}:h))}catch(a){s(d=>d.map(h=>h.email===n.email?{...h,status:"error",result:a.message.substring(0,150),credentialsUpdated:!1}:h))}}t()})()},[i,e,t]);let c=(r,n)=>{let a;switch(r){case"pending":a={text:"\u23F3 Pending",color:"gray"};break;case"testing":a={text:"\u{1F504} Testing",color:"yellow"};break;case"success":a={text:"\u2705 Active",color:"green"};break;case"error":a={text:"\u274C Failed",color:"red"};break;default:a={text:"\u2753 Unknown",color:"gray"}}return n&&(r==="success"||r==="error")&&(a.text+=" \u{1F504}"),a};return m.createElement(y,{flexDirection:"column",marginTop:1},m.createElement(w,{bold:!0,color:"blue"},"\u{1F504} Testing accounts and refreshing tokens..."),m.createElement(w,null," "),m.createElement(y,null,m.createElement(y,{width:"3%"},m.createElement(w,{bold:!0,color:"gray"},"#")),m.createElement(y,{width:"3%"},m.createElement(w,{bold:!0,color:"gray"}," ")),m.createElement(y,{width:"30%"},m.createElement(w,{bold:!0,color:"gray"},"Email")),m.createElement(y,{width:"15%"},m.createElement(w,{bold:!0,color:"gray"},"Status")),m.createElement(y,{width:"49%"},m.createElement(w,{bold:!0,color:"gray"},"Result"))),m.createElement(y,null,m.createElement(w,{color:"gray"},"\u2500".repeat(80))),...o.map((r,n)=>{let a=c(r.status,r.credentialsUpdated);return m.createElement(y,{key:r.email},m.createElement(y,{width:"3%"},m.createElement(w,{color:"gray"},n+1)),m.createElement(y,{width:"3%"},m.createElement(w,{color:"gray"}," ")),m.createElement(y,{width:"30%"},m.createElement(w,{color:"white"},r.email)),m.createElement(y,{width:"15%"},m.createElement(w,{color:a.color},a.text)),m.createElement(y,{width:"49%"},m.createElement(w,{color:"gray"},r.result+(r.credentialsUpdated?" (Updated)":""))))}),m.createElement(w,null," "))},_=oe;var N=class{constructor(e){this.ccrotate=e}async execute(){try{await new v(this.ccrotate).execute(!0)}catch(r){console.log(`Note: Could not save current account - ${r.message}`)}let e=this.ccrotate.loadProfiles(),t=Object.keys(e);if(t.length===0)throw new Error("No saved accounts found. Please add accounts first using `ccrotate snap`.");let o;try{o=this.ccrotate.getCurrentAccount().email}catch{o=null}let s=this.ccrotate.backupCurrentCredentials(),c=t.map(r=>({email:r}));return new Promise(r=>{let n=async h=>{try{let p=e[h];return this.ccrotate.writeClaudeFiles(p),await this.ccrotate.testAccount(h)}catch(p){return{status:"error",response:p.message.substring(0,150)}}},a=()=>{if(o){let h=this.ccrotate.loadProfiles();if(h[o]){let p=h[o];this.ccrotate.writeClaudeFiles(p)}else this.ccrotate.restoreCredentials(s)}else this.ccrotate.restoreCredentials(s);r()},d=se.createElement(_,{accounts:c,onTestAccount:n,onComplete:a});ce(d)})}};import D from"chalk";import ne from"msgpack-lite";import{gzipSync as ae}from"zlib";import{createHash as ie}from"crypto";var I=class{constructor(e){this.ccrotate=e}optimizeProfiles(e){let t={};for(let[o,s]of Object.entries(e)){let c=s.credentials?.claudeAiOauth,r=s.oauthAccount;!c||!r||(t[o]={c:{a:c.accessToken,r:c.refreshToken,e:c.expiresAt,s:c.scopes,t:c.subscriptionType},o:{u:r.accountUuid,e:r.emailAddress,g:r.organizationUuid,r:r.organizationRole,w:r.workspaceRole,n:r.organizationName},l:s.lastUsed})}return t}async execute(){let e=this.ccrotate.loadProfiles();if(Object.keys(e).length===0){console.log(D.yellow("No profiles found to export."));return}try{let t=JSON.stringify(e),o=this.optimizeProfiles(e),s=ne.encode(o),r=ae(s).toString("base64"),n=Object.keys(e).sort().reduce((p,x)=>(p[x]=e[x],p),{}),a=JSON.stringify(n),d=ie("md5").update(a).digest("hex").slice(0,8),h=`${d}:${r}`;console.log(D.green("\u2713 Profiles exported (Shell-Safe compression + CRC verification):")),console.log(D.dim(`${Object.keys(e).length} accounts: ${t.length} \u2192 ${h.length} chars (-${Math.round((1-h.length/t.length)*100)}%)`)),console.log(D.dim(`CRC32: ${d} (data integrity guaranteed)`)),console.log(),console.log('"mp-gz-b64:'+h+'"')}catch(t){throw new Error(`Failed to export profiles: ${t.message}`)}}};import P from"chalk";import le from"prompts";import ue from"msgpack-lite";import{gunzipSync as de}from"zlib";import{createHash as me}from"crypto";import he from"fs";var R=class{constructor(e){this.ccrotate=e}restoreProfiles(e){let t={};for(let[o,s]of Object.entries(e))t[o]={credentials:{claudeAiOauth:{accessToken:s.c.a,refreshToken:s.c.r,expiresAt:s.c.e,scopes:s.c.s,subscriptionType:s.c.t}},oauthAccount:{accountUuid:s.o.u,emailAddress:s.o.e,organizationUuid:s.o.g,organizationRole:s.o.r,workspaceRole:s.o.w,organizationName:s.o.n},lastUsed:s.l};return t}async execute(e){if(!e)throw new Error("No compressed data provided. Usage: ccrotate import <compressed-data>");let t=e.trim();if((t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1)),!t.startsWith("mp-gz-b64:"))throw new Error("Invalid data format. Expected mp-gz-b64: prefix.");let o=t.slice(10),s=o.indexOf(":");if(s===-1)throw new Error("Invalid data format. Missing CRC hash.");let c=o.slice(0,s),r=o.slice(s+1);if(c.length!==8)throw new Error("Invalid CRC hash format. Expected 8 characters.");let n;try{let p=Buffer.from(r,"base64"),x=de(p),G=ue.decode(x);n=this.restoreProfiles(G);let q=Object.keys(n).sort().reduce((L,H)=>(L[H]=n[H],L),{}),K=JSON.stringify(q),j=me("md5").update(K).digest("hex").slice(0,8);if(j!==c)throw new Error(`CRC verification failed. Expected: ${c}, Got: ${j}. Data may be corrupted.`);console.log(P.green(`\u2713 CRC verification passed: ${j}`))}catch(p){throw new Error(`Failed to parse imported data: ${p.message}`)}if(typeof n!="object"||n===null)throw new Error("Invalid profile data structure.");for(let[p,x]of Object.entries(n)){if(!x.credentials?.claudeAiOauth||!x.oauthAccount)throw new Error(`Invalid profile structure for ${p}. Missing required fields.`);if(!x.oauthAccount.emailAddress)throw new Error(`Invalid OAuth account structure for ${p}.`)}let a=Object.keys(n).length,d=Object.keys(n).join(", ");if(console.log(P.blue(`Found ${a} accounts to import:`)),console.log(P.dim(d)),console.log(),he.existsSync(this.ccrotate.profilesFile)&&console.log(P.yellow("Warning: This will replace existing profile data.")),!(await le({type:"confirm",name:"proceed",message:"Do you want to proceed with the import?",initial:!1})).proceed){console.log(P.yellow("Import cancelled."));return}try{this.ccrotate.saveProfiles(n),console.log(P.green(`\u2713 Successfully imported ${a} accounts.`))}catch(p){throw new Error(`Failed to save imported profiles: ${p.message}`)}}};var J=class{constructor(){this.profilesDir=b.join($.homedir(),".ccrotate"),this.profilesFile=b.join(this.profilesDir,"profiles.json"),this.claudeDir=b.join($.homedir(),".claude"),this.credentialsFile=b.join(this.claudeDir,".credentials.json"),this.claudeConfigFile=b.join($.homedir(),".claude.json"),this.claudePath=null,this.commands={snap:new v(this),list:new O(this),switch:new k(this),next:new U(this),remove:new T(this),refresh:new N(this),export:new I(this),import:new R(this)}}findClaudePath(){if(this.claudePath)return this.claudePath;let e=[b.join($.homedir(),".claude/local/claude"),"/usr/local/bin/claude","/opt/homebrew/bin/claude",b.join($.homedir(),"bin/claude"),"/usr/bin/claude"];for(let t of e)try{if(l.existsSync(t)&&l.statSync(t).mode&73)return this.claudePath=t,t}catch{continue}try{let t=process.env.SHELL||"/bin/bash",o=t.includes("zsh")?"~/.zshrc":t.includes("bash")?"~/.bashrc":"~/.profile",c=$.platform()==="win32"?"where claude":"which claude",r=fe(`source ${o} && ${c}`,{encoding:"utf8",shell:t,env:process.env,timeout:5e3}).trim();if(r){let n=r.match(/aliased to (.+)$/);if(n){let d=n[1].trim();if(l.existsSync(d))return this.claudePath=d,d}let a=r.split(` `)[0].trim();if(a&&l.existsSync(a))return this.claudePath=a,a}}catch{}if(process.env.CLAUDE_PATH){let t=process.env.CLAUDE_PATH;try{if(l.existsSync(t)&&l.statSync(t).mode&73)return this.claudePath=t,t}catch{}}throw new Error(`Claude executable not found. Please try: 1. Reinstall claude-code: npm install -g @anthropic/claude-code 2. Set custom path: export CLAUDE_PATH="/path/to/claude" 3. Check installation: which claude`)}ensureProfilesDir(){l.existsSync(this.profilesDir)||l.mkdirSync(this.profilesDir,{recursive:!0})}loadProfiles(){if(!l.existsSync(this.profilesFile))return{};try{let e=l.readFileSync(this.profilesFile,"utf8");return JSON.parse(e)}catch(e){throw new Error(`Failed to parse profiles.json: ${e.message}`)}}saveProfiles(e){this.ensureProfilesDir();try{let t=this.profilesFile+".tmp";l.writeFileSync(t,JSON.stringify(e,null,2),"utf8"),l.renameSync(t,this.profilesFile)}catch(t){throw new Error(`Failed to save profiles: ${t.message}`)}}getCurrentAccount(){if(!l.existsSync(this.credentialsFile))throw new Error("No active Claude account found. Please login with claude-code first.");if(!l.existsSync(this.claudeConfigFile))throw new Error("Claude config file not found. Please login with claude-code first.");try{let e=JSON.parse(l.readFileSync(this.credentialsFile,"utf8")),t=JSON.parse(l.readFileSync(this.claudeConfigFile,"utf8"));if(!t.oauthAccount||!t.oauthAccount.emailAddress)throw new Error("No OAuth account information found in Claude config.");return{email:t.oauthAccount.emailAddress,credentials:e,userId:t.userId,oauthAccount:t.oauthAccount}}catch(e){throw new Error(`Failed to read current account: ${e.message}`)}}writeClaudeFiles(e){try{let t=this.credentialsFile+".tmp",o=this.claudeConfigFile+".tmp";l.writeFileSync(t,JSON.stringify(e.credentials,null,2),"utf8");let c={...l.existsSync(this.claudeConfigFile)?JSON.parse(l.readFileSync(this.claudeConfigFile,"utf8")):{},userId:e.userId,oauthAccount:e.oauthAccount};l.writeFileSync(o,JSON.stringify(c,null,2),"utf8"),l.renameSync(t,this.credentialsFile),l.renameSync(o,this.claudeConfigFile)}catch(t){throw new Error(`Failed to write account files: ${t.message}`)}}async snap(e=!1){return this.commands.snap.execute(e)}async list(){return this.commands.list.execute()}async switch(e){return this.commands.switch.execute(e)}async next(){return this.commands.next.execute()}async remove(e){return this.commands.remove.execute(e)}backupCurrentCredentials(){let e={credentials:null,config:null};try{l.existsSync(this.credentialsFile)&&(e.credentials=l.readFileSync(this.credentialsFile,"utf8")),l.existsSync(this.claudeConfigFile)&&(e.config=l.readFileSync(this.claudeConfigFile,"utf8"))}catch(t){throw new Error(`Failed to backup current credentials: ${t.message}`)}return e}restoreCredentials(e){try{e.credentials&&l.existsSync(this.credentialsFile)&&l.writeFileSync(this.credentialsFile,e.credentials,"utf8"),e.config&&l.existsSync(this.claudeConfigFile)&&l.writeFileSync(this.claudeConfigFile,e.config,"utf8")}catch(t){throw new Error(`Failed to restore credentials: ${t.message}`)}}async testAccount(e){return new Promise(t=>{let s=`${this.findClaudePath()} -p "Only say Hi" --model sonnet`,c=pe(s,{stdio:["pipe","pipe","pipe"],shell:!0});c.stdin.end();let r="";c.stdout.on("data",a=>{r+=a.toString()}),c.stderr.on("data",a=>{r+=a.toString()});let n=setTimeout(()=>{c.kill("SIGTERM"),t({status:"error",response:"Command timeout after 30 seconds",credentialsUpdated:!1})},3e4);c.on("close",a=>{clearTimeout(n);let d=this.checkAndUpdateProfile(e);if(a!==0){t({status:"error",response:(r||"Command failed").substring(0,150),credentialsUpdated:d});return}let h=r.trim();if(h.length===0){t({status:"error",response:"No response received",credentialsUpdated:d});return}t({status:"success",response:h.substring(0,150),credentialsUpdated:d})}),c.on("error",a=>{clearTimeout(n),t({status:"error",response:a.message.substring(0,150),credentialsUpdated:!1})})})}checkAndUpdateProfile(e){try{let t=this.loadProfiles();if(!t[e])return!1;let o=JSON.parse(l.readFileSync(this.credentialsFile,"utf8")),s=JSON.parse(l.readFileSync(this.claudeConfigFile,"utf8")),c=t[e],r=JSON.stringify(c.credentials)!==JSON.stringify(o),n=JSON.stringify(c.oauthAccount)!==JSON.stringify(s.oauthAccount)||c.userId!==s.userId;return r||n?(t[e]={...c,credentials:o,userId:s.userId,oauthAccount:s.oauthAccount,lastUsed:new Date().toISOString()},this.saveProfiles(t),!0):!1}catch(t){return console.error(`Error checking profile update for ${e}:`,t.message),!1}}async refresh(){return this.commands.refresh.execute()}async export(){return this.commands.export.execute()}async import(e){return this.commands.import.execute(e)}},V=J;var ye="1.0.13",E=new ge,A=new V;E.name("ccrotate").description("A simple CLI tool to manage and rotate multiple Claude Code accounts, helping you bypass rate limits").version(ye,"-v, --version","output the version number");E.command("snap").description("Save current account information").option("--force","Skip confirmation prompt when overwriting existing account").action(async i=>{try{await A.snap(i.force)}catch(e){console.error(C.red(`Error: ${e.message}`)),process.exit(1)}});E.command("list").alias("ls").description("Show saved accounts").action(async()=>{try{await A.list()}catch(i){console.error(C.red(`Error: ${i.message}`)),process.exit(1)}});E.command("switch <email>").description("Switch to specific account").action(async i=>{try{await A.switch(i)}catch(e){console.error(C.red(`Error: ${e.message}`)),process.exit(1)}});E.command("next").description("Switch to next account in rotation").action(async()=>{try{await A.next()}catch(i){console.error(C.red(`Error: ${i.message}`)),process.exit(1)}});E.command("remove <email>").alias("rm").description("Remove saved account").action(async i=>{try{await A.remove(i)}catch(e){console.error(C.red(`Error: ${e.message}`)),process.exit(1)}});E.command("refresh").alias("rf").description("Test all accounts and refresh tokens").action(async()=>{try{await A.refresh()}catch(i){console.error(C.red(`Error: ${i.message}`)),process.exit(1)}});E.command("export").description("Export all profiles as compressed string").action(async()=>{try{await A.export()}catch(i){console.error(C.red(`Error: ${i.message}`)),process.exit(1)}});E.command("import <data>").description("Import profiles from compressed string").action(async i=>{try{await A.import(i)}catch(e){console.error(C.red(`Error: ${e.message}`)),process.exit(1)}});E.parse();process.argv.slice(2).length||E.outputHelp();