@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,358 @@
1
+ /**
2
+ * cli/commands/plugin.mjs
3
+ *
4
+ * v5.0.0 — Plugin marketplace CLI.
5
+ *
6
+ * Talks to the running dashboard's HTTP API (which in turn uses
7
+ * `bizar-dash/src/server/plugins/{registry,store,sandbox}.mjs`).
8
+ *
9
+ * Usage:
10
+ * bizar plugin search <query> — search the registry
11
+ * bizar plugin install <id> — install from registry
12
+ * bizar plugin list — list installed
13
+ * bizar plugin info <id> — show details + config schema
14
+ * bizar plugin config <id> [key] [value] — get/set config (dot-path key)
15
+ * bizar plugin update <id> — update to latest
16
+ * bizar plugin uninstall <id> — remove
17
+ * bizar plugin invoke <id> <method> [...args] — run a plugin method
18
+ */
19
+ import chalk from 'chalk';
20
+ import { readFileSync } from 'node:fs';
21
+ import { homedir } from 'node:os';
22
+ import { join } from 'node:path';
23
+
24
+ // ── Config dir helper (same shape as cli/commands/mod.mjs) ────────────────
25
+
26
+ function getBizarConfigDir() {
27
+ if (process.platform === 'win32') {
28
+ return process.env.APPDATA
29
+ ? join(process.env.APPDATA, 'bizar')
30
+ : join(homedir(), '.config', 'bizar');
31
+ }
32
+ return process.env.XDG_CONFIG_HOME
33
+ ? join(process.env.XDG_CONFIG_HOME, 'bizar')
34
+ : join(homedir(), '.config', 'bizar');
35
+ }
36
+
37
+ // ── Help ───────────────────────────────────────────────────────────────────
38
+
39
+ export function showPluginHelp() {
40
+ console.log(`
41
+ bizar plugin — Manage marketplace plugins (via the dashboard's HTTP API)
42
+
43
+ Usage:
44
+ bizar plugin search <query> Search the public registry
45
+ bizar plugin install <id> Install a plugin from the registry
46
+ bizar plugin list List installed plugins
47
+ bizar plugin info <id> Show details + config schema
48
+ bizar plugin config <id> [key] [value] Get or set a config value
49
+ bizar plugin update <id> Update to the latest version
50
+ bizar plugin uninstall <id> Remove a plugin
51
+ bizar plugin invoke <id> <method> [...args] Invoke a plugin method
52
+
53
+ Examples:
54
+ bizar plugin search deploy
55
+ bizar plugin install vercel-deploy
56
+ bizar plugin info vercel-deploy
57
+ bizar plugin config vercel-deploy apiKey sk-xxx
58
+ bizar plugin update vercel-deploy
59
+ bizar plugin invoke vercel-deploy deploy ./my-project
60
+
61
+ Description:
62
+ Subcommands call the running dashboard's HTTP API. If no dashboard is
63
+ reachable, you'll be told to run \`bizar dash start --bg\` first.
64
+
65
+ \`bizar plugin config <id> <key> <value>\` sets a single key at the
66
+ given dot-path. To replace the entire config object, use the dashboard
67
+ UI or the REST API directly.
68
+ `);
69
+ }
70
+
71
+ // ── API helpers (mirror cli/commands/mod.mjs) ──────────────────────────────
72
+
73
+ async function request(baseUrl, method, path, body) {
74
+ const res = await fetch(`${baseUrl}${path}`, {
75
+ method,
76
+ headers: body ? { 'Content-Type': 'application/json' } : {},
77
+ body: body ? JSON.stringify(body) : undefined,
78
+ });
79
+ const text = await res.text();
80
+ let json = null;
81
+ try { json = text ? JSON.parse(text) : null; } catch { /* keep raw */ }
82
+ if (!res.ok) {
83
+ const msg = json?.message || json?.error || text || `HTTP ${res.status}`;
84
+ const err = new Error(`${method} ${path} failed: ${msg}`);
85
+ err.status = res.status;
86
+ err.code = json?.error;
87
+ throw err;
88
+ }
89
+ return { status: res.status, json };
90
+ }
91
+
92
+ async function readDashboardUrl() {
93
+ const portFile = join(getBizarConfigDir(), 'dashboard.port');
94
+ let port = null;
95
+ try {
96
+ port = parseInt(readFileSync(portFile, 'utf8').trim(), 10);
97
+ if (!Number.isFinite(port) || port <= 0) port = null;
98
+ } catch {
99
+ port = null;
100
+ }
101
+ if (!port) {
102
+ console.error(
103
+ chalk.red(' ✗ Dashboard is not running (no port file at ' + portFile + ').'),
104
+ );
105
+ console.error(chalk.dim(' Start it first: `bizar dash start --bg`'));
106
+ process.exit(1);
107
+ }
108
+ return `http://127.0.0.1:${port}`;
109
+ }
110
+
111
+ // ── Subcommand implementations ─────────────────────────────────────────────
112
+
113
+ async function cmdSearch(args, baseUrl) {
114
+ const query = args.join(' ').trim();
115
+ const r = await request(baseUrl, 'GET',
116
+ `/api/plugins/registry?q=${encodeURIComponent(query)}`);
117
+ const { plugins = [], registry: reg } = r.json;
118
+ console.log(chalk.dim(` Source: ${reg?.source || '(unknown)'}`));
119
+ console.log('');
120
+ if (plugins.length === 0) {
121
+ console.log(chalk.dim(' (no plugins matched)'));
122
+ return;
123
+ }
124
+ for (const p of plugins) {
125
+ const installed = p.installed
126
+ ? chalk.green(`installed v${p.installedVersion || '?'}`)
127
+ : chalk.dim('not installed');
128
+ const upgrade = p.upgradeAvailable
129
+ ? chalk.yellow(` ↑ v${p.upgradeAvailable} available`)
130
+ : '';
131
+ console.log(
132
+ ` ${p.id.padEnd(24)} v${(p.version || '?').padEnd(10)} ${installed}${upgrade} ${p.name || ''}`,
133
+ );
134
+ }
135
+ }
136
+
137
+ async function cmdInstall(args, baseUrl) {
138
+ const id = args[0];
139
+ if (!id) {
140
+ console.error(chalk.red(' ✗ Missing plugin id. Usage: bizar plugin install <id>'));
141
+ process.exit(1);
142
+ }
143
+ const force = args.includes('--force') || args.includes('-f');
144
+ const r = await request(baseUrl, 'POST', '/api/plugins/install',
145
+ { pluginId: id, force });
146
+ console.log(chalk.green(` ✓ Installed "${r.json.id}" v${r.json.version}`));
147
+ }
148
+
149
+ async function cmdList(_args, baseUrl) {
150
+ const r = await request(baseUrl, 'GET', '/api/plugins/installed');
151
+ const plugins = r.json.plugins || [];
152
+ if (plugins.length === 0) {
153
+ console.log(chalk.dim(' (no plugins installed)'));
154
+ return;
155
+ }
156
+ for (const p of plugins) {
157
+ console.log(` ${p.id.padEnd(24)} v${(p.version || '?').padEnd(10)} ${p.name || ''}`);
158
+ }
159
+ }
160
+
161
+ async function cmdInfo(args, baseUrl) {
162
+ const id = args[0];
163
+ if (!id) {
164
+ console.error(chalk.red(' ✗ Missing plugin id. Usage: bizar plugin info <id>'));
165
+ process.exit(1);
166
+ }
167
+ // Try registry first (gets description + schema); fall back to
168
+ // installed lookup if not in registry.
169
+ let plugin;
170
+ try {
171
+ const r = await request(baseUrl, 'GET',
172
+ `/api/plugins/registry/${encodeURIComponent(id)}`);
173
+ plugin = r.json;
174
+ } catch (err) {
175
+ if (err.status !== 404) throw err;
176
+ plugin = null;
177
+ }
178
+ const installed = await safeGetInstalled(id, baseUrl);
179
+ const merged = { ...(plugin || {}), ...(installed || {}) };
180
+ if (!merged.id) {
181
+ console.error(chalk.red(` ✗ Plugin "${id}" not found in registry or installed list`));
182
+ process.exit(1);
183
+ }
184
+ console.log(` ${chalk.bold(merged.name || merged.id)} ${chalk.dim('v' + (merged.version || '?'))}`);
185
+ if (merged.description) console.log(` ${chalk.dim(merged.description)}`);
186
+ if (merged.author) console.log(` ${chalk.dim('author:')} ${merged.author}`);
187
+ if (merged.category) console.log(` ${chalk.dim('category:')} ${merged.category}`);
188
+ if (Array.isArray(merged.tags) && merged.tags.length) {
189
+ console.log(` ${chalk.dim('tags:')} ${merged.tags.join(', ')}`);
190
+ }
191
+ if (merged.homepage) console.log(` ${chalk.dim('homepage:')} ${merged.homepage}`);
192
+ if (Array.isArray(merged.permissions) && merged.permissions.length) {
193
+ console.log(` ${chalk.dim('permissions:')} ${merged.permissions.join(', ')}`);
194
+ }
195
+ if (installed) {
196
+ console.log('');
197
+ console.log(` ${chalk.dim('install path:')} ${installed.path}`);
198
+ console.log(` ${chalk.dim('installed:')} ${installed.installedAt}`);
199
+ console.log(` ${chalk.dim('config:')}`);
200
+ if (!installed.config || Object.keys(installed.config).length === 0) {
201
+ console.log(chalk.dim(' (empty)'));
202
+ } else {
203
+ for (const [k, v] of Object.entries(installed.config)) {
204
+ const display = typeof v === 'string' && v.length > 40
205
+ ? v.slice(0, 37) + '...'
206
+ : JSON.stringify(v);
207
+ console.log(` ${chalk.cyan(k)}: ${display}`);
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ async function safeGetInstalled(id, baseUrl) {
214
+ try {
215
+ const r = await request(baseUrl, 'GET', '/api/plugins/installed');
216
+ return (r.json.plugins || []).find((p) => p.id === id) || null;
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ async function cmdConfig(args, baseUrl) {
223
+ const id = args[0];
224
+ if (!id) {
225
+ console.error(chalk.red(
226
+ ' ✗ Missing plugin id. Usage: bizar plugin config <id> [key] [value]'));
227
+ process.exit(1);
228
+ }
229
+ const installed = await safeGetInstalled(id, baseUrl);
230
+ if (!installed) {
231
+ console.error(chalk.red(` ✗ Plugin "${id}" is not installed`));
232
+ process.exit(1);
233
+ }
234
+ if (args.length === 1) {
235
+ // No key — print full config.
236
+ console.log(JSON.stringify(installed.config || {}, null, 2));
237
+ return;
238
+ }
239
+ const key = args[1];
240
+ if (args.length === 2) {
241
+ // Just key — print that value.
242
+ const v = getDotPath(installed.config || {}, key);
243
+ console.log(JSON.stringify(v, null, 2));
244
+ return;
245
+ }
246
+ // key + value — replace full config with the new merged value.
247
+ // We merge to avoid wiping sibling keys the user didn't mention.
248
+ const newConfig = JSON.parse(JSON.stringify(installed.config || {}));
249
+ setDotPath(newConfig, key, args.slice(2).join(' '));
250
+ await request(baseUrl, 'PUT',
251
+ `/api/plugins/${encodeURIComponent(id)}`,
252
+ { action: 'config', config: newConfig });
253
+ console.log(chalk.green(` ✓ Set ${id}.${key}`));
254
+ }
255
+
256
+ function getDotPath(obj, path) {
257
+ const parts = path.split('.');
258
+ let cur = obj;
259
+ for (const p of parts) {
260
+ if (cur == null || typeof cur !== 'object') return undefined;
261
+ cur = cur[p];
262
+ }
263
+ return cur;
264
+ }
265
+
266
+ function setDotPath(obj, path, value) {
267
+ const parts = path.split('.');
268
+ let cur = obj;
269
+ for (let i = 0; i < parts.length - 1; i++) {
270
+ const p = parts[i];
271
+ if (cur[p] == null || typeof cur[p] !== 'object') cur[p] = {};
272
+ cur = cur[p];
273
+ }
274
+ cur[parts[parts.length - 1]] = value;
275
+ }
276
+
277
+ async function cmdUpdate(args, baseUrl) {
278
+ const id = args[0];
279
+ if (!id) {
280
+ console.error(chalk.red(' ✗ Missing plugin id. Usage: bizar plugin update <id>'));
281
+ process.exit(1);
282
+ }
283
+ const r = await request(baseUrl, 'PUT',
284
+ `/api/plugins/${encodeURIComponent(id)}`, { action: 'update' });
285
+ const note = r.json.from === r.json.to
286
+ ? chalk.dim(' (already at latest)')
287
+ : '';
288
+ console.log(chalk.green(` ✓ Updated "${id}" v${r.json.from} → v${r.json.to}`) + note);
289
+ }
290
+
291
+ async function cmdUninstall(args, baseUrl) {
292
+ const id = args[0];
293
+ if (!id) {
294
+ console.error(chalk.red(' ✗ Missing plugin id. Usage: bizar plugin uninstall <id>'));
295
+ process.exit(1);
296
+ }
297
+ await request(baseUrl, 'DELETE', `/api/plugins/${encodeURIComponent(id)}`);
298
+ console.log(chalk.green(` ✓ Uninstalled "${id}"`));
299
+ }
300
+
301
+ async function cmdInvoke(args, baseUrl) {
302
+ const [id, method, ...rest] = args;
303
+ if (!id || !method) {
304
+ console.error(chalk.red(
305
+ ' ✗ Usage: bizar plugin invoke <id> <method> [...args]'));
306
+ process.exit(1);
307
+ }
308
+ const r = await request(baseUrl, 'POST',
309
+ `/api/plugins/${encodeURIComponent(id)}/invoke`,
310
+ { method, args: rest });
311
+ if (r.json.ok) {
312
+ console.log(JSON.stringify(r.json.result, null, 2));
313
+ } else {
314
+ console.error(chalk.red(` ✗ ${r.json.error}`));
315
+ if (r.json.code) console.error(chalk.dim(` code: ${r.json.code}`));
316
+ process.exit(1);
317
+ }
318
+ }
319
+
320
+ // ── Dispatcher ─────────────────────────────────────────────────────────────
321
+
322
+ export async function runPluginCommand(pluginArgs) {
323
+ const sub = pluginArgs[0];
324
+ const rest = pluginArgs.slice(1);
325
+
326
+ if (!sub || sub === '--help' || sub === '-h') {
327
+ showPluginHelp();
328
+ return;
329
+ }
330
+
331
+ const baseUrl = await readDashboardUrl();
332
+ const handlers = {
333
+ search: cmdSearch,
334
+ install: cmdInstall,
335
+ list: cmdList,
336
+ info: cmdInfo,
337
+ config: cmdConfig,
338
+ update: cmdUpdate,
339
+ uninstall: cmdUninstall,
340
+ invoke: cmdInvoke,
341
+ };
342
+ const handler = handlers[sub];
343
+ if (!handler) {
344
+ console.error(chalk.red(` ✗ Unknown plugin subcommand: ${sub}`));
345
+ showPluginHelp();
346
+ process.exit(1);
347
+ }
348
+ try {
349
+ await handler(rest, baseUrl);
350
+ } catch (err) {
351
+ console.error(chalk.red(` ✗ ${err.message}`));
352
+ process.exit(1);
353
+ }
354
+ }
355
+
356
+ export async function run(name, args, isHelpRequest) {
357
+ await runPluginCommand(args);
358
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * cli/commands/voice.mjs
3
+ *
4
+ * v5.0.0 — Voice notes CLI — talks to the running dashboard's HTTP API.
5
+ */
6
+ import chalk from 'chalk';
7
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+
11
+ const HOME = homedir();
12
+
13
+ // ── Config dir helper ──────────────────────────────────────────────────────────
14
+
15
+ function getBizarConfigDir() {
16
+ if (process.platform === 'win32') {
17
+ return process.env.APPDATA
18
+ ? join(process.env.APPDATA, 'bizar')
19
+ : join(homedir(), '.config', 'bizar');
20
+ }
21
+ return process.env.XDG_CONFIG_HOME
22
+ ? join(process.env.XDG_CONFIG_HOME, 'bizar')
23
+ : join(homedir(), '.config', 'bizar');
24
+ }
25
+
26
+ function getDashboardPort() {
27
+ const portFile = join(getBizarConfigDir(), 'dashboard.port');
28
+ try {
29
+ const port = parseInt(readFileSync(portFile, 'utf8').trim(), 10);
30
+ if (Number.isFinite(port) && port > 0) return port;
31
+ } catch {
32
+ /* fall through */
33
+ }
34
+ return null;
35
+ }
36
+
37
+ // ── API helpers ───────────────────────────────────────────────────────────────
38
+
39
+ async function getJson(baseUrl, path) {
40
+ const res = await fetch(`${baseUrl}${path}`);
41
+ const text = await res.text();
42
+ let json = null;
43
+ try { json = text ? JSON.parse(text) : null; } catch { /* keep raw */ }
44
+ if (!res.ok) {
45
+ const msg = json?.message || json?.error || text || `HTTP ${res.status}`;
46
+ throw new Error(`${path} failed: ${msg}`);
47
+ }
48
+ return json;
49
+ }
50
+
51
+ async function delJson(baseUrl, path) {
52
+ const res = await fetch(`${baseUrl}${path}`, { method: 'DELETE' });
53
+ if (!res.ok && res.status !== 204) {
54
+ const text = await res.text();
55
+ let json = null;
56
+ try { json = text ? JSON.parse(text) : null; } catch { /* keep raw */ }
57
+ const msg = json?.message || json?.error || text || `HTTP ${res.status}`;
58
+ throw new Error(`${path} failed: ${msg}`);
59
+ }
60
+ return json;
61
+ }
62
+
63
+ // ── Help ───────────────────────────────────────────────────────────────────────
64
+
65
+ export function showVoiceHelp() {
66
+ console.log(`
67
+ bizar voice — Manage voice notes (via the dashboard's HTTP API)
68
+
69
+ Usage:
70
+ bizar voice list [vaultPath] List voice notes (optionally scoped to vault)
71
+ bizar voice delete <id> Delete a voice note by id
72
+ bizar voice configure --api-key <key> Set Whisper API key
73
+ bizar voice transcribe <audio-path> Transcribe an audio file (CLI-side)
74
+
75
+ Description:
76
+ Voice notes are recorded in the browser (MediaRecorder API) and uploaded
77
+ to the dashboard, which saves audio and transcribes via Whisper.
78
+
79
+ Subcommands call the running dashboard's HTTP API. If no dashboard is
80
+ reachable, you'll be told to run \`bizar dash start\` first.
81
+
82
+ Examples:
83
+ bizar voice list
84
+ bizar voice list ~/vault
85
+ bizar voice delete abc123def456
86
+ bizar voice configure --api-key sk-...
87
+ `);
88
+ }
89
+
90
+ // ── Config helpers ─────────────────────────────────────────────────────────────
91
+
92
+ const CONFIG_FILE = join(getBizarConfigDir(), 'voice-config.json');
93
+
94
+ function loadConfig() {
95
+ try {
96
+ if (!existsSync(CONFIG_FILE)) return {};
97
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
98
+ } catch {
99
+ return {};
100
+ }
101
+ }
102
+
103
+ function saveConfig(cfg) {
104
+ const dir = join(getBizarConfigDir());
105
+ if (!existsSync(dir)) {
106
+ require('node:fs').mkdirSync(dir, { recursive: true });
107
+ }
108
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), 'utf8');
109
+ }
110
+
111
+ // ── Command runner ────────────────────────────────────────────────────────────
112
+
113
+ export async function runVoiceCommand(args) {
114
+ const sub = args[0];
115
+ const positional = args.slice(1).filter((a) => !a.startsWith('-'));
116
+ const flags = args.slice(1).filter((a) => a.startsWith('-'));
117
+
118
+ if (!sub || sub === '--help' || sub === '-h' || flags.includes('--help') || flags.includes('-h')) {
119
+ showVoiceHelp();
120
+ return;
121
+ }
122
+
123
+ const port = getDashboardPort();
124
+ if (!port) {
125
+ console.error(chalk.red(' ✗ Dashboard is not running (no port file).'));
126
+ console.error(chalk.dim(' Start it first: `bizar dash start --bg`'));
127
+ process.exit(1);
128
+ }
129
+ const baseUrl = `http://127.0.0.1:${port}`;
130
+
131
+ try {
132
+ if (sub === 'list') {
133
+ const vaultPath = positional[0] || '';
134
+ const q = vaultPath ? `?vaultPath=${encodeURIComponent(vaultPath)}` : '';
135
+ const r = await getJson(baseUrl, `/api/voice/list${q}`);
136
+ const notes = r.notes || [];
137
+ if (notes.length === 0) {
138
+ console.log(chalk.dim(' (no voice notes)'));
139
+ return;
140
+ }
141
+ console.log(chalk.bold(` Voice Notes (${notes.length}):`));
142
+ for (const n of notes) {
143
+ const date = new Date(n.createdAtMs).toLocaleString();
144
+ const transcript = n.transcript
145
+ ? n.transcript.slice(0, 60) + (n.transcript.length > 60 ? '…' : '')
146
+ : '(no transcript)';
147
+ console.log(` ${n.id} ${date}`);
148
+ console.log(` ${chalk.dim(transcript)}`);
149
+ }
150
+ } else if (sub === 'delete') {
151
+ const id = positional[0];
152
+ if (!id) {
153
+ console.error(chalk.red(' ✗ Missing note id. Usage: bizar voice delete <id>'));
154
+ process.exit(1);
155
+ }
156
+ await delJson(baseUrl, `/api/voice/${id}`);
157
+ console.log(chalk.green(` ✓ Deleted note ${id}`));
158
+ } else if (sub === 'configure') {
159
+ const apiKeyIdx = flags.indexOf('--api-key');
160
+ const apiKey = apiKeyIdx !== -1 ? flags[apiKeyIdx + 1] : positional[0];
161
+ if (!apiKey) {
162
+ console.error(chalk.red(' ✗ Missing API key. Usage: bizar voice configure --api-key <key>'));
163
+ process.exit(1);
164
+ }
165
+ const cfg = loadConfig();
166
+ cfg.openaiApiKey = apiKey;
167
+ saveConfig(cfg);
168
+ console.log(chalk.green(' ✓ Whisper API key saved to voice config.'));
169
+ console.log(chalk.dim(' Set BIZAR_WHISPER_ENDPOINT env var to use a custom endpoint.'));
170
+ } else if (sub === 'transcribe') {
171
+ // CLI-side transcription: reads a local audio file and POSTs to the upload endpoint
172
+ const audioPath = positional[0];
173
+ if (!audioPath) {
174
+ console.error(chalk.red(' ✗ Missing audio path. Usage: bizar voice transcribe <audio-path>'));
175
+ process.exit(1);
176
+ }
177
+ if (!existsSync(audioPath)) {
178
+ console.error(chalk.red(` ✗ File not found: ${audioPath}`));
179
+ process.exit(1);
180
+ }
181
+ console.log(chalk.dim(` Transcribing ${audioPath}…`));
182
+ const { readFileSync: rf } = await import('node:fs');
183
+ const audioBuffer = rf(audioPath);
184
+ const form = new FormData();
185
+ form.append('audio', new Blob([audioBuffer], { type: 'audio/webm' }), 'audio.webm');
186
+ form.append('vaultPath', '');
187
+ const r = await fetch(`${baseUrl}/api/voice/upload`, { method: 'POST', body: form });
188
+ if (!r.ok) {
189
+ const text = await r.text();
190
+ throw new Error(`upload failed: ${text}`);
191
+ }
192
+ const data = await r.json();
193
+ console.log(chalk.green(` ✓ Transcribed and saved (id: ${data.id})`));
194
+ if (data.transcription) {
195
+ console.log(chalk.dim(' Transcript:'));
196
+ console.log(` ${data.transcription}`);
197
+ }
198
+ } else {
199
+ console.error(chalk.red(` ✗ Unknown voice subcommand: ${sub}`));
200
+ showVoiceHelp();
201
+ process.exit(1);
202
+ }
203
+ } catch (err) {
204
+ console.error(chalk.red(` ✗ ${err.message}`));
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ export async function run(name, args, isHelpRequest) {
210
+ await runVoiceCommand(args);
211
+ }