opencode-agent-flow 0.1.0 → 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
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # opencode-agent-flow
2
2
 
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
+
3
10
  A live, read-only visualization of OpenCode V2 orchestration. See agent trees,
4
11
  grouped parallel calls, current tools, approval waits, token usage and estimated
5
12
  costs in one WebGL dashboard. Connect to a local or remote OpenCode server using
@@ -7,23 +14,79 @@ only its public HTTP API and SSE—no database access required.
7
14
 
8
15
  Zero runtime npm dependencies. Node.js 20+; no frontend build step.
9
16
 
10
- ![Agent Flow demo dashboard](docs/screenshot.png)
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.
11
20
 
12
21
  ## Quick start
13
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
+
14
43
  ```sh
15
- npx opencode-agent-flow
16
- # Protected server: supply the password through your environment, not CLI flags.
17
- OPENCODE_SERVER_PASSWORD="$YOUR_OPENCODE_PASSWORD" npx opencode-agent-flow
18
- # Remote server
19
- OPENCODE_SERVER_PASSWORD="$YOUR_OPENCODE_PASSWORD" npx opencode-agent-flow --server https://opencode.example.org
20
- # Fictional data, no server required
21
- npx opencode-agent-flow --demo --open
44
+ npx opencode-agent-flow --server https://opencode.example.org --open
22
45
  ```
23
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
+
24
53
  Open <http://127.0.0.1:3340>. For an unpublished local checkout:
25
54
  `node bin/cli.mjs --demo`.
26
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
+
27
90
  ## Options
28
91
 
29
92
  | Flag | Environment fallback | Default |
@@ -57,7 +120,19 @@ individual-call popovers preserve the orchestration hierarchy.
57
120
  including the root. The recent activity window applies to root selection; explicit
58
121
  session mode loads the complete selected tree, including dormant children.
59
122
 
60
- ## Security and compatibility
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
61
136
 
62
137
  - Loopback binding by default. **The UI has no authentication**; exposing it on
63
138
  another interface reveals session titles, paths and activity. Use an authenticated
@@ -70,8 +145,6 @@ session mode loads the complete selected tree, including dormant children.
70
145
  **best-effort**, not a guarantee. Do not expose it to a network without a
71
146
  reverse proxy providing authentication. Report vulnerabilities via
