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 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
@@ -0,0 +1,3 @@
1
+ # Contributing
2
+
3
+ Open an issue to discuss substantial changes. Keep pull requests focused, add regression tests, and run `node --test` on Node 20 and a current Node release. Never commit credentials or real session data. See SECURITY.md for private vulnerability reports.
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
- # Temporary Holding Version
1
+ # opencode-agent-flow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/opencode-agent-flow)](https://www.npmjs.com/package/opencode-agent-flow)
4
+ [![CI](https://github.com/orybakov/opencode-agent-flow/actions/workflows/ci.yml/badge.svg)](https://github.com/orybakov/opencode-agent-flow/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Node.js](https://img.shields.io/node/v/opencode-agent-flow)](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
+ ![Live Agent Flow demo: session tree, active tools and animated token transport](https://raw.githubusercontent.com/orybakov/opencode-agent-flow/main/docs/demo.gif)
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
+ }
@@ -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
+ }