@polderlabs/bizar 5.2.0 → 5.3.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.
Files changed (56) hide show
  1. package/bizar-dash/dist/assets/{MobileChat-TCputYzr.js → MobileChat-OidrxXR9.js} +1 -1
  2. package/bizar-dash/dist/assets/{MobileChat-TCputYzr.js.map → MobileChat-OidrxXR9.js.map} +1 -1
  3. package/bizar-dash/dist/assets/{MobileSettings-BxcbL5XT.js → MobileSettings-B91gVFii.js} +1 -1
  4. package/bizar-dash/dist/assets/{MobileSettings-BxcbL5XT.js.map → MobileSettings-B91gVFii.js.map} +1 -1
  5. package/bizar-dash/dist/assets/{icons-DRDXfbBP.js → icons-Bd8piSb2.js} +18 -13
  6. package/bizar-dash/dist/assets/icons-Bd8piSb2.js.map +1 -0
  7. package/bizar-dash/dist/assets/{main-xFpWMd32.css → main-3G6mOhiK.css} +1 -1
  8. package/bizar-dash/dist/assets/main-6YUZY968.js +19 -0
  9. package/bizar-dash/dist/assets/main-6YUZY968.js.map +1 -0
  10. package/bizar-dash/dist/assets/{mobile-j3rOZK6v.js → mobile-CSsp30lr.js} +2 -2
  11. package/bizar-dash/dist/assets/{mobile-j3rOZK6v.js.map → mobile-CSsp30lr.js.map} +1 -1
  12. package/bizar-dash/dist/assets/{mobile--17fkfrl.js → mobile-DtwK7DOV.js} +1 -1
  13. package/bizar-dash/dist/assets/{mobile--17fkfrl.js.map → mobile-DtwK7DOV.js.map} +1 -1
  14. package/bizar-dash/dist/assets/{useSlashCommands-BG-DhEck.js → useSlashCommands-DEkxEvOO.js} +2 -2
  15. package/bizar-dash/dist/assets/{useSlashCommands-BG-DhEck.js.map → useSlashCommands-DEkxEvOO.js.map} +1 -1
  16. package/bizar-dash/dist/index.html +5 -5
  17. package/bizar-dash/dist/mobile.html +3 -3
  18. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  19. package/bizar-dash/src/server/auth.mjs +14 -1
  20. package/bizar-dash/src/server/eval-store.mjs +55 -0
  21. package/bizar-dash/src/server/eval.mjs +63 -0
  22. package/bizar-dash/src/server/plugins/permission-audit.mjs +109 -0
  23. package/bizar-dash/src/server/plugins/sandbox.mjs +76 -2
  24. package/bizar-dash/src/server/plugins/store.mjs +4 -0
  25. package/bizar-dash/src/server/routes/eval.mjs +42 -2
  26. package/bizar-dash/src/server/routes/plugins.mjs +44 -4
  27. package/bizar-dash/src/server/routes/users.mjs +18 -0
  28. package/bizar-dash/src/server/schedules-runner.mjs +61 -0
  29. package/bizar-dash/src/web/components/EvalExportButton.tsx +27 -0
  30. package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +62 -0
  31. package/bizar-dash/src/web/components/MarketplacePluginCard.tsx +51 -0
  32. package/bizar-dash/src/web/components/PluginCard.tsx +2 -2
  33. package/bizar-dash/src/web/components/PluginPermissions.tsx +1 -1
  34. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +1 -0
  35. package/bizar-dash/src/web/components/Topbar.tsx +3 -0
  36. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +28 -0
  37. package/bizar-dash/src/web/styles/main.css +139 -0
  38. package/bizar-dash/src/web/views/Doctor.tsx +3 -3
  39. package/bizar-dash/src/web/views/Eval.tsx +225 -25
  40. package/bizar-dash/src/web/views/EvalReport.tsx +7 -3
  41. package/bizar-dash/src/web/views/Marketplace.tsx +135 -0
  42. package/bizar-dash/src/web/views/Plugins.tsx +9 -3
  43. package/bizar-dash/src/web/views/Schedules.tsx +1 -1
  44. package/bizar-dash/tests/a11y/components.test.tsx +149 -0
  45. package/bizar-dash/tests/a11y/forms.test.tsx +191 -0
  46. package/bizar-dash/tests/a11y/navigation.test.tsx +112 -0
  47. package/bizar-dash/tests/components/marketplace-plugin-card.test.tsx +144 -0
  48. package/bizar-dash/tests/eval-csv.test.mjs +67 -0
  49. package/bizar-dash/tests/eval-scheduled.test.mjs +84 -0
  50. package/bizar-dash/tests/plugins-permissions.test.mjs +525 -0
  51. package/cli/commands/dash.mjs +5 -0
  52. package/cli/commands/tailscale.mjs +8 -1
  53. package/package.json +1 -1
  54. package/bizar-dash/dist/assets/icons-DRDXfbBP.js.map +0 -1
  55. package/bizar-dash/dist/assets/main-D5Ditnrd.js +0 -19
  56. package/bizar-dash/dist/assets/main-D5Ditnrd.js.map +0 -1
