{"slug": "quota-tavily-instant-quota-tavily-report-for-opencode", "title": "quota-tavily: instant /quota-tavily report for OpenCode", "summary": "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.", "body_md": "|  | /** | \n|  | * quota-tavily — instant `/quota-tavily` report, no LLM turn. | \n|  | * | \n|  | * Registers the command from the `config` hook and answers it from | \n|  | * api.tavily.com/usage (GET, Bearer key, 15 s timeout), injecting the text with | \n|  | * `noReply: true` and `ignored: true` so it stays out of future model context. | \n|  | * | \n|  | * OpenCode cannot mark a `command.execute.before` command as handled: hooks only | \n|  | * mutate `output.parts`, and returning runs the command template. The hook | \n|  | * therefore throws an error to abort processing before the LLM runs. That error | \n|  | * carries the Symbol.for(\"quota-tavily\") marker, so the abort is identifiable | \n|  | * rather than being a plain, anonymous failure. | \n|  | * | \n|  | * Loader note: the default export is a V1 plugin module `{ id, server }`. | \n|  | * OpenCode's legacy fallback treats *every* module export as a plugin and | \n|  | * throws \"Plugin export is not a function\" on non-function exports, so the | \n|  | * default module shape must stay; named exports are then ignored by the loader | \n|  | * and remain safe as test seams. `id` is required for file-based plugins. | \n|  | * | \n|  | * API key (never printed): $TAVILY_API_KEY, else the first whitespace token of | \n|  | * ~/.secrets/tavily-api-key. | \n|  | * | \n|  | * Error handling: no key -> \"no API key\"; request or abort failure -> \"request | \n|  | * failed\"; 401 -> \"API key rejected\"; 429 -> \"rate limited\"; other non-2xx or | \n|  | * unparseable JSON -> message plus the first 500 body chars. A 2xx with valid | \n|  | * JSON renders the usage report. | \n|  | * | \n|  | * Requires Node.js 22 (oldest non-EOL LTS). Node builtins only; read-only. | \n|  | * | \n|  | * Public gist: https://gist.github.com/cardin/ca901db7f5a1e8e1919885bed6cb4963 | \n|  | * Install/upgrade: | \n|  | * curl -fsSL https://gist.githubusercontent.com/cardin/ca901db7f5a1e8e1919885bed6cb4963/raw/quota-tavily.ts \\ | \n|  | * -o ~/.config/opencode/plugins/quota-tavily.ts | \n|  | * Or clone and pull: | \n|  | * gh gist clone ca901db7f5a1e8e1919885bed6cb4963 ~/quota-tavily && git -C ~/quota-tavily pull | \n|  | */ | \n|  | import { readFile } from \"node:fs/promises\"; | \n|  | import { homedir } from \"node:os\"; | \n|  | import { join } from \"node:path\"; | \n|  | import type { Plugin, PluginModule } from \"@opencode-ai/plugin\"; | \n|  | export const COMMAND_ID = \"quota-tavily\"; | \n|  | export const COMMAND_DESCRIPTION = \"Show Tavily API quota remaining\"; | \n|  | export const API_URL = \"https://api.tavily.com/usage\"; | \n|  | export const KEY_FILE = join(homedir(), \".secrets\", \"tavily-api-key\"); | \n|  | export const REQUEST_TIMEOUT_MS = 15_000; | \n|  | export const MAX_ERROR_BODY_CHARS = 500; | \n|  | export const HANDLED_COMMAND = Symbol.for(\"quota-tavily\"); | \n|  | /** `api.tavily.com/usage`, derived so display text can never drift from API_URL. */ | \n|  | const { host: API_HOST, pathname: API_PATH } = new URL(API_URL); | \n|  | const API_DISPLAY = `${API_HOST}${API_PATH}`; | \n|  | export interface TavilyUsage { | \n|  | key?: { | \n|  | usage?: number \\| null; | \n|  | limit?: number \\| null; | \n|  | } \\| null; | \n|  | account?: { | \n|  | current_plan?: string \\| null; | \n|  | plan_usage?: number \\| null; | \n|  | plan_limit?: number \\| null; | \n|  | } \\| null; | \n|  | } | \n|  | /** | \n|  | * Coerce an untrusted value to a finite number, or null when it is not one. | \n|  | * This is the single rule behind every field read below. | \n|  | */ | \n|  | export function toCount(value: unknown): number \\| null { | \n|  | return typeof value === \"number\" && Number.isFinite(value) ? value : null; | \n|  | } | \n|  | /** Render a used/limit pair. */ | \n|  | export function formatCount(used: unknown, limit: unknown): string { | \n|  | const usedCount = toCount(used); | \n|  | const limitCount = toCount(limit); | \n|  | if (usedCount === null && limitCount === null) return \"n/a\"; | \n|  | if (limitCount === null) return `${usedCount} used / unlimited`; | \n|  | if (usedCount === null) return `${limitCount} limit`; | \n|  | if (limitCount === 0) return `${usedCount} used / 0 limit`; | \n|  | const remaining = limitCount - usedCount; | \n|  | const percentLeft = (remaining / limitCount) * 100; | \n|  | return `${remaining} / ${limitCount} remaining (${percentLeft.toFixed(1)}% left, ${usedCount} used)`; | \n|  | } | \n|  | /** Render a parsed usage payload. */ | \n|  | export function formatUsage(data: TavilyUsage): string { | \n|  | const account = data.account ?? {}; | \n|  | const key = data.key ?? {}; | \n|  | const lines = [`Tavily quota (${API_DISPLAY}):`]; | \n|  | const hasPlanData = toCount(account.plan_usage) !== null \\|\\| toCount(account.plan_limit) !== null; | \n|  | const hasKeyData = toCount(key.usage) !== null \\|\\| toCount(key.limit) !== null; | \n|  | if (hasPlanData) { | \n|  | const planName = | \n|  | typeof account.current_plan === \"string\" && account.current_plan.length > 0 | \n|  | ? ` (${account.current_plan})` | \n|  | : \"\"; | \n|  | lines.push(`Plan${planName}: ${formatCount(account.plan_usage, account.plan_limit)}`); | \n|  | } else if (hasKeyData) { | \n|  | lines.push(`Key: ${formatCount(key.usage, key.limit)}`); | \n|  | } else { | \n|  | lines.push(\"No usage data returned.\"); | \n|  | } | \n|  | return lines.join(\"\\n\"); | \n|  | } | \n|  | export function noApiKeyMessage(): string { | \n|  | return [ | \n|  | \"Tavily quota: no API key found.\", | \n|  | `Set TAVILY_API_KEY or write the key to ${KEY_FILE}.`, | \n|  | ].join(\"\\n\"); | \n|  | } | \n|  | export function truncateBody(body: string): string { | \n|  | return body.slice(0, MAX_ERROR_BODY_CHARS); | \n|  | } | \n|  | /** | \n|  | * Turn an HTTP status and raw body into the user-facing report. | \n|  | * Pure: no I/O, so every status branch is directly unit-testable. | \n|  | */ | \n|  | export function renderUsageResponse(status: number, body: string): string { | \n|  | if (status === 401) { | \n|  | return `Tavily quota: API key rejected (401). Check TAVILY_API_KEY or ${KEY_FILE}.`; | \n|  | } | \n|  | if (status === 429) { | \n|  | return \"Tavily quota: rate limited on /usage (10 requests / 10 min). Try again later.\"; | \n|  | } | \n|  | if (status < 200 \\|\\| status >= 300) { | \n|  | return `Tavily quota: HTTP ${status} from ${API_DISPLAY}\\n${truncateBody(body)}`; | \n|  | } | \n|  | let data: TavilyUsage; | \n|  | try { | \n|  | data = JSON.parse(body) as TavilyUsage; | \n|  | } catch { | \n|  | return `Tavily quota: invalid JSON from ${API_DISPLAY}\\n${truncateBody(body)}`; | \n|  | } | \n|  | return formatUsage(data); | \n|  | } | \n|  | /** | \n|  | * Resolve the API key from the environment, then the key file. | \n|  | * Parameters are injectable so tests never touch the real environment or file. | \n|  | */ | \n|  | export async function loadApiKey( | \n|  | env: Record<string, string \\| undefined> = process.env, | \n|  | keyFile: string = KEY_FILE, | \n|  | ): Promise<string> { | \n|  | const fromEnv = (env.TAVILY_API_KEY ?? \"\").trim(); | \n|  | if (fromEnv) return fromEnv; | \n|  | try { | \n|  | const contents = await readFile(keyFile, \"utf8\"); | \n|  | return contents.trim().split(/\\s+/)[0] ?? \"\"; | \n|  | } catch { | \n|  | return \"\"; | \n|  | } | \n|  | } | \n|  | /** | \n|  | * Fetch and render the Tavily usage report. The key loader is injectable so | \n|  | * the no-key branch (and error branches) can be tested offline. | \n|  | */ | \n|  | export async function fetchUsage( | \n|  | loadKey: () => Promise<string> = loadApiKey, | \n|  | ): Promise<string> { | \n|  | const key = await loadKey(); | \n|  | if (!key) return noApiKeyMessage(); | \n|  | let response: Response; | \n|  | let body: string; | \n|  | try { | \n|  | response = await fetch(API_URL, { | \n|  | headers: { Authorization: `Bearer ${key}` }, | \n|  | signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | \n|  | }); | \n|  | body = await response.text().catch(() => \"\"); | \n|  | } catch (error) { | \n|  | const reason = error instanceof Error ? error.message : String(error); | \n|  | return `Tavily quota: request failed (${reason}). Check your network.`; | \n|  | } | \n|  | return renderUsageResponse(response.status, body); | \n|  | } | \n|  | /** Abort command processing, tagging the thrown error with the sentinel. */ | \n|  | function abortHandledCommand(): never { | \n|  | const error = new Error(\"quota-tavily: command handled, output already injected\"); | \n|  | error.name = \"QuotaTavilyHandled\"; | \n|  | Object.defineProperty(error, HANDLED_COMMAND, { value: true }); | \n|  | throw error; | \n|  | } | \n|  | export const TavilyQuota: Plugin = async ({ client }) => { | \n|  | /** Inject text without triggering a model reply or entering future context. */ | \n|  | async function injectOutput(sessionID: string, text: string): Promise<void> { | \n|  | try { | \n|  | await client.session.prompt({ | \n|  | path: { id: sessionID }, | \n|  | body: { | \n|  | noReply: true, | \n|  | parts: [{ type: \"text\", text, ignored: true }], | \n|  | }, | \n|  | }); | \n|  | } catch (error) { | \n|  | await client.app.log({ | \n|  | body: { | \n|  | service: \"quota-tavily\", | \n|  | level: \"warn\", | \n|  | message: \"Failed to inject quota output\", | \n|  | extra: { error: error instanceof Error ? error.message : String(error) }, | \n|  | }, | \n|  | }); | \n|  | throw error; | \n|  | } | \n|  | } | \n|  | async function handleCommand(input: { command: string; sessionID: string }): Promise<void> { | \n|  | const isTarget = input.command.replace(/^\\//, \"\") === COMMAND_ID; | \n|  | if (!isTarget \\|\\| !input.sessionID) return; | \n|  | await injectOutput(input.sessionID, await fetchUsage()); | \n|  | abortHandledCommand(); | \n|  | } | \n|  | return { | \n|  | config: async (config) => { | \n|  | config.command ??= {}; | \n|  | config.command[COMMAND_ID] ??= { | \n|  | template: `/${COMMAND_ID}`, | \n|  | description: COMMAND_DESCRIPTION, | \n|  | }; | \n|  | }, | \n|  | \"command.execute.before\": handleCommand, | \n|  | }; | \n|  | }; | \n|  | export default { | \n|  | id: \"quota-tavily\", | \n|  | server: TavilyQuota, | \n|  | } satisfies PluginModule; |", "url": "https://wpnews.pro/news/quota-tavily-instant-quota-tavily-report-for-opencode", "canonical_source": "https://gist.github.com/cardin/ca901db7f5a1e8e1919885bed6cb4963", "published_at": "2026-09-11 13:29:42+00:00", "updated_at": "2026-09-11 14:42:19.294661+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["OpenCode", "Tavily", "Node.js", "quota-tavily", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/quota-tavily-instant-quota-tavily-report-for-opencode", "markdown": "https://wpnews.pro/news/quota-tavily-instant-quota-tavily-report-for-opencode.md", "text": "https://wpnews.pro/news/quota-tavily-instant-quota-tavily-report-for-opencode.txt", "jsonld": "https://wpnews.pro/news/quota-tavily-instant-quota-tavily-report-for-opencode.jsonld"}}