@polderlabs/bizar 2.3.0 → 2.6.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 (48) hide show
  1. package/cli/bin.mjs +73 -0
  2. package/cli/copy.mjs +42 -2
  3. package/cli/dashboard/api.mjs +473 -0
  4. package/cli/dashboard/browser.mjs +40 -0
  5. package/cli/dashboard/server.mjs +366 -0
  6. package/cli/dashboard/state.mjs +438 -0
  7. package/cli/dashboard/tasks-store.mjs +203 -0
  8. package/cli/dashboard/watcher.mjs +81 -0
  9. package/cli/dashboard.mjs +97 -0
  10. package/cli/install.mjs +17 -4
  11. package/config/commands/bizar.md +18 -0
  12. package/config/commands/plan.md +26 -0
  13. package/config/commands/visual-plan.md +15 -0
  14. package/config/opencode.json +259 -1
  15. package/dist/assets/index-BVvY22Gt.css +1 -0
  16. package/dist/assets/index-CO3c8O32.js +285 -0
  17. package/dist/assets/index-CO3c8O32.js.map +1 -0
  18. package/dist/index.html +18 -0
  19. package/package.json +26 -2
  20. package/src/App.tsx +233 -0
  21. package/src/components/Button.tsx +55 -0
  22. package/src/components/Card.tsx +40 -0
  23. package/src/components/EmptyState.tsx +30 -0
  24. package/src/components/Modal.tsx +137 -0
  25. package/src/components/Spinner.tsx +19 -0
  26. package/src/components/StatusBadge.tsx +25 -0
  27. package/src/components/Tag.tsx +28 -0
  28. package/src/components/Toast.tsx +142 -0
  29. package/src/components/Topbar.tsx +88 -0
  30. package/src/index.html +17 -0
  31. package/src/lib/api.ts +71 -0
  32. package/src/lib/markdown.tsx +59 -0
  33. package/src/lib/types.ts +200 -0
  34. package/src/lib/utils.ts +79 -0
  35. package/src/lib/ws.ts +132 -0
  36. package/src/main.tsx +12 -0
  37. package/src/styles/main.css +2324 -0
  38. package/src/views/Agents.tsx +199 -0
  39. package/src/views/Chat.tsx +255 -0
  40. package/src/views/Config.tsx +250 -0
  41. package/src/views/Overview.tsx +267 -0
  42. package/src/views/Plans.tsx +667 -0
  43. package/src/views/Projects.tsx +155 -0
  44. package/src/views/Settings.tsx +253 -0
  45. package/src/views/Tasks.tsx +567 -0
  46. package/tsconfig.json +23 -0
  47. package/vite.config.ts +24 -0
  48. package/config/opencode.json.template +0 -52
