@polderlabs/bizar 3.15.1 → 3.16.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/cli/bin.mjs CHANGED
@@ -535,6 +535,10 @@ async function main() {
535
535
  // v3.11.1 — Background agent manager (list / view / kill / logs).
536
536
  const { runBg } = await import('./bg.mjs');
537
537
  await runBg(args[1], args.slice(2));
538
+ } else if (args[0] === 'providers' && args[1] === 'detect') {
539
+ // v3.16.0 — Auto-detect provider API keys from env + opencode.json.
540
+ const { runProvidersDetect } = await import('./providers-detect.mjs');
541
+ await runProvidersDetect(args.slice(2));
538
542
  } else if (args[0] === 'dash' || args[0] === 'dashboard') {
539
543
  // `bizar dashboard` is a deprecated alias for `bizar dash`
540
544
  if (args[0] === 'dashboard') {
package/cli/install.mjs CHANGED
@@ -406,6 +406,14 @@ export async function runInstaller() {
406
406
  ));
407
407
  console.log();
408
408
 
409
+ // ── Provider auto-detect (v3.16.0) — best-effort, doesn't block install
410
+ const { runProvidersDetect } = await import('./providers-detect.mjs');
411
+ try {
412
+ await runProvidersDetect(['--no-probe']);
413
+ } catch {
414
+ // best-effort
415
+ }
416
+
409
417
  // ── Restart prompt ──
410
418
  const shouldRestart = await promptRestartOpenCode();
411
419
  if (shouldRestart) {
@@ -0,0 +1,233 @@
1
+ /**
2
+ * cli/providers-detect.mjs
3
+ *
4
+ * v3.16.0 — Auto-detect providers from environment variables and
5
+ * opencode.json. Surfaces which providers have working API keys,
6
+ * which have keys in unexpected formats, and which need configuration.
7
+ *
8
+ * Usage:
9
+ * bizar providers detect — probe + print
10
+ * bizar providers detect --no-probe — skip the /models probe
11
+ * bizar providers detect --json — machine-readable
12
+ * bizar providers detect --install <id> — auto-add to opencode.json
13
+ *
14
+ * Recognised env vars: ANTHROPIC_API_KEY, OPENAI_API_KEY,
15
+ * GEMINI_API_KEY, GOOGLE_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY,
16
+ * COHERE_API_KEY, OPENROUTER_API_KEY, DEEPSEEK_API_KEY,
17
+ * MINIMAX_API_KEY.
18
+ */
19
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
+ import { homedir } from 'node:os';
22
+ import chalk from 'chalk';
23
+
24
+ const HOME = homedir();
25
+ const OPENCODE_JSON = join(HOME, '.config', 'opencode', 'opencode.json');
26
+
27
+ const KNOWN_PROVIDERS = [
28
+ { id: 'anthropic', name: 'Anthropic', envKeys: ['ANTHROPIC_API_KEY'], baseURL: 'https://api.anthropic.com/v1', keyPattern: /^sk-ant-[A-Za-z0-9_-]{20,}$/ },
29
+ { id: 'openai', name: 'OpenAI', envKeys: ['OPENAI_API_KEY'], baseURL: 'https://api.openai.com/v1', keyPattern: /^sk-[A-Za-z0-9]{20,}$/ },
30
+ { id: 'google', name: 'Google AI', envKeys: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'], baseURL: 'https://generativelanguage.googleapis.com/v1beta', keyPattern: /^AIza[A-Za-z0-9_-]{30,}$/ },
31
+ { id: 'mistral', name: 'Mistral', envKeys: ['MISTRAL_API_KEY'], baseURL: 'https://api.mistral.ai/v1', keyPattern: /^[A-Za-z0-9]{20,}$/ },
32
+ { id: 'groq', name: 'Groq', envKeys: ['GROQ_API_KEY'], baseURL: 'https://api.groq.com/openai/v1', keyPattern: /^gsk_[A-Za-z0-9]{20,}$/ },
33
+ { id: 'cohere', name: 'Cohere', envKeys: ['COHERE_API_KEY'], baseURL: 'https://api.cohere.com/v1', keyPattern: /^[A-Za-z0-9]{20,}$/ },
34
+ { id: 'openrouter', name: 'OpenRouter', envKeys: ['OPENROUTER_API_KEY'], baseURL: 'https://openrouter.ai/api/v1', keyPattern: /^sk-or-[A-Za-z0-9_-]{20,}$/ },
35
+ { id: 'deepseek', name: 'DeepSeek', envKeys: ['DEEPSEEK_API_KEY'], baseURL: 'https://api.deepseek.com/v1', keyPattern: /^sk-[A-Za-z0-9]{20,}$/ },
36
+ { id: 'minimax', name: 'MiniMax', envKeys: ['MINIMAX_API_KEY', 'ANTHROPIC_API_KEY'], baseURL: 'https://api.minimax.chat/v1', keyPattern: /^[A-Za-z0-9]{20,}$/ },
37
+ ];
38
+
39
+ function loadOpencodeConfig() {
40
+ try {
41
+ if (!existsSync(OPENCODE_JSON)) return {};
42
+ return JSON.parse(readFileSync(OPENCODE_JSON, 'utf8')) || {};
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
47
+
48
+ function saveOpencodeConfig(cfg) {
49
+ mkdirSync(dirname(OPENCODE_JSON), { recursive: true });
50
+ const tmp = `${OPENCODE_JSON}.tmp.${process.pid}`;
51
+ writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
52
+ renameSync(tmp, OPENCODE_JSON);
53
+ }
54
+
55
+ async function probe(spec, apiKey) {
56
+ const ctrl = new AbortController();
57
+ const timer = setTimeout(() => ctrl.abort(), 1500);
58
+ try {
59
+ const resp = await fetch(`${spec.baseURL}/models`, {
60
+ headers: { Authorization: `Bearer ${apiKey}` },
61
+ signal: ctrl.signal,
62
+ });
63
+ if (resp.ok) {
64
+ let modelCount = 0;
65
+ try {
66
+ const body = await resp.json();
67
+ if (Array.isArray(body?.data)) modelCount = body.data.length;
68
+ else if (Array.isArray(body)) modelCount = body.length;
69
+ } catch { /* ignore */ }
70
+ return { ok: true, status: resp.status, modelCount };
71
+ }
72
+ return { ok: false, status: resp.status, reason: `HTTP ${resp.status}` };
73
+ } catch (err) {
74
+ return { ok: false, reason: err?.name === 'AbortError' ? 'timeout' : 'network' };
75
+ } finally {
76
+ clearTimeout(timer);
77
+ }
78
+ }
79
+
80
+ async function detect({ probe: doProbe }) {
81
+ const cfg = loadOpencodeConfig();
82
+ const out = [];
83
+ for (const spec of KNOWN_PROVIDERS) {
84
+ let apiKey = '';
85
+ let keySource = '';
86
+ const cfgProvider = cfg.provider?.[spec.id];
87
+ if (cfgProvider?.apiKey) {
88
+ apiKey = cfgProvider.apiKey;
89
+ keySource = 'config';
90
+ } else if (cfgProvider?.options?.apiKey) {
91
+ apiKey = cfgProvider.options.apiKey;
92
+ keySource = 'config';
93
+ }
94
+ for (const k of spec.envKeys) {
95
+ const v = process.env[k];
96
+ if (typeof v === 'string' && v.length > 0) {
97
+ apiKey = v;
98
+ keySource = 'env';
99
+ break;
100
+ }
101
+ }
102
+ let status;
103
+ if (!apiKey) {
104
+ status = 'no-key';
105
+ } else if (spec.keyPattern && !spec.keyPattern.test(apiKey)) {
106
+ status = 'unknown';
107
+ } else {
108
+ status = 'configured';
109
+ }
110
+ let probed = null;
111
+ if (doProbe && status === 'configured') {
112
+ probed = await probe(spec, apiKey);
113
+ if (!probed.ok && probed.status) status = 'unknown';
114
+ }
115
+ out.push({
116
+ id: spec.id,
117
+ name: spec.name,
118
+ baseURL: spec.baseURL,
119
+ status,
120
+ keySource,
121
+ hasKey: !!apiKey,
122
+ probed,
123
+ });
124
+ }
125
+ return out;
126
+ }
127
+
128
+ function printTable(results) {
129
+ const pad = (s, n) => String(s).padEnd(n);
130
+ const idW = Math.max(2, ...results.map((r) => r.id.length));
131
+ const nameW = Math.max(4, ...results.map((r) => r.name.length));
132
+ const statusW = Math.max(6, ...results.map((r) => r.status.length));
133
+ const sourceW = Math.max(6, ...results.map((r) => r.keySource.length || 1));
134
+ console.log();
135
+ console.log(chalk.bold(` ${pad('ID', idW)} ${pad('NAME', nameW)} ${pad('STATUS', statusW)} ${pad('SOURCE', sourceW)} PROBE`));
136
+ console.log(chalk.dim(` ${'-'.repeat(idW)} ${'-'.repeat(nameW)} ${'-'.repeat(statusW)} ${'-'.repeat(sourceW)} ${'-'.repeat(20)}`));
137
+ for (const r of results) {
138
+ const id = pad(r.id, idW);
139
+ const name = pad(r.name, nameW);
140
+ const status = pad(r.status, statusW);
141
+ const source = pad(r.keySource || '-', sourceW);
142
+ const statusColored =
143
+ r.status === 'configured' ? chalk.green(status) :
144
+ r.status === 'unknown' ? chalk.yellow(status) :
145
+ chalk.dim(status);
146
+ const probeTxt = r.probed
147
+ ? (r.probed.modelCount != null ? chalk.green(`${r.probed.modelCount} models`) : chalk.dim(r.probed.reason || 'ok'))
148
+ : chalk.dim('-');
149
+ console.log(` ${id} ${name} ${statusColored} ${pad(source, sourceW)} ${probeTxt}`);
150
+ }
151
+ console.log();
152
+ const configured = results.filter((r) => r.status === 'configured').length;
153
+ const unknown = results.filter((r) => r.status === 'unknown').length;
154
+ const noKey = results.filter((r) => r.status === 'no-key').length;
155
+ console.log(chalk.bold(` ${configured} configured, ${unknown} unknown, ${noKey} no-key`));
156
+ console.log();
157
+ }
158
+
159
+ function installProvider(id) {
160
+ const spec = KNOWN_PROVIDERS.find((s) => s.id === id);
161
+ if (!spec) {
162
+ console.error(chalk.red(` ✗ unknown provider: ${id}`));
163
+ console.error(chalk.dim(` Available: ${KNOWN_PROVIDERS.map((s) => s.id).join(', ')}`));
164
+ process.exit(1);
165
+ }
166
+ // Get the apiKey from env or config
167
+ let apiKey = '';
168
+ for (const k of spec.envKeys) {
169
+ const v = process.env[k];
170
+ if (typeof v === 'string' && v.length > 0) {
171
+ apiKey = v;
172
+ break;
173
+ }
174
+ }
175
+ if (!apiKey) {
176
+ console.error(chalk.red(` ✗ no API key found in env (${spec.envKeys.join(', ')})`));
177
+ process.exit(1);
178
+ }
179
+ const cfg = loadOpencodeConfig();
180
+ cfg.provider = cfg.provider || {};
181
+ cfg.provider[spec.id] = {
182
+ name: spec.name,
183
+ baseURL: spec.baseURL,
184
+ apiKey,
185
+ enabled: true,
186
+ };
187
+ saveOpencodeConfig(cfg);
188
+ console.log(chalk.green(` ✓ ${spec.name} (${spec.id}) added to opencode.json`));
189
+ }
190
+
191
+ function showHelp() {
192
+ console.log(`
193
+ ${chalk.bold('bizar providers detect')} — auto-detect provider API keys
194
+
195
+ Usage:
196
+ bizar providers detect Probe env + opencode.json, print table
197
+ bizar providers detect --no-probe Skip the /models probe
198
+ bizar providers detect --json Print JSON instead of a table
199
+ bizar providers detect --install <id> Add a configured provider to opencode.json
200
+
201
+ Recognised env vars:
202
+ ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY,
203
+ MISTRAL_API_KEY, GROQ_API_KEY, COHERE_API_KEY, OPENROUTER_API_KEY,
204
+ DEEPSEEK_API_KEY, MINIMAX_API_KEY
205
+ `);
206
+ }
207
+
208
+ export async function runProvidersDetect(args) {
209
+ const doProbe = !args.includes('--no-probe');
210
+ const json = args.includes('--json');
211
+ const help = args.includes('--help') || args.includes('-h');
212
+
213
+ if (help) {
214
+ showHelp();
215
+ return;
216
+ }
217
+
218
+ const installIdx = args.indexOf('--install');
219
+ const installId = installIdx >= 0 ? args[installIdx + 1] : null;
220
+ if (installId) {
221
+ installProvider(installId);
222
+ return;
223
+ }
224
+
225
+ const results = await detect({ probe: doProbe });
226
+ if (json) {
227
+ console.log(JSON.stringify(results, null, 2));
228
+ return;
229
+ }
230
+ console.log(chalk.bold.cyan('\n Provider auto-detect\n'));
231
+ console.log(chalk.dim(` Probing ${KNOWN_PROVIDERS.length} providers...`));
232
+ printTable(results);
233
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "3.15.1",
3
+ "version": "3.16.0",
4
4
  "description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness.",
5
5
  "type": "module",
6
6
  "bin": {