@timefly/opencode-plugin
v0.2.12
Published
TimeFly telemetry plugin for OpenCode — sessions, tokens, models, and tools
Downloads
1,463
Maintainers
Readme
@timefly/opencode-plugin
TimeFly telemetry for OpenCode. Tracks sessions, models, tokens, tool calls, and errors — not prompt content.
Quick start
# 1. Add plugin to OpenCode config
bunx @timefly/opencode-plugin setup-opencode -- --target user
# 2. Sign in with your TimeFly account (Google OAuth)
bunx @timefly/opencode-plugin login
# 3. Restart OpenCodeNote:
loginandsetup-opencodeare subcommands. Ifloginruns the installer again, update to v0.2.1+ or usebunx timefly-opencode-login.
You need a TimeFly Supporter plan for sync. Free accounts can install the plugin but POST /ai/sync returns 403.
Installation (step by step)
Prerequisites
- OpenCode installed and working
- Bun (OpenCode uses it internally; you need it for
bunx) - TimeFly account with Supporter plan (pricing)
Option A — npm (when published)
# Global plugin (all projects on this machine)
bunx @timefly/opencode-plugin setup-opencode -- --target user
# Sign in once
bunx @timefly/opencode-plugin login
# Restart OpenCodeOption B — one project only
bunx @timefly/opencode-plugin setup-opencode -- --target project --project /path/to/your/repo
bunx @timefly/opencode-plugin login
# restart OpenCode in that projectWrites to ./opencode.json in the project root.
Option C — local development (monorepo)
cd ai-integrations
bun install
bun run build
bun run --filter @timefly/opencode-plugin setup-opencode -- --target user --local
bun run --filter @timefly/opencode-plugin login
# restart OpenCode--local adds an absolute path to dist/index.js instead of the npm package name.
What setup-opencode changes
It merges into your OpenCode config (does not overwrite other settings):
| Target | Config file |
|--------|-------------|
| --target user | ~/.config/opencode/opencode.json (Windows: %USERPROFILE%\.config\opencode\opencode.json) |
| --target project | <project>/opencode.json |
Example result:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@timefly/opencode-plugin@latest"]
}OpenCode loads plugins at startup. You must restart OpenCode after install or config changes.
Verify it is loaded
After restart, use OpenCode normally (send a prompt, run a tool). Check:
- OpenCode logs — warnings if not signed in or not Supporter
- TimeFly dashboard → AI Usage card (may take ~10s after backend ingest)
- Local queue file — if sync fails, events appear in
<project>/.timefly-ai-queue.json
How auth works
OpenCode plugins run inside the OpenCode process. There is no built-in TimeFly UI inside OpenCode — auth is handled outside:
| Step | What happens |
|------|----------------|
| login | Opens browser → Google OAuth → saves tokens to ~/.config/opencode/timefly-auth.json |
| Plugin startup | Reads auth file from login |
| Each sync | Sends Authorization: Bearer <accessToken> to POST /ai/sync |
| Token expiry | SDK auto-refreshes using refreshToken (~30 days) and updates the auth file |
| No auth | Events queue locally in .timefly-ai-queue.json (project cwd) |
How sync works
The SDK batches OpenCode events before sending them to TimeFly. This keeps VPS load predictable as usage grows.
| Setting | Default | Behavior |
|---------|---------|----------|
| Debounce | 5s | Flush pending events 5 seconds after the last event in a burst |
| Max batch | 50 events | Flush immediately when the in-memory buffer is full |
| Immediate flush | session_end | Session totals are sent right away |
Typical flow during a coding session:
- You send a prompt →
llm_requestis queued - Model responds →
turn_complete, tool events, etc. join the same batch - After ~5s idle (or 50 events), one gzip
POST /ai/syncsends the batch session_endflushes immediately so session totals are not delayed
Compared with the old per-event sync model, one active agent turn usually produces 1–2 HTTP requests instead of 10–15+.
Unlike the VS Code extension (timer every ~2 minutes), OpenCode still syncs in near real time — just batched within each active burst.
What each sync request does
1. flushPendingQueue() ← retry anything in .timefly-ai-queue.json
2. gzip JSON batch ← usually 1–3 events per request
3. POST /ai/sync ← Bearer token from auth file
4. Gateway ← JWT validate + Supporter role check
5. Ingest → Redis queue ← accepted immediately (202-style)
6. Backend worker (~5s) ← drains Redis → ClickHouse
7. Dashboard ← reads aggregated data from ClickHouseEnd-to-end latency: ~1–10 seconds from event to dashboard (network + 5s worker poll).
Offline queue
If sync fails (no network, 401, 403), events append to:
<OpenCode project cwd>/.timefly-ai-queue.jsonOn the next successful sync, the SDK flushes this file first. Nothing is lost unless you delete the file.
Auth file location
~/.config/opencode/timefly-auth.json{
"accessToken": "...",
"refreshToken": "...",
"apiBaseUrl": "https://api.timefly.dev",
"savedAt": "2026-06-20T..."
}Access token expires in ~15 minutes; the SDK refreshes automatically using refreshToken (~30 days).
Architecture diagram
OpenCode hooks → @timefly/opencode-plugin → @timefly/ai-sdk
→ POST /ai/sync (gzip JSON, Bearer token)
→ Gateway auth + Supporter check
→ Ingest queue (Redis)
→ Worker (every 5s) → ClickHouse activity_events (ai.* activities)
→ Dashboard GET /analytics/ai-usageIf sync fails:
- 401 — run
loginagain - 403 — upgrade to Supporter at timefly.dev/pricing
- Network — events stay in local queue and retry on next event
Errors are logged via OpenCode's client.app.log() (service: timefly-opencode-plugin).
Install options (quick reference)
# Global (all projects)
bunx @timefly/opencode-plugin setup-opencode -- --target user
# Single project
bunx @timefly/opencode-plugin setup-opencode -- --target project --project /path/to/repo
# Local dev build
bun run --filter @timefly/opencode-plugin setup-opencode -- --target user --local
bun run --filter @timefly/opencode-plugin loginManual config
Add to opencode.json or ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@timefly/opencode-plugin@latest"]
}OpenCode installs npm plugins automatically at startup via Bun. Re-run setup-opencode to refresh @latest and clear the plugin cache.
Events captured
Based on OpenCode source (packages/opencode, @opencode-ai/plugin hooks, and @opencode-ai/sdk event types). We only emit metadata — never prompts, tool args, or file contents.
Plugin hooks we use
| Hook | When it fires (OpenCode source) | TimeFly event |
|------|----------------------------------|---------------|
| event | Bus events forwarded from EventV2Bridge to all plugins | see bus table below |
| chat.params | Before every LLM call (LLMRequestPrep.prepare) | llm_request |
| tool.execute.before | Before built-in, MCP, and registry tools run (SessionTools.resolve) | tool_call |
| tool.execute.after | After tool completes (same path) | tool_result |
| experimental.session.compacting | Before compaction LLM call (SessionCompaction.process) | compaction |
Bus events we handle (event hook)
| OpenCode event | Status | TimeFly event | Data captured |
|----------------|--------|---------------|---------------|
| session.created | Active | session_start | title, project, directory |
| session.status (type: idle) | Preferred | session_end | session token/tool/request totals |
| session.idle | Deprecated alias | session_end | same as above |
| message.updated (assistant, completed) | Active | turn_complete + llm_response | tokens, cost, duration, finish reason |
| message.updated (user) | Active | llm_request | model, agent (metadata only) |
| message.part.updated (step-finish) | Active | llm_response | per-step tokens, cost |
| message.part.updated (retry) | Active | error | retry attempt |
| message.part.updated (compaction) | Active | compaction | auto/manual flag |
| session.compacted | Active | compaction | session id |
| session.error | Active | error | error name/message |
| command.executed | Active | tool_call | command name (as command:*) |
Bus events we intentionally skip
| OpenCode event | Why not tracked |
|----------------|-----------------|
| message.part.updated (text, reasoning, tool, …) | Would expose prompt/response/tool args |
| message.removed, message.part.removed | Deletion only — no usage signal |
| session.updated, session.deleted, session.diff | Metadata or file diffs — not AI usage |
| file.edited, file.watcher.updated | File paths/content |
| permission.asked, permission.replied | No token/model signal |
| todo.updated | Task list text |
| lsp.*, pty.*, tui.*, installation.*, server.* | IDE/infra — not LLM usage |
Hooks we do not use (available in OpenCode, not needed for usage telemetry)
| Hook | Reason |
|------|--------|
| chat.message | Full user message + parts — privacy |
| chat.headers | Auth headers — security |
| shell.env | Environment variables — secrets risk |
| permission.ask | Could track denials; not implemented yet |
| command.execute.before | Parts contain prompt fragments |
| experimental.chat.messages.transform | Full message history |
| experimental.compaction.autocontinue | No extra telemetry beyond compaction events |
| tool (custom tools) | Execution still flows through tool.execute.* |
Known limitations
| Topic | Detail |
|-------|--------|
| Plugin cache | OpenCode caches plugins under ~/.cache/opencode/packages/. If you upgraded but still see old errors, delete @timefly there and restart OpenCode. |
| Custom providers (e.g. nan) | OpenCode runtime passes flat Provider.Info (provider.id) via LLMRequestPrep.prepare — not provider.info.id. Plugin reads both runtime and typed ProviderContext shapes. |
| Token counts | Require provider to report usage. If message.updated lacks token fields, turn events are skipped (no crash). |
| Provider-side tools | Tools executed inside the provider (providerExecuted) may not hit tool.execute.* — no tool events for those. |
| Multi-step turns | step-finish parts can emit extra llm_response events; message-level turn_complete deduplicates by message id. |
| session.idle vs session.status | OpenCode marks session.idle deprecated; we handle both. |
| v2 event system | OpenCode is migrating to session.next.* internally; plugins still receive v1 bus types above via the bridge. |
Quick reference (captured signals)
| OpenCode signal | TimeFly eventType | Data |
|-----------------|---------------------|------|
| session.created | session_start | title, project, directory |
| session.status / session.idle | session_end | session token/tool/request totals |
| chat.params | llm_request | model, provider, agent, temperature |
| message.updated (assistant, completed) | turn_complete + llm_response | tokens, duration, tokens/s, cost |
| message.part.updated (step-finish) | llm_response | per-step tokens |
| tool.execute.* | tool_call / tool_result | tool name, output length |
| session.compacted / compaction | compaction | auto/manual |
| session.error / retry | error | error metadata |
Privacy
Metadata and token counts only. No prompts, responses, or file contents. See docs/PRIVACY.md.
