@polderlabs/bizar 2.6.1 → 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 (43) hide show
  1. package/cli/bin.mjs +158 -130
  2. package/cli/plan.test.mjs +2331 -0
  3. package/cli/service.mjs +309 -0
  4. package/package.json +19 -27
  5. package/cli/dashboard/api.mjs +0 -473
  6. package/cli/dashboard/browser.mjs +0 -40
  7. package/cli/dashboard/server.mjs +0 -366
  8. package/cli/dashboard/state.mjs +0 -438
  9. package/cli/dashboard/tasks-store.mjs +0 -203
  10. package/cli/dashboard/watcher.mjs +0 -81
  11. package/cli/dashboard.mjs +0 -97
  12. package/dist/assets/index-BVvY22Gt.css +0 -1
  13. package/dist/assets/index-CO3c8O32.js +0 -285
  14. package/dist/assets/index-CO3c8O32.js.map +0 -1
  15. package/dist/index.html +0 -18
  16. package/src/App.tsx +0 -233
  17. package/src/components/Button.tsx +0 -55
  18. package/src/components/Card.tsx +0 -40
  19. package/src/components/EmptyState.tsx +0 -30
  20. package/src/components/Modal.tsx +0 -137
  21. package/src/components/Spinner.tsx +0 -19
  22. package/src/components/StatusBadge.tsx +0 -25
  23. package/src/components/Tag.tsx +0 -28
  24. package/src/components/Toast.tsx +0 -142
  25. package/src/components/Topbar.tsx +0 -88
  26. package/src/index.html +0 -17
  27. package/src/lib/api.ts +0 -71
  28. package/src/lib/markdown.tsx +0 -59
  29. package/src/lib/types.ts +0 -200
  30. package/src/lib/utils.ts +0 -79
  31. package/src/lib/ws.ts +0 -132
  32. package/src/main.tsx +0 -12
  33. package/src/styles/main.css +0 -2324
  34. package/src/views/Agents.tsx +0 -199
  35. package/src/views/Chat.tsx +0 -255
  36. package/src/views/Config.tsx +0 -250
  37. package/src/views/Overview.tsx +0 -267
  38. package/src/views/Plans.tsx +0 -667
  39. package/src/views/Projects.tsx +0 -155
  40. package/src/views/Settings.tsx +0 -253
  41. package/src/views/Tasks.tsx +0 -567
  42. package/tsconfig.json +0 -23
  43. package/vite.config.ts +0 -24
