06

API Reference

Complete reference for the CoreApi UniFFI surface. Documents all public functions, data types, event channels, and the error model exposed to Kotlin and Swift host applications.

FFI Boundary

The entire Evern API is exposed through a single CoreApi struct with thread-safe internal state (Arc<Mutex<CoreState>>). UniFFI auto-generates idiomatic Kotlin and Swift bindings — platform code calls native methods, not raw FFI.

All public structs use Serialize/Deserialize for JSON marshaling across the FFI boundary. The overhead is negligible — measured and documented during Phase 0.

Session Lifecycle

A typical Evern session follows a fixed call sequence: initialize the profile store, load saved profiles, connect to a server, exchange terminal I/O, and disconnect. The following Kotlin example shows this end-to-end flow.

session lifecycle walkthrough (Kotlin)
// 1. Create the core API instance
val core = CoreApi()
 
// 2. Initialize the profile store (once per app launch)
core.initializeProfileStore(appDataDir)
 
// 3. Load saved server profiles
val profiles = core.listServerProfiles()
val server = profiles.first()
 
// 4. Connect to the server
val connected = core.connect(
profileId = server.id,
host = server.host,
port = server.port,
username = server.username,
authType = "password",
password = userPassword,
privateKeyBase64 = ""
)
 
// 5. Send input to the remote shell
core.sendInput("ls -la")
 
// 6. Drain events on a background thread
val termEvents = core.drainTerminalEvents()
val outputEvents = core.drainOutputEvents()
 
// 7. Disconnect when done
core.disconnect()

Steps 5 and 6 repeat for the duration of the session. The drain functions are non-blocking — they return whatever events have accumulated since the last call. See Common Patterns below for polling loop and error handling examples.

Session Management

session lifecycle
fn new() -> CoreApi
 
fn connect(&self,
profile_id: i64, host: String,
port: u16, username: String,
auth_type: String,
password: String,
private_key_base64: String
) -> bool
 
fn disconnect(&self) -> bool
fn pause_session(&self) -> bool
fn resume_session(&self) -> bool
Function Returns Description
new() CoreApi Create a new API instance with default state
connect() bool Initiate SSH/Mosh connection to a server
disconnect() bool Close the active session
pause_session() bool Pause session I/O (backgrounding)
resume_session() bool Resume a paused session
connection_state() String Returns CONNECTED, PAUSED, RECONNECTING, or DISCONNECTED
connection_type() String Returns active transport type (SSH, MOSH)
is_reconnecting() bool Whether reconnection is in progress
reconnect_attempt_count() u32 Current reconnection attempt number
notify_network_reachability() void Platform callback when network state changes

connect() — auth_type values

The auth_type parameter accepts three values:

  • "password" — authenticate using password. The private_key_base64 field is ignored.
  • "private_key" — authenticate using the PEM key in private_key_base64. If the key is passphrase-protected, provide the passphrase in password.
  • "keyboard_interactive" — use challenge-response authentication. The password field is sent as the response to the first challenge prompt.

When both password and private_key_base64 are provided with auth_type set to "private_key", key-based authentication takes precedence. Password is only used as the key passphrase in that case.

The connection timeout is 20 seconds (CONNECT_TIMEOUT). If the server does not respond within that window, the call returns false and a Timeout error is available via last_connection_error_kind().

Terminal I/O

terminal I/O
fn send_input(&self, input: String) -> String
fn send_raw_input(&self, input: String) -> String
 
fn drain_terminal_events(&self) -> Vec<TerminalEvent>
fn drain_output_events(&self) -> Vec<OutputEventRecord>
 
fn get_terminal_grid_text(&self,
max_rows: u32, max_cols: u32
) -> String
 
fn get_terminal_grid_spans_json(&self,
max_rows: u32, max_cols: u32
) -> String
 
fn set_scrollback_limit(&self, lines: u32) -> bool
 
fn session_activity_summary(&self) -> SessionActivitySummary
fn export_session_markdown(&self) -> String
Function Description
send_input() Write text to terminal (chat mode, appends newline)
send_raw_input() Write raw bytes to terminal (raw mode, no newline)
drain_terminal_events() Poll raw terminal events (sequence, kind, payload)
drain_output_events() Poll parsed AI output events with accessibility labels
get_terminal_grid_text() Read the current terminal character grid as plain text
get_terminal_grid_spans_json() Read the grid with styled runs (color, bold, italic, underline)
set_scrollback_limit() Configure scrollback buffer size (100 - 100,000 lines)
session_activity_summary() Get files changed, prompts sent, commands run
export_session_markdown() Export session as markdown (max 200,000 chars)

Polling guidance for drain functions

Both drain_terminal_events() and drain_output_events() are non-blocking. They return all events that have accumulated since the previous call, then clear the internal buffer. If no events are pending, they return an empty list.

Recommended polling intervals: Call drain_terminal_events() every ~100ms for responsive terminal rendering. Call drain_output_events() every ~200ms, since parsed output events arrive less frequently than raw terminal data.

