@cardinal4/opencode-tavily-quota 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,13 +26,17 @@ polls every 10 minutes and can be refreshed on demand with the command below.
26
26
  ## Requirements
27
27
 
28
28
  - OpenCode 2 (`opencode --version` reports `2.x`).
29
- - A Tavily API key, supplied one of two ways:
30
- - `TAVILY_API_KEY` in the environment OpenCode is started from, or
31
- - the `apiKey` plugin option configured on this plugin in `cli.json`.
32
-
33
- `TAVILY_API_KEY` takes precedence when both are set. The key is read in the CLI
34
- process and sent only to `api.tavily.com` as a `Bearer` token. It is never
35
- displayed.
29
+ - A Tavily API key, supplied by:
30
+ - `TAVILY_API_KEY` in the CLI environment,
31
+ - the `apiKey` plugin option configured in `cli.json`, or
32
+ - OpenCode's active Tavily connection (use `opencode auth login tavily` or
33
+ `/connect` to connect Tavily).
34
+
35
+ The CLI environment takes precedence over `apiKey`; OpenCode's active
36
+ connection is used when neither supplies a key. The server resolves its saved
37
+ credential and fetches `/usage` itself. Only quota data returns to the TUI;
38
+ the saved key is never sent over plugin RPC or displayed. With a remote server,
39
+ install this plugin there too to use its active connection.
36
40
 
37
41
  The `apiKey` option also accepts `{file:...}` and `{env:...}` references, which
38
42
  the plugin expands itself because `cli.json` does not (unlike `opencode.jsonc`).
@@ -40,12 +44,13 @@ the plugin expands itself because `cli.json` does not (unlike `opencode.jsonc`).
40
44
  ## Layout
41
45
 
42
46
  ```
43
- index.ts # server entrypoint (no-op; required for discovery)
47
+ index.ts # server entrypoint: integration credential + quota RPC
48
+ rpc.ts # shared RPC definition
44
49
  tui.tsx # TUI entrypoint: sidebar slot + command
45
50
  usage.ts # key resolution, /usage fetch, formatting
46
51
  usage.test.ts # unit tests for usage.ts
47
52
  scripts/build.mjs
48
- dist/ # built entrypoints shipped to npm (dist/index.js, dist/tui.js)
53
+ dist/ # built entrypoints shipped to npm
49
54
  ```
50
55
 
51
56
  `npm run build` compiles the TypeScript/JSX sources into `dist/`, which is what
@@ -55,12 +60,13 @@ Solid JSX runtime (`@opentui/solid`) is only provided at runtime, not from the
55
60
  installed plugin.
56
61
 
57
62
  OpenCode installs package plugins under `~/.cache/opencode/npm/`. The server
58
- entrypoint (`dist/index.js`) satisfies discovery; the `tui` entrypoint
59
- (`dist/tui.js`) is loaded by the CLI to render the sidebar.
63
+ entrypoint (`dist/index.js`) registers the quota RPC; the `tui` entrypoint
64
+ (`dist/tui.js`) renders the sidebar in the CLI.
60
65
 
61
66
  ## Configuration
62
67
 
63
- Register the plugin in `cli.json` to pass the API key as a plugin option:
68
+ If you already connected Tavily in OpenCode, no key option is needed. To pass
69
+ the API key as a plugin option instead, register the plugin in `cli.json`:
64
70
 
65
71
  ```json title="cli.json"
66
72
  {
@@ -122,19 +128,18 @@ therefore handled.
122
128
  ## Development
123
129
 
124
130
  `usage.ts` is deliberately free of OpenCode and OpenTUI imports so it can be
125
- tested anywhere. `usage.test.ts` covers the pure quota math (`deriveSnapshot`,
126
- `formatPercent`, `formatRemaining`), key resolution, and every `fetchQuota`
127
- error branch using an injected `fetch`.
131
+ tested anywhere. The tests cover quota math, local-key precedence and fallback,
132
+ the connected-key RPC, and HTTP error handling using a mock `fetch`.
128
133
 
129
134
  Run the tests and type check from the repository root:
130
135
 
131
136
  ```sh
132
137
  npm install