@@ -1,438 +0,0 @@
1
- /**
2
- * cli/dashboard/state.mjs
3
- *
4
- * Aggregates every piece of state the dashboard surfaces. Each getter is
5
- * defensive: missing files, missing directories, parse errors — all become
6
- * sensible empty defaults rather than crashes.
7
- *
8
- * Data sources:
9
- * - getOverview: counts + .bizar/activity.log tail
10
- * - getChat: .bizar/sessions/*.jsonl (one JSON record per line)
11
- * - getAgents: ~/.config/opencode/agents/*.md (frontmatter parse)
12
- * - getPlans: scans plans/ (worktree) and ~/.config/opencode/plans/
13
- * - getProjects: walks cwd for .bizar/PROJECT.md, also ~/Projects/*
14
- * - getConfig: ~/.config/opencode/opencode.json (live)
15
- * - getSettings: ~/.config/bizar/settings.json (created on demand)
16
- */
17
- import {
18
- existsSync,
19
- readFileSync,
20
- writeFileSync,
21
- readdirSync,
22
- statSync,
23
- mkdirSync,
24
- } from 'node:fs';
25
- import { join, basename, dirname } from 'node:path';
26
- import { homedir } from 'node:os';
27
- import { loadTasks } from './tasks-store.mjs';
28
-
29
- const HOME = homedir();
30
-
31
- /**
32
- * @param {object} opts
33
- * @param {string} opts.projectRoot
34
- * @param {string} opts.opencodeConfigDir
35
- * @param {string} opts.bizarRoot
36
- */
37
- export function createState({ projectRoot, opencodeConfigDir, bizarRoot }) {
38
- const paths = {
39
- projectRoot,
40
- opencodeConfigDir,
41
- bizarRoot,
42
- opencodeJson: join(opencodeConfigDir, 'opencode.json'),
43
- agentsDir: join(opencodeConfigDir, 'agents'),
44
- commandsDir: join(opencodeConfigDir, 'commands-bizar'),
45
- bizarDir: join(projectRoot, '.bizar'),
46
- sessionsDir: join(projectRoot, '.bizar', 'sessions'),
47
- activityLog: join(projectRoot, '.bizar', 'activity.log'),
48
- plansDir: join(projectRoot, 'plans'),
49
- globalPlansDir: join(opencodeConfigDir, 'plans'),
50
- settingsFile: join(HOME, '.config', 'bizar', 'settings.json'),
51
- };
52
-
53
- // ── helpers ───────────────────────────────────────────────────────────────
54
-
55
- function safeReadJSON(file, fallback = null) {
56
- try {
57
- if (!existsSync(file)) return fallback;
58
- const text = readFileSync(file, 'utf8');
59
- if (!text.trim()) return fallback;
60
- return JSON.parse(text);
61
- } catch {
62
- return fallback;
63
- }
64
- }
65
-
66
- function safeReadText(file, fallback = '') {
67
- try {
68
- if (!existsSync(file)) return fallback;
69
- return readFileSync(file, 'utf8');
70
- } catch {
71
- return fallback;
72
- }
73
- }
74
-
75
- function listFiles(dir, ext) {
76
- try {
77
- if (!existsSync(dir)) return [];
78
- return readdirSync(dir).filter((f) =>
79
- ext ? f.endsWith(ext) : true,
80
- );
81
- } catch {
82
- return [];
83
- }
84
- }
85
-
86
- function safeStat(p) {
87
- try {
88
- return statSync(p);
89
- } catch {
90
- return null;
91
- }
92
- }
93
-
94
- /** Minimal frontmatter parser: returns { frontmatter, body }. */
95
- function parseFrontmatter(raw) {
96
- if (!raw.startsWith('---')) return { frontmatter: {}, body: raw };
97
- const end = raw.indexOf('\n---', 3);
98
- if (end === -1) return { frontmatter: {}, body: raw };
99
- const fmBlock = raw.slice(3, end).trim();
100
- const body = raw.slice(end + 4).replace(/^\s+/, '');
101
- const frontmatter = {};
102
- for (const line of fmBlock.split(/\r?\n/)) {
103
- const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
104
- if (!m) continue;
105
- const key = m[1];
106
- let val = m[2].trim();
107
- // strip surrounding quotes
108
- if (
109
- (val.startsWith('"') && val.endsWith('"')) ||
110
- (val.startsWith("'") && val.endsWith("'"))
111
- ) {
112
- val = val.slice(1, -1);
113
- }
114
- frontmatter[key] = val;
115
- }
116
- return { frontmatter, body };
117
- }
118
-
119
- // ── public getters ────────────────────────────────────────────────────────
120
-
121
- function getOverview() {
122
- const agents = getAgents();
123
- const plans = getPlans();
124
- const projects = getProjects();
125
-
126
- const sessionsDir = paths.sessionsDir;
127
- let sessionCount = 0;
128
- if (existsSync(sessionsDir)) {
129
- try {
130
- sessionCount = readdirSync(sessionsDir).filter((f) =>
131
- f.endsWith('.jsonl'),
132
- ).length;
133
- } catch {
134
- sessionCount = 0;
135
- }
136
- }
137
-
138
- const recentActivity = [];
139
- try {
140
- if (existsSync(paths.activityLog)) {
141
- const lines = readFileSync(paths.activityLog, 'utf8').split(/\r?\n/);
142
- for (const line of lines) {
143
- if (!line.trim()) continue;
144
- try {
145
- recentActivity.push(JSON.parse(line));
146
- } catch {
147
- // skip malformed lines
148
- }
149
- }
150
- }
151
- } catch {
152
- /* ignore */
153
- }
154
- // Last 30, newest first
155
- recentActivity.reverse();
156
- const trimmedActivity = recentActivity.slice(0, 30);
157
-
158
- return {
159
- counts: {
160
- agents: agents.length,
161
- plans: plans.length,
162
- projects: projects.length,
163
- sessions: sessionCount,
164
- },
165
- recentActivity: trimmedActivity,
166
- versions: {
167
- node: process.version,
168
- platform: process.platform,
169
- bizarRoot,
170
- projectRoot,
171
- },
172
- generatedAt: new Date().toISOString(),
173
- };
174
- }
175
-
176
- function getChat({ sessionId = null, limit = 200 } = {}) {
177
- if (!existsSync(paths.sessionsDir)) return { messages: [], sessions: [] };
178
- const allFiles = readdirSync(paths.sessionsDir).filter((f) =>
179
- f.endsWith('.jsonl'),
180
- );
181
- const sessions = allFiles.map((f) => {
182
- const st = safeStat(join(paths.sessionsDir, f));
183
- return {
184
- id: f.replace(/\.jsonl$/, ''),
185
- file: f,
186
- mtime: st ? st.mtimeMs : 0,
187
- size: st ? st.size : 0,
188
- };
189
- });
190
- sessions.sort((a, b) => b.mtime - a.mtime);
191
-
192
- const target = sessionId
193
- ? allFiles.filter((f) => f === `${sessionId}.jsonl`)
194
- : allFiles;
195
-
196
- const messages = [];
197
- for (const file of target) {
198
- const full = join(paths.sessionsDir, file);
199
- try {
200
- const lines = readFileSync(full, 'utf8').split(/\r?\n/);
201
- for (const line of lines) {
202
- if (!line.trim()) continue;
203
- try {
204
- messages.push(JSON.parse(line));
205
- } catch {
206
- // skip malformed line
207
- }
208
- }
209
- } catch {
210
- // skip unreadable file
211
- }
212
- }
213
-
214
- // newest first, then truncate
215
- messages.reverse();
216
- return {
217
- messages: messages.slice(0, limit),
218
- sessions,
219
- };
220
- }
221
-
222
- function getAgents() {
223
- const dir = paths.agentsDir;
224
- if (!existsSync(dir)) return [];
225
- const out = [];
226
- for (const file of readdirSync(dir)) {
227
- if (!file.endsWith('.md')) continue;
228
- const full = join(dir, file);
229
- const raw = safeReadText(full);
230
- const { frontmatter } = parseFrontmatter(raw);
231
- const st = safeStat(full);
232
- out.push({
233
- name:
234
- frontmatter.name ||
235
- basename(file, '.md') ||
236
- file.replace(/\.md$/, ''),
237
- description: frontmatter.description || '',
238
- model: frontmatter.model || '',
239
- mode: frontmatter.mode || '',
240
- file,
241
- path: full,
242
- mtime: st ? st.mtimeMs : 0,
243
- });
244
- }
245
- out.sort((a, b) => a.name.localeCompare(b.name));
246
- return out;
247
- }
248
-
249
- function readPlansFromDir(dir) {
250
- if (!existsSync(dir)) return [];
251
- const out = [];
252
- for (const entry of readdirSync(dir)) {
253
- const full = join(dir, entry);
254
- const st = safeStat(full);
255
- if (!st || !st.isDirectory()) continue;
256
- const meta = safeReadJSON(join(full, 'meta.json'), null);
257
- const planPath = join(full, 'plan.mdx');
258
- const planExists = existsSync(planPath);
259
- out.push({
260
- slug: entry,
261
- title: meta?.title || entry,
262
- status: meta?.status || 'draft',
263
- source: dir === paths.plansDir ? 'worktree' : 'global',
264
- elementCount: meta?.elementCount ?? null,
265
- commentCount: meta?.commentCount ?? null,
266
- mtime: st.mtimeMs,
267
- planUrl: planExists ? `/${entry}/` : null,
268
- });
269
- }
270
- return out;
271
- }
272
-
273
- function getPlans() {
274
- const a = readPlansFromDir(paths.plansDir);
275
- const b = readPlansFromDir(paths.globalPlansDir);
276
- const seen = new Set();
277
- const out = [];
278
- for (const p of [...a, ...b]) {
279
- if (seen.has(p.slug)) continue;
280
- seen.add(p.slug);
281
- out.push(p);
282
- }
283
- out.sort((a, b) => b.mtime - a.mtime);
284
- return out;
285
- }
286
-
287
- function getProjects() {
288
- const out = [];
289
- // 1. Walk up from projectRoot looking for .bizar/PROJECT.md markers
290
- let dir = projectRoot;
291
- const seen = new Set();
292
- while (dir && dir !== dirname(dir) && !seen.has(dir)) {
293
- seen.add(dir);
294
- const marker = join(dir, '.bizar', 'PROJECT.md');
295
- if (existsSync(marker)) {
296
- const st = safeStat(marker);
297
- const hindsight = join(dir, '.bizar', '.hindsight');
298
- let hcount = 0;
299
- if (existsSync(hindsight)) {
300
- try {
301
- hcount = readdirSync(hindsight).length;
302
- } catch {
303
- hcount = 0;
304
- }
305
- }
306
- out.push({
307
- name: basename(dir),
308
- path: dir,
309
- projectMdSize: st ? st.size : 0,
310
- hindsightCount: hcount,
311
- mtime: st ? st.mtimeMs : 0,
312
- active: dir === projectRoot,
313
- });
314
- }
315
- dir = dirname(dir);
316
- }
317
-
318
- // 2. Also scan ~/Projects/* as a discovery surface
319
- const projectsRoot = join(HOME, 'Projects');
320
- if (existsSync(projectsRoot)) {
321
- try {
322
- for (const entry of readdirSync(projectsRoot)) {
323
- const full = join(projectsRoot, entry);
324
- const st = safeStat(full);
325
- if (!st || !st.isDirectory()) continue;
326
- if (out.some((p) => p.path === full)) continue;
327
- const marker = join(full, '.bizar', 'PROJECT.md');
328
- const hasMarker = existsSync(marker);
329
- out.push({
330
- name: entry,
331
- path: full,
332
- projectMdSize: hasMarker ? safeStat(marker)?.size || 0 : 0,
333
- hindsightCount: 0,
334
- mtime: st.mtimeMs,
335
- active: false,
336
- });
337
- }
338
- } catch {
339
- /* ignore */
340
- }
341
- }
342
-
343
- out.sort((a, b) => (b.active ? 1 : 0) - (a.active ? 1 : 0));
344
- return out;
345
- }
346
-
347
- function getConfig() {
348
- const data = safeReadJSON(paths.opencodeJson, null);
349
- return {
350
- path: paths.opencodeJson,
351
- data,
352
- raw: data === null ? '' : JSON.stringify(data, null, 2),
353
- exists: existsSync(paths.opencodeJson),
354
- };
355
- }
356
-
357
- function getSettings() {
358
- const defaults = {
359
- theme: 'dark',
360
- defaultAgent: 'odin',
361
- defaultModel: '',
362
- notifications: {
363
- onAgentComplete: true,
364
- onPlanApproval: true,
365
- },
366
- about: {
367
- version: '2.5.0',
368
- homepage: 'https://github.com/DrB0rk/BizarHarness',
369
- license: 'MIT',
370
- },
371
- };
372
- const existing = safeReadJSON(paths.settingsFile, null);
373
- const merged = { ...defaults, ...(existing || {}) };
374
- return {
375
- path: paths.settingsFile,
376
- data: merged,
377
- exists: existsSync(paths.settingsFile),
378
- };
379
- }
380
-
381
- function getTasks() {
382
- return loadTasks();
383
- }
384
-
385
- // ── mutators ──────────────────────────────────────────────────────────────
386
-
387
- function setConfig(newData) {
388
- mkdirSync(dirname(paths.opencodeJson), { recursive: true });
389
- writeFileSync(
390
- paths.opencodeJson,
391
- JSON.stringify(newData, null, 2) + '\n',
392
- 'utf8',
393
- );
394
- return getConfig();
395
- }
396
-
397
- function setSettings(newData) {
398
- mkdirSync(dirname(paths.settingsFile), { recursive: true });
399
- writeFileSync(
400
- paths.settingsFile,
401
- JSON.stringify(newData, null, 2) + '\n',
402
- 'utf8',
403
- );
404
- return getSettings();
405
- }
406
-
407
- function appendActivity(event) {
408
- try {
409
- mkdirSync(paths.bizarDir, { recursive: true });
410
- const record = {
411
- ts: new Date().toISOString(),
412
- ...event,
413
- };
414
- writeFileSync(
415
- paths.activityLog,
416
- JSON.stringify(record) + '\n',
417
- { flag: 'a', encoding: 'utf8' },
418
- );
419
- } catch (err) {
420
- console.error('[dashboard state] appendActivity failed:', err);
421
- }
422
- }
423
-
424
- return {
425
- paths,
426
- getOverview,
427
- getChat,
428
- getAgents,
429
- getPlans,
430
- getProjects,
431
- getConfig,
432
- getSettings,
433
- getTasks,
434
- setConfig,
435
- setSettings,
436
- appendActivity,
437
- };
438
- }
@@ -1,203 +0,0 @@
1
- /**
2
- * cli/dashboard/tasks-store.mjs
3
- *
4
- * Per-user task storage at ~/.config/bizar/tasks.json.
5
- * In-process mutex (single-lock) to handle concurrent reads/writes.
6
- *
7
- * File format:
8
- * {
9
- * "version": 1,
10
- * "tasks": [ ...task objects... ]
11
- * }
12
- *
13
- * Task object:
14
- * {
15
- * "id": "tsk_<8 base36 chars>",
16
- * "title": "string",
17
- * "description": "markdown string",
18
- * "status": "queued" | "doing" | "done",
19
- * "tags": ["string"],
20
- * "priority": "low" | "normal" | "high",
21
- * "createdAt": "ISO timestamp",
22
- * "updatedAt": "ISO timestamp",
23
- * "completedAt": "ISO timestamp | null
24
- * }
25
- */
26
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
27
- import { dirname } from 'node:path';
28
- import { homedir } from 'node:os';
29
- import { randomBytes } from 'node:crypto';
30
-
31
- const HOME = homedir();
32
- const TASKS_FILE = `${HOME}/.config/bizar/tasks.json`;
33
- const TASKS_DIR = `${HOME}/.config/bizar`;
34
-
35
- // ── mutex ───────────────────────────────────────────────────────────────────
36
-
37
- let _busy = false;
38
- const _waiters = [];
39
-
40
- function acquire() {
41
- return new Promise((resolve) => {
42
- if (!_busy) {
43
- _busy = true;
44
- resolve();
45
- } else {
46
- _waiters.push({ resolve });
47
- }
48
- });
49
- }
50
-
51
- function release() {
52
- if (_waiters.length > 0) {
53
- const next = _waiters.shift();
54
- next.resolve();
55
- } else {
56
- _busy = false;
57
- }
58
- }
59
-
60
- // ── file helpers ─────────────────────────────────────────────────────────────
61
-
62
- function safeReadJSON(file, fallback = null) {
63
- try {
64
- if (!existsSync(file)) return fallback;
65
- const text = readFileSync(file, 'utf8');
66
- if (!text.trim()) return fallback;
67
- return JSON.parse(text);
68
- } catch {
69
- return fallback;
70
- }
71
- }
72
-
73
- function loadStore() {
74
- const raw = safeReadJSON(TASKS_FILE, null);
75
- if (!raw || typeof raw !== 'object' || !Array.isArray(raw.tasks)) {
76
- return { version: 1, tasks: [] };
77
- }
78
- return raw;
79
- }
80
-
81
- function saveStore(store) {
82
- mkdirSync(TASKS_DIR, { recursive: true });
83
- writeFileSync(TASKS_FILE, JSON.stringify(store, null, 2) + '\n', 'utf8');
84
- }
85
-
86
- // ── ID generation ────────────────────────────────────────────────────────────
87
-
88
- function genId() {
89
- // Use hex encoding (universally supported) and take first 8 chars
90
- return 'tsk_' + randomBytes(8).toString('hex').slice(0, 8);
91
- }
92
-
93
- // ── public API ───────────────────────────────────────────────────────────────
94
-
95
- /** Returns all tasks, newest first. */
96
- export function loadTasks() {
97
- const store = loadStore();
98
- return store.tasks.slice().sort((a, b) => {
99
- // newest first
100
- return new Date(b.createdAt) - new Date(a.createdAt);
101
- });
102
- }
103
-
104
- /** Persists the full tasks array. Call after any mutation. */
105
- export function saveTasks(tasks) {
106
- saveStore({ version: 1, tasks });
107
- }
108
-
109
- /**
110
- * Create a new task.
111
- * @param {{ title, description?, status?, tags?, priority? }} opts
112
- * @returns the created task object
113
- */
114
- export async function createTask({ title, description = '', status = 'queued', tags = [], priority = 'normal' }) {
115
- await acquire();
116
- try {
117
- const store = loadStore();
118
- const now = new Date().toISOString();
119
- const task = {
120
- id: genId(),
121
- title,
122
- description,
123
- status,
124
- tags: Array.isArray(tags) ? tags : [],
125
- priority: ['low', 'normal', 'high'].includes(priority) ? priority : 'normal',
126
- createdAt: now,
127
- updatedAt: now,
128
- completedAt: status === 'done' ? now : null,
129
- };
130
- store.tasks.push(task);
131
- saveStore(store);
132
- return task;
133
- } finally {
134
- release();
135
- }
136
- }
137
-
138
- /**
139
- * Partially update a task by id.
140
- * @param {string} id
141
- * @param {object} patch - partial task fields
142
- * @returns the updated task or null if not found
143
- */
144
- export async function updateTask(id, patch = {}) {
145
- await acquire();
146
- try {
147
- const store = loadStore();
148
- const idx = store.tasks.findIndex((t) => t.id === id);
149
- if (idx === -1) return null;
150
- const task = { ...store.tasks[idx] };
151
-
152
- // Apply allowed fields
153
- if (typeof patch.title === 'string') task.title = patch.title.slice(0, 200);
154
- if (typeof patch.description === 'string') task.description = patch.description;
155
- if (typeof patch.status === 'string' && ['queued', 'doing', 'done'].includes(patch.status)) {
156
- task.status = patch.status;
157
- }
158
- if (Array.isArray(patch.tags)) task.tags = patch.tags;
159
- if (['low', 'normal', 'high'].includes(patch.priority)) task.priority = patch.priority;
160
-
161
- task.updatedAt = new Date().toISOString();
162
- if (task.status === 'done' && !task.completedAt) {
163
- task.completedAt = task.updatedAt;
164
- } else if (task.status !== 'done') {
165
- task.completedAt = null;
166
- }
167
-
168
- store.tasks[idx] = task;
169
- saveStore(store);
170
- return task;
171
- } finally {
172
- release();
173
- }
174
- }
175
-
176
- /**
177
- * Delete a task by id.
178
- * @param {string} id
179
- * @returns true if deleted, false if not found
180
- */
181
- export async function deleteTask(id) {
182
- await acquire();
183
- try {
184
- const store = loadStore();
185
- const idx = store.tasks.findIndex((t) => t.id === id);
186
- if (idx === -1) return false;
187
- store.tasks.splice(idx, 1);
188
- saveStore(store);
189
- return true;
190
- } finally {
191
- release();
192
- }
193
- }
194
-
195
- /**
196
- * Move a task to a new status column.
197
- * @param {string} id
198
- * @param {'queued'|'doing'|'done'} newStatus
199
- * @returns the updated task or null if not found
200
- */
201
- export async function moveTask(id, newStatus) {
202
- return updateTask(id, { status: newStatus });
203
- }