@@ -0,0 +1,567 @@
1
+ // src/views/Tasks.tsx — Personal Task Kanban Board.
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import {
4
+ CheckSquare,
5
+ Plus,
6
+ Pencil,
7
+ Trash2,
8
+ ChevronLeft,
9
+ ChevronRight,
10
+ Search,
11
+ X as XIcon,
12
+ } from 'lucide-react';
13
+ import { Button } from '../components/Button';
14
+ import { Card, CardTitle } from '../components/Card';
15
+ import { EmptyState } from '../components/EmptyState';
16
+ import { Spinner } from '../components/Spinner';
17
+ import { StatusBadge } from '../components/StatusBadge';
18
+ import { Tag } from '../components/Tag';
19
+ import { useModal } from '../components/Modal';
20
+ import { useToast } from '../components/Toast';
21
+ import { api } from '../lib/api';
22
+ import { cn, formatRelative, priorityColors } from '../lib/utils';
23
+ import type { Settings, Snapshot, Task } from '../lib/types';
24
+
25
+ type Props = {
26
+ snapshot: Snapshot;
27
+ settings: Settings;
28
+ activeTab: string;
29
+ setActiveTab: (id: string) => void;
30
+ refreshSnapshot: () => Promise<void>;
31
+ };
32
+
33
+ type Column = {
34
+ id: Task['status'] | string;
35
+ label: string;
36
+ kind: 'info' | 'accent' | 'success';
37
+ };
38
+
39
+ const COLUMNS: Column[] = [
40
+ { id: 'queued', label: 'Queued', kind: 'info' },
41
+ { id: 'doing', label: 'Doing', kind: 'accent' },
42
+ { id: 'done', label: 'Done', kind: 'success' },
43
+ ];
44
+
45
+ const PRIORITIES = ['low', 'normal', 'high'] as const;
46
+ type Priority = (typeof PRIORITIES)[number];
47
+
48
+ export function Tasks({ snapshot }: Props) {
49
+ const toast = useToast();
50
+ const modal = useModal();
51
+ const [tasks, setTasks] = useState<Task[]>(snapshot.tasks || []);
52
+ const [loading, setLoading] = useState(!snapshot.tasks);
53
+ const [filter, setFilter] = useState('');
54
+ const [focusedId, setFocusedId] = useState<string | null>(null);
55
+
56
+ const reload = async () => {
57
+ try {
58
+ const data = await api.get<Task[]>('/tasks');
59
+ setTasks(Array.isArray(data) ? data : []);
60
+ } catch (err) {
61
+ toast.error(`Tasks load failed: ${(err as Error).message}`);
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ };
66
+
67
+ useEffect(() => {
68
+ if (snapshot.tasks?.length || snapshot.tasks) {
69
+ setTasks(snapshot.tasks || []);
70
+ setLoading(false);
71
+ return;
72
+ }
73
+ reload();
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
+ }, [snapshot.tasks]);
76
+
77
+ const filtered = useMemo(() => {
78
+ if (!filter.trim()) return tasks;
79
+ const q = filter.toLowerCase();
80
+ return tasks.filter((t) => {
81
+ const title = (t.title || '').toLowerCase();
82
+ const desc = (t.description || '').toLowerCase();
83
+ const tags = (t.tags || []).join(' ').toLowerCase();
84
+ return title.includes(q) || desc.includes(q) || tags.includes(q);
85
+ });
86
+ }, [tasks, filter]);
87
+
88
+ const moveTask = async (taskId: string, newStatus: string) => {
89
+ const t = tasks.find((x) => x.id === taskId);
90
+ if (!t) return;
91
+ const prev = t.status;
92
+ setTasks((cur) =>
93
+ cur.map((x) => (x.id === taskId ? { ...x, status: newStatus } : x)),
94
+ );
95
+ try {
96
+ await api.patch(`/tasks/${encodeURIComponent(taskId)}/status`, {
97
+ status: newStatus,
98
+ });
99
+ toast.success(`Moved to ${newStatus}.`, 1500);
100
+ } catch (err) {
101
+ // Revert
102
+ setTasks((cur) =>
103
+ cur.map((x) => (x.id === taskId ? { ...x, status: prev } : x)),
104
+ );
105
+ toast.error(`Move failed: ${(err as Error).message}`);
106
+ }
107
+ };
108
+
109
+ const deleteTask = async (taskId: string) => {
110
+ if (
111
+ // eslint-disable-next-line no-alert
112
+ !confirm('Delete this task?')
113
+ )
114
+ return;
115
+ try {
116
+ await api.del(`/tasks/${encodeURIComponent(taskId)}`);
117
+ setTasks((cur) => cur.filter((t) => t.id !== taskId));
118
+ toast.success('Task deleted.', 1500);
119
+ } catch (err) {
120
+ toast.error(`Delete failed: ${(err as Error).message}`);
121
+ }
122
+ };
123
+
124
+ // Keyboard shortcuts
125
+ useEffect(() => {
126
+ const handler = (e: KeyboardEvent) => {
127
+ const t = (e.target as HTMLElement | null)?.tagName?.toLowerCase();
128
+ if (
129
+ t === 'input' ||
130
+ t === 'textarea' ||
131
+ (e.target as HTMLElement)?.isContentEditable ||
132
+ e.metaKey ||
133
+ e.ctrlKey ||
134
+ e.altKey
135
+ )
136
+ return;
137
+ if (e.key === 'n') {
138
+ e.preventDefault();
139
+ openTaskModal(modal, toast, null, 'queued', reload);
140
+ } else if (e.key === '1') {
141
+ document
142
+ .querySelector('[data-column="queued"]')
143
+ ?.scrollIntoView({ behavior: 'smooth' });
144
+ } else if (e.key === '2') {
145
+ document
146
+ .querySelector('[data-column="doing"]')
147
+ ?.scrollIntoView({ behavior: 'smooth' });
148
+ } else if (e.key === '3') {
149
+ document
150
+ .querySelector('[data-column="done"]')
151
+ ?.scrollIntoView({ behavior: 'smooth' });
152
+ } else if (e.key === 'e' && focusedId) {
153
+ const task = tasks.find((x) => x.id === focusedId);
154
+ if (task) openTaskModal(modal, toast, task, task.status, reload);
155
+ } else if ((e.key === 'Delete' || e.key === 'Backspace') && focusedId) {
156
+ e.preventDefault();
157
+ deleteTask(focusedId);
158
+ }
159
+ };
160
+ document.addEventListener('keydown', handler);
161
+ return () => document.removeEventListener('keydown', handler);
162
+ // eslint-disable-next-line react-hooks/exhaustive-deps
163
+ }, [focusedId, tasks]);
164
+
165
+ return (
166
+ <div className="view view-tasks">
167
+ <header className="view-header">
168
+ <div className="view-header-text">
169
+ <h2 className="view-title">
170
+ <CheckSquare size={18} /> Tasks
171
+ </h2>
172
+ <p className="view-subtitle">
173
+ Personal kanban. Press <kbd>n</kbd> for new, <kbd>1-3</kbd> to jump columns, <kbd>e</kbd> edit, <kbd>Del</kbd> delete.
174
+ </p>
175
+ </div>
176
+ <div className="view-actions">
177
+ <div className="search-input">
178
+ <Search size={14} />
179
+ <input
180
+ className="input"
181
+ type="text"
182
+ placeholder="Search…"
183
+ value={filter}
184
+ onChange={(e) => setFilter(e.target.value)}
185
+ />
186
+ {filter && (
187
+ <button
188
+ type="button"
189
+ className="icon-btn"
190
+ aria-label="Clear search"
191
+ onClick={() => setFilter('')}
192
+ >
193
+ <XIcon size={12} />
194
+ </button>
195
+ )}
196
+ </div>
197
+ <Button
198
+ variant="primary"
199
+ size="sm"
200
+ onClick={() => openTaskModal(modal, toast, null, 'queued', reload)}
201
+ >
202
+ <Plus size={14} /> Add task
203
+ </Button>
204
+ </div>
205
+ </header>
206
+
207
+ {loading ? (
208
+ <div className="view-loading">
209
+ <Spinner size="lg" />
210
+ </div>
211
+ ) : (
212
+ <div className="kanban">
213
+ {COLUMNS.map((col) => (
214
+ <KanbanColumn
215
+ key={col.id}
216
+ column={col}
217
+ tasks={filtered.filter((t) => t.status === col.id)}
218
+ focusedId={focusedId}
219
+ onFocus={setFocusedId}
220
+ onMove={moveTask}
221
+ onDelete={deleteTask}
222
+ onEdit={(t) => openTaskModal(modal, toast, t, t.status, reload)}
223
+ onAdd={() => openTaskModal(modal, toast, null, col.id, reload)}
224
+ />
225
+ ))}
226
+ </div>
227
+ )}
228
+ </div>
229
+ );
230
+ }
231
+
232
+ function KanbanColumn({
233
+ column,
234
+ tasks,
235
+ focusedId,
236
+ onFocus,
237
+ onMove,
238
+ onDelete,
239
+ onEdit,
240
+ onAdd,
241
+ }: {
242
+ column: Column;
243
+ tasks: Task[];
244
+ focusedId: string | null;
245
+ onFocus: (id: string | null) => void;
246
+ onMove: (id: string, status: string) => void;
247
+ onDelete: (id: string) => void;
248
+ onEdit: (task: Task) => void;
249
+ onAdd: () => void;
250
+ }) {
251
+ const [dragOver, setDragOver] = useState(false);
252
+ return (
253
+ <div
254
+ className={cn('kanban-column', dragOver && 'kanban-column-drop')}
255
+ data-column={column.id}
256
+ onDragOver={(e) => {
257
+ e.preventDefault();
258
+ setDragOver(true);
259
+ }}
260
+ onDragLeave={() => setDragOver(false)}
261
+ onDrop={(e) => {
262
+ e.preventDefault();
263
+ setDragOver(false);
264
+ const id = e.dataTransfer.getData('text/task-id');
265
+ if (id) onMove(id, column.id);
266
+ }}
267
+ >
268
+ <div className="kanban-col-header">
269
+ <CardTitle>
270
+ <StatusBadge kind={column.kind} dot>
271
+ {column.label}
272
+ </StatusBadge>
273
+ </CardTitle>
274
+ <span className="kanban-col-count tabular-nums">{tasks.length}</span>
275
+ </div>
276
+ <div className="kanban-col-body">
277
+ {tasks.length === 0 ? (
278
+ <div className="kanban-empty">No tasks</div>
279
+ ) : (
280
+ tasks.map((t) => (
281
+ <TaskCard
282
+ key={t.id}
283
+ task={t}
284
+ focused={focusedId === t.id}
285
+ onFocus={() =>
286
+ onFocus(focusedId === t.id ? null : t.id)
287
+ }
288
+ onMove={(dir) => {
289
+ const idx = COLUMNS.findIndex((c) => c.id === t.status);
290
+ const next = idx + dir;
291
+ if (next >= 0 && next < COLUMNS.length)
292
+ onMove(t.id, COLUMNS[next].id);
293
+ }}
294
+ onEdit={() => onEdit(t)}
295
+ onDelete={() => onDelete(t.id)}
296
+ />
297
+ ))
298
+ )}
299
+ </div>
300
+ <footer className="kanban-col-footer">
301
+ <Button variant="ghost" size="sm" onClick={onAdd} className="w-full">
302
+ <Plus size={12} /> Add task
303
+ </Button>
304
+ </footer>
305
+ </div>
306
+ );
307
+ }
308
+
309
+ function TaskCard({
310
+ task,
311
+ focused,
312
+ onFocus,
313
+ onMove,
314
+ onEdit,
315
+ onDelete,
316
+ }: {
317
+ task: Task;
318
+ focused: boolean;
319
+ onFocus: () => void;
320
+ onMove: (dir: -1 | 1) => void;
321
+ onEdit: () => void;
322
+ onDelete: () => void;
323
+ }) {
324
+ return (
325
+ <div
326
+ className={cn(
327
+ 'task-card',
328
+ `priority-${task.priority}`,
329
+ focused && 'task-card-focused',
330
+ )}
331
+ data-task-id={task.id}
332
+ draggable
333
+ onDragStart={(e) => {
334
+ e.dataTransfer.setData('text/task-id', task.id);
335
+ e.dataTransfer.effectAllowed = 'move';
336
+ }}
337
+ onClick={onFocus}
338
+ >
339
+ <div className="task-card-head">
340
+ <span
341
+ className="priority-dot"
342
+ style={{ background: priorityColors[task.priority] || 'var(--info)' }}
343
+ />
344
+ <div className="task-card-title">{task.title}</div>
345
+ </div>
346
+ {task.description && (
347
+ <div className="task-card-desc">{task.description.slice(0, 160)}</div>
348
+ )}
349
+ {task.tags && task.tags.length > 0 && (
350
+ <div className="task-card-tags">
351
+ {task.tags.map((t) => (
352
+ <Tag key={t}>{t}</Tag>
353
+ ))}
354
+ </div>
355
+ )}
356
+ <div className="task-card-footer">
357
+ <span className="task-card-time tabular-nums muted">
358
+ {formatRelative(task.updatedAt || task.createdAt)}
359
+ </span>
360
+ <div className="task-card-actions" onClick={(e) => e.stopPropagation()}>
361
+ <button
362
+ type="button"
363
+ className="icon-btn"
364
+ aria-label="Move left"
365
+ title="Move left"
366
+ onClick={() => onMove(-1)}
367
+ >
368
+ <ChevronLeft size={14} />
369
+ </button>
370
+ <button
371
+ type="button"
372
+ className="icon-btn"
373
+ aria-label="Edit"
374
+ title="Edit (e)"
375
+ onClick={onEdit}
376
+ >
377
+ <Pencil size={14} />
378
+ </button>
379
+ <button
380
+ type="button"
381
+ className="icon-btn icon-btn-danger"
382
+ aria-label="Delete"
383
+ title="Delete"
384
+ onClick={onDelete}
385
+ >
386
+ <Trash2 size={14} />
387
+ </button>
388
+ <button
389
+ type="button"
390
+ className="icon-btn"
391
+ aria-label="Move right"
392
+ title="Move right"
393
+ onClick={() => onMove(1)}
394
+ >
395
+ <ChevronRight size={14} />
396
+ </button>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ );
401
+ }
402
+
403
+ function openTaskModal(
404
+ modal: ReturnType<typeof useModal>,
405
+ toast: ReturnType<typeof useToast>,
406
+ task: Task | null,
407
+ initialStatus: string,
408
+ reload: () => Promise<void>,
409
+ ) {
410
+ let titleEl: HTMLInputElement | null = null;
411
+ let descEl: HTMLTextAreaElement | null = null;
412
+ let tagsEl: HTMLInputElement | null = null;
413
+ let priorityEl: HTMLDivElement | null = null;
414
+ let statusEl: HTMLSelectElement | null = null;
415
+
416
+ const submit = async () => {
417
+ const title = titleEl?.value.trim() || '';
418
+ if (!title) {
419
+ toast.warning('Title is required.');
420
+ titleEl?.focus();
421
+ return;
422
+ }
423
+ const description = descEl?.value.trim() || '';
424
+ const priority =
425
+ (priorityEl?.querySelector<HTMLInputElement>(
426
+ 'input[name="task-priority"]:checked',
427
+ )?.value as Priority) || 'normal';
428
+ const tagsRaw = tagsEl?.value.trim() || '';
429
+ const tags = tagsRaw
430
+ ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean)
431
+ : [];
432
+ const status = statusEl?.value || initialStatus;
433
+
434
+ try {
435
+ if (task) {
436
+ await api.put(`/tasks/${encodeURIComponent(task.id)}`, {
437
+ title,
438
+ description,
439
+ tags,
440
+ priority,
441
+ status,
442
+ });
443
+ toast.success('Task updated.', 1500);
444
+ } else {
445
+ await api.post('/tasks', {
446
+ title,
447
+ description,
448
+ status,
449
+ tags,
450
+ priority,
451
+ });
452
+ toast.success('Task created.', 1500);
453
+ }
454
+ modal.close();
455
+ await reload();
456
+ } catch (err) {
457
+ toast.error(`Save failed: ${(err as Error).message}`);
458
+ }
459
+ };
460
+
461
+ modal.open({
462
+ title: task ? 'Edit task' : 'New task',
463
+ width: 520,
464
+ children: (
465
+ <div className="task-form">
466
+ <label className="field-label" htmlFor="task-title">
467
+ Title *
468
+ </label>
469
+ <input
470
+ ref={(el) => {
471
+ titleEl = el;
472
+ }}
473
+ id="task-title"
474
+ className="input"
475
+ type="text"
476
+ maxLength={200}
477
+ placeholder="What needs to be done?"
478
+ defaultValue={task?.title || ''}
479
+ autoFocus
480
+ />
481
+
482
+ <label className="field-label" htmlFor="task-desc">
483
+ Description
484
+ </label>
485
+ <textarea
486
+ ref={(el) => {
487
+ descEl = el;
488
+ }}
489
+ id="task-desc"
490
+ className="textarea"
491
+ rows={3}
492
+ placeholder="Markdown supported…"
493
+ defaultValue={task?.description || ''}
494
+ />
495
+
496
+ <div className="task-form-row">
497
+ <div className="task-form-field">
498
+ <label className="field-label" htmlFor="task-status">
499
+ Status
500
+ </label>
501
+ <select
502
+ ref={(el) => {
503
+ statusEl = el;
504
+ }}
505
+ id="task-status"
506
+ className="select"
507
+ defaultValue={task?.status || initialStatus}
508
+ >
509
+ {COLUMNS.map((c) => (
510
+ <option key={c.id} value={c.id}>
511
+ {c.label}
512
+ </option>
513
+ ))}
514
+ </select>
515
+ </div>
516
+ <div className="task-form-field">
517
+ <label className="field-label">Priority</label>
518
+ <div
519
+ className="radio-row"
520
+ ref={(el) => {
521
+ priorityEl = el;
522
+ }}
523
+ >
524
+ {PRIORITIES.map((p) => (
525
+ <label key={p} className="radio-label">
526
+ <input
527
+ type="radio"
528
+ name="task-priority"
529
+ value={p}
530
+ defaultChecked={
531
+ (task?.priority || 'normal') === p
532
+ }
533
+ />
534
+ <span className="capitalize">{p}</span>
535
+ </label>
536
+ ))}
537
+ </div>
538
+ </div>
539
+ </div>
540
+
541
+ <label className="field-label" htmlFor="task-tags">
542
+ Tags
543
+ </label>
544
+ <input
545
+ ref={(el) => {
546
+ tagsEl = el;
547
+ }}
548
+ id="task-tags"
549
+ className="input"
550
+ type="text"
551
+ placeholder="comma-separated, e.g. bug, frontend"
552
+ defaultValue={(task?.tags || []).join(', ')}
553
+ />
554
+ </div>
555
+ ),
556
+ footer: (
557
+ <div className="modal-footer-actions">
558
+ <Button variant="ghost" onClick={() => modal.close()}>
559
+ Cancel
560
+ </Button>
561
+ <Button variant="primary" onClick={submit}>
562
+ {task ? 'Save' : 'Create'}
563
+ </Button>
564
+ </div>
565
+ ),
566
+ });
567
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noUnusedLocals": false,
10
+ "noUnusedParameters": false,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "allowSyntheticDefaultImports": true,
18
+ "useDefineForClassFields": true,
19
+ "types": ["node"]
20
+ },
21
+ "include": ["src/**/*"],
22
+ "exclude": ["node_modules", "dist"]
23
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, resolve } from 'node:path';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export default defineConfig({
9
+ plugins: [react()],
10
+ root: 'src',
11
+ base: './',
12
+ build: {
13
+ outDir: resolve(__dirname, 'dist'),
14
+ emptyOutDir: true,
15
+ sourcemap: true,
16
+ rollupOptions: {
17
+ input: resolve(__dirname, 'src/index.html'),
18
+ },
19
+ },
20
+ server: {
21
+ port: 5173,
22
+ strictPort: false,
23
+ },
24
+ });
@@ -1,52 +0,0 @@
1
- {
2
- "$schema": "https://opencode.ai/config.json",
3
- "model": "opencode/deepseek-v4-flash-free",
4
- "small_model": "opencode/deepseek-v4-flash-free",
5
- "default_agent": "odin",
6
- "permission": "allow",
7
- "snapshot": false,
8
- "mcp": {
9
- "supabase": {
10
- "type": "remote",
11
- "url": "https://mcp.supabase.com/mcp",
12
- "enabled": true
13
- },
14
- "hindsight": {
15
- "type": "remote",
16
- "url": "https://memory-api.polderlabs.io/mcp",
17
- "enabled": true,
18
- "oauth": false,
19
- "headers": {
20
- "Authorization": "Bearer YOUR_HINDSIGHT_API_KEY",
21
- "Content-Type": "application/json"
22
- }
23
- },
24
- "semble": {
25
- "type": "local",
26
- "command": [
27
- "uvx",
28
- "--from",
29
- "semble[mcp]",
30
- "semble"
31
- ],
32
- "enabled": true
33
- }
34
- },
35
- "plugin": [
36
- ["./plugins/bizar/index.ts", {
37
- "loopThresholdWarn": 5,
38
- "loopThresholdEscalate": 8,
39
- "loopThresholdBlock": 12,
40
- "loopWindowSize": 10
41
- }]
42
- ],
43
- "tools": {
44
- "bizar_plan_action": true,
45
- "bizar_get_plan_comments": true,
46
- "bizar_wait_for_feedback": true,
47
- "bizar_spawn_background": true,
48
- "bizar_status": true,
49
- "bizar_collect": true,
50
- "bizar_kill": true
51
- }
52
- }