@polderlabs/bizar 6.2.2 → 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 +51 -1
- 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 +249 -75
- package/cli/commands/setup-provider.test.mjs +198 -98
- package/cli/commands/validate.mjs +63 -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 +57 -4
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* cli/commands/setup-provider.test.mjs
|
|
3
3
|
*
|
|
4
|
-
* v6.2.
|
|
4
|
+
* v6.2.3 — Unit tests for the setup-provider subcommand.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* v6.2.3 changed the storage backend from `~/.cline/cline.json` to
|
|
7
|
+
* `~/.cline/data/settings/providers.json` (the file the Cline CLI
|
|
8
|
+
* and kanban mode actually read). The default providerId changed
|
|
9
|
+
* from `9router` to `litellm` (a real entry in Cline's catalog).
|
|
10
|
+
*
|
|
11
|
+
* Exercises the pure helpers (`buildProviderBlock`, `parseFlags`,
|
|
12
|
+
* `OPENAI_COMPATIBLE_PROVIDERS`) and the filesystem mutations
|
|
13
|
+
* (`applyProviderBlock`, `removeProvider`) with a mocked CLINE_DIR.
|
|
9
14
|
*/
|
|
10
15
|
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
11
16
|
import assert from 'node:assert/strict';
|
|
@@ -19,27 +24,41 @@ const {
|
|
|
19
24
|
removeProvider,
|
|
20
25
|
listGatewayModels,
|
|
21
26
|
parseFlags,
|
|
27
|
+
OPENAI_COMPATIBLE_PROVIDERS,
|
|
22
28
|
} = await import('./setup-provider.mjs');
|
|
23
29
|
|
|
24
30
|
const ORIG_CLINE_DIR = process.env.CLINE_DIR;
|
|
31
|
+
const ORIG_DATA_DIR = process.env.CLINE_DATA_DIR;
|
|
32
|
+
const ORIG_PROVIDER_PATH = process.env.CLINE_PROVIDER_SETTINGS_PATH;
|
|
25
33
|
let workDir;
|
|
26
34
|
|
|
27
35
|
function freshWorkDir() {
|
|
28
36
|
workDir = mkdtempSync(join(tmpdir(), 'bizar-setup-provider-'));
|
|
37
|
+
// CLINE_DIR is the *base* dir (e.g. ~/.cline). The settings file
|
|
38
|
+
// lives at `${CLINE_DIR}/data/settings/providers.json`. Mocking
|
|
39
|
+
// CLINE_DIR is the easiest way to redirect the whole tree.
|
|
29
40
|
process.env.CLINE_DIR = workDir;
|
|
41
|
+
// Clear overrides that could change the resolution path.
|
|
42
|
+
delete process.env.CLINE_DATA_DIR;
|
|
43
|
+
delete process.env.CLINE_PROVIDER_SETTINGS_PATH;
|
|
30
44
|
return workDir;
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
function
|
|
47
|
+
function settingsFile() {
|
|
48
|
+
return join(workDir, 'data', 'settings', 'providers.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeFakeSettings(root, body = {}) {
|
|
34
52
|
const cfg = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
permission: 'allow',
|
|
39
|
-
snapshot: false,
|
|
53
|
+
version: 1,
|
|
54
|
+
providers: {},
|
|
55
|
+
lastUsedProvider: 'litellm',
|
|
40
56
|
...body,
|
|
41
57
|
};
|
|
42
|
-
|
|
58
|
+
const file = join(root, 'data', 'settings', 'providers.json');
|
|
59
|
+
mkdirSync(join(root, 'data', 'settings'), { recursive: true });
|
|
60
|
+
writeFileSync(file, JSON.stringify(cfg, null, 2));
|
|
61
|
+
return file;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
beforeEach(() => {
|
|
@@ -50,137 +69,215 @@ afterEach(() => {
|
|
|
50
69
|
if (workDir && existsSync(workDir)) rmSync(workDir, { recursive: true, force: true });
|
|
51
70
|
if (ORIG_CLINE_DIR === undefined) delete process.env.CLINE_DIR;
|
|
52
71
|
else process.env.CLINE_DIR = ORIG_CLINE_DIR;
|
|
72
|
+
if (ORIG_DATA_DIR === undefined) delete process.env.CLINE_DATA_DIR;
|
|
73
|
+
else process.env.CLINE_DATA_DIR = ORIG_DATA_DIR;
|
|
74
|
+
if (ORIG_PROVIDER_PATH === undefined) delete process.env.CLINE_PROVIDER_SETTINGS_PATH;
|
|
75
|
+
else process.env.CLINE_PROVIDER_SETTINGS_PATH = ORIG_PROVIDER_PATH;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('OPENAI_COMPATIBLE_PROVIDERS', () => {
|
|
79
|
+
test('includes litellm (the new v6.2.3 default)', () => {
|
|
80
|
+
assert.ok(OPENAI_COMPATIBLE_PROVIDERS.includes('litellm'),
|
|
81
|
+
'litellm must be the canonical openai-compatible providerId');
|
|
82
|
+
});
|
|
83
|
+
test('does NOT include the fake "openai-compatible" or "9router" IDs', () => {
|
|
84
|
+
assert.equal(OPENAI_COMPATIBLE_PROVIDERS.includes('openai-compatible'), false,
|
|
85
|
+
'openai-compatible is a family, not a valid providerId');
|
|
86
|
+
assert.equal(OPENAI_COMPATIBLE_PROVIDERS.includes('9router'), false,
|
|
87
|
+
'9router is not a built-in Cline providerId (would fail in kanban)');
|
|
88
|
+
});
|
|
53
89
|
});
|
|
54
90
|
|
|
55
91
|
describe('buildProviderBlock()', () => {
|
|
56
|
-
test('emits a
|
|
92
|
+
test('emits a Cline-settings-shaped block (version 1 + settings + updatedAt)', () => {
|
|
57
93
|
const block = buildProviderBlock({
|
|
58
|
-
name: '
|
|
94
|
+
name: 'litellm',
|
|
59
95
|
gateway: 'http://localhost:20128/v1',
|
|
60
96
|
apiKey: 'sk-test',
|
|
61
|
-
|
|
97
|
+
model: 'minimaxcustom/MiniMax-M3',
|
|
62
98
|
});
|
|
63
|
-
assert.ok(block
|
|
64
|
-
assert.equal(block
|
|
65
|
-
assert.equal(block
|
|
66
|
-
assert.
|
|
99
|
+
assert.ok(block.settings);
|
|
100
|
+
assert.equal(block.settings.provider, 'litellm');
|
|
101
|
+
assert.equal(block.settings.model, 'minimaxcustom/MiniMax-M3');
|
|
102
|
+
assert.equal(block.settings.apiKey, 'sk-test');
|
|
103
|
+
assert.equal(block.settings.baseUrl, 'http://localhost:20128/v1');
|
|
104
|
+
assert.ok(block.settings.reasoning);
|
|
105
|
+
assert.equal(block.settings.reasoning.enabled, false);
|
|
106
|
+
assert.ok(block.updatedAt);
|
|
107
|
+
assert.match(block.updatedAt, /^\d{4}-\d{2}-\d{2}T/);
|
|
108
|
+
assert.equal(block.tokenSource, 'manual');
|
|
67
109
|
});
|
|
68
110
|
|
|
69
111
|
test('wraps env-var-style keys in ${env:...}', () => {
|
|
70
112
|
const block = buildProviderBlock({
|
|
71
|
-
name: '
|
|
113
|
+
name: 'litellm',
|
|
72
114
|
gateway: 'http://localhost:20128/v1',
|
|
73
115
|
apiKey: 'NINEROUTER_KEY',
|
|
74
|
-
|
|
116
|
+
model: 'm1',
|
|
75
117
|
});
|
|
76
|
-
assert.equal(block
|
|
118
|
+
assert.equal(block.settings.apiKey, '${env:NINEROUTER_KEY}');
|
|
77
119
|
});
|
|
78
120
|
|
|
79
|
-
test('
|
|
121
|
+
test('sets reasoning.enabled=true when reason=true', () => {
|
|
80
122
|
const block = buildProviderBlock({
|
|
81
|
-
name: '
|
|
82
|
-
gateway: 'https://x.example/v1',
|
|
83
|
-
apiKey: 'sk-abc-123',
|
|
84
|
-
models: ['m1'],
|
|
123
|
+
name: 'litellm', gateway: 'http://x', apiKey: 'k', model: 'm', reason: true,
|
|
85
124
|
});
|
|
86
|
-
assert.equal(block
|
|
125
|
+
assert.equal(block.settings.reasoning.enabled, true);
|
|
87
126
|
});
|
|
88
127
|
});
|
|
89
128
|
|
|
90
129
|
describe('applyProviderBlock()', () => {
|
|
91
|
-
test('writes
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
gateway: 'http://localhost:20128/v1',
|
|
96
|
-
apiKey: 'sk-test',
|
|
97
|
-
models: ['minimax/MiniMax-M3'],
|
|
130
|
+
test('writes to ${CLINE_DIR}/data/settings/providers.json (NOT ~/.cline/cline.json)', () => {
|
|
131
|
+
const body = buildProviderBlock({
|
|
132
|
+
name: 'litellm', gateway: 'http://localhost:20128/v1',
|
|
133
|
+
apiKey: 'sk-test', model: 'minimaxcustom/MiniMax-M3',
|
|
98
134
|
});
|
|
99
|
-
const r = applyProviderBlock(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
assert.ok(
|
|
103
|
-
assert.equal(cfg.provider['9router'].baseUrl, 'http://localhost:20128/v1');
|
|
135
|
+
const r = applyProviderBlock('litellm', body);
|
|
136
|
+
const expected = settingsFile();
|
|
137
|
+
assert.equal(r.path, expected, 'must write to the Cline settings file');
|
|
138
|
+
assert.ok(existsSync(expected));
|
|
104
139
|
});
|
|
105
140
|
|
|
106
|
-
test('
|
|
107
|
-
|
|
108
|
-
|
|
141
|
+
test('bootstraps the settings file when it does not exist', () => {
|
|
142
|
+
// No makeFakeSettings call.
|
|
143
|
+
const body = buildProviderBlock({
|
|
144
|
+
name: 'litellm', gateway: 'http://x', apiKey: 'k', model: 'm',
|
|
109
145
|
});
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
applyProviderBlock(block);
|
|
117
|
-
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
118
|
-
assert.ok(cfg.provider.existing, 'existing provider preserved');
|
|
119
|
-
assert.ok(cfg.provider['9router'], 'new provider added');
|
|
146
|
+
const r = applyProviderBlock('litellm', body);
|
|
147
|
+
assert.ok(existsSync(r.path));
|
|
148
|
+
const cfg = JSON.parse(readFileSync(r.path, 'utf8'));
|
|
149
|
+
assert.equal(cfg.version, 1);
|
|
150
|
+
assert.equal(cfg.lastUsedProvider, 'litellm');
|
|
151
|
+
assert.ok(cfg.providers.litellm);
|
|
120
152
|
});
|
|
121
153
|
|
|
122
|
-
test('
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
test('preserves other provider entries when adding a new one', () => {
|
|
155
|
+
makeFakeSettings(workDir, {
|
|
156
|
+
providers: { ollama: { settings: { provider: 'ollama', model: 'llama3' } } },
|
|
125
157
|
});
|
|
126
|
-
const
|
|
127
|
-
name: '
|
|
128
|
-
gateway: 'http://localhost:20128/v1',
|
|
129
|
-
apiKey: 'new',
|
|
130
|
-
models: ['minimax/MiniMax-M3'],
|
|
158
|
+
const body = buildProviderBlock({
|
|
159
|
+
name: 'litellm', gateway: 'http://x', apiKey: 'k', model: 'm',
|
|
131
160
|
});
|
|
132
|
-
applyProviderBlock(
|
|
133
|
-
const cfg = JSON.parse(readFileSync(
|
|
134
|
-
assert.
|
|
135
|
-
assert.
|
|
136
|
-
assert.ok(cfg.provider['9router'].models['minimax/MiniMax-M3']);
|
|
161
|
+
applyProviderBlock('litellm', body);
|
|
162
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
163
|
+
assert.ok(cfg.providers.ollama, 'existing provider preserved');
|
|
164
|
+
assert.ok(cfg.providers.litellm, 'new provider added');
|
|
137
165
|
});
|
|
138
166
|
|
|
139
|
-
test('
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
name: '9router',
|
|
143
|
-
gateway: 'http://localhost:20128/v1',
|
|
144
|
-
apiKey: 'sk-test',
|
|
145
|
-
models: ['minimax/MiniMax-M3'],
|
|
167
|
+
test('overwrites an existing provider with the same name', () => {
|
|
168
|
+
makeFakeSettings(workDir, {
|
|
169
|
+
providers: { litellm: { settings: { provider: 'litellm', baseUrl: 'https://old', model: 'old' } } },
|
|
146
170
|
});
|
|
147
|
-
|
|
171
|
+
const body = buildProviderBlock({
|
|
172
|
+
name: 'litellm', gateway: 'http://new', apiKey: 'new', model: 'new',
|
|
173
|
+
});
|
|
174
|
+
applyProviderBlock('litellm', body);
|
|
175
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
176
|
+
assert.equal(cfg.providers.litellm.settings.baseUrl, 'http://new');
|
|
177
|
+
assert.equal(cfg.providers.litellm.settings.model, 'new');
|
|
148
178
|
});
|
|
149
179
|
|
|
150
180
|
test('creates a backup file', () => {
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
makeFakeSettings(workDir, {
|
|
182
|
+
providers: { x: { settings: { provider: 'x' } } },
|
|
153
183
|
});
|
|
154
|
-
const
|
|
155
|
-
name: '
|
|
156
|
-
gateway: 'http://localhost:20128/v1',
|
|
157
|
-
apiKey: 'sk-test',
|
|
158
|
-
models: ['minimax/MiniMax-M3'],
|
|
184
|
+
const body = buildProviderBlock({
|
|
185
|
+
name: 'litellm', gateway: 'http://x', apiKey: 'k', model: 'm',
|
|
159
186
|
});
|
|
160
|
-
const r = applyProviderBlock(
|
|
187
|
+
const r = applyProviderBlock('litellm', body);
|
|
161
188
|
assert.ok(existsSync(r.backup), 'backup file created');
|
|
162
189
|
const backup = JSON.parse(readFileSync(r.backup, 'utf8'));
|
|
163
|
-
assert.
|
|
190
|
+
assert.ok(backup.providers.x, 'backup contains original entry');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('migrateLegacyOpenaiCompatible()', async () => {
|
|
195
|
+
const { migrateLegacyOpenaiCompatible } = await import('./setup-provider.mjs');
|
|
196
|
+
|
|
197
|
+
test('renames an openai-compatible entry to litellm (preserving baseUrl/apiKey/model)', () => {
|
|
198
|
+
makeFakeSettings(workDir, {
|
|
199
|
+
providers: {
|
|
200
|
+
'openai-compatible': {
|
|
201
|
+
settings: {
|
|
202
|
+
provider: 'openai-compatible',
|
|
203
|
+
baseUrl: 'http://localhost:20128/v1',
|
|
204
|
+
apiKey: 'sk-test',
|
|
205
|
+
model: 'minimaxcustom/MiniMax-M3',
|
|
206
|
+
reasoning: { enabled: false, effort: 'medium' },
|
|
207
|
+
},
|
|
208
|
+
updatedAt: '2026-07-09T21:20:00.000Z',
|
|
209
|
+
tokenSource: 'manual',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
lastUsedProvider: 'openai-compatible',
|
|
213
|
+
});
|
|
214
|
+
const r = migrateLegacyOpenaiCompatible('litellm');
|
|
215
|
+
assert.equal(r.migrated, true);
|
|
216
|
+
assert.equal(r.from, 'openai-compatible');
|
|
217
|
+
assert.equal(r.to, 'litellm');
|
|
218
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
219
|
+
assert.equal(cfg.providers['openai-compatible'], undefined, 'old key removed');
|
|
220
|
+
assert.ok(cfg.providers.litellm, 'new key added');
|
|
221
|
+
assert.equal(cfg.providers.litellm.settings.baseUrl, 'http://localhost:20128/v1', 'baseUrl preserved');
|
|
222
|
+
assert.equal(cfg.providers.litellm.settings.apiKey, 'sk-test', 'apiKey preserved');
|
|
223
|
+
assert.equal(cfg.providers.litellm.settings.model, 'minimaxcustom/MiniMax-M3', 'model preserved');
|
|
224
|
+
assert.equal(cfg.lastUsedProvider, 'litellm', 'active provider switched');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('no-op when no legacy entry exists', () => {
|
|
228
|
+
makeFakeSettings(workDir, {
|
|
229
|
+
providers: { litellm: { settings: { provider: 'litellm' } } },
|
|
230
|
+
});
|
|
231
|
+
const r = migrateLegacyOpenaiCompatible('litellm');
|
|
232
|
+
assert.equal(r.migrated, false);
|
|
233
|
+
assert.equal(r.reason, 'no legacy entry');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('no-op when settings file missing', () => {
|
|
237
|
+
// No makeFakeSettings call
|
|
238
|
+
const r = migrateLegacyOpenaiCompatible('litellm');
|
|
239
|
+
assert.equal(r.migrated, false);
|
|
240
|
+
assert.equal(r.reason, 'no settings file');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('preserves lastUsedProvider when it pointed at a different entry', () => {
|
|
244
|
+
makeFakeSettings(workDir, {
|
|
245
|
+
providers: {
|
|
246
|
+
'openai-compatible': { settings: { provider: 'openai-compatible', baseUrl: 'http://a' } },
|
|
247
|
+
ollama: { settings: { provider: 'ollama' } },
|
|
248
|
+
},
|
|
249
|
+
lastUsedProvider: 'ollama',
|
|
250
|
+
});
|
|
251
|
+
migrateLegacyOpenaiCompatible('litellm');
|
|
252
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
253
|
+
// Should NOT clobber the active provider if it wasn't the broken one
|
|
254
|
+
assert.equal(cfg.lastUsedProvider, 'ollama', 'active ollama preserved');
|
|
164
255
|
});
|
|
165
256
|
});
|
|
166
257
|
|
|
167
258
|
describe('removeProvider()', () => {
|
|
168
259
|
test('removes a named provider', () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
a: {
|
|
172
|
-
b: {
|
|
260
|
+
makeFakeSettings(workDir, {
|
|
261
|
+
providers: {
|
|
262
|
+
a: { settings: { provider: 'a' } },
|
|
263
|
+
b: { settings: { provider: 'b' } },
|
|
173
264
|
},
|
|
174
265
|
});
|
|
175
266
|
const r = removeProvider('a');
|
|
176
267
|
assert.equal(r.removed, true);
|
|
177
|
-
const cfg = JSON.parse(readFileSync(
|
|
178
|
-
assert.equal(cfg.
|
|
179
|
-
assert.ok(cfg.
|
|
268
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
269
|
+
assert.equal(cfg.providers.a, undefined);
|
|
270
|
+
assert.ok(cfg.providers.b, 'untouched provider preserved');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('returns removed=false when settings file is missing', () => {
|
|
274
|
+
const r = removeProvider('nope');
|
|
275
|
+
assert.equal(r.removed, false);
|
|
276
|
+
assert.equal(r.reason, 'no settings file');
|
|
180
277
|
});
|
|
181
278
|
|
|
182
279
|
test('returns removed=false for a missing provider', () => {
|
|
183
|
-
|
|
280
|
+
makeFakeSettings(workDir);
|
|
184
281
|
const r = removeProvider('nonexistent');
|
|
185
282
|
assert.equal(r.removed, false);
|
|
186
283
|
assert.equal(r.reason, 'not present');
|
|
@@ -188,23 +285,26 @@ describe('removeProvider()', () => {
|
|
|
188
285
|
});
|
|
189
286
|
|
|
190
287
|
describe('parseFlags()', () => {
|
|
191
|
-
test('parses --gateway, --key, --provider with space separator', () => {
|
|
192
|
-
const f = parseFlags(['--gateway', 'http://x', '--key', 'sk-x', '--provider', '
|
|
288
|
+
test('parses --gateway, --key, --provider, --model with space separator', () => {
|
|
289
|
+
const f = parseFlags(['--gateway', 'http://x', '--key', 'sk-x', '--provider', 'litellm', '--model', 'm1']);
|
|
193
290
|
assert.equal(f.gateway, 'http://x');
|
|
194
291
|
assert.equal(f.key, 'sk-x');
|
|
195
|
-
assert.equal(f.provider, '
|
|
292
|
+
assert.equal(f.provider, 'litellm');
|
|
293
|
+
assert.equal(f.model, 'm1');
|
|
196
294
|
});
|
|
197
295
|
|
|
198
|
-
test('parses
|
|
199
|
-
const f = parseFlags(['--gateway=http://x', '--key=sk-x', '--provider=
|
|
296
|
+
test('parses equals form', () => {
|
|
297
|
+
const f = parseFlags(['--gateway=http://x', '--key=sk-x', '--provider=litellm', '--model=m1']);
|
|
200
298
|
assert.equal(f.gateway, 'http://x');
|
|
201
299
|
assert.equal(f.key, 'sk-x');
|
|
202
|
-
assert.equal(f.provider, '
|
|
300
|
+
assert.equal(f.provider, 'litellm');
|
|
301
|
+
assert.equal(f.model, 'm1');
|
|
203
302
|
});
|
|
204
303
|
|
|
205
|
-
test('parses --list
|
|
206
|
-
const f = parseFlags(['--list']);
|
|
304
|
+
test('parses --list, --discover, --remove', () => {
|
|
305
|
+
const f = parseFlags(['--list', '--discover']);
|
|
207
306
|
assert.equal(f.list, true);
|
|
307
|
+
assert.equal(f.discover, true);
|
|
208
308
|
const g = parseFlags(['--remove', 'myprov']);
|
|
209
309
|
assert.equal(g.remove, 'myprov');
|
|
210
310
|
});
|
|
@@ -65,6 +65,23 @@ const REQUIRED_HOOKS = [
|
|
|
65
65
|
|
|
66
66
|
const REQUIRED_RUNTIME_DEPS = ['zod', '@cline/sdk', '@cline/core', '@cline/shared'];
|
|
67
67
|
|
|
68
|
+
// v6.2.3 — Built-in Cline providerIds that are safe for any
|
|
69
|
+
// "OpenAI-compatible endpoint" use case. The harness warns about
|
|
70
|
+
// any providerId NOT in this set (since they fail in kanban mode).
|
|
71
|
+
const OPENAI_COMPATIBLE_BUILTIN_IDS = new Set([
|
|
72
|
+
'litellm', 'cline', 'ollama', 'lmstudio', 'huggingface',
|
|
73
|
+
// Plus the first-class providers (also OpenAI-compatible):
|
|
74
|
+
'openrouter', 'deepseek', 'xai', 'together', 'fireworks',
|
|
75
|
+
'groq', 'cerebras', 'sambanova', 'nebius', 'baseten',
|
|
76
|
+
'requesty', 'vercel-ai-gateway', 'v0', 'aihubmix', 'hicap',
|
|
77
|
+
'nousResearch', 'huawei-cloud-maas', 'qwen', 'qwen-code',
|
|
78
|
+
'doubao', 'zai', 'zai-coding-plan', 'moonshot', 'wandb',
|
|
79
|
+
'xiaomi', 'kilo', 'oca', 'asksage', 'sapaicore',
|
|
80
|
+
// Non-OpenAI-compatible (kept so we don't false-positive):
|
|
81
|
+
'anthropic', 'google', 'bedrock', 'vertex', 'gemini',
|
|
82
|
+
'openai', 'openai-native', 'openai-codex',
|
|
83
|
+
]);
|
|
84
|
+
|
|
68
85
|
function check(name, fn) {
|
|
69
86
|
return Promise.resolve()
|
|
70
87
|
.then(fn)
|
|
@@ -284,6 +301,50 @@ const CHECKS = {
|
|
|
284
301
|
return `user-configured providers: ${summary.join('; ')}`;
|
|
285
302
|
},
|
|
286
303
|
|
|
304
|
+
'cline-settings-provider': async () => {
|
|
305
|
+
// v6.2.3 — The Cline CLI and kanban mode read from
|
|
306
|
+
// `~/.cline/data/settings/providers.json`. The harness config
|
|
307
|
+
// (`cline.json`) is a separate thing. This check inspects the
|
|
308
|
+
// actual settings file and:
|
|
309
|
+
// 1. Warns if the user has a legacy `openai-compatible` providerId
|
|
310
|
+
// (from the v6.0.1 Cline auto-migration shim). Kanban mode
|
|
311
|
+
// fails on this with "Unknown or disabled provider". Run
|
|
312
|
+
// `bizar setup-provider` to auto-migrate.
|
|
313
|
+
// 2. Reports the active `lastUsedProvider` so the user can
|
|
314
|
+
// see which provider Cline will actually use on the next
|
|
315
|
+
// session.
|
|
316
|
+
const file = join(homedir(), '.cline', 'data', 'settings', 'providers.json');
|
|
317
|
+
if (!existsSync(file)) {
|
|
318
|
+
return 'no settings file at ~/.cline/data/settings/providers.json — run `bizar setup-provider` to add one';
|
|
319
|
+
}
|
|
320
|
+
const settings = readJsonSafe(file);
|
|
321
|
+
if (!settings || !settings.providers) {
|
|
322
|
+
return 'settings file is empty or invalid — run `bizar setup-provider`';
|
|
323
|
+
}
|
|
324
|
+
const names = Object.keys(settings.providers);
|
|
325
|
+
if (names.length === 0) {
|
|
326
|
+
throw new Error('no providers configured in Cline settings — run `bizar setup-provider`');
|
|
327
|
+
}
|
|
328
|
+
const issues = [];
|
|
329
|
+
if (names.includes('openai-compatible')) {
|
|
330
|
+
issues.push(`legacy 'openai-compatible' providerId present (broken in kanban mode) — run \`bizar setup-provider\` to auto-migrate to 'litellm'`);
|
|
331
|
+
}
|
|
332
|
+
// Check for any providerId that's not in Cline's built-in catalog.
|
|
333
|
+
for (const n of names) {
|
|
334
|
+
if (n === 'openai-compatible') continue; // handled above
|
|
335
|
+
if (!OPENAI_COMPATIBLE_BUILTIN_IDS.has(n)) {
|
|
336
|
+
issues.push(`'${n}' providerId is NOT in Cline's built-in catalog (likely broken in kanban mode)`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const lastUsed = settings.lastUsedProvider || '(none)';
|
|
340
|
+
if (issues.length > 0) {
|
|
341
|
+
// Throw so the ⚠ marker shows up; the check is in LENIENT_CHECKS
|
|
342
|
+
// so this won't cause `bizar validate` to exit non-zero.
|
|
343
|
+
throw new Error(`WARN: ${issues.join('; ')}. lastUsedProvider=${lastUsed} (${names.join(', ')})`);
|
|
344
|
+
}
|
|
345
|
+
return `lastUsedProvider=${lastUsed}; providers: ${names.join(', ')}`;
|
|
346
|
+
},
|
|
347
|
+
|
|
287
348
|
'9router-reachable': async () => {
|
|
288
349
|
const url = process.env.NINEROUTER_URL || 'http://localhost:20128';
|
|
289
350
|
const ac = new AbortController();
|
|
@@ -371,12 +432,14 @@ const CHECK_ORDER = [
|
|
|
371
432
|
'default-agent-set',
|
|
372
433
|
'instructions-loaded',
|
|
373
434
|
'provider-config',
|
|
435
|
+
'cline-settings-provider',
|
|
374
436
|
'9router-reachable',
|
|
375
437
|
];
|
|
376
438
|
|
|
377
439
|
const LENIENT_CHECKS = new Set([
|
|
378
440
|
'9router-reachable',
|
|
379
441
|
'provider-config', // v6.2.2+ — installer no longer touches provider config; user must configure
|
|
442
|
+
'cline-settings-provider', // v6.2.3 — warns about legacy/fake providerIds; non-blocking
|
|
380
443
|
]);
|
|
381
444
|
|
|
382
445
|
export function showValidateHelp() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.3",
|
|
4
4
|
"description": "Norse-pantheon multi-agent system for cline — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness. v4 ships as a single npm package bundling the dashboard server, cline plugin, and typed SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test:sdk": "node_modules/.bin/vitest run --root packages/sdk",
|
|
25
25
|
"test:web": "cd bizar-dash && npx vitest run",
|
|
26
26
|
"test:sdk:watch": "node_modules/.bin/vitest --root packages/sdk",
|
|
27
|
-
"test": "npm run typecheck && npm run test:sdk && node --test cli/install.test.mjs cli/provision.test.mjs cli/commands/validate.test.mjs cli/commands/setup-provider.test.mjs && bun test plugins/bizar/tests/loop.test.ts plugins/bizar/tests/block.test.ts plugins/bizar/tests/stall-think.test.ts plugins/bizar/tests/tools/bg-get-comments.test.ts plugins/bizar/tests/tools/bg-spawn-delegation.test.ts plugins/bizar/tests/tools/cline-runner.test.ts plugins/bizar/tests/settings.test.ts plugins/bizar/tests/commands.test.ts plugins/bizar/tests/commands-impl.test.ts plugins/bizar/tests/tools/plan-action.test.ts plugins/bizar/tests/tools/wait-for-feedback.test.ts plugins/bizar/tests/tools/read-glyph-feedback.test.ts plugins/bizar/tests/reasoning-clean.test.ts plugins/bizar/tests/key-rotation.test.ts && node bizar-dash/tests/smoke-v2.mjs && node --test bizar-dash/tests/path-safe.test.mjs bizar-dash/tests/tmux-wrap.test.mjs bizar-dash/tests/cline-sessions-detail.test.mjs bizar-dash/tests/cline-runner.test.mjs bizar-dash/tests/mod-instructions.node.test.mjs bizar-dash/tests/mod-upgrade.node.test.mjs bizar-dash/tests/graphify-mod-spawn.node.test.mjs bizar-dash/tests/no-agent-browser.node.test.mjs bizar-dash/tests/providers-store-backup-keys.node.test.mjs bizar-dash/tests/dashboard-ports.test.mjs bizar-dash/tests/submit-feedback.test.mjs bizar-dash/tests/yaml.test.mjs bizar-dash/tests/memory-store.test.mjs bizar-dash/tests/memory-schema.test.mjs bizar-dash/tests/memory-secrets.test.mjs bizar-dash/tests/memory-git.test.mjs bizar-dash/tests/memory-sync.test.mjs bizar-dash/tests/obsidian-back-compat.test.mjs bizar-dash/tests/memory-lightrag.test.mjs bizar-dash/tests/memory-config.test.mjs bizar-dash/tests/memory-cli.test.mjs bizar-dash/tests/memory-cli-readlistdelete.test.mjs bizar-dash/tests/memory-cli-setup.test.mjs bizar-dash/tests/memory-conflicts.test.mjs bizar-dash/tests/memory-namespace.test.mjs bizar-dash/tests/memory-path-safety.test.mjs bizar-dash/tests/memory-protocol-drift.test.mjs bizar-dash/tests/memory-roundtrip.test.mjs bizar-dash/tests/cli-bugfixes.test.mjs bizar-dash/tests/server-bugfixes.test.mjs bizar-dash/tests/frontend-bugfixes.test.mjs",
|
|
27
|
+
"test": "npm run typecheck && npm run test:sdk && node --test cli/install.test.mjs cli/provision.test.mjs cli/commands/validate.test.mjs cli/commands/setup-provider.test.mjs cli/commands/rca.test.mjs && bun test plugins/bizar/tests/loop.test.ts plugins/bizar/tests/block.test.ts plugins/bizar/tests/stall-think.test.ts plugins/bizar/tests/tools/bg-get-comments.test.ts plugins/bizar/tests/tools/bg-spawn-delegation.test.ts plugins/bizar/tests/tools/cline-runner.test.ts plugins/bizar/tests/settings.test.ts plugins/bizar/tests/commands.test.ts plugins/bizar/tests/commands-impl.test.ts plugins/bizar/tests/tools/plan-action.test.ts plugins/bizar/tests/tools/wait-for-feedback.test.ts plugins/bizar/tests/tools/read-glyph-feedback.test.ts plugins/bizar/tests/reasoning-clean.test.ts plugins/bizar/tests/key-rotation.test.ts && node bizar-dash/tests/smoke-v2.mjs && node --test bizar-dash/tests/path-safe.test.mjs bizar-dash/tests/tmux-wrap.test.mjs bizar-dash/tests/cline-sessions-detail.test.mjs bizar-dash/tests/cline-runner.test.mjs bizar-dash/tests/mod-instructions.node.test.mjs bizar-dash/tests/mod-upgrade.node.test.mjs bizar-dash/tests/graphify-mod-spawn.node.test.mjs bizar-dash/tests/no-agent-browser.node.test.mjs bizar-dash/tests/providers-store-backup-keys.node.test.mjs bizar-dash/tests/dashboard-ports.test.mjs bizar-dash/tests/submit-feedback.test.mjs bizar-dash/tests/yaml.test.mjs bizar-dash/tests/memory-store.test.mjs bizar-dash/tests/memory-schema.test.mjs bizar-dash/tests/memory-secrets.test.mjs bizar-dash/tests/memory-git.test.mjs bizar-dash/tests/memory-sync.test.mjs bizar-dash/tests/obsidian-back-compat.test.mjs bizar-dash/tests/memory-lightrag.test.mjs bizar-dash/tests/memory-config.test.mjs bizar-dash/tests/memory-cli.test.mjs bizar-dash/tests/memory-cli-readlistdelete.test.mjs bizar-dash/tests/memory-cli-setup.test.mjs bizar-dash/tests/memory-conflicts.test.mjs bizar-dash/tests/memory-namespace.test.mjs bizar-dash/tests/memory-path-safety.test.mjs bizar-dash/tests/memory-protocol-drift.test.mjs bizar-dash/tests/memory-roundtrip.test.mjs bizar-dash/tests/cli-bugfixes.test.mjs bizar-dash/tests/server-bugfixes.test.mjs bizar-dash/tests/frontend-bugfixes.test.mjs",
|
|
28
28
|
"build": "npm run build:sdk && npm run build:dash",
|
|
29
29
|
"prepublishOnly": "npm run build"
|
|
30
30
|
},
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar-plugin",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.3",
|
|
4
4
|
"description": "Bizar Norse-pantheon multi-agent plugin for Cline — 14 agents across 4 cost tiers with cost-aware routing and plans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
7
7
|
"cline": {
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
|
-
"paths": [
|
|
11
|
-
|
|
10
|
+
"paths": [
|
|
11
|
+
"./index.ts"
|
|
12
|
+
],
|
|
13
|
+
"capabilities": [
|
|
14
|
+
"tools",
|
|
15
|
+
"hooks"
|
|
16
|
+
]
|
|
12
17
|
}
|
|
13
18
|
]
|
|
14
19
|
},
|
|
@@ -21,8 +26,14 @@
|
|
|
21
26
|
"@cline/shared": "*"
|
|
22
27
|
},
|
|
23
28
|
"peerDependenciesMeta": {
|
|
24
|
-
"@cline/sdk": {
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
"@cline/sdk": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"@cline/core": {
|
|
33
|
+
"optional": true
|
|
34
|
+
},
|
|
35
|
+
"@cline/shared": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
27
38
|
}
|
|
28
39
|
}
|
|
@@ -160,7 +160,7 @@ export class ClineRuntime {
|
|
|
160
160
|
workspaceRoot: opts.workspaceRoot,
|
|
161
161
|
cwd: opts.workspaceRoot,
|
|
162
162
|
enableTools: true,
|
|
163
|
-
enableSpawnAgent:
|
|
163
|
+
enableSpawnAgent: true,
|
|
164
164
|
enableAgentTeams: true,
|
|
165
165
|
...(execution ? { execution } : {}),
|
|
166
166
|
...(recovery ? { onConsecutiveMistakeLimitReached: recovery } : {}),
|
|
@@ -180,7 +180,7 @@ describe("ClineRuntime.startSession — agent teams plumbing (v6.2.0)", () => {
|
|
|
180
180
|
expect(cfg.enableAgentTeams).toBe(true);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
test("enableTools is true, enableSpawnAgent is
|
|
183
|
+
test("enableTools is true, enableSpawnAgent is true (subagents + task tool)", async () => {
|
|
184
184
|
const runtime = new ClineRuntime({ logger: stubLogger() });
|
|
185
185
|
(runtime as unknown as { core: unknown }).core = makeFakeCore();
|
|
186
186
|
await runtime.startSession({
|
|
@@ -191,7 +191,8 @@ describe("ClineRuntime.startSession — agent teams plumbing (v6.2.0)", () => {
|
|
|
191
191
|
});
|
|
192
192
|
const cfg = capturedConfig[0]!.config;
|
|
193
193
|
expect(cfg.enableTools).toBe(true);
|
|
194
|
-
expect(cfg.enableSpawnAgent).toBe(
|
|
194
|
+
expect(cfg.enableSpawnAgent).toBe(true,
|
|
195
|
+
"enableSpawnAgent must be true so Odin can use the `task` tool and `use_subagents`");
|
|
195
196
|
});
|
|
196
197
|
|
|
197
198
|
test("all three boolean flags survive even when caller passes no opts", async () => {
|
|
@@ -206,7 +207,27 @@ describe("ClineRuntime.startSession — agent teams plumbing (v6.2.0)", () => {
|
|
|
206
207
|
const cfg = capturedConfig[0]!.config;
|
|
207
208
|
expect(cfg.enableAgentTeams).toBe(true);
|
|
208
209
|
expect(cfg.enableTools).toBe(true);
|
|
209
|
-
expect(cfg.enableSpawnAgent).toBe(
|
|
210
|
+
expect(cfg.enableSpawnAgent).toBe(true,
|
|
211
|
+
"v6.2.3 — subagents + task tool must be available even without caller opts");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("ClineRuntime.startSession — subagents plumbing (v6.2.3)", () => {
|
|
216
|
+
test("enableSpawnAgent regression: must NEVER be false (v6.2.3 fix)", async () => {
|
|
217
|
+
// v6.2.3 flipped enableSpawnAgent from false → true. Before this
|
|
218
|
+
// change, Odin could not use the `task` tool or `use_subagents`,
|
|
219
|
+
// which silently broke subagent delegation. This test pins the fix.
|
|
220
|
+
const runtime = new ClineRuntime({ logger: stubLogger() });
|
|
221
|
+
(runtime as unknown as { core: unknown }).core = makeFakeCore();
|
|
222
|
+
await runtime.startSession({
|
|
223
|
+
providerId: "anthropic",
|
|
224
|
+
modelId: "claude-sonnet-4-6",
|
|
225
|
+
workspaceRoot: "/tmp",
|
|
226
|
+
prompt: "go",
|
|
227
|
+
});
|
|
228
|
+
const cfg = capturedConfig[0]!.config;
|
|
229
|
+
expect(cfg.enableSpawnAgent).not.toBe(false);
|
|
230
|
+
expect(cfg.enableSpawnAgent).toBe(true);
|
|
210
231
|
});
|
|
211
232
|
});
|
|
212
233
|
|