@polderlabs/bizar 6.2.2 → 6.2.4
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 +92 -0
- package/cli/commands/validate.test.mjs +37 -0
- package/config/agents/_shared/AGENT_BASELINE.md +40 -1
- package/config/agents/_shared/CLINE_TOOLS.md +398 -0
- package/config/agents/agent-browser.md +1 -1
- 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/package.json +2 -2
- package/plugins/bizar/package.json +17 -6
- package/plugins/bizar/src/clineruntime.ts +19 -4
- package/plugins/bizar/src/options.ts +14 -7
- package/plugins/bizar/tests/clineruntime-config.test.ts +69 -7
- package/plugins/bizar/tests/options.test.ts +6 -6
- package/scripts/bh-full-e2e.mjs +79 -4
- package/scripts/check-agents.mjs +73 -0
|
@@ -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();
|
|
@@ -301,6 +362,33 @@ const CHECKS = {
|
|
|
301
362
|
}
|
|
302
363
|
},
|
|
303
364
|
|
|
365
|
+
// v6.2.4 — Warn if the user's clineruntimeMaxConsecutiveMistakes is
|
|
366
|
+
// below the plugin's recommended minimum. Cline's CLI default is 3
|
|
367
|
+
// which aborts sessions on the 3rd tool mistake. The Bizar plugin
|
|
368
|
+
// recommends 10 (or `--retries N` with N ≥ 10).
|
|
369
|
+
'mistake-limit-floor': async () => {
|
|
370
|
+
const MIN = 10;
|
|
371
|
+
const cfg = readJsonSafe(clineJsonPath());
|
|
372
|
+
if (!cfg?.plugin || !Array.isArray(cfg.plugin)) {
|
|
373
|
+
return 'no plugin entries in cline.json — skip mistake-limit check';
|
|
374
|
+
}
|
|
375
|
+
const bizarEntry = cfg.plugin.find((p) => Array.isArray(p) && p[0] && p[0].includes('plugins/bizar'));
|
|
376
|
+
if (!bizarEntry) {
|
|
377
|
+
return 'Bizar plugin not registered — skip mistake-limit check';
|
|
378
|
+
}
|
|
379
|
+
const opts = (Array.isArray(bizarEntry) && bizarEntry[1]) || {};
|
|
380
|
+
const raw = opts.clineruntimeMaxConsecutiveMistakes;
|
|
381
|
+
if (typeof raw !== 'number' || raw >= MIN) {
|
|
382
|
+
return `clineruntimeMaxConsecutiveMistakes=${raw ?? 'default 10'} (≥ ${MIN}) — OK`;
|
|
383
|
+
}
|
|
384
|
+
throw new Error(
|
|
385
|
+
`clineruntimeMaxConsecutiveMistakes=${raw} is BELOW the recommended minimum (${MIN}). ` +
|
|
386
|
+
`Cline's default of 3 aborts the session on the 3rd tool mistake. ` +
|
|
387
|
+
`Edit ~/.cline/cline.json plugin[1].clineruntimeMaxConsecutiveMistakes to ${MIN}, or run ` +
|
|
388
|
+
`\`bizar install\` to reset to defaults. (See _shared/CLINE_TOOLS.md for the top-5 mistakes.)`,
|
|
389
|
+
);
|
|
390
|
+
},
|
|
391
|
+
|
|
304
392
|
'default-agent-set': async () => {
|
|
305
393
|
const cfg = readJsonSafe(clineJsonPath());
|
|
306
394
|
if (!cfg?.default_agent) {
|
|
@@ -371,12 +459,16 @@ const CHECK_ORDER = [
|
|
|
371
459
|
'default-agent-set',
|
|
372
460
|
'instructions-loaded',
|
|
373
461
|
'provider-config',
|
|
462
|
+
'cline-settings-provider',
|
|
374
463
|
'9router-reachable',
|
|
464
|
+
'mistake-limit-floor',
|
|
375
465
|
];
|
|
376
466
|
|
|
377
467
|
const LENIENT_CHECKS = new Set([
|
|
378
468
|
'9router-reachable',
|
|
379
469
|
'provider-config', // v6.2.2+ — installer no longer touches provider config; user must configure
|
|
470
|
+
'cline-settings-provider', // v6.2.3 — warns about legacy/fake providerIds; non-blocking
|
|
471
|
+
'mistake-limit-floor', // v6.2.4 — warns about low mistake limit; non-blocking
|
|
380
472
|
]);
|
|
381
473
|
|
|
382
474
|
export function showValidateHelp() {
|
|
@@ -304,4 +304,41 @@ describe('runValidate() — JSON output', () => {
|
|
|
304
304
|
const { exitCode } = await captureRun({ json: true, only: 'this-does-not-exist' });
|
|
305
305
|
assert.equal(exitCode, 2);
|
|
306
306
|
});
|
|
307
|
+
|
|
308
|
+
test('v6.2.4 — mistake-limit-floor warns when clineruntimeMaxConsecutiveMistakes is < 10', async () => {
|
|
309
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
310
|
+
cfg.plugin = [['./plugins/bizar', { clineruntimeMaxConsecutiveMistakes: 3 }]];
|
|
311
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
312
|
+
const { stdout, exitCode } = await captureRun({ json: true });
|
|
313
|
+
assert.equal(exitCode, 0, 'mistake-limit-floor is lenient so exit stays 0');
|
|
314
|
+
const parsed = JSON.parse(stdout);
|
|
315
|
+
const check = parsed.results.find((r) => r.name === 'mistake-limit-floor');
|
|
316
|
+
assert.ok(check, 'mistake-limit-floor check should be present');
|
|
317
|
+
assert.equal(check.ok, false, 'low mistake limit should warn');
|
|
318
|
+
assert.match(check.message, /BELOW the recommended minimum/);
|
|
319
|
+
assert.match(check.message, /clineruntimeMaxConsecutiveMistakes=3/);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('v6.2.4 — mistake-limit-floor OK when clineruntimeMaxConsecutiveMistakes >= 10', async () => {
|
|
323
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
324
|
+
cfg.plugin = [['./plugins/bizar', { clineruntimeMaxConsecutiveMistakes: 15 }]];
|
|
325
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
326
|
+
const { stdout, exitCode } = await captureRun({ json: true });
|
|
327
|
+
assert.equal(exitCode, 0);
|
|
328
|
+
const parsed = JSON.parse(stdout);
|
|
329
|
+
const check = parsed.results.find((r) => r.name === 'mistake-limit-floor');
|
|
330
|
+
assert.equal(check.ok, true);
|
|
331
|
+
assert.match(check.message, /15/);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('v6.2.4 — mistake-limit-floor OK when no plugin entry (legacy cline.json)', async () => {
|
|
335
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
336
|
+
delete cfg.plugin;
|
|
337
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
338
|
+
const { stdout } = await captureRun({ json: true });
|
|
339
|
+
const parsed = JSON.parse(stdout);
|
|
340
|
+
const check = parsed.results.find((r) => r.name === 'mistake-limit-floor');
|
|
341
|
+
assert.match(check.message, /no plugin entries/);
|
|
342
|
+
assert.equal(check.ok, true);
|
|
343
|
+
});
|
|
307
344
|
});
|
|
@@ -5,7 +5,19 @@ description: Always-on rules for every Bizar agent. Loaded automatically by clin
|
|
|
5
5
|
|
|
6
6
|
# Agent Baseline — Always-On Rules
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
> **v6.2.4 — Read `CLINE_TOOLS.md` first.** The Cline tools (`read_file`,
|
|
9
|
+
> `editor`, `apply_patch`, `ask_question`, `use_subagents`, `task`, …)
|
|
10
|
+
> have strict argument shapes. Passing the wrong shape — e.g. `options:
|
|
11
|
+
> null` on `ask_question` — silently fails and counts as a "mistake".
|
|
12
|
+
> After 3 mistakes (10 since v6.2.4) Cline aborts the session.
|
|
13
|
+
> See `_shared/CLINE_TOOLS.md` for the exact schemas.
|
|
14
|
+
|
|
15
|
+
Every Bizar agent follows these rules at all times. They are the
|
|
16
|
+
canonical agent baseline for the BizarHarness system — the Norse-pantheon
|
|
17
|
+
multi-agent system built on top of Cline. The rules below are tuned for
|
|
18
|
+
Bizar specifically: tool names reference Cline's built-in tools
|
|
19
|
+
(`read_file`, `editor`, `ask_question`, …) and Bizar's plugin tools
|
|
20
|
+
(`bizar_*`); storage paths reference `~/.cline/` and `~/.bizar_memory/`.
|
|
9
21
|
|
|
10
22
|
---
|
|
11
23
|
|
|
@@ -25,6 +37,33 @@ Every Bizar agent follows these rules at all times. They are translated from the
|
|
|
25
37
|
|
|
26
38
|
---
|
|
27
39
|
|
|
40
|
+
## 1a. Tool Mistakes — Don't Kill the Session
|
|
41
|
+
|
|
42
|
+
Cline's runtime counts consecutive tool failures (mistakes). When the
|
|
43
|
+
count hits the limit (3 in v6.2.0–v6.2.3; **10 since v6.2.4**), it aborts
|
|
44
|
+
the session with `max consecutive mistakes reached`. The most common
|
|
45
|
+
cause is calling a tool with the wrong argument shape.
|
|
46
|
+
|
|
47
|
+
**Top 5 mistakes that abort sessions — read this and avoid them:**
|
|
48
|
+
|
|
49
|
+
1. **`ask_question` with `options: null` or `options: undefined`.** The tool silently fails and counts as a mistake. Always pass an array of 2–5 strings.
|
|
50
|
+
2. **`editor` with non-matching `old_text`.** Whitespace must match exactly. Always `read_file` first and copy the bytes.
|
|
51
|
+
3. **`editor` with `old_text` that matches multiple places.** Make `old_text` more specific (include more surrounding context) so it matches exactly once.
|
|
52
|
+
4. **`execute_command` with shell redirects (`>`, `>>`).** Bizar blocks these by default. Use `editor` or `apply_patch` to write files.
|
|
53
|
+
5. **`use_subagents` with a single prompt.** Subagents are parallel research. Spawn 3–5 at once, not one at a time.
|
|
54
|
+
|
|
55
|
+
**Recovery: if you hit the mistake limit:**
|
|
56
|
+
|
|
57
|
+
- Stop retrying the same broken call — each retry wastes a mistake.
|
|
58
|
+
- Read `_shared/CLINE_TOOLS.md` for the exact schema.
|
|
59
|
+
- Spawn a fresh session via `bizar_spawn_background` if the runtime is stuck.
|
|
60
|
+
- Ask the user to abort and restart with `bizar doctor` + `bizar validate`.
|
|
61
|
+
|
|
62
|
+
The full reference is at `_shared/CLINE_TOOLS.md` (linked from every agent
|
|
63
|
+
file's `description` frontmatter).
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
28
67
|
## 2. Codebase Search — Use Semble First
|
|
29
68
|
|
|
30
69
|
Semble is the local code search tool — faster and more token-efficient than reading files directly.
|