@polderlabs/bizar 6.2.1 → 6.2.3
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 +69 -0
- package/cli/commands/cline-cmd.mjs +289 -0
- package/cli/commands/rca.mjs +198 -0
- package/cli/commands/rca.test.mjs +99 -0
- package/cli/commands/setup-provider.mjs +466 -0
- package/cli/commands/setup-provider.test.mjs +311 -0
- package/cli/commands/validate.mjs +81 -13
- package/cli/commands/validate.test.mjs +36 -10
- package/cli/provision.mjs +52 -77
- package/config/agents/baldr.md +1 -1
- package/config/agents/forseti.md +1 -1
- package/config/agents/frigg.md +1 -1
- package/config/agents/heimdall.md +1 -1
- package/config/agents/hermod.md +1 -1
- package/config/agents/mimir.md +1 -1
- package/config/agents/odin.md +1 -1
- package/config/agents/quick.md +1 -1
- package/config/agents/semble-search.md +1 -1
- package/config/agents/thor.md +1 -1
- package/config/agents/tyr.md +1 -1
- package/config/agents/vidarr.md +1 -1
- package/config/agents/vor.md +1 -1
- package/config/cline.json.template +20 -93
- package/config/commands/setup-provider.md +95 -0
- package/package.json +2 -2
- package/plugins/bizar/package.json +17 -6
- package/plugins/bizar/src/clineruntime.ts +1 -1
- package/plugins/bizar/tests/clineruntime-config.test.ts +24 -3
- package/scripts/bh-full-e2e.mjs +64 -9
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/setup-provider.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.2.3 — `bizar setup-provider` subcommand.
|
|
5
|
+
*
|
|
6
|
+
* v6.2.2 wrote to `~/.cline/cline.json` (the harness config). That was
|
|
7
|
+
* the wrong place — the Cline CLI and kanban mode read from
|
|
8
|
+
* `~/.cline/data/settings/providers.json` (the real Cline settings
|
|
9
|
+
* file). v6.2.3 fixes this.
|
|
10
|
+
*
|
|
11
|
+
* The default providerId is `litellm` because:
|
|
12
|
+
* - It's a real entry in Cline's built-in provider catalog (works
|
|
13
|
+
* in both Cline CLI AND kanban mode)
|
|
14
|
+
* - It's `family: "openai-compatible"` and accepts a custom
|
|
15
|
+
* baseUrl — the perfect match for the local 9Router gateway
|
|
16
|
+
* - The previous v6.2.2 default `9router` (and the user's hand-
|
|
17
|
+
* written `openai-compatible`) are NOT in Cline's catalog, so
|
|
18
|
+
* kanban mode fails with "Unknown or disabled provider".
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* bizar setup-provider # default 9Router gateway, litellm provider
|
|
22
|
+
* bizar setup-provider --list # print the live model catalog
|
|
23
|
+
* bizar setup-provider --gateway <url> # override gateway URL
|
|
24
|
+
* bizar setup-provider --key <key> # provider API key
|
|
25
|
+
* bizar setup-provider --provider <name> # built-in providerId (default: litellm)
|
|
26
|
+
* bizar setup-provider --discover # auto-detect a local gateway and configure
|
|
27
|
+
* bizar setup-provider --remove <name> # remove a provider
|
|
28
|
+
*
|
|
29
|
+
* The default gateway is the local 9Router at http://localhost:20128/v1.
|
|
30
|
+
* The model catalog is fetched live from `${gateway}/v1/models`.
|
|
31
|
+
*/
|
|
32
|
+
import chalk from 'chalk';
|
|
33
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, copyFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
34
|
+
import { join, dirname } from 'node:path';
|
|
35
|
+
import { homedir } from 'node:os';
|
|
36
|
+
|
|
37
|
+
function clineDir() {
|
|
38
|
+
if (process.env.CLINE_DIR && process.env.CLINE_DIR.trim()) {
|
|
39
|
+
return process.env.CLINE_DIR.trim();
|
|
40
|
+
}
|
|
41
|
+
if (process.platform === 'win32') {
|
|
42
|
+
return join(process.env.APPDATA || homedir(), 'cline');
|
|
43
|
+
}
|
|
44
|
+
return join(homedir(), '.cline');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function dataDir() {
|
|
48
|
+
if (process.env.CLINE_DATA_DIR && process.env.CLINE_DATA_DIR.trim()) {
|
|
49
|
+
return process.env.CLINE_DATA_DIR.trim();
|
|
50
|
+
}
|
|
51
|
+
return join(clineDir(), 'data');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Path to the Cline settings file the CLI + kanban actually read. */
|
|
55
|
+
function settingsPath() {
|
|
56
|
+
if (process.env.CLINE_PROVIDER_SETTINGS_PATH && process.env.CLINE_PROVIDER_SETTINGS_PATH.trim()) {
|
|
57
|
+
return process.env.CLINE_PROVIDER_SETTINGS_PATH.trim();
|
|
58
|
+
}
|
|
59
|
+
return join(dataDir(), 'settings', 'providers.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEFAULT_GATEWAY = 'http://localhost:20128/v1';
|
|
63
|
+
// v6.2.3 — `litellm` is the right built-in providerId for a custom
|
|
64
|
+
// OpenAI-compatible endpoint. It's in Cline's catalog (so kanban
|
|
65
|
+
// mode recognizes it) and accepts a custom baseUrl.
|
|
66
|
+
const DEFAULT_PROVIDER = 'litellm';
|
|
67
|
+
|
|
68
|
+
// v6.2.3 — Built-in Cline providerIds that support `family: "openai-compatible"`
|
|
69
|
+
// with a custom baseUrl. These are the ones the user can pick for
|
|
70
|
+
// "I have my own OpenAI-compatible endpoint". `9router`, `openai-compatible`
|
|
71
|
+
// and other fake IDs are NOT in this list — they fail in kanban mode.
|
|
72
|
+
export const OPENAI_COMPATIBLE_PROVIDERS = [
|
|
73
|
+
'litellm', // best fit: defaults to localhost:4000, openai-responses
|
|
74
|
+
'cline', // Cline's own API (can be overridden)
|
|
75
|
+
'ollama', // localhost:11434
|
|
76
|
+
'lmstudio', // localhost:1234
|
|
77
|
+
'huggingface', // api-inference.huggingface.co
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function readJsonSafe(file) {
|
|
81
|
+
try {
|
|
82
|
+
if (!existsSync(file)) return null;
|
|
83
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writeJsonAtomic(file, data) {
|
|
90
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
91
|
+
const tmp = `${file}.tmp`;
|
|
92
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
93
|
+
renameSync(tmp, file);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Pretty-print the live model catalog from `${gateway}/v1/models`. */
|
|
97
|
+
export async function listGatewayModels(gateway) {
|
|
98
|
+
const url = `${gateway.replace(/\/$/, '')}/models`;
|
|
99
|
+
const ac = new AbortController();
|
|
100
|
+
const timer = setTimeout(() => ac.abort(), 10000);
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(url, { signal: ac.signal });
|
|
103
|
+
if (!res.ok) throw new Error(`GET ${url} returned HTTP ${res.status}`);
|
|
104
|
+
const body = await res.json();
|
|
105
|
+
const data = Array.isArray(body?.data) ? body.data : [];
|
|
106
|
+
return data.map((m) => ({
|
|
107
|
+
id: m.id,
|
|
108
|
+
ownedBy: m.owned_by || '(unknown)',
|
|
109
|
+
}));
|
|
110
|
+
} finally {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Heuristic: an all-caps-with-underscores string is an env var name. */
|
|
116
|
+
function looksLikeEnvVar(s) {
|
|
117
|
+
return /^[A-Z][A-Z0-9_]{1,}$/.test(s);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* v6.2.3 — Build the Cline settings.json entry for one provider.
|
|
122
|
+
*
|
|
123
|
+
* The shape matches `~/.cline/data/settings/providers.json`:
|
|
124
|
+
* { "version": 1, "lastUsedProvider": "litellm", "providers": { "litellm": { ... } } }
|
|
125
|
+
*/
|
|
126
|
+
export function buildProviderBlock({ name, gateway, apiKey, model, reason = false }) {
|
|
127
|
+
return {
|
|
128
|
+
settings: {
|
|
129
|
+
provider: name,
|
|
130
|
+
model,
|
|
131
|
+
apiKey: looksLikeEnvVar(apiKey) ? `\${env:${apiKey}}` : apiKey,
|
|
132
|
+
baseUrl: gateway,
|
|
133
|
+
...(reason ? { reasoning: { enabled: true, effort: 'medium' } } : { reasoning: { enabled: false, effort: 'medium' } }),
|
|
134
|
+
},
|
|
135
|
+
updatedAt: new Date().toISOString(),
|
|
136
|
+
tokenSource: 'manual',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Apply (create or update) one provider in the settings file. */
|
|
141
|
+
export function applyProviderBlock(name, body) {
|
|
142
|
+
const file = settingsPath();
|
|
143
|
+
if (!existsSync(file)) {
|
|
144
|
+
// Bootstrap with a minimal but valid providers.json
|
|
145
|
+
const bootstrap = { version: 1, providers: {}, lastUsedProvider: name };
|
|
146
|
+
writeJsonAtomic(file, bootstrap);
|
|
147
|
+
}
|
|
148
|
+
const backup = `${file}.bak`;
|
|
149
|
+
try { copyFileSync(file, backup); } catch { /* best-effort */ }
|
|
150
|
+
|
|
151
|
+
const cfg = readJsonSafe(file, { version: 1, providers: {} });
|
|
152
|
+
if (!cfg.providers || typeof cfg.providers !== 'object') cfg.providers = {};
|
|
153
|
+
cfg.providers[name] = body;
|
|
154
|
+
// v6.2.3 — Always set lastUsedProvider to the most recently applied
|
|
155
|
+
// provider. The user is actively configuring this one; they want
|
|
156
|
+
// it to be the active one. (Previous v6.2.2 only set it when missing,
|
|
157
|
+
// which left stale `lastUsedProvider: "openai-compatible"` entries
|
|
158
|
+
// in the file when the user upgraded.)
|
|
159
|
+
cfg.lastUsedProvider = name;
|
|
160
|
+
cfg.version = cfg.version || 1;
|
|
161
|
+
writeJsonAtomic(file, cfg);
|
|
162
|
+
return { name, path: file, backup };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Remove a provider by name from the settings file. */
|
|
166
|
+
export function removeProvider(name) {
|
|
167
|
+
const file = settingsPath();
|
|
168
|
+
if (!existsSync(file)) return { removed: false, name, reason: 'no settings file' };
|
|
169
|
+
const cfg = readJsonSafe(file, { version: 1, providers: {} });
|
|
170
|
+
if (!cfg.providers || !cfg.providers[name]) {
|
|
171
|
+
return { removed: false, name, reason: 'not present' };
|
|
172
|
+
}
|
|
173
|
+
delete cfg.providers[name];
|
|
174
|
+
if (cfg.lastUsedProvider === name) {
|
|
175
|
+
cfg.lastUsedProvider = Object.keys(cfg.providers)[0] || undefined;
|
|
176
|
+
}
|
|
177
|
+
writeJsonAtomic(file, cfg);
|
|
178
|
+
return { removed: true, name, path: file };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* v6.2.3 — Detect the broken `openai-compatible` providerId pattern
|
|
183
|
+
* from earlier installs and the Cline v6.0.1 migration shim.
|
|
184
|
+
*
|
|
185
|
+
* Background: v6.0.1's Cline auto-migration created synthetic providers
|
|
186
|
+
* with `provider: "openai-compatible"` (the `family`, not a real
|
|
187
|
+
* `id`). Cline CLI accepts this, but Cline kanban mode looks up the
|
|
188
|
+
* provider in its built-in catalog and fails with:
|
|
189
|
+
* "Unknown or disabled provider \"openai-compatible\"."
|
|
190
|
+
*
|
|
191
|
+
* The fix: if the settings file has a `provider: "openai-compatible"`
|
|
192
|
+
* entry, rename it to a real catalog ID (default `litellm`) so
|
|
193
|
+
* kanban mode recognizes it. We do this IN-PLACE without losing the
|
|
194
|
+
* user's baseUrl, apiKey, or model.
|
|
195
|
+
*/
|
|
196
|
+
export function migrateLegacyOpenaiCompatible(targetName = 'litellm') {
|
|
197
|
+
const file = settingsPath();
|
|
198
|
+
if (!existsSync(file)) return { migrated: false, reason: 'no settings file' };
|
|
199
|
+
const cfg = readJsonSafe(file, { version: 1, providers: {} });
|
|
200
|
+
const broken = cfg.providers && cfg.providers['openai-compatible'];
|
|
201
|
+
if (!broken) return { migrated: false, reason: 'no legacy entry' };
|
|
202
|
+
|
|
203
|
+
// Move the entry to a real providerId.
|
|
204
|
+
const newBody = { ...broken };
|
|
205
|
+
// Strip the `provider: "openai-compatible"` field (replaced by the
|
|
206
|
+
// real providerId at the top level) and keep the rest (baseUrl,
|
|
207
|
+
// apiKey, model, reasoning, etc.).
|
|
208
|
+
if (newBody.settings) {
|
|
209
|
+
newBody.settings.provider = targetName;
|
|
210
|
+
}
|
|
211
|
+
cfg.providers[targetName] = newBody;
|
|
212
|
+
delete cfg.providers['openai-compatible'];
|
|
213
|
+
// If the broken entry was the active one, switch the new one in.
|
|
214
|
+
if (cfg.lastUsedProvider === 'openai-compatible' || !cfg.lastUsedProvider) {
|
|
215
|
+
cfg.lastUsedProvider = targetName;
|
|
216
|
+
}
|
|
217
|
+
writeJsonAtomic(file, cfg);
|
|
218
|
+
return { migrated: true, from: 'openai-compatible', to: targetName, path: file };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* v6.2.3 — `bizar setup-provider --discover`.
|
|
223
|
+
*
|
|
224
|
+
* Probe a list of well-known local gateway URLs and use the first one
|
|
225
|
+
* that responds. Then write the provider block with the discovered URL.
|
|
226
|
+
*/
|
|
227
|
+
export async function discoverLocalGateway() {
|
|
228
|
+
const candidates = [
|
|
229
|
+
'http://localhost:20128/v1', // 9Router (Bizar default)
|
|
230
|
+
'http://127.0.0.1:20128/v1',
|
|
231
|
+
'http://localhost:4000/v1', // LiteLLM
|
|
232
|
+
'http://localhost:11434/v1', // Ollama
|
|
233
|
+
'http://localhost:1234/v1', // LM Studio
|
|
234
|
+
];
|
|
235
|
+
for (const url of candidates) {
|
|
236
|
+
try {
|
|
237
|
+
const models = await listGatewayModels(url);
|
|
238
|
+
if (models.length > 0) {
|
|
239
|
+
return { url, modelCount: models.length, models };
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Try the next one
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function showSetupProviderHelp() {
|
|
249
|
+
console.log(`
|
|
250
|
+
bizar setup-provider — Configure a provider in Cline's settings
|
|
251
|
+
|
|
252
|
+
Usage:
|
|
253
|
+
bizar setup-provider Interactive setup (default: litellm + 9Router gateway)
|
|
254
|
+
bizar setup-provider --list Print the live model catalog
|
|
255
|
+
bizar setup-provider --gateway <url> Override the gateway URL (default: http://localhost:20128/v1)
|
|
256
|
+
bizar setup-provider --key <key> Provider API key
|
|
257
|
+
- All-caps-with-underscores → \${env:KEY}
|
|
258
|
+
- Otherwise → literal value
|
|
259
|
+
bizar setup-provider --provider <name> Built-in providerId (default: litellm)
|
|
260
|
+
See OPENAI_COMPATIBLE_PROVIDERS for the supported list
|
|
261
|
+
bizar setup-provider --model <id> Model ID (default: first model from \${gateway}/v1/models)
|
|
262
|
+
bizar setup-provider --discover Auto-detect a local gateway and configure
|
|
263
|
+
bizar setup-provider --remove <name> Remove a provider by name
|
|
264
|
+
bizar setup-provider --help Show this help
|
|
265
|
+
|
|
266
|
+
Description:
|
|
267
|
+
Since v6.2.2 the Bizar installer no longer touches provider config.
|
|
268
|
+
Use this command to add or update a provider in
|
|
269
|
+
\`~/.cline/data/settings/providers.json\` (the file the Cline CLI
|
|
270
|
+
and kanban mode both read).
|
|
271
|
+
|
|
272
|
+
v6.2.3 changed the default providerId from \`9router\` to \`litellm\`.
|
|
273
|
+
\`litellm\` is a real entry in Cline's built-in catalog (so it works
|
|
274
|
+
in kanban mode), \`family: "openai-compatible"\`, and accepts a
|
|
275
|
+
custom baseUrl — perfect for the local 9Router gateway.
|
|
276
|
+
|
|
277
|
+
The previous v6.2.2 default \`9router\` and any hand-written
|
|
278
|
+
providerId like \`openai-compatible\` are NOT in Cline's catalog,
|
|
279
|
+
so they fail in kanban mode with:
|
|
280
|
+
"Unknown or disabled provider \\"openai-compatible\\"."
|
|
281
|
+
|
|
282
|
+
Examples:
|
|
283
|
+
bizar setup-provider
|
|
284
|
+
bizar setup-provider --list
|
|
285
|
+
bizar setup-provider --discover
|
|
286
|
+
bizar setup-provider --provider ollama --gateway http://localhost:11434/v1
|
|
287
|
+
|
|
288
|
+
Related:
|
|
289
|
+
bizar connect Interactive TUI for provider setup
|
|
290
|
+
bizar install Installs the Bizar scaffolding (NOT provider config)
|
|
291
|
+
bizar validate Confirms provider config is wired correctly
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function parseFlags(args) {
|
|
296
|
+
const flags = {};
|
|
297
|
+
for (let i = 0; i < args.length; i++) {
|
|
298
|
+
const a = args[i];
|
|
299
|
+
if (a === '--list') flags.list = true;
|
|
300
|
+
else if (a === '--discover') flags.discover = true;
|
|
301
|
+
else if (a === '--gateway' && i + 1 < args.length) { flags.gateway = args[++i]; }
|
|
302
|
+
else if (a.startsWith('--gateway=')) flags.gateway = a.slice('--gateway='.length);
|
|
303
|
+
else if (a === '--key' && i + 1 < args.length) { flags.key = args[++i]; }
|
|
304
|
+
else if (a.startsWith('--key=')) flags.key = a.slice('--key='.length);
|
|
305
|
+
else if (a === '--provider' && i + 1 < args.length) { flags.provider = args[++i]; }
|
|
306
|
+
else if (a.startsWith('--provider=')) flags.provider = a.slice('--provider='.length);
|
|
307
|
+
else if (a === '--model' && i + 1 < args.length) { flags.model = args[++i]; }
|
|
308
|
+
else if (a.startsWith('--model=')) flags.model = a.slice('--model='.length);
|
|
309
|
+
else if (a === '--remove' && i + 1 < args.length) { flags.remove = args[++i]; }
|
|
310
|
+
else if (a.startsWith('--remove=')) flags.remove = a.slice('--remove='.length);
|
|
311
|
+
}
|
|
312
|
+
return flags;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function runSetupProvider(args = []) {
|
|
316
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
317
|
+
showSetupProviderHelp();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const flags = parseFlags(args);
|
|
321
|
+
|
|
322
|
+
// v6.2.3 — Auto-migrate the legacy `openai-compatible` providerId
|
|
323
|
+
// shim. This runs on every invocation (except --help/--list/--remove)
|
|
324
|
+
// because it's idempotent and fixes a real bug. Users who hit
|
|
325
|
+
// "Unknown or disabled provider openai-compatible" in kanban mode
|
|
326
|
+
// get healed automatically the next time they run setup-provider.
|
|
327
|
+
if (!flags.list && !flags.remove) {
|
|
328
|
+
const mig = migrateLegacyOpenaiCompatible(DEFAULT_PROVIDER);
|
|
329
|
+
if (mig.migrated) {
|
|
330
|
+
console.log(chalk.yellow(`\n ⚠ Migrated legacy '${mig.from}' → '${mig.to}'`));
|
|
331
|
+
console.log(chalk.yellow(' (kanban mode requires a built-in providerId; litellm works for any OpenAI-compatible endpoint.)'));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --list — print the catalog and exit
|
|
336
|
+
if (flags.list) {
|
|
337
|
+
const gateway = flags.gateway || DEFAULT_GATEWAY;
|
|
338
|
+
console.log(chalk.cyan(`\n Model catalog from ${gateway}/models:\n`));
|
|
339
|
+
try {
|
|
340
|
+
const models = await listGatewayModels(gateway);
|
|
341
|
+
if (models.length === 0) {
|
|
342
|
+
console.log(chalk.yellow(' (catalog is empty)'));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Group by owner.
|
|
346
|
+
const byOwner = new Map();
|
|
347
|
+
for (const m of models) {
|
|
348
|
+
if (!byOwner.has(m.ownedBy)) byOwner.set(m.ownedBy, []);
|
|
349
|
+
byOwner.get(m.ownedBy).push(m.id);
|
|
350
|
+
}
|
|
351
|
+
for (const [owner, ids] of byOwner) {
|
|
352
|
+
console.log(chalk.bold(` ${owner}:`));
|
|
353
|
+
for (const id of ids) {
|
|
354
|
+
console.log(` ${id}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.log(chalk.red(` ✗ Failed to fetch catalog: ${err.message}`));
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// --remove <name>
|
|
365
|
+
if (flags.remove) {
|
|
366
|
+
try {
|
|
367
|
+
const r = removeProvider(flags.remove);
|
|
368
|
+
if (r.removed) {
|
|
369
|
+
console.log(chalk.green(` ✓ Removed provider '${r.name}' from ${r.path}`));
|
|
370
|
+
} else {
|
|
371
|
+
console.log(chalk.yellow(` ! Provider '${r.name}' was not present`));
|
|
372
|
+
}
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.log(chalk.red(` ✗ ${err.message}`));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// --discover — auto-detect a local gateway
|
|
381
|
+
let gateway = flags.gateway;
|
|
382
|
+
if (flags.discover || (!gateway && !flags.key)) {
|
|
383
|
+
console.log(chalk.cyan('\n Probing for local gateways...'));
|
|
384
|
+
const found = await discoverLocalGateway();
|
|
385
|
+
if (found) {
|
|
386
|
+
gateway = found.url;
|
|
387
|
+
console.log(chalk.green(` ✓ Found ${found.modelCount} model(s) at ${found.url}`));
|
|
388
|
+
} else if (flags.discover) {
|
|
389
|
+
console.log(chalk.red(' ✗ No local gateway found. Tried:'));
|
|
390
|
+
for (const c of ['20128 (9Router)', '4000 (LiteLLM)', '11434 (Ollama)', '1234 (LM Studio)']) {
|
|
391
|
+
console.log(chalk.red(` - localhost:${c.split(' ')[0]}`));
|
|
392
|
+
}
|
|
393
|
+
process.exit(1);
|
|
394
|
+
} else {
|
|
395
|
+
gateway = DEFAULT_GATEWAY;
|
|
396
|
+
console.log(chalk.dim(` ! No local gateway detected, using default ${gateway}`));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
gateway = gateway || DEFAULT_GATEWAY;
|
|
400
|
+
|
|
401
|
+
const name = flags.provider || DEFAULT_PROVIDER;
|
|
402
|
+
const apiKey = flags.key || process.env.NINEROUTER_KEY || process.env.OPENAI_API_KEY || 'sk-replace-me';
|
|
403
|
+
|
|
404
|
+
// Interactive prompt for the key if missing and we're on a TTY.
|
|
405
|
+
if (!flags.key && !process.env.NINEROUTER_KEY && !process.env.OPENAI_API_KEY &&
|
|
406
|
+
process.stdin.isTTY && process.stdout.isTTY) {
|
|
407
|
+
const { createInterface } = await import('node:readline/promises');
|
|
408
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
409
|
+
try {
|
|
410
|
+
const answer = await rl.question(` Enter API key for ${name} (or env var name like NINEROUTER_KEY): `);
|
|
411
|
+
if (answer.trim()) {
|
|
412
|
+
Object.assign(flags, parseFlags(['--key', answer.trim()]));
|
|
413
|
+
}
|
|
414
|
+
} finally {
|
|
415
|
+
rl.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const finalKey = flags.key || apiKey;
|
|
420
|
+
|
|
421
|
+
// Pick the model: explicit > first from catalog > sensible default.
|
|
422
|
+
let model = flags.model;
|
|
423
|
+
if (!model) {
|
|
424
|
+
console.log(chalk.cyan(`\n Fetching model catalog from ${gateway}...`));
|
|
425
|
+
try {
|
|
426
|
+
const catalog = await listGatewayModels(gateway);
|
|
427
|
+
if (catalog.length > 0) {
|
|
428
|
+
// Prefer minimaxcustom/MiniMax-M3 if available (the Bizar default model)
|
|
429
|
+
const preferred = catalog.find((m) => m.id === 'minimaxcustom/MiniMax-M3');
|
|
430
|
+
model = preferred ? preferred.id : catalog[0].id;
|
|
431
|
+
console.log(chalk.green(` ✓ Found ${catalog.length} model(s); using '${model}'`));
|
|
432
|
+
} else {
|
|
433
|
+
model = 'minimaxcustom/MiniMax-M3';
|
|
434
|
+
console.log(chalk.yellow(` ! Empty catalog; defaulting to ${model}`));
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
model = 'minimaxcustom/MiniMax-M3';
|
|
438
|
+
console.log(chalk.yellow(` ! Could not fetch catalog (${err.message}); defaulting to ${model}`));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const body = buildProviderBlock({ name, gateway, apiKey: finalKey, model });
|
|
443
|
+
try {
|
|
444
|
+
const result = applyProviderBlock(name, body);
|
|
445
|
+
console.log(chalk.green(`\n ✓ Provider '${result.name}' written to ${result.path}`));
|
|
446
|
+
console.log(chalk.dim(` backup: ${result.backup}`));
|
|
447
|
+
console.log(chalk.dim(` baseUrl: ${gateway}`));
|
|
448
|
+
console.log(chalk.dim(` model: ${model}`));
|
|
449
|
+
console.log(chalk.dim(` apiKey: ${looksLikeEnvVar(finalKey) ? `\${env:${finalKey}}` : (finalKey.length > 12 ? finalKey.slice(0, 8) + '...' : finalKey)}`));
|
|
450
|
+
console.log('');
|
|
451
|
+
console.log(chalk.cyan(' Next:'));
|
|
452
|
+
console.log(chalk.cyan(' • Run `bizar validate` to confirm everything is wired up.'));
|
|
453
|
+
console.log(chalk.cyan(' • The same provider now works in both `cline` CLI and `cline --kanban` mode.'));
|
|
454
|
+
} catch (err) {
|
|
455
|
+
console.log(chalk.red(` ✗ ${err.message}`));
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ── run() entry point (used by bin.mjs dispatcher) ─────────────────────────
|
|
461
|
+
|
|
462
|
+
export async function run(name, args, isHelpRequest) {
|
|
463
|
+
if (name !== 'setup-provider') return false;
|
|
464
|
+
await runSetupProvider(args);
|
|
465
|
+
return true;
|
|
466
|
+
}
|