@geohar/opencode-sharedserver
v0.9.2
Published
OpenCode plugin: manage shared backend processes via the sharedserver CLI.
Maintainers
Readme
opencode-sharedserver
An OpenCode plugin that manages shared backend processes
through the sharedserver CLI.
When OpenCode starts, the plugin brings up this host's profile with
sharedserver up --profile opencode. When OpenCode exits, it releases it with
sharedserver down. The sharedserver binary reads the config, expands
${VAR}, and selects the profile's servers itself. Because sharedserver is
reference-counted, multiple OpenCode instances — or other tools using the same
name — share a single backend process. The server survives opencode restarts
inside its grace period and shuts down automatically when the last client leaves.
About sharedserver
sharedserver
(crates.io) is a small Rust CLI
that runs a long-lived process on behalf of several clients with reference
counting, a configurable grace period after the last client detaches, and a
watcher that reaps dead clients automatically. It exposes a tiny verb
surface — use, unuse, up, down, list, info, check (up/down
bring a whole profile up or down) — and stores per-server
state in lockfiles under $XDG_RUNTIME_DIR/sharedserver/ (or
/tmp/sharedserver/). This plugin only ever speaks to that CLI; it doesn't
manage processes directly.
You do not need to install it. On first use this plugin fetches a matching
sharedserver from GitHub releases if one isn't already present — prebuilt, so no
Rust toolchain is involved. It only does this when nothing usable is found; any
sharedserver already on PATH (or in ~/.cargo/bin, ~/.local/bin,
/opt/homebrew/bin, /usr/local/bin) is used as-is, and an explicit binary
option or SHAREDSERVER_BIN is always honoured without being second-guessed.
The version fetched matches this plugin's own version, so the pair stay in lockstep. If an installed binary is older, the plugin says so and fetches the matching release; if that download fails it carries on with the older binary rather than leaving you with nothing. This is identical to the Claude Code plugin's behaviour — the two even share an install lock, so starting both at once won't race.
To install it yourself anyway:
# prebuilt, no toolchain
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/georgeharker/sharedserver/releases/latest/download/sharedserver-installer.sh | sh
# or, with cargo (see rustup.rs if you have no toolchain)
cargo install sharedserverCargo drops the binary at ~/.cargo/bin/sharedserver, which the plugin's
binary-resolution order already covers. Verify with:
sharedserver --version
sharedserver list # should print "(no servers)" on a fresh installIf you'd rather build from source, clone the repo and run cargo build
--release inside rust/ — the binary ends up at
rust/target/release/sharedserver. Point at it with the plugin's binary
option or SHAREDSERVER_BIN env var.
The upstream README has the full feature tour: grace-period semantics, state-machine diagram, dead-client detection, admin commands for debugging, and shell-completion install. Worth a skim before you wire servers in.
Why
sharedserver is useful for long-lived development services that several
clients want to share: vector DBs, language servers behind a wrapper, model
inference servers, dev HTTP servers, and so on. This plugin wires those
services to opencode's lifecycle so they come up with opencode and tear
down cleanly when it exits, without you having to start them manually.
Requirements
- OpenCode (with plugin support)
curl, for the one-time fetch ofsharedserveron first use — nothing else. A Rust toolchain is not required. If you'd rather supply the binary yourself, anysharedserverreachable viaPATH, thebinaryoption, or theSHAREDSERVER_BINenvironment variable is used instead of downloading.
Install
Add the plugin to your OpenCode config (~/.config/opencode/config.json).
OpenCode installs npm-published plugins automatically the first time it
encounters them in the plugin list.
{
"plugin": [
["@geohar/opencode-sharedserver@latest", {
"servers": {
"chroma": {
"command": "chroma",
"args": ["run", "--path", "{env:HOME}/.local/share/chromadb"],
"env": { "ANONYMIZED_TELEMETRY": "False" },
"gracePeriod": "30m"
}
}
}]
]
}The bare-string form ("@geohar/opencode-sharedserver@latest") loads the
plugin too, but no options reach it — so it has no inline servers and falls
back to a discovered servers.json (per-project or global). With neither an
inline nor a discovered config it brings nothing up and stays quiet. Use the
tuple form to pass inline servers or a profile/config.
⚠️
@latestrefreshes opportunistically, not on every launch. OpenCode installs the spec the first time it sees it (under~/.cache/opencode/packages/<spec>/) and reuses the cached copy on every subsequent load — itsNpm.adddoes not re-resolve dist-tags. The cache only gets wiped when opencode bumps its internalCACHE_VERSIONconstant, which happens once every few weeks alongside its own releases (the opencode team uses it as a plugin-refresh mechanism). So:
- Steady-state users on auto-updating opencode:
@latestpicks up new plugin versions "eventually", typically within a couple of weeks of a publish.- For Plugin developers iterating between publishes or those who need current version: that cadence is less helpful. Pin a specific version in the spec (e.g.
@geohar/[email protected]) and bump it per publish, orrm -rf ~/.cache/opencode/packages/@geohar/opencode-sharedserver@latest/before each restart.
OpenCode expands two substitution tokens inside the config:
{env:VAR}— replaced with the value of$VAR(empty string if unset).{file:path}— replaced with the contents ofpath(relative to the config file,~/expands to home).
These are plain text substitutions applied before JSONC parsing, so use
them anywhere a literal would go. {env:HOME} is the easiest way to keep
the config portable across machines.
Configuration
Top-level options:
| Field | Type | Description |
|-----------|-------------------------------|--------------------------------------------------------------------------|
| binary | string | Path to the sharedserver executable. Overrides SHAREDSERVER_BIN/PATH lookup. |
| lockdir | string | Forwarded as SHAREDSERVER_LOCKDIR to child invocations. |
| notify | boolean | Show TUI toasts for attach success/failure. Defaults to true. |
| servers | Record<string, ServerSpec> | Map of sharedserver name → server config. Takes precedence over any config file. |
| profiles| Record<string, string[]> | Optional named profiles for the inline servers ({ "<profile>": ["<server>", ...] }). |
| config | string | Explicit path to a servers.json. Overrides the discovery chain below. |
| profile | string | Profile this session brings up. Default opencode (or $OPENCODE_SHAREDSERVER_PROFILE). |
Where servers come from
Inline servers wins — it's materialized to a temp servers.json so it flows
through the sharedserver binary exactly like a file config does. With none set,
the binary reads the same servers.json as the Claude Code plugin, so one
file drives every client. First hit wins, and a per-project file replaces the
global rather than merging with it:
configoption, or$SHAREDSERVER_CONFIG.- Per-project —
.sharedserver.jsonor.sharedserver/servers.json, searched walking up from the current directory, so a config at a repo root applies to sessions started anywhere inside it. ~/.config/sharedserver/servers.json— global fallback.
The sharedserver binary expands ${VAR} throughout the config, so ${HOME}
and ${USER} work in any string value.
This plugin brings up the opencode profile (override via the profile
option or $OPENCODE_SHAREDSERVER_PROFILE). A server named by no profile is
universal and comes up regardless; a config with no profiles brings up
every server, exactly as before.
No servers configured is a normal state, not an error — the plugin does nothing and the session starts clean.
Per-server (ServerSpec):
| Field | Type | Description |
|---------------|----------------------------|------------------------------------------------------------------------------------------|
| command | string | Binary to run as the shared server. Required unless lazy is true. |
| args | string[] | Arguments passed to command. |
| env | Record<string, string> | Extra environment variables forwarded via sharedserver --env KEY=VALUE. |
| gracePeriod | string | Duration string: 30s, 5m, 1h, 2h30m. Time the server stays alive with no clients.|
| logFile | string | Capture server stdout/stderr to this path. |
| metadata | string | Optional metadata string forwarded to sharedserver. |
| lazy | boolean | Only attach if the server is already running; never start it. |
| skipIfEnv | string | Name of an env var; when it is set (non-empty) this server is skipped entirely — neither started nor attached. Use it when another host already launched the process for this session. |
Binary resolution order:
binaryoptionSHAREDSERVER_BINenvironment variablesharedserveronPATH~/.cargo/bin/sharedserver~/.local/bin/sharedserver/usr/local/bin/sharedserver/opt/homebrew/bin/sharedserver
What it runs
On plugin load:
sharedserver up --profile opencode --pid <opencode-pid> \
--profile-optional --json [--config <file>]up reads the config, expands ${VAR}, selects the opencode profile (plus any
universal, profile-less servers), and starts/attaches each — applying that
server's gracePeriod, env, logFile, metadata, and lazy. The --json
report tells the plugin exactly what came up, so it health-checks those servers
2.5s later (sharedserver info --json). Inline servers are materialized to a
temp file that --config points at.
On exit / SIGINT / SIGTERM / SIGHUP:
sharedserver down --profile opencode --pid <opencode-pid> --profile-optionaldown re-resolves the same selection and releases it, runs synchronously so it
completes from inside exit handlers, and removes any temp config. After
draining, signal handlers re-raise the original signal so OpenCode's exit code is
preserved.
Status surfacing
- A success toast (
started X; attached Y) fires in the OpenCode TUI once startup attach succeeds.startedlists servers freshly brought up this run;attachedlists servers that were already running. - ~2.5 s after a successful attach, the plugin polls
sharedserver infoandkill -0on the server PID. If the wrapped binary died on startup (sharedserver returned success but the underlying process crashed), an error toast fires. The structured log also gets ahealth check passedorserver PID … died shortly after startline. - Each failure (binary missing, bad config,
sharedserver usenon-zero exit, dead-on-arrival) fires its own error toast. - Disable all toasts with
notify: false. Errors still go to the log. - When OpenCode is running headless (CLI/script, no TUI), the toast endpoint no-ops and the plugin continues normally.
Behavior
- Any failure (missing binary, unreadable config,
sharedserver upnon-zero exit, dead-on-arrival from the health check) is logged and surfaced as an error toast. The plugin never throws — opencode keeps running even if every configured server fails to start, and one server failing does not abort the rest of the profile. sharedserverhas its own dead-client detection that polls every 5 s, so even if the plugin can't run its cleanup (hard crash,kill -9) the refcount eventually self-corrects.- Multiple opencode instances pointing at the same
nameshare one server. The first instance starts it; subsequent ones increment the refcount; the last one to exit triggers the grace period.
Example: multiple servers
{
"plugin": [
["@geohar/opencode-sharedserver@latest", {
"binary": "/opt/homebrew/bin/sharedserver",
"servers": {
"chroma": {
"command": "chroma",
"args": ["run", "--path", "{env:HOME}/.local/share/chromadb"],
"gracePeriod": "1h"
},
"ollama": {
"command": "ollama",
"args": ["serve"],
"env": { "OLLAMA_HOST": "127.0.0.1:11434" },
"gracePeriod": "2h",
"logFile": "/tmp/ollama.log"
},
"watchman": {
"lazy": true
}
}
}]
]
}Local development
git clone https://github.com/georgeharker/sharedserver
cd sharedserver/plugins/opencode
npm install
npm run build # emits dist/
npm run typecheck # without emitTo test a local checkout against your OpenCode without publishing, point the plugin spec at the directory:
{
"plugin": [
["file:///Users/me/Development/sharedserver/plugins/opencode", { "servers": { ... } }]
]
}OpenCode reads package.json's main field to find the compiled entry, so
run npm run build first.
Diagnostics
Plugin events are written to opencode's structured log under
service=sharedserver. The usual location is:
${XDG_DATA_HOME:-$HOME/.local/share}/opencode/log/Tail the latest log and watch for plugin activity:
tail -F "$(ls -t ~/.local/share/opencode/log/*.log | head -1)" \
| grep service=sharedserverExpected line shapes:
INFO service=plugin path=@geohar/opencode-sharedserver@latest loading plugin
INFO service=sharedserver profile "opencode": started chroma
INFO service=sharedserver chroma: health check passed (pid=12345, state=active)Failure shapes:
ERROR service=sharedserver up --profile opencode exited 1 (<stderr>)
ERROR service=sharedserver chroma: failed to start (<reason>)
ERROR service=sharedserver chroma: server PID 12345 died shortly after startIf you see loading plugin but no loaded options line, your options
aren't reaching the plugin — most likely the bare-string form (see above)
or a cached older version (also see above).
To inspect sharedserver itself:
sharedserver list
sharedserver info <name> # add --json for machine-readable
sharedserver admin doctor # validate state, clean stale lockfilesStatus tool
The plugin registers a read-only sharedserver_status tool that the agent can
call to report what sharedserver is running (servers, PIDs, refcounts). It is
read-only — it never starts or stops anything.
OpenCode's plugin API doesn't let a plugin register user slash commands (only
model-callable tools), and bringing profiles up/down is a user/lifecycle action
rather than the model's to trigger, so only status is exposed. Use sharedserver
up --profile <p> / down directly, or the config's profiles, for on-demand
control.
License
MIT
