@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.
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js +656 -0
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +1 -0
- package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
- package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js +16 -0
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +1 -0
- package/bizar-dash/dist/assets/{main-DfmIfOUS.css → main-ZAfGKENE.css} +1 -1
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +2 -0
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-DHXXbn1A.js +1 -0
- package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DHXXbn1A.js.map} +1 -1
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -3
- package/bizar-dash/dist/mobile.html +5 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/skills/eval/SKILL.md +237 -0
- package/bizar-dash/src/server/api.mjs +28 -0
- package/bizar-dash/src/server/auth.mjs +155 -1
- package/bizar-dash/src/server/eval-store.mjs +226 -0
- package/bizar-dash/src/server/eval.mjs +347 -0
- package/bizar-dash/src/server/ocr.mjs +55 -0
- package/bizar-dash/src/server/plugins/registry.mjs +363 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
- package/bizar-dash/src/server/plugins/store.mjs +659 -0
- package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
- package/bizar-dash/src/server/routes/eval.mjs +147 -0
- package/bizar-dash/src/server/routes/ocr.mjs +182 -0
- package/bizar-dash/src/server/routes/plugins.mjs +220 -0
- package/bizar-dash/src/server/routes/users.mjs +84 -0
- package/bizar-dash/src/server/routes/voice.mjs +131 -0
- package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
- package/bizar-dash/src/server/voice-store.mjs +202 -0
- package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
- package/bizar-dash/src/server/workspaces.mjs +626 -0
- package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
- package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
- package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
- package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
- package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
- package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
- package/bizar-dash/src/web/styles/memory.css +82 -0
- package/bizar-dash/src/web/views/Memory.tsx +17 -2
- package/bizar-dash/src/web/views/Settings.tsx +3 -0
- package/bizar-dash/src/web/views/Workspace.tsx +294 -0
- package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
- package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
- package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
- package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
- package/bizar-dash/tests/clipboard.test.mjs +147 -0
- package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
- package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
- package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
- package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
- package/bizar-dash/tests/eval/report.test.mjs +284 -0
- package/bizar-dash/tests/eval/runner.test.mjs +471 -0
- package/bizar-dash/tests/ocr.test.mjs +87 -0
- package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
- package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
- package/bizar-dash/tests/plugins-store.test.mjs +455 -0
- package/bizar-dash/tests/users.test.mjs +108 -0
- package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
- package/bizar-dash/tests/voice-store.test.mjs +148 -0
- package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
- package/bizar-dash/tests/workspaces.test.mjs +527 -0
- package/cli/bin.mjs +72 -2
- package/cli/commands/clip.mjs +146 -0
- package/cli/commands/deploy/cloudflare.mjs +250 -0
- package/cli/commands/deploy/docker.mjs +221 -0
- package/cli/commands/deploy/fly.mjs +161 -0
- package/cli/commands/deploy/vercel.mjs +225 -0
- package/cli/commands/deploy.mjs +240 -0
- package/cli/commands/eval.mjs +378 -0
- package/cli/commands/marketplace.mjs +64 -0
- package/cli/commands/ocr.mjs +165 -0
- package/cli/commands/plugin.mjs +358 -0
- package/cli/commands/voice.mjs +211 -0
- package/cli/commands/workspace.mjs +247 -0
- package/package.json +12 -8
- package/templates/deploy/cloudflare/README.md +32 -0
- package/templates/deploy/cloudflare/functions-index.template.js +15 -0
- package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
- package/templates/deploy/docker/.env.template +16 -0
- package/templates/deploy/docker/README.md +58 -0
- package/templates/deploy/docker/docker-compose.template.yml +23 -0
- package/templates/deploy/fly/README.md +35 -0
- package/templates/deploy/fly/fly.toml.template +28 -0
- package/templates/deploy/vercel/README.md +29 -0
- package/templates/deploy/vercel/api-index.template.js +18 -0
- package/templates/deploy/vercel/vercel.json.template +16 -0
- package/templates/eval-fixtures/README.md +58 -0
- package/templates/eval-fixtures/code-search-basic.json +28 -0
- package/templates/eval-fixtures/latency-bounds.json +16 -0
- package/templates/eval-fixtures/regression-suite.json +79 -0
- package/templates/eval-fixtures/response-format.json +30 -0
- package/templates/eval-fixtures/tool-call-correctness.json +24 -0
- package/templates/plugin-template/README.md +121 -0
- package/templates/plugin-template/index.js +66 -0
- package/templates/plugin-template/plugin.json +42 -0
- package/templates/plugin-template/tests/plugin.test.js +83 -0
- package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
- package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
- package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
- 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-
|
|
33
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
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-
|
|
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-
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
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>
|
package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":
|
|
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
|
+
}
|