feat: add list_folders_in_directory and read_file command handlers (#1489)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user