@polderlabs/bizar-dash 3.0.0

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.
Files changed (59) hide show
  1. package/dist/assets/index-B5X9g8B4.css +1 -0
  2. package/dist/assets/index-LqQuSp9d.js +388 -0
  3. package/dist/assets/index-LqQuSp9d.js.map +1 -0
  4. package/dist/index.html +18 -0
  5. package/package.json +67 -0
  6. package/src/cli.mjs +228 -0
  7. package/src/server/agents-store.mjs +190 -0
  8. package/src/server/api.mjs +913 -0
  9. package/src/server/browser.mjs +40 -0
  10. package/src/server/diagnostics-store.mjs +138 -0
  11. package/src/server/mods-loader.mjs +361 -0
  12. package/src/server/projects-store.mjs +198 -0
  13. package/src/server/providers-store.mjs +183 -0
  14. package/src/server/schedules-runner.mjs +150 -0
  15. package/src/server/schedules-store.mjs +233 -0
  16. package/src/server/search-store.mjs +120 -0
  17. package/src/server/server.mjs +388 -0
  18. package/src/server/state.mjs +357 -0
  19. package/src/server/tailscale-store.mjs +113 -0
  20. package/src/server/tasks-store.mjs +275 -0
  21. package/src/server/tui.mjs +844 -0
  22. package/src/server/watcher.mjs +81 -0
  23. package/src/web/App.tsx +316 -0
  24. package/src/web/components/Button.tsx +55 -0
  25. package/src/web/components/Card.tsx +40 -0
  26. package/src/web/components/EmptyState.tsx +30 -0
  27. package/src/web/components/Modal.tsx +137 -0
  28. package/src/web/components/SearchModal.tsx +185 -0
  29. package/src/web/components/Spinner.tsx +19 -0
  30. package/src/web/components/StatusBadge.tsx +25 -0
  31. package/src/web/components/Tag.tsx +28 -0
  32. package/src/web/components/Toast.tsx +142 -0
  33. package/src/web/components/Topbar.tsx +203 -0
  34. package/src/web/index.html +17 -0
  35. package/src/web/lib/api.ts +71 -0
  36. package/src/web/lib/markdown.tsx +59 -0
  37. package/src/web/lib/types.ts +388 -0
  38. package/src/web/lib/utils.ts +79 -0
  39. package/src/web/lib/ws.ts +132 -0
  40. package/src/web/main.tsx +12 -0
  41. package/src/web/styles/main.css +3148 -0
  42. package/src/web/views/Agents.tsx +406 -0
  43. package/src/web/views/Chat.tsx +527 -0
  44. package/src/web/views/Config.tsx +683 -0
  45. package/src/web/views/Mods.tsx +350 -0
  46. package/src/web/views/Overview.tsx +350 -0
  47. package/src/web/views/Plans.tsx +667 -0
  48. package/src/web/views/Schedules.tsx +299 -0
  49. package/src/web/views/Settings.tsx +571 -0
  50. package/src/web/views/Tasks.tsx +761 -0
  51. package/templates/mod/FORMAT.md +76 -0
  52. package/templates/mod/hello-mod/README.md +19 -0
  53. package/templates/mod/hello-mod/agents/greeter.md +8 -0
  54. package/templates/mod/hello-mod/commands/hello.md +6 -0
  55. package/templates/mod/hello-mod/mod.json +20 -0
  56. package/templates/mod/hello-mod/routes/ping.mjs +9 -0
  57. package/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  58. package/tsconfig.json +23 -0
  59. package/vite.config.ts +24 -0
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Bizar Dashboard</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🪩</text></svg>" />
8
+ <link rel="preconnect" href="https://rsms.me/" />
9
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
12
+ <script type="module" crossorigin src="./assets/index-LqQuSp9d.js"></script>
13
+ <link rel="stylesheet" crossorigin href="./assets/index-B5X9g8B4.css">
14
+ </head>
15
+ <body>
16
+ <div id="root"></div>
17
+ </body>
18
+ </html>
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@polderlabs/bizar-dash",
3
+ "version": "3.0.0",
4
+ "description": "Web + TUI dashboard for the Bizar agent platform (optional companion to @polderlabs/bizar)",
5
+ "type": "module",
6
+ "bin": {
7
+ "bizar-dash": "src/cli.mjs"
8
+ },
9
+ "main": "src/server/server.mjs",
10
+ "files": [
11
+ "src/",
12
+ "dist/",
13
+ "vite.config.ts",
14
+ "tsconfig.json",
15
+ "templates/"
16
+ ],
17
+ "scripts": {
18
+ "dev": "vite",
19
+ "build": "vite build",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "blessed": "^0.1.81",
24
+ "chokidar": "^3.6.0",
25
+ "express": "^4.22.0",
26
+ "fuse.js": "^7.0.0",
27
+ "lucide-react": "^0.460.0",
28
+ "react": "^18.3.0",
29
+ "react-dom": "^18.3.0",
30
+ "react-markdown": "^9.0.0",
31
+ "remark-gfm": "^4.0.0",
32
+ "ws": "^8.18.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^18.3.0",
36
+ "@types/react-dom": "^18.3.0",
37
+ "@vitejs/plugin-react": "^4.3.0",
38
+ "typescript": "^5.5.0",
39
+ "vite": "^5.4.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@polderlabs/bizar": ">=2.7.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "@polderlabs/bizar-dash": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "engines": {
50
+ "node": ">=20"
51
+ },
52
+ "license": "MIT",
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/DrB0rk/BizarHarness.git"
59
+ },
60
+ "keywords": [
61
+ "opencode",
62
+ "ai-agent",
63
+ "dashboard",
64
+ "bizar",
65
+ "norse"
66
+ ]
67
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * src/cli.mjs
4
+ *
5
+ * v3.0.0 — `bizar-dash` command.
6
+ *
7
+ * Subcommands:
8
+ * start — launch dashboard (default)
9
+ * stop — kill the running dashboard
10
+ * status — print port + URL of any running dashboard
11
+ * tui — run the TUI dashboard
12
+ * --no-web — TUI only (no browser)
13
+ * --web-only — web only
14
+ * --bg, --detach — start in background and return
15
+ */
16
+ import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+ import { spawn } from 'node:child_process';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { dirname } from 'node:path';
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+ const BIZAR_HOME = join(homedir(), '.config', 'bizar');
26
+ const PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
27
+ const PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
28
+ const DEFAULT_PORT = 4321;
29
+
30
+ function showHelp() {
31
+ console.log(`
32
+ bizar-dash — Web + TUI dashboard for the Bizar agent platform
33
+
34
+ Usage:
35
+ bizar-dash Launch the dashboard (default = web)
36
+ bizar-dash start Start the dashboard in this process
37
+ bizar-dash stop Kill the running dashboard
38
+ bizar-dash status Show port + URL of any running dashboard
39
+ bizar-dash tui [--no-web] Run the TUI dashboard
40
+ bizar-dash --bg, --detach Start in background, return to shell
41
+ bizar-dash --web-only Web dashboard only (no TUI)
42
+ bizar-dash --no-web TUI only (no browser)
43
+ bizar-dash --help Show this help
44
+
45
+ Notes:
46
+ The dashboard reads your opencode config at
47
+ ~/.config/opencode/. The per-project state lives in
48
+ ~/.config/opencode/projects/<id>/. Mods are installed to
49
+ ~/.config/bizar/mods/.
50
+
51
+ Install:
52
+ npm install -g @polderlabs/bizar-dash
53
+ npm install -g @polderlabs/bizar # required peer
54
+ `);
55
+ }
56
+
57
+ async function findFreePort(preferred) {
58
+ const net = await import('node:net');
59
+ for (let p = preferred; p < preferred + 100; p++) {
60
+ if (await isPortFree(net, p)) return p;
61
+ }
62
+ throw new Error(`no free port in range ${preferred}..${preferred + 99}`);
63
+ }
64
+
65
+ function isPortFree(net, port) {
66
+ return new Promise((resolve) => {
67
+ const server = net.createServer();
68
+ let settled = false;
69
+ const finish = (ok) => {
70
+ if (settled) return;
71
+ settled = true;
72
+ resolve(ok);
73
+ };
74
+ server.once('error', () => finish(false));
75
+ server.once('listening', () => server.close(() => finish(true)));
76
+ const t = setTimeout(() => finish(false), 1000);
77
+ server.listen(port, '127.0.0.1', () => clearTimeout(t));
78
+ });
79
+ }
80
+
81
+ async function startDashboard({ port, projectRoot, opencodeConfigDir, bizarRoot } = {}) {
82
+ const { createServer } = await import('./server/server.mjs');
83
+ const { launchBrowser } = await import('./server/browser.mjs');
84
+
85
+ const usePort = port || (await findFreePort(DEFAULT_PORT));
86
+ const { server, close } = await createServer({
87
+ port: usePort,
88
+ projectRoot: projectRoot || process.cwd(),
89
+ opencodeConfigDir: opencodeConfigDir || join(homedir(), '.config', 'opencode'),
90
+ bizarRoot: bizarRoot || join(__dirname, '..', '..'),
91
+ });
92
+
93
+ await new Promise((resolve, reject) => {
94
+ server.once('error', reject);
95
+ server.listen(usePort, '127.0.0.1', () => {
96
+ server.off('error', reject);
97
+ resolve();
98
+ });
99
+ });
100
+
101
+ mkdirSync(BIZAR_HOME, { recursive: true });
102
+ writeFileSync(PORT_FILE, String(usePort), 'utf8');
103
+ writeFileSync(PID_FILE, String(process.pid), 'utf8');
104
+
105
+ const url = `http://localhost:${usePort}/`;
106
+ await launchBrowser(url);
107
+ console.log(`Bizar dashboard: ${url}`);
108
+ console.log('Press Ctrl-C to stop the dashboard.');
109
+
110
+ await new Promise(() => {});
111
+ // Caller keeps the process alive
112
+ return { url, port: usePort, close };
113
+ }
114
+
115
+ async function startInBackground(args) {
116
+ const binPath = join(__dirname, 'cli.mjs');
117
+ const child = spawn(process.execPath, [binPath, ...args], {
118
+ detached: true,
119
+ stdio: 'ignore',
120
+ cwd: process.cwd(),
121
+ env: process.env,
122
+ });
123
+ child.on('error', (err) => {
124
+ console.error(`Failed to start background dashboard: ${err.message}`);
125
+ });
126
+ child.unref();
127
+ await new Promise((resolve) => setTimeout(resolve, 1500));
128
+ if (existsSync(PORT_FILE)) {
129
+ const port = readFileSync(PORT_FILE, 'utf8').trim();
130
+ console.log(`Bizar dashboard started in background on http://localhost:${port}/`);
131
+ console.log(`Use 'bizar-dash status' to check, 'bizar-dash stop' to stop.`);
132
+ } else {
133
+ console.log('Bizar dashboard starting in background (port file not yet written)...');
134
+ }
135
+ }
136
+
137
+ async function stopDashboard() {
138
+ if (!existsSync(PID_FILE)) {
139
+ console.log('No Bizar dashboard is running.');
140
+ return;
141
+ }
142
+ const pid = parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10);
143
+ if (!Number.isFinite(pid)) {
144
+ console.log(`Bad PID file: ${PID_FILE}`);
145
+ return;
146
+ }
147
+ try {
148
+ process.kill(pid, 'SIGTERM');
149
+ console.log(`Stopped Bizar dashboard (pid ${pid}).`);
150
+ } catch (err) {
151
+ console.log(`Could not stop dashboard (pid ${pid}): ${err.message}`);
152
+ }
153
+ try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
154
+ try { unlinkSync(PID_FILE); } catch { /* ignore */ }
155
+ }
156
+
157
+ function showStatus() {
158
+ if (existsSync(PORT_FILE)) {
159
+ const port = readFileSync(PORT_FILE, 'utf8').trim();
160
+ console.log(`Bizar dashboard is running at http://localhost:${port}/`);
161
+ if (existsSync(PID_FILE)) {
162
+ console.log(`PID: ${readFileSync(PID_FILE, 'utf8').trim()}`);
163
+ }
164
+ } else {
165
+ console.log('No Bizar dashboard is running. Use: bizar-dash start');
166
+ }
167
+ }
168
+
169
+ async function runTui({ launchWeb } = {}) {
170
+ const { createServer } = await import('./server/server.mjs');
171
+ const { launchBrowser } = await import('./server/browser.mjs');
172
+ const { launchTui } = await import('./server/tui.mjs');
173
+
174
+ const port = await findFreePort(DEFAULT_PORT);
175
+ const { server, close: closeServer } = await createServer({
176
+ port,
177
+ projectRoot: process.cwd(),
178
+ opencodeConfigDir: join(homedir(), '.config', 'opencode'),
179
+ bizarRoot: join(__dirname, '..', '..'),
180
+ });
181
+ await new Promise((resolve, reject) => {
182
+ server.once('error', reject);
183
+ server.listen(port, '127.0.0.1', () => {
184
+ server.off('error', reject);
185
+ resolve();
186
+ });
187
+ });
188
+ mkdirSync(BIZAR_HOME, { recursive: true });
189
+ writeFileSync(PORT_FILE, String(port), 'utf8');
190
+ writeFileSync(PID_FILE, String(process.pid), 'utf8');
191
+
192
+ if (launchWeb) {
193
+ const url = `http://localhost:${port}/`;
194
+ launchBrowser(url).catch(() => {});
195
+ console.log(`Web dashboard: ${url}`);
196
+ }
197
+
198
+ try {
199
+ await launchTui({ port });
200
+ } finally {
201
+ try { closeServer(); } catch { /* ignore */ }
202
+ try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
203
+ try { unlinkSync(PID_FILE); } catch { /* ignore */ }
204
+ }
205
+ }
206
+
207
+ const args = process.argv.slice(2);
208
+
209
+ if (args.includes('--help') || args.includes('-h')) {
210
+ showHelp();
211
+ } else if (args[0] === 'stop') {
212
+ await stopDashboard();
213
+ } else if (args[0] === 'status') {
214
+ showStatus();
215
+ } else if (args[0] === 'tui') {
216
+ const rest = args.slice(1);
217
+ const skipWeb = rest.includes('--no-web');
218
+ await runTui({ launchWeb: !skipWeb });
219
+ } else if (args.includes('--bg') || args.includes('--detach')) {
220
+ await startInBackground(['start']);
221
+ } else if (args.includes('--web-only')) {
222
+ await startDashboard();
223
+ } else if (args[0] === 'start' || args.length === 0) {
224
+ await startDashboard();
225
+ } else {
226
+ // Default: show help if we don't understand
227
+ showHelp();
228
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * src/server/agents-store.mjs
3
+ *
4
+ * v3.0.0 — Editable agents.
5
+ *
6
+ * Each agent is a markdown file with frontmatter at:
7
+ * ~/.config/opencode/agents/<name>.md
8
+ *
9
+ * The store reads / writes these files. The format is:
10
+ * ---
11
+ * description: ...
12
+ * model: ...
13
+ * mode: ...
14
+ * color: ...
15
+ * tools: ["bash","read","edit"]
16
+ * permissions: {...}
17
+ * ---
18
+ * <prompt body>
19
+ */
20
+ import {
21
+ existsSync,
22
+ readFileSync,
23
+ writeFileSync,
24
+ readdirSync,
25
+ statSync,
26
+ mkdirSync,
27
+ unlinkSync,
28
+ } from 'node:fs';
29
+ import { join, dirname, basename } from 'node:path';
30
+ import { homedir } from 'node:os';
31
+
32
+ const HOME = homedir();
33
+ const AGENTS_DIR = join(HOME, '.config', 'opencode', 'agents');
34
+
35
+ function safeReadText(file, fallback = '') {
36
+ try {
37
+ if (!existsSync(file)) return fallback;
38
+ return readFileSync(file, 'utf8');
39
+ } catch {
40
+ return fallback;
41
+ }
42
+ }
43
+
44
+ function parseFrontmatter(raw) {
45
+ if (!raw.startsWith('---')) return { frontmatter: {}, body: raw };
46
+ const end = raw.indexOf('\n---', 3);
47
+ if (end === -1) return { frontmatter: {}, body: raw };
48
+ const fmBlock = raw.slice(3, end).trim();
49
+ const body = raw.slice(end + 4).replace(/^\s+/, '');
50
+ const frontmatter = {};
51
+ for (const line of fmBlock.split(/\r?\n/)) {
52
+ const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
53
+ if (!m) continue;
54
+ const key = m[1];
55
+ let val = m[2].trim();
56
+ if (
57
+ (val.startsWith('"') && val.endsWith('"')) ||
58
+ (val.startsWith("'") && val.endsWith("'"))
59
+ ) {
60
+ val = val.slice(1, -1);
61
+ }
62
+ frontmatter[key] = val;
63
+ }
64
+ return { frontmatter, body };
65
+ }
66
+
67
+ /** Serialize a frontmatter key — string is bare, value with special chars gets quoted. */
68
+ function fmVal(v) {
69
+ if (typeof v !== 'string') return JSON.stringify(v);
70
+ if (v === '') return '""';
71
+ if (/[:#\-?{}[\],&*!|>'"%@`]/.test(v) || v.includes(' ')) {
72
+ return JSON.stringify(v);
73
+ }
74
+ return v;
75
+ }
76
+
77
+ function serializeFrontmatter(fm) {
78
+ const lines = ['---'];
79
+ for (const [k, v] of Object.entries(fm)) {
80
+ if (v == null) continue;
81
+ if (Array.isArray(v)) {
82
+ lines.push(`${k}: [${v.map((x) => (typeof x === 'string' ? JSON.stringify(x) : x)).join(', ')}]`);
83
+ } else if (typeof v === 'object') {
84
+ lines.push(`${k}:`);
85
+ for (const [k2, v2] of Object.entries(v)) {
86
+ lines.push(` ${k2}: ${fmVal(String(v2))}`);
87
+ }
88
+ } else {
89
+ lines.push(`${k}: ${fmVal(String(v))}`);
90
+ }
91
+ }
92
+ lines.push('---');
93
+ return lines.join('\n') + '\n';
94
+ }
95
+
96
+ function readAgent(name) {
97
+ const file = join(AGENTS_DIR, `${name}.md`);
98
+ if (!existsSync(file)) return null;
99
+ const raw = safeReadText(file);
100
+ const { frontmatter, body } = parseFrontmatter(raw);
101
+ const st = statSync(file);
102
+ return {
103
+ name,
104
+ description: frontmatter.description || '',
105
+ model: frontmatter.model || '',
106
+ mode: frontmatter.mode || 'subagent',
107
+ color: frontmatter.color || '',
108
+ tools: typeof frontmatter.tools === 'string'
109
+ ? frontmatter.tools.split(',').map((s) => s.trim()).filter(Boolean)
110
+ : Array.isArray(frontmatter.tools) ? frontmatter.tools : [],
111
+ permissions: frontmatter.permissions || null,
112
+ prompt: body.trim(),
113
+ file,
114
+ path: file,
115
+ mtime: st.mtimeMs,
116
+ };
117
+ }
118
+
119
+ export const agentsStore = {
120
+ AGENTS_DIR,
121
+
122
+ ensure() {
123
+ mkdirSync(AGENTS_DIR, { recursive: true });
124
+ },
125
+
126
+ list() {
127
+ this.ensure();
128
+ const out = [];
129
+ for (const f of readdirSync(AGENTS_DIR)) {
130
+ if (!f.endsWith('.md')) continue;
131
+ const name = basename(f, '.md');
132
+ const agent = readAgent(name);
133
+ if (agent) out.push(agent);
134
+ }
135
+ out.sort((a, b) => a.name.localeCompare(b.name));
136
+ return out;
137
+ },
138
+
139
+ get(name) {
140
+ return readAgent(name);
141
+ },
142
+
143
+ create(input) {
144
+ if (!input || typeof input !== 'object') throw new Error('agent input required');
145
+ if (!input.name || !/^[a-z0-9][a-z0-9-]{0,63}$/i.test(input.name)) {
146
+ throw new Error('invalid agent name');
147
+ }
148
+ const file = join(AGENTS_DIR, `${input.name}.md`);
149
+ if (existsSync(file)) {
150
+ throw new Error(`agent "${input.name}" already exists`);
151
+ }
152
+ const data = {
153
+ name: input.name,
154
+ description: input.description || '',
155
+ model: input.model || '',
156
+ mode: input.mode || 'subagent',
157
+ color: input.color || '',
158
+ tools: Array.isArray(input.tools) ? input.tools : [],
159
+ permissions: input.permissions || null,
160
+ prompt: input.prompt || '',
161
+ };
162
+ this.ensure();
163
+ writeFileSync(file, serializeFrontmatter(data) + '\n' + data.prompt, 'utf8');
164
+ return readAgent(input.name);
165
+ },
166
+
167
+ update(name, patch) {
168
+ if (!patch || typeof patch !== 'object') throw new Error('agent patch required');
169
+ const cur = readAgent(name);
170
+ if (!cur) throw new Error(`agent "${name}" not found`);
171
+ const data = {
172
+ description: patch.description ?? cur.description,
173
+ model: patch.model ?? cur.model,
174
+ mode: patch.mode ?? cur.mode,
175
+ color: patch.color ?? cur.color,
176
+ tools: Array.isArray(patch.tools) ? patch.tools : cur.tools,
177
+ permissions: patch.permissions ?? cur.permissions,
178
+ prompt: patch.prompt ?? cur.prompt,
179
+ };
180
+ writeFileSync(cur.file, serializeFrontmatter(data) + '\n' + data.prompt, 'utf8');
181
+ return readAgent(name);
182
+ },
183
+
184
+ delete(name) {
185
+ const file = join(AGENTS_DIR, `${name}.md`);
186
+ if (!existsSync(file)) return false;
187
+ unlinkSync(file);
188
+ return true;
189
+ },
190
+ };