@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 +4 -0
- package/cli/install.mjs +8 -0
- package/cli/providers-detect.mjs +233 -0
- package/package.json +1 -1
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.
|
|
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": {
|