@@ -0,0 +1,27 @@
1
+ // src/components/EvalExportButton.tsx — v5.3.0
2
+ // Export button for downloading eval run results as CSV.
3
+
4
+ import React from 'react';
5
+ import { Download } from 'lucide-react';
6
+ import { Button } from './Button';
7
+ import { api } from '../lib/api';
8
+
9
+ type Props = {
10
+ runId: string;
11
+ variant?: 'primary' | 'secondary' | 'ghost';
12
+ size?: 'sm' | 'md' | 'lg';
13
+ };
14
+
15
+ export function EvalExportButton({ runId, variant = 'secondary', size = 'sm' }: Props) {
16
+ const onExport = () => {
17
+ const token = api.getToken();
18
+ window.open(`/api/eval/runs/${encodeURIComponent(runId)}/export.csv?_t=${encodeURIComponent(token)}`, '_blank');
19
+ };
20
+
21
+ return (
22
+ <Button variant={variant} size={size} onClick={onExport} title="Export results as CSV">
23
+ <Download size={14} aria-hidden />
24
+ Export CSV
25
+ </Button>
26
+ );
27
+ }
@@ -0,0 +1,62 @@
1
+ // src/web/components/InstallConfirmDialog.tsx — confirmation dialog for installing a marketplace plugin.
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { AlertTriangle } from 'lucide-react';
5
+ import { useModal } from './Modal';
6
+ import { Button } from './Button';
7
+ import { PluginPermissions } from './PluginPermissions';
8
+ import type { MarketplacePlugin } from './MarketplacePluginCard';
9
+
10
+ export type InstallConfirmDialogProps = {
11
+ plugin: MarketplacePlugin;
12
+ onConfirm: () => void | Promise<void>;
13
+ onCancel: () => void;
14
+ installing: boolean;
15
+ };
16
+
17
+ export function InstallConfirmDialog({ plugin, onConfirm, onCancel, installing }: InstallConfirmDialogProps) {
18
+ const modal = useModal();
19
+ // Track the plugin id we've opened the modal for, so we don't re-open on every render.
20
+ const openedForRef = useRef<string | null>(null);
21
+
22
+ useEffect(() => {
23
+ if (!plugin) return;
24
+ // Only open if we haven't already opened for this plugin.
25
+ if (openedForRef.current === plugin.id) return;
26
+ openedForRef.current = plugin.id;
27
+
28
+ modal.open({
29
+ title: `Install ${plugin.name}?`,
30
+ width: 480,
31
+ onClose: () => {
32
+ openedForRef.current = null;
33
+ onCancel();
34
+ },
35
+ children: (
36
+ <div className="install-confirm">
37
+ <h3>{plugin.name} v{plugin.version}</h3>
38
+ {plugin.description && <p>{plugin.description}</p>}
39
+
40
+ <div className="install-warning">
41
+ <AlertTriangle size={16} />
42
+ <p>This plugin will run code on your machine. It requests these permissions:</p>
43
+ </div>
44
+
45
+ <PluginPermissions permissions={plugin.permissions ?? []} />
46
+
47
+ <div className="install-confirm-actions">
48
+ <Button onClick={onCancel} disabled={installing}>Cancel</Button>
49
+ <Button variant="primary" onClick={onConfirm} disabled={installing}>
50
+ {installing ? 'Installing...' : 'Install'}
51
+ </Button>
52
+ </div>
53
+ </div>
54
+ ),
55
+ });
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [plugin, installing]);
58
+
59
+ // This component manages its own modal state via useModal.
60
+ // Render nothing in the React tree — the modal is rendered via portal.
61
+ return null;
62
+ }
@@ -0,0 +1,51 @@
1
+ // src/web/components/MarketplacePluginCard.tsx — card for a plugin listed in the marketplace.
2
+
3
+ import { Card, CardTitle, CardMeta } from './Card';
4
+ import { Button } from './Button';
5
+ import { PluginPermissions } from './PluginPermissions';
6
+
7
+ export type MarketplacePlugin = {
8
+ id: string;
9
+ name: string;
10
+ version: string;
11
+ author: string;
12
+ description?: string;
13
+ category: string;
14
+ tags?: string[];
15
+ permissions?: string[];
16
+ };
17
+
18
+ export type MarketplacePluginCardProps = {
19
+ plugin: MarketplacePlugin;
20
+ onInstall: () => void;
21
+ onView: () => void;
22
+ };
23
+
24
+ export function MarketplacePluginCard({ plugin, onInstall, onView }: MarketplacePluginCardProps) {
25
+ return (
26
+ <Card className="marketplace-plugin-card">
27
+ <div className="marketplace-plugin-head">
28
+ <div>
29
+ <CardTitle>{plugin.name}</CardTitle>
30
+ <CardMeta>v{plugin.version} · {plugin.category}</CardMeta>
31
+ </div>
32
+ <div className="marketplace-plugin-author">{plugin.author}</div>
33
+ </div>
34
+ {plugin.description && (
35
+ <p className="marketplace-plugin-description">{plugin.description}</p>
36
+ )}
37
+ {plugin.tags && plugin.tags.length > 0 && (
38
+ <div className="marketplace-plugin-tags">
39
+ {plugin.tags.map((tag) => (
40
+ <span key={tag} className="tag">{tag}</span>
41
+ ))}
42
+ </div>
43
+ )}
44
+ <PluginPermissions permissions={plugin.permissions ?? []} />
45
+ <div className="marketplace-plugin-actions">
46
+ <Button onClick={onView}>Details</Button>
47
+ <Button variant="primary" onClick={onInstall}>Install</Button>
48
+ </div>
49
+ </Card>
50
+ );
51
+ }
@@ -46,10 +46,10 @@ export function PluginCard({ plugin, onToggle, onUninstall, onConfigure }: Plugi
46
46
  )}
