@polderlabs/bizar 4.9.0 → 5.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 (107) hide show
  1. package/bizar-dash/dist/assets/icons-CFqu2M-c.js +656 -0
  2. package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +1 -0
  3. package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
  4. package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
  5. package/bizar-dash/dist/assets/main-Dl8yY5_H.js +16 -0
  6. package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +1 -0
  7. package/bizar-dash/dist/assets/{main-DfmIfOUS.css → main-ZAfGKENE.css} +1 -1
  8. package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
  9. package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
  10. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +2 -0
  11. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +1 -0
  12. package/bizar-dash/dist/assets/mobile-DHXXbn1A.js +1 -0
  13. package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DHXXbn1A.js.map} +1 -1
  14. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
  15. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
  16. package/bizar-dash/dist/index.html +6 -3
  17. package/bizar-dash/dist/mobile.html +5 -2
  18. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  19. package/bizar-dash/skills/eval/SKILL.md +237 -0
  20. package/bizar-dash/src/server/api.mjs +28 -0
  21. package/bizar-dash/src/server/auth.mjs +155 -1
  22. package/bizar-dash/src/server/eval-store.mjs +226 -0
  23. package/bizar-dash/src/server/eval.mjs +347 -0
  24. package/bizar-dash/src/server/ocr.mjs +55 -0
  25. package/bizar-dash/src/server/plugins/registry.mjs +363 -0
  26. package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
  27. package/bizar-dash/src/server/plugins/store.mjs +659 -0
  28. package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
  29. package/bizar-dash/src/server/routes/eval.mjs +147 -0
  30. package/bizar-dash/src/server/routes/ocr.mjs +182 -0
  31. package/bizar-dash/src/server/routes/plugins.mjs +220 -0
  32. package/bizar-dash/src/server/routes/users.mjs +84 -0
  33. package/bizar-dash/src/server/routes/voice.mjs +131 -0
  34. package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
  35. package/bizar-dash/src/server/voice-store.mjs +202 -0
  36. package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
  37. package/bizar-dash/src/server/workspaces.mjs +626 -0
  38. package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
  39. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
  40. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
  41. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
  42. package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
  43. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
  44. package/bizar-dash/src/web/styles/memory.css +82 -0
  45. package/bizar-dash/src/web/views/Memory.tsx +17 -2
  46. package/bizar-dash/src/web/views/Settings.tsx +3 -0
  47. package/bizar-dash/src/web/views/Workspace.tsx +294 -0
  48. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
  49. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
  50. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
  51. package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
  52. package/bizar-dash/tests/clipboard.test.mjs +147 -0
  53. package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
  54. package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
  55. package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
  56. package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
  57. package/bizar-dash/tests/eval/report.test.mjs +284 -0
  58. package/bizar-dash/tests/eval/runner.test.mjs +471 -0
  59. package/bizar-dash/tests/ocr.test.mjs +87 -0
  60. package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
  61. package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
  62. package/bizar-dash/tests/plugins-store.test.mjs +455 -0
  63. package/bizar-dash/tests/users.test.mjs +108 -0
  64. package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
  65. package/bizar-dash/tests/voice-store.test.mjs +148 -0
  66. package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
  67. package/bizar-dash/tests/workspaces.test.mjs +527 -0
  68. package/cli/bin.mjs +72 -2
  69. package/cli/commands/clip.mjs +146 -0
  70. package/cli/commands/deploy/cloudflare.mjs +250 -0
  71. package/cli/commands/deploy/docker.mjs +221 -0
  72. package/cli/commands/deploy/fly.mjs +161 -0
  73. package/cli/commands/deploy/vercel.mjs +225 -0
  74. package/cli/commands/deploy.mjs +240 -0
  75. package/cli/commands/eval.mjs +378 -0
  76. package/cli/commands/marketplace.mjs +64 -0
  77. package/cli/commands/ocr.mjs +165 -0
  78. package/cli/commands/plugin.mjs +358 -0
  79. package/cli/commands/voice.mjs +211 -0
  80. package/cli/commands/workspace.mjs +247 -0
  81. package/package.json +12 -8
  82. package/templates/deploy/cloudflare/README.md +32 -0
  83. package/templates/deploy/cloudflare/functions-index.template.js +15 -0
  84. package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
  85. package/templates/deploy/docker/.env.template +16 -0
  86. package/templates/deploy/docker/README.md +58 -0
  87. package/templates/deploy/docker/docker-compose.template.yml +23 -0
  88. package/templates/deploy/fly/README.md +35 -0
  89. package/templates/deploy/fly/fly.toml.template +28 -0
  90. package/templates/deploy/vercel/README.md +29 -0
  91. package/templates/deploy/vercel/api-index.template.js +18 -0
  92. package/templates/deploy/vercel/vercel.json.template +16 -0
  93. package/templates/eval-fixtures/README.md +58 -0
  94. package/templates/eval-fixtures/code-search-basic.json +28 -0
  95. package/templates/eval-fixtures/latency-bounds.json +16 -0
  96. package/templates/eval-fixtures/regression-suite.json +79 -0
  97. package/templates/eval-fixtures/response-format.json +30 -0
  98. package/templates/eval-fixtures/tool-call-correctness.json +24 -0
  99. package/templates/plugin-template/README.md +121 -0
  100. package/templates/plugin-template/index.js +66 -0
  101. package/templates/plugin-template/plugin.json +42 -0
  102. package/templates/plugin-template/tests/plugin.test.js +83 -0
  103. package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
  104. package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
  105. package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
  106. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
  107. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +0 -1
