This is the final state I would target for the remote filesystem shim.
Summary:
listDirectoryfor directory listingsreadFilefor text and byte reads, including paged readsgetMetadatafor path metadata and revision tokenswriteFilefor plain and conflict-aware writescreateDirectory,deletePath,movePath, andcopyPathfor path mutationssearchFilesandsearchContentfor path and content searchwatchPathfor filesystem change events
Returns the direct children of a directory.
listDirectory({ absolutePath })Returns:
{
entries: Array<{
absolutePath: string
name: string
kind: "file" | "directory" | "symlink" | "other"
}>
}Reads a file as text or bytes. offset and maxBytes support paged reads.
Should return an opaque revision token representing the version of the file that was read.
readFile({ absolutePath, offset, maxBytes, encoding })Returns:
{
kind: "text" | "bytes"
content: string | Uint8Array
byteLength: number
exceededLimit: boolean
revision: string
}If exceededLimit is true, more data is available after the returned chunk and the client can continue reading by calling readFile again with a larger offset.
Returns file metadata, or null if the path does not exist.
Should return an opaque revision token representing the current version of the path.
getMetadata({ absolutePath })Returns:
null | {
absolutePath: string
kind: "file" | "directory" | "symlink" | "other"
size: number | null
createdAt: string | null
modifiedAt: string | null
accessedAt: string | null
mode?: number | null
permissions?: string | null
owner?: string | null
group?: string | null
symlinkTarget?: string | null
revision: string
}Writes file contents. ifMatch is the recommended conflict-aware write mechanism.
Use an opaque revision token from readFile or getMetadata. A revision is a freshness token for the version of the file the client last observed.
Without a precondition, this should act as a plain overwrite-or-create write.
writeFile({
absolutePath,
content,
encoding,
precondition: {
ifMatch: revision,
},
})Returns:
| {
ok: true
revision: string
}
| {
ok: false
reason: "conflict"
currentRevision: string
}Creates a directory. File creation should happen through writeFile.
createDirectory({ absolutePath })Returns:
{
absolutePath: string
kind: "directory"
}Deletes a path. permanent controls trash vs hard delete behavior.
deletePath({ absolutePath, permanent })Returns:
{
absolutePath: string
}Moves a path. Rename is just a same-parent move.
movePath({ sourceAbsolutePath, destinationAbsolutePath })Returns:
{
fromAbsolutePath: string
toAbsolutePath: string
}Copies a path.
copyPath({ sourceAbsolutePath, destinationAbsolutePath })Returns:
{
fromAbsolutePath: string
toAbsolutePath: string
}Searches file names and paths.
searchFiles({ query, includeHidden, includePattern, excludePattern, limit })Returns:
{
matches: Array<{
absolutePath: string
relativePath: string
name: string
kind: "file" | "directory" | "symlink" | "other"
score: number
}>
}Searches file contents and should return line/column-oriented matches.
searchContent({ query, includeHidden, includePattern, excludePattern, limit })Returns:
{
matches: Array<{
absolutePath: string
relativePath: string
line: number
column: number
preview: string
}>
}Subscribes to path changes.
watchPath({ absolutePath, recursive })Yields:
{
events: Array<{
kind: "create" | "update" | "delete" | "rename" | "overflow"
absolutePath: string
oldAbsolutePath?: string
}>
}Keep search as two distinct primitives:
searchFilessearchContent
Do not collapse them into a single overloaded search(...).
They have different semantics, different cost profiles, and different result shapes.
- The shim should be pure path-based
- Workspace scoping should live in client logic, not in the remote filesystem interface
- higher-level helpers like
readWorkspaceDirectoryorsearchFilesMultishould stay above the shim layer