The frontend talks to the backend exclusively through the commands below, wrapped
in src/lib/api.ts. All payloads use camelCase. On failure a command rejects
with an ApiError:
interface ApiError { code: string; message: string }code is one of: CONNECTION, AUTH, SESSION_NOT_FOUND, TRANSFER_NOT_FOUND,
REMOTE, IO, DB, KEYCHAIN, INVALID.
Opens a connection and registers a session.
interface ConnectionRequest {
protocol: "sftp" | "ftp" | "ftps";
host: string; port: number; username: string;
password?: string; // password auth
privateKey?: string; // SFTP key auth (PEM contents)
passphrase?: string; // key passphrase
}
interface Session { id: string; protocol: Protocol; host: string; username: string; cwd: string }The request also accepts optional ftpEncryption, ftpMode, acceptInvalidCert,
timeoutSecs, retries, retryDelaySecs, a proxy object
({ type: "socks4"|"socks5"|"http", host, port, username?, password? }),
minTlsVersion ("1.0".."1.3", FTPS floor; default "1.2"),
sftpCompression (request zlib compression on SFTP), useAgent
(try the SSH agent / SSH_AUTH_SOCK before password/key for SFTP),
ftpKeepAlive (send periodic NOOP to keep an idle FTP control connection
alive), preallocate (reserve the file size before an SFTP download), and
maxConcurrent (max simultaneous transfer connections for the session; default 1).
Security: SFTP verifies the server host key before sending credentials
(trust-on-first-use). A first connection remembers the key; a later connection
whose key differs fails with HOST_KEY_MISMATCH. FTPS verifies the TLS cert and
fails with CERT_UNTRUSTED unless the user has trusted it. Errors: CONNECTION,
AUTH, CERT_UNTRUSTED, HOST_KEY_MISMATCH, REMOTE.
Forget a remembered SSH host key, so the next connect re-trusts the current key
(used to accept a legitimately-rotated server key after a HOST_KEY_MISMATCH).
Closes and drops a session. No error if already gone.
Lists a remote directory. Errors: SESSION_NOT_FOUND, REMOTE.
Lists a local directory. Errors: IO.
Returns the user's home directory. Errors: INVALID.
Open a local path with the OS default app, or reveal it in the file manager.
Open a local path with a specific application (the "Open With…" flow). On macOS
app is an application name or .app path; elsewhere it is an executable path.
Download a remote file to a temp dir and return the local path, so the frontend
can openPath it ("Open with…" for remote files). Errors: SESSION_NOT_FOUND, REMOTE.
Read a small remote text file (config/version/log) as UTF-8, capped at maxBytes
(≤ 1 MiB). Used by the assistant to inspect files. Errors: SESSION_NOT_FOUND, REMOTE.
Download a remote file to a temp dir, open it (with editor or the OS default
app), and watch it: each time the local copy is saved, the backend emits
editor://changed with { sessionId, remotePath, localPath }. The frontend then
confirms with the user (unless disabled) and queues the upload via enqueueUpload.
If the file is already being edited, fresh = true discards the local copy and
re-downloads; any other value reopens the existing local copy. The watcher stops
when the session closes. Returns the temp path. Errors: SESSION_NOT_FOUND, REMOTE.
Whether a remote file already has a local copy open for editing (an active watcher). Used to offer "reopen local" vs "discard and re-download".
type EntryKind = "file" | "directory" | "symlink";
interface DirEntry {
name: string; path: string; kind: EntryKind; size: number;
modified?: string; permissions?: string; owner?: string;
}Queue a transfer. If the source is a directory the tree is expanded into one
Transfer per file (creating destination directories as needed), so the return
value is the array of queued transfers. For enqueueDownload, pass
isDirectory: true when the remote path is a folder. The optional filter
({ chars, replacement }) replaces illegal characters in server-supplied
filenames; path separators and control characters are always replaced.
Transfers run through a per-session FIFO scheduler backed by a connection
pool: a session runs up to maxConcurrent transfers at once (default 1 ⇒
sequential), each over its own pooled connection, while a separate interactive
connection handles browsing. Different sessions always run in parallel. A
per-site connectionLimit caps the pool size. Errors: SESSION_NOT_FOUND.
Set global transfer speed caps in KiB/s (0 = unlimited). burstSecs (0-30,
default 0) grants that many seconds' worth of allowance above the cap as a
short burst. momentarySpeed reports instantaneous instead of average speed.
Applies to transfers started after the change.
Hold (or release) an OS "stay awake" assertion while transfers run. macOS uses
caffeinate, Linux systemd-inhibit; other platforms are a no-op. Never errors.
Control a running or queued transfer. Cancelling a still-queued transfer
finalizes it as cancelled without ever starting it. Errors: TRANSFER_NOT_FOUND.
Snapshot of all transfers.
Cancelling a running transfer aborts it at the next data chunk (the progress
callback signals the protocol loop to stop) and finalizes it as cancelled,
leaving any partial file in place.
type TransferStatus = "queued"|"transferring"|"paused"|"completed"|"failed"|"cancelled";
interface Transfer {
id: string; direction: "upload"|"download"; name: string;
localPath: string; remotePath: string; status: TransferStatus;
bytesTransferred: number; totalBytes: number; speed: number;
etaSeconds: number | null; error?: string; scope?: string;
}Logs and finished transfers are persisted to a SQLite history DB
(turbofiles-history.sqlite, sibling of the site store) so they survive restarts.
The frontend hydrates a recent window (latest 2000) at startup; the database
retains everything.
interface LogEntry { timestamp: string; level: "info"|"warn"|"error"|"debug"; message: string; scope?: string; }interface Site {
id: string; name: string; protocol: Protocol; host: string; port: number;
username: string; defaultRemotePath?: string; hasStoredSecret: boolean;
createdAt: string; updatedAt: string;
}The optional natural-language assistant uses a Bring-Your-Own-Key model. The API
key is stored in the OS keychain and never returned to the web layer; llmProxy
injects it server-side, so the CSP stays locked to 'self'.
Forward the provider-native chat-completion request body to url, injecting the
stored key as the correct auth header (anthropic → x-api-key +
anthropic-version; anything else → Authorization: Bearer). The key is optional
for OpenAI-compatible servers (local Ollama / LM Studio need none). Returns the raw
JSON response. Errors: INVALID (Anthropic with no key), CONNECTION, REMOTE.
List model ids available from the provider - Anthropic's /v1/models, or any
OpenAI-compatible server's /models (blank baseUrl → OpenAI; works with local
Ollama / LM Studio). Used to populate the model picker.
Fetch the latest published GitHub release for repo ("owner/name"), via Rust
reqwest so the renderer's CSP stays locked to 'self'. Returns { version, tag, url, notes }, or null when no release is published yet. The UI compares version
to the running APP_VERSION to decide whether to surface "Update available".
Emitted continuously by the transfer worker (throttled to ~150ms) and on completion/failure.
interface TransferProgressEvent {
id: string; bytesTransferred: number; totalBytes: number;
speed: number; etaSeconds: number | null;
status: TransferStatus; error?: string;
}Subscribe via onTransferProgress(handler) which returns an unlisten function.