133
- npm test # tsx --test usage.test.ts
138
+ npm test
134
139
  npm run typecheck
135
140
  ```
136
141
 
137
142
  ## Notes
138
143
 
139
- - The plugin is a no-op on the server. All work happens in the CLI runtime,
140
- which is where TUI slots live.
144
+ - The server handles OpenCode-connected credentials and their quota requests;
145
+ the CLI handles local keys and renders TUI slots.
package/dist/index.js CHANGED
@@ -1,21 +1,31 @@
1
1
  /**
2
- * Server entrypoint for the Tavily quota sidebar plugin.
3
- *
4
- * OpenCode discovers a local plugin directory by its `index` (server) entry and
5
- * loads the `tui` entry beside it for the terminal UI. All rendering happens in
6
- * the CLI runtime (`tui.tsx`); this file only exists so the directory is a
7
- * complete plugin and shows up by id instead of as an anonymous entry.
8
- *
9
- * It deliberately imports nothing beyond `@opencode/plugin` so the background
10
- * service can load it in any runtime.
2
+ * Server entrypoint. Resolve the active Tavily integration credential and
3
+ * fetch quota here; only the quota result crosses the RPC boundary to the TUI.
11
4
  */
12
5
 
13
6
  import { Plugin } from "@opencode/plugin";
7
+ import { TavilyQuotaRpc } from "./rpc.js";
8
+ import { fetchQuota } from "./usage.js";
14
9
  export const PLUGIN_ID = "opencode-tavily-quota";
15
10
  export default Plugin.define({
16
11
  id: PLUGIN_ID,
17
- setup() {
18
- // Intentional no-op: the sidebar and command are registered by the TUI
19
- // entrypoint (tui.tsx), which runs in the CLI process.
12
+ async setup(ctx) {
13
+ await ctx.rpc.register(TavilyQuotaRpc, {
14
+ usage: async () => {
15
+ const connection = await ctx.integration.connection.active("tavily");
16
+ const credential = connection && (await ctx.integration.connection.resolve(connection));
17
+ const key = credential?.type === "key" ? credential.key : connection?.type === "env" ? process.env[connection.name] : undefined;
18
+ if (!key) {
19
+ return {
20
+ ok: false,
21
+ message: "no active Tavily connection (run opencode auth login tavily)"
22
+ };
23
+ }
24
+ return fetchQuota({
25
+ apiKey: key,
26
+ env: {}
27
+ });
28
+ }
29
+ });
20
30
  }
21
31
  });
package/dist/rpc.js ADDED
@@ -0,0 +1,54 @@
1
+ import { Rpc } from "@opencode/plugin/rpc";
2
+
3
+ /** The server fetches quota so the saved OpenCode credential never enters the TUI. */
4
+ export const TavilyQuotaRpc = Rpc.define({
5
+ id: "opencode-tavily-quota",
6
+ methods: {
7
+ usage: {
8
+ input: {
9
+ type: "object",
10
+ properties: {},
11
+ additionalProperties: false
12
+ },
13
+ output: {
14
+ type: "object",
15
+ properties: {
16
+ ok: {
17
+ type: "boolean"
18
+ },
19
+ message: {
20
+ type: "string"
21
+ },
22
+ snapshot: {
23
+ type: "object",
24
+ properties: {
25
+ planName: {
26
+ type: "string"
27
+ },
28
+ used: {
29
+ type: "number"
30
+ },
31
+ limit: {
32
+ type: "number"
33
+ },
34
+ remaining: {
35
+ type: "number"
36
+ },
37
+ percentRemaining: {
38
+ type: "number"
39
+ },
40
+ updatedAt: {
41
+ type: "number"
42
+ }
43
+ },
44
+ required: ["planName", "used", "limit", "remaining", "percentRemaining", "updatedAt"],
45
+ additionalProperties: false
46
+ }
47
+ },
48
+ required: ["ok"],
49
+ additionalProperties: false
50
+ }
51
+ }
52
+ },
53
+ events: {}
54
+ });
package/dist/tui.js CHANGED
@@ -19,14 +19,14 @@ import { createElement as _$createElement } from "@opentui/solid";
19
19
  * Colors follow the built-in quota sections: the heading uses the default text
20
20
  * color and the body (bar included) uses the muted/subdued text color.
21
21
  *
22
- * Authenticate with `TAVILY_API_KEY`, or with the `apiKey` plugin option
23
- * configured on this plugin in `cli.json`. Resolution and the network call
24
- * live in `./usage.ts`.
22
+ * Authenticate with `TAVILY_API_KEY`, the `apiKey` plugin option in `cli.json`,
23
+ * or OpenCode's active Tavily integration. A saved key is used server-side.
25
24
  */
26
25
 
27
26
  import { Plugin } from "@opencode/plugin/tui";
28
27
  import { createSignal } from "solid-js";
29
- import { fetchQuota, formatPercent, formatQuotaLine, formatRemaining } from "./usage.js";
28
+ import { fetchQuotaWithFallback, formatPercent, formatQuotaLine, formatRemaining } from "./usage.js";
29
+ import { TavilyQuotaRpc } from "./rpc.js";
30
30
  export const PLUGIN_ID = "opencode-tavily-quota";
31
31
  // Tavily's `/usage` endpoint allows at most 10 requests per 10 minutes, so poll
32
32
  // once per window and rely on the `/tavily-quota` command for on-demand reads.
@@ -118,6 +118,7 @@ export const TavilyQuotaTuiPlugin = Plugin.define({
118
118
  status: "loading"
119
119
  });
120
120
  const apiKey = typeof context.options.apiKey === "string" ? context.options.apiKey : undefined;
121
+ const remote = context.client.rpc(TavilyQuotaRpc);
121
122
  const refreshMs = typeof context.options.refreshMs === "number" && context.options.refreshMs > 0 ? context.options.refreshMs : REFRESH_INTERVAL_MS;
122
123
  let disposed = false;
123
124
  let inFlight = false;
@@ -125,8 +126,11 @@ export const TavilyQuotaTuiPlugin = Plugin.define({
125
126
  if (inFlight) return state();
126
127
  inFlight = true;
127
128
  try {
128
- const result = await fetchQuota({
129
- apiKey
129
+ const result = await fetchQuotaWithFallback({
130
+ apiKey,
131
+ remote: () => remote.usage({}, {
132
+ location: context.location ?? context.data.location.default()
133
+ })
130
134
  });
131
135
  if (!disposed) {
132
136
  setState(result.ok ? {
package/dist/usage.js CHANGED
@@ -36,6 +36,29 @@ export const DEFAULT_BAR_WIDTH = SIDEBAR_WIDTH - BAR_SEPARATOR.length - PERCENT_
36
36
 
37
37
  /** Shape of `GET https://api.tavily.com/usage` (all fields optional). */
