@polderlabs/bizar 4.8.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 (129) 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-DmpSFPJY.js +9 -0
  4. package/bizar-dash/dist/assets/index-DmpSFPJY.js.map +1 -0
  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-DX_Jh8Wc.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-Chvf9u_B.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/memory-lightrag.mjs +109 -0
  25. package/bizar-dash/src/server/memory-store.mjs +121 -0
  26. package/bizar-dash/src/server/ocr.mjs +55 -0
  27. package/bizar-dash/src/server/otel.mjs +133 -0
  28. package/bizar-dash/src/server/plugins/registry.mjs +363 -0
  29. package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
  30. package/bizar-dash/src/server/plugins/store.mjs +659 -0
  31. package/bizar-dash/src/server/routes/chat.mjs +246 -170
  32. package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
  33. package/bizar-dash/src/server/routes/eval.mjs +147 -0
  34. package/bizar-dash/src/server/routes/memory.mjs +46 -0
  35. package/bizar-dash/src/server/routes/ocr.mjs +182 -0
  36. package/bizar-dash/src/server/routes/opencode-sessions.mjs +82 -48
  37. package/bizar-dash/src/server/routes/plugins.mjs +220 -0
  38. package/bizar-dash/src/server/routes/users.mjs +84 -0
  39. package/bizar-dash/src/server/routes/voice.mjs +131 -0
  40. package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
  41. package/bizar-dash/src/server/server.mjs +40 -0
  42. package/bizar-dash/src/server/voice-store.mjs +202 -0
  43. package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
  44. package/bizar-dash/src/server/workspaces.mjs +626 -0
  45. package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
  46. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
  47. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
  48. package/bizar-dash/src/web/components/SettingsSearch.tsx +204 -89
  49. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
  50. package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
  51. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
  52. package/bizar-dash/src/web/lib/search.ts +115 -0
  53. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +10 -35
  54. package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +69 -0
  55. package/bizar-dash/src/web/styles/memory.css +166 -1
  56. package/bizar-dash/src/web/styles/settings.css +80 -0
  57. package/bizar-dash/src/web/views/Memory.tsx +22 -2
  58. package/bizar-dash/src/web/views/Settings.tsx +99 -0
  59. package/bizar-dash/src/web/views/Workspace.tsx +294 -0
  60. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
  61. package/bizar-dash/src/web/views/memory/MemoryGraphLegend.tsx +29 -0
  62. package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +192 -0
  63. package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +336 -0
  64. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
  65. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
  66. package/bizar-dash/tests/bundle-analysis.test.mjs +73 -0
  67. package/bizar-dash/tests/clipboard.test.mjs +147 -0
  68. package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
  69. package/bizar-dash/tests/components/settings-search.test.tsx +180 -0
  70. package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
  71. package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
  72. package/bizar-dash/tests/docker-build.test.mjs +96 -0
  73. package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
  74. package/bizar-dash/tests/eval/report.test.mjs +284 -0
  75. package/bizar-dash/tests/eval/runner.test.mjs +471 -0
  76. package/bizar-dash/tests/lib/search-fuzzy.test.ts +149 -0
  77. package/bizar-dash/tests/memory-graph-view.test.tsx +69 -0
  78. package/bizar-dash/tests/memory-graph.test.mjs +95 -0
  79. package/bizar-dash/tests/ocr.test.mjs +87 -0
  80. package/bizar-dash/tests/otel.test.mjs +188 -0
  81. package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
  82. package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
  83. package/bizar-dash/tests/plugins-store.test.mjs +455 -0
  84. package/bizar-dash/tests/users.test.mjs +108 -0
  85. package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
  86. package/bizar-dash/tests/voice-store.test.mjs +148 -0
  87. package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
  88. package/bizar-dash/tests/workspaces.test.mjs +527 -0
  89. package/cli/bin.mjs +72 -2
  90. package/cli/commands/clip.mjs +146 -0
  91. package/cli/commands/dash.mjs +6 -0
  92. package/cli/commands/deploy/cloudflare.mjs +250 -0
  93. package/cli/commands/deploy/docker.mjs +221 -0
  94. package/cli/commands/deploy/fly.mjs +161 -0
  95. package/cli/commands/deploy/vercel.mjs +225 -0
  96. package/cli/commands/deploy.mjs +240 -0
  97. package/cli/commands/eval.mjs +378 -0
  98. package/cli/commands/marketplace.mjs +64 -0
  99. package/cli/commands/ocr.mjs +165 -0
  100. package/cli/commands/plugin.mjs +358 -0
  101. package/cli/commands/voice.mjs +211 -0
  102. package/cli/commands/workspace.mjs +247 -0
  103. package/package.json +12 -2
  104. package/templates/deploy/cloudflare/README.md +32 -0
  105. package/templates/deploy/cloudflare/functions-index.template.js +15 -0
  106. package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
  107. package/templates/deploy/docker/.env.template +16 -0
  108. package/templates/deploy/docker/README.md +58 -0
  109. package/templates/deploy/docker/docker-compose.template.yml +23 -0
  110. package/templates/deploy/fly/README.md +35 -0
  111. package/templates/deploy/fly/fly.toml.template +28 -0
  112. package/templates/deploy/vercel/README.md +29 -0
  113. package/templates/deploy/vercel/api-index.template.js +18 -0
  114. package/templates/deploy/vercel/vercel.json.template +16 -0
  115. package/templates/eval-fixtures/README.md +58 -0
  116. package/templates/eval-fixtures/code-search-basic.json +28 -0
  117. package/templates/eval-fixtures/latency-bounds.json +16 -0
  118. package/templates/eval-fixtures/regression-suite.json +79 -0
  119. package/templates/eval-fixtures/response-format.json +30 -0
  120. package/templates/eval-fixtures/tool-call-correctness.json +24 -0
  121. package/templates/plugin-template/README.md +121 -0
  122. package/templates/plugin-template/index.js +66 -0
  123. package/templates/plugin-template/plugin.json +42 -0
  124. package/templates/plugin-template/tests/plugin.test.js +83 -0
  125. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +0 -361
  126. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +0 -1
  127. package/bizar-dash/dist/assets/mobile-BK8-ythT.js +0 -351
  128. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +0 -1
  129. package/bizar-dash/dist/assets/mobile-Chvf9u_B.js +0 -1
