Add sqlite plugin: open/query/exec/script against .db files
Adds a single-file V2 plugin that exposes eight tools under the `sqlite` namespace (open, close, list, query, exec, script, tables, schema) so a model can read and write SQLite databases from a session. - OpenCode discovers it automatically from ~/.config/opencode/plugins/. - New runtime dependency: better-sqlite3 (installed in this repo's node_modules so the host's module resolution picks it up). - README documents install, copy-to-another-machine, options, and troubleshooting; SETUP.md is a copy-paste checklist. - Schema/options were trimmed down while authoring: the initial draft registered a no-op SQLite function on every open as a 'future hook'; removed, it was dead code.
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
# sqlite
|
||||
|
||||
An OpenCode V2 plugin that adds a small set of `sqlite_*` tools so a model
|
||||
can read and write SQLite database files from a session.
|
||||
|
||||
It is intentionally **not** a replacement for the existing app. It registers
|
||||
extra, opt-in tools under the `sqlite` namespace, leaving every other part of
|
||||
OpenCode unchanged.
|
||||
|
||||
- OpenCode V2 plugin docs: <https://opencode.ai/v2/docs/build/plugins/>
|
||||
- OpenCode plugin config: <https://opencode.ai/v2/docs/plugins>
|
||||
- better-sqlite3 (sync API, used under the hood): <https://github.com/WiseLibs/better-sqlite3>
|
||||
|
||||
## What you get
|
||||
|
||||
Eight tools, all under the `sqlite` namespace (effective names shown):
|
||||
|
||||
| Tool | Purpose |
|
||||
| ---- | ------- |
|
||||
| `sqlite_open` | Open or create a database, bind it to a short alias. |
|
||||
| `sqlite_close` | Close a previously opened connection. |
|
||||
| `sqlite_list` | List all currently open connections and their modes. |
|
||||
| `sqlite_query` | Run a SQL query (SELECT) and return rows. |
|
||||
| `sqlite_exec` | Run a non-SELECT statement (INSERT/UPDATE/DELETE/DDL). |
|
||||
| `sqlite_script` | Run a multi-statement SQL script in one transaction. |
|
||||
| `sqlite_tables` | List user-visible tables (and optionally views). |
|
||||
| `sqlite_schema` | Show columns and the original CREATE statement. |
|
||||
|
||||
Every tool except `list` is addressed by the `alias` returned from
|
||||
`sqlite_open`, so a single session can keep several databases live at once.
|
||||
|
||||
## Install
|
||||
|
||||
The plugin source is a single TypeScript file at
|
||||
`~/.config/opencode/plugins/sqlite/index.ts`. It has **two npm dependencies**
|
||||
that must be installed into the OpenCode config's own `node_modules` (not
|
||||
the project you are running OpenCode in). The OpenCode service loads them
|
||||
from `~/.config/opencode/node_modules` regardless of the project directory.
|
||||
|
||||
The host redirects module resolution to that location, so if you copy this
|
||||
plugin to another machine you must run the install steps there too.
|
||||
|
||||
### 1. Match `@opencode/plugin` to your OpenCode version
|
||||
|
||||
The package name and the API shape change between releases. The docs'
|
||||
import specifiers are correct, but only at the matching version.
|
||||
|
||||
```sh
|
||||
opencode --version # e.g. v2.0.24
|
||||
cd ~/.config/opencode
|
||||
npm i @opencode/plugin@$(opencode --version | sed 's/^v//')
|
||||
```
|
||||
|
||||
A stale `@opencode-ai/plugin` (V1-era name) sitting in `node_modules` is a
|
||||
leftover user-side dependency and does **not** reflect what the host
|
||||
provides. Ignore its export list.
|
||||
|
||||
### 2. Install the runtime dependency (`better-sqlite3`)
|
||||
|
||||
`better-sqlite3` ships native bindings; the install step runs the right
|
||||
prebuilt binary for your Node/electron ABI on most platforms.
|
||||
|
||||
```sh
|
||||
cd ~/.config/opencode
|
||||
npm i better-sqlite3
|
||||
```
|
||||
|
||||
If the prebuilt binary does not match your platform, the install will print
|
||||
`node-gyp` output. On Linux you may need `build-essential`, `python3`, and
|
||||
`make`:
|
||||
|
||||
```sh
|
||||
sudo apt-get install -y build-essential python3 make
|
||||
```
|
||||
|
||||
If you need to pin a specific version, the same version OpenCode's bundled
|
||||
Node loads is usually the safest choice. The plugin was developed against
|
||||
`better-sqlite3@13.0.3`; anything `>= 11.0.0` exposes the same surface.
|
||||
|
||||
### 3. Restart the OpenCode service
|
||||
|
||||
The background service caches module resolution, so a freshly installed
|
||||
package is invisible until the service restarts.
|
||||
|
||||
```sh
|
||||
opencode service restart
|
||||
opencode plugin list # expect a row for "sqlite" with source = local
|
||||
```
|
||||
|
||||
If `opencode plugin list` does not show the plugin, check
|
||||
`~/.local/share/opencode/log/opencode.log` and filter on the plugin
|
||||
directory name. The two messages you are most likely to hit are:
|
||||
|
||||
- `Cannot find package '@opencode/plugin' …` — step 1 was skipped.
|
||||
- `Cannot find package 'better-sqlite3' …` — step 2 was skipped.
|
||||
|
||||
### 4. (Optional) Configure the plugin
|
||||
|
||||
In `~/.config/opencode/opencode.jsonc` (or a project-local
|
||||
`.opencode/opencode.jsonc`), add the plugin. If you are using a relative
|
||||
path, **remember the path resolves against the config file, not the working
|
||||
directory**.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugins": [
|
||||
"./plugins/sqlite"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can also pass options here:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"package": "./plugins/sqlite",
|
||||
"options": {
|
||||
"maxRows": 5000,
|
||||
"maxBytes": 10485760,
|
||||
"defaultMode": "create",
|
||||
"allowedRoots": ["/home/me/data", "/tmp"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Meaning |
|
||||
| ------ | ------- | ------- |
|
||||
| `maxRows` | `1000` | Hard row cap on `sqlite_query`. Caller can ask for less but not more. |
|
||||
| `maxBytes` | `5242880` (5 MiB) | Hard byte cap on the JSON-serialised result of `sqlite_query`. |
|
||||
| `defaultMode` | `"create"` | Open mode when `sqlite_open` is called without `mode`. |
|
||||
| `allowedRoots` | `[]` (no restriction) | Absolute or cwd-relative roots; paths outside are rejected. |
|
||||
|
||||
If you copied this plugin to a different location, replace the path in the
|
||||
example above with wherever you put it. The plugin files are pure
|
||||
TypeScript — they are loaded by OpenCode's own transformer, so no separate
|
||||
build step is needed.
|
||||
|
||||
## Usage
|
||||
|
||||
A typical session goes: open → inspect → query → modify → close.
|
||||
|
||||
```text
|
||||
> open ./data/app.db as alias "app"
|
||||
> tables on "app"
|
||||
> schema of users on "app"
|
||||
> query "SELECT id, email FROM users WHERE created_at > ?"
|
||||
on "app" with params ["2026-01-01"]
|
||||
> exec "UPDATE users SET email = ? WHERE id = ?"
|
||||
on "app" with params {"email": "new@example.com", "id": 42}
|
||||
> script "BEGIN; DELETE FROM sessions WHERE expired < strftime('%s','now');
|
||||
INSERT INTO audit(message) VALUES ('cleanup'); COMMIT;"
|
||||
on "app"
|
||||
> close "app"
|
||||
```
|
||||
|
||||
### Modes for `sqlite_open`
|
||||
|
||||
| Mode | Behaviour |
|
||||
| ---- | --------- |
|
||||
| `create` (default) | Open if exists, otherwise create. Read-write. |
|
||||
| `rw` | Open read-write; fail if the file does not exist (`fileMustExist: true`). |
|
||||
| `ro` | Open read-only. `sqlite_exec` and `sqlite_script` reject writes; `sqlite_query` works. |
|
||||
| `memory` | In-memory database regardless of `path`. |
|
||||
|
||||
Relative `path` values resolve against OpenCode's current working directory
|
||||
for the session (`ctx.location.directory`). Absolute paths are taken as-is.
|
||||
Use `:memory:` to skip filesystem entirely.
|
||||
|
||||
### Parameters
|
||||
|
||||
`sqlite_query` and `sqlite_exec` accept a `params` field that is either:
|
||||
|
||||
- An **array** of values for `?` placeholders (in order), or
|
||||
- An **object** of `{ name: value, … }` for `:name` / `@name` / `$name`
|
||||
placeholders. The name is given **without** the leading sigil; better-
|
||||
sqlite3 maps any of the three prefix styles in the SQL to the same name.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
// positional
|
||||
params: [42, "alice"]
|
||||
|
||||
// named (matches ":id", "@id", or "$id" in the SQL)
|
||||
params: {"id": 42, "name": "alice"}
|
||||
```
|
||||
|
||||
### Reading large results
|
||||
|
||||
`sqlite_query` returns rows lazily. When the result is truncated, the
|
||||
output ends with `(truncated by cap of N rows / M bytes)`. To get the
|
||||
rest, either:
|
||||
|
||||
- Add a tighter `WHERE` clause and re-run, or
|
||||
- Raise `maxRows` / `maxBytes` in the plugin config, or
|
||||
- Run several `sqlite_query` calls with `LIMIT` / `OFFSET` in the SQL
|
||||
itself.
|
||||
|
||||
### Reading BLOBs
|
||||
|
||||
BLOB columns are returned as `{"__b64__": "...", "__bytes__": N}`. The
|
||||
base64 string is the raw bytes; the byte count is provided so the model
|
||||
can decide whether to read them or skip.
|
||||
|
||||
### Errors
|
||||
|
||||
Every tool returns a string. Errors are reported on a single line starting
|
||||
with `Error: ` and include the SQLite error code when available, for
|
||||
example:
|
||||
|
||||
```text
|
||||
Error: SQLite error SQLITE_CONSTRAINT_UNIQUE: UNIQUE constraint failed: users.email
|
||||
```
|
||||
|
||||
The model should be told (or already knows) that this prefix means "the
|
||||
operation failed; do not pretend it succeeded."
|
||||
|
||||
## File layout
|
||||
|
||||
This plugin is a single directory:
|
||||
|
||||
```text
|
||||
~/.config/opencode/plugins/sqlite/
|
||||
├── index.ts # the plugin
|
||||
├── README.md # this file
|
||||
└── SETUP.md # the install steps in copy-paste form
|
||||
```
|
||||
|
||||
To move it to another machine, copy the whole directory, then run the
|
||||
steps in `SETUP.md` (or the "Install" section above) on the new machine.
|
||||
Nothing in the plugin hard-codes the absolute path of the OpenCode config.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| ------- | ------------ | --- |
|
||||
| `opencode plugin list` does not show `sqlite` | Plugin directory is not in `plugins`; or `.git` was created inside `plugins/sqlite` (that turns the directory into a *different* plugin shape and OpenCode fails to load it). | Add `./plugins/sqlite` to `plugins` in `opencode.jsonc`; remove any `.git` inside the plugin dir. |
|
||||
| `failed to load plugin … Cannot find package '@opencode/plugin'` | Step 1 of the install was skipped. | `cd ~/.config/opencode && npm i @opencode/plugin@<version>`. |
|
||||
| `failed to load plugin … Cannot find package 'better-sqlite3'` | Step 2 was skipped, or `npm i` was run in the wrong directory. | `cd ~/.config/opencode && npm i better-sqlite3`. |
|
||||
| Plugin loads but `sqlite_open` reports `Cannot find module 'better-sqlite3'` at runtime | Same as above, but only surfaces when the tool actually runs. | Re-run `npm i better-sqlite3` inside `~/.config/opencode`. |
|
||||
| `node-gyp` build fails during install | Missing C toolchain. | `sudo apt-get install -y build-essential python3 make` (Debian/Ubuntu), or the equivalent for your distro, then `npm i better-sqlite3` again. |
|
||||
| `SQLITE_READONLY_DBMOVED` or `attempt to write a readonly database` | Connected in `ro` mode. | Re-open with `mode: "create"` (the default). |
|
||||
| `sqlite_open` reports `Path … is outside the configured allowedRoots` | The plugin is configured with `allowedRoots` and the path is outside them. | Add the parent directory to `allowedRoots`, or remove the option. |
|
||||
| `sqlite_exec` reports `SQLite returned SQLITE_SCHEMA …` | A cached prepared statement went stale because the schema changed under it. | Re-run the same call; the plugin retries with a fresh prepare. If it persists, the statement is genuinely incompatible with the current schema. |
|
||||
|
||||
For server-side plugin logs, use
|
||||
`opencode --standalone --print-logs` (the `--print-logs` flag alone prints
|
||||
client logs; plugin loading shows up in the service log at
|
||||
`~/.local/share/opencode/log/opencode.log`).
|
||||
|
||||
## Notes
|
||||
|
||||
- The plugin does not enable or disable any of OpenCode's existing
|
||||
providers, models, hooks, or tools. It only adds eight new tools.
|
||||
- better-sqlite3 is **synchronous**. Open runs in milliseconds; a single
|
||||
query on a normal-sized database runs in well under a second. Heavy
|
||||
long-running writes are not a great fit — they will block the plugin's
|
||||
`execute` until they complete, which is the only model the better-
|
||||
sqlite3 API offers.
|
||||
- All write tools refuse to run on a connection opened in `ro` mode,
|
||||
including the implicit-write operations inside `sqlite_script`.
|
||||
- The plugin keeps connections in memory. They are closed on OpenCode
|
||||
shutdown. The cleanup hook also runs on plugin reload, so editing the
|
||||
file in place does not leak file descriptors.
|
||||
Reference in New Issue
Block a user