47
47
  <PluginPermissions permissions={plugin.permissions ?? []} />
48
48
  <div className="plugin-card-actions">
49
- <Button size="sm" onClick={() => onConfigure(plugin.id)}>
49
+ <Button size="sm" onClick={() => onConfigure(plugin.id)} aria-label={`Configure ${plugin.name}`}>
50
50
  Configure
51
51
  </Button>
52
- <Button size="sm" variant="danger" onClick={() => onUninstall(plugin.id)}>
52
+ <Button size="sm" variant="danger" onClick={() => onUninstall(plugin.id)} aria-label={`Uninstall ${plugin.name}`}>
53
53
  Uninstall
54
54
  </Button>
55
55
  </div>
@@ -16,7 +16,7 @@ export type PluginPermissionsProps = {
16
16
 
17
17
  export function PluginPermissions({ permissions }: PluginPermissionsProps) {
18
18
  return (
19
- <div className="plugin-permissions">
19
+ <div className="plugin-permissions" aria-label="Required permissions">
20
20
  {permissions.map((perm) => {
21
21
  const info = PERMISSION_LABELS[perm] ?? { label: perm, icon: Shield, color: 'gray', description: '' };
22
22
  const Icon = info.icon;
@@ -63,6 +63,7 @@ export function ScheduleTemplateCard({ template, onUse }: Props) {
63
63
  variant="secondary"
64
64
  size="sm"
65
65
  onClick={() => onUse(template)}
66
+ aria-label={`Use template: ${template.name}`}
66
67
  >
67
68
  Use this template
68
69
  </Button>
@@ -25,6 +25,7 @@ import {
25
25
  Brain,
26
26
  Stethoscope,
27
27
  ClipboardCheck,
28
+ Store,
28
29
  type LucideIcon,
29
30
  } from 'lucide-react';
30
31
  import { cn } from '../lib/utils';
@@ -51,6 +52,8 @@ export const TABS: TabDef[] = [
51
52
  { id: 'skills', label: 'Skills', icon: Sparkles },
52
53
  { id: 'memory', label: 'Memory', icon: Brain },
53
54
  { id: 'mods', label: 'Mods', icon: Puzzle },
55
+ // v5.3.0 — Plugin marketplace browse + install UI.
56
+ { id: 'marketplace', label: 'Marketplace', icon: Store },
54
57
  { id: 'schedules', label: 'Schedules', icon: Clock },
55
58
  { id: 'plugins', label: 'Plugins', icon: Puzzle },
56
59
  { id: 'history', label: 'History', icon: HistoryIcon },
@@ -120,6 +120,34 @@ export function VoiceNotesPanel({ vaultPath, refreshKey = 0 }: Props) {
120
120
 
121
121
  useEffect(() => {
122
122
  loadNotes();
123
+
124
+ // v5.2.0 — Listen for transcription worker updates
125
+ const handleMessage = (e: MessageEvent) => {
126
+ try {
127
+ const msg = JSON.parse(e.data);
128
+ if (msg.type === 'voice:updated' && msg.noteId) {
129
+ // Update the matching note's transcript in place
130
+ setNotes((prev) => prev.map((n) =>
131
+ n.id === msg.noteId
132
+ ? { ...n, transcript: msg.patch?.transcript ?? n.transcript }
133
+ : n
134
+ ));
135
+ }
136
+ } catch { /* ignore parse errors */ }
137
+ };
138
+
139
+ // Connect to WS for real-time transcript updates
140
+ const wsUrl = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
141
+ let ws: WebSocket | null = null;
142
+ try {
143
+ ws = new WebSocket(wsUrl);
144
+ ws.addEventListener('message', handleMessage);
145
+ } catch { /* ignore */ }
146
+
147
+ return () => {
148
+ ws?.removeEventListener('message', handleMessage);
149
+ ws?.close();
150
+ };
123
151
  }, [vaultPath, refreshKey]);
124
152
 
125
153
  const handleDelete = async (id: string) => {
@@ -9162,6 +9162,120 @@ select:focus-visible,
9162
9162
  margin-top: var(--space-2);
9163
9163
  }
9164
9164
 
9165
+ /* ─── v5.3.0 — Plugin marketplace ──────────────────────────────── */
9166
+ .marketplace-grid {
9167
+ display: grid;
9168
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
9169
+ gap: 16px;
9170
+ margin-top: 24px;
9171
+ }
9172
+
9173
+ .marketplace-plugin-card {
9174
+ display: flex;
9175
+ flex-direction: column;
9176
+ }
9177
+
9178
+ .marketplace-plugin-head {
9179
+ display: flex;
9180
+ align-items: flex-start;
9181
+ justify-content: space-between;
9182
+ gap: var(--space-3);
9183
+ }
9184
+
9185
+ .marketplace-plugin-description {
9186
+ color: var(--text-dim);
9187
+ margin: var(--space-2) 0;
9188
+ font-size: 13px;
9189
+ line-height: 1.5;
9190
+ }
9191
+
9192
+ .marketplace-plugin-tags {
9193
+ display: flex;
9194
+ flex-wrap: wrap;
9195
+ gap: 4px;
9196
+ margin: 8px 0;
9197
+ }
9198
+
9199
+ .marketplace-plugin-tags .tag {
9200
+ background: var(--surface-3, rgba(255, 255, 255, 0.05));
9201
+ color: var(--text-dim, #8b949e);
9202
+ padding: 2px 6px;
9203
+ border-radius: 8px;
9204
+ font-size: 10px;
9205
+ }
9206
+
9207
+ .marketplace-plugin-actions {
9208
+ display: flex;
9209
+ gap: var(--space-2);
9210
+ flex-wrap: wrap;
9211
+ margin-top: auto;
9212
+ padding-top: var(--space-2);
9213
+ }
9214
+
9215
+ .marketplace-plugin-author {
9216
+ font-size: 12px;
9217
+ color: var(--text-dim);
9218
+ }
9219
+
9220
+ .marketplace-search-row {
9221
+ margin-bottom: 16px;
9222
+ }
9223
+
9224
+ .category-chips {
9225
+ display: flex;
9226
+ gap: 8px;
9227
+ margin: 16px 0;
9228
+ flex-wrap: wrap;
9229
+ }
9230
+
9231
+ .category-chips button {
9232
+ padding: 6px 12px;
9233
+ border: 1px solid var(--border, #30363d);
9234
+ border-radius: 14px;
9235
+ background: transparent;
9236
+ color: var(--text-dim);
9237
+ cursor: pointer;
9238
+ font-size: 13px;
9239
+ }
9240
+
9241
+ .category-chips button:hover {
9242
+ background: var(--surface-2);
9243
+ }
9244
+
9245
+ .category-chips button.active {
9246
+ background: var(--accent, #8b5cf6);
9247
+ color: white;
9248
+ border-color: var(--accent, #8b5cf6);
9249
+ }
9250
+
9251
+ .install-confirm h3 {
9252
+ margin: 0 0 var(--space-2);
9253
+ font-size: 16px;
9254
+ }
9255
+
9256
+ .install-warning {
9257
+ display: flex;
9258
+ gap: 8px;
9259
+ background: rgba(245, 158, 11, 0.1);
9260
+ padding: 12px;
9261
+ border-radius: 6px;
9262
+ margin: 12px 0;
9263
+ color: #d29922;
9264
+ font-size: 13px;
9265
+ }
9266
+
9267
+ .install-warning p {
9268
+ margin: 0;
9269
+ color: var(--text);
9270
+ }
9271
+
9272
+ .install-confirm-actions {
9273
+ display: flex;
9274
+ gap: var(--space-2);
9275
+ justify-content: flex-end;
9276
+ margin-top: var(--space-4);
9277
+ }
9278
+
9165
9279
  /* ─── Plugin permissions ──────────────────────────────────────────── */
9166
9280
  .plugin-permissions {
9167
9281
  display: flex;
@@ -9610,3 +9724,28 @@ select:focus-visible,
9610
9724
  display: flex;
9611
9725
  flex-direction: column;
9612
9726
  }
9727
+
9728
+ /* ─── High-contrast mode (prefers-contrast: more) ──────────────────── */
9729
+ @media (prefers-contrast: more) {
9730
+ :root {
9731
+ --text-dim: var(--text);
9732
+ --text-muted: var(--text);
9733
+ }
9734
+
9735
+ [data-theme='light'] {
9736
+ --text-dim: #374151;
9737
+ --text-muted: #374151;
9738
+ }
9739
+
9740
+ :focus-visible,
9741
+ button:focus-visible,
9742
+ [role='button']:focus-visible,
9743
+ a:focus-visible,
9744
+ input:focus-visible,
9745
+ textarea:focus-visible,
9746
+ select:focus-visible,
9747
+ [tabindex]:focus-visible {
9748
+ outline-width: 3px;
9749
+ outline-style: solid;
9750
+ }
9751
+ }
@@ -208,7 +208,7 @@ function DoctorInner({
208
208
  </div>
209
209
  <div className="doctor-header-meta muted">
210
210
  <Clock size={12} aria-hidden />
211
- <span>
211
+ <span aria-live="polite" aria-atomic="true">
212
212
  v{data.bizarVersion} · {data.platform}/{data.arch} ·{' '}
213
213
  up {Math.floor(data.uptime)}s
214
214
  {lastFetched ? ` · refreshed ${formatTime(new Date(lastFetched))}` : ''}
@@ -266,7 +266,7 @@ function DoctorInner({
266
266
  </CardTitle>
267
267
  <CardMeta>Run a check, view logs, or export the snapshot.</CardMeta>
268
268
  <div className="doctor-actions-row">
269
- <Button variant="primary" onClick={fetchSnapshot} disabled={refreshing}>
269
+ <Button variant="primary" onClick={fetchSnapshot} disabled={refreshing} aria-label="Refresh diagnostics">
270
270
  {refreshing ? <Spinner size="sm" /> : <RefreshCw size={12} />}
271
271
  {refreshing ? 'Running…' : 'Run Health Check Now'}
272
272
  </Button>
@@ -279,7 +279,7 @@ function DoctorInner({
279
279
  <Button
280
280
  variant="ghost"
281
281
  onClick={() => setAutoRefresh((v) => !v)}
282
- title="Toggle auto-refresh"
282
+ aria-label={autoRefresh ? 'Pause auto-refresh' : 'Resume auto-refresh'}
283
283
  >
284
284
  {autoRefresh ? 'Pause auto-refresh' : 'Resume auto-refresh'}
285
285
  </Button>