@polderlabs/bizar 2.6.0 → 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 (44) hide show
  1. package/cli/bin.mjs +158 -130
  2. package/cli/copy.mjs +39 -34
  3. package/cli/plan.test.mjs +2331 -0
  4. package/cli/service.mjs +309 -0
  5. package/package.json +19 -27
  6. package/cli/dashboard/api.mjs +0 -473
  7. package/cli/dashboard/browser.mjs +0 -40
  8. package/cli/dashboard/server.mjs +0 -366
  9. package/cli/dashboard/state.mjs +0 -438
  10. package/cli/dashboard/tasks-store.mjs +0 -203
  11. package/cli/dashboard/watcher.mjs +0 -81
  12. package/cli/dashboard.mjs +0 -97
  13. package/dist/assets/index-BVvY22Gt.css +0 -1
  14. package/dist/assets/index-CO3c8O32.js +0 -285
  15. package/dist/assets/index-CO3c8O32.js.map +0 -1
  16. package/dist/index.html +0 -18
  17. package/src/App.tsx +0 -233
  18. package/src/components/Button.tsx +0 -55
  19. package/src/components/Card.tsx +0 -40
  20. package/src/components/EmptyState.tsx +0 -30
  21. package/src/components/Modal.tsx +0 -137
  22. package/src/components/Spinner.tsx +0 -19
  23. package/src/components/StatusBadge.tsx +0 -25
  24. package/src/components/Tag.tsx +0 -28
  25. package/src/components/Toast.tsx +0 -142
  26. package/src/components/Topbar.tsx +0 -88
  27. package/src/index.html +0 -17
  28. package/src/lib/api.ts +0 -71
  29. package/src/lib/markdown.tsx +0 -59
  30. package/src/lib/types.ts +0 -200
  31. package/src/lib/utils.ts +0 -79
  32. package/src/lib/ws.ts +0 -132
  33. package/src/main.tsx +0 -12
  34. package/src/styles/main.css +0 -2324
  35. package/src/views/Agents.tsx +0 -199
  36. package/src/views/Chat.tsx +0 -255
  37. package/src/views/Config.tsx +0 -250
  38. package/src/views/Overview.tsx +0 -267
  39. package/src/views/Plans.tsx +0 -667
  40. package/src/views/Projects.tsx +0 -155
  41. package/src/views/Settings.tsx +0 -253
  42. package/src/views/Tasks.tsx +0 -567
  43. package/tsconfig.json +0 -23
  44. package/vite.config.ts +0 -24
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli/service.mjs
4
+ *
5
+ * v3.0.0 — Background service daemon for Bizar.
6
+ *
7
+ * Subcommands:
8
+ * start — spawn the service detached, return immediately
9
+ * stop — kill the service
10
+ * status — show running state
11
+ * logs — tail the service log
12
+ *
13
+ * The service:
14
+ * - Watches per-project schedules
15
+ * - Fires due schedules (interval / cron / once)
16
+ * - Records every run in schedules.json
17
+ * - Logs to ~/.config/bizar/service.log
18
+ * - Writes its PID to ~/.config/bizar/service.pid
19
+ */
20
+ import {
21
+ existsSync,
22
+ readFileSync,
23
+ writeFileSync,
24
+ mkdirSync,
25
+ appendFileSync,
26
+ } from 'node:fs';
27
+ import { join, dirname } from 'node:path';
28
+ import { homedir } from 'node:os';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { spawn } from 'node:child_process';
31
+
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = dirname(__filename);
34
+ const HOME = homedir();
35
+ const BIZAR_HOME = join(HOME, '.config', 'bizar');
36
+ const LOG_FILE = join(BIZAR_HOME, 'service.log');
37
+ const PID_FILE = join(BIZAR_HOME, 'service.pid');
38
+ const TICK_MS = 5_000; // 5s
39
+
40
+ function nowIso() {
41
+ return new Date().toISOString();
42
+ }
43
+
44
+ function ensureDir() {
45
+ mkdirSync(BIZAR_HOME, { recursive: true });
46
+ }
47
+
48
+ function logLine(line) {
49
+ try {
50
+ ensureDir();
51
+ appendFileSync(LOG_FILE, `[${nowIso()}] ${line}\n`, 'utf8');
52
+ } catch {
53
+ /* ignore */
54
+ }
55
+ }
56
+
57
+ function readPid() {
58
+ if (!existsSync(PID_FILE)) return null;
59
+ try {
60
+ const pid = parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10);
61
+ return Number.isFinite(pid) ? pid : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function isAlive(pid) {
68
+ if (!pid) return false;
69
+ try {
70
+ process.kill(pid, 0);
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ async function startService() {
78
+ const existing = readPid();
79
+ if (existing && isAlive(existing)) {
80
+ console.log(`Service already running (pid ${existing}).`);
81
+ return;
82
+ }
83
+ // Spawn detached child
84
+ const child = spawn(process.execPath, [__filename, '_daemon'], {
85
+ detached: true,
86
+ stdio: 'ignore',
87
+ cwd: process.cwd(),
88
+ env: process.env,
89
+ });
90
+ child.on('error', (err) => {
91
+ console.error(`Failed to start service: ${err.message}`);
92
+ });
93
+ child.unref();
94
+ await new Promise((r) => setTimeout(r, 800));
95
+ const pid = readPid();
96
+ if (pid && isAlive(pid)) {
97
+ console.log(`Bizar service started (pid ${pid}).`);
98
+ console.log(`Log: ${LOG_FILE}`);
99
+ } else {
100
+ console.log('Service start command issued, but PID not yet alive. Check the log.');
101
+ }
102
+ }
103
+
104
+ function stopService() {
105
+ const pid = readPid();
106
+ if (!pid) {
107
+ console.log('No Bizar service is running.');
108
+ return;
109
+ }
110
+ if (!isAlive(pid)) {
111
+ console.log(`Stale PID file (pid ${pid} not running). Cleaning up.`);
112
+ try { writeFileSync(PID_FILE, ''); } catch { /* ignore */ }
113
+ return;
114
+ }
115
+ try {
116
+ process.kill(pid, 'SIGTERM');
117
+ console.log(`Stopped Bizar service (pid ${pid}).`);
118
+ } catch (err) {
119
+ console.log(`Could not stop service: ${err.message}`);
120
+ }
121
+ try {
122
+ writeFileSync(PID_FILE, '');
123
+ } catch { /* ignore */ }
124
+ }
125
+
126
+ function serviceStatus() {
127
+ const pid = readPid();
128
+ if (!pid) {
129
+ console.log('Bizar service: stopped (no PID file)');
130
+ return;
131
+ }
132
+ if (!isAlive(pid)) {
133
+ console.log(`Bizar service: stopped (stale PID file: ${pid})`);
134
+ return;
135
+ }
136
+ console.log(`Bizar service: running (pid ${pid})`);
137
+ console.log(`Log: ${LOG_FILE}`);
138
+ }
139
+
140
+ function tailLogs(follow) {
141
+ if (!existsSync(LOG_FILE)) {
142
+ console.log('(no log file yet)');
143
+ return;
144
+ }
145
+ // Read the last 50 lines synchronously; if --follow, switch to a tail
146
+ // loop.
147
+ const text = readFileSync(LOG_FILE, 'utf8');
148
+ const lines = text.split(/\r?\n/).filter(Boolean);
149
+ const tail = lines.slice(-50).join('\n');
150
+ console.log(tail);
151
+ if (follow) {
152
+ console.log('--- following log (Ctrl-C to exit) ---');
153
+ let pos = text.length;
154
+ setInterval(() => {
155
+ try {
156
+ const cur = readFileSync(LOG_FILE, 'utf8');
157
+ if (cur.length > pos) {
158
+ process.stdout.write(cur.slice(pos));
159
+ pos = cur.length;
160
+ }
161
+ } catch {
162
+ /* ignore */
163
+ }
164
+ }, 1000);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * The actual daemon loop. Imports the runner from the bizarre-dash package
170
+ * via a relative path (so this file works without npm packaging).
171
+ */
172
+ async function daemonLoop() {
173
+ try {
174
+ writeFileSync(PID_FILE, String(process.pid), 'utf8');
175
+ } catch (err) {
176
+ console.error(`Cannot write PID file ${PID_FILE}: ${err.message}`);
177
+ process.exit(1);
178
+ }
179
+ logLine(`service started (pid ${process.pid})`);
180
+
181
+ // Resolve the runner module. The daemon lives in the bizar package;
182
+ // the runner lives in the bizarre-dash package. We try a few candidate
183
+ // paths so this works whether bizarre-dash is installed globally,
184
+ // locally, or alongside the source tree (development).
185
+ const candidates = [
186
+ // dev: <repo>/bizar-dash/src/server/schedules-runner.mjs
187
+ join(__dirname, '..', 'bizar-dash', 'src', 'server', 'schedules-runner.mjs'),
188
+ // npm-global fallback
189
+ join(HOME, '.npm-global', 'lib', 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'server', 'schedules-runner.mjs'),
190
+ ];
191
+ // Probe $PWD and global node_modules
192
+ try {
193
+ const { execSync } = await import('node:child_process');
194
+ const root = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
195
+ candidates.push(join(root, '@polderlabs', 'bizar-dash', 'src', 'server', 'schedules-runner.mjs'));
196
+ } catch {
197
+ /* ignore */
198
+ }
199
+ // local node_modules (cwd)
200
+ candidates.push(join(process.cwd(), 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'server', 'schedules-runner.mjs'));
201
+ // local node_modules of this package
202
+ candidates.push(join(__dirname, '..', 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'server', 'schedules-runner.mjs'));
203
+ // also try the parent (when service lives inside a subdir)
204
+ candidates.push(join(__dirname, '..', '..', 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'server', 'schedules-runner.mjs'));
205
+
206
+ let runner = null;
207
+ for (const c of candidates) {
208
+ if (existsSync(c)) {
209
+ try {
210
+ runner = await import(c);
211
+ logLine(`using runner at ${c}`);
212
+ break;
213
+ } catch (err) {
214
+ logLine(`failed to import runner at ${c}: ${err.message}`);
215
+ }
216
+ }
217
+ }
218
+ if (!runner) {
219
+ logLine('FATAL: could not locate @polderlabs/bizar-dash/schedules-runner. Service exiting.');
220
+ try { writeFileSync(PID_FILE, ''); } catch { /* ignore */ }
221
+ process.exit(2);
222
+ }
223
+
224
+ let stopping = false;
225
+ const shutdown = () => {
226
+ if (stopping) return;
227
+ stopping = true;
228
+ logLine(`service stopping (pid ${process.pid})`);
229
+ try { writeFileSync(PID_FILE, ''); } catch { /* ignore */ }
230
+ process.exit(0);
231
+ };
232
+ process.on('SIGTERM', shutdown);
233
+ process.on('SIGINT', shutdown);
234
+
235
+ const tick = async () => {
236
+ try {
237
+ const fired = await runner.schedulesRunner.tick();
238
+ if (Array.isArray(fired) && fired.length) {
239
+ logLine(`tick fired ${fired.length} schedule(s)`);
240
+ }
241
+ } catch (err) {
242
+ logLine(`tick error: ${err.message}`);
243
+ }
244
+ };
245
+
246
+ // Run immediately, then on a 5s interval
247
+ await tick();
248
+ setInterval(tick, TICK_MS);
249
+ }
250
+
251
+ export async function runService(sub, _rest) {
252
+ if (sub === 'start') {
253
+ await startService();
254
+ return;
255
+ }
256
+ if (sub === 'stop') {
257
+ stopService();
258
+ return;
259
+ }
260
+ if (sub === 'status') {
261
+ serviceStatus();
262
+ return;
263
+ }
264
+ if (sub === 'logs') {
265
+ tailLogs(false);
266
+ return;
267
+ }
268
+ if (sub === 'follow' || sub === 'tail') {
269
+ tailLogs(true);
270
+ return;
271
+ }
272
+ console.log(`
273
+ bizar service — Manage the background service daemon
274
+
275
+ Usage:
276
+ bizar service start
277
+ bizar service stop
278
+ bizar service status
279
+ bizar service logs
280
+ `);
281
+ }
282
+
283
+ // ── direct entry: when invoked as `node cli/service.mjs _daemon` ──────────
284
+ const isMain = import.meta.url === `file://${process.argv[1]}`;
285
+ if (isMain) {
286
+ const sub = process.argv[2];
287
+ if (sub === '_daemon') {
288
+ // Run the daemon loop — never returns
289
+ await daemonLoop();
290
+ } else if (sub === 'stop') {
291
+ stopService();
292
+ } else if (sub === 'status') {
293
+ serviceStatus();
294
+ } else if (sub === 'logs') {
295
+ tailLogs(false);
296
+ } else if (sub === 'start') {
297
+ await startService();
298
+ } else {
299
+ console.log(`
300
+ bizar service — Manage the background service daemon
301
+
302
+ Usage:
303
+ node cli/service.mjs start
304
+ node cli/service.mjs stop
305
+ node cli/service.mjs status
306
+ node cli/service.mjs logs
307
+ `);
308
+ }
309
+ }
package/package.json CHANGED
@@ -1,25 +1,18 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "2.6.0",
4
- "description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing and per-project Hindsight memory",
3
+ "version": "3.0.0",
4
+ "description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, per-project Hindsight memory, mods, schedules, and a background service daemon",
5
5
  "type": "module",
6
6
  "bin": {
7
- "bizar": "cli/bin.mjs"
7
+ "bizar": "cli/bin.mjs",
8
+ "install": "cli/bin.mjs"
8
9
  },
9
10
  "files": [
10
11
  "cli/",
11
12
  "config/",
12
- "dist/",
13
- "src/",
14
- "templates/",
15
- "vite.config.ts",
16
- "tsconfig.json",
17
- "!cli/*.test.mjs"
13
+ "templates/"
18
14
  ],
19
15
  "scripts": {
20
- "dev": "vite",
21
- "build": "vite build",
22
- "preview": "vite preview",
23
16
  "typecheck": "tsc --noEmit",
24
17
  "postinstall": "node cli/bin.mjs --postinstall"
25
18
  },
@@ -29,22 +22,25 @@
29
22
  "norse",
30
23
  "multi-agent",
31
24
  "router",
32
- "cli"
25
+ "cli",
26
+ "platform",
27
+ "mods",
28
+ "schedules"
33
29
  ],
34
30
  "license": "MIT",
35
31
  "dependencies": {
36
32
  "boxen": "^8.0.0",
37
33
  "chalk": "^5.4.0",
38
- "chokidar": "^3.6.0",
39
- "express": "^4.22.2",
40
34
  "inquirer": "^12.0.0",
41
- "lucide-react": "^0.460.0",
42
- "ora": "^8.1.0",
43
- "react": "^18.3.1",
44
- "react-dom": "^18.3.1",
45
- "react-markdown": "^9.1.0",
46
- "remark-gfm": "^4.0.1",
47
- "ws": "^8.21.0"
35
+ "ora": "^8.1.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@polderlabs/bizar-dash": ">=3.0.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "@polderlabs/bizar-dash": {
42
+ "optional": true
43
+ }
48
44
  },
49
45
  "engines": {
50
46
  "node": ">=20"
@@ -62,10 +58,6 @@
62
58
  },
63
59
  "devDependencies": {
64
60
  "@types/node": "^22.19.21",
65
- "@types/react": "^18.3.31",
66
- "@types/react-dom": "^18.3.7",
67
- "@vitejs/plugin-react": "^4.7.0",
68
- "typescript": "^5.9.3",
69
- "vite": "^5.4.21"
61
+ "typescript": "^5.9.3"
70
62
  }
71
63
  }