@@ -29,10 +29,13 @@
29
29
  }
30
30
  })();
31
31
  </script>
32
- <script type="module" crossorigin src="/assets/main-DaC1Lc6q.js"></script>
33
- <link rel="modulepreload" crossorigin href="/assets/mobile-D5WTWvuh.js">
32
+ <script type="module" crossorigin src="/assets/main-Dl8yY5_H.js"></script>
33
+ <link rel="modulepreload" crossorigin href="/assets/react-vendor-DZRUXSPQ.js">
34
+ <link rel="modulepreload" crossorigin href="/assets/icons-CFqu2M-c.js">
35
+ <link rel="modulepreload" crossorigin href="/assets/markdown-DIquRulQ.js">
36
+ <link rel="modulepreload" crossorigin href="/assets/mobile-C2gysFOZ.js">
34
37
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
35
- <link rel="stylesheet" crossorigin href="/assets/main-DfmIfOUS.css">
38
+ <link rel="stylesheet" crossorigin href="/assets/main-ZAfGKENE.css">
36
39
  </head>
37
40
  <body>
38
41
  <div id="root"></div>
@@ -12,8 +12,11 @@
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com" />
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
14
  <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
15
- <script type="module" crossorigin src="/assets/mobile-CL5uUQEC.js"></script>
16
- <link rel="modulepreload" crossorigin href="/assets/mobile-D5WTWvuh.js">
15
+ <script type="module" crossorigin src="/assets/mobile-DHXXbn1A.js"></script>
16
+ <link rel="modulepreload" crossorigin href="/assets/react-vendor-DZRUXSPQ.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/icons-CFqu2M-c.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/markdown-DIquRulQ.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/mobile-C2gysFOZ.js">
17
20
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
18
21
  </head>
19
22
  <body>
