1. Docs
  2. Data and settings reference

Data and settings API

This reference is generated from the canonical English repository document. Navigation and guides follow your selected language.

Read Markdown for agents · Repository source

These are plugin ctx methods, not the Hub HTTP API. Use them from activate(ctx) or its mounts. The quickstart supplies a minimal manifest and import steps; no backend is needed for these examples.

Read a snapshot

await ctx.data.query(query) reads captured data for the App's selected source. It does not probe providers or start another collector. The host defaults version to 1; callers need not supply it.

Query option Contract
resource Required: usage, models, tools, devices, projects, sessions, history, limits, subscriptions, status
period today (default), month, allTime
filter Optional string-valued filters; see below. Use returned opaque IDs, not raw host IDs.
sort {field, direction}; default id / asc. Fields: id, totalTokens, knownUsd, updatedAt, lastUsedAt, date, label; direction asc or desc. Choose a field meaningful for the resource.
page {limit, cursor?}; default limit 100, range 1–500. Omit cursor for the first page.
resolution, groupBy History only: daily / monthly / summary, and none / model / client; defaults daily, none.

Supported filter keys (values are strings of at most 256 characters):

Resource Filters
usage, status id, deviceId
models id, deviceId, model, search
tools id, deviceId, client, model, search
devices id, deviceId, search
projects, sessions id, deviceId, projectId (sessions only), client, model, search
history id, deviceId, from, to, client, model
limits id, deviceId, accountId, provider, search
subscriptions id, accountId, provider, search

from/to bound history dates inclusively. search matches available title/label/account/plan text case-insensitively. IDs are opaque and scoped to the plugin and selected source/privacy identity; do not parse them or assume they survive a source/privacy change.

Results and failure handling

The result contains version, schemaVersion, snapshotRevision, availability, rows, pageInfo: {nextCursor, hasMore} and aggregateCoverage. Successful snapshots also carry asOf, collectedAt, freshness: {ageMs, state} and redacted. Treat absent metadata on failure results as unavailable, rather than inventing timestamps or coverage.

  • Check availability before using rows. available can still have partial coverage; notCollected is not an empty measured total. Other outcomes include scopeUnavailable, denied, temporarilyUnavailable, budgetExceeded and rateLimited (with retryAfterMs). Do not busy-loop retries.
  • Invalid input is normally returned as an availability result such as invalidFilter, invalidPeriod or unsupportedResource; transport/disabled-plugin failures can reject the Promise. Handle both paths.
  • Read freshness.state (fresh, stale, unknown) and aggregateCoverage.kind (authorizedCollection, partial, unavailable). Page sums are not necessarily whole-source totals. Missing remote/lifetime session detail stays missing.
  • Limits rows expose provider as the stable provider identifier and providerLabel as the App's tool display name (for example codex / Codex, or opencode / OpenCode). Unknown providers retain their identifier as the label. Use the label for presentation and the identifier for filters and deduplication; a display name is not an account identity. Each limits window exposes displayLabel from the same display helper used by the App (for example Session, Weekly, Monthly or 5-hour), preserving an explicit provider label when present. The original nullable label remains unchanged; display defaults do not alter wire data.
  • The envelope freshness clock follows the captured usage snapshot. Limits can update independently while usage is idle; assess each quota row using its own status and updatedAt, rather than treating an old usage clock as proof that all quota readings are stale.
  • Usage rows expose totalTokens and cost: {knownUsd, unpricedTokens, currency, completeness}. knownUsd: null is unknown, not zero; a partial known amount is not the complete bill. Do not coerce missing values with || 0.

Keep the same query options when passing pageInfo.nextCursor as page.cursor. Ordinary data updates can leave an admitted cursor usable against its captured snapshot. If availability is snapshotExpired or invalidCursor, discard accumulated pages and restart without a cursor. Do not combine rows from different traversals. A source/privacy reset also requires clearing old rows and cursors.

Live updates with cleanup

Replace the quickstart's ui.mjs with this example. It displays today's usage, receives the initial snapshot through the subscription, and refetches on invalidation. It shows status rather than presenting missing data as zero.