Threading: Both drain functions must be called from a background thread, not the main/UI thread. The underlying mutex lock is fast (sub-millisecond) but should not block UI rendering. On Android, use a coroutine on Dispatchers.IO; on iOS, use a DispatchQueue or Swift concurrency task.

Profiles

server profiles
fn initialize_profile_store(&self,
app_data_dir: String)
 
fn list_server_profiles(&self
) -> Vec<ServerProfile>
 
fn upsert_server_profile(&self,
id: i64, label: String,
host: String, port: u16,
username: String,
auth_type: String,
group_name: String
) -> i64
 
fn delete_server_profile(&self,
id: i64) -> bool
Function Description
initialize_profile_store() Initialize SQLite database at the given app data directory
list_server_profiles() Return all saved server profiles
upsert_server_profile() Create or update a server profile (id=0 creates new)
delete_server_profile() Delete a server profile by ID

Voice

voice / STT
// Engine configuration
fn set_preferred_stt_engine(&self, engine: String) -> bool
fn preferred_stt_engine(&self) -> String
fn active_stt_engine(&self) -> String
fn initialize_stt_engine(&self, model_dir: String) -> bool
 
// Streaming STT
fn start_stt_stream(&self,
sample_rate_hz: u32, channels: u8) -> bool
fn push_stt_audio_chunk(&self,
samples: Vec<f32>) -> String
fn finish_stt_stream(&self) -> String
fn cancel_stt_stream(&self) -> bool
 
// Preprocessing
fn preprocess_voice_input(&self,
input: String) -> String
fn preprocess_voice_input_with_context(&self,
input: String,
recent_commands: Vec<String>,
server_names: Vec<String>,
personal_dictionary: Vec<VoiceCorrectionPair>
) -> String
 
// Personal dictionary
fn record_voice_correction(&self,
raw_phrase: String,
corrected_phrase: String) -> String
fn list_active_voice_corrections(&self
) -> Vec<VoiceCorrectionEntry>
fn remove_voice_correction(&self,
raw_phrase: String,
corrected_phrase: String) -> bool
fn clear_voice_corrections(&self) -> bool

Intelligence

Function Returns Description
drain_output_events() Vec<OutputEventRecord> Poll parsed AI output events (code blocks, diffs, tool use, thinking blocks)
session_activity_summary() SessionActivitySummary Aggregate stats: files changed, prompts sent, commands run
export_session_markdown() String Export full session as markdown
list_developer_dictionary_terms() Vec<String> List all ~500 developer dictionary entries

Snippets

snippets & history
fn upsert_snippet(&self,
id: i64, title: String, body: String,
variables: Vec<String>,
tags: Vec<String>,
folder: String) -> i64
 
fn list_snippets(&self, folder: String
) -> Vec<SnippetEntry>
fn list_snippet_folders(&self) -> Vec<String>
fn delete_snippet(&self, id: i64) -> bool
fn rename_snippet_folder(&self,
old_name: String, new_name: String) -> i64
fn delete_snippet_folder(&self,
folder: String) -> i64
 
// History
fn append_history_entry(&self,
history_kind: String, content: String) -> bool
fn recent_history(&self,
history_kind: String, limit: u32
) -> Vec<String>
fn search_history(&self,
history_kind: String,
query: String, limit: u32
) -> Vec<String>

Security & Errors

The API uses a stateful error model with three independent error channels. Errors persist until cleared — clients can query the current error state at any time.

Connection Errors

Connection and authentication failures. Kinds: InvalidInput, MissingCredentials, HostKeyMismatch, AuthFailed, Timeout, TransportError.

Session I/O Errors

Runtime transport failures. Kinds: InvalidInput, NotConnected, Paused, TransportError.

Data Errors

Database and persistence failures. Kinds: InvalidInput, StoreUnavailable, PersistenceError.

error introspection
// Connection errors
fn last_connection_error(&self
) -> Option<ConnectionError>
fn last_connection_error_kind(&self
) -> String
fn last_connection_error_message(&self
) -> String
 
// Session I/O errors
fn last_session_io_error(&self
) -> Option<SessionIoError>
fn last_session_io_error_kind(&self
) -> String
 
// Data errors
fn last_data_error(&self
) -> Option<DataError>
fn last_data_error_kind(&self
) -> String

Host Key & Multiplexer

Function Description
override_host_key(host, port) Accept a changed host key after user confirmation
list_multiplexer_sessions() List tmux/screen sessions on the remote host
attach_multiplexer_session(entry) Reattach to a tmux/screen session

Handling HostKeyMismatch

When connect() returns false and last_connection_error_kind() returns "HostKeyMismatch", the server's host key has changed since the last connection. This can indicate a legitimate server reinstallation or a man-in-the-middle attack.

The expected handling flow is:

  1. Read the error message from last_connection_error_message() — it contains the old and new key fingerprints.
  2. Present a confirmation dialog to the user explaining the key change and showing both fingerprints.
  3. If the user accepts, call override_host_key(host, port) to persist the new key.
  4. Retry the connect() call with the same parameters. The connection will now succeed (assuming credentials are valid).

