vfs.tsā¢2.15 kB
import type { UseToolResult } from '@modelcontextprotocol/sdk/react';
export type FileSystemEntry = {
name: string;
path: string;
isDirectory: boolean;
children?: FileSystemEntry[];
};
type RunCommandTool = UseToolResult<{ command: string; cwd?: string }, { type: "text"; text: string }>;
// Helper to extract command output and ignore the CWD marker
const getCommandOutput = (result: { content: { type: "text"; text: string }[] } | null | undefined): string => {
if (result?.content?.[0]?.type === 'text') {
return result.content[0].text.split('\nCWD_MARKER:')[0].trim();
}
return '';
};
export class VFS {
private runCommandTool: RunCommandTool;
constructor(runCommandTool: RunCommandTool) {
this.runCommandTool = runCommandTool;
}
async getTree(path: string): Promise<FileSystemEntry[]> {
// Using `ls -p` which appends `/` to directories
const result = await this.runCommandTool.call({ command: `ls -p "${path}"` });
const output = getCommandOutput(result);
if (!output) return [];
return output.split('\n').filter(Boolean).map(name => {
const isDirectory = name.endsWith('/');
const cleanName = isDirectory ? name.slice(0, -1) : name;
return {
name: cleanName,
path: `${path === '/' ? '' : path}/${cleanName}`.replace('//','/'),
isDirectory,
};
});
}
async readFile(path: string): Promise<string> {
const result = await this.runCommandTool.call({ command: `cat "${path}"` });
if (result?.content?.[0]?.type === 'text') {
return getCommandOutput(result);
}
throw new Error(`Could not read file at ${path}`);
}
async writeFile(path: string, content: string): Promise<void> {
// Escape single quotes for bash `printf`
const escapedContent = content.replace(/'/g, "'\\''");
const command = `printf '%s' '${escapedContent}' > "${path}"`;
const result = await this.runCommandTool.call({ command });
const output = getCommandOutput(result);
if (output) { // There should be no output on success
throw new Error(`Failed to write file at ${path}: ${output}`);
}
}
}