opencode-plugin-kit 1.0.0-alpha.5 → 1.0.0-alpha.6

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
@@ -5,7 +5,7 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  Shared building blocks for [OpenCode](https://opencode.ai) sidebar plugins —
8
- the pieces that every sidebar widget ends up reimplementing. Used by five
8
+ the pieces that every sidebar widget ends up reimplementing. Used by four
9
9
  production plugins with **100% test coverage**.
10
10
 
11
11
  ## Install
@@ -16,24 +16,16 @@ Published on [npm](https://www.npmjs.com/package/opencode-plugin-kit):
16
16
  npm install opencode-plugin-kit
17
17
  ```
18
18
 
19
- Plugins depending on the kit declare it in `dependencies`; the SDK peer
20
- (`@opencode-ai/plugin`) must match your OpenCode runtime version (see each
19
+ Plugins depending on the kit declare it in `dependencies`; the host peer
20
+ (`@opencode/plugin`) must match your OpenCode v2 runtime version (see each
21
21
  plugin's README).
22
22
 
23
23
  ## Status
24
24
 
25
- **v1.0.0-alpha.1** — API stabilized, ready for integration testing.
25
+ **v1.0.0-alpha.6** — API stabilized, ready for integration testing.
26
26
 
27
27
  ## Quick Start
28
28
 
29
- ```sh
30
- # npm
31
- npm install opencode-plugin-kit
32
-
33
- # bun
34
- bun add opencode-plugin-kit
35
- ```
36
-
37
29
  ```ts
38
30
  import { createViewPicker, createCachedStore } from "opencode-plugin-kit"
39
31
 
@@ -95,7 +87,7 @@ their unique data and rendering.
95
87
  | `commands.ts` | `createToggle()`, `registerKeymapCommand()` | Toggle command + keymap registration |
96
88
  | `workspace.ts` | `resolveLocation()`, `workspaceDirectory()` | Workspace location resolution |
97
89
  | `toast.ts` | `showToast()` | Toast helper |
98
- | `host.ts` | `KitContext`, `KitMessageShape` | Structural host-contract types |
90
+ | `host.ts` | `KitContext`, `KitMessageShape`, `KitModelShape` | Structural host-contract types |
99
91
 
100
92
  Everything is re-exported from the package root (`src/index.ts`).
101
93
 
@@ -199,22 +191,22 @@ const { input, output, cost } = sumProviderTokens(context, sessionID, "opencode"
199
191
 
200
192
  ## Consuming Plugins
201
193
 
202
- | Plugin | What it does |
203
- | --------------------------------------------------------------- | -------------------------------------------------------------- |
204
- | [opencode-usage-quota-tracker](../opencode-usage-quota-tracker) | Live provider quota + usage in sidebar footer |
205
- | [opencode-model-recommender](../opencode-model-recommender) | Model recommendations by cache ratio, token cost, session cost |
206
- | [opencode-skill-lister](../opencode-skill-lister) | Skills list in sidebar |
207
- | [opencode-plugin-manager](../opencode-plugin-manager) | Plugin manager in sidebar |
194
+ | Plugin | What it does |
195
+ | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
196
+ | [opencode-usage-quota-tracker](https://github.com/ranjithrajv/opencode-usage-quota-tracker) | Live provider quota + usage in sidebar footer |
197
+ | [opencode-model-recommender](https://github.com/ranjithrajv/opencode-model-recommender) | Model recommendations by cache ratio, token cost, session cost |
198
+ | [opencode-skill-lister](https://github.com/ranjithrajv/opencode-skill-lister) | Skills list in sidebar |
199
+ | [opencode-plugin-manager](https://github.com/ranjithrajv/opencode-plugin-manager) | Plugin manager in sidebar |
208
200
 
209
201
  Link locally with `"opencode-plugin-kit": "file:../opencode-plugin-kit"` in the
210
202
  consumer's `package.json`, then `npm install` (or `bun install`).
211
203
 
212
204
  ## Compatibility
213
205
 
214
- The host plugin API is beta; its types are the spec. Kit consumes the context
206
+ The OpenCode v2 host plugin API's types are the spec. Kit consumes the context
215
207
  structurally — anything the host ships with the expected members satisfies it.
216
208
 
217
- - **No runtime dependency** on `@opencode-ai/plugin` — types only
209
+ - **No runtime dependency** on `@opencode/plugin` — types only
218
210
  - **Optional peer** — declared as optional so consumers share one copy
219
211
  - **CI tripwire** — `host.test-d.ts` pins published host types against kit's
220
212
  contract; drift fails CI with a readable diff
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-plugin-kit",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "Shared building blocks for OpenCode sidebar plugins: provider vocabulary, row formatting, defensive message traversal, connected-provider tracking, polling fetcher, cached resources, and a persisted view/filter picker (registry + slash command + dialog + toast).",
5
5
  "keywords": [
6
6
  "opencode",
@@ -12,7 +12,7 @@
12
12
  "author": "Ranjith Raj",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://github.com/ranjithraj/opencode-plugin-kit.git"
15
+ "url": "git+https://github.com/ranjithrajv/opencode-plugin-kit.git"
16
16
  },
17
17
  "files": [
18
18
  "src",
@@ -36,8 +36,9 @@
36
36
  "test:coverage": "vitest run --coverage.enabled"
37
37
  },
38
38
  "devDependencies": {
39
- "@opencode-ai/plugin": "0.0.0-beta-19242",
40
- "@opencode-ai/sdk": "0.0.0-beta-19234",
39
+ "@opencode/plugin": "2.0.15",
40
+ "@opencode/sdk": "2.0.15",
41
+ "@opencode/theme": "2.0.15",
41
42
  "@opentui/core": "^0.5.11",
42
43
  "@opentui/solid": "^0.5.11",
43
44
  "@types/node": "^26.5.0",
@@ -52,12 +53,12 @@
52
53
  "zod": "^4.5.4"
53
54
  },
54
55
  "peerDependencies": {
55
- "@opencode-ai/plugin": "0.0.0-beta-19242",
56
+ "@opencode/plugin": "^2.0.15",
56
57
  "solid-js": "^1.9.12",
57
58
  "zod": "^4.5.4"
58
59
  },
59
60
  "peerDependenciesMeta": {
60
- "@opencode-ai/plugin": {
61
+ "@opencode/plugin": {
61
62
  "optional": true
62
63
  }
63
64
  },
@@ -12,7 +12,7 @@
12
12
  // count, always toggleable.
13
13
  import { createSignal, Show } from "solid-js"
14
14
  import type { JSX } from "solid-js"
15
- import { usePlugin } from "@opencode-ai/plugin/tui"
15
+ import { usePlugin } from "@opencode/plugin/tui"
16
16
 
17
17
  /** The top-level collapsible sidebar section. */
18
18
  export function CollapsibleSection(props: {
@@ -37,13 +37,13 @@ export function CollapsibleSection(props: {
37
37
  <box flexDirection="column">
38
38
  <box flexDirection="row" gap={1} onMouseDown={() => expandable() && setExpanded((e) => !e)}>
39
39
  <Show when={expandable()}>
40
- <text fg={theme.text.default}>{expanded() ? "▼" : "▶"}</text>
40
+ <text fg={theme.text.base}>{expanded() ? "▼" : "▶"}</text>
41
41
  </Show>
42
- <text fg={theme.text.default}>
42
+ <text fg={theme.text.base}>
43
43
  <b>{props.title}</b>
44
44
  </text>
45
45
  <Show when={!expanded()}>
46
- <text fg={theme.text.subdued}> ({props.summary ?? count()})</text>
46
+ <text fg={theme.text.muted}> ({props.summary ?? count()})</text>
47
47
  </Show>
48
48
  </box>
49
49
  {props.pinned}
@@ -69,8 +69,8 @@ export function CollapsibleGroup(props: {
69
69
  return (
70
70
  <box flexDirection="column">
71
71
  <box flexDirection="row" gap={1} onMouseDown={() => setCollapsed((c) => !c)}>
72
- <text fg={theme.text.subdued}>{collapsed() ? "▸" : "▾"}</text>
73
- <text fg={theme.text.subdued}>
72
+ <text fg={theme.text.muted}>{collapsed() ? "▸" : "▾"}</text>
73
+ <text fg={theme.text.muted}>
74
74
  {props.title} ({props.count})
75
75
  </text>
76
76
  </box>
package/src/host.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Narrow host-contract types for opencode-plugin-kit.
2
2
  //
3
- // `@opencode-ai/plugin` is beta: its types ARE the spec. Kit consumes only a
3
+ // `@opencode/plugin` (v2) types ARE the spec. Kit consumes only a
4
4
  // small slice of the host context, so instead of accepting `any` everywhere
5
5
  // (which silently survives host upgrades and breaks at runtime), this module
6
6
  // defines the *structural minimum* kit needs and re-exports the exact host
@@ -14,7 +14,7 @@
14
14
  // Host types kit re-exports (single import point for consumers)
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
- /** Toast payload — mirrored from the host's TuiToast; host drift here breaks CI. */
17
+ /** Toast payload — structural subset of the host's v2 `ToastOptions`; drift breaks CI. */
18
18
  export type ToastInput = {
19
19
  message: string
20
20
  variant?: "success" | "error" | "warning" | "info"
@@ -32,10 +32,10 @@ export type SelectOption<Value = string> = {
32
32
  // Structural minimum of the host context kit touches
33
33
  // ---------------------------------------------------------------------------
34
34
 
35
- /** `context.storage.store(key, { initial })` — the older host returns
36
- * `[value, ref]` (mutating `ref` persists); the newer floating `beta` host
37
- * types it as `readonly [T, (mutation: (draft: T) => void) => Promise<void>]`.
38
- * Both shapes satisfy this union; kit code reads `[0]` and mutates it, which
35
+ /** `context.storage.store(key, { initial })` — v2 types this as
36
+ * `readonly [Store<T>, (mutation: (draft: T) => void) => Promise<void>]`;
37
+ * the older host returned `[T, T]` (mutating the second entry persisted).
38
+ * Both shapes satisfy this union; kit reads `[0]` and mutates it, which
39
39
  * both hosts persist. */
40
40
  export interface KitStorage {
41
41
  store<T extends object>(
@@ -60,7 +60,7 @@ export interface KitUI {
60
60
  alert(input: { title: string; message: string }): Promise<unknown>
61
61
  select(input: {
62
62
  title: string
63
- message: string
63
+ placeholder?: string
64
64
  current?: string
65
65
  options: readonly SelectOption[]
66
66
  }): Promise<unknown>
@@ -75,9 +75,9 @@ export interface KitCommandEntry {
75
75
  readonly title: string
76
76
  readonly description: string
77
77
  readonly group: string
78
- readonly palette?: boolean
78
+ readonly palette?: true
79
79
  readonly suggested?: boolean
80
- readonly slash?: { name: string; aliases?: string[]; arguments?: boolean }
80
+ readonly slash?: { name: string; aliases?: string[]; arguments?: true }
81
81
  run: (input?: string) => void
82
82
  }
83
83
 
@@ -102,7 +102,7 @@ export interface KitClient {
102
102
  }
103
103
 
104
104
  /** The structural minimum context for every kit factory. Deliberately
105
- * narrower than the host `TuiPluginApi`: kit only reads these surfaces, so
105
+ * narrower than the host `Plugin.Context`: kit only reads these surfaces, so
106
106
  * consumers can pass their real context directly. */
107
107
  export interface KitContext {
108
108
  readonly storage: KitStorage
@@ -117,9 +117,20 @@ export interface KitContext {
117
117
  // Structural message/model shapes the defensive readers walk
118
118
  // ---------------------------------------------------------------------------
119
119
 
120
- /** A message in either of the two beta-API shapes: a discriminated
121
- * `type`-tagged object or `{ info: Message }` envelope. Kit's readers
122
- * (`unwrap`, `isAssistant`, `providerId`, `modelId`) accept both. */
120
+ /** A model-list entry (host `ModelInfo`) — what the model accessors accept.
121
+ * Deliberately omits `cost`, which is a number on messages but an array on
122
+ * model entries, so the same accessors serve both. */
123
+ export interface KitModelShape {
124
+ readonly id?: string
125
+ readonly modelID?: string
126
+ readonly providerID?: string
127
+ readonly name?: string
128
+ readonly model?: { readonly providerID?: string; readonly modelID?: string; readonly id?: string }
129
+ }
130
+
131
+ /** A message in either of the two v2 shapes: a discriminated `type`-tagged
132
+ * object or `{ info: Message }` envelope. Kit's readers (`unwrap`,
133
+ * `isAssistant`, `providerId`, `modelId`) accept both. */
123
134
  export interface KitMessageShape {
124
135
  readonly type?: string
125
136
  readonly role?: string
package/src/providers.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  // Shared provider vocabulary for OpenCode sidebar widgets.
2
2
  // Provider IDs are the OpenCode workspace provider ids ("opencode" = Zen,
3
3
  // "opencode-go" = Go).
4
- import { readFileSync } from "node:fs"
4
+ import { existsSync, readFileSync } from "node:fs"
5
5
  import { homedir } from "node:os"
6
6
  import { join } from "node:path"
7
+ import { createRequire } from "node:module"
7
8
  import { createSignal } from "solid-js"
8
9
  import { connectedProviderIds } from "./schemas.ts"
9
- import type { KitContext, KitMessageShape } from "./host.ts"
10
+ import type { KitContext, KitMessageShape, KitModelShape } from "./host.ts"
10
11
 
11
12
  export const ZEN_PROVIDER = "opencode"
12
13
  export const GO_PROVIDER = "opencode-go"
@@ -52,17 +53,17 @@ export function asArray<T = any>(out: unknown): T[] {
52
53
  }
53
54
 
54
55
  /** Model id from either a model-list object or a message/message-part shape. */
55
- export function modelId(m: KitMessageShape): string {
56
+ export function modelId(m: KitModelShape): string {
56
57
  return m?.model?.modelID ?? m?.modelID ?? m?.model?.id ?? m?.id ?? ""
57
58
  }
58
59
 
59
60
  /** Provider id from either a model-list object or a message/message-part shape. */
60
- export function providerId(m: KitMessageShape): string {
61
+ export function providerId(m: KitModelShape): string {
61
62
  return m?.model?.providerID ?? m?.providerID ?? ""
62
63
  }
63
64
 
64
65
  /** Display name, falling back to the model id when the API omits `name`. */
65
- export function modelName(m: KitMessageShape): string {
66
+ export function modelName(m: KitModelShape): string {
66
67
  return m?.name ?? modelId(m)
67
68
  }
68
69
 
@@ -71,21 +72,85 @@ export function modelName(m: KitMessageShape): string {
71
72
  // ---------------------------------------------------------------------------
72
73
 
73
74
  const AUTH_PATH = () => join(homedir(), ".local/share/opencode/auth.json")
75
+ const DB_PATH = () => join(homedir(), ".local/share/opencode/opencode.db")
74
76
 
75
- /** Single defensive read of auth.json, keyed by provider id.
76
- * Every consumer shares this parse so the path and shape handling can't drift. */
77
+ /** Run a query with either SQLite driver's API and return rows. */
78
+ function queryRows(db: any, sql: string): any[] {
79
+ try {
80
+ return db.query(sql).all()
81
+ } catch {}
82
+ try {
83
+ return db.prepare(sql).all()
84
+ } catch {}
85
+ return []
86
+ }
87
+
88
+ /** Open OpenCode 2's SQLite store read-only, resolving the driver across
89
+ * Bun (`bun:sqlite`) and Node 22+ (`node:sqlite`). Returns null when the
90
+ * store is absent or no driver is available. */
91
+ function openCredentialDb(): any | null {
92
+ if (!existsSync(DB_PATH())) return null
93
+ const attempts: Array<() => any> = [
94
+ () => (import.meta as any).require?.("bun:sqlite"),
95
+ () => (globalThis as any).require?.("bun:sqlite"),
96
+ () => createRequire(import.meta.url)("bun:sqlite"),
97
+ () => (Function("return require")() as any)("bun:sqlite"),
98
+ ]
99
+ for (const attempt of attempts) {
100
+ try {
101
+ const { Database } = attempt()
102
+ if (Database) return new Database(DB_PATH(), { readonly: true })
103
+ } catch {}
104
+ }
105
+ try {
106
+ const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite")
107
+ if (DatabaseSync) return new DatabaseSync(DB_PATH(), { readOnly: true })
108
+ } catch {}
109
+ return null
110
+ }
111
+
112
+ let _dbCache: { at: number; keys: Record<string, string> } | null = null
113
+
114
+ /** API keys from OpenCode 2's SQLite `credential` table (integration_id →
115
+ * key). Cached briefly because `readAuth` runs on every render. */
116
+ function readAuthDb(): Record<string, string> {
117
+ const now = Date.now()
118
+ if (_dbCache && now - _dbCache.at < 30_000) return _dbCache.keys
119
+ const keys: Record<string, string> = {}
120
+ try {
121
+ const db = openCredentialDb()
122
+ if (db) {
123
+ for (const row of queryRows(db, "SELECT integration_id, value FROM credential")) {
124
+ const id = String(row?.integration_id ?? "").trim()
125
+ try {
126
+ const key = JSON.parse(String(row?.value ?? ""))?.key
127
+ if (id && typeof key === "string" && key.trim()) keys[id] = key.trim()
128
+ } catch {}
129
+ }
130
+ try {
131
+ db.close?.()
132
+ } catch {}
133
+ }
134
+ } catch {}
135
+ _dbCache = { at: now, keys }
136
+ return keys
137
+ }
138
+
139
+ /** Single defensive read of the auth sources, keyed by provider id.
140
+ * OpenCode 2 keeps credentials in its SQLite store; `auth.json` is the
141
+ * legacy V1 store. Both are read (SQLite wins on conflict) so consumers see
142
+ * the same keys regardless of OpenCode version. */
77
143
  export function readAuth(): Record<string, string> {
144
+ const out: Record<string, string> = {}
78
145
  try {
79
146
  const auth = JSON.parse(readFileSync(AUTH_PATH(), "utf8"))
80
- const out: Record<string, string> = {}
81
147
  for (const [id, cfg] of Object.entries(auth)) {
82
148
  const key = (cfg as any)?.key
83
149
  if (typeof key === "string" && key.trim()) out[id] = key.trim()
84
150
  }
85
- return out
86
- } catch {
87
- return {}
88
- }
151
+ } catch {}
152
+ for (const [id, key] of Object.entries(readAuthDb())) out[id] = key
153
+ return out
89
154
  }
90
155
 
91
156
  /** The API key for one provider, or "" when absent. */
package/src/viewPicker.ts CHANGED
@@ -131,7 +131,7 @@ export function createViewPicker<T extends PickerOption>(context: KitContext, co
131
131
  try {
132
132
  const selected = await context.ui.dialog.select({
133
133
  title: config.dialog.title,
134
- message: config.dialog.message,
134
+ placeholder: config.dialog.message,
135
135
  current: currentID(),
136
136
  options: registry.filter(selectable).map((e) => ({
137
137
  title: e.title,
package/tsconfig.json CHANGED
@@ -11,6 +11,5 @@
11
11
  "jsxImportSource": "@opentui/solid",
12
12
  "types": ["node"]
13
13
  },
14
- "include": ["src", "tests"],
15
- "exclude": ["tests/**/*.test-d.ts"]
14
+ "include": ["src", "tests"]
16
15
  }