Do not call override_host_key() automatically without user confirmation. The whole point of host key verification is to prevent silent MITM attacks.

Data Types

Type Fields
ServerProfile id: i64, label, host, port: u16, username, auth_type, group_name
TerminalEvent sequence: u64, kind, payload
OutputEventRecord sequence: u64, kind, payload, accessibility_label
SessionActivitySummary files_changed: Vec<SessionFileChange>, prompts_sent: u32, commands_run: u32
SessionFileChange path, operation, occurrences: u32
TerminalGridStyleRun text, fg_hex, bg_hex, bold, italic, underline
TerminalGridStyleLine runs: Vec<TerminalGridStyleRun>
SnippetEntry id: i64, title, body, variables, tags, folder
VoiceCorrectionPair raw_phrase, corrected_phrase
VoiceCorrectionEntry raw_phrase, corrected_phrase, occurrences: u32, status
ConnectionError kind, message
SessionIoError kind, message
DataError kind, message

Constants

Constant Value
DEFAULT_SCROLLBACK_LINES 10,000
MAX_SCROLLBACK_LINES 100,000
MIN_SCROLLBACK_LINES 100
MAX_OUTPUT_EVENT_HISTORY 4,000
MAX_MARKDOWN_EXPORT_CHARS 200,000
MAX_SNIPPET_BODY_CHARS 12,000
MAX_SNIPPET_VARIABLES 30
CONNECT_TIMEOUT 20 seconds
DEFAULT_TERMINAL_COLUMNS 160
DEFAULT_TERMINAL_ROWS 48

Common Patterns

The following Kotlin examples show integration patterns that recur in host applications. Each example is self-contained and can be adapted for Swift with minor syntax changes.

Connection error handling with HostKeyMismatch recovery

This pattern covers the full connection attempt, including the HostKeyMismatch recovery flow, authentication failures, and timeouts.

error handling flow (Kotlin)
suspend fun connectWithErrorHandling(
core: CoreApi,
profile: ServerProfile,
password: String
): Boolean {
val ok = core.connect(
profileId = profile.id,
host = profile.host,
port = profile.port,
username = profile.username,
authType = profile.authType,
password = password,
privateKeyBase64 = ""
)
if (ok) return true
 
when (core.lastConnectionErrorKind()) {
"HostKeyMismatch" -> {
val msg = core.lastConnectionErrorMessage()
// Show dialog with old + new fingerprints
val accepted = showHostKeyDialog(msg)
if (accepted) {
core.overrideHostKey(
profile.host, profile.port
)
// Retry the connection
return core.connect(
profileId = profile.id,
host = profile.host,
port = profile.port,
username = profile.username,
authType = profile.authType,
password = password,
privateKeyBase64 = ""
)
}
}
"AuthFailed" ->
showError("Authentication failed")
"Timeout" ->
showError("Connection timed out (20s)")
else ->
showError(core.lastConnectionErrorMessage())
}
return false
}

Background polling loop for terminal events

Terminal and output events must be drained on a background thread. This coroutine-based loop polls at the recommended intervals and dispatches results to the UI layer.

polling loop (Kotlin)
fun CoroutineScope.startEventPolling(
core: CoreApi,
onTerminalEvents: (List<TerminalEvent>) -> Unit,
onOutputEvents: (List<OutputEventRecord>) -> Unit
) {
// Terminal events: ~100ms for responsive rendering
launch(Dispatchers.IO) {
while (isActive) {
val events = core.drainTerminalEvents()
if (events.isNotEmpty()) {
withContext(Dispatchers.Main) {
onTerminalEvents(events)
}
}
delay(100)
}
}
 
// Output events: ~200ms (less frequent)
launch(Dispatchers.IO) {
while (isActive) {
val events = core.drainOutputEvents()
if (events.isNotEmpty()) {
withContext(Dispatchers.Main) {
onOutputEvents(events)
}
}
delay(200)
}
}
}

Voice input preprocessing with context

Voice-to-command preprocessing uses the personal dictionary and recent command history to correct STT output before sending it to the terminal. The context-aware variant produces significantly better results for technical terms.

voice input preprocessing (Kotlin)
suspend fun handleVoiceInput(
core: CoreApi,
rawTranscription: String
) {
// Gather context for better preprocessing
val recentCmds = core.recentHistory(
historyKind = "command",
limit = 20u
)
val servers = core.listServerProfiles()
.map { it.label }
val dictionary = core.listActiveVoiceCorrections()
.map { VoiceCorrectionPair(it.rawPhrase, it.correctedPhrase) }
 
// Preprocess with full context
val command = core.preprocessVoiceInputWithContext(
input = rawTranscription,
recentCommands = recentCmds,
serverNames = servers,
personalDictionary = dictionary
)
 
// Send the corrected command
core.sendInput(command)
 
// Record correction if user edits afterward
if (command != rawTranscription) {
core.recordVoiceCorrection(
rawPhrase = rawTranscription,
correctedPhrase = command
)
}
}