@diggerhq/catty 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/catty.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/index.js");
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import{program as u}from"commander";import{Command as Ae}from"commander";import Ee from"open";var k="https://api.catty.dev",b=".catty",I="credentials.json";function p(o){return o||(process.env.CATTY_API_ADDR?process.env.CATTY_API_ADDR:k)}function T(o){return new Promise(e=>setTimeout(e,o))}import{homedir as se}from"os";import{join as v}from"path";import{readFileSync as ne,writeFileSync as re,mkdirSync as ie,unlinkSync as ae,existsSync as ce}from"fs";function M(){return v(se(),b)}function R(){return v(M(),I)}function g(){let o=R();try{let e=ne(o,"utf-8");return JSON.parse(e)}catch{return null}}function _(o){let e=M(),t=R();ie(e,{recursive:!0,mode:448}),re(t,JSON.stringify(o,null,2),{mode:384})}function P(){let o=R();ce(o)&&ae(o)}function h(){let o=g();return!(!o||!o.access_token||o.expires_at&&!o.refresh_token&&new Date(o.expires_at)<=new Date)}function N(){return g()?.access_token||null}function O(){return g()?.refresh_token||null}var y=class extends Error{constructor(t,n,s,r){super(s);this.statusCode=t;this.errorCode=n;this.upgradeURL=r;this.name="APIError"}isQuotaExceeded(){return this.statusCode===402&&this.errorCode==="quota_exceeded"}},l=class{baseURL;authToken;constructor(e){this.baseURL=e||process.env.CATTY_API_ADDR||k,this.authToken=N()}async doRequest(e,t,n){let s=new AbortController,r=setTimeout(()=>s.abort(),12e4);try{let i={"Content-Type":"application/json"};return this.authToken&&(i.Authorization=`Bearer ${this.authToken}`),await fetch(`${this.baseURL}${t}`,{method:e,headers:i,body:n?JSON.stringify(n):void 0,signal:s.signal})}finally{clearTimeout(r)}}async doRequestWithRefresh(e,t,n){let s=await this.doRequest(e,t,n);return s.status===401&&await this.refreshAuthToken()&&(s=await this.doRequest(e,t,n)),s}async refreshAuthToken(){let e=O();if(!e)return!1;try{let t=await this.doRequest("POST","/v1/auth/refresh",{refresh_token:e});if(!t.ok)return!1;let n=await t.json();if(!n.access_token)return!1;let s=g();return s&&(s.access_token=n.access_token,n.refresh_token&&(s.refresh_token=n.refresh_token),n.expires_in&&(s.expires_at=new Date(Date.now()+(n.expires_in-30)*1e3).toISOString()),_(s),this.authToken=n.access_token),!0}catch{return!1}}async handleResponse(e){if(!e.ok){let t;try{t=await e.json()}catch{t={error:e.statusText}}throw new y(e.status,t.code||"",t.error||e.statusText,t.upgrade_url)}return e.json()}async createSession(e){let t=await this.doRequestWithRefresh("POST","/v1/sessions",e);return this.handleResponse(t)}async listSessions(){let e=await this.doRequestWithRefresh("GET","/v1/sessions");return this.handleResponse(e)}async getSession(e,t){let n=t?`/v1/sessions/${e}?live=true`:`/v1/sessions/${e}`,s=await this.doRequestWithRefresh("GET",n);return this.handleResponse(s)}async stopSession(e,t){let n=t?`/v1/sessions/${e}?delete=true`:`/v1/sessions/${e}`,s=await this.doRequestWithRefresh("DELETE",n);if(!s.ok){let r;try{r=await s.json()}catch{r={error:s.statusText}}throw new y(s.status,r.code||"",r.error||s.statusText)}}async createCheckoutSession(){let e=await this.doRequestWithRefresh("POST","/v1/checkout");return(await this.handleResponse(e)).url}};import S from"ws";var w=class{wasRaw=!1;cleanupDone=!1;isTerminal(){return process.stdin.isTTY===!0}makeRaw(){if(!this.isTerminal()||this.wasRaw)return;process.stdin.setRawMode(!0),process.stdin.resume(),this.wasRaw=!0;let e=()=>this.restore();process.on("exit",e),process.on("SIGINT",()=>{e(),process.exit(130)}),process.on("SIGTERM",()=>{e(),process.exit(143)})}restore(){if(!this.cleanupDone){if(this.wasRaw&&process.stdin.isTTY){try{process.stdin.setRawMode(!1)}catch{}this.wasRaw=!1}this.cleanupDone=!0}}getSize(){return{cols:process.stdout.columns||80,rows:process.stdout.rows||24}}onResize(e){process.stdout.on("resize",e)}offResize(e){process.stdout.off("resize",e)}};var c={RESIZE:"resize",SIGNAL:"signal",PING:"ping",PONG:"pong",READY:"ready",EXIT:"exit",ERROR:"error",SYNC_BACK:"sync_back",SYNC_BACK_ACK:"sync_back_ack",FILE_CHANGE:"file_change"};function D(o){let e=JSON.parse(o);switch(e.type){case c.RESIZE:return JSON.parse(o);case c.SIGNAL:return JSON.parse(o);case c.PING:return{type:"ping"};case c.PONG:return{type:"pong"};case c.READY:return{type:"ready"};case c.EXIT:return JSON.parse(o);case c.ERROR:return JSON.parse(o);case c.SYNC_BACK:return JSON.parse(o);case c.SYNC_BACK_ACK:return JSON.parse(o);case c.FILE_CHANGE:return JSON.parse(o);default:return e}}function x(o,e){return JSON.stringify({type:c.RESIZE,cols:o,rows:e})}function L(){return JSON.stringify({type:c.PONG})}function $(o){return JSON.stringify({type:c.SYNC_BACK,enabled:o})}import{writeFileSync as le,unlinkSync as de,mkdirSync as ue,renameSync as me}from"fs";import{join as B,dirname as U,normalize as A,isAbsolute as ge,sep as W}from"path";function z(o){let e=A(o.path.replace(/^\.\//,""));if(!e||e===".")return;if(ge(e)){console.error(`sync-back rejected absolute path: ${e}`);return}if(e===".."||e.startsWith(".."+W)){console.error(`sync-back rejected traversal path: ${e}`);return}let t=process.cwd(),n=B(t,e),s=A(t),r=A(n);if(!r.startsWith(s+W)&&r!==s){console.error(`sync-back rejected path outside base: ${n}`);return}try{if(o.action==="delete")de(n);else if(o.action==="write"){let i=Buffer.from(o.content||"","base64");ue(U(n),{recursive:!0});let m=B(U(n),`.catty-sync-${Date.now()}`);le(m,i,{mode:o.mode||420}),me(m,n)}}catch{}}async function C(o){let e=new w;if(!e.isTerminal())throw new Error("stdin is not a terminal");let t=new S(o.connectURL,{headers:{...o.headers,Authorization:`Bearer ${o.connectToken}`}});return new Promise((n,s)=>{let r=!1,i=0,m=()=>{let{cols:a,rows:d}=e.getSize();t.readyState===S.OPEN&&t.send(x(a,d))},f=()=>{e.restore(),e.offResize(m),process.stdin.off("data",E)},E=a=>{t.readyState===S.OPEN&&t.send(a)};t.on("open",()=>{o.syncBack&&(t.send($(!0)),setTimeout(()=>{r||process.stderr.write(`\r
3
+ (sync-back) No ack from executor yet \u2014 this machine may be running an older catty-exec image without sync-back.\r
4
+ `)},2e3)),e.makeRaw();let{cols:a,rows:d}=e.getSize();t.send(x(a,d)),e.onResize(m),process.stdin.on("data",E)}),t.on("message",(a,d)=>{if(d)process.stdout.write(a);else try{let te=D(a.toString());oe(te)}catch{}});function oe(a){switch(a.type){case"exit":{let d=a;i=d.code,o.onExit?.(d.code),process.stderr.write(`\r
5
+ Process exited with code ${d.code}\r
6
+ `),f(),t.close(),n();break}case"error":{let d=a;process.stderr.write(`\r
7
+ Error: ${d.message}\r
8
+ `);break}case"ping":t.send(L());break;case"file_change":z(a);break;case"sync_back_ack":r=!0;break}}t.on("close",a=>{f(),n()}),t.on("error",a=>{f(),s(a)}),process.on("exit",()=>{f(),t.readyState===S.OPEN&&t.close()})})}import ye from"archiver";import _e from"ignore";import{readFileSync as we,readdirSync as Se,statSync as Ce}from"fs";import{join as G,relative as ke}from"path";var Re=[".git",".git/**","node_modules","node_modules/**","__pycache__","__pycache__/**",".venv",".venv/**","venv","venv/**",".env","*.pyc",".DS_Store","*.log"];async function xe(o){let e=_e().add(Re);try{let t=we(G(o,".gitignore"),"utf-8");e.add(t)}catch{}return new Promise((t,n)=>{let s=[],r=ye("zip",{zlib:{level:9}});r.on("data",i=>s.push(i)),r.on("end",()=>t(Buffer.concat(s))),r.on("error",n),j(o,o,e,r),r.finalize()})}function j(o,e,t,n){let s;try{s=Se(e)}catch{return}for(let r of s){let i=G(e,r),m=ke(o,i);if(t.ignores(m))continue;let f;try{f=Ce(i)}catch{continue}if(f.isDirectory()){if(t.ignores(m+"/"))continue;j(o,i,t,n)}else f.isFile()&&n.file(i,{name:m})}}async function Y(o,e,t){let n=process.cwd(),s=await xe(n);if(s.length>104857600)throw new Error(`Workspace too large (${s.length} bytes, max ${104857600})`);let r=await fetch(o,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/zip","fly-force-instance-id":t},body:s});if(!r.ok){let i=await r.text();throw new Error(`Upload failed: ${r.status} - ${i}`)}}function J(o){return o.replace("wss://","https://").replace("ws://","http://").replace("/connect","/upload")}var q=new Ae("new").description("Start a new remote agent session").option("--agent <name>","Agent to use: claude or codex","claude").option("--no-upload","Don't upload current directory").option("--no-sync-back","Disable sync-back").option("--enable-prompts","Enable permission prompts (by default, all permissions are auto-approved)",!1).action(async function(){let o=this.opts(),e=p(this.optsWithGlobals().api);h()||(console.error("Not logged in. Please run 'catty login' first."),process.exit(1));let t=new l(e);console.log("Creating session...");let n;switch(o.agent){case"claude":o.enablePrompts?n=["claude-wrapper"]:n=["claude-wrapper","--dangerously-skip-permissions"];break;case"codex":n=["codex"];break;default:console.error(`Unknown agent: ${o.agent} (must be 'claude' or 'codex')`),process.exit(1)}let s;try{s=await t.createSession({agent:o.agent,cmd:n,region:"iad",ttl_sec:7200})}catch(r){if(r instanceof y&&r.isQuotaExceeded()){await be(t);return}throw r}if(console.log(`Session created: ${s.label}`),console.log(` Reconnect with: catty connect ${s.label}`),o.upload!==!1){console.log("Uploading workspace...");let r=J(s.connect_url);await Y(r,s.connect_token,s.headers["fly-force-instance-id"]),console.log("Workspace uploaded.")}console.log(`Connecting to ${s.connect_url}...`),await C({connectURL:s.connect_url,connectToken:s.connect_token,headers:s.headers,syncBack:o.syncBack!==!1})});async function be(o){console.error(""),console.error("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),console.error(" Free tier quota exceeded (1M tokens/month)"),console.error(" Upgrade to Pro for unlimited usage."),console.error("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),console.error("");try{let e=await o.createCheckoutSession();console.error("Opening upgrade page in your browser..."),await Ee(e)}catch(e){console.error(`Failed to create checkout session: ${e}`),console.error("Please visit https://catty.dev to upgrade.")}}import{Command as Ie}from"commander";var K=new Ie("connect").description("Reconnect to an existing session").argument("<label>","Session label (e.g., brave-tiger-1234)").option("--sync-back","Sync remote file changes back to local",!0).option("--no-sync-back","Disable sync-back").action(async function(o){let e=this.opts(),t=p(this.optsWithGlobals().api);h()||(console.error("Not logged in. Please run 'catty login' first."),process.exit(1));let n=new l(t);console.log(`Looking up session ${o}...`);let s=await n.getSession(o,!0);if(s.status==="stopped")throw new Error(`Session ${s.label} is stopped`);if(s.machine_state&&s.machine_state!=="started")throw new Error(`Machine is not running (state: ${s.machine_state})`);console.log(`Reconnecting to ${s.label}...`),await C({connectURL:s.connect_url,connectToken:s.connect_token,headers:{"fly-force-instance-id":s.machine_id},syncBack:e.syncBack})});import{Command as Te}from"commander";var V=new Te("list").aliases(["ls"]).description("List all sessions").action(async function(){let o=p(this.optsWithGlobals().api),t=await new l(o).listSessions();if(t.length===0){console.log("No sessions found");return}console.log("LABEL STATUS REGION CREATED");for(let s of t){let r=ve(new Date(s.created_at)),i=[s.label.padEnd(22),s.status.padEnd(9),s.region.padEnd(7),r].join(" ");console.log(i)}});function ve(o){let e=Math.floor((Date.now()-o.getTime())/1e3);if(e<60)return`${e}s ago`;let t=Math.floor(e/60);if(t<60)return`${t}m ago`;let n=Math.floor(t/60);return n<24?`${n}h ago`:`${Math.floor(n/24)}d ago`}import{Command as Me}from"commander";var Z=new Me("stop").description("Stop a session").argument("<label>","Session ID or label").option("--delete","Delete the machine after stopping",!1).action(async function(o){let e=this.opts(),t=p(this.optsWithGlobals().api);await new l(t).stopSession(o,e.delete),e.delete?console.log(`Session ${o} stopped and deleted`):console.log(`Session ${o} stopped`)});import{Command as Pe}from"commander";var X=new Pe("stop-all-sessions-dangerously").description("Stop and delete ALL sessions").option("--yes-i-mean-it","Confirm you want to stop all sessions",!1).action(async function(){let o=this.opts(),e=p(this.optsWithGlobals().api);if(!o.yesIMeanIt)throw new Error("Must pass --yes-i-mean-it to confirm");let t=new l(e),n=await t.listSessions();if(n.length===0){console.log("No sessions to stop");return}console.log(`Stopping ${n.length} sessions...`);for(let s of n){process.stdout.write(` Stopping ${s.session_id}... `);try{await t.stopSession(s.session_id,!0),console.log("done")}catch(r){console.log(`ERROR: ${r}`)}}});import{Command as Ne}from"commander";import Oe from"open";var Q=new Ne("login").description("Log in to Catty").action(async function(){let o=p(this.optsWithGlobals().api);if(h()){let r=g();console.log(`Already logged in as ${r?.email}`),console.log("Run 'catty logout' to log out first");return}console.log("Starting login...");let e=await fetch(`${o}/v1/auth/device`,{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"});if(!e.ok)throw new Error(`Failed to start auth: ${e.statusText}`);let t=await e.json();console.log(`
9
+ Your confirmation code:
10
+ `),console.log(` ${t.user_code}
11
+ `),console.log(`Opening ${t.verification_uri_complete}
12
+ `),await Oe(t.verification_uri_complete),console.log("Waiting for authentication...");let n=(t.interval||5)*1e3,s=Date.now()+t.expires_in*1e3;for(;Date.now()<s;){await T(n);let i=await(await fetch(`${o}/v1/auth/device/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:t.device_code})})).json();if(!i.pending){if(i.error)throw new Error(i.error);if(i.access_token){_({access_token:i.access_token,refresh_token:i.refresh_token,user_id:i.user?.id||"",email:i.user?.email||"",expires_at:i.expires_in?new Date(Date.now()+(i.expires_in-30)*1e3).toISOString():void 0}),console.log(`
13
+ Logged in as ${i.user?.email}`),console.log("You can now run 'catty new' to start a session");return}}}throw new Error("Authentication timed out")});import{Command as De}from"commander";var H=new De("logout").description("Log out of Catty").action(async()=>{if(!h()){console.log("Not logged in");return}let e=g()?.email||"";P(),console.log(e?`Logged out from ${e}`:"Logged out")});import{Command as Le}from"commander";var ee=new Le("version").description("Print the version number").action(()=>{console.log("0.3.7")});var $e="0.3.7";u.name("catty").description("Catty - Remote AI agent sessions").option("--api <url>","API server address").version($e);u.addCommand(q);u.addCommand(K);u.addCommand(V);u.addCommand(Z);u.addCommand(X,{hidden:!0});u.addCommand(Q);u.addCommand(H);u.addCommand(ee);u.exitOverride();async function Be(){try{await u.parseAsync(process.argv)}catch(o){o instanceof Error?(o.name==="CommanderError"&&["commander.helpDisplayed","commander.version"].includes(o.code||"")&&process.exit(0),console.error(`Error: ${o.message}`)):console.error(`Error: ${o}`),process.exit(1)}}Be();
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/new.ts","../src/lib/config.ts","../src/lib/auth.ts","../src/lib/api-client.ts","../src/lib/websocket.ts","../src/lib/terminal.ts","../src/protocol/messages.ts","../src/lib/syncback.ts","../src/lib/workspace.ts","../src/commands/connect.ts","../src/commands/list.ts","../src/commands/stop.ts","../src/commands/stopall.ts","../src/commands/login.ts","../src/commands/logout.ts","../src/commands/version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { program } from 'commander';\nimport { newCommand } from './commands/new.js';\nimport { connectCommand } from './commands/connect.js';\nimport { listCommand } from './commands/list.js';\nimport { stopCommand } from './commands/stop.js';\nimport { stopAllCommand } from './commands/stopall.js';\nimport { loginCommand } from './commands/login.js';\nimport { logoutCommand } from './commands/logout.js';\nimport { versionCommand } from './commands/version.js';\n\n// VERSION is replaced at build time by tsup\ndeclare const __VERSION__: string;\nconst version = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'dev';\n\nprogram\n .name('catty')\n .description('Catty - Remote AI agent sessions')\n .option('--api <url>', 'API server address')\n .version(version);\n\nprogram.addCommand(newCommand);\nprogram.addCommand(connectCommand);\nprogram.addCommand(listCommand);\nprogram.addCommand(stopCommand);\nprogram.addCommand(stopAllCommand, { hidden: true });\nprogram.addCommand(loginCommand);\nprogram.addCommand(logoutCommand);\nprogram.addCommand(versionCommand);\n\n// Handle errors gracefully\nprogram.exitOverride();\n\nasync function main() {\n try {\n await program.parseAsync(process.argv);\n } catch (err: unknown) {\n if (err instanceof Error) {\n // Commander throws for help/version, ignore those\n if (\n err.name === 'CommanderError' &&\n ['commander.helpDisplayed', 'commander.version'].includes(\n (err as { code?: string }).code || ''\n )\n ) {\n process.exit(0);\n }\n\n console.error(`Error: ${err.message}`);\n } else {\n console.error(`Error: ${err}`);\n }\n process.exit(1);\n }\n}\n\nmain();\n","import { Command } from 'commander';\nimport open from 'open';\nimport { getAPIAddr } from '../lib/config.js';\nimport { isLoggedIn } from '../lib/auth.js';\nimport { APIClient, APIError } from '../lib/api-client.js';\nimport { connectToSession } from '../lib/websocket.js';\nimport { uploadWorkspace, buildUploadURL } from '../lib/workspace.js';\n\nexport const newCommand = new Command('new')\n .description('Start a new remote agent session')\n .option('--agent <name>', 'Agent to use: claude or codex', 'claude')\n .option('--no-upload', \"Don't upload current directory\")\n .option('--no-sync-back', 'Disable sync-back')\n .option(\n '--enable-prompts',\n 'Enable permission prompts (by default, all permissions are auto-approved)',\n false\n )\n .action(async function (this: Command) {\n const opts = this.opts();\n const apiAddr = getAPIAddr(this.optsWithGlobals().api);\n\n if (!isLoggedIn()) {\n console.error(\"Not logged in. Please run 'catty login' first.\");\n process.exit(1);\n }\n\n const client = new APIClient(apiAddr);\n\n console.log('Creating session...');\n\n // Determine command arguments based on agent and prompts setting\n let cmdArgs: string[];\n switch (opts.agent) {\n case 'claude':\n if (opts.enablePrompts) {\n // User wants prompts - don't skip permissions\n cmdArgs = ['claude-wrapper'];\n } else {\n // Default: auto-approve all permissions\n cmdArgs = ['claude-wrapper', '--dangerously-skip-permissions'];\n }\n break;\n case 'codex':\n cmdArgs = ['codex'];\n break;\n default:\n console.error(\n `Unknown agent: ${opts.agent} (must be 'claude' or 'codex')`\n );\n process.exit(1);\n }\n\n let session;\n try {\n session = await client.createSession({\n agent: opts.agent,\n cmd: cmdArgs,\n region: 'iad',\n ttl_sec: 7200,\n });\n } catch (err) {\n if (err instanceof APIError && err.isQuotaExceeded()) {\n await handleQuotaExceeded(client);\n return;\n }\n throw err;\n }\n\n console.log(`Session created: ${session.label}`);\n console.log(` Reconnect with: catty connect ${session.label}`);\n\n // Upload workspace\n if (opts.upload !== false) {\n console.log('Uploading workspace...');\n const uploadURL = buildUploadURL(session.connect_url);\n\n await uploadWorkspace(\n uploadURL,\n session.connect_token,\n session.headers['fly-force-instance-id']\n );\n console.log('Workspace uploaded.');\n }\n\n console.log(`Connecting to ${session.connect_url}...`);\n\n await connectToSession({\n connectURL: session.connect_url,\n connectToken: session.connect_token,\n headers: session.headers,\n syncBack: opts.syncBack !== false,\n });\n });\n\nasync function handleQuotaExceeded(client: APIClient): Promise<void> {\n console.error('');\n console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n console.error(' Free tier quota exceeded (1M tokens/month)');\n console.error(' Upgrade to Pro for unlimited usage.');\n console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n console.error('');\n\n try {\n const checkoutURL = await client.createCheckoutSession();\n console.error('Opening upgrade page in your browser...');\n await open(checkoutURL);\n } catch (err) {\n console.error(`Failed to create checkout session: ${err}`);\n console.error('Please visit https://catty.dev to upgrade.');\n }\n}\n","export const DEFAULT_API_ADDR = 'https://api.catty.dev';\nexport const CREDENTIALS_DIR = '.catty';\nexport const CREDENTIALS_FILE = 'credentials.json';\nexport const MAX_UPLOAD_SIZE = 100 * 1024 * 1024; // 100MB\n\n// Timeouts\nexport const API_TIMEOUT_MS = 120_000; // 120 seconds for API requests (machine creation can be slow)\nexport const WS_WRITE_TIMEOUT_MS = 10_000; // 10 seconds for WebSocket writes\nexport const WS_READ_TIMEOUT_MS = 60_000; // 60 seconds (must be > 25s ping interval)\nexport const SYNC_BACK_ACK_TIMEOUT_MS = 2_000; // Warn if no sync-back ack after 2s\n\n// WebSocket close codes\nexport const WS_POLICY_VIOLATION = 1008; // Connection replaced by new one\n\n// Helper to get API address (checks flag, env var, or default)\nexport function getAPIAddr(cliOption?: string): string {\n if (cliOption) return cliOption;\n if (process.env.CATTY_API_ADDR) return process.env.CATTY_API_ADDR;\n return DEFAULT_API_ADDR;\n}\n\n// Sleep helper for polling\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { homedir } from 'os';\nimport { join } from 'path';\nimport {\n readFileSync,\n writeFileSync,\n mkdirSync,\n unlinkSync,\n existsSync,\n} from 'fs';\nimport { CREDENTIALS_DIR, CREDENTIALS_FILE } from './config.js';\nimport type { Credentials } from '../types/index.js';\n\nexport function getCredentialsDir(): string {\n return join(homedir(), CREDENTIALS_DIR);\n}\n\nexport function getCredentialsPath(): string {\n return join(getCredentialsDir(), CREDENTIALS_FILE);\n}\n\nexport function loadCredentials(): Credentials | null {\n const path = getCredentialsPath();\n try {\n const content = readFileSync(path, 'utf-8');\n return JSON.parse(content) as Credentials;\n } catch {\n return null;\n }\n}\n\nexport function saveCredentials(creds: Credentials): void {\n const dir = getCredentialsDir();\n const path = getCredentialsPath();\n\n // Create directory with 0700 permissions\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n\n // Write file with 0600 permissions\n writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 0o600 });\n}\n\nexport function deleteCredentials(): void {\n const path = getCredentialsPath();\n if (existsSync(path)) {\n unlinkSync(path);\n }\n}\n\nexport function isLoggedIn(): boolean {\n const creds = loadCredentials();\n if (!creds) return false;\n\n // Check if token exists\n if (!creds.access_token) return false;\n\n // If there's an expiry and no refresh token, check if expired\n if (creds.expires_at && !creds.refresh_token) {\n const expiresAt = new Date(creds.expires_at);\n if (expiresAt <= new Date()) {\n return false;\n }\n }\n\n // If we have a refresh token, we can refresh even if access token expired\n return true;\n}\n\nexport function getAccessToken(): string | null {\n const creds = loadCredentials();\n return creds?.access_token || null;\n}\n\nexport function getRefreshToken(): string | null {\n const creds = loadCredentials();\n return creds?.refresh_token || null;\n}\n","import {\n DEFAULT_API_ADDR,\n API_TIMEOUT_MS,\n} from './config.js';\nimport {\n getAccessToken,\n getRefreshToken,\n loadCredentials,\n saveCredentials,\n} from './auth.js';\nimport type {\n CreateSessionRequest,\n CreateSessionResponse,\n SessionInfo,\n APIErrorResponse,\n} from '../types/index.js';\n\nexport class APIError extends Error {\n constructor(\n public statusCode: number,\n public errorCode: string,\n message: string,\n public upgradeURL?: string\n ) {\n super(message);\n this.name = 'APIError';\n }\n\n isQuotaExceeded(): boolean {\n return this.statusCode === 402 && this.errorCode === 'quota_exceeded';\n }\n}\n\nexport class APIClient {\n private baseURL: string;\n private authToken: string | null;\n\n constructor(baseURL?: string) {\n this.baseURL = baseURL || process.env.CATTY_API_ADDR || DEFAULT_API_ADDR;\n this.authToken = getAccessToken();\n }\n\n private async doRequest(\n method: string,\n path: string,\n body?: unknown\n ): Promise<Response> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);\n\n try {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n if (this.authToken) {\n headers['Authorization'] = `Bearer ${this.authToken}`;\n }\n\n const response = await fetch(`${this.baseURL}${path}`, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n return response;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n private async doRequestWithRefresh(\n method: string,\n path: string,\n body?: unknown\n ): Promise<Response> {\n let response = await this.doRequest(method, path, body);\n\n if (response.status === 401) {\n const refreshed = await this.refreshAuthToken();\n if (refreshed) {\n response = await this.doRequest(method, path, body);\n }\n }\n\n return response;\n }\n\n private async refreshAuthToken(): Promise<boolean> {\n const refreshToken = getRefreshToken();\n if (!refreshToken) return false;\n\n try {\n const response = await this.doRequest('POST', '/v1/auth/refresh', {\n refresh_token: refreshToken,\n });\n\n if (!response.ok) return false;\n\n const data = await response.json();\n if (!data.access_token) return false;\n\n // Update stored credentials\n const creds = loadCredentials();\n if (creds) {\n creds.access_token = data.access_token;\n if (data.refresh_token) {\n creds.refresh_token = data.refresh_token;\n }\n if (data.expires_in) {\n creds.expires_at = new Date(\n Date.now() + (data.expires_in - 30) * 1000\n ).toISOString();\n }\n saveCredentials(creds);\n this.authToken = data.access_token;\n }\n\n return true;\n } catch {\n return false;\n }\n }\n\n private async handleResponse<T>(response: Response): Promise<T> {\n if (!response.ok) {\n let errorData: APIErrorResponse;\n try {\n errorData = await response.json();\n } catch {\n errorData = { error: response.statusText };\n }\n\n throw new APIError(\n response.status,\n errorData.code || '',\n errorData.error || response.statusText,\n errorData.upgrade_url\n );\n }\n\n return response.json() as Promise<T>;\n }\n\n async createSession(req: CreateSessionRequest): Promise<CreateSessionResponse> {\n const response = await this.doRequestWithRefresh('POST', '/v1/sessions', req);\n return this.handleResponse<CreateSessionResponse>(response);\n }\n\n async listSessions(): Promise<SessionInfo[]> {\n const response = await this.doRequestWithRefresh('GET', '/v1/sessions');\n return this.handleResponse<SessionInfo[]>(response);\n }\n\n async getSession(idOrLabel: string, live?: boolean): Promise<SessionInfo> {\n const path = live\n ? `/v1/sessions/${idOrLabel}?live=true`\n : `/v1/sessions/${idOrLabel}`;\n const response = await this.doRequestWithRefresh('GET', path);\n return this.handleResponse<SessionInfo>(response);\n }\n\n async stopSession(idOrLabel: string, del?: boolean): Promise<void> {\n const path = del\n ? `/v1/sessions/${idOrLabel}?delete=true`\n : `/v1/sessions/${idOrLabel}`;\n const response = await this.doRequestWithRefresh('DELETE', path);\n\n if (!response.ok) {\n let errorData: APIErrorResponse;\n try {\n errorData = await response.json();\n } catch {\n errorData = { error: response.statusText };\n }\n throw new APIError(\n response.status,\n errorData.code || '',\n errorData.error || response.statusText\n );\n }\n }\n\n async createCheckoutSession(): Promise<string> {\n const response = await this.doRequestWithRefresh('POST', '/v1/checkout');\n const data = await this.handleResponse<{ url: string }>(response);\n return data.url;\n }\n}\n","import WebSocket from 'ws';\nimport { Terminal } from './terminal.js';\nimport {\n SYNC_BACK_ACK_TIMEOUT_MS,\n WS_POLICY_VIOLATION,\n} from './config.js';\nimport {\n parseMessage,\n createResizeMessage,\n createPongMessage,\n createSyncBackMessage,\n type Message,\n type ExitMessage,\n type ErrorMessage,\n type FileChangeMessage,\n} from '../protocol/messages.js';\nimport { applyRemoteFileChange } from './syncback.js';\n\nexport interface WebSocketConnectOptions {\n connectURL: string;\n connectToken: string;\n headers: Record<string, string>;\n syncBack: boolean;\n onExit?: (code: number) => void;\n}\n\nexport async function connectToSession(\n opts: WebSocketConnectOptions\n): Promise<void> {\n const terminal = new Terminal();\n\n if (!terminal.isTerminal()) {\n throw new Error('stdin is not a terminal');\n }\n\n const ws = new WebSocket(opts.connectURL, {\n headers: {\n ...opts.headers,\n Authorization: `Bearer ${opts.connectToken}`,\n },\n });\n\n return new Promise((resolve, reject) => {\n let syncBackAcked = false;\n let exitCode = 0;\n\n const handleResize = () => {\n const { cols, rows } = terminal.getSize();\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(createResizeMessage(cols, rows));\n }\n };\n\n const cleanup = () => {\n terminal.restore();\n terminal.offResize(handleResize);\n process.stdin.off('data', handleStdinData);\n };\n\n const handleStdinData = (data: Buffer) => {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(data); // Binary\n }\n };\n\n ws.on('open', () => {\n // Enable sync-back if requested\n if (opts.syncBack) {\n ws.send(createSyncBackMessage(true));\n\n // Warn if no ack after timeout\n setTimeout(() => {\n if (!syncBackAcked) {\n process.stderr.write(\n '\\r\\n(sync-back) No ack from executor yet — this machine may be running an older catty-exec image without sync-back.\\r\\n'\n );\n }\n }, SYNC_BACK_ACK_TIMEOUT_MS);\n }\n\n // Enter raw mode\n terminal.makeRaw();\n\n // Send initial size\n const { cols, rows } = terminal.getSize();\n ws.send(createResizeMessage(cols, rows));\n\n // Handle resize\n terminal.onResize(handleResize);\n\n // Relay stdin -> WebSocket\n process.stdin.on('data', handleStdinData);\n });\n\n // Relay WebSocket -> stdout\n ws.on('message', (data: WebSocket.RawData, isBinary: boolean) => {\n if (isBinary) {\n process.stdout.write(data as Buffer);\n } else {\n try {\n const msg = parseMessage(data.toString());\n handleControlMessage(msg);\n } catch {\n // Ignore parse errors\n }\n }\n });\n\n function handleControlMessage(msg: Message) {\n switch (msg.type) {\n case 'exit': {\n const exitMsg = msg as ExitMessage;\n exitCode = exitMsg.code;\n opts.onExit?.(exitMsg.code);\n process.stderr.write(`\\r\\nProcess exited with code ${exitMsg.code}\\r\\n`);\n cleanup();\n ws.close();\n resolve();\n break;\n }\n case 'error': {\n const errorMsg = msg as ErrorMessage;\n process.stderr.write(`\\r\\nError: ${errorMsg.message}\\r\\n`);\n break;\n }\n case 'ping':\n ws.send(createPongMessage());\n break;\n case 'file_change':\n applyRemoteFileChange(msg as FileChangeMessage);\n break;\n case 'sync_back_ack':\n syncBackAcked = true;\n break;\n }\n }\n\n ws.on('close', (code: number) => {\n cleanup();\n // Code 1008 (WS_POLICY_VIOLATION) = connection replaced by new one\n // This is a clean termination, not an error\n if (code === WS_POLICY_VIOLATION) {\n resolve();\n } else {\n resolve();\n }\n });\n\n ws.on('error', (err: Error) => {\n cleanup();\n reject(err);\n });\n\n // Handle process exit\n process.on('exit', () => {\n cleanup();\n if (ws.readyState === WebSocket.OPEN) {\n ws.close();\n }\n });\n });\n}\n","export class Terminal {\n private wasRaw = false;\n private cleanupDone = false;\n\n isTerminal(): boolean {\n return process.stdin.isTTY === true;\n }\n\n makeRaw(): void {\n if (!this.isTerminal()) return;\n if (this.wasRaw) return;\n\n process.stdin.setRawMode(true);\n process.stdin.resume();\n this.wasRaw = true;\n\n // Ensure terminal is restored on process exit\n const cleanup = () => this.restore();\n\n process.on('exit', cleanup);\n\n process.on('SIGINT', () => {\n cleanup();\n process.exit(130); // 128 + SIGINT(2)\n });\n\n process.on('SIGTERM', () => {\n cleanup();\n process.exit(143); // 128 + SIGTERM(15)\n });\n }\n\n restore(): void {\n if (this.cleanupDone) return;\n if (this.wasRaw && process.stdin.isTTY) {\n try {\n process.stdin.setRawMode(false);\n } catch {\n // Ignore errors during cleanup\n }\n this.wasRaw = false;\n }\n this.cleanupDone = true;\n }\n\n getSize(): { cols: number; rows: number } {\n return {\n cols: process.stdout.columns || 80,\n rows: process.stdout.rows || 24,\n };\n }\n\n onResize(callback: () => void): void {\n process.stdout.on('resize', callback);\n }\n\n offResize(callback: () => void): void {\n process.stdout.off('resize', callback);\n }\n}\n","export const MessageType = {\n RESIZE: 'resize',\n SIGNAL: 'signal',\n PING: 'ping',\n PONG: 'pong',\n READY: 'ready',\n EXIT: 'exit',\n ERROR: 'error',\n SYNC_BACK: 'sync_back',\n SYNC_BACK_ACK: 'sync_back_ack',\n FILE_CHANGE: 'file_change',\n} as const;\n\nexport interface BaseMessage {\n type: string;\n}\n\nexport interface ResizeMessage {\n type: 'resize';\n cols: number;\n rows: number;\n}\n\nexport interface SignalMessage {\n type: 'signal';\n name: string;\n}\n\nexport interface PingMessage {\n type: 'ping';\n}\n\nexport interface PongMessage {\n type: 'pong';\n}\n\nexport interface ReadyMessage {\n type: 'ready';\n}\n\nexport interface ExitMessage {\n type: 'exit';\n code: number;\n signal: string | null;\n}\n\nexport interface ErrorMessage {\n type: 'error';\n message: string;\n}\n\nexport interface SyncBackMessage {\n type: 'sync_back';\n enabled: boolean;\n}\n\nexport interface SyncBackAckMessage {\n type: 'sync_back_ack';\n enabled: boolean;\n workspace_dir?: string;\n interval_ms?: number;\n}\n\nexport interface FileChangeMessage {\n type: 'file_change';\n action: 'write' | 'delete';\n path: string;\n content?: string; // base64 encoded\n mode?: number;\n}\n\nexport type Message =\n | ResizeMessage\n | SignalMessage\n | PingMessage\n | PongMessage\n | ReadyMessage\n | ExitMessage\n | ErrorMessage\n | SyncBackMessage\n | SyncBackAckMessage\n | FileChangeMessage\n | BaseMessage;\n\nexport function parseMessage(data: string): Message {\n const base = JSON.parse(data) as BaseMessage;\n\n switch (base.type) {\n case MessageType.RESIZE:\n return JSON.parse(data) as ResizeMessage;\n case MessageType.SIGNAL:\n return JSON.parse(data) as SignalMessage;\n case MessageType.PING:\n return { type: 'ping' } as PingMessage;\n case MessageType.PONG:\n return { type: 'pong' } as PongMessage;\n case MessageType.READY:\n return { type: 'ready' } as ReadyMessage;\n case MessageType.EXIT:\n return JSON.parse(data) as ExitMessage;\n case MessageType.ERROR:\n return JSON.parse(data) as ErrorMessage;\n case MessageType.SYNC_BACK:\n return JSON.parse(data) as SyncBackMessage;\n case MessageType.SYNC_BACK_ACK:\n return JSON.parse(data) as SyncBackAckMessage;\n case MessageType.FILE_CHANGE:\n return JSON.parse(data) as FileChangeMessage;\n default:\n return base;\n }\n}\n\nexport function createResizeMessage(cols: number, rows: number): string {\n return JSON.stringify({ type: MessageType.RESIZE, cols, rows });\n}\n\nexport function createSignalMessage(name: string): string {\n return JSON.stringify({ type: MessageType.SIGNAL, name });\n}\n\nexport function createPingMessage(): string {\n return JSON.stringify({ type: MessageType.PING });\n}\n\nexport function createPongMessage(): string {\n return JSON.stringify({ type: MessageType.PONG });\n}\n\nexport function createSyncBackMessage(enabled: boolean): string {\n return JSON.stringify({ type: MessageType.SYNC_BACK, enabled });\n}\n","import { writeFileSync, unlinkSync, mkdirSync, renameSync } from 'fs';\nimport { join, dirname, normalize, isAbsolute, sep } from 'path';\nimport type { FileChangeMessage } from '../protocol/messages.js';\n\n/**\n * Apply a remote file change to the local filesystem.\n * Best-effort: errors are logged but don't break the terminal.\n */\nexport function applyRemoteFileChange(msg: FileChangeMessage): void {\n // Validate path (no absolute, no traversal)\n const rel = normalize(msg.path.replace(/^\\.\\//, ''));\n\n if (!rel || rel === '.') return;\n\n if (isAbsolute(rel)) {\n console.error(`sync-back rejected absolute path: ${rel}`);\n return;\n }\n\n if (rel === '..' || rel.startsWith('..' + sep)) {\n console.error(`sync-back rejected traversal path: ${rel}`);\n return;\n }\n\n const cwd = process.cwd();\n const destPath = join(cwd, rel);\n\n // Ensure destPath is within cwd (resolve any remaining symlinks/tricks)\n const resolvedCwd = normalize(cwd);\n const resolvedDest = normalize(destPath);\n\n if (!resolvedDest.startsWith(resolvedCwd + sep) && resolvedDest !== resolvedCwd) {\n console.error(`sync-back rejected path outside base: ${destPath}`);\n return;\n }\n\n try {\n if (msg.action === 'delete') {\n unlinkSync(destPath);\n } else if (msg.action === 'write') {\n const content = Buffer.from(msg.content || '', 'base64');\n mkdirSync(dirname(destPath), { recursive: true });\n\n // Atomic write via temp file\n const tmpPath = join(dirname(destPath), `.catty-sync-${Date.now()}`);\n writeFileSync(tmpPath, content, { mode: msg.mode || 0o644 });\n renameSync(tmpPath, destPath);\n }\n } catch {\n // Best-effort, don't break terminal\n }\n}\n","import archiver from 'archiver';\nimport ignore, { type Ignore } from 'ignore';\nimport { createReadStream, readFileSync, readdirSync, statSync } from 'fs';\nimport { join, relative } from 'path';\nimport { MAX_UPLOAD_SIZE } from './config.js';\n\nconst DEFAULT_IGNORES = [\n '.git',\n '.git/**',\n 'node_modules',\n 'node_modules/**',\n '__pycache__',\n '__pycache__/**',\n '.venv',\n '.venv/**',\n 'venv',\n 'venv/**',\n '.env',\n '*.pyc',\n '.DS_Store',\n '*.log',\n];\n\nexport async function createWorkspaceZip(dir: string): Promise<Buffer> {\n const ig = ignore().add(DEFAULT_IGNORES);\n\n // Load .gitignore if exists\n try {\n const gitignore = readFileSync(join(dir, '.gitignore'), 'utf-8');\n ig.add(gitignore);\n } catch {\n // No .gitignore, use defaults only\n }\n\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n const archive = archiver('zip', { zlib: { level: 9 } });\n\n archive.on('data', (chunk: Buffer) => chunks.push(chunk));\n archive.on('end', () => resolve(Buffer.concat(chunks)));\n archive.on('error', reject);\n\n // Walk directory and add files\n walkDir(dir, dir, ig, archive);\n archive.finalize();\n });\n}\n\nfunction walkDir(\n baseDir: string,\n currentDir: string,\n ig: Ignore,\n archive: archiver.Archiver\n): void {\n let entries: string[];\n try {\n entries = readdirSync(currentDir);\n } catch {\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(currentDir, entry);\n const relativePath = relative(baseDir, fullPath);\n\n // Check if ignored\n if (ig.ignores(relativePath)) {\n continue;\n }\n\n let stat;\n try {\n stat = statSync(fullPath);\n } catch {\n continue;\n }\n\n if (stat.isDirectory()) {\n // Also check directory with trailing slash\n if (ig.ignores(relativePath + '/')) {\n continue;\n }\n walkDir(baseDir, fullPath, ig, archive);\n } else if (stat.isFile()) {\n archive.file(fullPath, { name: relativePath });\n }\n }\n}\n\nexport async function uploadWorkspace(\n uploadURL: string,\n token: string,\n machineID: string\n): Promise<void> {\n const cwd = process.cwd();\n const zipData = await createWorkspaceZip(cwd);\n\n if (zipData.length > MAX_UPLOAD_SIZE) {\n throw new Error(\n `Workspace too large (${zipData.length} bytes, max ${MAX_UPLOAD_SIZE})`\n );\n }\n\n const response = await fetch(uploadURL, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/zip',\n 'fly-force-instance-id': machineID,\n },\n body: zipData,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`Upload failed: ${response.status} - ${text}`);\n }\n}\n\n/**\n * Build upload URL from connect URL.\n * Converts wss://app.fly.dev/connect to https://app.fly.dev/upload\n */\nexport function buildUploadURL(connectURL: string): string {\n return connectURL\n .replace('wss://', 'https://')\n .replace('ws://', 'http://')\n .replace('/connect', '/upload');\n}\n","import { Command } from 'commander';\nimport { getAPIAddr } from '../lib/config.js';\nimport { isLoggedIn } from '../lib/auth.js';\nimport { APIClient } from '../lib/api-client.js';\nimport { connectToSession } from '../lib/websocket.js';\n\nexport const connectCommand = new Command('connect')\n .description('Reconnect to an existing session')\n .argument('<label>', 'Session label (e.g., brave-tiger-1234)')\n .option('--sync-back', 'Sync remote file changes back to local', true)\n .option('--no-sync-back', 'Disable sync-back')\n .action(async function (this: Command, label: string) {\n const opts = this.opts();\n const apiAddr = getAPIAddr(this.optsWithGlobals().api);\n\n if (!isLoggedIn()) {\n console.error(\"Not logged in. Please run 'catty login' first.\");\n process.exit(1);\n }\n\n const client = new APIClient(apiAddr);\n\n console.log(`Looking up session ${label}...`);\n const session = await client.getSession(label, true);\n\n if (session.status === 'stopped') {\n throw new Error(`Session ${session.label} is stopped`);\n }\n if (session.machine_state && session.machine_state !== 'started') {\n throw new Error(`Machine is not running (state: ${session.machine_state})`);\n }\n\n console.log(`Reconnecting to ${session.label}...`);\n\n await connectToSession({\n connectURL: session.connect_url,\n connectToken: session.connect_token!,\n headers: { 'fly-force-instance-id': session.machine_id },\n syncBack: opts.syncBack,\n });\n });\n","import { Command } from 'commander';\nimport { getAPIAddr } from '../lib/config.js';\nimport { APIClient } from '../lib/api-client.js';\n\nexport const listCommand = new Command('list')\n .aliases(['ls'])\n .description('List all sessions')\n .action(async function (this: Command) {\n const apiAddr = getAPIAddr(this.optsWithGlobals().api);\n const client = new APIClient(apiAddr);\n\n const sessions = await client.listSessions();\n\n if (sessions.length === 0) {\n console.log('No sessions found');\n return;\n }\n\n // Simple table output\n const header = 'LABEL STATUS REGION CREATED';\n console.log(header);\n\n for (const s of sessions) {\n const age = formatAge(new Date(s.created_at));\n const row = [\n s.label.padEnd(22),\n s.status.padEnd(9),\n s.region.padEnd(7),\n age,\n ].join(' ');\n console.log(row);\n }\n });\n\n/**\n * Human-readable time ago formatting\n */\nfunction formatAge(date: Date): string {\n const seconds = Math.floor((Date.now() - date.getTime()) / 1000);\n\n if (seconds < 60) return `${seconds}s ago`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n return `${days}d ago`;\n}\n","import { Command } from 'commander';\nimport { getAPIAddr } from '../lib/config.js';\nimport { APIClient } from '../lib/api-client.js';\n\nexport const stopCommand = new Command('stop')\n .description('Stop a session')\n .argument('<label>', 'Session ID or label')\n .option('--delete', 'Delete the machine after stopping', false)\n .action(async function (this: Command, label: string) {\n const opts = this.opts();\n const apiAddr = getAPIAddr(this.optsWithGlobals().api);\n const client = new APIClient(apiAddr);\n\n await client.stopSession(label, opts.delete);\n\n if (opts.delete) {\n console.log(`Session ${label} stopped and deleted`);\n } else {\n console.log(`Session ${label} stopped`);\n }\n });\n","import { Command } from 'commander';\nimport { getAPIAddr } from '../lib/config.js';\nimport { APIClient } from '../lib/api-client.js';\n\nexport const stopAllCommand = new Command('stop-all-sessions-dangerously')\n .description('Stop and delete ALL sessions')\n .option('--yes-i-mean-it', 'Confirm you want to stop all sessions', false)\n .action(async function (this: Command) {\n const opts = this.opts();\n const apiAddr = getAPIAddr(this.optsWithGlobals().api);\n\n if (!opts.yesIMeanIt) {\n throw new Error('Must pass --yes-i-mean-it to confirm');\n }\n\n const client = new APIClient(apiAddr);\n const sessions = await client.listSessions();\n\n if (sessions.length === 0) {\n console.log('No sessions to stop');\n return;\n }\n\n console.log(`Stopping ${sessions.length} sessions...`);\n\n for (const s of sessions) {\n process.stdout.write(` Stopping ${s.session_id}... `);\n try {\n await client.stopSession(s.session_id, true);\n console.log('done');\n } catch (err) {\n console.log(`ERROR: ${err}`);\n }\n }\n });\n","import { Command } from 'commander';\nimport open from 'open';\nimport { getAPIAddr, sleep } from '../lib/config.js';\nimport { isLoggedIn, loadCredentials, saveCredentials } from '../lib/auth.js';\nimport type { DeviceAuthResponse, TokenResponse } from '../types/index.js';\n\nexport const loginCommand = new Command('login')\n .description('Log in to Catty')\n .action(async function (this: Command) {\n const apiAddr = getAPIAddr(this.optsWithGlobals().api);\n\n if (isLoggedIn()) {\n const creds = loadCredentials();\n console.log(`Already logged in as ${creds?.email}`);\n console.log(\"Run 'catty logout' to log out first\");\n return;\n }\n\n console.log('Starting login...');\n\n // Step 1: Start device auth flow\n const authResp = await fetch(`${apiAddr}/v1/auth/device`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n\n if (!authResp.ok) {\n throw new Error(`Failed to start auth: ${authResp.statusText}`);\n }\n\n const auth: DeviceAuthResponse = await authResp.json();\n\n // Step 2: Show code and open browser\n console.log('\\nYour confirmation code:\\n');\n console.log(` ${auth.user_code}\\n`);\n console.log(`Opening ${auth.verification_uri_complete}\\n`);\n\n await open(auth.verification_uri_complete);\n console.log('Waiting for authentication...');\n\n // Step 3: Poll for token\n const interval = (auth.interval || 5) * 1000;\n const deadline = Date.now() + auth.expires_in * 1000;\n\n while (Date.now() < deadline) {\n await sleep(interval);\n\n const tokenResp = await fetch(`${apiAddr}/v1/auth/device/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ device_code: auth.device_code }),\n });\n\n const token: TokenResponse = await tokenResp.json();\n\n if (token.pending) continue;\n if (token.error) throw new Error(token.error);\n\n if (token.access_token) {\n saveCredentials({\n access_token: token.access_token,\n refresh_token: token.refresh_token,\n user_id: token.user?.id || '',\n email: token.user?.email || '',\n expires_at: token.expires_in\n ? new Date(Date.now() + (token.expires_in - 30) * 1000).toISOString()\n : undefined,\n });\n console.log(`\\nLogged in as ${token.user?.email}`);\n console.log(\"You can now run 'catty new' to start a session\");\n return;\n }\n }\n\n throw new Error('Authentication timed out');\n });\n","import { Command } from 'commander';\nimport { isLoggedIn, loadCredentials, deleteCredentials } from '../lib/auth.js';\n\nexport const logoutCommand = new Command('logout')\n .description('Log out of Catty')\n .action(async () => {\n if (!isLoggedIn()) {\n console.log('Not logged in');\n return;\n }\n\n const creds = loadCredentials();\n const email = creds?.email || '';\n\n deleteCredentials();\n\n if (email) {\n console.log(`Logged out from ${email}`);\n } else {\n console.log('Logged out');\n }\n });\n","import { Command } from 'commander';\n\n// VERSION is replaced at build time by tsup\ndeclare const __VERSION__: string;\n\nexport const versionCommand = new Command('version')\n .description('Print the version number')\n .action(() => {\n console.log(typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'dev');\n });\n"],"mappings":"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","names":["program","Command","open","DEFAULT_API_ADDR","CREDENTIALS_DIR","CREDENTIALS_FILE","getAPIAddr","cliOption","DEFAULT_API_ADDR","sleep","ms","resolve","homedir","join","readFileSync","writeFileSync","mkdirSync","unlinkSync","existsSync","getCredentialsDir","join","homedir","CREDENTIALS_DIR","getCredentialsPath","CREDENTIALS_FILE","loadCredentials","path","content","readFileSync","saveCredentials","creds","dir","mkdirSync","writeFileSync","deleteCredentials","existsSync","unlinkSync","isLoggedIn","getAccessToken","getRefreshToken","APIError","statusCode","errorCode","message","upgradeURL","APIClient","baseURL","DEFAULT_API_ADDR","getAccessToken","method","path","body","controller","timeoutId","headers","response","refreshToken","getRefreshToken","data","creds","loadCredentials","saveCredentials","errorData","req","idOrLabel","live","del","WebSocket","Terminal","cleanup","callback","MessageType","parseMessage","data","base","createResizeMessage","cols","rows","createPongMessage","MessageType","createSyncBackMessage","enabled","writeFileSync","unlinkSync","mkdirSync","renameSync","join","dirname","normalize","isAbsolute","sep","applyRemoteFileChange","msg","rel","cwd","destPath","resolvedCwd","resolvedDest","content","tmpPath","connectToSession","opts","terminal","Terminal","ws","WebSocket","resolve","reject","syncBackAcked","exitCode","handleResize","cols","rows","createResizeMessage","cleanup","handleStdinData","data","createSyncBackMessage","isBinary","msg","parseMessage","handleControlMessage","exitMsg","errorMsg","createPongMessage","applyRemoteFileChange","code","err","archiver","ignore","readFileSync","readdirSync","statSync","join","relative","DEFAULT_IGNORES","createWorkspaceZip","dir","ig","ignore","gitignore","readFileSync","join","resolve","reject","chunks","archive","archiver","chunk","walkDir","baseDir","currentDir","entries","readdirSync","entry","fullPath","relativePath","relative","stat","statSync","uploadWorkspace","uploadURL","token","machineID","cwd","zipData","response","text","buildUploadURL","connectURL","newCommand","Command","opts","apiAddr","getAPIAddr","isLoggedIn","client","APIClient","cmdArgs","session","err","APIError","handleQuotaExceeded","uploadURL","buildUploadURL","uploadWorkspace","connectToSession","checkoutURL","open","Command","connectCommand","Command","label","opts","apiAddr","getAPIAddr","isLoggedIn","client","APIClient","session","connectToSession","Command","listCommand","Command","apiAddr","getAPIAddr","sessions","APIClient","age","formatAge","row","date","seconds","minutes","hours","Command","stopCommand","Command","label","opts","apiAddr","getAPIAddr","APIClient","Command","stopAllCommand","Command","opts","apiAddr","getAPIAddr","client","APIClient","sessions","err","Command","open","loginCommand","Command","apiAddr","getAPIAddr","isLoggedIn","creds","loadCredentials","authResp","auth","open","interval","deadline","sleep","token","saveCredentials","Command","logoutCommand","Command","isLoggedIn","email","loadCredentials","deleteCredentials","Command","versionCommand","version","program","newCommand","connectCommand","listCommand","stopCommand","stopAllCommand","loginCommand","logoutCommand","versionCommand","main","err"]}
package/package.json CHANGED
@@ -1,46 +1,47 @@
1
1
  {
2
2
  "name": "@diggerhq/catty",
3
- "version": "0.3.5",
4
- "description": "Run Claude Code sessions remotely",
3
+ "version": "0.3.7",
4
+ "description": "Catty - Remote AI agent sessions",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "catty": "./bin/catty.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "scripts": {
14
+ "build": "tsup && npm run build:bin",
15
+ "build:bin": "mkdir -p bin && cp scripts/catty-bin.js bin/catty.js && chmod +x bin/catty.js",
16
+ "dev": "tsup --watch",
17
+ "prepublishOnly": "npm run build"
18
+ },
5
19
  "keywords": [
20
+ "catty",
6
21
  "ai",
7
- "claude",
8
22
  "agent",
9
- "remote",
10
- "terminal",
11
- "code"
23
+ "claude",
24
+ "codex",
25
+ "remote"
12
26
  ],