@@ -1 +1 @@
1
- {"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":19.332510000000013,"failed":false}],[":tests/components/Button.test.tsx",{"duration":67.29968700000006,"failed":false}],[":tests/components/Card.test.tsx",{"duration":22.37102600000003,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":86.52507100000003,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":103.76524499999994,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":107.89120399999996,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":4.197033999999974,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":28.41539800000004,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":70.67926699999998,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":49.17134299999998,"failed":false}],[":tests/backup-restore.test.tsx",{"duration":103.80786699999999,"failed":false}],[":tests/a11y.test.tsx",{"duration":537.859814,"failed":false}],[":tests/lib/search-fuzzy.test.ts",{"duration":6.371206000000029,"failed":false}],[":tests/components/settings-search.test.tsx",{"duration":680.7986129999999,"failed":false}],[":tests/memory-graph-view.test.tsx",{"duration":34.91235599999999,"failed":false}]]}
1
+ {"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":24.136259999999993,"failed":false}],[":tests/components/Button.test.tsx",{"duration":83.28092300000003,"failed":false}],[":tests/components/Card.test.tsx",{"duration":29.62249799999995,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":108.65311200000008,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":127.17467799999997,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":129.80008299999997,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":3.760281999999961,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":33.259794000000056,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":87.20143099999996,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":55.38087500000006,"failed":false}],[":tests/backup-restore.test.tsx",{"duration":148.675974,"failed":false}],[":tests/a11y.test.tsx",{"duration":638.991055,"failed":false}],[":tests/lib/search-fuzzy.test.ts",{"duration":8.135348000000022,"failed":false}],[":tests/components/settings-search.test.tsx",{"duration":693.5110860000001,"failed":false}],[":tests/memory-graph-view.test.tsx",{"duration":44.16916100000003,"failed":false}],[":tests/components/workspace-selector.test.tsx",{"duration":103.51388600000007,"failed":false}],[":tests/components/screenshot-ocr.test.tsx",{"duration":37.170334000000025,"failed":false}],[":tests/voice-recorder.test.tsx",{"duration":99.73518000000001,"failed":false}]]}
@@ -0,0 +1,237 @@
1
+ # Eval Framework Skill
2
+
3
+ v5.0.0 — Evaluate AI agent outputs against golden fixtures for regression testing, agent comparison, and quality assurance.
4
+
5
+ ## Overview
6
+
7
+ The eval framework runs fixtures — JSON files that define a prompt, expected outputs, and validation rules — against a live agent and reports pass/fail results.
8
+
9
+ ## Fixture Schema
10
+
11
+ ```json
12
+ {
13
+ "id": "unique-fixture-id",
14
+ "name": "Human Readable Name",
15
+ "description": "What this fixture verifies",
16
+ "agent": "thor",
17
+ "prompt": "The prompt to send to the agent",
18
+ "expected": {
19
+ "contains": ["substring 1", "substring 2"],
20
+ "notContains": ["forbidden substring"],
21
+ "regex": ["expected regex pattern"],
22
+ "jsonSchema": null,
23
+ "maxTokens": 2000,
24
+ "maxLatencyMs": 30000
25
+ },
26
+ "tags": ["smoke", "regression"]
27
+ }
28
+ ```
29
+
30
+ ### Fields
31
+
32
+ | Field | Type | Required | Description |
33
+ |-------|------|----------|-------------|
34
+ | `id` | string | Yes | Unique identifier across all fixtures |
35
+ | `name` | string | Yes | Human-readable name |
36
+ | `description` | string | Yes | What the fixture verifies |
37
+ | `agent` | string | Yes | Agent to use: `thor`, `tyr`, `heimdall`, etc. |
38
+ | `prompt` | string | Yes | The prompt sent to the agent |
39
+ | `expected` | object | Yes | Validation rules |
40
+ | `tags` | string[] | No | Tags for filtering |
41
+
42
+ ### Expected Validation Rules
43
+
44
+ | Field | Type | Description |
45
+ |-------|------|-------------|
46
+ | `contains` | string[] | All substrings must appear in the response |
47
+ | `notContains` | string[] | No substring may appear in the response |
48
+ | `regex` | string[] | All regex patterns must match the response |
49
+ | `jsonSchema` | object\|null | If set, response must be valid JSON matching the schema |
50
+ | `maxTokens` | number | Maximum total tokens in the response |
51
+ | `maxLatencyMs` | number | Maximum response time in milliseconds |
52
+
53
+ ## Running Evals
54
+
55
+ ### CLI
56
+
57
+ ```bash
58
+ # Run a fixture suite
59
+ bizar eval run ./templates/eval-fixtures
60
+
61
+ # Run with custom concurrency
62
+ bizar eval run ./path/to/fixtures --concurrency=5 --agent=thor
63
+
64
+ # List recent runs
65
+ bizar eval list --limit=20
66
+
67
+ # Show run details
68
+ bizar eval show run_2026-07-05-120000
69
+
70
+ # Diff two runs
71
+ bizar eval diff run_2026-07-05 run_2026-07-04
72
+
73
+ # Scaffold a new fixture
74
+ bizar eval init ./my-fixtures
75
+
76
+ # Validate a fixture without running
77
+ bizar eval validate ./my-fixtures/my-fixture.json
78
+ ```
79
+
80
+ ### REST API
81
+
82
+ ```bash
83
+ # Run a suite (POST)
84
+ curl -X POST http://127.0.0.1:4321/api/eval/run \
85
+ -H "Content-Type: application/json" \
86
+ -d '{"suitePath": "./templates/eval-fixtures", "concurrency": 5}'
87
+
88
+ # List runs (GET)
89
+ curl http://127.0.0.1:4321/api/eval/runs?limit=20
90
+
91
+ # Get run details (GET)
92
+ curl http://127.0.0.1:4321/api/eval/runs/run_2026-07-05-120000
93
+
94
+ # Diff runs (GET)
95
+ curl http://127.0.0.1:4321/api/eval/runs/run_2026-07-05/compare/run_2026-07-04
96
+
97
+ # List fixtures in a path (GET)
98
+ curl "http://127.0.0.1:4321/api/eval/fixtures?path=./templates/eval-fixtures"
99
+ ```
100
+
101
+ ## Writing Good Fixtures
102
+
103
+ ### Start with `contains`
104
+
105
+ The simplest check — verify a substring appears in the response:
106
+
107
+ ```json
108
+ {
109
+ "id": "my-fixture",
110
+ "prompt": "What is 2+2?",
111
+ "expected": {
112
+ "contains": ["4", "four"]
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### Use `notContains` for Anti-Patterns
118
+
119
+ Catch common mistakes the agent should NOT make:
120
+
121
+ ```json
122
+ {
123
+ "id": "no-apologies",
124
+ "prompt": "Tell me about Python",
125
+ "expected": {
126
+ "notContains": ["I'm sorry", "I apologize", "as an AI"]
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### Regex for Structured Patterns
132
+
133
+ Match structured output without strict JSON validation:
134
+
135
+ ```json
136
+ {
137
+ "id": "function-signature",
138
+ "prompt": "Write a function that adds two numbers",
139
+ "expected": {
140
+ "regex": ["function\\s+add\\s*\\(", "def\\s+add\\s*\\("]
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### JSON Schema for Structured Responses
146
+
147
+ Validate the exact shape of JSON responses:
148
+
149
+ ```json
150
+ {
151
+ "id": "json-response",
152
+ "prompt": "Return your version as JSON",
153
+ "expected": {
154
+ "jsonSchema": {
155
+ "type": "object",
156
+ "required": ["version"],
157
+ "properties": {
158
+ "version": { "type": "string" }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ ```
164
+
165
+ ### Latency Budgets
166
+
167
+ Catch performance regressions:
168
+
169
+ ```json
170
+ {
171
+ "id": "fast-response",
172
+ "prompt": "Say 'hi'",
173
+ "expected": {
174
+ "contains": ["hi"],
175
+ "maxLatencyMs": 5000
176
+ }
177
+ }
178
+ ```
179
+
180
+ ## Regression Tracking
181
+
182
+ Run evals regularly and compare results:
183
+
184
+ ```bash
185
+ # Run today
186
+ bizar eval run ./fixtures --agent=thor
187
+
188
+ # Compare with yesterday's run
189
+ bizar eval diff run_2026-07-05 run_2026-07-04
190
+ ```
191
+
192
+ The diff categorizes fixtures as:
193
+ - **Improved** — was failing, now passing
194
+ - **Regressed** — was passing, now failing
195
+ - **Unchanged** — same result in both runs
196
+
197
+ ## Test Isolation
198
+
199
+ Fixtures should be:
200
+ - **Idempotent** — running twice gives the same result
201
+ - **Independent** — no fixture depends on another
202
+ - **Deterministic** — same prompt → same result (modulo non-determinism in the model)
203
+
204
+ ## Debugging Failures
205
+
206
+ When a fixture fails:
207
+
208
+ 1. **Check the check details** — each check has a `message` explaining why it failed
209
+ 2. **Increase token budget** — the model may be truncating output
210
+ 3. **Relax latency** — the model may be slow due to load
211
+ 4. **Check the prompt** — the model may be misunderstanding the task
212
+
213
+ ## CI Integration
214
+
215
+ Add to your CI pipeline:
216
+
217
+ ```yaml
218
+ # .github/workflows/eval.yml
219
+ - name: Run eval suite
220
+ run: |
221
+ bizar eval run ./fixtures --concurrency=3
222
+
223
+ - name: Check for regressions
224
+ run: |
225
+ RESULT=$(bizar eval list --limit=1)
226
+ # Fail if any fixtures failed
227
+ ```
228
+
229
+ ## Files
230
+
231
+ | File | Purpose |
232
+ |------|---------|
233
+ | `bizar-dash/src/server/eval.mjs` | Core runner — loads fixtures, calls LLM, validates |
234
+ | `bizar-dash/src/server/eval-store.mjs` | Persistence — saves runs to `~/.local/share/bizar/eval/` |
235
+ | `bizar-dash/src/server/routes/eval.mjs` | REST API surface |
236
+ | `cli/commands/eval.mjs` | CLI subcommands |
237
+ | `templates/eval-fixtures/` | Example fixtures |
@@ -31,6 +31,7 @@ import { createTasksRouter } from './routes/tasks.mjs';
31
31
  import { createArtifactsRouter } from './routes/artifacts.mjs';
32
32
  import { createSchedulesRouter } from './routes/schedules.mjs';
33
33
  import { createModsRouter } from './routes/mods.mjs';
34
+ import { createPluginsRouter } from './routes/plugins.mjs';
34
35
  import { createAgentsRouter } from './routes/agents.mjs';
35
36
  import { createBackgroundRouter } from './routes/background.mjs';
36
37
  import { createActivityRouter } from './routes/activity.mjs';
@@ -54,6 +55,11 @@ import { createEnvVarsRouter } from './routes/env-vars.mjs';
54
55
  import { createUpdateRouter } from './routes/update.mjs';
55
56
  import { createUsageRouter } from './routes/usage.mjs';
56
57
  import { createHeadroomRouter } from './routes/headroom.mjs';
58
+ import { createEvalRouter } from './routes/eval.mjs';
59
+ import { createWorkspacesRouter } from './routes/workspaces.mjs';
60
+ import { createUsersRouter } from './routes/users.mjs';
61
+ import { createVoiceRouter } from './routes/voice.mjs';
62
+ import { attachUserContext } from './auth.mjs';
57
63
 
58
64
  /**
59
65
  * @param {object} deps
@@ -81,6 +87,11 @@ export async function createApiRouter({
81
87
  // companion clients and (later) scope them to a single project.
82
88
  router.use(pairStore.middleware);
83
89
 
90
+ // v5.0.0 — User context middleware. Attaches req.userId and req.workspaceId
91
+ // from the bearer token. Runs after requireAuth (applied in server.mjs),
92
+ // so the user is already authenticated at this point.
93
+ router.use(attachUserContext());
94
+
84
95
  // Compose each domain router in the order the original monolith
85
96
  // declared them. Order is mostly cosmetic — Express path patterns
86
97
  // don't overlap across domains — but it keeps the route table
@@ -101,6 +112,10 @@ export async function createApiRouter({
101
112
  router.use(createArtifactsRouter({ state, broadcast, projectRoot }));
102
113
  router.use(createSchedulesRouter({ broadcast }));
103
114
  router.use(createModsRouter());
115
+ // v5.0.0 — Plugin marketplace routes. Mounted right after mods so
116
+ // the related-domain grouping is visible in the route table. The
117
+ // router is factory-created; no shared deps needed.
118
+ router.use(createPluginsRouter());
104
119
  router.use(createAgentsRouter({ state, broadcast }));
105
120
  router.use(createBackgroundRouter({ broadcast }));
106
121
  router.use(createActivityRouter({ state }));
@@ -126,6 +141,8 @@ export async function createApiRouter({
126
141
  // so a sibling that owns this file can ship independently.
127
142
  const { createLightragRouter } = await import('./routes/lightrag.mjs');
128
143
  router.use(createLightragRouter({ projectRoot }));
144
+ // v5.0.0 — Voice notes (record, transcribe, list, delete, stream audio).
145
+ router.use(createVoiceRouter({}));
129
146
  router.use(createDiagnosticsRouter());
130
147
  router.use(createPairRouter({ state, broadcast }));
131
148
  router.use(createThemesRouter({ state }));
@@ -138,6 +155,11 @@ export async function createApiRouter({
138
155
  // Each route handler inside the router is at its bare path (e.g. '/status'),
139
156
  // which becomes '/api/headroom/status' at the top level.
140
157
  router.use('/headroom', createHeadroomRouter());
158
+ // v5.0.0 — Eval framework endpoints.
159
+ router.use(createEvalRouter({ state, broadcast }));
160
+ // v5.0.0 — Workspace and user management endpoints.
161
+ router.use(createWorkspacesRouter());
162
+ router.use(createUsersRouter());
141
163
  // v4.8.0 — Weekly digest endpoints. Lazy-imported so digest-store
142
164
  // module-level imports (tasks, schedules, etc.) don't block boot.
143
165
  const { createDigestsRouter } = await import('./routes/digests.mjs');
@@ -146,6 +168,12 @@ export async function createApiRouter({
146
168
  // module-level fs operations don't block boot.
147
169
  const { createBackupRouter } = await import('./routes/backup.mjs');
148
170
  router.use(createBackupRouter({ projectRoot }));
171
+ // v5.0.0 — Web Clipper (clipboard) routes. Saves page/selection content.
172
+ const { createClipboardRouter } = await import('./routes/clipboard.mjs');
173
+ router.use(await createClipboardRouter({ projectRoot }));
174
+ // v5.0.0 — Screenshot OCR routes. Accepts images, extracts text.
175
+ const { createOcrRouter } = await import('./routes/ocr.mjs');
176
+ router.use(await createOcrRouter({ projectRoot }));
149
177
  router.use(createMiscRouter({ state, broadcast }));
150
178
 
151
179
  // /api/auth/* must be reachable WITHOUT the bearer token so a fresh
@@ -40,7 +40,7 @@ import {
40
40
  } from 'node:fs';
41
41
  import { join, dirname } from 'node:path';
42
42
  import { homedir } from 'node:os';
43
- import { randomBytes } from 'node:crypto';
43
+ import { randomBytes, createHmac, createHash } from 'node:crypto';
44
44
  import { isIP } from 'node:net';
45
45
 
46
46
  /** Absolute path to the on-disk token. Override via BIZAR_DASHBOARD_SECRET_PATH. */
@@ -379,3 +379,157 @@ function timingSafeEqual(a, b) {
379
379
  }
380
380
  return diff === 0;
381
381
  }
382
+
383
+ // ── v5.0.0: JWT helpers for multi-user workspaces ──────────────────────────────
384
+
385
+ /**
386
+ * Simple HMAC-SHA256 based JWT encoding. Not standards-compliant (no
387
+ * Base64URL padding quirks, no claims validation beyond expiry), but
388
+ * sufficient for local dashboard use and avoids adding a jwt library
389
+ * dependency. The secret is the dashboard secret, giving us free key
390
+ * distribution for existing single-user setups.
391
+ *
392
+ * Payload shape:
393
+ * {
394
+ * userId: string,
395
+ * email?: string,
396
+ * name?: string,
397
+ * role?: string,
398
+ * workspaceIds?: string[],
399
+ * iat: number, // issued at (unix ms)
400
+ * exp: number, // expiry (unix ms)
401
+ * }
402
+ *
403
+ * @param {object} payload
404
+ * @param {number} [expiresInMs=30 days]
405
+ * @returns {string} JWT-like token (base64header.base64payload.signature)
406
+ */
407
+ export function mintToken(payload, expiresInMs = 30 * 24 * 60 * 60 * 1000) {
408
+ // We use the raw crypto module for HMAC-SHA256, which is built-in.
409
+ // use named import
410
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64');
411
+ const now = Date.now();
412
+ const claims = { ...payload, iat: now, exp: now + expiresInMs };
413
+ const payloadEncoded = Buffer.from(JSON.stringify(claims)).toString('base64');
414
+ const signature = createHmac('sha256', getOrCreateSecret())
415
+ .update(`${header}.${payloadEncoded}`)
416
+ .digest('base64')
417
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
418
+ return `${header}.${payloadEncoded}.${signature}`;
419
+ }
420
+
421
+ /**
422
+ * Decode and verify a JWT-like token. Returns null if the token is
423
+ * invalid, expired, or has been tampered with.
424
+ *
425
+ * @param {string} token
426
+ * @returns {{ payload: object } | null}
427
+ */
428
+ export function verifyToken(token) {
429
+ if (!token || typeof token !== 'string') return null;
430
+ const parts = token.split('.');
431
+ if (parts.length !== 3) return null;
432
+
433
+ const [headerB64, payloadB64, signatureB64] = parts;
434
+ // use named import
435
+ const expectedSig = createHmac('sha256', getOrCreateSecret())
436
+ .update(`${headerB64}.${payloadB64}`)
437
+ .digest('base64')
438
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
439
+
440
+ if (signatureB64 !== expectedSig) return null;
441
+
442
+ let payload;
443
+ try {
444
+ // Restore base64 padding and standard base64
445
+ const payloadStd = payloadB64.replace(/-/g, '+').replace(/_/g, '/');
446
+ const padded = payloadStd + '=='.slice(0, (4 - payloadStd.length % 4) % 4);
447
+ payload = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
448
+ } catch {
449
+ return null;
450
+ }
451
+
452
+ // Check expiry
453
+ if (payload.exp && Date.now() > payload.exp) return null;
454
+
455
+ return { payload };
456
+ }
457
+
458
+ /**
459
+ * Extract the raw token from a request (header or query param).
460
+ *
461
+ * @param {import('express').Request} req
462
+ * @returns {string}
463
+ */
464
+ function extractToken(req) {
465
+ const auth = req.headers?.authorization || '';
466
+ if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
467
+ return auth.slice(7);
468
+ }
469
+ if (req.query && req.query.token) {
470
+ return String(req.query.token);
471
+ }
472
+ return '';
473
+ }
474
+
475
+ /**
476
+ * Determine the current userId from the request.
477
+ *
478
+ * Tries to decode the token as a JWT first (v5.0 multi-user format).
479
+ * Falls back to treating the raw token as a legacy secret, deriving a
480
+ * deterministic userId from it for single-user compatibility.
481
+ *
482
+ * @param {import('express').Request} req
483
+ * @returns {string|null} userId or null if not authenticated
484
+ */
485
+ export function getCurrentUserId(req) {
486
+ const token = extractToken(req);
487
+ if (!token) return null;
488
+
489
+ const secret = getOrCreateSecret();
490
+
491
+ // If the token exactly matches the secret, it's a legacy single-user setup.
492
+ // Derive a deterministic userId from the secret for backward compat.
493
+ if (timingSafeEqual(token, secret)) {
494
+ // use named import
495
+ return 'usr_' + createHash('sha256').update(secret).digest('hex').slice(0, 12);
496
+ }
497
+
498
+ // Try JWT decode
499
+ const verified = verifyToken(token);
500
+ if (verified?.payload?.userId) {
501
+ return verified.payload.userId;
502
+ }
503
+
504
+ return null;
505
+ }
506
+
507
+ /**
508
+ * Get the workspaceId the request wants to operate in.
509
+ * Reads X-Workspace-Id header, falls back to first workspace for the user.
510
+ *
511
+ * @param {import('express').Request} req
512
+ * @returns {string|null}
513
+ */
514
+ export function getCurrentWorkspaceId(req) {
515
+ const headerWsId = req.headers?.['x-workspace-id'];
516
+ if (typeof headerWsId === 'string' && headerWsId.trim()) {
517
+ return headerWsId.trim();
518
+ }
519
+ // Fallback: let the caller decide (usually first workspace)
520
+ return null;
521
+ }
522
+
523
+ /**
524
+ * Express middleware: attach userId and workspaceId to the request
525
+ * from the bearer token. Must run AFTER requireAuth.
526
+ *
527
+ * @returns {import('express').RequestHandler}
528
+ */
529
+ export function attachUserContext() {
530
+ return (req, _res, next) => {
531
+ req.userId = getCurrentUserId(req);
532
+ req.workspaceId = getCurrentWorkspaceId(req);
533
+ next();
534
+ };
535
+ }