72
147
  [GitHub Security Advisories](https://github.com/orybakov/opencode-agent-flow/security/advisories/new).
73
- - Tested against **OpenCode 2.0.30**. The V2 HTTP API is **experimental**; future
74
- changes may require adapter updates. Non-V2 servers remain in connecting state.
75
148
  - SSE is live-only, without replay. Reconciliation recovers durable snapshots,
76
149
  not every transient event. No historical permission-decision journal is available.
77
150
  - Token/cost panels use available history (at least 12 hours/current day for
@@ -87,7 +160,8 @@ subscriptions may differ, and these figures are not billing statements.
87
160
 
88
161
  ```sh
89
162
  node bin/cli.mjs --demo
90
- node --test
163
+ node scripts/check-syntax.mjs
164
+ npm test
91
165
  npm pack --dry-run
92
166
  ```
93
167
 
@@ -95,6 +169,13 @@ See [architecture](docs/architecture.md) and
95
169
  [public API notes](docs/opencode-v2-api-notes.md). Vendored browser modules avoid
96
170
  CDN/network dependencies; there is no install or bundling step.
97
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
+
98
179
  ## License and acknowledgements
99
180
 
100
181
  MIT © 2026 orybakov. WebGL rendering uses [three.js](https://threejs.org/),
package/bin/cli.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { start } from '../server.mjs';
4
5
  process.on('unhandledRejection', error => console.error('Unhandled rejection:', error?.name || 'Error'));
5
6
  process.on('uncaughtException', error => console.error('Uncaught exception:', error?.name || 'Error'));
6
7
 
7
- const help = `opencode-agent-flow 0.1.0 — live OpenCode V2 agent visualization
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
8
10
 
9
11
  Usage: npx opencode-agent-flow [options]
10
12
  --server URL OpenCode V2 URL (OPENCODE_URL; http://127.0.0.1:4096)
@@ -1,6 +1,6 @@
1
1
  # OpenCode V2 public API: contributor notes
2
2
 
3
- Contract verified against OpenCode **2.0.30**. The server describes this HTTP API
3
+ Contract verified against OpenCode **2.0.30** and clean **2.0.32**. The server describes this HTTP API
4
4
  as experimental. Version checks reject non-V2 servers but do not guarantee that
5
5
  every future V2 revision preserves this contract.
6
6
 
@@ -84,6 +84,38 @@ messages, location-scoped pending requests, Basic authentication and a live glob
84
84
  stream containing multiple locations. Remote deployments and future versions
85
85
  require additional verification; capability changes should fail clearly.
86
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
+
87
119
  Sources: [API](https://opencode.ai/v2/docs/api),
88
120
  [client](https://opencode.ai/v2/docs/build/client), published
89
121
  `@opencode/client@2.0.30` event types, and live read-only HTTP/SSE observations.
package/lib/demo.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export function demo() {
1
+ export function demo(onChange = () => {}, { interval = setInterval, cancel = clearInterval, clock = Date.now } = {}) {
2
2
  const now = Date.now(), sessions = new Map(), messages = new Map();
3
3
  const agents = ['lead', 'builder', 'researcher', 'reviewer', 'builder', 'tester', 'designer'];
4
4
  agents.forEach((agent, i) => {
@@ -6,5 +6,37 @@ export function demo() {
6
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
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
8
  });
9
- return { sessions, messages, active: { ses_demo1: { type: 'running' }, ses_demo2: { type: 'running' } }, approvals: [], health: { ok: true, status: 200 }, version: '2.0.30', connection: 'connected', cutoff: () => now - 86400000, start: async () => {}, stop() {}, ensureTree: async () => {} };
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;
10
42
  }
package/lib/upstream.mjs CHANGED
@@ -167,7 +167,7 @@ export class Upstream {
167
167
  void this.initialize();
168
168
  }
169
169
  async initialize() {
170
- let delay = 1000;
170
+ let delay = 1000, lastError;
171
171
  while (!this.abort.signal.aborted) {
172
172
  try {
173
173
  let info;
@@ -175,11 +175,15 @@ export class Upstream {
175
175
  this.version = info.version || info.data?.version;
176
176
  if (!/^2\./.test(this.version || '')) throw new Error('Unsupported server: OpenCode V2 /api/info version 2.x is required.');
177
177
  await this.sync(); if (this.abort.signal.aborted) return; void this.stream();
178
+ console.log(`Connected to OpenCode ${this.version}.`);
178
179
  this.syncTimer = setInterval(() => { void this.sync().catch(() => console.error('OpenCode reconciliation failed; retrying in 60s.')); }, 60000);
179
180
  this.pendingTimer = setInterval(() => { void this.pollPending(); }, 5000);
180
181
  return;
181
- } catch {
182
+ } catch (error) {
182
183
  if (this.abort.signal.aborted) return;
184
+ // Keep retrying, but do not silently hide authentication/version failures.
185
+ const message = error.message;
186
+ if (message !== lastError) { console.error(message); lastError = message; }
183
187
  this.connection = 'connecting'; this.changed();
184
188
  await new Promise(resolve => { const finish = () => { clearTimeout(timer); this.abort.signal.removeEventListener('abort', finish); resolve(); }; const timer = setTimeout(finish, delay); this.abort.signal.addEventListener('abort', finish, { once: true }); });
185
189
  delay = Math.min(delay * 2, 30000);
package/package.json CHANGED
@@ -1,17 +1,48 @@
1
1
  {
2
2
  "name": "opencode-agent-flow",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Live agent flow visualization for OpenCode V2 over its public HTTP API and SSE",
5
5
  "type": "module",
6
- "bin": { "opencode-agent-flow": "bin/cli.mjs" },
7
- "engines": { "node": ">=20" },
6
+ "bin": {
7
+ "opencode-agent-flow": "bin/cli.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
8
12
  "license": "MIT",
9
13
  "author": "orybakov (https://github.com/orybakov)",
10
- "repository": { "type": "git", "url": "https://github.com/orybakov/opencode-agent-flow.git" },
11
- "bugs": { "url": "https://github.com/orybakov/opencode-agent-flow/issues" },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/orybakov/opencode-agent-flow.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/orybakov/opencode-agent-flow/issues"
20
+ },
12
21
  "homepage": "https://github.com/orybakov/opencode-agent-flow#readme",
13
- "keywords": ["opencode", "ai-agents", "visualizer", "observability", "webgl"],
22
+ "keywords": [
23
+ "opencode",
24
+ "ai-agents",
25
+ "visualizer",
26
+ "observability",
27
+ "webgl"
28
+ ],
14
29
  "exports": {},
15
- "files": ["bin", "server.mjs", "lib", "public", "prices.json", "README.md", "LICENSE", "SECURITY.md", "CONTRIBUTING.md", "docs/*.md", "docs/screenshot.png"],
16
- "scripts": { "start": "node bin/cli.mjs", "test": "node --test" }
30
+ "files": [
31
+ "bin",
32
+ "server.mjs",
33
+ "lib",
34
+ "public",
35
+ "prices.json",
36
+ "README.md",
37
+ "LICENSE",
38
+ "SECURITY.md",
39
+ "CONTRIBUTING.md",
40
+ "CHANGELOG.md",
41
+ "docs/*.md",
42
+ "docs/screenshot.png"
43
+ ],
44
+ "scripts": {
45
+ "start": "node bin/cli.mjs",
46
+ "test": "node --test"
47
+ }
17
48
  }
package/server.mjs CHANGED
@@ -15,7 +15,7 @@ const json = s => { try { return JSON.parse(s || '{}'); } catch { return {}; } }
15
15
  const modelId = m => clip((typeof m === 'string' ? m : m?.id || m?.modelID) || 'unknown', 100);
16
16
  const rate = m => prices.models[m] || Object.entries(prices.models).find(([k]) => m.startsWith(k))?.[1] || prices.default;
17
17
  let snapshot, signature = '', timer;
18
- const upstream = config.demo ? demo() : new Upstream(config, schedule);
18
+ const upstream = config.demo ? demo(schedule) : new Upstream(config, schedule);
19
19
  let approvals = [], permissionHealth = { ok: false, status: null };
20
20
  const clients = new Set();
21
21
  const rowCache = new Map();