@@ -0,0 +1,147 @@
1
+ /**
2
+ * src/server/routes/eval.mjs
3
+ *
4
+ * v5.0.0 — REST surface for the eval framework.
5
+ *
6
+ * Endpoints:
7
+ * GET /api/eval/runs — list recent runs
8
+ * GET /api/eval/runs/:id — get run details
9
+ * POST /api/eval/run — run a suite
10
+ * GET /api/eval/runs/:id/compare/:otherId — diff two runs
11
+ * GET /api/eval/fixtures — list fixtures in a suite path
12
+ *
13
+ * The llmCall defaults to minimax.chatCompletion but can be overridden
14
+ * via the state for custom providers.
15
+ */
16
+ import { Router } from 'express';
17
+ import { loadFixtures, runFixture, runSuite } from '../eval.mjs';
18
+ import { saveRun, listRuns, getRun, compareRuns, buildRunId } from '../eval-store.mjs';
19
+ import { chatCompletion } from '../minimax.mjs';
20
+ import { wrap } from './_shared.mjs';
21
+
22
+ /**
23
+ * @param {object} deps
24
+ * @param {object} deps.state
25
+ * @param {Function} deps.broadcast
26
+ * @returns {import('express').Router}
27
+ */
28
+ export function createEvalRouter({ state, broadcast }) {
29
+ const router = Router();
30
+
31
+ // GET /api/eval/runs — list recent runs
32
+ router.get('/eval/runs', wrap(async (req, res) => {
33
+ const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
34
+ const runs = await listRuns({ limit });
35
+ res.json({ runs });
36
+ }));
37
+
38
+ // GET /api/eval/runs/:id — get run details
39
+ router.get('/eval/runs/:id', wrap(async (req, res) => {
40
+ const run = await getRun(req.params.id);
41
+ if (!run) {
42
+ res.status(404).json({ error: 'not_found', message: `run ${req.params.id} not found` });
43
+ return;
44
+ }
45
+ res.json(run);
46
+ }));
47
+
48
+ // GET /api/eval/fixtures — list fixtures in a suite path
49
+ router.get('/eval/fixtures', wrap(async (req, res) => {
50
+ const { path: suitePath } = req.query;
51
+ if (!suitePath || typeof suitePath !== 'string') {
52
+ res.status(400).json({ error: 'bad_request', message: 'query.path is required' });
53
+ return;
54
+ }
55
+ const fixtures = loadFixtures(suitePath);
56
+ res.json({ fixtures });
57
+ }));
58
+
59
+ // POST /api/eval/run — run a suite
60
+ router.post('/eval/run', wrap(async (req, res) => {
61
+ const { suitePath, fixtures: fixtureIds, concurrency = 5, agent = 'thor' } = req.body || {};
62
+
63
+ if (!suitePath || typeof suitePath !== 'string') {
64
+ res.status(400).json({ error: 'bad_request', message: 'suitePath is required' });
65
+ return;
66
+ }
67
+
68
+ // Build the llmCall — use minimax by default, allow state override
69
+ const llmCall = async (prompt, opts = {}) => {
70
+ // Check if state has a custom eval LLM provider
71
+ const customCall = state?.getSettings?.()?.eval?.llmCall;
72
+ if (customCall) {
73
+ return customCall(prompt, opts);
74
+ }
75
+ // Default: minimax chatCompletion
76
+ const result = await chatCompletion({ prompt, model: 'MiniMax-M3' });
77
+ if (!result.ok) {
78
+ throw new Error(result.message || 'llm call failed');
79
+ }
80
+ return {
81
+ content: result.content,
82
+ usage: {
83
+ inputTokens: result.usage?.prompt_tokens ?? 0,
84
+ outputTokens: result.usage?.completion_tokens ?? 0,
85
+ totalTokens: result.usage?.total_tokens ?? 0,
86
+ },
87
+ };
88
+ };
89
+
90
+ const runId = buildRunId();
91
+ const startedAt = new Date().toISOString();
92
+
93
+ // Load fixtures
94
+ let fixtures = loadFixtures(suitePath);
95
+ if (fixtureIds?.length) {
96
+ fixtures = fixtures.filter((f) => fixtureIds.includes(f.id));
97
+ }
98
+
99
+ if (fixtures.length === 0) {
100
+ res.status(400).json({ error: 'bad_request', message: 'no fixtures found in suite' });
101
+ return;
102
+ }
103
+
104
+ // Broadcast start
105
+ broadcast({ type: 'eval:run:start', runId, total: fixtures.length });
106
+
107
+ // Run the suite
108
+ const suiteResult = await runSuite(suitePath, {
109
+ llmCall,
110
+ concurrency,
111
+ timeoutMs: 120_000,
112
+ });
113
+
114
+ const finishedAt = new Date().toISOString();
115
+ const run = {
116
+ id: runId,
117
+ startedAt,
118
+ finishedAt,
119
+ suitePath,
120
+ total: suiteResult.total,
121
+ passed: suiteResult.passed,
122
+ failed: suiteResult.failed,
123
+ results: suiteResult.results,
124
+ };
125
+
126
+ // Persist
127
+ await saveRun(run);
128
+
129
+ // Broadcast complete
130
+ broadcast({ type: 'eval:run:complete', runId, passed: run.passed, failed: run.failed });
131
+
132
+ res.status(201).json(run);
133
+ }));
134
+
135
+ // GET /api/eval/runs/:id/compare/:otherId — diff two runs
136
+ router.get('/eval/runs/:id/compare/:otherId', wrap(async (req, res) => {
137
+ const { id, otherId } = req.params;
138
+ const diff = await compareRuns(id, otherId);
139
+ if (diff.error) {
140
+ res.status(404).json({ error: 'not_found', message: diff.error });
141
+ return;
142
+ }
143
+ res.json(diff);
144
+ }));
145
+
146
+ return router;
147
+ }
@@ -1149,6 +1149,52 @@ export function createMemoryRouter({ projectRoot }) {
1149
1149
  }