13
- "author": "izalutski",
27
+ "author": "DiggerHQ",
14
28
  "license": "MIT",
15
- "repository": {
16
- "type": "git",
17
- "url": "git+https://github.com/diggerhq/catty.git"
29
+ "dependencies": {
30
+ "archiver": "^7.0.1",
31
+ "commander": "^12.1.0",
32
+ "ignore": "^5.3.2",
33
+ "open": "^10.1.0",
34
+ "ws": "^8.18.0"
18
35
  },
19
- "bin": {
20
- "catty": "./bin/catty"
21
- },
22
- "scripts": {
23
- "postinstall": "node scripts/install.js",
24
- "release": "node scripts/release.js patch",
25
- "release:ci": "node scripts/release.js --no-bump",
26
- "release:patch": "node scripts/release.js patch",
27
- "release:minor": "node scripts/release.js minor",
28
- "release:major": "node scripts/release.js major"
36
+ "devDependencies": {
37
+ "@types/archiver": "^6.0.2",
38
+ "@types/node": "^22.0.0",
39
+ "@types/ws": "^8.5.12",
40
+ "tsup": "^8.3.0",
41
+ "typescript": "^5.6.0"
29
42
  },
30
43
  "files": [
31
- "scripts",
32
- "bin",
33
- "README.md"
34
- ],
35
- "engines": {
36
- "node": ">=16"
37
- },
38
- "os": [
39
- "darwin",
40
- "linux"
41
- ],
42
- "cpu": [
43
- "x64",
44
- "arm64"
44
+ "dist",
45
+ "bin"
45
46
  ]
46
47
  }
package/README.md DELETED
@@ -1,58 +0,0 @@
1
- # Catty
2
-
3
- Run Claude Code sessions remotely.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install -g @diggerhq/catty
9
- ```
10
-
11
- Or use directly with npx:
12
-
13
- ```bash
14
- npx @diggerhq/catty new
15
- ```
16
-
17
- ## Usage
18
-
19
- ### First-time setup
20
-
21
- ```bash
22
- # Log in (required once)
23
- catty login
24
- ```
25
-
26
- ### Start a new Claude Code session
27
-
28
- ```bash
29
- catty new
30
- ```
31
-
32
- This will:
33
- 1. Upload your current directory to a remote machine
34
- 2. Start Claude Code in the remote environment
35
- 3. Connect you to an interactive terminal session
36
-
37
- ### Commands
38
-
39
- ```bash
40
- catty login # Authenticate (required before first use)
41
- catty logout # Remove stored credentials
42
- catty new # Start Claude Code (default)
43
- catty new --no-upload # Don't upload current directory
44
- catty list # List active sessions
45
- catty stop <session-id> # Stop a session
46
- ```
47
-
48
- ## Requirements
49
-
50
- - Node.js 16+
51
-
52
- ## How it works
53
-
54
- Catty creates isolated machines on-demand, uploads your workspace, and streams the terminal to you. Each session is isolated and secure.
55
-
56
- ## License
57
-
58
- MIT
package/bin/catty DELETED
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- // Placeholder entrypoint so npm reliably creates the global `catty` shim.
3
- // `npm/scripts/install.js` downloads the real platform binary and overwrites this file.
4
-
5
- console.error("catty: install incomplete (postinstall did not download the binary).");
6
- console.error("Try: npm config set ignore-scripts false && npm install -g @diggerhq/catty --foreground-scripts");
7
- process.exit(1);
8
-
9
-
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const https = require('https');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { execSync } = require('child_process');
7
-
8
- const packageJson = require('../package.json');
9
- const VERSION = process.env.CATTY_VERSION || packageJson.version;
10
- const GITHUB_REPO = 'diggerhq/catty-releases';
11
-
12
- function getPlatform() {
13
- const platform = process.platform;
14
- const arch = process.arch;
15
-
16
- const platformMap = {
17
- darwin: 'darwin',
18
- linux: 'linux',
19
- win32: 'windows',
20
- };
21
-
22
- const archMap = {
23
- x64: 'amd64',
24
- arm64: 'arm64',
25
- };
26
-
27
- const os = platformMap[platform];
28
- const cpu = archMap[arch];
29
-
30
- if (!os || !cpu) {
31
- throw new Error(`Unsupported platform: ${platform}-${arch}`);
32
- }
33
-
34
- return { os, cpu, platform, arch };
35
- }
36
-
37
- function getBinaryName(os) {
38
- return os === 'windows' ? 'catty.exe' : 'catty';
39
- }
40
-
41
- function getDownloadUrl(os, cpu) {
42
- const ext = os === 'windows' ? '.exe' : '';
43
- return `https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}/catty-${os}-${cpu}${ext}`;
44
- }
45
-
46
- function download(url, dest) {
47
- return new Promise((resolve, reject) => {
48
- const file = fs.createWriteStream(dest);
49
-
50
- const request = (url) => {
51
- https.get(url, (response) => {
52
- // Handle redirects
53
- if (response.statusCode === 302 || response.statusCode === 301) {
54
- request(response.headers.location);
55
- return;
56
- }
57
-
58
- if (response.statusCode !== 200) {
59
- reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`));
60
- return;
61
- }
62
-
63
- response.pipe(file);
64
- file.on('finish', () => {
65
- file.close(resolve);
66
- });
67
- }).on('error', (err) => {
68
- fs.unlink(dest, () => {});
69
- reject(err);
70
- });
71
- };
72
-
73
- request(url);
74
- });
75
- }
76
-
77
- async function install() {
78
- try {
79
- const { os, cpu, platform } = getPlatform();
80
- const binaryName = getBinaryName(os);
81
- const binDir = path.join(__dirname, '..', 'bin');
82
- const binaryPath = path.join(binDir, binaryName);
83
-
84
- // Create bin directory if it doesn't exist
85
- if (!fs.existsSync(binDir)) {
86
- fs.mkdirSync(binDir, { recursive: true });
87
- }
88
-
89
- // Check if binary already exists and is actually a binary (not placeholder)
90
- if (fs.existsSync(binaryPath)) {
91
- const stats = fs.statSync(binaryPath);
92
- // Placeholder is ~300 bytes, real binary is much larger
93
- if (stats.size > 1000) {
94
- console.log('catty binary already installed');
95
- return;
96
- }
97
- // Remove placeholder
98
- fs.unlinkSync(binaryPath);
99
- }
100
-
101
- const url = getDownloadUrl(os, cpu);
102
- console.log(`Downloading catty from ${url}...`);
103
-
104
- await download(url, binaryPath);
105
-
106
- // Make executable on Unix
107
- if (platform !== 'win32') {
108
- fs.chmodSync(binaryPath, 0o755);
109
- }
110
-
111
- console.log('catty installed successfully!');
112
- } catch (error) {
113
- console.error('Failed to install catty:', error.message);
114
- console.error('');
115
- console.error('You can manually download the binary from:');
116
- console.error(`https://github.com/${GITHUB_REPO}/releases`);
117
- process.exit(1);
118
- }
119
- }
120
-
121
- install();
@@ -1,47 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const { execSync } = require('child_process');
4
- const path = require('path');
5
-
6
- const rootDir = path.join(__dirname, '..', '..');
7
- const npmDir = path.join(__dirname, '..');
8
-
9
- function run(cmd, cwd = rootDir) {
10
- console.log(`> ${cmd}`);
11
- execSync(cmd, { cwd, stdio: 'inherit' });
12
- }
13
-
14
- const args = process.argv.slice(2);
15
- if (args.includes('--help') || args.includes('-h')) {
16
- console.log('Usage: node scripts/release.js [patch|minor|major] [--no-bump|ci]');
17
- process.exit(0);
18
- }
19
- const noBump = args.includes('--no-bump') || args.includes('ci');
20
- const filtered = args.filter((a) => a !== '--no-bump' && a !== 'ci');
21
-
22
- const versionType = filtered[0] || 'patch';
23
- if (!noBump && !['patch', 'minor', 'major'].includes(versionType)) {
24
- console.error('Usage: node scripts/release.js [patch|minor|major] [--no-bump|ci]');
25
- process.exit(1);
26
- }
27
-
28
- // 1. Bump version (unless running in CI mode)
29
- if (!noBump) {
30
- run(`npm version ${versionType} --no-git-tag-version`, npmDir);
31
- }
32
-
33
- // 2. Get new version
34
- const packageJson = require('../package.json');
35
- const version = packageJson.version;
36
- console.log(`\nReleasing v${version}...\n`);
37
-
38
- // 3. Build binaries (pass version to Makefile)
39
- run(`make release VERSION=${version}`);
40
-
41
- // 4. Create GitHub release (in catty-releases repo)
42
- run(`gh release create v${version} dist/* --repo diggerhq/catty-releases --title "v${version}" --notes "Release v${version}"`);
43
-
44
- // 5. Publish to npm
45
- run('npm publish --access public', npmDir);
46
-
47
- console.log(`\nReleased v${version} successfully!`);