This document describes the host APIs available to plugins via the ctx object passed to probe(ctx).
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
}Current UTC timestamp in ISO 8601 format (e.g., 2026-01-15T12:30:00.000Z).
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.
host.log.info(message: string): void
host.log.warn(message: string): void
host.log.error(message: string): voidLogs 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)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~expands to the user's home directory~/fooexpands to$HOME/foo
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."
}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))host.env.get(name: string): string | nullReads an environment variable by name.
- 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
const codexHome = ctx.host.env.get("CODEX_HOME")
const authPath = codexHome
? codexHome.replace(/\/+$/, "") + "/auth.json"
: "~/.config/codex/auth.json"host.crypto.decryptAes256Gcm(envelope: string, keyB64: string): string
host.crypto.encryptAes256Gcm(plaintext: string, keyB64: string): stringAES-256-GCM helpers for plugins that need to read or write encrypted local auth stores.
keyB64must be a base64-encoded 32-byte key.encryptAes256Gcm(...)returns a JSON envelope string withnonceandciphertextfields.decryptAes256Gcm(...)expects that same envelope shape and returns the decrypted UTF-8 plaintext.- Both methods throw on malformed base64, malformed envelopes, or failed decryption.
const keyB64 = ctx.host.fs.readText("~/.factory/auth.v2.key").trim()
const envelope = ctx.host.fs.readText("~/.factory/auth.v2.file")
const plaintext = ctx.host.crypto.decryptAes256Gcm(envelope, keyB64)
const auth = JSON.parse(plaintext)
auth.access_token = refreshedAccessToken
const nextEnvelope = ctx.host.crypto.encryptAes256Gcm(JSON.stringify(auth, null, 2), keyB64)
ctx.host.fs.writeText("~/.factory/auth.v2.file", nextEnvelope)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
}- No redirects: The HTTP client does not follow redirects (policy: none)
- Throws on network errors: Connection failures, DNS errors, and timeouts throw
- Capability-gated domains: HTTP requests are limited to exact hosts or
*.example.comwildcard hosts fromcapabilities.httpDomains. Empty or omitted domains block all plugin HTTP requests.
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)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,
})host.keychain.readGenericPassword(service: string): string
host.keychain.readGenericPasswordForAccount(service: string, account: string): string
host.keychain.readGenericPasswordForTarget(target: string): string
host.keychain.writeGenericPassword(service: string, value: string): void
host.keychain.deleteGenericPassword(service: string): voidReads and writes credentials through the platform credential store.
readGenericPassword(service)uses the OpenUsage-scoped credential namespace.readGenericPasswordForAccount(service, account)reads a native service/account pair directly. Use it only when a provider must integrate with an external tool's credential entry, such as GitHub CLI.readGenericPasswordForTarget(target)reads a native Windows generic credential by its raw target name. Use it only when a Windows app stores credentials under a fixed target instead of an app-owned OpenUsage namespace.- All keychain methods throw if the credential store is unavailable or the requested entry cannot be read.
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."
}
}host.providerSecrets.read(secretKey: string): string
host.providerSecrets.write(secretKey: string, value: string): voidReads and writes app-owned provider secrets for the currently running plugin.
- Secrets are automatically namespaced to the current plugin/provider id.
- On Windows, values are stored through the local protected provider secret store.
- On macOS/Linux, values are stored in the system credential vault.
read(...)throws when the secret is missing or the credential store is unavailable.write(...)throws when the value is empty or the credential store write fails.
const secretKey = "account:" + profileId + ":authJson"
const authJson = ctx.host.providerSecrets.read(secretKey)
const auth = JSON.parse(authJson)
auth.tokens.access_token = refreshedAccessToken
ctx.host.providerSecrets.write(secretKey, JSON.stringify(auth))host.sqlite.query(dbPath: string, sql: string): stringExecutes 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].valuehost.sqlite.exec(dbPath: string, sql: string): voidExecutes a write SQL statement against a SQLite database.
Behavior:
- Capability-gated: Only available when
capabilities.sqliteWriteis explicitlytrueinplugin.json; otherwise calls are blocked and logged - 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.
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.
Helper functions for creating output lines. All builders use an options object pattern.
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
}): MetricLineExample:
ctx.line.text({ label: "Account", value: "user@example.com" })
ctx.line.text({ label: "Status", value: "Active", color: "#22c55e", subtitle: "Since Jan 2024" })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
}): MetricLineNotes:
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"),
})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
}): MetricLineExample:
ctx.line.badge({ label: "Plan", text: "Pro", color: "#000000" })
ctx.line.badge({ label: "Status", text: "Connected", color: "#22c55e" })Helper functions for formatting values.
Capitalizes a plan name string.
ctx.fmt.planLabel("pro") // "Pro"
ctx.fmt.planLabel("team_plan") // "Team_plan"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"Converts cents to dollars.
ctx.fmt.dollars(1234) // 12.34
ctx.fmt.dollars(500) // 5Formats Unix milliseconds as short date.
ctx.fmt.date(1704067200000) // "Jan 1"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),
})host.ccusage.query(opts: {
provider?: "claude" | "codex" | "opencode" | "amp" | "droid" | "codebuff" | "hermes" | "pi" | "goose" | "openclaw" | "kilo" | "kimi" | "qwen" | "copilot" | "gemini", // 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 data-root override (source-specific environment variable)
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 - OpenCode:
ccusage opencode daily - Amp:
ccusage amp daily - Droid/Factory:
ccusage droid daily - Kilo, Kimi, Qwen, and Gemini: source-focused
ccusage <source> dailycommands
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
- Runtime runners: Executes pinned
ccusage@20.0.19via fallback chainbunx -> pnpm dlx -> yarn dlx -> npm exec -> npx - Windows launchers: Resolves
.exe,.cmd, and.batpackage-manager launchers from the user's npm application-data directory andPATH; a globally installed ccusage version is not used - Cold starts: Gives each pinned package-runner attempt up to 30 seconds so an initial registry/cache fill is not discarded by the normal warm-start budget
- Provider-aware: Resolves provider from
opts.provideror the plugin id, including aliases such asfactory→droidandopencode-go→opencode - Focused commands: Uses
ccusage <source> daily; it intentionally does not useccusage dailybecause that aggregates all detected agents - Legacy fallback: If
ccusage@20.0.19cannot 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
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 when the selected provider reports input usage |
outputTokens |
number |
Present when the selected provider reports output usage |
cacheCreationTokens |
number |
Provider-reported cache-write usage |
cacheReadTokens |
number |
Provider-reported cache-read usage |
reasoningTokens |
number |
Provider-reported reasoning usage |
cachedInputTokens |
number |
Codex field |
totalTokens |
number |
Optional aggregate; derive it only when every token component is present, otherwise keep it unknown |
totalCost |
number | null |
Claude cost field |
costUSD |
number |
Codex cost field |
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
var tokens = day.totalTokens
if (
tokens == null &&
day.inputTokens != null &&
day.outputTokens != null &&
day.cacheCreationTokens != null &&
day.cacheReadTokens != null &&
day.reasoningTokens != null
) {
tokens =
day.inputTokens +
day.outputTokens +
day.cacheCreationTokens +
day.cacheReadTokens +
day.reasoningTokens
}
ctx.host.log.info(
day.date + ": " + (tokens != null ? tokens : "n/a") + " tokens, $" + (cost != null ? cost : "n/a")
)
}
} else if (result.status === "no_runner") {
ctx.host.log.warn("ccusage unavailable: no package runner found")
}- Plugin Schema - Plugin structure, manifest format, and output schema