38
38
 
39
+ /** Prefer CLI credentials; ask the server only when the CLI has no key. */
40
+ export async function fetchQuotaWithFallback(options) {
41
+ try {
42
+ const key = resolveApiKey(options.env, options.apiKey, options);
43
+ if (key) return fetchQuota({
44
+ ...options,
45
+ apiKey: key,
46
+ env: {}
47
+ });
48
+ } catch {
49
+ // Preserve the existing inline error for an unreadable {file:...} option.
50
+ return fetchQuota(options);
51
+ }
52
+ try {
53
+ return await options.remote();
54
+ } catch (error) {
55
+ const reason = error instanceof Error ? error.message : String(error);
56
+ return {
57
+ ok: false,
58
+ message: `OpenCode Tavily credential unavailable: ${reason}`
59
+ };
60
+ }
61
+ }
39
62
  function finite(value) {
40
63
  return typeof value === "number" && Number.isFinite(value) ? value : null;
41
64
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@cardinal4/opencode-tavily-quota",
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "description": "OpenCode 2 TUI plugin that shows remaining Tavily API credits in the sidebar.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "build": "node scripts/build.mjs",
21
21
  "prepublishOnly": "npm run build",
22
- "test": "tsx --test usage.test.ts",
22
+ "test": "tsx --test usage.test.ts index.test.ts",
23
23
  "typecheck": "tsc --noEmit"
24
24
  },
25
25
  "keywords": [