1150
1150
  }));
1151
1151
 
1152
+ // GET /memory/graph — combined knowledge graph (LightRAG entities + Obsidian wikilinks).
1153
+ // Query params: ?root=<noteId>&depth=2&limit=200
1154
+ router.get('/memory/graph', wrap(async (req, res) => {
1155
+ const root = req.query.root ? String(req.query.root) : null;
1156
+ const depth = Math.min(parseInt(req.query.depth, 10) || 2, 3);
1157
+ const limit = Math.min(parseInt(req.query.limit, 10) || 200, 500);
1158
+
1159
+ try {
1160
+ const lightrag = await getMemoryLightrag();
1161
+ const [lrGraph, obsidianGraph] = await Promise.all([
1162
+ lightrag.getLightRAGGraph({ projectRoot, root, depth, limit }),
1163
+ (async () => {
1164
+ try {
1165
+ const { getObsidianLinkGraph } = memoryStore;
1166
+ return getObsidianLinkGraph({ projectRoot, limit });
1167
+ } catch {
1168
+ return { nodes: [], edges: [] };
1169
+ }
1170
+ })(),
1171
+ ]);
1172
+
1173
+ // Dedupe nodes by id; prefer the lightrag node (has richer type/size).
1174
+ const nodeMap = new Map();
1175
+ for (const n of obsidianGraph.nodes) nodeMap.set(n.id, n);
1176
+ for (const n of lrGraph.nodes) {
1177
+ if (!nodeMap.has(n.id)) nodeMap.set(n.id, n);
1178
+ }
1179
+ const nodes = [...nodeMap.values()].slice(0, limit);
1180
+
1181
+ // Merge edges from both sources; deduplicate by source+target.
1182
+ const edgeSet = new Set();
1183
+ const edges = [];
1184
+ for (const e of [...obsidianGraph.edges, ...lrGraph.edges]) {
1185
+ const key = `${e.source}|${e.target}`;
1186
+ if (!edgeSet.has(key)) {
1187
+ edgeSet.add(key);
1188
+ edges.push(e);
1189
+ }
1190
+ }
1191
+
1192
+ res.json({ nodes, edges, totalNodes: nodes.length, totalEdges: edges.length });
1193
+ } catch (err) {
1194
+ res.status(500).json({ error: 'graph_failed', message: err.message });
1195
+ }
1196
+ }));
1197
+
1152
1198
  // POST /memory/lightrag/reindex — alias of /memory/reindex (the canonical
