Host API Reference
June 2, 2026 ยท View on GitHub
This document describes the host APIs available to plugins via the ctx object passed to probe(ctx).
Context Object
type ProbeContext = {
nowIso: string // Current UTC time (ISO 8601)
app: {
version: string // App version
platform: string // OS platform (e.g., "macos")
appDataDir: string // App data directory
pluginDataDir: string // Plugin-specific data dir (auto-created)
}
host: HostApi
}
ctx.nowIso
Current UTC timestamp in ISO 8601 format (e.g., 2026-01-15T12:30:00.000Z).
ctx.app
Application metadata:
| Property | Description |
|---|---|
version | App version string |
platform | OS platform (e.g., "macos", "windows", "linux") |
appDataDir | App's data directory path |
pluginDataDir | Plugin-specific data directory (auto-created on demand) |
The pluginDataDir is unique per plugin ({appDataDir}/plugins_data/{pluginId}/) and is automatically created when the plugin runs. Use it to store config files, cached data, or state.
Logging
host.log.info(message: string): void
host.log.warn(message: string): void
host.log.error(message: string): void
Logs are prefixed with [plugin:<id>] and written to the app's log output.
Example:
ctx.host.log.info("Fetching usage data...")
ctx.host.log.warn("Token expires soon")
ctx.host.log.error("API request failed: " + error.message)
Filesystem
host.fs.exists(path: string): boolean
host.fs.readText(path: string): string // Throws on error
host.fs.writeText(path: string, content: string): void // Throws on error
host.fs.listDir(path: string): string[] // Throws if directory cannot be opened; per-entry errors are silently skipped
Path Expansion
~expands to the user's home directory~/fooexpands to$HOME/foo
Error Handling
Both readText and writeText throw on errors. Always wrap in try/catch:
try {
const content = ctx.host.fs.readText("~/.config/myapp/settings.json")
const settings = JSON.parse(content)
} catch (e) {
ctx.host.log.error("Failed to read settings: " + String(e))
throw "Failed to read settings. Check your config."
}
Directory Listing
Use listDir to inspect immediate child names under a directory:
let entries = []
try {
entries = ctx.host.fs.listDir("~/Library/Application Support/JetBrains")
} catch {
entries = []
}
Example: Persisting plugin state
const statePath = ctx.app.pluginDataDir + "/state.json"
// Read state
let state = { counter: 0 }
if (ctx.host.fs.exists(statePath)) {
try {
state = JSON.parse(ctx.host.fs.readText(statePath))
} catch {
// Use default state
}
}
// Update and save state
state.counter++
ctx.host.fs.writeText(statePath, JSON.stringify(state, null, 2))
Crypto
host.crypto.decryptAes256Gcm(envelope: string, keyB64: string): string // Throws on error
host.crypto.encryptAes256Gcm(plaintext: string, keyB64: string): string // Throws on error
AES-256-GCM helpers for plugins that need to read or write locally encrypted auth/config blobs.
Envelope Format
iv_b64:tag_b64:ciphertext_b64keyB64must be a base64-encoded 32-byte AES-256 key- Both helpers operate on UTF-8 plaintext strings
Example
const keyB64 = ctx.host.fs.readText("~/.factory/auth.v2.key").trim()
const encrypted = ctx.host.fs.readText("~/.factory/auth.v2.file")
const rawJson = ctx.host.crypto.decryptAes256Gcm(encrypted, keyB64)
const auth = JSON.parse(rawJson)
auth.access_token = "new-token"
const nextEnvelope = ctx.host.crypto.encryptAes256Gcm(JSON.stringify(auth, null, 2), keyB64)
ctx.host.fs.writeText("~/.factory/auth.v2.file", nextEnvelope)
Environment
host.env.get(name: string): string | null
Reads an environment variable by name.
Behavior
- Returns variable value as string when set
- Returns
nullwhen missing - Variable must be whitelisted first in
src-tauri/src/plugin_engine/host_api.rs - Resolution order: current process env first, then a login+interactive shell lookup (macOS)
- Values may be cached for the app session; restart OpenUsage after changing shell config
Example
const codexHome = ctx.host.env.get("CODEX_HOME")
const authPath = codexHome
? codexHome.replace(/\/+$/, "") + "/auth.json"
: "~/.config/codex/auth.json"
HTTP
host.http.request({
method?: string, // Default: "GET"
url: string,
headers?: Record<string, string>,
bodyText?: string,
timeoutMs?: number // Default: 10000
}): {
status: number,
headers: Record<string, string>,
bodyText: string
}
Behavior
- No redirects: The HTTP client does not follow redirects (policy: none)
- Throws on network errors: Connection failures, DNS errors, and timeouts throw
- No domain allowlist: Any URL is allowed (for now)
Example: GET request
let resp
try {
resp = ctx.host.http.request({
method: "GET",
url: "https://api.example.com/usage",
headers: {
"Authorization": "Bearer " + token,
"Accept": "application/json",
},
timeoutMs: 5000,
})
} catch (e) {
throw "Network error. Check your connection."
}
if (resp.status !== 200) {
throw "Request failed (HTTP " + resp.status + "). Try again later."
}
const data = JSON.parse(resp.bodyText)
Example: POST request with JSON body
const resp = ctx.host.http.request({
method: "POST",
url: "https://api.example.com/refresh",
headers: {
"Content-Type": "application/json",
},
bodyText: JSON.stringify({ refresh_token: token }),
timeoutMs: 10000,
})
Keychain (macOS only)
host.keychain.readGenericPassword(service: string, account?: string): string
Reads a generic password from the macOS Keychain. Pass account when the service stores multiple accounts and the plugin must avoid a service-wide match.
Behavior
- macOS only: Throws on other platforms
- Throws if not found: Returns the password string if found, throws otherwise
- Optional account scope: When
accountis set, lookup uses both service and account
Example
let credentials = null
// Try file first, fall back to keychain
if (ctx.host.fs.exists("~/.myapp/credentials.json")) {
credentials = JSON.parse(ctx.host.fs.readText("~/.myapp/credentials.json"))
} else {
try {
const keychainValue = ctx.host.keychain.readGenericPassword("MyApp-credentials")
credentials = JSON.parse(keychainValue)
} catch {
throw "Login required. Sign in to continue."
}
}
SQLite
Query (Read-Only)
host.sqlite.query(dbPath: string, sql: string): string
Executes a read-only SQL query against a SQLite database.
Behavior:
- Read-only: Database is opened with
-readonlyflag - Returns JSON string: Result is a JSON array of row objects (must
JSON.parse()) - Dot-commands blocked: Commands like
.schema,.tablesare rejected - Throws on errors: Invalid SQL, missing database, etc.
Example:
const dbPath = "~/Library/Application Support/MyApp/state.db"
const sql = "SELECT key, value FROM settings WHERE key = 'token'"
let rows
try {
const json = ctx.host.sqlite.query(dbPath, sql)
rows = JSON.parse(json)
} catch (e) {
ctx.host.log.error("SQLite query failed: " + String(e))
throw "DB error. Check your data source."
}
if (rows.length === 0) {
throw "Not configured. Update your settings."
}
const token = rows[0].value
Exec (Read-Write)
host.sqlite.exec(dbPath: string, sql: string): void
Executes a write SQL statement against a SQLite database.
Behavior:
- Read-write: Database is opened with full write access
- Returns nothing: Use for INSERT, UPDATE, DELETE, or other write operations
- Dot-commands blocked: Commands like
.schema,.tablesare rejected - Throws on errors: Invalid SQL, missing database, permission denied, etc.
Example:
const dbPath = "~/Library/Application Support/MyApp/state.db"
// Escape single quotes in value for SQL safety
const escaped = newToken.replace(/'/g, "''")
const sql = "INSERT OR REPLACE INTO settings (key, value) VALUES ('token', '" + escaped + "')"
try {
ctx.host.sqlite.exec(dbPath, sql)
} catch (e) {
ctx.host.log.error("SQLite write failed: " + String(e))
throw "Failed to save token."
}
Warning: Be careful with SQL injection. Always escape user-provided values.
Execution Timing
probe(ctx) is called when:
- The app loads
- The user clicks Refresh (per-provider retry button)
- The auto-update timer fires (configurable: 5/15/30/60 minutes)
Any token refresh logic (e.g., OAuth refresh) must run inside probe(ctx) at those times.
Line Builders
Helper functions for creating output lines. All builders use an options object pattern.
ctx.line.text(opts)
Creates a text line (label/value pair).
ctx.line.text({
label: string, // Required: label shown on the left
value: string, // Required: value shown on the right
color?: string, // Optional: hex color for value text
subtitle?: string // Optional: smaller text below the line
}): MetricLine
Example:
ctx.line.text({ label: "Account", value: "user@example.com" })
ctx.line.text({ label: "Status", value: "Active", color: "#22c55e", subtitle: "Since Jan 2024" })
ctx.line.progress(opts)
Creates a progress bar line.
ctx.line.progress({
label: string, // Required: label shown on the left
used: number, // Required: amount used (>= 0)
limit: number, // Required: limit (> 0)
format: { // Required: formatting rules
kind: "percent" | "dollars" | "count",
suffix?: string // Required when kind="count" (e.g. "credits")
},
resetsAt?: string | null, // Optional: ISO timestamp for when usage resets
periodDurationMs?: number, // Optional: period length in ms for pace tracking
color?: string, // Optional: hex color for progress bar
}): MetricLine
Notes:
usedmay exceedlimit(overages).- For
format.kind: "percent",limitmust be100. - Prefer setting
resetsAt(viactx.util.toIso(...)) instead of putting reset info in other lines. periodDurationMs: when provided withresetsAt, enables pace visuals (Dot Pacing status + in-bar pace marker) and projected-rate messaging.
Example:
ctx.line.progress({ label: "Usage", used: 42, limit: 100, format: { kind: "percent" } })
ctx.line.progress({ label: "Spend", used: 12.34, limit: 100, format: { kind: "dollars" } })
ctx.line.progress({
label: "Session",
used: 75,
limit: 100,
format: { kind: "percent" },
resetsAt: ctx.util.toIso("2026-02-01T00:00:00Z"),
})
ctx.line.badge(opts)
Creates a badge line (status indicator).
ctx.line.badge({
label: string, // Required: label shown on the left
text: string, // Required: badge text
color?: string, // Optional: hex color for badge border/text
subtitle?: string // Optional: smaller text below the line
}): MetricLine
Example:
ctx.line.badge({ label: "Plan", text: "Pro", color: "#000000" })
ctx.line.badge({ label: "Status", text: "Connected", color: "#22c55e" })
ctx.line.barChart(opts)
Creates a compact vertical bar chart line for usage history.
ctx.line.barChart({
label: string, // Required: chart label
points: Array<{
label: string, // Required: point label, e.g. day
value: number, // Required: non-negative chart value
valueLabel?: string // Optional: displayed value, e.g. "1.2M tokens"
}>,
note?: string, // Optional: small muted note below chart
color?: string, // Optional: hex color for bars
}): MetricLine
Example:
ctx.line.barChart({
label: "Usage Trend",
points: [
{ label: "2/1", value: 1200, valueLabel: "1.2K tokens" },
{ label: "2/2", value: 2400, valueLabel: "2.4K tokens" },
],
note: "Estimated from local logs",
})
Formatters
Helper functions for formatting values.
ctx.fmt.planLabel(value)
Capitalizes a plan name string.
ctx.fmt.planLabel("pro") // "Pro"
ctx.fmt.planLabel("team_plan") // "Team_plan"
ctx.fmt.resetIn(seconds)
Formats seconds until reset as human-readable duration.
ctx.fmt.resetIn(180000) // "2d 2h"
ctx.fmt.resetIn(7200) // "2h 0m"
ctx.fmt.resetIn(300) // "5m"
ctx.fmt.resetIn(30) // "<1m"
ctx.fmt.dollars(cents)
Converts cents to dollars.
ctx.fmt.dollars(1234) // 12.34
ctx.fmt.dollars(500) // 5
ctx.fmt.date(unixMs)
Formats Unix milliseconds as short date.
ctx.fmt.date(1704067200000) // "Jan 1"
Utilities
ctx.util.toIso(value)
Normalizes a timestamp into an ISO string (or returns null if the input can't be parsed).
Accepts common inputs like:
- ISO strings (with or without timezone; timezone-less is treated as UTC)
- Unix seconds / milliseconds (number or numeric string)
Dateobjects
Example:
ctx.line.progress({
label: "Weekly",
used: 24,
limit: 100,
format: { kind: "percent" },
resetsAt: ctx.util.toIso(data.resets_at),
})
ccusage (Token Usage)
host.ccusage.query(opts: {
provider?: "claude" | "codex", // Optional; defaults to plugin id, then "claude"
since?: string, // Start date (YYYYMMDD or YYYY-MM-DD)
until?: string, // End date (YYYYMMDD or YYYY-MM-DD)
homePath?: string, // Provider home override (CLAUDE_CONFIG_DIR or CODEX_HOME)
claudePath?: string, // Legacy Claude-only override (deprecated; use homePath)
}):
| { status: "ok", data: { daily: DailyUsage[] } }
| { status: "no_runner" }
| { status: "runner_failed" }
Queries local token usage via provider-focused ccusage commands:
- Claude:
ccusage claude daily - Codex:
ccusage codex daily
Returns a status envelope:
ok: query succeeded, usage data is indata.dailyno_runner: no package runner (bunx/pnpm/yarn/npm/npx) was foundrunner_failed: at least one runner was available but all attempts failed
Behavior
- Runtime runners: Executes pinned
ccusage@20.0.2via fallback chainbunx -> pnpm dlx -> yarn dlx -> npm exec -> npx - Provider-aware: Resolves provider from
opts.provideror plugin id (claude/codex) - Focused commands: Uses
ccusage claude dailyorccusage codex daily; it intentionally does not useccusage dailybecause that aggregates all detected agents - Legacy fallback: If
ccusage@20.0.2cannot run through the package manager release-age policy, retries with release-age-safeccusage@18.0.11for Claude or@ccusage/codex@18.0.11for Codex - No provider API calls: Usage is computed from local JSONL session files; the host does not call Claude/Codex (or other provider) APIs, but package runners may contact a package registry to download the
ccusageCLI if it is not already available locally - Graceful degradation: returns
no_runnerwhen no runner exists,runner_failedwhen execution fails - Pricing: Uses ccusage's built-in LiteLLM pricing data
DailyUsage
The host normalizes only the top-level shape to { daily: [...] }. Inner day fields come from the selected CLI and may differ by provider/version.
Commonly observed fields include:
| Property | Type | Notes |
|---|---|---|
date | string | Date label from CLI output (provider/locale-dependent) |
inputTokens | number | Present in Claude and Codex |
outputTokens | number | Present in Claude and Codex |
cacheCreationTokens | number | Claude field |
cacheReadTokens | number | Claude field |
cachedInputTokens | number | Codex field |
totalTokens | number | Present in current Claude/Codex outputs |
totalCost | number | null | Claude cost field |
costUSD | number | Codex cost field |
Example
var result = ctx.host.ccusage.query({ provider: "codex", since: "20260101" })
if (result.status === "ok") {
for (var i = 0; i < result.data.daily.length; i++) {
var day = result.data.daily[i]
var cost = day.totalCost != null ? day.totalCost : day.costUSD
ctx.host.log.info(day.date + ": " + (day.totalTokens || 0) + " tokens, $" + (cost != null ? cost : "n/a"))
}
} else if (result.status === "no_runner") {
ctx.host.log.warn("ccusage unavailable: no package runner found")
}
See Also
- Plugin Schema - Plugin structure, manifest format, and output schema