@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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/voice-store.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.0.0 — Voice note persistence layer.
|
|
5
|
+
*
|
|
6
|
+
* Audio files are stored at:
|
|
7
|
+
* ~/.local/share/bizar/voice-notes/<id>.webm
|
|
8
|
+
*
|
|
9
|
+
* Markdown transcripts are written to the user's vault at:
|
|
10
|
+
* <vaultPath>/.obsidian/voice-notes/<id>.md
|
|
11
|
+
* or (if no vaultPath is set) to the default vault:
|
|
12
|
+
* ~/.local/share/bizar/voice-notes/transcripts/<id>.md
|
|
13
|
+
*
|
|
14
|
+
* Metadata index is at:
|
|
15
|
+
* ~/.local/share/bizar/voice-notes/index.json
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
renameSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readdirSync,
|
|
25
|
+
unlinkSync,
|
|
26
|
+
} from 'node:fs';
|
|
27
|
+
import { join, dirname } from 'node:path';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { randomBytes } from 'node:crypto';
|
|
30
|
+
import { info, warn, child } from './logger.mjs';
|
|
31
|
+
|
|
32
|
+
const log = child({ module: 'voice-store' });
|
|
33
|
+
|
|
34
|
+
const HOME = homedir();
|
|
35
|
+
const VOICE_DIR = join(HOME, '.local', 'share', 'bizar', 'voice-notes');
|
|
36
|
+
const INDEX_FILE = join(VOICE_DIR, 'index.json');
|
|
37
|
+
const TRANSCRIPT_DIR = join(VOICE_DIR, 'transcripts');
|
|
38
|
+
|
|
39
|
+
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function audioPath(id) {
|
|
42
|
+
return join(VOICE_DIR, `${id}.webm`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function transcriptPath(id, vaultPath) {
|
|
46
|
+
if (vaultPath) {
|
|
47
|
+
const dir = join(vaultPath, '.obsidian', 'voice-notes');
|
|
48
|
+
return join(dir, `${id}.md`);
|
|
49
|
+
}
|
|
50
|
+
return join(TRANSCRIPT_DIR, `${id}.md`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Atomic helpers ──────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function atomicWriteJson(filePath, data) {
|
|
56
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
57
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
58
|
+
renameSync(tmp, filePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Index ───────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {Object} VoiceNote
|
|
65
|
+
* @property {string} id
|
|
66
|
+
* @property {string} audioPath
|
|
67
|
+
* @property {string|null} notePath - markdown transcript path (may not exist yet)
|
|
68
|
+
* @property {string|null} transcript
|
|
69
|
+
* @property {number|null} durationSec
|
|
70
|
+
* @property {string|null} vaultPath
|
|
71
|
+
* @property {number} createdAtMs
|
|
72
|
+
* @property {number} updatedAtMs
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/** @returns {VoiceNote[]} */
|
|
76
|
+
function loadIndex() {
|
|
77
|
+
try {
|
|
78
|
+
if (!existsSync(INDEX_FILE)) return [];
|
|
79
|
+
const text = readFileSync(INDEX_FILE, 'utf8');
|
|
80
|
+
if (!text.trim()) return [];
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function saveIndex(notes) {
|
|
88
|
+
mkdirSync(dirname(INDEX_FILE), { recursive: true });
|
|
89
|
+
atomicWriteJson(INDEX_FILE, notes);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Save a voice note: write audio file + markdown transcript + update index.
|
|
96
|
+
*
|
|
97
|
+
* @param {{ audioBuffer: Buffer|Uint8Array, transcript: string|null, durationSec: number|null, vaultPath: string|null }} opts
|
|
98
|
+
* @returns {Promise<{ notePath: string, audioPath: string, id: string }>}
|
|
99
|
+
*/
|
|
100
|
+
export async function saveVoiceNote({ audioBuffer, transcript, durationSec, vaultPath }) {
|
|
101
|
+
const id = randomBytes(8).toString('hex');
|
|
102
|
+
const ap = audioPath(id);
|
|
103
|
+
|
|
104
|
+
// 1. Write audio (binary, no atomic needed for raw bytes — just ensure dir)
|
|
105
|
+
mkdirSync(dirname(ap), { recursive: true });
|
|
106
|
+
writeFileSync(ap, Buffer.from(audioBuffer));
|
|
107
|
+
|
|
108
|
+
// 2. Write markdown transcript if we have one
|
|
109
|
+
let notePath = null;
|
|
110
|
+
if (transcript) {
|
|
111
|
+
const tp = transcriptPath(id, vaultPath);
|
|
112
|
+
const tpDir = dirname(tp);
|
|
113
|
+
mkdirSync(tpDir, { recursive: true });
|
|
114
|
+
const md = [
|
|
115
|
+
'---',
|
|
116
|
+
`title: Voice Note ${new Date().toISOString().slice(0, 16).replace('T', ' ')}`,
|
|
117
|
+
`date: ${new Date().toISOString()}`,
|
|
118
|
+
`type: voice-note`,
|
|
119
|
+
`audio: ${id}.webm`,
|
|
120
|
+
`duration: ${durationSec ?? '?'}`,
|
|
121
|
+
'---',
|
|
122
|
+
'',
|
|
123
|
+
`# ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}`,
|
|
124
|
+
'',
|
|
125
|
+
transcript,
|
|
126
|
+
].join('\n');
|
|
127
|
+
writeFileSync(tp, md, 'utf8');
|
|
128
|
+
notePath = tp;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Update index
|
|
132
|
+
const notes = loadIndex();
|
|
133
|
+
/** @type {VoiceNote} */
|
|
134
|
+
const entry = {
|
|
135
|
+
id,
|
|
136
|
+
audioPath: ap,
|
|
137
|
+
notePath,
|
|
138
|
+
transcript: transcript || null,
|
|
139
|
+
durationSec: durationSec || null,
|
|
140
|
+
vaultPath: vaultPath || null,
|
|
141
|
+
createdAtMs: Date.now(),
|
|
142
|
+
updatedAtMs: Date.now(),
|
|
143
|
+
};
|
|
144
|
+
notes.unshift(entry); // newest first
|
|
145
|
+
saveIndex(notes);
|
|
146
|
+
|
|
147
|
+
log.info('voice note saved', { id, notePath, audioPath: ap });
|
|
148
|
+
return { notePath, audioPath: ap, id };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {{ vaultPath?: string|null, limit?: number }} opts
|
|
153
|
+
* @returns {VoiceNote[]}
|
|
154
|
+
*/
|
|
155
|
+
export function listVoiceNotes({ vaultPath = null, limit = 50 } = {}) {
|
|
156
|
+
const notes = loadIndex();
|
|
157
|
+
const filtered = vaultPath
|
|
158
|
+
? notes.filter((n) => n.vaultPath === vaultPath)
|
|
159
|
+
: notes;
|
|
160
|
+
return filtered.slice(0, limit);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} id
|
|
165
|
+
* @returns {VoiceNote|undefined}
|
|
166
|
+
*/
|
|
167
|
+
export function getVoiceNote(id) {
|
|
168
|
+
return loadIndex().find((n) => n.id === id);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} id
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
export function deleteVoiceNote(id) {
|
|
176
|
+
const notes = loadIndex();
|
|
177
|
+
const idx = notes.findIndex((n) => n.id === id);
|
|
178
|
+
if (idx === -1) return false;
|
|
179
|
+
|
|
180
|
+
const { audioPath: ap, notePath: tp } = notes[idx];
|
|
181
|
+
|
|
182
|
+
// Remove audio file
|
|
183
|
+
try {
|
|
184
|
+
if (existsSync(ap)) unlinkSync(ap);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
warn('failed to remove audio file', { id, ap, err: err.message });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Remove transcript file
|
|
190
|
+
if (tp) {
|
|
191
|
+
try {
|
|
192
|
+
if (existsSync(tp)) unlinkSync(tp);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
warn('failed to remove transcript file', { id, tp, err: err.message });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
notes.splice(idx, 1);
|
|
199
|
+
saveIndex(notes);
|
|
200
|
+
log.info('voice note deleted', { id });
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/voice-transcribe.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.0.0 — Whisper transcription for voice notes.
|
|
5
|
+
*
|
|
6
|
+
* Uses OpenAI Whisper API (or a compatible BIZAR_WHISPER_ENDPOINT).
|
|
7
|
+
* Returns null on failure so callers can degrade gracefully.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { info, warn, child } from './logger.mjs';
|
|
11
|
+
|
|
12
|
+
const log = child({ module: 'voice-transcribe' });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Transcribe an audio buffer via Whisper.
|
|
16
|
+
*
|
|
17
|
+
* @param {Buffer|Uint8Array} audioBuffer - raw audio bytes
|
|
18
|
+
* @param {{ mimeType?: string }} opts
|
|
19
|
+
* @returns {Promise<string|null>} transcript text or null
|
|
20
|
+
*/
|
|
21
|
+
export async function transcribe(audioBuffer, { mimeType = 'audio/webm' } = {}) {
|
|
22
|
+
if (!process.env.OPENAI_API_KEY && !process.env.BIZAR_WHISPER_ENDPOINT) {
|
|
23
|
+
log.debug('no whisper credentials configured, skipping transcription');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const endpoint =
|
|
28
|
+
process.env.BIZAR_WHISPER_ENDPOINT || 'https://api.openai.com/v1/audio/transcriptions';
|
|
29
|
+
|
|
30
|
+
const body = new FormData();
|
|
31
|
+
body.append(
|
|
32
|
+
'file',
|
|
33
|
+
new Blob([audioBuffer], { type: mimeType }),
|
|
34
|
+
'audio.webm',
|
|
35
|
+
);
|
|
36
|
+
body.append('model', 'whisper-1');
|
|
37
|
+
|
|
38
|
+
const headers = {};
|
|
39
|
+
if (process.env.OPENAI_API_KEY) {
|
|
40
|
+
headers['Authorization'] = `Bearer ${process.env.OPENAI_API_KEY}`;
|
|
41
|
+
}
|
|
42
|
+
// BIZAR_WHISPER_ENDPOINT may be a local proxy that doesn't need auth
|
|
43
|
+
// but may also support Authorization header — only set if key exists
|
|
44
|
+
|
|
45
|
+
log.info('submitting audio to whisper', { endpoint, sizeBytes: audioBuffer.byteLength });
|
|
46
|
+
|
|
47
|
+
let resp;
|
|
48
|
+
try {
|
|
49
|
+
resp = await fetch(endpoint, { method: 'POST', body, headers });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
warn('whisper fetch failed', { err: err.message });
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!resp.ok) {
|
|
56
|
+
const text = await resp.text().catch(() => '');
|
|
57
|
+
warn('whisper returned error', { status: resp.status, body: text });
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let data;
|
|
62
|
+
try {
|
|
63
|
+
data = await resp.json();
|
|
64
|
+
} catch {
|
|
65
|
+
warn('whisper returned non-JSON');
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const text = typeof data.text === 'string' ? data.text.trim() : null;
|
|
70
|
+
log.info('transcription complete', { textLength: text?.length ?? 0 });
|
|
71
|
+
return text;
|
|
72
|
+
}
|