From f93ec1338288e7e29b3258378825eb903c704b3f Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Mon, 23 Mar 2026 14:58:44 -0700 Subject: [PATCH] feat: add list_folders_in_directory and read_file command handlers (#1489) Co-authored-by: Letta Code --- src/types/protocol_v2.ts | 18 +++++- src/websocket/listener/client.ts | 74 +++++++++++++++++++++- src/websocket/listener/protocol-inbound.ts | 24 ++++++- 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/types/protocol_v2.ts b/src/types/protocol_v2.ts index 5cd0226..0294694 100644 --- a/src/types/protocol_v2.ts +++ b/src/types/protocol_v2.ts @@ -395,6 +395,20 @@ export interface SearchFilesCommand { max_results?: number; } +export interface ListFoldersInDirectoryCommand { + type: "list_folders_in_directory"; + /** Absolute path to list folders in. */ + path: string; +} + +export interface ReadFileCommand { + type: "read_file"; + /** Absolute path to the file to read. */ + path: string; + /** Echoed back in the response for request correlation. */ + request_id: string; +} + export type WsProtocolCommand = | InputCommand | ChangeDeviceStateCommand @@ -404,7 +418,9 @@ export type WsProtocolCommand = | TerminalInputCommand | TerminalResizeCommand | TerminalKillCommand - | SearchFilesCommand; + | SearchFilesCommand + | ListFoldersInDirectoryCommand + | ReadFileCommand; export type WsProtocolMessage = | DeviceStatusUpdateMessage diff --git a/src/websocket/listener/client.ts b/src/websocket/listener/client.ts index 4196686..f905fa5 100644 --- a/src/websocket/listener/client.ts +++ b/src/websocket/listener/client.ts @@ -61,7 +61,12 @@ import { loadPersistedPermissionModeMap, setConversationPermissionModeState, } from "./permissionMode"; -import { isSearchFilesCommand, parseServerMessage } from "./protocol-inbound"; +import { + isListFoldersCommand, + isReadFileCommand, + isSearchFilesCommand, + parseServerMessage, +} from "./protocol-inbound"; import { buildDeviceStatus, buildLoopStatus, @@ -1016,6 +1021,73 @@ async function connectWithRetry( return; } + // ── Folder listing (no runtime scope required) ──────────────────── + if (isListFoldersCommand(parsed)) { + void (async () => { + try { + const { readdir } = await import("node:fs/promises"); + const entries = await readdir(parsed.path, { withFileTypes: true }); + const folders = entries + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + socket.send( + JSON.stringify({ + type: "list_folders_in_directory_response", + path: parsed.path, + folders, + hasMore: false, + success: true, + }), + ); + } catch (err) { + socket.send( + JSON.stringify({ + type: "list_folders_in_directory_response", + path: parsed.path, + folders: [], + hasMore: false, + success: false, + error: + err instanceof Error ? err.message : "Failed to list folders", + }), + ); + } + })(); + return; + } + + // ── File reading (no runtime scope required) ───────────────────── + if (isReadFileCommand(parsed)) { + void (async () => { + try { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(parsed.path, "utf-8"); + socket.send( + JSON.stringify({ + type: "read_file_response", + request_id: parsed.request_id, + path: parsed.path, + content, + success: true, + }), + ); + } catch (err) { + socket.send( + JSON.stringify({ + type: "read_file_response", + request_id: parsed.request_id, + path: parsed.path, + content: null, + success: false, + error: err instanceof Error ? err.message : "Failed to read file", + }), + ); + } + })(); + return; + } + // ── Terminal commands (no runtime scope required) ────────────────── if (parsed.type === "terminal_spawn") { handleTerminalSpawn( diff --git a/src/websocket/listener/protocol-inbound.ts b/src/websocket/listener/protocol-inbound.ts index 951a88d..81c36d3 100644 --- a/src/websocket/listener/protocol-inbound.ts +++ b/src/websocket/listener/protocol-inbound.ts @@ -3,6 +3,8 @@ import type { AbortMessageCommand, ChangeDeviceStateCommand, InputCommand, + ListFoldersInDirectoryCommand, + ReadFileCommand, RuntimeScope, SearchFilesCommand, SyncCommand, @@ -253,6 +255,24 @@ export function isSearchFilesCommand( ); } +export function isListFoldersCommand( + value: unknown, +): value is ListFoldersInDirectoryCommand { + if (!value || typeof value !== "object") return false; + const c = value as { type?: unknown; path?: unknown }; + return c.type === "list_folders_in_directory" && typeof c.path === "string"; +} + +export function isReadFileCommand(value: unknown): value is ReadFileCommand { + if (!value || typeof value !== "object") return false; + const c = value as { type?: unknown; path?: unknown; request_id?: unknown }; + return ( + c.type === "read_file" && + typeof c.path === "string" && + typeof c.request_id === "string" + ); +} + export function parseServerMessage( data: WebSocket.RawData, ): ParsedServerMessage | null { @@ -268,7 +288,9 @@ export function parseServerMessage( isTerminalInputCommand(parsed) || isTerminalResizeCommand(parsed) || isTerminalKillCommand(parsed) || - isSearchFilesCommand(parsed) + isSearchFilesCommand(parsed) || + isListFoldersCommand(parsed) || + isReadFileCommand(parsed) ) { return parsed as WsProtocolCommand; }