opencode-agent-flow 0.0.0-stage → 0.1.1
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/CHANGELOG.md +34 -0
- package/CONTRIBUTING.md +3 -0
- package/LICENSE +21 -0
- package/README.md +181 -2
- package/SECURITY.md +3 -0
- package/bin/cli.mjs +54 -0
- package/docs/architecture.md +58 -0
- package/docs/opencode-v2-api-notes.md +121 -0
- package/docs/screenshot.png +0 -0
- package/lib/demo.mjs +42 -0
- package/lib/privacy.mjs +9 -0
- package/lib/upstream.mjs +194 -0
- package/package.json +46 -4
- package/prices.json +10 -0
- package/public/app.js +223 -0
- package/public/flow-webgl.js +155 -0
- package/public/index.html +10 -0
- package/public/style.css +6 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/postprocessing/EffectComposer.js +363 -0
- package/public/vendor/three/postprocessing/MaskPass.js +195 -0
- package/public/vendor/three/postprocessing/OutputPass.js +139 -0
- package/public/vendor/three/postprocessing/Pass.js +191 -0
- package/public/vendor/three/postprocessing/RenderPass.js +183 -0
- package/public/vendor/three/postprocessing/ShaderPass.js +135 -0
- package/public/vendor/three/postprocessing/UnrealBloomPass.js +492 -0
- package/public/vendor/three/shaders/CopyShader.js +52 -0
- package/public/vendor/three/shaders/LuminosityHighPassShader.js +68 -0
- package/public/vendor/three/shaders/OutputShader.js +103 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/server.mjs +217 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.1] - 2026-10-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Actionable HTTP 401 error reporting without exposing upstream credentials.
|
|
12
|
+
- Connected OpenCode version in backend logs.
|
|
13
|
+
- Cyclic demo activity: tools, working states and bounded events every 1.5 seconds.
|
|
14
|
+
- Animated GIF and MP4 README demos, excluded from the npm package.
|
|
15
|
+
- CI for Node.js 20/22/24 on Linux and Node.js 22 on macOS/Windows.
|
|
16
|
+
- Bug report, feature request and pull request templates, plus security/discussion links.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Verified compatibility with a clean OpenCode 2.0.32 installation.
|
|
21
|
+
- Documented local/remote setup and generated or explicit server passwords.
|
|
22
|
+
- Normalized package.json metadata with `npm pkg fix`.
|
|
23
|
+
- CLI help reads the release version from package.json.
|
|
24
|
+
|
|
25
|
+
## [0.1.0] - 2026-10-16
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Initial release: read-only OpenCode V2 dashboard over public HTTP and SSE.
|
|
30
|
+
- WebGL agent trees, grouped parallel calls, tools, approval waits, tokens and estimated costs.
|
|
31
|
+
- Local/remote upstream support, fictional demo data and zero runtime npm dependencies.
|
|
32
|
+
|
|
33
|
+
[0.1.1]: https://github.com/orybakov/opencode-agent-flow/compare/v0.1.0...v0.1.1
|
|
34
|
+
[0.1.0]: https://github.com/orybakov/opencode-agent-flow/releases/tag/v0.1.0
|
package/CONTRIBUTING.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orybakov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,182 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-agent-flow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-agent-flow)
|
|
4
|
+
[](https://github.com/orybakov/opencode-agent-flow/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
**Watch your agents work, not just their logs.**
|
|
9
|
+
|
|
10
|
+
A live, read-only visualization of OpenCode V2 orchestration. See agent trees,
|
|
11
|
+
grouped parallel calls, current tools, approval waits, token usage and estimated
|
|
12
|
+
costs in one WebGL dashboard. Connect to a local or remote OpenCode server using
|
|
13
|
+
only its public HTTP API and SSE—no database access required.
|
|
14
|
+
|
|
15
|
+
Zero runtime npm dependencies. Node.js 20+; no frontend build step.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
[Watch the MP4 demo](https://github.com/orybakov/opencode-agent-flow/raw/refs/heads/main/docs/demo.mp4) · fictional data; no credentials or private sessions.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
### Local OpenCode server
|
|
24
|
+
|
|
25
|
+
Start OpenCode V2 in one terminal:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
opencode serve --hostname 127.0.0.1 --port 4096
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
In another terminal, set `OPENCODE_SERVER_PASSWORD` to the password printed by
|
|
32
|
+
that server (see below), then run:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npx opencode-agent-flow --open
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Remote OpenCode server
|
|
39
|
+
|
|
40
|
+
Set `OPENCODE_SERVER_PASSWORD` in your local terminal to the remote server's
|
|
41
|
+
password, then connect over HTTPS:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx opencode-agent-flow --server https://opencode.example.org --open
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The dashboard itself stays on loopback. The remote server must expose the V2
|
|
48
|
+
HTTP API and SSE; no database access or browser-side credentials are needed.
|
|
49
|
+
|
|
50
|
+
For a preview without any OpenCode server: `npx opencode-agent-flow --demo --open`.
|
|
51
|
+
Demo tools, agent states and events cycle every 1.5 seconds.
|
|
52
|
+
|
|
53
|
+
Open <http://127.0.0.1:3340>. For an unpublished local checkout:
|
|
54
|
+
`node bin/cli.mjs --demo`.
|
|
55
|
+
|
|
56
|
+
## Where do I get the password?
|
|
57
|
+
|
|
58
|
+
On a clean **OpenCode 2.0.32**, `opencode serve` generates a password when none
|
|
59
|
+
is configured and prints it in the launching terminal on the **`server password`**
|
|
60
|
+
line, after the listening URL. Copy that value privately into the
|
|
61
|
+
`OPENCODE_SERVER_PASSWORD` environment variable of the Agent Flow process.
|
|
62
|
+
The default HTTP Basic username is **`opencode`**; use `--username` or
|
|
63
|
+
`OPENCODE_SERVER_USERNAME` if yours differs.
|
|
64
|
+
|
|
65
|
+
Alternatively, choose a strong password yourself and set
|
|
66
|
+
`OPENCODE_SERVER_PASSWORD` **before starting `opencode serve`**, then supply the
|
|
67
|
+
same value to Agent Flow. For example, after loading it from your secret manager
|
|
68
|
+
into a shell variable (POSIX shell):
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
export OPENCODE_SERVER_PASSWORD="$YOUR_OPENCODE_PASSWORD"
|
|
72
|
+
opencode serve --hostname 127.0.0.1 --port 4096
|
|
73
|
+
# In another terminal, export the same environment variable, then:
|
|
74
|
+
npx opencode-agent-flow --open
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
In PowerShell, set an environment variable with
|
|
78
|
+
`$env:OPENCODE_SERVER_PASSWORD = $YourOpenCodePassword` instead. Do not put the
|
|
79
|
+
password in a URL, CLI flag, screenshot, issue or committed config. Agent Flow
|
|
80
|
+
does not discover credentials from the OpenCode database or managed service.
|
|
81
|
+
An already-running server does not inherit environment changes: set the value
|
|
82
|
+
when launching it, not only when launching this dashboard.
|
|
83
|
+
|
|
84
|
+
The generated-password output and explicit environment password were verified
|
|
85
|
+
against an isolated 2.0.32 installation. Current V2 docs describe the
|
|
86
|
+
[client/server model](https://opencode.ai/v2/docs/cli) and
|
|
87
|
+
[foreground server environment](https://opencode.ai/v2/docs/network#foreground);
|
|
88
|
+
`opencode serve` is distinct from the automatically managed background service.
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
| Flag | Environment fallback | Default |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `--server URL` | `OPENCODE_URL` | `http://127.0.0.1:4096` |
|
|
95
|
+
| `--username NAME` | `OPENCODE_SERVER_USERNAME` | `opencode` |
|
|
96
|
+
| `--port NUMBER` | `PORT` | `3340` |
|
|
97
|
+
| `--host ADDRESS` | — | `127.0.0.1` |
|
|
98
|
+
| `--allowed-host HOST` (repeatable) | — | Additional allowed hostname |
|
|
99
|
+
| `--window HOURS` | `WINDOW_HOURS` | `4` |
|
|
100
|
+
| `--open` | — | Open the browser |
|
|
101
|
+
| `--demo` | — | Built-in fictional data |
|
|
102
|
+
| `--help` | — | Show usage |
|
|
103
|
+
|
|
104
|
+
`OPENCODE_SERVER_PASSWORD` is accepted **only through the environment**. Without
|
|
105
|
+
it the BFF tries unauthenticated access; rejected access leaves the UI connecting
|
|
106
|
+
while the backend retries.
|
|
107
|
+
|
|
108
|
+
## How it works
|
|
109
|
+
|
|
110
|
+
A thin Node backend-for-frontend (BFF) loads paginated session/message history,
|
|
111
|
+
keeps one global upstream SSE subscription, and sends aggregated snapshots to
|
|
112
|
+
the browser. Reconnects use exponential backoff (1–30 seconds), reset only after
|
|
113
|
+
a connection lasts at least 30 seconds. Full resynchronization is limited to
|
|
114
|
+
once per 10 seconds; initial connection failures retry with the same backoff.
|
|
115
|
+
A periodic reconciliation (every 60 seconds) catches missed events. Permissions and forms are
|
|
116
|
+
polled for each active location. All/session modes, grouped `×N` agents and
|
|
117
|
+
individual-call popovers preserve the orchestration hierarchy.
|
|
118
|
+
|
|
119
|
+
`N agents · M calls` counts visible groups and all sessions respectively, each
|
|
120
|
+
including the root. The recent activity window applies to root selection; explicit
|
|
121
|
+
session mode loads the complete selected tree, including dormant children.
|
|
122
|
+
|
|
123
|
+
## Compatibility
|
|
124
|
+
|
|
125
|
+
- Verified with **OpenCode 2.0.30 and 2.0.32** on Linux (official V2 npm package:
|
|
126
|
+
`@opencode/cli`, not the V1 `opencode-ai` package).
|
|
127
|
+
- Tests pass locally on Linux with **Node.js 20, 22 and 24**.
|
|
128
|
+
- CI runs the test suite, syntax checks and package dry-run on Linux with Node
|
|
129
|
+
20/22/24, plus **macOS and Windows with Node 22**. Those platforms are covered
|
|
130
|
+
by CI tests only, not a verified live OpenCode/browser deployment; the first
|
|
131
|
+
hosted run happens after push.
|
|
132
|
+
- The V2 HTTP API is **experimental**; future changes may require adapter updates.
|
|
133
|
+
Non-V2 servers remain in connecting state.
|
|
134
|
+
|
|
135
|
+
## Security and limitations
|
|
136
|
+
|
|
137
|
+
- Loopback binding by default. **The UI has no authentication**; exposing it on
|
|
138
|
+
another interface reveals session titles, paths and activity. Use an authenticated
|
|
139
|
+
reverse proxy for shared deployments.
|
|
140
|
+
- The upstream password stays in Node, never in browser responses or URLs.
|
|
141
|
+
Use HTTPS to protect Basic authentication to remote servers.
|
|
142
|
+
- Host/Origin checks reject unapproved hostnames (DNS-rebinding protection).
|
|
143
|
+
Extra hostnames require `--allowed-host`; the configured port is enforced.
|
|
144
|
+
- The UI displays session titles, commands and paths. Secret redaction is
|
|
145
|
+
**best-effort**, not a guarantee. Do not expose it to a network without a
|
|
146
|
+
reverse proxy providing authentication. Report vulnerabilities via
|
|
147
|
+
[GitHub Security Advisories](https://github.com/orybakov/opencode-agent-flow/security/advisories/new).
|
|
148
|
+
- SSE is live-only, without replay. Reconciliation recovers durable snapshots,
|
|
149
|
+
not every transient event. No historical permission-decision journal is available.
|
|
150
|
+
- Token/cost panels use available history (at least 12 hours/current day for
|
|
151
|
+
default aggregates). Context size is approximate, not an exact provider meter.
|
|
152
|
+
|
|
153
|
+
## Cost estimates
|
|
154
|
+
|
|
155
|
+
`prices.json` supplies editable USD-per-million input/output/cache-read rates and
|
|
156
|
+
context limits. The UI labels costs **`≈ est.`**: pricing may be outdated, provider
|
|
157
|
+
subscriptions may differ, and these figures are not billing statements.
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
node bin/cli.mjs --demo
|
|
163
|
+
node scripts/check-syntax.mjs
|
|
164
|
+
npm test
|
|
165
|
+
npm pack --dry-run
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
See [architecture](docs/architecture.md) and
|
|
169
|
+
[public API notes](docs/opencode-v2-api-notes.md). Vendored browser modules avoid
|
|
170
|
+
CDN/network dependencies; there is no install or bundling step.
|
|
171
|
+
|
|
172
|
+
## Roadmap
|
|
173
|
+
|
|
174
|
+
- Ensemble team views and richer orchestration context.
|
|
175
|
+
- Memory and rendering performance for thousands of sessions.
|
|
176
|
+
- A small, ready-to-run Docker image.
|
|
177
|
+
- Additional themes and visual customization.
|
|
178
|
+
|
|
179
|
+
## License and acknowledgements
|
|
180
|
+
|
|
181
|
+
MIT © 2026 orybakov. WebGL rendering uses [three.js](https://threejs.org/),
|
|
182
|
+
vendored under its MIT license in `public/vendor/three/LICENSE`.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
Please report vulnerabilities privately using [GitHub Security Advisories](https://github.com/orybakov/opencode-agent-flow/security/advisories/new). Include reproduction steps and the affected version. Do not include credentials or private session data in public issues.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { start } from '../server.mjs';
|
|
5
|
+
process.on('unhandledRejection', error => console.error('Unhandled rejection:', error?.name || 'Error'));
|
|
6
|
+
process.on('uncaughtException', error => console.error('Uncaught exception:', error?.name || 'Error'));
|
|
7
|
+
|
|
8
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
9
|
+
const help = `opencode-agent-flow ${version} — live OpenCode V2 agent visualization
|
|
10
|
+
|
|
11
|
+
Usage: npx opencode-agent-flow [options]
|
|
12
|
+
--server URL OpenCode V2 URL (OPENCODE_URL; http://127.0.0.1:4096)
|
|
13
|
+
--username NAME Basic auth username (OPENCODE_SERVER_USERNAME; opencode)
|
|
14
|
+
--port NUMBER UI port (PORT; 3340)
|
|
15
|
+
--host ADDRESS UI bind address (127.0.0.1)
|
|
16
|
+
--allowed-host HOST Allow an additional Host hostname (repeatable)
|
|
17
|
+
--window HOURS Recent activity window (WINDOW_HOURS; 4)
|
|
18
|
+
--open Open the UI in your default browser
|
|
19
|
+
--demo Use built-in fictional data; no upstream connection
|
|
20
|
+
--help, -h Show this help
|
|
21
|
+
|
|
22
|
+
Password: OPENCODE_SERVER_PASSWORD environment variable only.
|
|
23
|
+
The UI has no authentication. Keep the default loopback bind address.`;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const config = { server: process.env.OPENCODE_URL || 'http://127.0.0.1:4096', username: process.env.OPENCODE_SERVER_USERNAME || 'opencode', password: process.env.OPENCODE_SERVER_PASSWORD, port: process.env.PORT || 3340, host: '127.0.0.1', window: process.env.WINDOW_HOURS || 4, open: false, demo: false };
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const [flag, inline] = args[i].split(/=(.*)/s);
|
|
30
|
+
if (['--help', '-h'].includes(flag)) { console.log(help); process.exit(0); }
|
|
31
|
+
if (['--open', '--demo'].includes(flag)) { if (inline !== undefined) throw new Error(`${flag} does not take a value.`); config[flag.slice(2)] = true; continue; }
|
|
32
|
+
if (!['--server', '--username', '--port', '--host', '--window', '--allowed-host'].includes(flag)) throw new Error('Unknown option. Use --help. Passwords are accepted only via OPENCODE_SERVER_PASSWORD.');
|
|
33
|
+
const value = inline ?? args[++i];
|
|
34
|
+
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${flag}.`);
|
|
35
|
+
if (flag === '--allowed-host') (config.allowedHosts ||= []).push(value); else config[flag.slice(2)] = value;
|
|
36
|
+
}
|
|
37
|
+
config.port = Number(config.port); config.window = Number(config.window);
|
|
38
|
+
if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) throw new Error('--port must be an integer from 1 to 65535.');
|
|
39
|
+
if (!Number.isFinite(config.window) || config.window <= 0) throw new Error('--window must be a positive number of hours.');
|
|
40
|
+
let url;
|
|
41
|
+
try { url = new URL(config.server); } catch { throw new Error('--server must be a valid HTTP(S) URL.'); }
|
|
42
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('--server must be HTTP(S), without credentials, query or fragment.');
|
|
43
|
+
config.server = url.href.replace(/\/$/, '');
|
|
44
|
+
if (config.host === '[::1]') config.host = '::1';
|
|
45
|
+
if (!['127.0.0.1', 'localhost', '::1'].includes(config.host)) console.error('WARNING: UI has no authentication and exposes session data. Non-loopback bind requested.');
|
|
46
|
+
if (config.password && url.protocol === 'http:' && !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)) console.error('WARNING: use HTTPS for a remote OpenCode server; Basic authentication is not encrypted over HTTP.');
|
|
47
|
+
await start(config);
|
|
48
|
+
if (config.open) {
|
|
49
|
+
const target = `http://${config.host.includes(':') ? `[${config.host}]` : config.host}:${config.port}`;
|
|
50
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open';
|
|
51
|
+
const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', target] : [target];
|
|
52
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' }); child.on('error', () => console.error('Unable to open browser; open the UI URL manually.')); child.unref();
|
|
53
|
+
}
|
|
54
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Agent Flow is a read-only backend-for-frontend (BFF) for OpenCode V2. Node.js
|
|
4
|
+
20+ serves static browser assets and aggregates the public HTTP API in memory.
|
|
5
|
+
There is no database access, build step, or runtime npm dependency.
|
|
6
|
+
|
|
7
|
+
## Data lifecycle
|
|
8
|
+
|
|
9
|
+
1. Check `/api/info` for a V2 server version. Authenticate with Basic only when
|
|
10
|
+
an environment password is configured. Credentials never reach the browser.
|
|
11
|
+
2. Open one global `/api/event` SSE subscription before loading history.
|
|
12
|
+
3. Page `/api/session` descending by `time.updated`, stopping locally at the
|
|
13
|
+
aggregation cutoff: the earliest of the activity window, 12 hours, and midnight.
|
|
14
|
+
4. Recover missing ancestors and recursively enumerate children of active roots,
|
|
15
|
+
including dormant children. Fetch message pages with concurrency capped at four.
|
|
16
|
+
Messages have no time filter; pagination stops locally after an old page.
|
|
17
|
+
5. Semantic SSE events mark affected sessions dirty. Coalesced GET requests replace
|
|
18
|
+
authoritative session/message snapshots, avoiding double-counted usage and
|
|
19
|
+
duplicated tool events. Reconnect uses exponential backoff (1–30 seconds), then
|
|
20
|
+
resynchronizes snapshots. A 60-second reconciliation covers missed events.
|
|
21
|
+
6. Poll pending permissions and forms every five seconds for every known active
|
|
22
|
+
session directory. Locations refer to the upstream server, not the BFF machine.
|
|
23
|
+
|
|
24
|
+
The selected-session endpoint lazily loads the complete descendant history,
|
|
25
|
+
including sessions outside the activity window. Default aggregates include only
|
|
26
|
+
the aggregation horizon; session-scoped aggregates include the selected tree.
|
|
27
|
+
The in-memory cache lasts for the process lifetime; no prompts or tool results
|
|
28
|
+
are written to disk. Unknown events are ignored or reconciled by snapshots.
|
|
29
|
+
|
|
30
|
+
## Browser contract
|
|
31
|
+
|
|
32
|
+
- `GET /api/state[?session=ID]`: roots, trees, grouped agents, last 200 events,
|
|
33
|
+
token aggregates, pending approvals, permission health, upstream version/status.
|
|
34
|
+
- `GET /api/sessions`: recent root session picker summaries.
|
|
35
|
+
- `GET /api/stream[?session=ID]`: `event: state` snapshots and 15-second heartbeats.
|
|
36
|
+
- `GET /api/permission/request`: normalized pending permissions/forms.
|
|
37
|
+
|
|
38
|
+
Children with the same nonempty agent name under one parent form a group (`×N`).
|
|
39
|
+
Unnamed sessions retain distinct identities; empty unnamed children are hidden.
|
|
40
|
+
Group status precedence is waiting, working, idle, done. `agentCount` counts visible
|
|
41
|
+
groups including the root; `callCount` counts all descendant sessions including
|
|
42
|
+
the root. Group popovers preserve individual sessions and token totals.
|
|
43
|
+
|
|
44
|
+
Statuses use active-session snapshots, running tools, pending permissions/forms,
|
|
45
|
+
and final idle messages. Context is the latest assistant input plus cache read and
|
|
46
|
+
write; missing current usage is marked estimated. Costs use `prices.json` and are
|
|
47
|
+
always approximate (`≈ est.`), not provider billing.
|
|
48
|
+
|
|
49
|
+
## Rendering and security
|
|
50
|
+
|
|
51
|
+
Vanilla browser modules render all/session modes, event log, hourly token chart,
|
|
52
|
+
cache ratio, activity heatmap, estimated cost, ledger and accessible group popovers.
|
|
53
|
+
Vendored three.js powers WebGL flow connections with a Canvas fallback.
|
|
54
|
+
|
|
55
|
+
Static assets use an explicit allowlist. The BFF exposes only GET routes, not a
|
|
56
|
+
general upstream proxy. Loopback is the default; non-loopback binding warns that
|
|
57
|
+
the UI has no authentication. Use HTTPS for remote upstream Basic authentication.
|
|
58
|
+
See [API contract notes](opencode-v2-api-notes.md) for compatibility limitations.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# OpenCode V2 public API: contributor notes
|
|
2
|
+
|
|
3
|
+
Contract verified against OpenCode **2.0.30** and clean **2.0.32**. The server describes this HTTP API
|
|
4
|
+
as experimental. Version checks reject non-V2 servers but do not guarantee that
|
|
5
|
+
every future V2 revision preserves this contract.
|
|
6
|
+
|
|
7
|
+
## HTTP snapshots
|
|
8
|
+
|
|
9
|
+
| Endpoint | Response and important details |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `/api/info` | Server `version`; OpenAPI info.version is not the server version |
|
|
12
|
+
| `/api/session?order=desc&limit=100` | `{data: Session[], cursor:{previous,next}}`, ordered by time.updated then ID |
|
|
13
|
+
| `/api/session?parentID=ID` | Children, also paginated; parentID=null lists roots |
|
|
14
|
+
| `/api/session/ID` | `{data: Session}` |
|
|
15
|
+
| `/api/session/ID/message?order=desc&limit=100` | `{data: Message[],cursor}`, sequence order within a session |
|
|
16
|
+
| `/api/session/active` | `{data: {sessionID: {type:"running"}}}` |
|
|
17
|
+
| `/api/permission/request?location[directory]=PATH` | `{location,data: Permission[]}` |
|
|
18
|
+
| `/api/form?location[directory]=PATH` | `{location,data: Form[]}` |
|
|
19
|
+
|
|
20
|
+
Subsequent pages use opaque `cursor` **without order**. Stop on empty pages as
|
|
21
|
+
well as null cursors: the final nonempty page may still contain a next cursor.
|
|
22
|
+
There are no session/message time filters. Apply cutoffs locally. Archived
|
|
23
|
+
sessions are not automatically excluded. A session directory is
|
|
24
|
+
`session.location.directory`; parentID is a tree edge, while fork is not.
|
|
25
|
+
Old descendants must be loaded independently of an updated-time cutoff.
|
|
26
|
+
|
|
27
|
+
Messages contain `id`, `type`, `time.created`, and assistant `content`, `model`,
|
|
28
|
+
`tokens`, optional `finish`, `time.streamed/completed`. Tool content contains
|
|
29
|
+
`id`, `name`, `state.input/status`, `time.created/ran/completed`. Statuses are
|
|
30
|
+
streaming, running, completed, error; streaming input can be a JSON string.
|
|
31
|
+
Unfiltered history includes idle messages, although `type=idle` is rejected.
|
|
32
|
+
The public API does not expose message row update timestamps or sequence fields.
|
|
33
|
+
|
|
34
|
+
Tokens are `{input,output,reasoning,cache:{read,write}}`. Session token totals are
|
|
35
|
+
cumulative: replace them, never add successive usage snapshots. A latest
|
|
36
|
+
assistant token snapshot approximates context size. Model cost can be zero;
|
|
37
|
+
local pricing is an estimate, not billing.
|
|
38
|
+
|
|
39
|
+
Permissions and forms are **location-scoped**. Omitting location only accesses
|
|
40
|
+
the server's default directory, not every session. Permission resources use
|
|
41
|
+
`resources: string[]`; forms contain id, sessionID, title and fields. Pending
|
|
42
|
+
forms count as waiting too. Session-scoped alternatives exist at
|
|
43
|
+
`/api/session/ID/permission` and `/api/session/ID/form`.
|
|
44
|
+
|
|
45
|
+
## Global event stream
|
|
46
|
+
|
|
47
|
+
`GET /api/event` covers all locations with one subscription. Frames use `data:`
|
|
48
|
+
containing JSON `{id,created,type,location?,data,durable?}`. The first frame is
|
|
49
|
+
`server.connected`. Heartbeats are comments. There is no documented replay,
|
|
50
|
+
time filter, cursor, or Last-Event-ID recovery; JSON event IDs do not imply replay.
|
|
51
|
+
Slow readers may be disconnected. The BFF must reconnect and reload snapshots.
|
|
52
|
+
|
|
53
|
+
Relevant event families include:
|
|
54
|
+
|
|
55
|
+
- session.created/deleted/renamed/moved, agent.selected/model.selected
|
|
56
|
+
- session.step.started/streamed/ended/failed and session.usage.updated
|
|
57
|
+
- session.tool.input.started/delta/ended, tool.called/progress/success/failed
|
|
58
|
+
- session.text.started/delta/ended, reasoning.started/delta/ended
|
|
59
|
+
- session.execution.started/succeeded/failed/interrupted, session.status/idle
|
|
60
|
+
- session.inbox.*, permission.asked/replied, form.created/replied/cancelled
|
|
61
|
+
|
|
62
|
+
Most data carries `sessionID`; assistant events carry `assistantMessageID` and
|
|
63
|
+
tool events carry tool `id`. form.created nests its session ID in `data.form`.
|
|
64
|
+
V1-style message.updated and message.part.updated events are not available.
|
|
65
|
+
Invalidating per-session snapshots avoids implementing a fragile full event store.
|
|
66
|
+
Do not log reasoning, prompts, tool outputs, form answers or authorization headers.
|
|
67
|
+
|
|
68
|
+
The experimental session log endpoint did not yield historical events in the
|
|
69
|
+
verified deployment (only log.synced); it is not used as a recovery guarantee.
|
|
70
|
+
Historical permission decisions and transient streaming deltas are not recoverable
|
|
71
|
+
from standard snapshots. Exact message row update timestamps cannot be reproduced.
|
|
72
|
+
|
|
73
|
+
## Authentication and portability
|
|
74
|
+
|
|
75
|
+
Basic authentication works with the configured server username/password. A
|
|
76
|
+
protected server returns 401 without credentials. Bearer authentication is not
|
|
77
|
+
equivalent. Keep credentials in environment variables and use HTTPS remotely.
|
|
78
|
+
Native browser EventSource cannot set Authorization, and arbitrary origins may
|
|
79
|
+
not be allowed by upstream CORS. The same-origin Node BFF avoids both issues.
|
|
80
|
+
It never forwards credentials to browser clients and performs only GET requests.
|
|
81
|
+
|
|
82
|
+
Verified against 2.0.30: version, session/active snapshots, pagination, idle
|
|
83
|
+
messages, location-scoped pending requests, Basic authentication and a live global
|
|
84
|
+
stream containing multiple locations. Remote deployments and future versions
|
|
85
|
+
require additional verification; capability changes should fail clearly.
|
|
86
|
+
|
|
87
|
+
### Clean 2.0.32 verification (2026-10-17)
|
|
88
|
+
|
|
89
|
+
Installed official `@opencode/cli@2.0.32` locally with isolated HOME/XDG paths;
|
|
90
|
+
the similarly named `opencode-ai` npm package is V1. No existing service or data
|
|
91
|
+
was used. Root and linked child sessions were created through `POST /api/session`
|
|
92
|
+
without model calls. One CLIProxy `gemini-3.8-flash-high` turn read a fixture
|
|
93
|
+
README using `read` and completed successfully.
|
|
94
|
+
|
|
95
|
+
Session/message/active snapshots and the global SSE envelope still match the
|
|
96
|
+
contract above. Observed 22 frames including `session.tool.input.started/ended`,
|
|
97
|
+
`session.tool.called/success`, `session.step.*`, `session.usage.updated`,
|
|
98
|
+
`session.text.*`, and `session.execution.started/succeeded`. Tool events retain
|
|
99
|
+
`data.sessionID`, `data.assistantMessageID`, and `data.id`. The BFF emitted four
|
|
100
|
+
live state frames (idle → working → tool completion → idle), retained the child,
|
|
101
|
+
and reported 10,301 input tokens, matching the two assistant snapshots. Upstream
|
|
102
|
+
output tokens were zero despite a text answer; the BFF preserves reported usage
|
|
103
|
+
rather than inventing output counts.
|
|
104
|
+
|
|
105
|
+
Both `/api/state` and `/api/state?session=ID`, `/api/sessions`, and the browser
|
|
106
|
+
session view were verified. Browser viewport: 1440×900; no console errors or
|
|
107
|
+
failed requests (headless WebGL ReadPixels performance warnings only).
|
|
108
|
+
|
|
109
|
+
Authentication caveat: an unset password does **not** produce an unprotected
|
|
110
|
+
server on this clean version. `serve` generates a password and unauthenticated
|
|
111
|
+
requests return 401. Basic auth works with both its generated password and an
|
|
112
|
+
explicit `OPENCODE_SERVER_PASSWORD`; a real password-free upstream scenario was
|
|
113
|
+
therefore unavailable. Wrong credentials now produce an actionable 401 log
|
|
114
|
+
without exposing credentials, while retaining the existing retry behavior.
|
|
115
|
+
Successful initialization logs the negotiated server version. No event-schema
|
|
116
|
+
adapter changes were needed. Multi-location streams and permission/form flows
|
|
117
|
+
were not re-exercised on 2.0.32; their earlier 2.0.30 checks remain the evidence.
|
|
118
|
+
|
|
119
|
+
Sources: [API](https://opencode.ai/v2/docs/api),
|
|
120
|
+
[client](https://opencode.ai/v2/docs/build/client), published
|
|
121
|
+
`@opencode/client@2.0.30` event types, and live read-only HTTP/SSE observations.
|
|
Binary file
|
package/lib/demo.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function demo(onChange = () => {}, { interval = setInterval, cancel = clearInterval, clock = Date.now } = {}) {
|
|
2
|
+
const now = Date.now(), sessions = new Map(), messages = new Map();
|
|
3
|
+
const agents = ['lead', 'builder', 'researcher', 'reviewer', 'builder', 'tester', 'designer'];
|
|
4
|
+
agents.forEach((agent, i) => {
|
|
5
|
+
const id = `ses_demo${i}`, model = { id: ['gpt-6.1-sol', 'claude-sonnet-4-5', 'gemini-2.5-pro'][i % 3] };
|
|
6
|
+
sessions.set(id, { id, parentID: i ? 'ses_demo0' : undefined, title: i ? `${agent} · public API integration` : 'Build a better developer experience', agent, model, time: { created: now - 3600000, updated: now - i * 1000 }, tokens: { input: 24000 + i * 7000, output: 3200, cache: { read: 180000 + i * 19000, write: 0 } } });
|
|
7
|
+
messages.set(id, [{ id: `msg_demo${i}`, type: 'assistant', model, time: { created: now - 90000 + i * 5000, completed: now - 20000 }, tokens: { input: 12000, output: 1200, cache: { read: 48000, write: 0 } }, content: [{ id: `call_demo${i}`, type: 'tool', name: ['read', 'patch', 'webfetch', 'grep', 'shell', 'shell', 'read'][i], state: { status: i === 1 || i === 2 ? 'running' : 'completed', input: { path: ['src/index.ts', 'src/api/client.ts', 'https://example.org/docs', 'src/**/*.ts', 'npm test', 'npm run check', 'docs/design.md'][i] } }, time: { created: now - 30000 + i * 2000, completed: now - 10000 } }], finish: 'tool-calls' }]);
|
|
8
|
+
});
|
|
9
|
+
let timer, tick = 0;
|
|
10
|
+
const tools = [
|
|
11
|
+
['read', 'src/index.ts'], ['patch', 'src/api/client.ts'],
|
|
12
|
+
['webfetch', 'https://example.org/docs'], ['grep', 'src/**/*.ts'],
|
|
13
|
+
['shell', 'npm test'], ['shell', 'npm run check'], ['read', 'docs/design.md']
|
|
14
|
+
];
|
|
15
|
+
const state = { sessions, messages, active: { ses_demo1: { type: 'running' }, ses_demo2: { type: 'running' } }, approvals: [], health: { ok: true, status: 200 }, version: '2.0.32', connection: 'connected', cutoff: () => clock() - 86400000,
|
|
16
|
+
start: async () => {
|
|
17
|
+
if (timer !== undefined) return;
|
|
18
|
+
timer = interval(() => {
|
|
19
|
+
tick++;
|
|
20
|
+
const time = clock();
|
|
21
|
+
state.active = {};
|
|
22
|
+
agents.forEach((agent, i) => {
|
|
23
|
+
const id = `ses_demo${i}`, previous = sessions.get(id);
|
|
24
|
+
const working = i === 0 || (tick + i) % 4 !== 0;
|
|
25
|
+
if (working) state.active[id] = { type: 'running' };
|
|
26
|
+
sessions.set(id, { ...previous, time: { ...previous.time, updated: time }, tokens: { ...previous.tokens, output: previous.tokens.output + 24 } });
|
|
27
|
+
// Replace arrays/objects to invalidate the BFF's identity-based caches.
|
|
28
|
+
const history = messages.get(id).map(message => ({ ...message, time: { ...message.time, completed: time }, content: message.content.map(tool => ({ ...tool, state: { ...tool.state, status: 'completed' }, time: { ...tool.time, completed: time } })) }));
|
|
29
|
+
const [name, path] = tools[(tick + i) % tools.length];
|
|
30
|
+
const tool = { id: `call_demo${i}_${tick}`, type: 'tool', name, state: { status: working ? 'running' : 'completed', input: { path } }, time: { created: time, ...(working ? {} : { completed: time }) } };
|
|
31
|
+
history.push({ id: `msg_demo${i}_${tick}`, type: 'assistant', model: previous.model, time: { created: time }, tokens: { input: 12000, output: 1200 + tick % 100, cache: { read: 48000, write: 0 } }, content: [tool], finish: 'tool-calls' });
|
|
32
|
+
messages.set(id, history.slice(-12));
|
|
33
|
+
});
|
|
34
|
+
onChange();
|
|
35
|
+
}, 1500);
|
|
36
|
+
timer.unref?.();
|
|
37
|
+
},
|
|
38
|
+
stop() { if (timer !== undefined) cancel(timer); timer = undefined; },
|
|
39
|
+
ensureTree: async () => {}
|
|
40
|
+
};
|
|
41
|
+
return state;
|
|
42
|
+
}
|
package/lib/privacy.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Best-effort display redaction, not a security boundary.
|
|
2
|
+
export function clip(value, limit = 180) {
|
|
3
|
+
return String(value ?? '')
|
|
4
|
+
.replace(/\bBearer\s+\S+/gi, 'Bearer [redacted]')
|
|
5
|
+
.replace(/(-u\s+)\S+:\S+/g, '$1[redacted]')
|
|
6
|
+
.replace(/((?:--)?(?:password|token|secret|authorization|api[_-]?key)["']?\s*(?:[:=]\s*|\s+))["']?[^\s,;"'}]+["']?/gi, '$1[redacted]')
|
|
7
|
+
.replace(/\b(?:sk-[\w-]+|ghp_[\w]+|github_pat_[\w_]+|xox[bp]-[\w-]+|AKIA[A-Z0-9]+)\b/g, '[redacted]')
|
|
8
|
+
.slice(0, limit);
|
|
9
|
+
}
|