@polderlabs/bizar 4.5.1 → 4.7.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 (94) hide show
  1. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  2. package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
  3. package/bizar-dash/dist/assets/main-DGGq-iZI.js +361 -0
  4. package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +1 -0
  5. package/bizar-dash/dist/assets/{mobile-O6ANdD4W.js → mobile-CWqPoGaT.js} +18 -19
  6. package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js → mobile-i4Uv9eW8.js} +1 -2
  8. package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js.map → mobile-i4Uv9eW8.js.map} +1 -1
  9. package/bizar-dash/dist/index.html +3 -3
  10. package/bizar-dash/dist/mobile.html +2 -2
  11. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  12. package/bizar-dash/src/server/headroom.mjs +37 -35
  13. package/bizar-dash/src/server/logger.mjs +71 -0
  14. package/bizar-dash/src/server/memory-lightrag.mjs +84 -37
  15. package/bizar-dash/src/server/metrics.mjs +193 -0
  16. package/bizar-dash/src/server/mods-loader.mjs +13 -5
  17. package/bizar-dash/src/server/providers-store.mjs +73 -1
  18. package/bizar-dash/src/server/routes/chat.mjs +41 -0
  19. package/bizar-dash/src/server/routes/lightrag.mjs +7 -2
  20. package/bizar-dash/src/server/routes/memory.mjs +13 -4
  21. package/bizar-dash/src/server/routes/misc.mjs +2 -1
  22. package/bizar-dash/src/server/routes/overview.mjs +2 -1
  23. package/bizar-dash/src/server/schedules-runner.mjs +4 -4
  24. package/bizar-dash/src/server/server.mjs +101 -37
  25. package/bizar-dash/src/server/watcher.mjs +2 -2
  26. package/bizar-dash/src/web/App.tsx +40 -6
  27. package/bizar-dash/src/web/components/SearchModal.tsx +3 -2
  28. package/bizar-dash/src/web/components/Toast.tsx +1 -1
  29. package/bizar-dash/src/web/components/Topbar.tsx +1 -1
  30. package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
  31. package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
  32. package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
  33. package/bizar-dash/src/web/lib/api.ts +12 -11
  34. package/bizar-dash/src/web/lib/i18n.ts +25 -0
  35. package/bizar-dash/src/web/locales/en.json +52 -0
  36. package/bizar-dash/src/web/main.tsx +5 -0
  37. package/bizar-dash/src/web/styles/chat.css +2 -0
  38. package/bizar-dash/src/web/styles/main.css +52 -23
  39. package/bizar-dash/src/web/styles/tasks.css +2 -0
  40. package/bizar-dash/src/web/views/Activity.tsx +24 -17
  41. package/bizar-dash/src/web/views/History.tsx +90 -75
  42. package/bizar-dash/src/web/views/Memory.tsx +4 -3
  43. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
  44. package/bizar-dash/src/web/views/Overview.tsx +15 -10
  45. package/bizar-dash/src/web/views/Settings.tsx +9 -2
  46. package/bizar-dash/src/web/views/Skills.tsx +3 -2
  47. package/bizar-dash/src/web/views/Tasks.tsx +4 -3
  48. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +8 -6
  49. package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
  50. package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
  51. package/bizar-dash/tests/components/Button.test.tsx +41 -0
  52. package/bizar-dash/tests/components/Card.test.tsx +42 -0
  53. package/bizar-dash/tests/components/Modal.test.tsx +104 -0
  54. package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
  55. package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
  56. package/bizar-dash/tests/components/Toast.test.tsx +108 -0
  57. package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
  58. package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
  59. package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
  60. package/bizar-dash/tests/lib/i18n.test.ts +46 -0
  61. package/bizar-dash/tests/lib/utils.test.ts +194 -0
  62. package/bizar-dash/tests/logger.test.mjs +207 -0
  63. package/bizar-dash/tests/metrics.test.mjs +183 -0
  64. package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
  65. package/bizar-dash/tests/setup.ts +7 -0
  66. package/bizar-dash/vitest.config.ts +13 -0
  67. package/cli/artifact-cli.mjs +605 -0
  68. package/cli/artifact-render.mjs +621 -0
  69. package/cli/artifact-server.mjs +847 -0
  70. package/cli/artifact.mjs +38 -2089
  71. package/cli/bg.mjs +5 -13
  72. package/cli/bin.mjs +170 -1348
  73. package/cli/commands/artifact.mjs +20 -0
  74. package/cli/commands/dash.mjs +160 -0
  75. package/cli/commands/headroom.mjs +204 -0
  76. package/cli/commands/install.mjs +169 -0
  77. package/cli/commands/memory.mjs +25 -0
  78. package/cli/commands/minimax.mjs +265 -0
  79. package/cli/commands/mod.mjs +185 -0
  80. package/cli/commands/service.mjs +65 -0
  81. package/cli/commands/usage.mjs +109 -0
  82. package/cli/commands/util.mjs +306 -0
  83. package/cli/doctor.mjs +6 -16
  84. package/cli/memory.mjs +65 -21
  85. package/cli/provision.mjs +10 -15
  86. package/cli/service-controller.mjs +1 -11
  87. package/cli/service.mjs +1 -11
  88. package/cli/utils.mjs +41 -1
  89. package/install.sh +1 -2
  90. package/package.json +8 -3
  91. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +0 -1
  92. package/bizar-dash/dist/assets/main-usWhlPWa.js +0 -362
  93. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +0 -1
  94. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +0 -1
