@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.
- 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-DmpSFPJY.js +9 -0
- package/bizar-dash/dist/assets/index-DmpSFPJY.js.map +1 -0
- 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-DX_Jh8Wc.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-Chvf9u_B.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/memory-lightrag.mjs +109 -0
- package/bizar-dash/src/server/memory-store.mjs +121 -0
- package/bizar-dash/src/server/ocr.mjs +55 -0
- package/bizar-dash/src/server/otel.mjs +133 -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/chat.mjs +246 -170
- 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/memory.mjs +46 -0
- package/bizar-dash/src/server/routes/ocr.mjs +182 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +82 -48
- 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/server.mjs +40 -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/SettingsSearch.tsx +204 -89
- 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/lib/search.ts +115 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +10 -35
- package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +69 -0
- package/bizar-dash/src/web/styles/memory.css +166 -1
- package/bizar-dash/src/web/styles/settings.css +80 -0
- package/bizar-dash/src/web/views/Memory.tsx +22 -2
- package/bizar-dash/src/web/views/Settings.tsx +99 -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/MemoryGraphLegend.tsx +29 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +192 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +336 -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 +73 -0
- 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/settings-search.test.tsx +180 -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/docker-build.test.mjs +96 -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/lib/search-fuzzy.test.ts +149 -0
- package/bizar-dash/tests/memory-graph-view.test.tsx +69 -0
- package/bizar-dash/tests/memory-graph.test.mjs +95 -0
- package/bizar-dash/tests/ocr.test.mjs +87 -0
- package/bizar-dash/tests/otel.test.mjs +188 -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/dash.mjs +6 -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 -2
- 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-DHZmbnxQ.js +0 -361
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js +0 -351
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +0 -1
- 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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|