@polderlabs/bizar 6.1.0 → 6.2.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/bizar-dash/DESIGN.md +18 -23
- package/bizar-dash/critique.json +6 -6
- package/cli/bin.mjs +19 -1
- package/cli/commands/validate.mjs +439 -0
- package/cli/commands/validate.test.mjs +279 -0
- package/cli/post-install-smoke.mjs +96 -3
- package/cli/post-install-smoke.test.mjs +3 -5
- package/cli/provision.mjs +97 -11
- package/config/cline.json.template +15 -0
- package/config/commands/team.md +124 -0
- package/config/commands/test.md +60 -0
- package/config/commands/validate.md +66 -0
- package/package.json +5 -4
- package/plugins/bizar/package.json +1 -1
- package/plugins/bizar/src/clineruntime.ts +1 -1
- package/plugins/bizar/tests/clineruntime-config.test.ts +44 -0
- package/scripts/bh-full-e2e.mjs +389 -0
- package/scripts/check-arch.sh +80 -0
- package/scripts/check-deps.mjs +510 -0
- package/scripts/clean-state-check.sh +81 -0
- package/scripts/git-hooks/pre-commit +59 -0
- package/scripts/git-hooks/pre-push +54 -0
- package/scripts/install-hooks.sh +29 -0
- package/scripts/install-service.mjs +190 -0
- package/scripts/session-trace.sh +52 -0
- package/scripts/verify-feature.sh +102 -0
- package/bizar-dash/image-1.png +0 -0
- package/bizar-dash/image-2.png +0 -0
- package/bizar-dash/image-3.png +0 -0
- package/bizar-dash/image-4.png +0 -0
- package/bizar-dash/image-5.png +0 -0
- package/bizar-dash/image-6.png +0 -0
- package/bizar-dash/image-7.png +0 -0
- package/bizar-dash/image.png +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/validate.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.2.0 — Unit tests for the new `bizar validate` subcommand.
|
|
5
|
+
*
|
|
6
|
+
* We exercise `runValidate()` directly. CLINE_DIR is mocked via
|
|
7
|
+
* `process.env.CLINE_DIR`; a minimal-but-complete Bizar install is
|
|
8
|
+
* staged inside a tmpdir. We then progressively delete files to
|
|
9
|
+
* verify each check fails with a sensible message.
|
|
10
|
+
*/
|
|
11
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { Writable } from 'node:stream';
|
|
17
|
+
|
|
18
|
+
const { runValidate } = await import('./validate.mjs');
|
|
19
|
+
|
|
20
|
+
const ORIG_CLINE_DIR = process.env.CLINE_DIR;
|
|
21
|
+
const ORIG_HOME = process.env.HOME;
|
|
22
|
+
|
|
23
|
+
let workDir;
|
|
24
|
+
|
|
25
|
+
function freshWorkDir() {
|
|
26
|
+
const root = mkdtempSync(join(tmpdir(), 'bizar-validate-'));
|
|
27
|
+
process.env.CLINE_DIR = root;
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeFakeClineInstall(root) {
|
|
32
|
+
writeFileSync(
|
|
33
|
+
join(root, 'cline.json'),
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
$schema: 'https://docs.cline.bot/config.json',
|
|
36
|
+
plugin: [[
|
|
37
|
+
'./plugins/bizar',
|
|
38
|
+
{ loopThresholdWarn: 5, loopThresholdEscalate: 8, loopThresholdBlock: 12, loopWindowSize: 10 },
|
|
39
|
+
]],
|
|
40
|
+
default_agent: 'odin',
|
|
41
|
+
permission: 'allow',
|
|
42
|
+
snapshot: false,
|
|
43
|
+
instructions: ['.cline/instructions/bizar-tools.md'],
|
|
44
|
+
provider: {
|
|
45
|
+
'9router': {
|
|
46
|
+
baseUrl: 'http://localhost:20128/v1',
|
|
47
|
+
apiKey: '${env:NINEROUTER_KEY}',
|
|
48
|
+
models: { 'minimax/MiniMax-M3': { reasoning: true } },
|
|
49
|
+
},
|
|
50
|
+
minimax: { models: {} },
|
|
51
|
+
},
|
|
52
|
+
}, null, 2),
|
|
53
|
+
);
|
|
54
|
+
const agentsDir = join(root, 'agents');
|
|
55
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
56
|
+
for (const f of [
|
|
57
|
+
'odin.md', 'vor.md', 'frigg.md', 'quick.md',
|
|
58
|
+
'mimir.md', 'heimdall.md', 'hermod.md', 'thor.md', 'baldr.md',
|
|
59
|
+
'tyr.md', 'vidarr.md', 'forseti.md',
|
|
60
|
+
'semble-search.md', 'agent-browser.md',
|
|
61
|
+
]) {
|
|
62
|
+
writeFileSync(join(agentsDir, f), '# fake agent');
|
|
63
|
+
}
|
|
64
|
+
writeFileSync(join(agentsDir, 'odin.yaml'), 'name: odin\ndescription: odin\n');
|
|
65
|
+
const cmdsDir = join(root, 'commands');
|
|
66
|
+
mkdirSync(cmdsDir, { recursive: true });
|
|
67
|
+
for (const f of [
|
|
68
|
+
'audit.md', 'bizar.md', 'explain.md', 'init.md', 'learn.md',
|
|
69
|
+
'plan.md', 'plow-through.md', 'pr-review.md', 'tailscale-serve.md',
|
|
70
|
+
'visual-plan.md',
|
|
71
|
+
'team.md', 'test.md', 'validate.md',
|
|
72
|
+
]) {
|
|
73
|
+
writeFileSync(join(cmdsDir, f), '# fake command');
|
|
74
|
+
}
|
|
75
|
+
const skillsDir = join(root, 'skills');
|
|
76
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
77
|
+
for (const skill of ['9router', 'bizar', 'obsidian']) {
|
|
78
|
+
mkdirSync(join(skillsDir, skill), { recursive: true });
|
|
79
|
+
writeFileSync(join(skillsDir, skill, 'SKILL.md'), '# fake skill');
|
|
80
|
+
}
|
|
81
|
+
const rulesDir = join(root, 'rules');
|
|
82
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
83
|
+
for (const r of ['general.md', 'git.md', 'javascript.md', 'python.md', 'testing.md', 'thinking.md', 'uncertainty.md']) {
|
|
84
|
+
writeFileSync(join(rulesDir, r), '# fake rule');
|
|
85
|
+
}
|
|
86
|
+
const hooksDir = join(root, 'hooks');
|
|
87
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
88
|
+
for (const h of ['pre-tool-use.md', 'post-tool-use.md', 'README.md']) {
|
|
89
|
+
writeFileSync(join(hooksDir, h), '# fake hook');
|
|
90
|
+
}
|
|
91
|
+
const pluginDir = join(root, 'plugins', 'bizar');
|
|
92
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
93
|
+
writeFileSync(join(pluginDir, 'index.ts'), '// fake plugin entry — >100 bytes to pass size check\n'.repeat(10));
|
|
94
|
+
mkdirSync(join(pluginDir, 'src'), { recursive: true });
|
|
95
|
+
writeFileSync(
|
|
96
|
+
join(pluginDir, 'src', 'clineruntime.ts'),
|
|
97
|
+
'export class ClineRuntime { config = { enableAgentTeams: true }; }\n',
|
|
98
|
+
);
|
|
99
|
+
// node_modules with the required runtime deps
|
|
100
|
+
const nmDir = join(pluginDir, 'node_modules');
|
|
101
|
+
mkdirSync(nmDir, { recursive: true });
|
|
102
|
+
mkdirSync(join(nmDir, 'zod'), { recursive: true });
|
|
103
|
+
writeFileSync(join(nmDir, 'zod', 'package.json'), '{}');
|
|
104
|
+
mkdirSync(join(nmDir, '@cline'), { recursive: true });
|
|
105
|
+
for (const dep of ['@cline/sdk', '@cline/core', '@cline/shared']) {
|
|
106
|
+
mkdirSync(join(nmDir, dep), { recursive: true });
|
|
107
|
+
writeFileSync(join(nmDir, dep, 'package.json'), '{}');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
workDir = freshWorkDir();
|
|
113
|
+
makeFakeClineInstall(workDir);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
if (workDir && existsSync(workDir)) rmSync(workDir, { recursive: true, force: true });
|
|
118
|
+
if (ORIG_CLINE_DIR === undefined) delete process.env.CLINE_DIR;
|
|
119
|
+
else process.env.CLINE_DIR = ORIG_CLINE_DIR;
|
|
120
|
+
if (ORIG_HOME === undefined) delete process.env.HOME;
|
|
121
|
+
else process.env.HOME = ORIG_HOME;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Capture stdout + exit code for a single runValidate() call.
|
|
126
|
+
* The test runner writes its own progress to stdout, so we swap
|
|
127
|
+
* process.stdout for a sink Writable during the call.
|
|
128
|
+
*/
|
|
129
|
+
async function captureRun(opts) {
|
|
130
|
+
const captured = [];
|
|
131
|
+
const sink = new Writable({
|
|
132
|
+
write(chunk, _enc, cb) {
|
|
133
|
+
captured.push(Buffer.from(chunk));
|
|
134
|
+
cb();
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const origStdoutDescriptor = Object.getOwnPropertyDescriptor(process, 'stdout');
|
|
138
|
+
const origExit = process.exit;
|
|
139
|
+
let exitCode = null;
|
|
140
|
+
Object.defineProperty(process, 'stdout', { value: sink, configurable: true, writable: true });
|
|
141
|
+
process.exit = (code) => { exitCode = code; };
|
|
142
|
+
try {
|
|
143
|
+
await runValidate(opts);
|
|
144
|
+
} finally {
|
|
145
|
+
if (origStdoutDescriptor) Object.defineProperty(process, 'stdout', origStdoutDescriptor);
|
|
146
|
+
process.exit = origExit;
|
|
147
|
+
}
|
|
148
|
+
return { stdout: Buffer.concat(captured).toString('utf8'), exitCode };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
describe('runValidate() — JSON output', () => {
|
|
152
|
+
test('returns structured JSON with passed/failed/results', async () => {
|
|
153
|
+
const { stdout, exitCode } = await captureRun({ json: true });
|
|
154
|
+
assert.equal(exitCode, 0, 'should exit 0 on success');
|
|
155
|
+
const parsed = JSON.parse(stdout);
|
|
156
|
+
assert.equal(typeof parsed.passed, 'number');
|
|
157
|
+
assert.equal(typeof parsed.failed, 'number');
|
|
158
|
+
assert.ok(Array.isArray(parsed.results));
|
|
159
|
+
assert.ok(parsed.results.length >= 18, 'expected at least 18 checks');
|
|
160
|
+
for (const r of parsed.results) {
|
|
161
|
+
assert.ok(typeof r.name === 'string');
|
|
162
|
+
assert.ok(typeof r.ok === 'boolean');
|
|
163
|
+
assert.ok(typeof r.message === 'string');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('fails on missing team command', async () => {
|
|
168
|
+
rmSync(join(workDir, 'commands', 'team.md'));
|
|
169
|
+
const { exitCode } = await captureRun({ json: true });
|
|
170
|
+
assert.equal(exitCode, 1, 'should exit 1 on team-command-present failure');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('fails on missing test command', async () => {
|
|
174
|
+
rmSync(join(workDir, 'commands', 'test.md'));
|
|
175
|
+
const { exitCode } = await captureRun({ json: true });
|
|
176
|
+
assert.equal(exitCode, 1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('fails on missing validate command', async () => {
|
|
180
|
+
rmSync(join(workDir, 'commands', 'validate.md'));
|
|
181
|
+
const { exitCode } = await captureRun({ json: true });
|
|
182
|
+
assert.equal(exitCode, 1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('fails on missing agent file', async () => {
|
|
186
|
+
rmSync(join(workDir, 'agents', 'mimir.md'));
|
|
187
|
+
const { exitCode } = await captureRun({ json: true });
|
|
188
|
+
assert.equal(exitCode, 1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('fails when cline.json missing', async () => {
|
|
192
|
+
rmSync(join(workDir, 'cline.json'));
|
|
193
|
+
const { exitCode } = await captureRun({ json: true });
|
|
194
|
+
assert.equal(exitCode, 1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('fails when enableAgentTeams is false in clineruntime.ts', async () => {
|
|
198
|
+
writeFileSync(
|
|
199
|
+
join(workDir, 'plugins', 'bizar', 'src', 'clineruntime.ts'),
|
|
200
|
+
'export class ClineRuntime { config = { enableAgentTeams: false }; }\n',
|
|
201
|
+
);
|
|
202
|
+
const { exitCode } = await captureRun({ json: true });
|
|
203
|
+
assert.equal(exitCode, 1, 'should fail when enableAgentTeams is false');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('fails when provider config is missing', async () => {
|
|
207
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
208
|
+
delete cfg.provider;
|
|
209
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
210
|
+
const { exitCode } = await captureRun({ json: true });
|
|
211
|
+
assert.equal(exitCode, 1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('passes when only 9router is configured (no minimax)', async () => {
|
|
215
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
216
|
+
delete cfg.provider.minimax;
|
|
217
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
218
|
+
const { exitCode } = await captureRun({ json: true });
|
|
219
|
+
assert.equal(exitCode, 0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('passes when only minimax is configured (no 9router)', async () => {
|
|
223
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
224
|
+
delete cfg.provider['9router'];
|
|
225
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
226
|
+
const { exitCode } = await captureRun({ json: true });
|
|
227
|
+
assert.equal(exitCode, 0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('default mode is lenient on 9router-unreachable', async () => {
|
|
231
|
+
const { stdout, exitCode } = await captureRun({ json: true });
|
|
232
|
+
const parsed = JSON.parse(stdout);
|
|
233
|
+
const nineR = parsed.results.find((r) => r.name === '9router-reachable');
|
|
234
|
+
assert.ok(nineR, '9router-reachable check should be present');
|
|
235
|
+
if (!nineR.ok) {
|
|
236
|
+
// 9router is not running. Default mode must be lenient.
|
|
237
|
+
assert.equal(exitCode, 0, 'default mode should be lenient when 9router is down');
|
|
238
|
+
} else {
|
|
239
|
+
// 9router is running (some test envs have it). Just confirm the
|
|
240
|
+
// check is in the list and exit code is sane.
|
|
241
|
+
assert.equal(exitCode, 0, 'expected exit 0 when all critical checks pass');
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('--strict fails when 9router is unreachable', async () => {
|
|
246
|
+
// We can't easily kill 9router in a test, but we can prove that
|
|
247
|
+
// --strict mode would fail if 9router were down: set NINEROUTER_URL
|
|
248
|
+
// to a port nothing is listening on.
|
|
249
|
+
const origUrl = process.env.NINEROUTER_URL;
|
|
250
|
+
process.env.NINEROUTER_URL = 'http://127.0.0.1:1'; // nothing here
|
|
251
|
+
try {
|
|
252
|
+
const { exitCode } = await captureRun({ json: true, strict: true });
|
|
253
|
+
assert.equal(exitCode, 1, 'strict mode should fail when 9router is unreachable');
|
|
254
|
+
} finally {
|
|
255
|
+
if (origUrl === undefined) delete process.env.NINEROUTER_URL;
|
|
256
|
+
else process.env.NINEROUTER_URL = origUrl;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('--only filters to a single check', async () => {
|
|
261
|
+
const { stdout, exitCode } = await captureRun({ json: true, only: 'team-command-present' });
|
|
262
|
+
assert.equal(exitCode, 0);
|
|
263
|
+
const parsed = JSON.parse(stdout);
|
|
264
|
+
assert.equal(parsed.results.length, 1);
|
|
265
|
+
assert.equal(parsed.results[0].name, 'team-command-present');
|
|
266
|
+
assert.equal(parsed.results[0].ok, true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('--only on a missing file fails with exit 1', async () => {
|
|
270
|
+
rmSync(join(workDir, 'commands', 'team.md'));
|
|
271
|
+
const { exitCode } = await captureRun({ json: true, only: 'team-command-present' });
|
|
272
|
+
assert.equal(exitCode, 1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('--only with an unknown name exits 2', async () => {
|
|
276
|
+
const { exitCode } = await captureRun({ json: true, only: 'this-does-not-exist' });
|
|
277
|
+
assert.equal(exitCode, 2);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* Verifies the installation is functional by checking:
|
|
7
7
|
* 1. Memory vault exists and is git-initialized
|
|
8
8
|
* 2. `bizar doctor` exits 0
|
|
9
|
-
* 3.
|
|
10
|
-
* 4.
|
|
9
|
+
* 3. Dashboard HTTP responds 200
|
|
10
|
+
* 4. WebSocket connects to dashboard
|
|
11
|
+
* 5. `bizar bg list` returns (even if empty)
|
|
12
|
+
* 6. lightrag-server installed
|
|
11
13
|
*
|
|
12
14
|
* Each check has a 30s timeout. Exits 0 if all pass, 1 otherwise.
|
|
13
15
|
*
|
|
@@ -27,6 +29,7 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
27
29
|
const HOME = homedir();
|
|
28
30
|
const DEFAULT_BIZAR_HOME = join(HOME, '.config', 'bizar');
|
|
29
31
|
const DEFAULT_MEMORY_VAULT = join(HOME, '.bizar_memory');
|
|
32
|
+
const DEFAULT_DASHBOARD_PORT = 4097;
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* @param {object} opts
|
|
@@ -37,6 +40,7 @@ const DEFAULT_MEMORY_VAULT = join(HOME, '.bizar_memory');
|
|
|
37
40
|
export async function runSmokeTest({ bizarHome, repoPath, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
38
41
|
const bh = bizarHome || DEFAULT_BIZAR_HOME;
|
|
39
42
|
const mv = process.env.BIZAR_MEMORY_VAULT || DEFAULT_MEMORY_VAULT;
|
|
43
|
+
const port = parseInt(process.env.BIZAR_DASHBOARD_PORT || String(DEFAULT_DASHBOARD_PORT), 10) || DEFAULT_DASHBOARD_PORT;
|
|
40
44
|
const checks = [];
|
|
41
45
|
|
|
42
46
|
function check(name, fn) {
|
|
@@ -77,7 +81,96 @@ export async function runSmokeTest({ bizarHome, repoPath, timeoutMs = DEFAULT_TI
|
|
|
77
81
|
}
|
|
78
82
|
});
|
|
79
83
|
|
|
80
|
-
// ── 3.
|
|
84
|
+
// ── 3. Dashboard HTTP responds 200 (with retry) ─────────────────────────────────
|
|
85
|
+
check('dashboard HTTP', async () => {
|
|
86
|
+
const maxAttempts = 5;
|
|
87
|
+
const delayMs = 2000;
|
|
88
|
+
let lastErr = null;
|
|
89
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
90
|
+
try {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`http://127.0.0.1:${port}/`, {
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
method: 'GET',
|
|
97
|
+
});
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
if (attempt > 1) {
|
|
101
|
+
return { ok: true, message: `HTTP ${res.status} on port ${port} (tried ${attempt}x)` };
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, message: `HTTP ${res.status} on port ${port}` };
|
|
104
|
+
}
|
|
105
|
+
lastErr = `HTTP ${res.status} on port ${port} (expected 200)`;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
lastErr = err.name === 'AbortError'
|
|
109
|
+
? `dashboard not responding on port ${port} (timeout)`
|
|
110
|
+
: `dashboard unreachable on port ${port}: ${err.message}`;
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
lastErr = `fetch not available: ${err.message}`;
|
|
114
|
+
}
|
|
115
|
+
if (attempt < maxAttempts) {
|
|
116
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { ok: false, message: lastErr || 'dashboard HTTP failed' };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── 4. WebSocket connects (with retry) ────────────────────────────────────────
|
|
123
|
+
check('dashboard WebSocket', async () => {
|
|
124
|
+
const maxAttempts = 5;
|
|
125
|
+
const delayMs = 2000;
|
|
126
|
+
let lastErr = null;
|
|
127
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
128
|
+
try {
|
|
129
|
+
const { WebSocket } = await import('ws' in globalThis
|
|
130
|
+
? { ws: globalThis.ws, WebSocket: globalThis.WebSocket }
|
|
131
|
+
: await import(`ws`).then(m => ({ ws: m, WebSocket: m.WebSocket || m.default }))
|
|
132
|
+
);
|
|
133
|
+
const url = `ws://127.0.0.1:${port}/ws`;
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
136
|
+
try {
|
|
137
|
+
const ws = new WebSocket(url);
|
|
138
|
+
await new Promise((resolve, reject) => {
|
|
139
|
+
ws.on('open', resolve);
|
|
140
|
+
ws.on('error', reject);
|
|
141
|
+
ws.on('close', () => reject(new Error('closed before open')));
|
|
142
|
+
controller.signal.addEventListener('abort', () => {
|
|
143
|
+
ws.close();
|
|
144
|
+
reject(new Error('timeout'));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
ws.close();
|
|
149
|
+
if (attempt > 1) {
|
|
150
|
+
return { ok: true, message: `WebSocket connected at ${url} (tried ${attempt}x)` };
|
|
151
|
+
}
|
|
152
|
+
return { ok: true, message: `WebSocket connected at ${url}` };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
lastErr = err.message === 'timeout'
|
|
156
|
+
? `WebSocket timeout on ${url}`
|
|
157
|
+
: `WebSocket failed on ${url}: ${err.message}`;
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
// ws module not available in this environment — skip
|
|
161
|
+
if (err.code === 'MODULE_NOT_FOUND' || err.message?.includes('ws')) {
|
|
162
|
+
return { ok: true, message: 'ws module not available — skipping WebSocket check' };
|
|
163
|
+
}
|
|
164
|
+
lastErr = `WebSocket check failed: ${err.message}`;
|
|
165
|
+
}
|
|
166
|
+
if (attempt < maxAttempts) {
|
|
167
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { ok: false, message: lastErr || 'WebSocket failed' };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── 5. `bizar bg list` returns ────────────────────────────────────────────
|
|
81
174
|
check('background agents', () => {
|
|
82
175
|
try {
|
|
83
176
|
execSync('bizar bg list', {
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Tests:
|
|
7
7
|
* 1. lightrag-server check: file existence is verified
|
|
8
|
-
* 2.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (the dashboard is optional and the checks were failing noisily on
|
|
12
|
-
* systems that don't run it). See PROGRESS.md.
|
|
8
|
+
* 2. HTTP retry: retries on failure, succeeds when server eventually responds
|
|
9
|
+
* 3. WS retry: retries on failure, succeeds when server eventually responds
|
|
10
|
+
* 4. lightrag: warns when not found in PATH or ~/.local/bin/
|
|
13
11
|
*/
|
|
14
12
|
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
15
13
|
import assert from 'node:assert/strict';
|
package/cli/provision.mjs
CHANGED
|
@@ -860,8 +860,46 @@ export async function patchClineJson({ dryRun, force }) {
|
|
|
860
860
|
(p) => Array.isArray(p) && typeof p[0] === 'string' && p[0].includes('plugins/bizar'),
|
|
861
861
|
);
|
|
862
862
|
|
|
863
|
-
//
|
|
864
|
-
//
|
|
863
|
+
// v6.2.0 — Multi-patch installer. On every install/update, ensure the
|
|
864
|
+
// full Bizar config surface is present in cline.json:
|
|
865
|
+
//
|
|
866
|
+
// 1. provider.9router (preferred gateway since v6.0.1) — add if
|
|
867
|
+
// missing so the install is "complete" out of the box.
|
|
868
|
+
// 2. provider.minimax (legacy fallback) — kept for back-compat.
|
|
869
|
+
// 3. default_agent — set to "odin" if missing.
|
|
870
|
+
// 4. $schema — add if missing (helps editors validate).
|
|
871
|
+
// 5. instructions — point at .cline/instructions/bizar-tools.md if
|
|
872
|
+
// missing, so Cline loads the tool reference on every session.
|
|
873
|
+
// 6. plugin entry — add if missing (the critical one; without this
|
|
874
|
+
// the Bizar plugin never loads).
|
|
875
|
+
// 7. permissions — set to "allow" if missing.
|
|
876
|
+
// 8. snapshot — set to false if missing.
|
|
877
|
+
//
|
|
878
|
+
// All patches are additive and idempotent. We never overwrite a value
|
|
879
|
+
// the user has set; the `force` flag bypasses that protection for the
|
|
880
|
+
// plugin entry only.
|
|
881
|
+
let addedProvider = false;
|
|
882
|
+
let addedDefaultAgent = false;
|
|
883
|
+
let addedSchema = false;
|
|
884
|
+
let addedInstructions = false;
|
|
885
|
+
let addedPermissions = false;
|
|
886
|
+
let addedSnapshot = false;
|
|
887
|
+
let added9router = false;
|
|
888
|
+
|
|
889
|
+
if (!cfg.provider) cfg.provider = {};
|
|
890
|
+
|
|
891
|
+
// 1. provider.9router — preferred gateway.
|
|
892
|
+
if (!cfg.provider['9router'] && existsSync(join(REPO_ROOT, 'config', 'cline.json'))) {
|
|
893
|
+
try {
|
|
894
|
+
const tpl = JSON.parse(readFileSync(join(REPO_ROOT, 'config', 'cline.json'), 'utf8'));
|
|
895
|
+
if (tpl.provider && tpl.provider['9router']) {
|
|
896
|
+
cfg.provider['9router'] = tpl.provider['9router'];
|
|
897
|
+
added9router = true;
|
|
898
|
+
}
|
|
899
|
+
} catch { /* ignore template parse errors */ }
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// 2. provider.minimax — legacy fallback.
|
|
865
903
|
const DEFAULT_MINIMAX_BLOCK = {
|
|
866
904
|
options: {
|
|
867
905
|
baseURL: 'https://api.minimax.io/v1',
|
|
@@ -874,21 +912,59 @@ export async function patchClineJson({ dryRun, force }) {
|
|
|
874
912
|
'MiniMax-M3-Reasoning': { name: 'MiniMax M3 Reasoning', interleaved: { field: 'reasoning_details' }, reasoning: true },
|
|
875
913
|
},
|
|
876
914
|
};
|
|
877
|
-
let addedProvider = false;
|
|
878
|
-
if (!cfg.provider) {
|
|
879
|
-
cfg.provider = {};
|
|
880
|
-
}
|
|
881
915
|
if (!cfg.provider.minimax) {
|
|
882
916
|
cfg.provider.minimax = DEFAULT_MINIMAX_BLOCK;
|
|
883
917
|
addedProvider = true;
|
|
884
918
|
}
|
|
885
919
|
|
|
886
|
-
|
|
920
|
+
// 3. default_agent
|
|
921
|
+
if (!cfg.default_agent) {
|
|
922
|
+
cfg.default_agent = 'odin';
|
|
923
|
+
addedDefaultAgent = true;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 4. $schema
|
|
927
|
+
if (!cfg.$schema) {
|
|
928
|
+
cfg.$schema = 'https://docs.cline.bot/config.json';
|
|
929
|
+
addedSchema = true;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// 5. instructions — point at the bundled Bizar tools reference.
|
|
933
|
+
if (!cfg.instructions || (Array.isArray(cfg.instructions) && cfg.instructions.length === 0)) {
|
|
934
|
+
cfg.instructions = ['.cline/instructions/bizar-tools.md'];
|
|
935
|
+
addedInstructions = true;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// 6. permission
|
|
939
|
+
if (!cfg.permission) {
|
|
940
|
+
cfg.permission = 'allow';
|
|
941
|
+
addedPermissions = true;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// 7. snapshot
|
|
945
|
+
if (typeof cfg.snapshot !== 'boolean') {
|
|
946
|
+
cfg.snapshot = false;
|
|
947
|
+
addedSnapshot = true;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const anyAdded = addedProvider || added9router || addedDefaultAgent || addedSchema
|
|
951
|
+
|| addedInstructions || addedPermissions || addedSnapshot;
|
|
952
|
+
|
|
953
|
+
if (hasEntry && !force && !anyAdded) {
|
|
887
954
|
return { ok: true, message: 'cline.json already has Bizar plugin entry' };
|
|
888
955
|
}
|
|
889
956
|
|
|
890
957
|
if (dryRun) {
|
|
891
|
-
|
|
958
|
+
const added = [];
|
|
959
|
+
if (!hasEntry) added.push('plugin entry');
|
|
960
|
+
if (added9router) added.push('provider.9router');
|
|
961
|
+
if (addedProvider) added.push('provider.minimax');
|
|
962
|
+
if (addedDefaultAgent) added.push('default_agent');
|
|
963
|
+
if (addedSchema) added.push('$schema');
|
|
964
|
+
if (addedInstructions) added.push('instructions');
|
|
965
|
+
if (addedPermissions) added.push('permission');
|
|
966
|
+
if (addedSnapshot) added.push('snapshot');
|
|
967
|
+
return { ok: true, message: `[dry-run] would patch cline.json with: ${added.join(', ')}` };
|
|
892
968
|
}
|
|
893
969
|
|
|
894
970
|
if (!hasEntry) {
|
|
@@ -904,16 +980,26 @@ export async function patchClineJson({ dryRun, force }) {
|
|
|
904
980
|
loopThresholdEscalate: 8,
|
|
905
981
|
loopThresholdBlock: 12,
|
|
906
982
|
loopWindowSize: 10,
|
|
983
|
+
clineruntimeMaxConsecutiveMistakes: 6,
|
|
907
984
|
}]);
|
|
908
985
|
cfg.plugin = plugins;
|
|
909
986
|
}
|
|
910
987
|
|
|
911
988
|
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
989
|
+
const added = [];
|
|
990
|
+
if (!hasEntry) added.push('plugin entry');
|
|
991
|
+
if (added9router) added.push('provider.9router');
|
|
992
|
+
if (addedProvider) added.push('provider.minimax');
|
|
993
|
+
if (addedDefaultAgent) added.push('default_agent');
|
|
994
|
+
if (addedSchema) added.push('$schema');
|
|
995
|
+
if (addedInstructions) added.push('instructions');
|
|
996
|
+
if (addedPermissions) added.push('permission');
|
|
997
|
+
if (addedSnapshot) added.push('snapshot');
|
|
912
998
|
return {
|
|
913
999
|
ok: true,
|
|
914
|
-
message:
|
|
915
|
-
?
|
|
916
|
-
: 'cline.json
|
|
1000
|
+
message: added.length > 0
|
|
1001
|
+
? `cline.json patched: ${added.join(', ')}`
|
|
1002
|
+
: 'cline.json already complete',
|
|
917
1003
|
};
|
|
918
1004
|
}
|
|
919
1005
|
|
|
@@ -317,6 +317,21 @@
|
|
|
317
317
|
"description": "Bizar Plugin Menu.",
|
|
318
318
|
"agent": "odin",
|
|
319
319
|
"template": "commands-bizar/bizar.md\n\n$ARGUMENTS"
|
|
320
|
+
},
|
|
321
|
+
"team": {
|
|
322
|
+
"description": "Spawn a Cline agent team — coordinate Thor, Tyr, Mimir, Hermod, and Forseti to complete a complex mission end-to-end.",
|
|
323
|
+
"agent": "odin",
|
|
324
|
+
"template": "commands-bizar/team.md\n\n$ARGUMENTS"
|
|
325
|
+
},
|
|
326
|
+
"test": {
|
|
327
|
+
"description": "Run the project's test suite via the Bizar test gate (auto-detects jest/vitest/bun/pytest/cargo/go).",
|
|
328
|
+
"agent": "thor",
|
|
329
|
+
"template": "commands-bizar/test.md\n\n$ARGUMENTS"
|
|
330
|
+
},
|
|
331
|
+
"validate": {
|
|
332
|
+
"description": "Validate the Bizar install: cline.json, plugin entry, agents, skills, hooks, rules, commands, and provider config.",
|
|
333
|
+
"agent": "heimdall",
|
|
334
|
+
"template": "commands-bizar/validate.md\n\n$ARGUMENTS"
|
|
320
335
|
}
|
|
321
336
|
},
|
|
322
337
|
"provider": {
|