quota-tavily: instant /quota-tavily report for OpenCode A developer published quota-tavily, an OpenCode plugin that answers the /quota-tavily command with an instant Tavily API usage report without consuming an LLM turn. The plugin fetches api.tavily.com/usage with a Bearer key and a 15-second timeout, then injects the result with noReply and ignored flags so it stays out of future model context; because OpenCode hooks cannot mark a command as handled, it aborts processing by throwing an error tagged with a Symbol.for("quota-tavily") marker. It requires Node.js 22, uses only Node builtins, is read-only, and is distributed as a public gist. | | / | | | quota-tavily — instant /quota-tavily report, no LLM turn. | | | | | | Registers the command from the config hook and answers it from | | | api.tavily.com/usage GET, Bearer key, 15 s timeout , injecting the text with | | | noReply: true and ignored: true so it stays out of future model context. | | | | | | OpenCode cannot mark a command.execute.before command as handled: hooks only | | | mutate output.parts , and returning runs the command template. The hook | | | therefore throws an error to abort processing before the LLM runs. That error | | | carries the Symbol.for "quota-tavily" marker, so the abort is identifiable | | | rather than being a plain, anonymous failure. | | | | | | Loader note: the default export is a V1 plugin module { id, server } . | | | OpenCode's legacy fallback treats every module export as a plugin and | | | throws "Plugin export is not a function" on non-function exports, so the | | | default module shape must stay; named exports are then ignored by the loader | | | and remain safe as test seams. id is required for file-based plugins. | | | | | | API key never printed : $TAVILY API KEY, else the first whitespace token of | | | ~/.secrets/tavily-api-key. | | | | | | Error handling: no key - "no API key"; request or abort failure - "request | | | failed"; 401 - "API key rejected"; 429 - "rate limited"; other non-2xx or | | | unparseable JSON - message plus the first 500 body chars. A 2xx with valid | | | JSON renders the usage report. | | | | | | Requires Node.js 22 oldest non-EOL LTS . Node builtins only; read-only. | | | | | | Public gist: https://gist.github.com/cardin/ca901db7f5a1e8e1919885bed6cb4963 | | | Install/upgrade: | | | curl -fsSL https://gist.githubusercontent.com/cardin/ca901db7f5a1e8e1919885bed6cb4963/raw/quota-tavily.ts \ | | | -o ~/.config/opencode/plugins/quota-tavily.ts | | | Or clone and pull: | | | gh gist clone ca901db7f5a1e8e1919885bed6cb4963 ~/quota-tavily && git -C ~/quota-tavily pull | | | / | | | import { readFile } from "node:fs/promises"; | | | import { homedir } from "node:os"; | | | import { join } from "node:path"; | | | import type { Plugin, PluginModule } from "@opencode-ai/plugin"; | | | export const COMMAND ID = "quota-tavily"; | | | export const COMMAND DESCRIPTION = "Show Tavily API quota remaining"; | | | export const API URL = "https://api.tavily.com/usage"; | | | export const KEY FILE = join homedir , ".secrets", "tavily-api-key" ; | | | export const REQUEST TIMEOUT MS = 15 000; | | | export const MAX ERROR BODY CHARS = 500; | | | export const HANDLED COMMAND = Symbol.for "quota-tavily" ; | | | / api.tavily.com/usage , derived so display text can never drift from API URL. / | | | const { host: API HOST, pathname: API PATH } = new URL API URL ; | | | const API DISPLAY = ${API HOST}${API PATH} ; | | | export interface TavilyUsage { | | | key?: { | | | usage?: number \| null; | | | limit?: number \| null; | | | } \| null; | | | account?: { | | | current plan?: string \| null; | | | plan usage?: number \| null; | | | plan limit?: number \| null; | | | } \| null; | | | } | | | / | | | Coerce an untrusted value to a finite number, or null when it is not one. | | | This is the single rule behind every field read below. | | | / | | | export function toCount value: unknown : number \| null { | | | return typeof value === "number" && Number.isFinite value ? value : null; | | | } | | | / Render a used/limit pair. / | | | export function formatCount used: unknown, limit: unknown : string { | | | const usedCount = toCount used ; | | | const limitCount = toCount limit ; | | | if usedCount === null && limitCount === null return "n/a"; | | | if limitCount === null return ${usedCount} used / unlimited ; | | | if usedCount === null return ${limitCount} limit ; | | | if limitCount === 0 return ${usedCount} used / 0 limit ; | | | const remaining = limitCount - usedCount; | | | const percentLeft = remaining / limitCount 100; | | | return ${remaining} / ${limitCount} remaining ${percentLeft.toFixed 1 }% left, ${usedCount} used ; | | | } | | | / Render a parsed usage payload. / | | | export function formatUsage data: TavilyUsage : string { | | | const account = data.account ?? {}; | | | const key = data.key ?? {}; | | | const lines = Tavily quota ${API DISPLAY} : ; | | | const hasPlanData = toCount account.plan usage == null \|\| toCount account.plan limit == null; | | | const hasKeyData = toCount key.usage == null \|\| toCount key.limit == null; | | | if hasPlanData { | | | const planName = | | | typeof account.current plan === "string" && account.current plan.length 0 | | | ? ${account.current plan} | | | : ""; | | | lines.push Plan${planName}: ${formatCount account.plan usage, account.plan limit } ; | | | } else if hasKeyData { | | | lines.push Key: ${formatCount key.usage, key.limit } ; | | | } else { | | | lines.push "No usage data returned." ; | | | } | | | return lines.join "\n" ; | | | } | | | export function noApiKeyMessage : string { | | | return | | | "Tavily quota: no API key found.", | | | Set TAVILY API KEY or write the key to ${KEY FILE}. , | | | .join "\n" ; | | | } | | | export function truncateBody body: string : string { | | | return body.slice 0, MAX ERROR BODY CHARS ; | | | } | | | / | | | Turn an HTTP status and raw body into the user-facing report. | | | Pure: no I/O, so every status branch is directly unit-testable. | | | / | | | export function renderUsageResponse status: number, body: string : string { | | | if status === 401 { | | | return Tavily quota: API key rejected 401 . Check TAVILY API KEY or ${KEY FILE}. ; | | | } | | | if status === 429 { | | | return "Tavily quota: rate limited on /usage 10 requests / 10 min . Try again later."; | | | } | | | if status < 200 \|\| status = 300 { | | | return Tavily quota: HTTP ${status} from ${API DISPLAY}\n${truncateBody body } ; | | | } | | | let data: TavilyUsage; | | | try { | | | data = JSON.parse body as TavilyUsage; | | | } catch { | | | return Tavily quota: invalid JSON from ${API DISPLAY}\n${truncateBody body } ; | | | } | | | return formatUsage data ; | | | } | | | / | | | Resolve the API key from the environment, then the key file. | | | Parameters are injectable so tests never touch the real environment or file. | | | / | | | export async function loadApiKey | | | env: Record