@polderlabs/bizar 5.0.2 → 5.2.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/MobileChat-TCputYzr.js +1 -0
- package/bizar-dash/dist/assets/MobileChat-TCputYzr.js.map +1 -0
- package/bizar-dash/dist/assets/MobileSettings-BxcbL5XT.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-BxcbL5XT.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Bo0iH9EC.js → icons-DRDXfbBP.js} +151 -136
- package/bizar-dash/dist/assets/icons-DRDXfbBP.js.map +1 -0
- package/bizar-dash/dist/assets/main-D5Ditnrd.js +19 -0
- package/bizar-dash/dist/assets/main-D5Ditnrd.js.map +1 -0
- package/bizar-dash/dist/assets/{main-DTkNlLrw.css → main-xFpWMd32.css} +1 -1
- package/bizar-dash/dist/assets/markdown-C6mXtQxD.js +1 -0
- package/bizar-dash/dist/assets/markdown-C6mXtQxD.js.map +1 -0
- package/bizar-dash/dist/assets/mobile--17fkfrl.js +1 -0
- package/bizar-dash/dist/assets/{mobile-DD-FZrTC.js.map → mobile--17fkfrl.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-j3rOZK6v.js +2 -0
- package/bizar-dash/dist/assets/mobile-j3rOZK6v.js.map +1 -0
- package/bizar-dash/dist/assets/{react-vendor-DZRUXSPQ.js → react-vendor-Dn4wqh4Z.js} +8 -8
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +1 -0
- package/bizar-dash/dist/assets/useSlashCommands-BG-DhEck.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-BG-DhEck.js.map +1 -0
- package/bizar-dash/dist/assets/vendor-CeHGtduv.js +29 -0
- package/bizar-dash/dist/assets/vendor-CeHGtduv.js.map +1 -0
- package/bizar-dash/dist/index.html +8 -6
- package/bizar-dash/dist/mobile.html +4 -5
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +4 -0
- package/bizar-dash/src/server/eval.mjs +93 -3
- package/bizar-dash/src/server/metrics.mjs +75 -0
- package/bizar-dash/src/server/otel.mjs +141 -14
- package/bizar-dash/src/server/plugins/registry.mjs +145 -43
- package/bizar-dash/src/server/routes/chat.mjs +46 -9
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +120 -17
- package/bizar-dash/src/server/routes/plugins.mjs +131 -16
- package/bizar-dash/src/server/routes/tailscale.mjs +46 -0
- package/bizar-dash/src/server/routes/voice.mjs +42 -6
- package/bizar-dash/src/server/routes/workspaces.mjs +156 -61
- package/bizar-dash/src/server/server.mjs +57 -15
- package/bizar-dash/src/server/voice-store.mjs +27 -0
- package/bizar-dash/src/server/workers/transcription-worker.mjs +213 -0
- package/bizar-dash/src/web/App.tsx +9 -0
- package/bizar-dash/src/web/MobileApp.tsx +18 -10
- package/bizar-dash/src/web/components/EvalDiff.tsx +157 -0
- package/bizar-dash/src/web/components/EvalRunCard.tsx +78 -0
- package/bizar-dash/src/web/components/PluginCard.tsx +64 -0
- package/bizar-dash/src/web/components/PluginPermissions.tsx +36 -0
- package/bizar-dash/src/web/components/TailscaleSettings.tsx +161 -0
- package/bizar-dash/src/web/components/Toggle.tsx +31 -0
- package/bizar-dash/src/web/components/Topbar.tsx +6 -0
- package/bizar-dash/src/web/mobile/MobileChat.tsx +138 -0
- package/bizar-dash/src/web/mobile/MobileSettings.tsx +196 -0
- package/bizar-dash/src/web/styles/main.css +545 -0
- package/bizar-dash/src/web/views/Eval.tsx +172 -0
- package/bizar-dash/src/web/views/EvalReport.tsx +349 -0
- package/bizar-dash/src/web/views/Plugins.tsx +128 -0
- package/bizar-dash/src/web/views/Settings.tsx +2 -0
- package/bizar-dash/tests/bundle-analysis.test.mjs +5 -4
- package/bizar-dash/tests/cli-tailscale.test.mjs +113 -0
- package/bizar-dash/tests/components/plugin-permissions.test.tsx +95 -0
- package/bizar-dash/tests/eval/fixtures-extra.test.mjs +285 -0
- package/bizar-dash/tests/eval-web-ui.test.tsx +220 -0
- package/bizar-dash/tests/otel-spans.test.mjs +253 -0
- package/bizar-dash/tests/plugins-registry-fallback.test.mjs +259 -0
- package/bizar-dash/tests/voice-transcribe-worker.test.mjs +343 -0
- package/cli/bin.mjs +14 -0
- package/cli/commands/dash.mjs +19 -1
- package/cli/commands/service.mjs +7 -0
- package/cli/commands/tailscale.mjs +251 -0
- package/package.json +1 -1
- package/templates/eval-fixtures/citation.json +16 -0
- package/templates/eval-fixtures/code-review.json +16 -0
- package/templates/eval-fixtures/concise-output.json +16 -0
- package/templates/eval-fixtures/context-window.json +16 -0
- package/templates/eval-fixtures/error-recovery.json +16 -0
- package/templates/eval-fixtures/json-output.json +20 -0
- package/templates/eval-fixtures/multi-language.json +16 -0
- package/templates/eval-fixtures/safe-paths.json +16 -0
- package/templates/eval-fixtures/tool-call-multi-step.json +18 -0
- package/templates/eval-fixtures/unicode-handling.json +16 -0
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +0 -1
- package/bizar-dash/dist/assets/index-DmpSFPJY.js +0 -9
- package/bizar-dash/dist/assets/index-DmpSFPJY.js.map +0 -1
- package/bizar-dash/dist/assets/main-C1cpttnv.js +0 -19
- package/bizar-dash/dist/assets/main-C1cpttnv.js.map +0 -1
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js +0 -29
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js +0 -2
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-DD-FZrTC.js +0 -1
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +0 -1
|
@@ -15,11 +15,18 @@
|
|
|
15
15
|
* POST /api/plugins/:id/invoke — invoke a method in the sandbox
|
|
16
16
|
* body { method, args? }
|
|
17
17
|
*
|
|
18
|
+
* v5.1.0 — every route is wrapped in a `withSpan(...)` and emits a
|
|
19
|
+
* `recordTrace(...)` line so metrics and traces share an attribute
|
|
20
|
+
* key namespace. Spans are no-op when OTEL is not initialised, so
|
|
21
|
+
* the dashboard's default behaviour is unchanged.
|
|
22
|
+
*
|
|
18
23
|
* Express-ordering note: /registry and /installed must come BEFORE the
|
|
19
24
|
* `:id` wildcard routes so Express doesn't match "registry" as an id.
|
|
20
25
|
*/
|
|
21
26
|
import { Router } from 'express';
|
|
22
27
|
import { wrap } from './_shared.mjs';
|
|
28
|
+
import { withSpan, setCommonAttributes } from '../otel.mjs';
|
|
29
|
+
import { recordTrace } from '../metrics.mjs';
|
|
23
30
|
import * as registry from '../plugins/registry.mjs';
|
|
24
31
|
import * as store from '../plugins/store.mjs';
|
|
25
32
|
|
|
@@ -30,14 +37,22 @@ export function createPluginsRouter() {
|
|
|
30
37
|
const router = Router();
|
|
31
38
|
|
|
32
39
|
// ── GET /api/plugins/registry?q=&category=&tag= ──────────────────────
|
|
33
|
-
router.get('/plugins/registry', wrap(async (req, res) => {
|
|
40
|
+
router.get('/plugins/registry', wrap(withSpan('plugin.registry.search', async (span, req, res) => {
|
|
34
41
|
const q = typeof req.query.q === 'string' ? req.query.q : '';
|
|
35
42
|
const category = typeof req.query.category === 'string' ? req.query.category : undefined;
|
|
36
43
|
const tag = typeof req.query.tag === 'string' ? req.query.tag : undefined;
|
|
44
|
+
setCommonAttributes(span, {
|
|
45
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
46
|
+
userAgent: req.headers?.['user-agent'],
|
|
47
|
+
});
|
|
48
|
+
if (q) span.setAttribute('plugin.search.q_length', q.length);
|
|
49
|
+
if (category) span.setAttribute('plugin.search.category', category);
|
|
50
|
+
if (tag) span.setAttribute('plugin.search.tag', tag);
|
|
37
51
|
try {
|
|
38
52
|
const plugins = await registry.searchPlugins(q, { category, tag });
|
|
39
53
|
// Annotate each entry with installed flag for the UI.
|
|
40
54
|
const installed = new Map(store.listInstalled().map((p) => [p.id, p]));
|
|
55
|
+
span.setAttribute('plugin.search.results', plugins.length);
|
|
41
56
|
res.json({
|
|
42
57
|
registry: {
|
|
43
58
|
source: registry.getRegistryUrl(),
|
|
@@ -54,22 +69,35 @@ export function createPluginsRouter() {
|
|
|
54
69
|
};
|
|
55
70
|
}),
|
|
56
71
|
});
|
|
72
|
+
recordTrace('plugin.registry.search', { outcome: 'ok', has_query: !!q });
|
|
57
73
|
} catch (err) {
|
|
58
74
|
const status = err.code === 'registry_unreachable' ? 502 : 500;
|
|
75
|
+
span.setAttribute('plugin.registry.error_code', err.code || 'unknown');
|
|
59
76
|
res.status(status).json({
|
|
60
77
|
error: err.code || 'registry_error',
|
|
61
78
|
message: err.message,
|
|
62
79
|
registryUrl: registry.getRegistryUrl(),
|
|
63
80
|
});
|
|
81
|
+
recordTrace('plugin.registry.search', {
|
|
82
|
+
outcome: 'error',
|
|
83
|
+
code: err.code || 'unknown',
|
|
84
|
+
});
|
|
64
85
|
}
|
|
65
|
-
}));
|
|
86
|
+
})));
|
|
66
87
|
|
|
67
88
|
// ── GET /api/plugins/registry/:id ────────────────────────────────────
|
|
68
|
-
router.get('/plugins/registry/:id', wrap(async (req, res) => {
|
|
89
|
+
router.get('/plugins/registry/:id', wrap(withSpan('plugin.registry.get', async (span, req, res) => {
|
|
90
|
+
setCommonAttributes(span, {
|
|
91
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
92
|
+
userAgent: req.headers?.['user-agent'],
|
|
93
|
+
});
|
|
94
|
+
span.setAttribute('plugin.id', req.params.id || '');
|
|
69
95
|
try {
|
|
70
96
|
const plugin = await registry.getPlugin(req.params.id);
|
|
71
97
|
if (!plugin) {
|
|
98
|
+
span.setAttribute('plugin.registry.outcome', 'not_found');
|
|
72
99
|
res.status(404).json({ error: 'not_found' });
|
|
100
|
+
recordTrace('plugin.registry.get', { outcome: 'not_found' });
|
|
73
101
|
return;
|
|
74
102
|
}
|
|
75
103
|
const local = store.getInstalled(plugin.id);
|
|
@@ -78,36 +106,72 @@ export function createPluginsRouter() {
|
|
|
78
106
|
installed: !!local,
|
|
79
107
|
installedVersion: local ? local.version : null,
|
|
80
108
|
upgradeAvailable:
|
|
109
|
+
// Preserve the v5.0.0 reference to `p.version` (it's a
|
|
110
|
+
// pre-existing typo for `plugin.version` in this branch —
|
|
111
|
+
// covered by a separate bug, kept here to avoid scope
|
|
112
|
+
// expansion of this PR).
|
|
81
113
|
local && local.version !== plugin.version ? plugin.version : null,
|
|
82
114
|
});
|
|
115
|
+
recordTrace('plugin.registry.get', { outcome: 'ok' });
|
|
83
116
|
} catch (err) {
|
|
84
117
|
const status = err.code === 'registry_unreachable' ? 502 : 500;
|
|
118
|
+
span.setAttribute('plugin.registry.error_code', err.code || 'unknown');
|
|
85
119
|
res.status(status).json({
|
|
86
120
|
error: err.code || 'registry_error',
|
|
87
121
|
message: err.message,
|
|
88
122
|
});
|
|
123
|
+
recordTrace('plugin.registry.get', {
|
|
124
|
+
outcome: 'error',
|
|
125
|
+
code: err.code || 'unknown',
|
|
126
|
+
});
|
|
89
127
|
}
|
|
90
|
-
}));
|
|
128
|
+
})));
|
|
91
129
|
|
|
92
130
|
// ── GET /api/plugins/installed ───────────────────────────────────────
|
|
93
|
-
router.get('/plugins/installed', wrap(async (_req, res) => {
|
|
94
|
-
|
|
95
|
-
|
|
131
|
+
router.get('/plugins/installed', wrap(withSpan('plugin.installed.list', async (span, _req, res) => {
|
|
132
|
+
setCommonAttributes(span, {
|
|
133
|
+
ip: _req.ip || _req.socket?.remoteAddress,
|
|
134
|
+
userAgent: _req.headers?.['user-agent'],
|
|
135
|
+
});
|
|
136
|
+
const installed = store.listInstalled();
|
|
137
|
+
const enhanced = installed.map(p => ({
|
|
138
|
+
...p,
|
|
139
|
+
permissions: p.permissions || [],
|
|
140
|
+
config: p.config || {},
|
|
141
|
+
methodCount: p.methodCount || 0,
|
|
142
|
+
invocations: p.invocations || 0,
|
|
143
|
+
lastInvokedAt: p.lastInvokedAt || null,
|
|
144
|
+
}));
|
|
145
|
+
span.setAttribute('plugin.installed.count', enhanced.length);
|
|
146
|
+
res.json({ plugins: enhanced });
|
|
147
|
+
recordTrace('plugin.installed.list', {
|
|
148
|
+
count_bucket: enhanced.length === 0 ? '0' : enhanced.length < 10 ? '1-9' : '10+',
|
|
149
|
+
});
|
|
150
|
+
})));
|
|
96
151
|
|
|
97
152
|
// ── POST /api/plugins/install ────────────────────────────────────────
|
|
98
|
-
router.post('/plugins/install', wrap(async (req, res) => {
|
|
153
|
+
router.post('/plugins/install', wrap(withSpan('plugin.install', async (span, req, res) => {
|
|
154
|
+
setCommonAttributes(span, {
|
|
155
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
156
|
+
userAgent: req.headers?.['user-agent'],
|
|
157
|
+
});
|
|
99
158
|
const pluginId = req.body?.pluginId || req.body?.id;
|
|
159
|
+
span.setAttribute('plugin.id', pluginId || '');
|
|
100
160
|
if (!pluginId || typeof pluginId !== 'string') {
|
|
101
161
|
res.status(400).json({
|
|
102
162
|
error: 'bad_request',
|
|
103
163
|
message: 'pluginId is required',
|
|
104
164
|
});
|
|
165
|
+
recordTrace('plugin.install', { outcome: 'missing_id' });
|
|
105
166
|
return;
|
|
106
167
|
}
|
|
107
168
|
const force = !!req.body?.force;
|
|
169
|
+
span.setAttribute('plugin.install.force', force);
|
|
108
170
|
try {
|
|
109
171
|
const installed = await store.installPlugin(pluginId, { force });
|
|
172
|
+
span.setAttribute('plugin.install.version', installed?.version || '');
|
|
110
173
|
res.status(201).json(installed);
|
|
174
|
+
recordTrace('plugin.install', { outcome: 'created' });
|
|
111
175
|
} catch (err) {
|
|
112
176
|
const map = {
|
|
113
177
|
not_found: 404,
|
|
@@ -117,31 +181,50 @@ export function createPluginsRouter() {
|
|
|
117
181
|
bad_manifest: 422,
|
|
118
182
|
};
|
|
119
183
|
const status = map[err.code] || 500;
|
|
184
|
+
span.setAttribute('plugin.install.error_code', err.code || 'unknown');
|
|
120
185
|
res.status(status).json({
|
|
121
186
|
error: err.code || 'install_failed',
|
|
122
187
|
message: err.message,
|
|
123
188
|
});
|
|
189
|
+
recordTrace('plugin.install', {
|
|
190
|
+
outcome: 'error',
|
|
191
|
+
code: err.code || 'unknown',
|
|
192
|
+
});
|
|
124
193
|
}
|
|
125
|
-
}));
|
|
194
|
+
})));
|
|
126
195
|
|
|
127
196
|
// ── PUT /api/plugins/:id (update OR set config) ──────────────────────
|
|
128
|
-
router.put('/plugins/:id', wrap(async (req, res) => {
|
|
197
|
+
router.put('/plugins/:id', wrap(withSpan('plugin.mutate', async (span, req, res) => {
|
|
198
|
+
setCommonAttributes(span, {
|
|
199
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
200
|
+
userAgent: req.headers?.['user-agent'],
|
|
201
|
+
});
|
|
202
|
+
span.setAttribute('plugin.id', req.params.id || '');
|
|
129
203
|
const action = req.body?.action;
|
|
204
|
+
span.setAttribute('plugin.action', action || '');
|
|
130
205
|
if (action === 'update') {
|
|
131
206
|
try {
|
|
132
207
|
const result = await store.updatePlugin(req.params.id);
|
|
208
|
+
span.setAttribute('plugin.update.from', result.from || '');
|
|
209
|
+
span.setAttribute('plugin.update.to', result.to || '');
|
|
133
210
|
res.json({
|
|
134
211
|
ok: true,
|
|
135
212
|
from: result.from,
|
|
136
213
|
to: result.to,
|
|
137
214
|
plugin: result.plugin,
|
|
138
215
|
});
|
|
216
|
+
recordTrace('plugin.update', { outcome: 'ok' });
|
|
139
217
|
} catch (err) {
|
|
140
218
|
const status = err.code === 'not_installed' ? 404 : 500;
|
|
219
|
+
span.setAttribute('plugin.update.error_code', err.code || 'unknown');
|
|
141
220
|
res.status(status).json({
|
|
142
221
|
error: err.code || 'update_failed',
|
|
143
222
|
message: err.message,
|
|
144
223
|
});
|
|
224
|
+
recordTrace('plugin.update', {
|
|
225
|
+
outcome: 'error',
|
|
226
|
+
code: err.code || 'unknown',
|
|
227
|
+
});
|
|
145
228
|
}
|
|
146
229
|
return;
|
|
147
230
|
}
|
|
@@ -152,17 +235,24 @@ export function createPluginsRouter() {
|
|
|
152
235
|
error: 'bad_request',
|
|
153
236
|
message: 'config must be an object',
|
|
154
237
|
});
|
|
238
|
+
recordTrace('plugin.config', { outcome: 'invalid_config' });
|
|
155
239
|
return;
|
|
156
240
|
}
|
|
157
241
|
try {
|
|
158
242
|
const updated = store.replaceConfig(req.params.id, config);
|
|
159
243
|
res.json(updated);
|
|
244
|
+
recordTrace('plugin.config', { outcome: 'ok' });
|
|
160
245
|
} catch (err) {
|
|
161
246
|
const status = err.code === 'not_installed' ? 404 : 400;
|
|
247
|
+
span.setAttribute('plugin.config.error_code', err.code || 'unknown');
|
|
162
248
|
res.status(status).json({
|
|
163
249
|
error: err.code || 'config_failed',
|
|
164
250
|
message: err.message,
|
|
165
251
|
});
|
|
252
|
+
recordTrace('plugin.config', {
|
|
253
|
+
outcome: 'error',
|
|
254
|
+
code: err.code || 'unknown',
|
|
255
|
+
});
|
|
166
256
|
}
|
|
167
257
|
return;
|
|
168
258
|
}
|
|
@@ -170,37 +260,57 @@ export function createPluginsRouter() {
|
|
|
170
260
|
error: 'bad_request',
|
|
171
261
|
message: 'action must be "update" or "config"',
|
|
172
262
|
});
|
|
173
|
-
|
|
263
|
+
recordTrace('plugin.mutate', { outcome: 'invalid_action' });
|
|
264
|
+
})));
|
|
174
265
|
|
|
175
266
|
// ── DELETE /api/plugins/:id ──────────────────────────────────────────
|
|
176
|
-
router.delete('/plugins/:id', wrap(async (req, res) => {
|
|
267
|
+
router.delete('/plugins/:id', wrap(withSpan('plugin.uninstall', async (span, req, res) => {
|
|
268
|
+
setCommonAttributes(span, {
|
|
269
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
270
|
+
userAgent: req.headers?.['user-agent'],
|
|
271
|
+
});
|
|
272
|
+
span.setAttribute('plugin.id', req.params.id || '');
|
|
177
273
|
const ok = store.uninstallPlugin(req.params.id);
|
|
178
274
|
if (!ok) {
|
|
179
275
|
res.status(404).json({ error: 'not_found' });
|
|
276
|
+
recordTrace('plugin.uninstall', { outcome: 'not_found' });
|
|
180
277
|
return;
|
|
181
278
|
}
|
|
182
279
|
res.status(204).end();
|
|
183
|
-
|
|
280
|
+
recordTrace('plugin.uninstall', { outcome: 'ok' });
|
|
281
|
+
})));
|
|
184
282
|
|
|
185
283
|
// ── POST /api/plugins/:id/invoke ─────────────────────────────────────
|
|
186
|
-
router.post('/plugins/:id/invoke', wrap(async (req, res) => {
|
|
284
|
+
router.post('/plugins/:id/invoke', wrap(withSpan('plugin.invoke', async (span, req, res) => {
|
|
285
|
+
setCommonAttributes(span, {
|
|
286
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
287
|
+
userAgent: req.headers?.['user-agent'],
|
|
288
|
+
});
|
|
289
|
+
span.setAttribute('plugin.id', req.params.id || '');
|
|
187
290
|
const method = req.body?.method;
|
|
188
291
|
const args = Array.isArray(req.body?.args) ? req.body.args : [];
|
|
292
|
+
if (method) span.setAttribute('plugin.invoke.method', method);
|
|
293
|
+
span.setAttribute('plugin.invoke.arg_count', args.length);
|
|
189
294
|
if (!method || typeof method !== 'string') {
|
|
190
295
|
res.status(400).json({
|
|
191
296
|
error: 'bad_request',
|
|
192
297
|
message: 'method (string) is required',
|
|
193
298
|
});
|
|
299
|
+
recordTrace('plugin.invoke', { outcome: 'missing_method' });
|
|
194
300
|
return;
|
|
195
301
|
}
|
|
196
302
|
const timeoutMs = Number.isFinite(req.body?.timeoutMs)
|
|
197
303
|
? req.body.timeoutMs
|
|
198
304
|
: undefined;
|
|
305
|
+
if (Number.isFinite(timeoutMs)) {
|
|
306
|
+
span.setAttribute('plugin.invoke.timeout_ms', timeoutMs);
|
|
307
|
+
}
|
|
199
308
|
const result = await store.invokePlugin(req.params.id, method, args, {
|
|
200
309
|
timeoutMs,
|
|
201
310
|
});
|
|
202
311
|
if (result.ok) {
|
|
203
312
|
res.json(result);
|
|
313
|
+
recordTrace('plugin.invoke', { outcome: 'ok', method });
|
|
204
314
|
return;
|
|
205
315
|
}
|
|
206
316
|
// Distinguish "this is a plugin bug" (500) from "this is a usage
|
|
@@ -213,8 +323,13 @@ export function createPluginsRouter() {
|
|
|
213
323
|
'corrupt_install',
|
|
214
324
|
]);
|
|
215
325
|
const status = usageCodes.has(result.code) ? 400 : 500;
|
|
326
|
+
span.setAttribute('plugin.invoke.error_code', result.code || 'unknown');
|
|
216
327
|
res.status(status).json(result);
|
|
217
|
-
|
|
328
|
+
recordTrace('plugin.invoke', {
|
|
329
|
+
outcome: 'error',
|
|
330
|
+
code: result.code || 'unknown',
|
|
331
|
+
});
|
|
332
|
+
})));
|
|
218
333
|
|
|
219
334
|
return router;
|
|
220
|
-
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/tailscale.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.2 — Tailscale auth key integration endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Endpoints:
|
|
7
|
+
* GET /api/tailscale/status — tailscale state (installed, authenticated, serve config)
|
|
8
|
+
* POST /api/tailscale/setup — authenticate + set up tailscale serve
|
|
9
|
+
* POST /api/tailscale/unserve — remove tailscale serve
|
|
10
|
+
*/
|
|
11
|
+
import { Router } from 'express';
|
|
12
|
+
import { tailscaleStore } from '../tailscale-store.mjs';
|
|
13
|
+
import { wrap } from './_shared.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} _deps
|
|
17
|
+
* @returns {import('express').Router}
|
|
18
|
+
*/
|
|
19
|
+
export function createTailscaleRouter(_deps) {
|
|
20
|
+
const router = Router();
|
|
21
|
+
|
|
22
|
+
// GET /api/tailscale/status
|
|
23
|
+
router.get('/status', wrap(async (_req, res) => {
|
|
24
|
+
res.json(await tailscaleStore.status());
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// POST /api/tailscale/setup — authenticate + enable serve
|
|
28
|
+
// Body: { authKey?: string, port?: number, https?: boolean, hostname?: string }
|
|
29
|
+
router.post('/setup', wrap(async (req, res) => {
|
|
30
|
+
const { authKey, port = 4321, https = true, hostname = '' } = req.body || {};
|
|
31
|
+
// If authKey is provided, set it in env for tailscale-store to pick up
|
|
32
|
+
if (authKey) {
|
|
33
|
+
process.env.TAILSCALE_AUTHKEY = authKey;
|
|
34
|
+
}
|
|
35
|
+
// delegates to tailscaleStore.enable() which runs `tailscale serve ...`
|
|
36
|
+
const result = await tailscaleStore.enable({ port, https, hostname });
|
|
37
|
+
res.json(result);
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// POST /api/tailscale/unserve — remove tailscale serve
|
|
41
|
+
router.post('/unserve', wrap(async (_req, res) => {
|
|
42
|
+
res.json(await tailscaleStore.disable());
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
return router;
|
|
46
|
+
}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* src/server/routes/voice.mjs
|
|
3
3
|
*
|
|
4
4
|
* v5.0.0 — Voice notes REST API.
|
|
5
|
+
* v5.2 — Upload now saves audio immediately and enqueues background
|
|
6
|
+
* transcription instead of blocking the request on Whisper.
|
|
5
7
|
*
|
|
6
8
|
* Endpoints:
|
|
7
|
-
* POST /api/voice/upload — multipart upload,
|
|
9
|
+
* POST /api/voice/upload — multipart upload, enqueue transcription
|
|
8
10
|
* GET /api/voice/list?vaultPath= — list notes (optionally filtered)
|
|
9
11
|
* GET /api/voice/:id — get note details
|
|
10
12
|
* DELETE /api/voice/:id — delete note + audio + transcript
|
|
@@ -16,7 +18,7 @@ import { createReadStream, existsSync } from 'node:fs';
|
|
|
16
18
|
import { join } from 'node:path';
|
|
17
19
|
import { info, warn, child } from '../logger.mjs';
|
|
18
20
|
import { saveVoiceNote, listVoiceNotes, getVoiceNote, deleteVoiceNote } from '../voice-store.mjs';
|
|
19
|
-
import {
|
|
21
|
+
import { enqueueTranscription } from '../workers/transcription-worker.mjs';
|
|
20
22
|
import { wrap } from './_shared.mjs';
|
|
21
23
|
import formidable from 'formidable';
|
|
22
24
|
|
|
@@ -29,6 +31,14 @@ const log = child({ module: 'voice-routes' });
|
|
|
29
31
|
export function createVoiceRouter(_deps) {
|
|
30
32
|
const router = Router();
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether Whisper is configured. When false, the worker is skipped
|
|
36
|
+
* entirely — uploads still succeed, just without transcripts.
|
|
37
|
+
*/
|
|
38
|
+
function whisperAvailable() {
|
|
39
|
+
return Boolean(process.env.OPENAI_API_KEY) || Boolean(process.env.BIZAR_WHISPER_ENDPOINT);
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
// POST /api/voice/upload — multipart: audio file + vaultPath field
|
|
33
43
|
router.post('/voice/upload', wrap(async (req, res) => {
|
|
34
44
|
const form = formidable({ maxFileSize: 50 * 1024 * 1024 }); // 50 MB limit
|
|
@@ -60,8 +70,11 @@ export function createVoiceRouter(_deps) {
|
|
|
60
70
|
return;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
//
|
|
64
|
-
|
|
73
|
+
// v5.2 — Transcription is now asynchronous. Save the audio first;
|
|
74
|
+
// the worker fills in the transcript when Whisper (or a local
|
|
75
|
+
// proxy) responds. Clients can poll GET /api/voice/:id or watch
|
|
76
|
+
// the `voice:updated` WS event for the transcript to appear.
|
|
77
|
+
const transcript = null;
|
|
65
78
|
|
|
66
79
|
// Duration from audioFile (formidable provides size but not duration — we
|
|
67
80
|
// store null and the frontmatter will show '?')
|
|
@@ -77,8 +90,31 @@ export function createVoiceRouter(_deps) {
|
|
|
77
90
|
return;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
// Kick the background worker ONLY when we have a place to send
|
|
94
|
+
// the audio. Without credentials the worker would just return
|
|
95
|
+
// null forever and waste an open file descriptor.
|
|
96
|
+
let transcriptionPending = false;
|
|
97
|
+
if (whisperAvailable()) {
|
|
98
|
+
try {
|
|
99
|
+
enqueueTranscription(saved.id);
|
|
100
|
+
transcriptionPending = true;
|
|
101
|
+
} catch (enqErr) {
|
|
102
|
+
warn('transcription enqueue failed', { id: saved.id, err: enqErr.message });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
log.info('voice note saved via upload', {
|
|
107
|
+
id: saved.id,
|
|
108
|
+
transcriptionPending,
|
|
109
|
+
transcriptLength: 0,
|
|
110
|
+
});
|
|
111
|
+
res.json({
|
|
112
|
+
notePath: saved.notePath,
|
|
113
|
+
audioPath: saved.audioPath,
|
|
114
|
+
id: saved.id,
|
|
115
|
+
transcription: null,
|
|
116
|
+
transcriptionPending,
|
|
117
|
+
});
|
|
82
118
|
});
|
|
83
119
|
}));
|
|
84
120
|
|