@@ -58,13 +58,20 @@ type PendingCommand = {
5858 timer : Timer ;
5959} ;
6060
61+ type PendingFileRead = {
62+ resolve : ( value : { path : string ; data : string ; size : number } ) => void ;
63+ reject : ( error : Error ) => void ;
64+ timer : Timer ;
65+ } ;
66+
6167interface Session {
6268 code : string ;
6369 meta ?: AgentMeta ;
6470 createdAt : number ;
6571 lastActivity : number ;
6672 agent : ServerWebSocket < unknown > | null ;
6773 pendingHttp : Map < string , PendingCommand > ;
74+ pendingFileRead : Map < string , PendingFileRead > ;
6875}
6976
7077type ProtocolMsg =
@@ -79,7 +86,15 @@ type ProtocolMsg =
7986 | { type : "error" ; message : string }
8087 | { type : "bye" ; reason ?: string }
8188 | { type : "ping" }
82- | { type : "pong" } ;
89+ | { type : "pong" }
90+ | {
91+ type : "file_read_result" ;
92+ id : string ;
93+ path : string ;
94+ data : string ;
95+ size : number ;
96+ error ?: string ;
97+ } ;
8398
8499type RouteRequest = Request & { params : Record < string , string | undefined > } ;
85100
@@ -102,6 +117,7 @@ export function createSession(code: string): Session {
102117 lastActivity : now ,
103118 agent : null ,
104119 pendingHttp : new Map ( ) ,
120+ pendingFileRead : new Map ( ) ,
105121 } ;
106122 sessions . set ( code , session ) ;
107123 return session ;
@@ -120,6 +136,11 @@ export function closeSession(code: string): void {
120136 pending . reject ( new Error ( "Session closed" ) ) ;
121137 }
122138 session . pendingHttp . clear ( ) ;
139+ for ( const pending of session . pendingFileRead . values ( ) ) {
140+ clearTimeout ( pending . timer ) ;
141+ pending . reject ( new Error ( "Session closed" ) ) ;
142+ }
143+ session . pendingFileRead . clear ( ) ;
123144 session . agent ?. close ( ) ;
124145 sessions . delete ( code ) ;
125146}
@@ -138,6 +159,9 @@ export const routes = {
138159 GET : commandRoute ,
139160 POST : commandRoute ,
140161 } ,
162+ "/api/session/:code/download" : {
163+ GET : downloadRoute ,
164+ } ,
141165 "/api/session/:code/prompt.md" : { GET : apiPromptRoute } ,
142166 "/c/:code" : connectRoute ,
143167 "/c/:code/windows.ps1" : connectWindowsRoute ,
@@ -184,6 +208,35 @@ export function commandRoute(req: RouteRequest): Promise<Response> {
184208 return handleCommand ( req , new URL ( req . url ) , session ) ;
185209}
186210
211+ export async function downloadRoute ( req : RouteRequest ) : Promise < Response > {
212+ const code = routeCode ( req ) ;
213+ if ( ! code ) return notFound ( ) ;
214+ const session = getSession ( code ) ;
215+ if ( ! session ) return notFound ( ) ;
216+ if ( ! session . agent ) return json ( { error : "Agent not connected" } , 409 ) ;
217+
218+ const url = new URL ( req . url ) ;
219+ const path = url . searchParams . get ( "path" ) ;
220+ if ( ! path ) return json ( { error : "Missing ?path= query parameter" } , 400 ) ;
221+
222+ try {
223+ const result = await executeFileRead ( session , path ) ;
224+ const bytes = Buffer . from ( result . data , "base64" ) ;
225+ const filename = path . split ( "/" ) . filter ( Boolean ) . pop ( ) || "download" ;
226+ return new Response ( bytes , {
227+ headers : {
228+ "Content-Type" : "application/octet-stream" ,
229+ "Content-Disposition" : `attachment; filename="${ encodeURIComponent ( filename ) } "` ,
230+ "Content-Length" : String ( result . size ) ,
231+ ...NO_CACHE ,
232+ } ,
233+ } ) ;
234+ } catch ( error ) {
235+ const msg = error instanceof Error ? error . message : "Download failed" ;
236+ return json ( { error : msg } , 500 ) ;
237+ }
238+ }
239+
187240export function apiPromptRoute ( req : RouteRequest ) : Response {
188241 return promptResponse ( req ) ;
189242}
@@ -265,20 +318,36 @@ export function handleAgentMessage(
265318 raw : string ,
266319) : void {
267320 const msg = parseMessage ( raw ) ;
268- if ( ! msg || msg . type !== "command_result" ) return ;
321+ if ( ! msg ) return ;
269322
270323 for ( const session of sessions . values ( ) ) {
271324 if ( session . agent !== ws ) continue ;
272- const pending = session . pendingHttp . get ( msg . id ) ;
273- if ( ! pending ) return ;
274- clearTimeout ( pending . timer ) ;
275- session . pendingHttp . delete ( msg . id ) ;
276- pending . resolve ( {
277- output : msg . output ,
278- exit_code : msg . exit_code ,
279- truncated : msg . truncated === true ,
280- } ) ;
281- return ;
325+
326+ if ( msg . type === "command_result" ) {
327+ const pending = session . pendingHttp . get ( msg . id ) ;
328+ if ( ! pending ) return ;
329+ clearTimeout ( pending . timer ) ;
330+ session . pendingHttp . delete ( msg . id ) ;
331+ pending . resolve ( {
332+ output : msg . output ,
333+ exit_code : msg . exit_code ,
334+ truncated : msg . truncated === true ,
335+ } ) ;
336+ return ;
337+ }
338+
339+ if ( msg . type === "file_read_result" && msg . id ) {
340+ const pending = session . pendingFileRead . get ( msg . id ) ;
341+ if ( ! pending ) return ;
342+ clearTimeout ( pending . timer ) ;
343+ session . pendingFileRead . delete ( msg . id ) ;
344+ if ( msg . error ) {
345+ pending . reject ( new Error ( msg . error ) ) ;
346+ } else {
347+ pending . resolve ( { path : msg . path , data : msg . data , size : msg . size } ) ;
348+ }
349+ return ;
350+ }
282351 }
283352}
284353
@@ -288,6 +357,11 @@ export function handleDisconnect(ws: ServerWebSocket<unknown>): void {
288357 session . agent = null ;
289358 session . meta = undefined ;
290359 session . lastActivity = Date . now ( ) ;
360+ for ( const pending of session . pendingFileRead . values ( ) ) {
361+ clearTimeout ( pending . timer ) ;
362+ pending . reject ( new Error ( "Agent disconnected" ) ) ;
363+ }
364+ session . pendingFileRead . clear ( ) ;
291365 return ;
292366 }
293367}
@@ -524,6 +598,28 @@ function executeHttpCommand(
524598 } ) ;
525599}
526600
601+ function executeFileRead (
602+ session : Session ,
603+ path : string ,
604+ timeoutSec = 30 ,
605+ ) : Promise < { path : string ; data : string ; size : number } > {
606+ if ( ! session . agent ) return Promise . reject ( new Error ( "Agent not connected" ) ) ;
607+
608+ const id = crypto . randomUUID ( ) ;
609+ const timeoutMs = timeoutSec * 1000 ;
610+
611+ return new Promise ( ( resolve , reject ) => {
612+ const timer = setTimeout ( ( ) => {
613+ session . pendingFileRead . delete ( id ) ;
614+ reject ( new Error ( `File read timed out after ${ Math . round ( timeoutMs / 1000 ) } s` ) ) ;
615+ } , timeoutMs ) ;
616+
617+ session . pendingFileRead . set ( id , { resolve, reject, timer } ) ;
618+ session . agent ! . send ( JSON . stringify ( { type : "file_read" , id, path } ) ) ;
619+ session . lastActivity = Date . now ( ) ;
620+ } ) ;
621+ }
622+
527623function createUniqueSessionCode ( ) : string {
528624 const code = generateCode ( ) ;
529625 if ( sessions . has ( code ) ) return createUniqueSessionCode ( ) ;
0 commit comments