js
export function activate(ctx) {
  return {
    mount(container) {
      const output = document.createElement('p');
      output.textContent = 'Loading usage…';
      container.append(output);
      const query = { resource: 'usage', period: 'today' };
      let closed = false, generation = 0, unsubscribe;

      function show(result) {
        if (closed) return;
        output.textContent = result.availability === 'available'
          ? `${result.rows.map(row => row.totalTokens ?? 'Unknown').join(', ')} tokens (${result.freshness.state})`
          : `Data: ${result.availability}`;
      }
      async function refetch() {
        const request = ++generation;
        try {
          const result = await ctx.data.query(query);
          if (request === generation) show(result);
        } catch (error) {
          if (!closed && request === generation) output.textContent = error.message;
        }
      }
      async function start() {
        try {
          const stop = await ctx.data.subscribe(query, event => {
            if (closed) return;
            if (event.type === 'snapshot') show(event.value);
            else if (event.type === 'invalidate') {
              if (event.reset) output.textContent = 'Source changed; reloading…';
              if (event.refetch) void refetch();
            }
          });
          if (closed) stop(); // Mount may have closed during subscription setup.
          else unsubscribe = stop;
        } catch (error) {
          if (!closed) output.textContent = error.message;
        }
      }
      void start();
      return {
        dispose() {
          closed = true;
          ++generation; // Ignore an in-flight result after disposal.
          unsubscribe?.();
          output.remove();
        }
      };
    }
  };
}

await ctx.data.subscribe(query, callback) resolves to an unsubscribe function. Subscribe without a cursor. Events are {type:'snapshot', value, sequence} followed by coalesced {type:'invalidate', refetch, reset, sequence, ...} events, not a stream of row patches. Do not throw from the callback. On reset, clear cached rows/cursors before refetching. The example uses a fixed period; for a period-dependent view, unsubscribe and resubscribe when update(context) changes its query. Honor visible:false by releasing work you do not need while hidden. Use ctx.onDispose(fn) for module-owned resources and mount dispose() for mount-owned resources.

Plugin settings

Add this top-level settings member to the quickstart manifest (keep its UI and file declarations):

json
{
  "settings": {
    "version": 1,
    "fields": [
      {"id":"showCost","type":"boolean","label":"Show cost","default":false},
      {"id":"rowLimit","type":"number","label":"Rows","default":20,"integer":true,"min":1,"max":100}
    ]
  }
}

This creates a native form that works even with plugin code disabled. A custom settings view is optional and executes trusted UI only when enabled.

Each field needs a unique id, type, label and valid default; description is optional. Labels/descriptions may be strings or locale maps. There are at most 64 fields. IDs start with an ASCII letter, then letters, digits, _ or -, up to 64 characters; prototype-reserved names are rejected.

Type Additional schema options
boolean Boolean default/value
number Finite number; optional min, max, integer
string minLength (default 0), maxLength (default 4096), both within 0–4096
select options: [{value:'compact', label:'Compact'}, ...]; value/default must match an option

Read and save

Use ctx.host.call('settings.get') and ctx.host.call('settings.update', request) for this plugin's preferences. No plugin ID is passed. Both return a snapshot with pluginId, schema, schemaVersion, storedSchemaVersion, revision, values, errors, status and readOnly.

For example, call this helper from your custom editor's Save handler. Pass the snapshot captured when the editor loaded, so concurrent edits are detected:

js
async function saveShowCost(ctx, current, checked, feedback) {
  if (current.readOnly) {
    feedback.textContent = 'Settings were saved by a newer schema; update the plugin.';
    return current;
  }
  try {
    const saved = await ctx.host.call('settings.update', {
      schemaVersion: current.schemaVersion,
      expectedRevision: current.revision,
      patch: { showCost: checked }
    });
    feedback.textContent = 'Saved';
    return saved; // Keep this revision for the next save.
  } catch (error) {
    feedback.textContent = `Save failed: ${error.message}. Reload settings before retrying.`;
    return current; // Keep the draft; do not silently overwrite a competing edit.
  }
}

Load the editor with const current = await ctx.host.call('settings.get') inside a try/catch. status is ready, invalid or newer-schema. errors maps invalid stored field IDs to validation reasons; those values are exposed as null and need explicit repair. Respect readOnly on schema downgrade.

settings.update patches declared fields only. A stale revision/schema rejects with settings-conflict; invalid fields reject with invalid-settings-values; a newer stored schema rejects with settings-newer-schema. Display failure feedback and reload deliberately before retrying; do not assume custom Error properties survive renderer IPC. settings.get also rejects if no schema is declared or the store cannot be read.

Increase settings.version when changing the schema. Optional declarative renames use migrations: [{fromVersion:1, renames:{oldName:'newName'}}] in a higher-version schema. Removed/unknown stored keys are retained privately for compatibility, not writable through the patch API. Rollback restores code, not arbitrary backend data migrations. This JSON preference store is not a secret vault; keep credential handling in a trusted backend and disclose its storage/access.