opencode-plugin-kit 1.0.0-alpha.5 → 1.0.0-alpha.7
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 +72 -23
- package/package.json +10 -8
- package/src/collapsible.tsx +45 -8
- package/src/host.ts +24 -13
- package/src/providers.ts +77 -12
- package/src/toast.ts +1 -1
- package/src/viewPicker.ts +1 -1
- package/tsconfig.json +1 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](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
|
|
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,22 @@ Published on [npm](https://www.npmjs.com/package/opencode-plugin-kit):
|
|
|
16
16
|
npm install opencode-plugin-kit
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
> **Still a prerelease.** `latest` currently resolves to `1.0.0-alpha.6` — the
|
|
20
|
+
> first OpenCode v2 build. The publish workflow tags every prerelease under
|
|
21
|
+
> its own identifier (`alpha`, `beta`, …) and leaves `latest` alone, so later
|
|
22
|
+
> alphas will _not_ move `latest` automatically. To track the newest build,
|
|
23
|
+
> install `opencode-plugin-kit@alpha`.
|
|
24
|
+
|
|
25
|
+
Plugins depending on the kit declare it in `dependencies`; the host peer
|
|
26
|
+
(`@opencode/plugin`) must match your OpenCode v2 runtime version (see each
|
|
21
27
|
plugin's README).
|
|
22
28
|
|
|
23
29
|
## Status
|
|
24
30
|
|
|
25
|
-
**v1.0.0-alpha.
|
|
31
|
+
**v1.0.0-alpha.6** — API stabilized, ready for integration testing.
|
|
26
32
|
|
|
27
33
|
## Quick Start
|
|
28
34
|
|
|
29
|
-
```sh
|
|
30
|
-
# npm
|
|
31
|
-
npm install opencode-plugin-kit
|
|
32
|
-
|
|
33
|
-
# bun
|
|
34
|
-
bun add opencode-plugin-kit
|
|
35
|
-
```
|
|
36
|
-
|
|
37
35
|
```ts
|
|
38
36
|
import { createViewPicker, createCachedStore } from "opencode-plugin-kit"
|
|
39
37
|
|
|
@@ -95,7 +93,7 @@ their unique data and rendering.
|
|
|
95
93
|
| `commands.ts` | `createToggle()`, `registerKeymapCommand()` | Toggle command + keymap registration |
|
|
96
94
|
| `workspace.ts` | `resolveLocation()`, `workspaceDirectory()` | Workspace location resolution |
|
|
97
95
|
| `toast.ts` | `showToast()` | Toast helper |
|
|
98
|
-
| `host.ts` | `KitContext`, `KitMessageShape`
|
|
96
|
+
| `host.ts` | `KitContext`, `KitMessageShape`, `KitModelShape` | Structural host-contract types |
|
|
99
97
|
|
|
100
98
|
Everything is re-exported from the package root (`src/index.ts`).
|
|
101
99
|
|
|
@@ -197,24 +195,75 @@ const totals = walkMessages(
|
|
|
197
195
|
const { input, output, cost } = sumProviderTokens(context, sessionID, "opencode")
|
|
198
196
|
```
|
|
199
197
|
|
|
198
|
+
### Collapsible
|
|
199
|
+
|
|
200
|
+
Imported from the `collapsible` subpath so the Solid JSX stays out of the main
|
|
201
|
+
entry.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { CollapsibleSection, CollapsibleGroup } from "opencode-plugin-kit/collapsible"
|
|
205
|
+
|
|
206
|
+
// From a host component that already has the plugin context:
|
|
207
|
+
const { theme } = usePlugin()
|
|
208
|
+
|
|
209
|
+
CollapsibleSection({
|
|
210
|
+
title: "Skills",
|
|
211
|
+
count: items.length,
|
|
212
|
+
threshold: 2, // Collapse the body at or below this count
|
|
213
|
+
pinned: <Note />, // Always visible between header and body
|
|
214
|
+
theme, // Header colors; pass the host's theme
|
|
215
|
+
children: <List />,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
CollapsibleGroup({
|
|
219
|
+
title: "Providers",
|
|
220
|
+
count: ids.length,
|
|
221
|
+
defaultCollapsed: true,
|
|
222
|
+
theme,
|
|
223
|
+
children: (collapsed) => (collapsed() ? <Summary /> : <List />),
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`theme` is optional but **recommended**. Without it the kit falls back to
|
|
228
|
+
`usePlugin()`, which throws when the kit is installed from `node_modules`
|
|
229
|
+
(it resolves its own copy of `@opencode/plugin` / `solid-js`, so the host's
|
|
230
|
+
plugin context is not visible). If the context is missing the header falls back
|
|
231
|
+
to unthemed text rather than throwing.
|
|
232
|
+
|
|
200
233
|
## Consuming Plugins
|
|
201
234
|
|
|
202
|
-
| Plugin
|
|
203
|
-
|
|
|
204
|
-
| [opencode-usage-quota-tracker](
|
|
205
|
-
| [opencode-model-recommender](
|
|
206
|
-
| [opencode-skill-lister](
|
|
207
|
-
| [opencode-plugin-manager](
|
|
235
|
+
| Plugin | What it does |
|
|
236
|
+
| ------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
237
|
+
| [opencode-usage-quota-tracker](https://github.com/ranjithrajv/opencode-usage-quota-tracker) | Live provider quota + usage in sidebar footer |
|
|
238
|
+
| [opencode-model-recommender](https://github.com/ranjithrajv/opencode-model-recommender) | Model recommendations by cache ratio, token cost, session cost |
|
|
239
|
+
| [opencode-skill-lister](https://github.com/ranjithrajv/opencode-skill-lister) | Skills list in sidebar |
|
|
240
|
+
| [opencode-plugin-manager](https://github.com/ranjithrajv/opencode-plugin-manager) | Plugin manager in sidebar |
|
|
241
|
+
|
|
242
|
+
Install from the registry:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
npm install opencode-plugin-kit@alpha
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The `latest` dist-tag tracks the newest v1 alpha; v2 releases ship under the
|
|
249
|
+
`alpha` dist-tag, so pin `opencode-plugin-kit@alpha` (or an explicit range) until
|
|
250
|
+
`latest` moves to v2.
|
|
208
251
|
|
|
209
|
-
|
|
210
|
-
|
|
252
|
+
For local development, link the sibling checkout with
|
|
253
|
+
`"opencode-plugin-kit": "file:../opencode-plugin-kit"` in the consumer's
|
|
254
|
+
`package.json`, then `npm install` (or `bun install`). Note that a linked
|
|
255
|
+
checkout shares one module graph with the host, so the collapsible components
|
|
256
|
+
find the plugin context without a `theme` prop; a registry install does not.
|
|
211
257
|
|
|
212
258
|
## Compatibility
|
|
213
259
|
|
|
214
|
-
The host plugin API
|
|
260
|
+
The OpenCode v2 host plugin API's types are the spec. Kit consumes the context
|
|
215
261
|
structurally — anything the host ships with the expected members satisfies it.
|
|
216
262
|
|
|
217
|
-
- **
|
|
263
|
+
- **Floor: `@opencode/plugin` 2.0.3.** Earlier 2.x releases have no `./tui`
|
|
264
|
+
subpath export, so nothing that imports `@opencode/plugin/tui` can resolve it.
|
|
265
|
+
Verified by typecheck and the type-tripwire against 2.0.3, 2.0.10 and 2.0.18.
|
|
266
|
+
- **No runtime dependency** on `@opencode/plugin` — types only
|
|
218
267
|
- **Optional peer** — declared as optional so consumers share one copy
|
|
219
268
|
- **CI tripwire** — `host.test-d.ts` pins published host types against kit's
|
|
220
269
|
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.
|
|
3
|
+
"version": "1.0.0-alpha.7",
|
|
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/
|
|
15
|
+
"url": "git+https://github.com/ranjithrajv/opencode-plugin-kit.git"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"src",
|
|
@@ -32,12 +32,14 @@
|
|
|
32
32
|
"fmt": "vp fmt",
|
|
33
33
|
"lint": "vp lint",
|
|
34
34
|
"typecheck": "tsc --noEmit",
|
|
35
|
-
"test
|
|
36
|
-
"test:
|
|
35
|
+
"test": "vp test",
|
|
36
|
+
"test:types": "vp test --typecheck",
|
|
37
|
+
"test:coverage": "vp exec vitest run --coverage"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@opencode
|
|
40
|
-
"@opencode
|
|
40
|
+
"@opencode/plugin": "2.0.15",
|
|
41
|
+
"@opencode/sdk": "2.0.15",
|
|
42
|
+
"@opencode/theme": "2.0.15",
|
|
41
43
|
"@opentui/core": "^0.5.11",
|
|
42
44
|
"@opentui/solid": "^0.5.11",
|
|
43
45
|
"@types/node": "^26.5.0",
|
|
@@ -52,12 +54,12 @@
|
|
|
52
54
|
"zod": "^4.5.4"
|
|
53
55
|
},
|
|
54
56
|
"peerDependencies": {
|
|
55
|
-
"@opencode
|
|
57
|
+
"@opencode/plugin": "^2.0.3",
|
|
56
58
|
"solid-js": "^1.9.12",
|
|
57
59
|
"zod": "^4.5.4"
|
|
58
60
|
},
|
|
59
61
|
"peerDependenciesMeta": {
|
|
60
|
-
"@opencode
|
|
62
|
+
"@opencode/plugin": {
|
|
61
63
|
"optional": true
|
|
62
64
|
}
|
|
63
65
|
},
|
package/src/collapsible.tsx
CHANGED
|
@@ -12,7 +12,40 @@
|
|
|
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
|
|
15
|
+
import { usePlugin } from "@opencode/plugin/tui"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Theme tokens a collapsible header needs. Derived from the host theme so
|
|
19
|
+
* callers can pass their `usePlugin().theme` straight through, and so we
|
|
20
|
+
* don't pin the host's color type (e.g. `RGBA`) here.
|
|
21
|
+
*/
|
|
22
|
+
type HostTheme = ReturnType<typeof usePlugin>["theme"]
|
|
23
|
+
export interface CollapsibleTheme {
|
|
24
|
+
text: {
|
|
25
|
+
base?: HostTheme["text"]["base"]
|
|
26
|
+
muted?: HostTheme["text"]["muted"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the theme for a header.
|
|
32
|
+
*
|
|
33
|
+
* Prefer an explicitly passed `theme`. When this package is consumed from
|
|
34
|
+
* `node_modules`, `usePlugin()` here resolves a different copy of
|
|
35
|
+
* `@opencode/plugin` (and `solid-js`) than the host renders with, so the
|
|
36
|
+
* host's plugin context is not visible and `usePlugin()` throws
|
|
37
|
+
* "PluginContextProvider is missing". Callers that already hold the host
|
|
38
|
+
* context pass `theme` down; the `usePlugin()` fallback keeps working for
|
|
39
|
+
* consumers whose module graph is deduped (tests, monorepos).
|
|
40
|
+
*/
|
|
41
|
+
function resolveTheme(explicit?: CollapsibleTheme): CollapsibleTheme {
|
|
42
|
+
if (explicit) return explicit
|
|
43
|
+
try {
|
|
44
|
+
return usePlugin().theme
|
|
45
|
+
} catch {
|
|
46
|
+
return { text: {} }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
16
49
|
|
|
17
50
|
/** The top-level collapsible sidebar section. */
|
|
18
51
|
export function CollapsibleSection(props: {
|
|
@@ -26,9 +59,11 @@ export function CollapsibleSection(props: {
|
|
|
26
59
|
/** Always-visible content between the header and the collapsible body
|
|
27
60
|
* (e.g. the plugin manager's status note). */
|
|
28
61
|
pinned?: JSX.Element
|
|
62
|
+
/** Header colors; pass the host's `usePlugin().theme` from the consumer. */
|
|
63
|
+
theme?: CollapsibleTheme
|
|
29
64
|
children: JSX.Element
|
|
30
65
|
}) {
|
|
31
|
-
const theme =
|
|
66
|
+
const theme = resolveTheme(props.theme)
|
|
32
67
|
const [expanded, setExpanded] = createSignal(false)
|
|
33
68
|
const count = () => props.count ?? 0
|
|
34
69
|
const threshold = () => props.threshold ?? 2
|
|
@@ -37,13 +72,13 @@ export function CollapsibleSection(props: {
|
|
|
37
72
|
<box flexDirection="column">
|
|
38
73
|
<box flexDirection="row" gap={1} onMouseDown={() => expandable() && setExpanded((e) => !e)}>
|
|
39
74
|
<Show when={expandable()}>
|
|
40
|
-
<text fg={theme.text.
|
|
75
|
+
<text fg={theme.text.base}>{expanded() ? "▼" : "▶"}</text>
|
|
41
76
|
</Show>
|
|
42
|
-
<text fg={theme.text.
|
|
77
|
+
<text fg={theme.text.base}>
|
|
43
78
|
<b>{props.title}</b>
|
|
44
79
|
</text>
|
|
45
80
|
<Show when={!expanded()}>
|
|
46
|
-
<text fg={theme.text.
|
|
81
|
+
<text fg={theme.text.muted}> ({props.summary ?? count()})</text>
|
|
47
82
|
</Show>
|
|
48
83
|
</box>
|
|
49
84
|
{props.pinned}
|
|
@@ -62,15 +97,17 @@ export function CollapsibleGroup(props: {
|
|
|
62
97
|
title: string
|
|
63
98
|
count: number
|
|
64
99
|
defaultCollapsed?: boolean
|
|
100
|
+
/** Header colors; pass the host's `usePlugin().theme` from the consumer. */
|
|
101
|
+
theme?: CollapsibleTheme
|
|
65
102
|
children: JSX.Element | ((collapsed: () => boolean) => JSX.Element)
|
|
66
103
|
}) {
|
|
67
|
-
const theme =
|
|
104
|
+
const theme = resolveTheme(props.theme)
|
|
68
105
|
const [collapsed, setCollapsed] = createSignal(props.defaultCollapsed ?? false)
|
|
69
106
|
return (
|
|
70
107
|
<box flexDirection="column">
|
|
71
108
|
<box flexDirection="row" gap={1} onMouseDown={() => setCollapsed((c) => !c)}>
|
|
72
|
-
<text fg={theme.text.
|
|
73
|
-
<text fg={theme.text.
|
|
109
|
+
<text fg={theme.text.muted}>{collapsed() ? "▸" : "▾"}</text>
|
|
110
|
+
<text fg={theme.text.muted}>
|
|
74
111
|
{props.title} ({props.count})
|
|
75
112
|
</text>
|
|
76
113
|
</box>
|
package/src/host.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Narrow host-contract types for opencode-plugin-kit.
|
|
2
2
|
//
|
|
3
|
-
// `@opencode
|
|
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 —
|
|
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 })` —
|
|
36
|
-
* `[
|
|
37
|
-
*
|
|
38
|
-
* Both shapes satisfy this union; kit
|
|
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
|
-
|
|
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?:
|
|
78
|
+
readonly palette?: true
|
|
79
79
|
readonly suggested?: boolean
|
|
80
|
-
readonly slash?: { name: string; aliases?: string[]; arguments?:
|
|
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 `
|
|
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
|
|
121
|
-
* `
|
|
122
|
-
*
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
/**
|
|
76
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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/toast.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The host's toast UI is optional at the edges (beta API): every plugin
|
|
4
4
|
// hand-rolled the same try/catch fallback around `ui.toast.show`.
|
|
5
|
-
import type {
|
|
5
|
+
import type { KitUI, ToastInput } from "./host.ts"
|
|
6
6
|
|
|
7
7
|
/** Show a toast; silently no-ops when the host has no toast UI or it throws.
|
|
8
8
|
* Takes a minimal structural slice — the full host context's `ui.slot` is
|
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
|
-
|
|
134
|
+
placeholder: config.dialog.message,
|
|
135
135
|
current: currentID(),
|
|
136
136
|
options: registry.filter(selectable).map((e) => ({
|
|
137
137
|
title: e.title,
|