@maol-1997/remote-cc 0.1.0 → 0.1.2

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 (2) hide show
  1. package/cli.js +3 -3
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{ConvexClient as Ct}from"convex/browser";import{randomUUID as kt}from"crypto";import{anyApi as xe,componentsGeneric as _e}from"convex/server";var m=xe;var bt=_e();var j="https://good-emu-457.convex.cloud";import{chmodSync as Ce,existsSync as ke,mkdirSync as Ee,readFileSync as Te,writeFileSync as be}from"fs";import{homedir as Ie}from"os";import{join as F}from"path";var z=F(Ie(),".remote-cc"),I=F(z,"config.json");function B(){try{return ke(I)?JSON.parse(Te(I,"utf8")):null}catch{return null}}function J(e){Ee(z,{recursive:!0}),be(I,JSON.stringify(e,null,2));try{Ce(I,384)}catch{}}var G=["default","acceptEdits","plan"],H=["opus","sonnet","haiku","claude-fable-5"];async function K(e,t){switch(t.type){case"start_session":await e.start(x(t),W(t));break;case"resume_session":await e.resume(x(t),Ae(t));break;case"stop_session":t.sessionId&&await e.stop(x(t));break;case"send_message":await e.sendMessage(x(t),Re(t).text);break;case"interrupt_turn":await e.interrupt(x(t));break;case"set_permission_mode":break;case"set_model":await e.setModel(x(t),Pe(t).model);break;default:throw new Error(`unknown command: ${t.type}`)}}function x(e){if(!e.sessionId)throw new Error(`${e.type} without sessionId`);return e.sessionId}function W(e){let t=A(e);return{name:_(e,t,"name"),cwd:_(e,t,"cwd"),mode:Me(e,t)}}function Ae(e){let t=A(e);return{...W(e),claudeSessionId:_(e,t,"claudeSessionId")}}function Re(e){return{text:_(e,A(e),"text")}}function A(e){if(typeof e.args!="object"||e.args===null)throw new Error(`${e.type} with malformed args`);return e.args}function _(e,t,s){let n=t[s];if(typeof n!="string"||!n)throw new Error(`${e.type} without ${s}`);return n}function Me(e,t){let s=_(e,t,"mode");if(!G.includes(s))throw new Error(`${e.type} with unknown mode "${s}"`);return s}function Pe(e){let t=_(e,A(e),"model");if(!H.includes(t))throw new Error(`${e.type} with unknown model "${t}"`);return{model:t}}function Y(e){return e.replace(/\.convex\.cloud\/?$/,".convex.site")}import{execFileSync as $e}from"child_process";import{arch as De,homedir as qe,release as D,type as Ve}from"os";import{existsSync as Oe}from"fs";import{homedir as Ne}from"os";import{join as Ue}from"path";function R(){let e=Ue(Ne(),".local","bin","claude");return Oe(e)?e:"claude"}var Le="0.1.0";function X(){return{platform:`${je()} \xB7 ${De()}`,claudeVersion:Fe(),baseDir:qe(),agentVersion:Le}}function je(){let e=Ve();return e==="Darwin"?`macOS ${D()}`:e==="Linux"?`Linux ${D()}`:`${e} ${D()}`}function Fe(){try{let t=$e(R(),["--version"],{encoding:"utf8",timeout:5e3}).match(/\d+\.\d+\.\d+/);return t?t[0]:void 0}catch{return}}async function Z(e,t,s,n){let o=await fetch(`${e}/pair/claim`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:t,name:s,platform:n.platform,claudeVersion:n.claudeVersion,baseDir:n.baseDir,agentVersion:n.agentVersion})}),r=await o.text(),a;try{a=JSON.parse(r)}catch{a={}}if(!o.ok)throw new Error(`pairing failed (${o.status}): ${a.error??r.slice(0,200)}`);let{deviceToken:u,deviceId:i,userId:p,name:l}=a;if(!u||!i||!p||!l)throw new Error(`pairing response is missing fields: ${r.slice(0,200)}`);return{deviceToken:u,deviceId:i,userId:p,name:l}}function Q(e,t,s){let n=async u=>{try{await e.mutation(m.devices.heartbeat,{deviceToken:t,...u})}catch(i){console.error("heartbeat error:",i instanceof Error?i.message:i)}},o=()=>n({});n({platform:s.platform,claudeVersion:s.claudeVersion,baseDir:s.baseDir,agentVersion:s.agentVersion});let a=setInterval(()=>{o()},2e4);return()=>clearInterval(a)}import{randomUUID as ot}from"crypto";import{chmodSync as it,existsSync as he,mkdirSync as at,readFileSync as ye,realpathSync as ct,writeFileSync as dt}from"fs";import{createRequire as ut}from"module";import{homedir as ve,tmpdir as lt}from"os";import{dirname as pt,join as U,resolve as mt}from"path";import gt from"node-pty";import{randomUUID as b}from"crypto";import{readFileSync as se}from"fs";import{homedir as re}from"os";import{join as oe}from"path";import{execFileSync as ee}from"child_process";var te="Claude Code-credentials";function Be(){let e="";try{e=ee("/usr/bin/security",["dump-keychain"],{encoding:"utf8",maxBuffer:64*1024*1024})}catch{}let t=new Set;for(let s of e.split(/\nkeychain: /)){if(!s.includes(`"svce"<blob>="${te}"`))continue;let n=s.match(/"acct"<blob>="([^"]*)"/);n&&t.add(n[1])}return[...t]}function Je(e){let t=["find-generic-password","-s",te];e&&t.push("-a",e),t.push("-w");try{return{account:e,raw:ee("/usr/bin/security",t,{encoding:"utf8"}).trim()}}catch{return null}}function ne(){let e=Be();return e.length||e.push(void 0),e.map(Je).filter(t=>t!==null)}var Ge=oe(re(),".claude",".credentials.json");function ie(e,t){let s;try{s=JSON.parse(e)?.claudeAiOauth}catch{return null}if(typeof s!="object"||s===null)return null;let{accessToken:n,refreshToken:o,expiresAt:r,scopes:a}=s;return typeof n!="string"||!n||typeof r!="number"||Number.isNaN(r)?null:{account:t,accessToken:n,refreshToken:typeof o=="string"?o:void 0,expiresAt:r,scopes:Array.isArray(a)?a.filter(u=>typeof u=="string"):void 0}}function He(){try{return ie(se(Ge,"utf8"))}catch{return null}}function Ke(){return ne().map(e=>ie(e.raw,e.account)).filter(e=>e!==null)}function We(){return JSON.parse(se(oe(re(),".claude.json"),"utf8"))?.oauthAccount?.organizationUuid}function M(){let e=process.platform==="darwin"?Ke():[];if(!e.length){let n=He();n&&e.push(n)}if(!e.length)throw new Error("no claude.ai credentials (Keychain on macOS / ~/.claude/.credentials.json on Linux). Run `claude` and log in.");e.sort((n,o)=>o.expiresAt-n.expiresAt);let s=e.find(n=>n.expiresAt>Date.now())??e[0];return{...s,orgUuid:We(),isExpired:s.expiresAt<=Date.now()}}var C=null;function Ye(){if((!C||C.expiresAt-Date.now()<6e4)&&(C=M(),C.isExpired))throw new Error("No valid claude.ai token (Keychain on macOS / ~/.claude/.credentials.json on Linux). You need a running Claude Code instance (an agent session works) to refresh and persist it.");return C}function Xe(){C=null}function Ze(){let e=Ye();return{Authorization:`Bearer ${e.accessToken}`,"Content-Type":"application/json","anthropic-version":"2023-06-01","anthropic-beta":"ccr-byoc-2025-07-29","x-organization-uuid":e.orgUuid??""}}async function P(e,t={}){let s=()=>{let o=new Headers(t.headers);for(let[r,a]of Object.entries(Ze()))o.has(r)||o.set(r,a);return{...t,headers:o}},n=await fetch(e,s());return n.status===401&&(Xe(),n=await fetch(e,s())),n}var y={controlRequest:"control_request",controlCancelRequest:"control_cancel_request",controlResponse:"control_response",result:"result",streamEvent:"stream_event",system:"system",user:"user"},T={canUseTool:"can_use_tool",interrupt:"interrupt",setModel:"set_model"};var q="https://api.anthropic.com";async function ae(e,t){let s={uuid:b(),session_id:e,type:y.user,message:{role:"user",content:t}},n=await P(`${q}/v1/sessions/${e}/events`,{method:"POST",body:JSON.stringify({events:[s]})}),o=await n.text();if(!n.ok)throw new Error(`POST events ${n.status}: ${o.slice(0,200)}`);return{status:n.status,body:o?Qe(o):null,sentUuid:s.uuid}}async function ce(e){let t={type:y.controlRequest,request_id:b(),request:{subtype:T.interrupt},uuid:b()},s=await P(`${q}/v1/sessions/${e}/events`,{method:"POST",body:JSON.stringify({events:[t]})}),n=await s.text();if(!s.ok)throw new Error(`POST interrupt ${s.status}: ${n.slice(0,200)}`)}async function de(e,t){let s={type:y.controlRequest,request_id:b(),request:{subtype:T.setModel,model:t},uuid:b()},n=await P(`${q}/v1/sessions/${e}/events`,{method:"POST",body:JSON.stringify({events:[s]})}),o=await n.text();if(!n.ok)throw new Error(`POST set_model ${n.status}: ${o.slice(0,200)}`)}function Qe(e){try{return JSON.parse(e)}catch{return e}}function k(e){return typeof e=="object"&&e!==null}function v(e){return typeof e=="string"?e:void 0}function ue(e){let t=k(e.message)?e.message:void 0,s=v(e.type),n=v(t?.role)||s||"system",o=t?.content,r=[];if(typeof o=="string")r.push({kind:"text",text:o});else if(Array.isArray(o))for(let a of o){if(!k(a))continue;let u=v(a.type)??"unknown";u==="text"?r.push({kind:"text",text:v(a.text)??""}):u==="thinking"?r.push({kind:"thinking",text:v(a.thinking)??""}):u==="tool_use"?r.push({kind:"tool_use",text:`${v(a.name)??"?"} ${JSON.stringify(a.input||{}).slice(0,300)}`}):u==="tool_result"?r.push({kind:"tool_result",text:et(a.content)}):r.push({kind:u,text:JSON.stringify(a).slice(0,200)})}return{uuid:v(e.uuid)??null,type:s,role:n,parts:r}}function et(e){return typeof e=="string"?e:Array.isArray(e)?e.map(t=>k(t)?t.type==="text"?v(t.text)??"":`[${v(t.type)??"unknown"}]`:"[unknown]").join(`
3
- `):JSON.stringify(e||"").slice(0,300)}import le from"ws";var tt=3e4,pe=2e3,nt=5,O=4003;function st(e){return e===O?pe*2:pe}function me(e,{onEvent:t,onStatus:s}={}){let n=null,o=null,r=0,a=!1;function u(){let i;try{i=M()}catch(l){s?.("unauthorized",l instanceof Error?l.message:String(l));return}let p=`wss://api.anthropic.com/v1/sessions/ws/${e}/subscribe?organization_uuid=${i.orgUuid}`;n=new le(p,{headers:{Authorization:`Bearer ${i.accessToken}`,"anthropic-version":"2023-06-01"}}),n.on("open",()=>{r=0,s?.("connected"),o=setInterval(()=>{try{n?.ping()}catch{}},tt)}),n.on("message",l=>{let f;try{f=JSON.parse(l.toString())}catch{return}f&&typeof f.type=="string"&&t?.(f)}),n.on("close",l=>{o&&clearInterval(o),!a&&(s?.(l===O?"unauthorized":"reconnecting",l===O?`WS ${O}`:void 0),r++<nt?setTimeout(u,st(l)):s?.("disconnected"))}),n.on("error",()=>{})}return u(),{close(){a=!0,o&&clearInterval(o);try{n?.close()}catch{}},send(i){if(!n||n.readyState!==le.OPEN)return!1;try{return n.send(JSON.stringify(i)),!0}catch{return!1}}}}var rt={connected:"connected",unauthorized:"unauthorized",disconnected:"disconnected",reconnecting:"reconnecting"};function fe(e){let{client:t,deviceToken:s,sessionId:n}=e,o,r=i=>{typeof i!="string"||!i||i===o||(o=i,t.mutation(m.sessions.adoptOrUpsert,{deviceToken:s,sessionId:n,model:i}).catch(()=>{}))},a=me(e.bridgeSessionId,{onStatus:i=>{let p=rt[i]??"reconnecting";t.mutation(m.sessions.setStreamStatus,{deviceToken:s,sessionId:n,streamStatus:p}).catch(()=>{})},onEvent:i=>{u(i)}});async function u(i){let p=i.type;if(p===y.controlRequest){let g=k(i.request)?i.request:{};g.subtype===T.canUseTool&&await t.mutation(m.permissions.open,{deviceToken:s,sessionId:n,requestId:String(i.request_id),toolName:typeof g.tool_name=="string"?g.tool_name:"?",input:g.input??{},description:typeof g.description=="string"?g.description:void 0,suggestions:Array.isArray(g.permission_suggestions)?g.permission_suggestions:void 0}).catch(c=>console.error("permissions.open:",ge(c)));return}if(p===y.controlCancelRequest){await t.mutation(m.permissions.cancel,{deviceToken:s,sessionId:n,requestId:String(i.request_id)}).catch(()=>{});return}if(p===y.result){await t.mutation(m.sessions.adoptOrUpsert,{deviceToken:s,sessionId:n,turnState:"idle"}).catch(()=>{});return}if(p===y.streamEvent)return;if(p===y.system){r(i.model);return}let l=ue(i);if(!l.parts.some(g=>g.text||g.kind!=="text"))return;let S=k(i.message)?i.message:void 0,h=S?.role;h==="assistant"&&r(S?.model);let w=h==="assistant"?"assistant":l.parts.some(g=>g.kind==="tool_result")?"tool":"user",E=l.parts.find(g=>g.kind==="text");await t.mutation(m.messages.ingest,{deviceToken:s,sessionId:n,bridgeUuid:l.uuid??void 0,kind:w,ts:Date.now(),parts:l.parts,text:E?.text,preview:E?.text?.slice(0,140),turnState:w==="assistant"?"working":void 0}).catch(g=>console.error("messages.ingest:",ge(g)))}return{respondPermission(i,p,l,f){let S=p==="approved"||p==="allow"?{behavior:"allow",updatedInput:l??{}}:{behavior:"deny",message:f??"Denied by the user"};return a.send({type:y.controlResponse,response:{subtype:"success",request_id:i,response:S}})},close:()=>a.close()}}function ge(e){return e instanceof Error?e.message:String(e)}var ft=1e3,N=class{constructor(t,s){this.client=t;this.deviceToken=s;wt()}client;deviceToken;sessions=new Map;has(t){return this.sessions.has(t)}getRuntime(t){return this.sessions.get(t)?.runtime??void 0}async start(t,s){this.sessions.has(t)||await this.launch(t,s,["--session-id",ot()])}async resume(t,s){this.sessions.has(t)||await this.launch(t,s,["--resume",s.claudeSessionId])}async launch(t,s,n){let o=n[1],r=xt(s.cwd);at(r,{recursive:!0}),vt(r);let a=U(lt(),`remote-cc-${_t(s.name)}-${process.pid}-${this.sessions.size}.log`),u=gt.spawn(R(),["--remote-control",s.name,...n,"--permission-mode",s.mode,"--debug-file",a],{name:"xterm-256color",cols:120,rows:40,cwd:r,env:St()});u.onData(()=>{});let i,p,l=new Promise((h,w)=>{i=h,p=w});l.catch(()=>{});let f={child:u,bridgeSessionId:null,runtime:null,ready:l};this.sessions.set(t,f),u.onExit(()=>{let h=this.sessions.get(t);h&&(h.runtime?.close(),this.sessions.delete(t),this.reportSessionState(t,"stopped","idle"))});let S;try{S=await yt(a)}catch(h){throw p(h),this.sessions.delete(t),V(u),await this.reportSessionState(t,"error","disconnected"),h}f.bridgeSessionId=S,f.runtime=fe({client:this.client,deviceToken:this.deviceToken,sessionId:t,bridgeSessionId:S}),i(),await this.client.mutation(m.sessions.adoptOrUpsert,{deviceToken:this.deviceToken,sessionId:t,bridgeSessionId:S,claudeSessionId:o,agentPid:u.pid,turnState:"idle",streamStatus:"connected"})}async stop(t){let s=this.sessions.get(t);s&&(V(s.child),s.runtime?.close(),this.sessions.delete(t)),await this.reportSessionState(t,"stopped","idle")}async sendMessage(t,s){let n=this.sessions.get(t);if(!n)throw new Error("the session is not running on this agent");if(await n.ready,!n.bridgeSessionId)throw new Error("the session is not running on this agent");await ae(n.bridgeSessionId,s)}async interrupt(t){let s=this.sessions.get(t);if(!s)throw new Error("the session is not running on this agent");if(await s.ready,!s.bridgeSessionId)throw new Error("the session is not running on this agent");await ce(s.bridgeSessionId),await this.reportSessionState(t,"idle")}async setModel(t,s){let n=this.sessions.get(t);if(!n)throw new Error("the session is not running on this agent");if(await n.ready,!n.bridgeSessionId)throw new Error("the session is not running on this agent");await de(n.bridgeSessionId,s)}closeAll(){for(let t of this.sessions.values())V(t.child),t.runtime?.close();this.sessions.clear()}async reportSessionState(t,s,n){await this.client.mutation(m.sessions.adoptOrUpsert,{deviceToken:this.deviceToken,sessionId:t,turnState:s,...n!==void 0?{streamStatus:n}:{}}).catch(()=>{})}};function V(e){try{e.kill()}catch{}}function St(){return Object.fromEntries(Object.entries(process.env).filter(e=>e[1]!==void 0))}function ht(e){return"session_"+e.slice(4)}function yt(e){return new Promise((t,s)=>{let n=Date.now(),o=setInterval(()=>{try{let r=ye(e,"utf8").match(/(?:Created session|Reattaching to session) (cse_[A-Za-z0-9]+)/);if(r){clearInterval(o),t(ht(r[1]));return}}catch{}Date.now()-n>45e3&&(clearInterval(o),s(new Error(`bridgeSessionId was not captured within ${45e3/1e3}s`)))},ft)})}function vt(e){try{let t=U(ve(),".claude.json");if(!he(t))return;let s=JSON.parse(ye(t,"utf8")),n=mt(e),o=n;try{o=ct(n)}catch{}s.projects??={};let r=!1;for(let a of new Set([n,o]))s.projects[a]??={},s.projects[a].hasTrustDialogAccepted!==!0&&(s.projects[a].hasTrustDialogAccepted=!0,r=!0);r&&dt(t,JSON.stringify(s,null,2))}catch{}}function wt(){if(process.platform==="darwin")try{let e=ut(import.meta.url),t=pt(e.resolve("node-pty/package.json"));for(let s of["darwin-arm64","darwin-x64"]){let n=U(t,"prebuilds",s,"spawn-helper");he(n)&&it(n,493)}}catch{}}function xt(e){return e.startsWith("~")?U(ve(),e.slice(1)):e}function _t(e){return e.replace(/[^a-zA-Z0-9_-]/g,"_").slice(0,40)}async function we(e){let t=B(),s=X();if(e.pairCode){let c=e.convexUrl??(j||void 0)??t?.convexUrl;if(!c)throw new Error("Missing --convex-url to pair (or a previous config).");console.log(`\u25B8 pairing with code ${e.pairCode}\u2026`);let d=await Z(Y(c),e.pairCode,void 0,s);t={convexUrl:c,deviceToken:d.deviceToken,deviceId:d.deviceId,userId:d.userId,name:d.name},J(t),console.log(`\u2705 paired as "${d.name}" (device ${d.deviceId})`)}t||(console.error("\u2717 This PC is not paired."),console.error(" Run: remote-cc agent --pair <code> --convex-url <url>"),process.exit(1));let n=t,o=kt(),r=new Ct(n.convexUrl),a=new N(r,n.deviceToken);console.log(`\u25B8 connected to Convex as "${n.name}" (${n.convexUrl})`);let u=Q(r,n.deviceToken,s);try{let c=await r.query(m.sessions.listForAgent,{deviceToken:n.deviceToken});for(let d of c)a.has(d._id)||await r.mutation(m.sessions.adoptOrUpsert,{deviceToken:n.deviceToken,sessionId:d._id,turnState:"error",streamStatus:"disconnected"})}catch(c){console.error("re-adoption:",c instanceof Error?c.message:c)}let i=c=>d=>{/invalid or revoked device token/i.test(d.message)?console.error("\u2717 This PC was revoked from the web. Pair it again with: npx remote-cc agent --pair <code>"):console.error(`\u2717 ${c} subscription failed:`,d.message),g(1)},p=new Set,l=r.onUpdate(m.commands.listPending,{deviceToken:n.deviceToken},c=>{for(let d of c)p.has(d._id)||(p.add(d._id),f(d._id).finally(()=>p.delete(d._id)))},i("command queue"));async function f(c){let d=await r.mutation(m.commands.claim,{deviceToken:n.deviceToken,commandId:c,claimToken:o});if(!(!d.ok||!d.command))try{await K(a,d.command),await r.mutation(m.commands.complete,{deviceToken:n.deviceToken,commandId:c})}catch($){let L=$ instanceof Error?$.message:String($);console.error(`command ${d.command.type} failed:`,L),await r.mutation(m.commands.fail,{deviceToken:n.deviceToken,commandId:c,error:L})}}let S=new Set,h=r.onUpdate(m.permissions.pendingForDevice,{deviceToken:n.deviceToken},c=>{for(let d of c)S.has(d._id)||(S.add(d._id),w(d).finally(()=>S.delete(d._id)))},i("permission decisions"));async function w(c){let d=a.getRuntime(c.sessionId);d?d.respondPermission(c.requestId,c.status,c.decisionInput,c.message):console.error(`decision for session ${c.sessionId} has no local runtime \u2014 cannot respond`),await r.mutation(m.permissions.markConsumed,{deviceToken:n.deviceToken,permissionRequestId:c._id})}let E=!1,g=async(c=0)=>{if(!E){E=!0,console.log(`
4
- \u25B8 disconnecting\u2026`),l(),h(),u(),a.closeAll();try{await r.mutation(m.devices.goingOffline,{deviceToken:n.deviceToken})}catch{}await r.close(),process.exit(c)}};process.on("SIGINT",()=>{g()}),process.on("SIGTERM",()=>{g()}),console.log("\u25B8 listening for commands and permissions\u2026")}function Et(e){let t={};for(let s=0;s<e.length;s++){let n=e[s];if(n==="--pair")t.pairCode=e[++s];else if(n==="--convex-url")t.convexUrl=e[++s];else{if(n==="agent")continue;n.startsWith("--")&&(console.error(`unknown flag: ${n}`),process.exit(1))}}return t}we(Et(process.argv.slice(2))).catch(e=>{console.error("\u2717",e instanceof Error?e.message:e),process.exit(1)});
2
+ import{ConvexClient as Tt}from"convex/browser";import{randomUUID as kt}from"crypto";import{anyApi as Ce,componentsGeneric as Ee}from"convex/server";var m=Ce;var At=Ee();var j="https://good-emu-457.convex.cloud";import{chmodSync as Te,existsSync as ke,mkdirSync as be,readFileSync as Ie,writeFileSync as Ae}from"fs";import{homedir as Re}from"os";import{join as F}from"path";var z=F(Re(),".remote-cc"),I=F(z,"config.json");function B(){try{return ke(I)?JSON.parse(Ie(I,"utf8")):null}catch{return null}}function J(e){be(z,{recursive:!0}),Ae(I,JSON.stringify(e,null,2));try{Te(I,384)}catch{}}var G=["default","acceptEdits","plan"],H=["opus","sonnet","haiku","claude-fable-5"];async function K(e,t){switch(t.type){case"start_session":await e.start(x(t),W(t));break;case"resume_session":await e.resume(x(t),Me(t));break;case"stop_session":t.sessionId&&await e.stop(x(t));break;case"send_message":await e.sendMessage(x(t),Pe(t).text);break;case"interrupt_turn":await e.interrupt(x(t));break;case"set_permission_mode":break;case"set_model":await e.setModel(x(t),Ne(t).model);break;default:throw new Error(`unknown command: ${t.type}`)}}function x(e){if(!e.sessionId)throw new Error(`${e.type} without sessionId`);return e.sessionId}function W(e){let t=A(e);return{name:_(e,t,"name"),cwd:_(e,t,"cwd"),mode:Oe(e,t)}}function Me(e){let t=A(e);return{...W(e),claudeSessionId:_(e,t,"claudeSessionId")}}function Pe(e){return{text:_(e,A(e),"text")}}function A(e){if(typeof e.args!="object"||e.args===null)throw new Error(`${e.type} with malformed args`);return e.args}function _(e,t,s){let n=t[s];if(typeof n!="string"||!n)throw new Error(`${e.type} without ${s}`);return n}function Oe(e,t){let s=_(e,t,"mode");if(!G.includes(s))throw new Error(`${e.type} with unknown mode "${s}"`);return s}function Ne(e){let t=_(e,A(e),"model");if(!H.includes(t))throw new Error(`${e.type} with unknown model "${t}"`);return{model:t}}function Y(e){return e.replace(/\.convex\.cloud\/?$/,".convex.site")}import{execFileSync as qe}from"child_process";import{arch as Le,homedir as Ve,release as D,type as je}from"os";import{existsSync as Ue}from"fs";import{homedir as $e}from"os";import{join as De}from"path";function R(){let e=De($e(),".local","bin","claude");return Ue(e)?e:"claude"}var Fe="0.1.2";function X(){return{platform:`${ze()} \xB7 ${Le()}`,claudeVersion:Be(),baseDir:Ve(),agentVersion:Fe}}function ze(){let e=je();return e==="Darwin"?`macOS ${D()}`:e==="Linux"?`Linux ${D()}`:`${e} ${D()}`}function Be(){try{let t=qe(R(),["--version"],{encoding:"utf8",timeout:5e3}).match(/\d+\.\d+\.\d+/);return t?t[0]:void 0}catch{return}}async function Z(e,t,s,n){let i=await fetch(`${e}/pair/claim`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:t,name:s,platform:n.platform,claudeVersion:n.claudeVersion,baseDir:n.baseDir,agentVersion:n.agentVersion})}),o=await i.text(),a;try{a=JSON.parse(o)}catch{a={}}if(!i.ok)throw new Error(`pairing failed (${i.status}): ${a.error??o.slice(0,200)}`);let{deviceToken:l,deviceId:f,userId:c,name:u}=a;if(!l||!f||!c||!u)throw new Error(`pairing response is missing fields: ${o.slice(0,200)}`);return{deviceToken:l,deviceId:f,userId:c,name:u}}function Q(e,t,s){let n=async l=>{try{await e.mutation(m.devices.heartbeat,{deviceToken:t,...l})}catch(f){console.error("heartbeat error:",f instanceof Error?f.message:f)}},i=()=>n({});n({platform:s.platform,claudeVersion:s.claudeVersion,baseDir:s.baseDir,agentVersion:s.agentVersion});let a=setInterval(()=>{i()},2e4);return()=>clearInterval(a)}import{randomUUID as at}from"crypto";import{chmodSync as ct,existsSync as ve,mkdirSync as dt,readFileSync as we,realpathSync as ut,writeFileSync as lt}from"fs";import{createRequire as pt}from"module";import{homedir as xe,tmpdir as mt}from"os";import{dirname as ft,join as U,resolve as gt}from"path";import yt from"node-pty";import{randomUUID as b}from"crypto";import{readFileSync as se}from"fs";import{homedir as re}from"os";import{join as oe}from"path";import{execFileSync as ee}from"child_process";var te="Claude Code-credentials";function Ge(){let e="";try{e=ee("/usr/bin/security",["dump-keychain"],{encoding:"utf8",maxBuffer:64*1024*1024})}catch{}let t=new Set;for(let s of e.split(/\nkeychain: /)){if(!s.includes(`"svce"<blob>="${te}"`))continue;let n=s.match(/"acct"<blob>="([^"]*)"/);n&&t.add(n[1])}return[...t]}function He(e){let t=["find-generic-password","-s",te];e&&t.push("-a",e),t.push("-w");try{return{account:e,raw:ee("/usr/bin/security",t,{encoding:"utf8"}).trim()}}catch{return null}}function ne(){let e=Ge();return e.length||e.push(void 0),e.map(He).filter(t=>t!==null)}var Ke=oe(re(),".claude",".credentials.json");function ie(e,t){let s;try{s=JSON.parse(e)?.claudeAiOauth}catch{return null}if(typeof s!="object"||s===null)return null;let{accessToken:n,refreshToken:i,expiresAt:o,scopes:a}=s;return typeof n!="string"||!n||typeof o!="number"||Number.isNaN(o)?null:{account:t,accessToken:n,refreshToken:typeof i=="string"?i:void 0,expiresAt:o,scopes:Array.isArray(a)?a.filter(l=>typeof l=="string"):void 0}}function We(){try{return ie(se(Ke,"utf8"))}catch{return null}}function Ye(){return ne().map(e=>ie(e.raw,e.account)).filter(e=>e!==null)}function Xe(){return JSON.parse(se(oe(re(),".claude.json"),"utf8"))?.oauthAccount?.organizationUuid}function M(){let e=process.platform==="darwin"?Ye():[];if(!e.length){let n=We();n&&e.push(n)}if(!e.length)throw new Error("no claude.ai credentials (Keychain on macOS / ~/.claude/.credentials.json on Linux). Run `claude` and log in.");e.sort((n,i)=>i.expiresAt-n.expiresAt);let s=e.find(n=>n.expiresAt>Date.now())??e[0];return{...s,orgUuid:Xe(),isExpired:s.expiresAt<=Date.now()}}var C=null;function Ze(){if((!C||C.expiresAt-Date.now()<6e4)&&(C=M(),C.isExpired))throw new Error("No valid claude.ai token (Keychain on macOS / ~/.claude/.credentials.json on Linux). You need a running Claude Code instance (an agent session works) to refresh and persist it.");return C}function Qe(){C=null}function et(){let e=Ze();return{Authorization:`Bearer ${e.accessToken}`,"Content-Type":"application/json","anthropic-version":"2023-06-01","anthropic-beta":"ccr-byoc-2025-07-29","x-organization-uuid":e.orgUuid??""}}async function P(e,t={}){let s=()=>{let i=new Headers(t.headers);for(let[o,a]of Object.entries(et()))i.has(o)||i.set(o,a);return{...t,headers:i}},n=await fetch(e,s());return n.status===401&&(Qe(),n=await fetch(e,s())),n}var h={controlRequest:"control_request",controlCancelRequest:"control_cancel_request",controlResponse:"control_response",result:"result",streamEvent:"stream_event",system:"system",user:"user"},k={canUseTool:"can_use_tool",interrupt:"interrupt",setModel:"set_model"};var q="https://api.anthropic.com";async function ae(e,t){let s={uuid:b(),session_id:e,type:h.user,message:{role:"user",content:t}},n=await P(`${q}/v1/sessions/${e}/events`,{method:"POST",body:JSON.stringify({events:[s]})}),i=await n.text();if(!n.ok)throw new Error(`POST events ${n.status}: ${i.slice(0,200)}`);return{status:n.status,body:i?tt(i):null,sentUuid:s.uuid}}async function ce(e){let t={type:h.controlRequest,request_id:b(),request:{subtype:k.interrupt},uuid:b()},s=await P(`${q}/v1/sessions/${e}/events`,{method:"POST",body:JSON.stringify({events:[t]})}),n=await s.text();if(!s.ok)throw new Error(`POST interrupt ${s.status}: ${n.slice(0,200)}`)}async function de(e,t){let s={type:h.controlRequest,request_id:b(),request:{subtype:k.setModel,model:t},uuid:b()},n=await P(`${q}/v1/sessions/${e}/events`,{method:"POST",body:JSON.stringify({events:[s]})}),i=await n.text();if(!n.ok)throw new Error(`POST set_model ${n.status}: ${i.slice(0,200)}`)}function tt(e){try{return JSON.parse(e)}catch{return e}}function E(e){return typeof e=="object"&&e!==null}function v(e){return typeof e=="string"?e:void 0}function ue(e){let t=E(e.message)?e.message:void 0,s=v(e.type),n=v(t?.role)||s||"system",i=t?.content,o=[];if(typeof i=="string")o.push({kind:"text",text:i});else if(Array.isArray(i))for(let a of i){if(!E(a))continue;let l=v(a.type)??"unknown";l==="thinking"||l==="redacted_thinking"||(l==="text"?o.push({kind:"text",text:v(a.text)??""}):l==="tool_use"?o.push({kind:"tool_use",text:`${v(a.name)??"?"} ${JSON.stringify(a.input||{}).slice(0,300)}`}):l==="tool_result"?o.push({kind:"tool_result",text:nt(a.content)}):o.push({kind:l,text:JSON.stringify(a).slice(0,200)}))}return{uuid:v(e.uuid)??null,type:s,role:n,parts:o}}function le(e){let t=e.find(i=>i.kind==="text")?.text??"";if(t.includes("<local-command-caveat>"))return{type:"caveat"};let s=t.match(/<command-name>([^<]*)<\/command-name>/);if(s)return{type:"command",name:s[1].trim()};let n=t.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);return n?{type:"stdout",text:n[1].trim()}:null}function nt(e){return typeof e=="string"?e:Array.isArray(e)?e.map(t=>E(t)?t.type==="text"?v(t.text)??"":`[${v(t.type)??"unknown"}]`:"[unknown]").join(`
3
+ `):JSON.stringify(e||"").slice(0,300)}import pe from"ws";var st=3e4,me=2e3,rt=5,O=4003;function ot(e){return e===O?me*2:me}function fe(e,{onEvent:t,onStatus:s}={}){let n=null,i=null,o=0,a=!1;function l(){let f;try{f=M()}catch(u){s?.("unauthorized",u instanceof Error?u.message:String(u));return}let c=`wss://api.anthropic.com/v1/sessions/ws/${e}/subscribe?organization_uuid=${f.orgUuid}`;n=new pe(c,{headers:{Authorization:`Bearer ${f.accessToken}`,"anthropic-version":"2023-06-01"}}),n.on("open",()=>{o=0,s?.("connected"),i=setInterval(()=>{try{n?.ping()}catch{}},st)}),n.on("message",u=>{let p;try{p=JSON.parse(u.toString())}catch{return}p&&typeof p.type=="string"&&t?.(p)}),n.on("close",u=>{i&&clearInterval(i),!a&&(s?.(u===O?"unauthorized":"reconnecting",u===O?`WS ${O}`:void 0),o++<rt?setTimeout(l,ot(u)):s?.("disconnected"))}),n.on("error",()=>{})}return l(),{close(){a=!0,i&&clearInterval(i);try{n?.close()}catch{}},send(f){if(!n||n.readyState!==pe.OPEN)return!1;try{return n.send(JSON.stringify(f)),!0}catch{return!1}}}}var ge="<synthetic>",it={connected:"connected",unauthorized:"unauthorized",disconnected:"disconnected",reconnecting:"reconnecting"};function Se(e){let{client:t,deviceToken:s,sessionId:n}=e,i,o=c=>{typeof c!="string"||!c||c===ge||c===i||(i=c,t.mutation(m.sessions.adoptOrUpsert,{deviceToken:s,sessionId:n,model:c}).catch(()=>{}))},a=fe(e.bridgeSessionId,{onStatus:c=>{let u=it[c]??"reconnecting";t.mutation(m.sessions.setStreamStatus,{deviceToken:s,sessionId:n,streamStatus:u}).catch(()=>{})},onEvent:c=>{l(c)}});async function l(c){let u=c.type;if(u===h.controlRequest){let r=E(c.request)?c.request:{};r.subtype===k.canUseTool&&await t.mutation(m.permissions.open,{deviceToken:s,sessionId:n,requestId:String(c.request_id),toolName:typeof r.tool_name=="string"?r.tool_name:"?",input:r.input??{},description:typeof r.description=="string"?r.description:void 0,suggestions:Array.isArray(r.permission_suggestions)?r.permission_suggestions:void 0}).catch(d=>console.error("permissions.open:",ye(d)));return}if(u===h.controlCancelRequest){await t.mutation(m.permissions.cancel,{deviceToken:s,sessionId:n,requestId:String(c.request_id)}).catch(()=>{});return}if(u===h.result){await t.mutation(m.sessions.adoptOrUpsert,{deviceToken:s,sessionId:n,turnState:"idle"}).catch(()=>{});return}if(u===h.streamEvent)return;if(u===h.system){o(c.model);return}let p=ue(c);if(!p.parts.some(r=>r.text||r.kind!=="text"))return;let g=E(c.message)?c.message:void 0,S=g?.role;if(S==="user"){let r=le(p.parts);if(r){await t.mutation(m.sessions.adoptOrUpsert,{deviceToken:s,sessionId:n,turnState:"idle"}).catch(()=>{}),r.type==="stdout"&&r.text&&await f("tool",p.uuid,[{kind:"text",text:r.text}]);return}}let T=S==="assistant"&&g?.model===ge;if(S==="assistant"&&o(g?.model),T&&p.parts.every(r=>r.kind==="text"&&r.text==="(no content)"))return;let w=S==="assistant"?"assistant":p.parts.some(r=>r.kind==="tool_result")?"tool":"user";await f(w,p.uuid,p.parts,w==="assistant"&&!T?"working":void 0)}async function f(c,u,p,y){let g=p.find(S=>S.kind==="text");await t.mutation(m.messages.ingest,{deviceToken:s,sessionId:n,bridgeUuid:u??void 0,kind:c,ts:Date.now(),parts:p,text:g?.text,preview:g?.text?.slice(0,140),turnState:y}).catch(S=>console.error("messages.ingest:",ye(S)))}return{respondPermission(c,u,p,y){let g=u==="approved"||u==="allow"?{behavior:"allow",updatedInput:p??{}}:{behavior:"deny",message:y??"Denied by the user"};return a.send({type:h.controlResponse,response:{subtype:"success",request_id:c,response:g}})},close:()=>a.close()}}function ye(e){return e instanceof Error?e.message:String(e)}var St=1e3,N=class{constructor(t,s){this.client=t;this.deviceToken=s;_t()}client;deviceToken;sessions=new Map;has(t){return this.sessions.has(t)}getRuntime(t){return this.sessions.get(t)?.runtime??void 0}async start(t,s){this.sessions.has(t)||await this.launch(t,s,["--session-id",at()])}async resume(t,s){this.sessions.has(t)||await this.launch(t,s,["--resume",s.claudeSessionId])}async launch(t,s,n){let i=n[1],o=Ct(s.cwd);dt(o,{recursive:!0}),xt(o);let a=U(mt(),`remote-cc-${Et(s.name)}-${process.pid}-${this.sessions.size}.log`),l=yt.spawn(R(),["--remote-control",s.name,...n,"--permission-mode",s.mode,"--debug-file",a],{name:"xterm-256color",cols:120,rows:40,cwd:o,env:ht()});l.onData(()=>{});let f,c,u=new Promise((g,S)=>{f=g,c=S});u.catch(()=>{});let p={child:l,bridgeSessionId:null,runtime:null,ready:u};this.sessions.set(t,p),l.onExit(()=>{let g=this.sessions.get(t);g&&(g.runtime?.close(),this.sessions.delete(t),this.reportSessionState(t,"stopped","idle"))});let y;try{y=await wt(a)}catch(g){throw c(g),this.sessions.delete(t),L(l),await this.reportSessionState(t,"error","disconnected"),g}p.bridgeSessionId=y,p.runtime=Se({client:this.client,deviceToken:this.deviceToken,sessionId:t,bridgeSessionId:y}),f(),await this.client.mutation(m.sessions.adoptOrUpsert,{deviceToken:this.deviceToken,sessionId:t,bridgeSessionId:y,claudeSessionId:i,agentPid:l.pid,turnState:"idle",streamStatus:"connected"})}async stop(t){let s=this.sessions.get(t);s&&(L(s.child),s.runtime?.close(),this.sessions.delete(t)),await this.reportSessionState(t,"stopped","idle")}async sendMessage(t,s){let n=this.sessions.get(t);if(!n)throw new Error("the session is not running on this agent");if(await n.ready,!n.bridgeSessionId)throw new Error("the session is not running on this agent");await ae(n.bridgeSessionId,s)}async interrupt(t){let s=this.sessions.get(t);if(!s)throw new Error("the session is not running on this agent");if(await s.ready,!s.bridgeSessionId)throw new Error("the session is not running on this agent");await ce(s.bridgeSessionId),await this.reportSessionState(t,"idle")}async setModel(t,s){let n=this.sessions.get(t);if(!n)throw new Error("the session is not running on this agent");if(await n.ready,!n.bridgeSessionId)throw new Error("the session is not running on this agent");await de(n.bridgeSessionId,s)}closeAll(){for(let t of this.sessions.values())L(t.child),t.runtime?.close();this.sessions.clear()}async reportSessionState(t,s,n){await this.client.mutation(m.sessions.adoptOrUpsert,{deviceToken:this.deviceToken,sessionId:t,turnState:s,...n!==void 0?{streamStatus:n}:{}}).catch(()=>{})}};function L(e){try{e.kill()}catch{}}function ht(){return Object.fromEntries(Object.entries(process.env).filter(e=>e[1]!==void 0))}function vt(e){return"session_"+e.slice(4)}function wt(e){return new Promise((t,s)=>{let n=Date.now(),i=setInterval(()=>{try{let o=we(e,"utf8").match(/(?:Created session|Reattaching to session) (cse_[A-Za-z0-9]+)/);if(o){clearInterval(i),t(vt(o[1]));return}}catch{}Date.now()-n>45e3&&(clearInterval(i),s(new Error(`bridgeSessionId was not captured within ${45e3/1e3}s`)))},St)})}function xt(e){try{let t=U(xe(),".claude.json");if(!ve(t))return;let s=JSON.parse(we(t,"utf8")),n=gt(e),i=n;try{i=ut(n)}catch{}s.projects??={};let o=!1;for(let a of new Set([n,i]))s.projects[a]??={},s.projects[a].hasTrustDialogAccepted!==!0&&(s.projects[a].hasTrustDialogAccepted=!0,o=!0);o&&lt(t,JSON.stringify(s,null,2))}catch{}}function _t(){if(process.platform==="darwin")try{let e=pt(import.meta.url),t=ft(e.resolve("node-pty/package.json"));for(let s of["darwin-arm64","darwin-x64"]){let n=U(t,"prebuilds",s,"spawn-helper");ve(n)&&ct(n,493)}}catch{}}function Ct(e){return e.startsWith("~")?U(xe(),e.slice(1)):e}function Et(e){return e.replace(/[^a-zA-Z0-9_-]/g,"_").slice(0,40)}async function _e(e){let t=B(),s=X();if(e.pairCode){let r=e.convexUrl??(j||void 0)??t?.convexUrl;if(!r)throw new Error("Missing --convex-url to pair (or a previous config).");console.log(`\u25B8 pairing with code ${e.pairCode}\u2026`);let d=await Z(Y(r),e.pairCode,void 0,s);t={convexUrl:r,deviceToken:d.deviceToken,deviceId:d.deviceId,userId:d.userId,name:d.name},J(t),console.log(`\u2705 paired as "${d.name}" (device ${d.deviceId})`)}t||(console.error("\u2717 This PC is not paired."),console.error(" Run: remote-cc agent --pair <code> --convex-url <url>"),process.exit(1));let n=t,i=kt(),o=new Tt(n.convexUrl),a=new N(o,n.deviceToken);console.log(`\u25B8 connected to Convex as "${n.name}" (${n.convexUrl})`);let l=Q(o,n.deviceToken,s);try{let r=await o.query(m.sessions.listForAgent,{deviceToken:n.deviceToken});for(let d of r)a.has(d._id)||await o.mutation(m.sessions.adoptOrUpsert,{deviceToken:n.deviceToken,sessionId:d._id,turnState:"error",streamStatus:"disconnected"})}catch(r){console.error("re-adoption:",r instanceof Error?r.message:r)}let f=r=>d=>{/invalid or revoked device token/i.test(d.message)?console.error("\u2717 This PC was revoked from the web. Pair it again with: npx remote-cc agent --pair <code>"):console.error(`\u2717 ${r} subscription failed:`,d.message),w(1)},c=new Set,u=o.onUpdate(m.commands.listPending,{deviceToken:n.deviceToken},r=>{for(let d of r)c.has(d._id)||(c.add(d._id),p(d._id).finally(()=>c.delete(d._id)))},f("command queue"));async function p(r){let d=await o.mutation(m.commands.claim,{deviceToken:n.deviceToken,commandId:r,claimToken:i});if(!(!d.ok||!d.command))try{await K(a,d.command),await o.mutation(m.commands.complete,{deviceToken:n.deviceToken,commandId:r})}catch($){let V=$ instanceof Error?$.message:String($);console.error(`command ${d.command.type} failed:`,V),await o.mutation(m.commands.fail,{deviceToken:n.deviceToken,commandId:r,error:V})}}let y=new Set,g=o.onUpdate(m.permissions.pendingForDevice,{deviceToken:n.deviceToken},r=>{for(let d of r)y.has(d._id)||(y.add(d._id),S(d).finally(()=>y.delete(d._id)))},f("permission decisions"));async function S(r){let d=a.getRuntime(r.sessionId);d?d.respondPermission(r.requestId,r.status,r.decisionInput,r.message):console.error(`decision for session ${r.sessionId} has no local runtime \u2014 cannot respond`),await o.mutation(m.permissions.markConsumed,{deviceToken:n.deviceToken,permissionRequestId:r._id})}let T=!1,w=async(r=0)=>{if(!T){T=!0,console.log(`
4
+ \u25B8 disconnecting\u2026`),u(),g(),l(),a.closeAll();try{await o.mutation(m.devices.goingOffline,{deviceToken:n.deviceToken})}catch{}await o.close(),process.exit(r)}};process.on("SIGINT",()=>{w()}),process.on("SIGTERM",()=>{w()}),console.log("\u25B8 listening for commands and permissions\u2026")}function bt(e){let t={};for(let s=0;s<e.length;s++){let n=e[s];if(n==="--pair")t.pairCode=e[++s];else if(n==="--convex-url")t.convexUrl=e[++s];else{if(n==="agent")continue;n.startsWith("--")&&(console.error(`unknown flag: ${n}`),process.exit(1))}}return t}_e(bt(process.argv.slice(2))).catch(e=>{console.error("\u2717",e instanceof Error?e.message:e),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maol-1997/remote-cc",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "remote-cc agent: control Claude Code on this PC from the web.",
5
5
  "type": "module",
6
6
  "bin": {