@@ -0,0 +1,265 @@
1
+ /**
2
+ * cli/commands/minimax.mjs
3
+ *
4
+ * MiniMax Token Plan integration CLI.
5
+ * v4.5.0+ — status, remains, test, config, clear, reset-onboarding.
6
+ */
7
+ import chalk from 'chalk';
8
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+
12
+ const HOME = homedir();
13
+ const BIZAR_HOME = join(HOME, '.config', 'bizar');
14
+
15
+ // ── Dashboard connection ────────────────────────────────────────────────────────
16
+
17
+ function readDashboardConn() {
18
+ const portFile = join(BIZAR_HOME, 'dashboard.port');
19
+ const authFile = join(BIZAR_HOME, 'dashboard-secret');
20
+ let port = 4321;
21
+ let secret = '';
22
+ try {
23
+ if (existsSync(portFile)) {
24
+ const parsed = parseInt(readFileSync(portFile, 'utf8').trim(), 10);
25
+ if (Number.isFinite(parsed) && parsed > 0) port = parsed;
26
+ }
27
+ } catch { /* ignore */ }
28
+ try {
29
+ if (existsSync(authFile)) secret = readFileSync(authFile, 'utf8').trim();
30
+ } catch { /* ignore */ }
31
+ return { port, secret };
32
+ }
33
+
34
+ async function minimaxApi(path, opts = {}) {
35
+ const { port, secret } = readDashboardConn();
36
+ const url = `http://127.0.0.1:${port}${path}`;
37
+ const headers = { 'content-type': 'application/json', accept: 'application/json' };
38
+ if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
39
+ const method = (opts.method || 'GET').toUpperCase();
40
+ try {
41
+ const resp = await fetch(url, { method, headers, body: opts.body ? JSON.stringify(opts.body) : undefined });
42
+ const text = await resp.text();
43
+ let data = null;
44
+ try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
45
+ if (!resp.ok) {
46
+ return { ok: false, error: `http_${resp.status}`, message: data?.message || data?.error || resp.statusText, status: resp.status };
47
+ }
48
+ return { ok: true, status: resp.status, data };
49
+ } catch (err) {
50
+ return { ok: false, error: 'network_error', message: err && err.message ? err.message : String(err) };
51
+ }
52
+ }
53
+
54
+ // ── Help ───────────────────────────────────────────────────────────────────────
55
+
56
+ export function showMinimaxHelp() {
57
+ console.log(`
58
+ bizar minimax — Manage the MiniMax Token Plan integration
59
+
60
+ Usage:
61
+ bizar minimax status Show whether the Subscription Key is configured
62
+ and where it was resolved from (auth.json,
63
+ opencode.json, env var, or none).
64
+ bizar minimax remains Fetch the live 5-hour + weekly remaining
65
+ quota per model. Shows reset times.
66
+ bizar minimax test Smoke-test the key with a one-shot chat
67
+ completion. Prints the usage block.
68
+ bizar minimax config <key> Save a new Subscription Key to
69
+ ~/.local/share/opencode/auth.json. The
70
+ key never leaves this machine.
71
+ bizar minimax clear Remove the Subscription Key from
72
+ opencode's auth.json.
73
+ bizar minimax reset-onboarding Re-trigger the first-run wizard. Use
74
+ this if the key was changed outside the
75
+ dashboard and you want to re-enter it.
76
+
77
+ Flags:
78
+ --base-url <url> Override the Token Plan host (default: https://www.minimax.io)
79
+ --chat-url <url> Override the chat-completions host (default: https://api.minimax.io/v1)
80
+ --yes Skip the confirmation prompt on 'clear' and 'reset-onboarding'
81
+ `);
82
+ }
83
+
84
+ // ── Subcommand handlers ─────────────────────────────────────────────────────────
85
+
86
+ async function runMinimaxCommand(minimaxArgs) {
87
+ const sub = minimaxArgs[0];
88
+ const flags = minimaxArgs.slice(1).filter((a) => a.startsWith('-'));
89
+ const positional = minimaxArgs.slice(1).filter((a) => !a.startsWith('-'));
90
+ const yes = flags.includes('--yes') || flags.includes('-y');
91
+
92
+ if (!sub || sub === '--help' || sub === '-h' || flags.includes('--help') || flags.includes('-h')) {
93
+ showMinimaxHelp();
94
+ return;
95
+ }
96
+
97
+ if (sub === 'status') {
98
+ const r = await minimaxApi('/api/minimax/status');
99
+ if (!r.ok) {
100
+ console.error(chalk.red(` ✗ ${r.message || r.error}`));
101
+ process.exit(1);
102
+ }
103
+ const s = r.data;
104
+ console.log('');
105
+ console.log(chalk.bold(' MiniMax Token Plan status'));
106
+ console.log('');
107
+ console.log(` ${chalk.dim('configured:')} ${s.configured ? chalk.green('yes') : chalk.red('no')}`);
108
+ console.log(` ${chalk.dim('key source:')} ${chalk.cyan(s.source)}`);
109
+ if (s.apiKeyHint) console.log(` ${chalk.dim('key hint:')} ${s.apiKeyHint}`);
110
+ console.log(` ${chalk.dim('group id:')} ${s.groupId}`);
111
+ console.log(` ${chalk.dim('token host:')} ${s.tokenBaseUrl}`);
112
+ console.log(` ${chalk.dim('chat host:')} ${s.chatBaseUrl}`);
113
+ console.log(` ${chalk.dim('key format:')} ${s.keyPatternValid === true ? chalk.green('ok') : s.keyPatternValid === false ? chalk.red('unexpected prefix') : chalk.dim('n/a')}`);
114
+ console.log('');
115
+ if (s.cache) {
116
+ console.log(` ${chalk.dim('cached:')} ${new Date(s.cache.fetchedAt).toLocaleString()} (${s.cache.modelCount} models)`);
117
+ } else {
118
+ console.log(` ${chalk.dim('cached:')} none yet — run \`bizar minimax remains\` to populate`);
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (sub === 'remains') {
124
+ const r = await minimaxApi('/api/minimax/remains');
125
+ if (!r.ok) {
126
+ console.error(chalk.red(` ✗ ${r.message || r.error}`));
127
+ process.exit(1);
128
+ }
129
+ if (!r.data?.ok) {
130
+ console.error(chalk.red(` ✗ ${r.data?.message || r.data?.error || 'unknown'}`));
131
+ process.exit(1);
132
+ }
133
+ const data = r.data;
134
+ console.log('');
135
+ console.log(chalk.bold(` MiniMax Token Plan quota (${new Date(data.fetchedAt).toLocaleString()})`));
136
+ if (data.apiKeyHint) console.log(chalk.dim(` Key: ${data.apiKeyHint} · source: ${data.keySource} · group: ${data.groupId}`));
137
+ console.log('');
138
+ for (const m of data.models || []) {
139
+ const five = m.current_interval_remaining_percent;
140
+ const week = m.current_weekly_remaining_percent;
141
+ const fiveBar = '█'.repeat(Math.round(five / 5)) + '░'.repeat(20 - Math.round(five / 5));
142
+ const weekBar = '█'.repeat(Math.round(week / 5)) + '░'.repeat(20 - Math.round(week / 5));
143
+ const fiveColor = five >= 75 ? chalk.green : five >= 25 ? chalk.yellow : chalk.red;
144
+ const weekColor = week >= 75 ? chalk.green : week >= 25 ? chalk.yellow : chalk.red;
145
+ console.log(` ${chalk.bold(m.model_name)}`);
146
+ console.log(` ${chalk.dim('5h:')} ${fiveColor(five + '%'.padStart(4))} ${fiveBar} resets in ${m.intervalResetInHuman}`);
147
+ console.log(` ${chalk.dim('week:')} ${weekColor(week + '%'.padStart(4))} ${weekBar} resets in ${m.weeklyResetInHuman}`);
148
+ console.log('');
149
+ }
150
+ return;
151
+ }
152
+
153
+ if (sub === 'test') {
154
+ const prompt = positional[0] || 'Reply with the single word: pong';
155
+ const r = await minimaxApi('/api/minimax/test', {
156
+ method: 'POST',
157
+ body: { prompt, model: 'MiniMax-M3', maxTokens: 32 },
158
+ });
159
+ if (!r.ok) {
160
+ console.error(chalk.red(` ✗ ${r.message || r.error}`));
161
+ process.exit(1);
162
+ }
163
+ const data = r.data;
164
+ if (!data?.ok) {
165
+ console.error(chalk.red(` ✗ ${data?.message || data?.error || 'unknown'}`));
166
+ process.exit(1);
167
+ }
168
+ console.log('');
169
+ console.log(chalk.green(' ✓ Key works'));
170
+ console.log(` ${chalk.dim('model:')} ${data.model}`);
171
+ console.log(` ${chalk.dim('finish:')} ${data.finishReason}`);
172
+ if (data.content) console.log(` ${chalk.dim('content:')} ${JSON.stringify(data.content.slice(0, 80))}${data.content.length > 80 ? '…' : ''}`);
173
+ if (data.usage) {
174
+ console.log(` ${chalk.dim('usage:')} total=${data.usage.total_tokens} prompt=${data.usage.prompt_tokens} completion=${data.usage.completion_tokens}`);
175
+ }
176
+ return;
177
+ }
178
+
179
+ if (sub === 'config' || sub === 'set') {
180
+ const key = positional[0];
181
+ if (!key) {
182
+ console.error(chalk.red(' ✗ Missing key. Usage: bizar minimax config <sk-cp-…>'));
183
+ process.exit(1);
184
+ }
185
+ if (!/^sk-(cp|ant|or)-[A-Za-z0-9_-]{20,}$/.test(key)) {
186
+ console.error(chalk.red(' ✗ Key does not look like a MiniMax key (expected sk-cp-…, sk-ant-…, or sk-or-… prefix)'));
187
+ process.exit(1);
188
+ }
189
+ console.log(chalk.dim(' Saving to opencode auth.json…'));
190
+ const r = await minimaxApi('/api/minimax/onboarding/save-key', {
191
+ method: 'POST',
192
+ body: { key, groupId: 'default' },
193
+ });
194
+ if (!r.ok) {
195
+ console.error(chalk.red(` ✗ ${r.message || r.error}`));
196
+ process.exit(1);
197
+ }
198
+ console.log(chalk.green(` ✓ Saved to ${r.data?.path}`));
199
+ console.log(` ${chalk.dim('key hint:')} ${r.data?.apiKeyHint}`);
200
+ return;
201
+ }
202
+
203
+ if (sub === 'clear' || sub === 'remove') {
204
+ if (!yes) {
205
+ console.log(chalk.yellow(` ⚠ This will remove the MiniMax Subscription Key from opencode's auth.json.`));
206
+ console.log(chalk.dim(' Continue? [y/N]'));
207
+ const buf = [];
208
+ process.stdin.setEncoding('utf8');
209
+ process.stdin.on('data', (c) => { buf.push(c); if (c.includes('\n')) process.stdin.pause(); });
210
+ await new Promise((resolve) => process.stdin.once('close', resolve));
211
+ const answer = buf.join('').trim().toLowerCase();
212
+ if (answer !== 'y' && answer !== 'yes') {
213
+ console.log(chalk.dim(' Cancelled.'));
214
+ return;
215
+ }
216
+ }
217
+ const authFile = join(HOME, '.local', 'share', 'opencode', 'auth.json');
218
+ let auth = {};
219
+ try {
220
+ if (existsSync(authFile)) auth = JSON.parse(readFileSync(authFile, 'utf8'));
221
+ } catch { /* ignore */ }
222
+ if (auth.minimax) {
223
+ delete auth.minimax;
224
+ writeFileSync(authFile, JSON.stringify(auth, null, 2) + '\n', 'utf8');
225
+ console.log(chalk.green(' ✓ MiniMax key removed from auth.json'));
226
+ } else {
227
+ console.log(chalk.dim(' No MiniMax key was configured.'));
228
+ }
229
+ return;
230
+ }
231
+
232
+ if (sub === 'reset-onboarding' || sub === 'reset') {
233
+ if (!yes) {
234
+ console.log(chalk.yellow(' ⚠ This will re-trigger the first-run MiniMax onboarding wizard.'));
235
+ console.log(chalk.dim(' Continue? [y/N]'));
236
+ const buf = [];
237
+ process.stdin.setEncoding('utf8');
238
+ process.stdin.on('data', (c) => { buf.push(c); if (c.includes('\n')) process.stdin.pause(); });
239
+ await new Promise((resolve) => process.stdin.once('close', resolve));
240
+ const answer = buf.join('').trim().toLowerCase();
241
+ if (answer !== 'y' && answer !== 'yes') {
242
+ console.log(chalk.dim(' Cancelled.'));
243
+ return;
244
+ }
245
+ }
246
+ const r = await minimaxApi('/api/minimax/onboarding', {
247
+ method: 'POST',
248
+ body: { dismissedAt: null },
249
+ });
250
+ if (!r.ok) {
251
+ console.error(chalk.red(` ✗ ${r.message || r.error}`));
252
+ process.exit(1);
253
+ }
254
+ console.log(chalk.green(' ✓ Onboarding wizard will show on next dashboard load'));
255
+ return;
256
+ }
257
+
258
+ console.error(chalk.red(` ✗ Unknown minimax subcommand: ${sub}`));
259
+ showMinimaxHelp();
260
+ process.exit(1);
261
+ }
262
+
263
+ export async function run(name, args, isHelpRequest) {
264
+ await runMinimaxCommand(args);
265
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * cli/commands/mod.mjs
3
+ *
4
+ * Mod manager CLI — talks to the running dashboard's HTTP API.
5
+ * v3.20.5+ — install, upgrade, list, registry.
6
+ */
7
+ import chalk from 'chalk';
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+
12
+ const HOME = homedir();
13
+ const BIZAR_HOME = join(HOME, '.config', 'bizar');
14
+
15
+ // ── Config dir helper ──────────────────────────────────────────────────────────
16
+
17
+ function getBizarConfigDir() {
18
+ if (process.platform === 'win32') {
19
+ return process.env.APPDATA
20
+ ? join(process.env.APPDATA, 'bizar')
21
+ : join(homedir(), '.config', 'bizar');
22
+ }
23
+ return process.env.XDG_CONFIG_HOME
24
+ ? join(process.env.XDG_CONFIG_HOME, 'bizar')
25
+ : join(homedir(), '.config', 'bizar');
26
+ }
27
+
28
+ // ── Help ───────────────────────────────────────────────────────────────────────
29
+
30
+ export function showModHelp() {
31
+ console.log(`
32
+ bizar mod — Manage mods (via the dashboard's HTTP API)
33
+
34
+ Usage:
35
+ bizar mod upgrade <id> [--backup] Upgrade an installed mod to the latest version
36
+ bizar mod install <id> Install a mod from the registry
37
+ bizar mod list List installed mods
38
+ bizar mod registry Show registry URL + available mods
39
+
40
+ Description:
41
+ Subcommands call the running dashboard's HTTP API. If no dashboard is
42
+ reachable, you'll be told to run \`bizar dash start\` first.
43
+
44
+ \`bizar mod upgrade <id>\` will:
45
+ 1. Snapshot the existing version of the mod
46
+ 2. Optionally back up the folder (--backup)
47
+ 3. Remove the existing copy (and its opencode-config instructions)
48
+ 4. Install the latest version from the registry
49
+ 5. Re-install the new mod's instruction files into opencode config
50
+ 6. Print from-version → to-version
51
+ `);
52
+ }
53
+
54
+ // ── API helpers ────────────────────────────────────────────────────────────────
55
+
56
+ async function postJson(baseUrl, path, body) {
57
+ const res = await fetch(`${baseUrl}${path}`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify(body || {}),
61
+ });
62
+ const text = await res.text();
63
+ let json = null;
64
+ try { json = text ? JSON.parse(text) : null; } catch { /* keep raw */ }
65
+ if (!res.ok) {
66
+ const msg = json?.message || json?.error || text || `HTTP ${res.status}`;
67
+ throw new Error(`${path} failed: ${msg}`);
68
+ }
69
+ return json;
70
+ }
71
+
72
+ async function getJson(baseUrl, path) {
73
+ const res = await fetch(`${baseUrl}${path}`);
74
+ const text = await res.text();
75
+ let json = null;
76
+ try { json = text ? JSON.parse(text) : null; } catch { /* keep raw */ }
77
+ if (!res.ok) {
78
+ const msg = json?.message || json?.error || text || `HTTP ${res.status}`;
79
+ throw new Error(`${path} failed: ${msg}`);
80
+ }
81
+ return json;
82
+ }
83
+
84
+ // ── Command runner ────────────────────────────────────────────────────────────
85
+
86
+ export async function runModCommand(modArgs) {
87
+ const sub = modArgs[0];
88
+ const positional = modArgs.slice(1).filter((a) => !a.startsWith('-'));
89
+ const flags = modArgs.slice(1).filter((a) => a.startsWith('-'));
90
+
91
+ if (!sub || sub === '--help' || sub === '-h' || flags.includes('--help') || flags.includes('-h')) {
92
+ showModHelp();
93
+ return;
94
+ }
95
+
96
+ // Read dashboard port from the port file the dashboard writes on start.
97
+ const portFile = join(getBizarConfigDir(), 'dashboard.port');
98
+ let port = null;
99
+ try {
100
+ port = parseInt(readFileSync(portFile, 'utf8').trim(), 10);
101
+ if (!Number.isFinite(port) || port <= 0) port = null;
102
+ } catch {
103
+ port = null;
104
+ }
105
+ if (!port) {
106
+ console.error(chalk.red(' ✗ Dashboard is not running (no port file at ' + portFile + ').'));
107
+ console.error(chalk.dim(' Start it first: `bizar dash start --bg`'));
108
+ process.exit(1);
109
+ }
110
+ const baseUrl = `http://127.0.0.1:${port}`;
111
+
112
+ if (sub === 'install') {
113
+ const id = positional[0];
114
+ if (!id) {
115
+ console.error(chalk.red(' ✗ Missing mod id. Usage: bizar mod install <id>'));
116
+ process.exit(1);
117
+ }
118
+ try {
119
+ const m = await postJson(baseUrl, '/api/mods', { id });
120
+ console.log(chalk.green(` ✓ Installed "${m.id}" v${m.version}`));
121
+ } catch (err) {
122
+ console.error(chalk.red(` ✗ ${err.message}`));
123
+ process.exit(1);
124
+ }
125
+ } else if (sub === 'upgrade') {
126
+ const id = positional[0];
127
+ if (!id) {
128
+ console.error(chalk.red(' ✗ Missing mod id. Usage: bizar mod upgrade <id> [--backup]'));
129
+ process.exit(1);
130
+ }
131
+ const backup = flags.includes('--backup') || flags.includes('-b');
132
+ try {
133
+ const r = await postJson(baseUrl, `/api/mods/${encodeURIComponent(id)}/upgrade`, { backup });
134
+ const note = r.backupPath ? chalk.dim(` (backup: ${r.backupPath})`) : '';
135
+ console.log(chalk.green(` ✓ Upgraded "${id}" v${r.from} → v${r.to}`) + note);
136
+ } catch (err) {
137
+ console.error(chalk.red(` ✗ ${err.message}`));
138
+ process.exit(1);
139
+ }
140
+ } else if (sub === 'list') {
141
+ try {
142
+ const r = await getJson(baseUrl, '/api/mods');
143
+ const mods = r.mods || [];
144
+ if (mods.length === 0) {
145
+ console.log(chalk.dim(' (no mods installed)'));
146
+ return;
147
+ }
148
+ for (const m of mods) {
149
+ const state = m.enabled ? chalk.green('enabled ') : chalk.yellow('disabled');
150
+ console.log(` ${m.id.padEnd(20)} v${m.version.padEnd(10)} ${state} ${m.name || ''}`);
151
+ }
152
+ } catch (err) {
153
+ console.error(chalk.red(` ✗ ${err.message}`));
154
+ process.exit(1);
155
+ }
156
+ } else if (sub === 'registry') {
157
+ try {
158
+ const r = await getJson(baseUrl, '/api/mods/registry');
159
+ console.log(chalk.dim(` Source: ${r.registry?.source || '(unknown)'}`));
160
+ console.log(chalk.dim(` Updated: ${r.registry?.updatedAt || '(unknown)'}`));
161
+ console.log('');
162
+ const mods = r.mods || [];
163
+ if (mods.length === 0) {
164
+ console.log(chalk.dim(' (no mods in registry)'));
165
+ return;
166
+ }
167
+ for (const m of mods) {
168
+ const installed = m.installed ? chalk.green(`installed v${m.installedVersion || '?'}`) : chalk.dim('not installed');
169
+ const upgrade = m.upgradeAvailable ? chalk.yellow(` ↑ v${m.upgradeAvailable} available`) : '';
170
+ console.log(` ${m.id.padEnd(20)} v${(m.latest || '?').padEnd(10)} ${installed}${upgrade} ${m.name || ''}`);
171
+ }
172
+ } catch (err) {
173
+ console.error(chalk.red(` ✗ ${err.message}`));
174
+ process.exit(1);
175
+ }
176
+ } else {
177
+ console.error(chalk.red(` ✗ Unknown mod subcommand: ${sub}`));
178
+ showModHelp();
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ export async function run(name, args, isHelpRequest) {
184
+ await runModCommand(args);
185
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * cli/commands/service.mjs
3
+ *
4
+ * Service command dispatcher — delegates to ../service.mjs.
5
+ * The service subcommand help text needs bizarConfigDir(), which we inline here
6
+ * (service.mjs owns its own bizarConfigDir for internal use).
7
+ */
8
+ import chalk from 'chalk';
9
+ import { homedir } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ function getBizarConfigDir() {
13
+ if (process.platform === 'win32') {
14
+ return process.env.APPDATA
15
+ ? join(process.env.APPDATA, 'bizar')
16
+ : join(homedir(), '.config', 'bizar');
17
+ }
18
+ return process.env.XDG_CONFIG_HOME
19
+ ? join(process.env.XDG_CONFIG_HOME, 'bizar')
20
+ : join(homedir(), '.config', 'bizar');
21
+ }
22
+
23
+ export function showServiceHelp() {
24
+ const bizarConfigDir = getBizarConfigDir();
25
+ console.log(`
26
+ bizar service — Manage the background service daemon
27
+
28
+ Usage:
29
+ bizar service start Start the service in background
30
+ bizar service stop Stop the running service
31
+ bizar service status Show whether the service is running
32
+ bizar service logs Tail the service log
33
+ bizar service follow Follow the service log until Ctrl-C
34
+ bizar service install Register with systemd / launchd / scheduled task
35
+ bizar service install --force Re-install even when the unit matches
36
+ bizar service uninstall Remove the OS-level autostart
37
+ bizar service uninstall --force Force-uninstall even when nothing is registered
38
+
39
+ Description:
40
+ The service watches per-project schedules (cron / interval / once)
41
+ and runs them at the right time. It logs to
42
+ ${bizarConfigDir}/service.log and writes its PID to
43
+ ${bizarConfigDir}/service.pid.
44
+
45
+ install registers the daemon under the OS init system — systemd user
46
+ unit on Linux, launchd LaunchAgent on macOS, scheduled task
47
+ ("BizarDashboardService", ONSTART, HIGHEST) on Windows. After
48
+ install, a normal user does not need to run \`bizar service start\`
49
+ for the dashboard background process — the OS does it at login.
50
+ `);
51
+ }
52
+
53
+ export async function runServiceCommand(sub) {
54
+ const { runService } = await import('../service.mjs');
55
+ await runService(sub || 'status', process.argv.slice(2));
56
+ }
57
+
58
+ export async function run(name, args, isHelpRequest) {
59
+ if (isHelpRequest) {
60
+ showServiceHelp();
61
+ return;
62
+ }
63
+ const { runService } = await import('../service.mjs');
64
+ await runService(args[0] || 'status', args.slice(1));
65
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * cli/commands/usage.mjs
3
+ *
4
+ * Usage analytics CLI — fetches from the running dashboard.
5
+ * v4.6.0+ — compact usage summary from the JSONL store.
6
+ */
7
+ import chalk from 'chalk';
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+
12
+ const HOME = homedir();
13
+ const BIZAR_HOME = join(HOME, '.config', 'bizar');
14
+
15
+ // ── Dashboard connection ────────────────────────────────────────────────────────
16
+
17
+ function readDashboardConn() {
18
+ const portFile = join(BIZAR_HOME, 'dashboard.port');
19
+ const authFile = join(BIZAR_HOME, 'dashboard-secret');
20
+ let port = 4321;
21
+ let secret = '';
22
+ try {
23
+ if (existsSync(portFile)) {
24
+ const parsed = parseInt(readFileSync(portFile, 'utf8').trim(), 10);
25
+ if (Number.isFinite(parsed) && parsed > 0) port = parsed;
26
+ }
27
+ } catch { /* ignore */ }
28
+ try {
29
+ if (existsSync(authFile)) secret = readFileSync(authFile, 'utf8').trim();
30
+ } catch { /* ignore */ }
31
+ return { port, secret };
32
+ }
33
+
34
+ // ── Help ───────────────────────────────────────────────────────────────────────
35
+
36
+ export function showUsageHelp() {
37
+ console.log(`
38
+ bizar usage — Show compact usage analytics summary
39
+
40
+ Usage:
41
+ bizar usage [24h|7d|30d] Show summary for the given range (default: 24h)
42
+
43
+ Examples:
44
+ bizar usage
45
+ bizar usage 7d
46
+ bizar usage 30d
47
+ `);
48
+ }
49
+
50
+ // ── Command runner ─────────────────────────────────────────────────────────────
51
+
52
+ async function runUsageCommand(args, wantJson = false) {
53
+ const range = (args[0] && ['24h', '7d', '30d'].includes(args[0])) ? args[0] : '24h';
54
+ const { port, secret } = readDashboardConn();
55
+ const url = `http://127.0.0.1:${port}/api/usage?range=${range}`;
56
+ const headers = { accept: 'application/json' };
57
+ if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
58
+ try {
59
+ const resp = await fetch(url, { method: 'GET', headers });
60
+ const text = await resp.text();
61
+ let data = null;
62
+ try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
63
+ if (!resp.ok || !data) {
64
+ console.error(chalk.red(` ✗ Failed to load usage data: ${data?.message ?? resp.statusText}`));
65
+ process.exit(1);
66
+ }
67
+ if (wantJson) {
68
+ process.stdout.write(JSON.stringify(data) + '\n');
69
+ return;
70
+ }
71
+ const t = data.totals;
72
+ console.log('');
73
+ console.log(chalk.bold(` Usage summary — ${range} (from JSONL store)`));
74
+ console.log('');
75
+ console.log(` ${chalk.dim('Requests:')} ${t.requests.toLocaleString()} (${t.errors} errors)`);
76
+ console.log(` ${chalk.dim('Tokens:')} ${t.totalTokens.toLocaleString()} total (${t.promptTokens.toLocaleString()} prompt · ${t.completionTokens.toLocaleString()} completion)`);
77
+ console.log(` ${chalk.dim('Cached:')} ${t.cachedTokens.toLocaleString()} tokens`);
78
+ console.log(` ${chalk.dim('Reasoning:')} ${t.reasoningTokens.toLocaleString()} tokens`);
79
+ console.log(` ${chalk.dim('Avg latency:')} ${t.avgLatencyMs}ms (p95: ${t.p95LatencyMs}ms)`);
80
+ if (t.costEstimate > 0) {
81
+ console.log(` ${chalk.dim('Est. cost:')} $${t.costEstimate.toFixed(4)} USD`);
82
+ }
83
+ console.log('');
84
+ if (data.daily && data.daily.length > 0) {
85
+ console.log(chalk.dim(` ${chalk.bold('Daily breakdown')}`));
86
+ for (const day of data.daily.slice(-7)) {
87
+ const barLen = Math.round((day.totalTokens / Math.max(...data.daily.map(d => d.totalTokens))) * 20);
88
+ const bar = '█'.repeat(barLen) + '░'.repeat(20 - barLen);
89
+ console.log(` ${day.date} ${bar} ${day.totalTokens.toLocaleString()} tok ${day.requests} req`);
90
+ }
91
+ }
92
+ if (data.perModel && data.perModel.length > 0) {
93
+ console.log('');
94
+ console.log(chalk.dim(` ${chalk.bold('Per model')}`));
95
+ for (const m of data.perModel.slice(0, 8)) {
96
+ console.log(` ${m.modelId.padEnd(24)} ${String(m.requests).padStart(6)} req ${String(m.totalTokens).padStart(8)} tok`);
97
+ }
98
+ }
99
+ console.log('');
100
+ } catch (err) {
101
+ console.error(chalk.red(` ✗ Network error: ${err && err.message ? err.message : String(err)}`));
102
+ console.error(chalk.dim(' Is the dashboard running? Run `bizar dash start` first.'));
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ export async function run(name, args, isHelpRequest) {
108
+ await runUsageCommand(args, false);
109
+ }