1153
1199
  // path lives there for backwards compat). Both return the same shape.
1154
1200
  router.post('/memory/lightrag/reindex', wrap(async (_req, res) => {
@@ -0,0 +1,182 @@
1
+ /**
2
+ * src/server/routes/ocr.mjs
3
+ *
4
+ * v5.0.0 — Screenshot OCR endpoints.
5
+ *
6
+ * Accepts image uploads, extracts text via Tesseract.js, and saves
7
+ * the result as a markdown note in the vault.
8
+ */
9
+
10
+ import { Router } from 'express';
11
+ import { join } from 'node:path';
12
+ import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync } from 'node:fs';
13
+
14
+ let _wrap = null;
15
+ let _extractText = null;
16
+ async function getShared() {
17
+ if (!_wrap) {
18
+ const mod = await import('./_shared.mjs');
19
+ _wrap = mod.wrap;
20
+ }
21
+ return { wrap: _wrap };
22
+ }
23
+
24
+ async function getOcr() {
25
+ if (!_extractText) {
26
+ const mod = await import('../ocr.mjs');
27
+ _extractText = mod.extractText;
28
+ }
29
+ return { extractText: _extractText };
30
+ }
31
+
32
+ function getLogPath(projectRoot) {
33
+ return join(projectRoot, '.bizar', 'ocr-log.json');
34
+ }
35
+
36
+ function loadLog(projectRoot) {
37
+ try {
38
+ const p = getLogPath(projectRoot);
39
+ if (!existsSync(p)) return [];
40
+ return JSON.parse(readFileSync(p, 'utf8'));
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ function saveLog(projectRoot, log) {
47
+ const p = getLogPath(projectRoot);
48
+ mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
49
+ writeFileSync(p, JSON.stringify(log, null, 2) + '\n', 'utf8');
50
+ }
51
+
52
+ function appendLog(projectRoot, entry) {
53
+ const log = loadLog(projectRoot);
54
+ log.unshift(entry);
55
+ if (log.length > 100) log.length = 100;
56
+ saveLog(projectRoot, log);
57
+ }
58
+
59
+ export async function createOcrRouter({ projectRoot }) {
60
+ const router = Router();
61
+ const { wrap } = await getShared();
62
+
63
+ /**
64
+ * POST /api/ocr/process
65
+ *
66
+ * Accepts a multipart image upload, runs OCR, saves the result
67
+ * as a markdown note under ocr/<slug>.md.
68
+ *
69
+ * Expects: multipart/form-data with field "image"
70
+ * Returns: { ok, text, notePath }
71
+ */
72
+ router.post('/ocr/process', wrap(async (req, res) => {
73
+ let extractText;
74
+ try {
75
+ ({ extractText } = await getOcr());
76
+ } catch (err) {
77
+ res.status(503).json({
78
+ error: 'ocr_unavailable',
79
+ message: 'OCR engine is not available. Install tesseract.js: npm install tesseract.js',
80
+ });
81
+ return;
82
+ }
83
+
84
+ // Read image from body (binary or base64) or multipart
85
+ let imageBuffer = null;
86
+
87
+ if (Buffer.isBuffer(req.body)) {
88
+ imageBuffer = req.body;
89
+ } else if (req.body && req.body.image) {
90
+ // Base64-encoded image
91
+ const raw = req.body.image;
92
+ const matches = raw.match(/^data:image\/(png|jpeg|jpg|gif|webp);base64,(.+)$/);
93
+ if (matches) {
94
+ imageBuffer = Buffer.from(matches[2], 'base64');
95
+ } else {
96
+ // Assume raw base64 without prefix
97
+ imageBuffer = Buffer.from(raw, 'base64');
98
+ }
99
+ }
100
+
101
+ if (!imageBuffer || imageBuffer.length === 0) {
102
+ res.status(400).json({
103
+ error: 'bad_request',
104
+ message: 'No image data provided. Send multipart/form-data with "image" field, or JSON with base64 "image" field.',
105
+ });
106
+ return;
107
+ }
108
+
109
+ // Validate size (max 20MB)
110
+ if (imageBuffer.length > 20 * 1024 * 1024) {
111
+ res.status(413).json({ error: 'too_large', message: 'Image exceeds 20MB limit' });
112
+ return;
113
+ }
114
+
115
+ // Run OCR
116
+ let text;
117
+ try {
118
+ text = await extractText(imageBuffer);
119
+ } catch (err) {
120
+ res.status(500).json({
121
+ error: 'ocr_error',
122
+ message: `OCR processing failed: ${err instanceof Error ? err.message : String(err)}`,
123
+ });
124
+ return;
125
+ }
126
+
127
+ // Build note
128
+ const lang = req.body?.lang || 'eng';
129
+ const timestamp = Date.now();
130
+ const slug = `screenshot_ocr_${timestamp}`;
131
+ const relPath = `ocr/${slug}.md`;
132
+ const savedAt = new Date().toISOString();
133
+
134
+ const fm = {
135
+ title: `OCR Screenshot — ${savedAt}`,
136
+ savedAt,
137
+ type: 'ocr',
138
+ lang,
139
+ tags: ['ocr', 'screenshot'],
140
+ };
141
+
142
+ const noteBody = [
143
+ '## Extracted Text',
144
+ '',
145
+ '```text',
146
+ text || '(no text found)',
147
+ '```',
148
+ '',
149
+ ].join('\n');
150
+
151
+ const vaultDir = join(projectRoot, '.obsidian');
152
+ mkdirSync(join(vaultDir, 'ocr'), { recursive: true });
153
+ const notePath = join(vaultDir, relPath);
154
+
155
+ const noteContent = [
156
+ '---',
157
+ ...Object.entries(fm).map(([k, v]) => `${k}: ${JSON.stringify(v)}`),
158
+ '---',
159
+ '',
160
+ noteBody,
161
+ ].join('\n');
162
+
163
+ writeFileSync(notePath, noteContent, 'utf8');
164
+
165
+ // Log
166
+ appendLog(projectRoot, { id: slug, notePath: relPath, savedAt, textLength: text.length });
167
+
168
+ res.status(201).json({ ok: true, text, notePath: relPath, id: slug });
169
+ }));
170
+
171
+ /**
172
+ * GET /api/ocr/list
173
+ *
174
+ * Returns recent OCR entries from the log.
175
+ */
176
+ router.get('/ocr/list', wrap(async (_req, res) => {
177
+ const entries = loadLog(projectRoot);
178
+ res.json({ entries });
179
+ }));
180
+
181
+ return router;
182
+ }
@@ -26,10 +26,12 @@
26
26
  */
27
27
 
28
28
  import { Router } from 'express';
29
+ import { SpanStatusCode } from '@opentelemetry/api';
29
30
  import Database from 'better-sqlite3';
30
31
  import { join } from 'node:path';
31
32
  import { homedir } from 'node:os';
32
33
  import { wrap } from './_shared.mjs';
34
+ import { tracer } from '../otel.mjs';
33
35
  import {
34
36
  readServeInfo,
35
37
  listOpencodeSessions,
@@ -118,58 +120,90 @@ export function createOpencodeSessionsRouter() {
118
120
  // Returns 503 when the opencode plugin is offline.
119
121
  // ────────────────────────────────────────────────────────────────────
120
122
  router.post('/opencode-sessions/new', wrap(async (req, res) => {
121
- const body = req.body && typeof req.body === 'object' ? req.body : {};
122
- const title = typeof body.title === 'string' && body.title.trim().length > 0
123
- ? body.title.trim().slice(0, TITLE_MAX)
124
- : null;
125
- const agent = typeof body.agent === 'string' ? body.agent.trim() : '';
126
- if (!agent) {
127
- res.status(400).json({ error: 'bad_request', message: '`agent` is required' });
128
- return;
129
- }
130
- if (!AGENT_NAME_RE.test(agent)) {
131
- res.status(400).json({ error: 'bad_request', message: '`agent` is invalid (allowed: [A-Za-z0-9_-]{1,64})' });
132
- return;
133
- }
134
- if (title !== null && title.length > TITLE_MAX) {
135
- res.status(400).json({ error: 'bad_request', message: `title too long (> ${TITLE_MAX} chars)` });
136
- return;
137
- }
123
+ return tracer.startActiveSpan('opencode.session.create', async (span) => {
124
+ let spanEnded = false;
125
+ const finishSpan = () => {
126
+ if (spanEnded) return;
127
+ spanEnded = true;
128
+ try {
129
+ const code = res.statusCode || 0;
130
+ if (code >= 400) {
131
+ span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${code}` });
132
+ } else {
133
+ span.setStatus({ code: SpanStatusCode.OK });
134
+ }
135
+ } finally {
136
+ span.end();
137
+ }
138
+ };
139
+ res.once('finish', finishSpan);
140
+ try {
141
+ const body = req.body && typeof req.body === 'object' ? req.body : {};
142
+ const title = typeof body.title === 'string' && body.title.trim().length > 0
143
+ ? body.title.trim().slice(0, TITLE_MAX)
144
+ : null;
145
+ const agent = typeof body.agent === 'string' ? body.agent.trim() : '';
146
+ span.setAttribute('opencode.session.agent', agent);
147
+ span.setAttribute('opencode.session.title_length', title === null ? 0 : title.length);
148
+ if (!agent) {
149
+ res.status(400).json({ error: 'bad_request', message: '`agent` is required' });
150
+ return;
151
+ }
152
+ if (!AGENT_NAME_RE.test(agent)) {
153
+ res.status(400).json({ error: 'bad_request', message: '`agent` is invalid (allowed: [A-Za-z0-9_-]{1,64})' });
154
+ return;
155
+ }
156
+ if (title !== null && title.length > TITLE_MAX) {
157
+ res.status(400).json({ error: 'bad_request', message: `title too long (> ${TITLE_MAX} chars)` });
158
+ return;
159
+ }
138
160
 
139
- const info = readServeInfo();
140
- if (!info) {
141
- res.status(503).json({
142
- error: 'plugin_offline',
143
- message: 'opencode plugin is not running',
144
- });
145
- return;
146
- }
161
+ const info = readServeInfo();
162
+ if (!info) {
163
+ res.status(503).json({
164
+ error: 'plugin_offline',
165
+ message: 'opencode plugin is not running',
166
+ });
167
+ return;
168
+ }
147
169
 
148
- const directory = typeof body.directory === 'string' && body.directory.length > 0
149
- ? body.directory
150
- : (info.worktree || '');
170
+ const directory = typeof body.directory === 'string' && body.directory.length > 0
171
+ ? body.directory
172
+ : (info.worktree || '');
173
+ span.setAttribute('opencode.session.directory', directory);
151
174
 
152
- const finalTitle = title || `Chat: ${agent}`;
153
- const result = await createOpencodeSession(
154
- info,
155
- { title: finalTitle, agent },
156
- directory,
157
- );
158
- if (!result.ok || !result.sessionId) {
159
- const status = result.status === 404 ? 404 : 502;
160
- res.status(status).json({
161
- error: 'opencode_error',
162
- message: result.error || 'failed to create opencode session',
163
- });
164
- return;
165
- }
175
+ const finalTitle = title || `Chat: ${agent}`;
176
+ const result = await createOpencodeSession(
177
+ info,
178
+ { title: finalTitle, agent },
179
+ directory,
180
+ );
181
+ if (!result.ok || !result.sessionId) {
182
+ const status = result.status === 404 ? 404 : 502;
183
+ res.status(status).json({
184
+ error: 'opencode_error',
185
+ message: result.error || 'failed to create opencode session',
186
+ });
187
+ return;
188
+ }
166
189
 
167
- res.status(201).json({
168
- id: result.sessionId,
169
- title: finalTitle,
170
- agent,
171
- directory,
172
- createdAt: Date.now(),
190
+ span.setAttribute('opencode.session.id', result.sessionId);
191
+ res.status(201).json({
192
+ id: result.sessionId,
193
+ title: finalTitle,
194
+ agent,
195
+ directory,
196
+ createdAt: Date.now(),
197
+ });
198
+ } catch (err) {
199
+ if (!spanEnded) {
200
+ spanEnded = true;
201
+ span.recordException(err);
202
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
203
+ span.end();
204
+ }
205
+ throw err;
206
+ }
173
207
  });
174
208
  }));
175
209