braintrust-lite 0.1.7 → 0.1.9

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/bin/consult DELETED
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- // CLI entry point for braintrust-lite.
5
- // Usage: consult [options] "prompt"
6
- // cat file | consult "review this"
7
-
8
- import { readFileSync } from 'fs';
9
- import { resolve } from 'path';
10
- import { consult } from '../src/consult.js';
11
- import { formatAsMarkdown, formatAsJson } from '../src/format.js';
12
-
13
- // ─── Arg parsing ──────────────────────────────────────────────────────────────
14
-
15
- const flags = { skip: [] };
16
- const positional = [];
17
- const argv = process.argv.slice(2);
18
-
19
- for (let i = 0; i < argv.length; i++) {
20
- const a = argv[i];
21
- if (a === '--skip') { flags.skip.push(argv[++i]); continue; }
22
- if (a === '--only') { flags.only = argv[++i]; continue; }
23
- if (a === '--timeout') { flags.timeout = Number(argv[++i]); continue; }
24
- if (a === '--dir') { flags.dir = argv[++i]; continue; }
25
- if (a === '--json') { flags.json = true; continue; }
26
- if (a === '--help' || a === '-h') { printHelp(); process.exit(0); }
27
- positional.push(a);
28
- }
29
-
30
- function printHelp() {
31
- console.error(`Usage: consult [options] "your question"
32
- cat file | consult "explain this"
33
-
34
- Options:
35
- --only <model> Only run one model: codex | gemini | claude
36
- --skip <model> Skip a model (repeatable)
37
- --timeout <sec> Per-model timeout in seconds (default: 90)
38
- --dir <path> Working directory for CLI subprocesses
39
- --json Output full JSON instead of markdown
40
- --help Show this help`);
41
- }
42
-
43
- // ─── Prompt ───────────────────────────────────────────────────────────────────
44
-
45
- let prompt = positional.join(' ');
46
-
47
- if (!process.stdin.isTTY) {
48
- const stdin = readFileSync(0, 'utf8').trim();
49
- if (stdin) prompt = prompt ? `${prompt}\n\n<context>\n${stdin}\n</context>` : stdin;
50
- }
51
-
52
- if (!prompt) {
53
- printHelp();
54
- process.exit(1);
55
- }
56
-
57
- // ─── Run ─────────────────────────────────────────────────────────────────────
58
-
59
- const { results, mapping } = await consult({
60
- prompt,
61
- only: flags.only,
62
- skip: flags.skip,
63
- timeoutMs: flags.timeout != null ? flags.timeout * 1000 : 90_000,
64
- cwd: flags.dir ? resolve(flags.dir) : undefined,
65
- blind: !flags.only, // blind mode only useful when multiple providers run
66
- });
67
-
68
- // Progress summary to stderr
69
- for (const r of results) {
70
- const status = r.error ? `⚠ ${r.error}` : `✓ ${(r.duration_ms / 1000).toFixed(1)}s`;
71
- process.stderr.write(`[${r.provider}: ${status}]\n`);
72
- }
73
-
74
- // Output to stdout
75
- if (flags.json) {
76
- console.log(formatAsJson(prompt, results, mapping));
77
- } else {
78
- console.log('\n' + formatAsMarkdown(results, mapping));
79
- }
package/scripts/setup.js DELETED
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env node
2
- import { mkdirSync, writeFileSync, existsSync, readFileSync, copyFileSync } from 'fs';
3
- import { join, dirname } from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { spawnSync } from 'child_process';
6
- import https from 'https';
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const SKILL_DIR = join(process.env.HOME || '/tmp', '.claude', 'skills', 'consult');
10
- const SKILL_PATH = join(SKILL_DIR, 'SKILL.md');
11
- const SKILL_URL = 'https://raw.githubusercontent.com/HongjieRen/braintrust-lite/main/skills/consult/SKILL.md';
12
- const G = '\x1b[32m✓\x1b[0m', R = '\x1b[31m✗\x1b[0m';
13
-
14
- function fetch(url) {
15
- return new Promise((resolve, reject) => {
16
- const req = https.get(url, { timeout: 10000 }, res => {
17
- if (res.statusCode === 301 || res.statusCode === 302) return fetch(res.headers.location).then(resolve).catch(reject);
18
- if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
19
- let body = ''; res.on('data', d => { body += d; }); res.on('end', () => resolve(body));
20
- });
21
- req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
22
- });
23
- }
24
-
25
- function getVersion(p) { try { return (readFileSync(p,'utf8').match(/^version:\s*(.+)$/m)||[])[1]?.trim(); } catch { return null; } }
26
-
27
- console.log('\nbraintrust-lite setup\n');
28
- console.log('Installing consult skill:');
29
- mkdirSync(SKILL_DIR, { recursive: true });
30
- if (existsSync(SKILL_PATH)) copyFileSync(SKILL_PATH, SKILL_PATH + '.bak');
31
-
32
- const bundled = join(__dirname, '..', 'skills', 'consult', 'SKILL.md');
33
- if (existsSync(bundled)) {
34
- copyFileSync(bundled, SKILL_PATH);
35
- console.log(` ${G} SKILL.md installed (v${getVersion(SKILL_PATH)})`);
36
- } else {
37
- try {
38
- const content = await fetch(SKILL_URL);
39
- writeFileSync(SKILL_PATH, content, 'utf8');
40
- console.log(` ${G} SKILL.md downloaded from GitHub (v${getVersion(SKILL_PATH)})`);
41
- } catch(e) {
42
- console.log(` ${R} SKILL.md download failed: ${e.message}`);
43
- process.exitCode = 1;
44
- }
45
- }
46
-
47
- console.log('\nRegistering MCP server:');
48
- const claudeOk = spawnSync('claude', ['--version'], { timeout: 5000 }).status === 0;
49
- if (!claudeOk) {
50
- console.log(` ${R} claude CLI not found — install Claude Code first`);
51
- console.log(` Then: claude mcp add braintrust-lite -- npx -y braintrust-lite@~0.1`);
52
- process.exitCode = 1;
53
- } else {
54
- const list = spawnSync('claude', ['mcp', 'list'], { encoding: 'utf8', timeout: 5000 });
55
- if ((list.stdout || '').includes('braintrust-lite')) {
56
- console.log(` ${G} MCP server already registered`);
57
- } else {
58
- const r = spawnSync('claude', ['mcp', 'add', 'braintrust-lite', '--', 'npx', '-y', 'braintrust-lite@~0.1'], { encoding: 'utf8', timeout: 10000 });
59
- if (r.status === 0) console.log(` ${G} MCP server registered (npx braintrust-lite@~0.1)`);
60
- else { console.log(` ${R} MCP registration failed`); console.log(` Manual: claude mcp add braintrust-lite -- npx -y braintrust-lite@~0.1`); process.exitCode = 1; }
61
- }
62
- }
63
-
64
- console.log();
65
- console.log(process.exitCode ? ' \x1b[33mSetup done with warnings — fix issues above.\x1b[0m' : ' \x1b[32mDone! Restart Claude Code, then use /consult.\x1b[0m');
66
- console.log();
package/src/consult.js DELETED
@@ -1,81 +0,0 @@
1
- import {
2
- runProcess,
3
- adaptCodex, adaptGemini, adaptClaude,
4
- CODEX_ARGS_PREFIX, GEMINI_ARGS_PREFIX, CLAUDE_ARGS_PREFIX,
5
- } from './providers.js';
6
-
7
- const SYSTEM_PROMPT = `你是一个独立思考的高级专家。请基于自己的判断给出高质量、可执行的回答。
8
- 要求:独立思考,不假设其他专家会补充;区分结论、依据、假设、风险;简洁但完整。`;
9
-
10
- const PROVIDERS = {
11
- codex: { cmd: 'codex', buildArgs: p => [...CODEX_ARGS_PREFIX, `${SYSTEM_PROMPT}\n\n${p}`], adapt: adaptCodex },
12
- gemini: { cmd: 'gemini', buildArgs: p => ['-p', `${SYSTEM_PROMPT}\n\n${p}`, ...GEMINI_ARGS_PREFIX], adapt: adaptGemini },
13
- claude: { cmd: 'claude', buildArgs: p => [...CLAUDE_ARGS_PREFIX, `${SYSTEM_PROMPT}\n\n${p}`], adapt: adaptClaude },
14
- };
15
-
16
- function shuffle(arr) {
17
- for (let i = arr.length - 1; i > 0; i--) {
18
- const j = Math.floor(Math.random() * (i + 1));
19
- [arr[i], arr[j]] = [arr[j], arr[i]];
20
- }
21
- return arr;
22
- }
23
-
24
- function anonymize(results) {
25
- const labels = ['Model A', 'Model B', 'Model C', 'Model D', 'Model E'];
26
- const shuffled = shuffle([...results]);
27
- const mapping = {};
28
- const anonymized = shuffled.map((r, i) => {
29
- mapping[labels[i]] = r.provider;
30
- return { ...r, provider: labels[i] };
31
- });
32
- return { results: anonymized, mapping };
33
- }
34
-
35
- async function runOne(name, prompt, { cwd, timeoutMs }) {
36
- const p = PROVIDERS[name];
37
- const start = Date.now();
38
- const raw = await runProcess(p.cmd, p.buildArgs(prompt), { cwd, timeoutMs });
39
- const duration_ms = Date.now() - start;
40
-
41
- const error_type = raw.error_type || null;
42
- const error = error_type === 'enoent' ? 'not installed'
43
- : error_type === 'timeout' ? 'timeout'
44
- : error_type === 'nonzero' ? `exit ${raw.code}`
45
- : error_type ? error_type
46
- : null;
47
-
48
- const { content } = error ? { content: '' } : p.adapt(raw);
49
- return { provider: name, content, duration_ms, error, error_type };
50
- }
51
-
52
- /**
53
- * Consult Codex, Gemini, and/or Claude in parallel.
54
- * Returns { results, mapping, successCount, totalCount }
55
- */
56
- export async function consult({ prompt, only, skip = [], timeoutMs = 90_000, cwd, blind = true } = {}) {
57
- const targets = Object.keys(PROVIDERS)
58
- .filter(name => (only ? name === only : true))
59
- .filter(name => !skip.includes(name));
60
-
61
- if (targets.length === 0) throw new Error('No providers selected — check --only / --skip flags.');
62
-
63
- const settled = await Promise.allSettled(
64
- targets.map(name => runOne(name, prompt, { cwd, timeoutMs }))
65
- );
66
-
67
- const results = targets.map((name, i) =>
68
- settled[i].status === 'fulfilled'
69
- ? settled[i].value
70
- : { provider: name, content: '', duration_ms: 0, error: 'rejected', error_type: 'rejected' }
71
- );
72
-
73
- const successCount = results.filter(r => !r.error).length;
74
- const totalCount = results.length;
75
-
76
- if (blind) {
77
- const { results: anon, mapping } = anonymize(results);
78
- return { results: anon, mapping, successCount, totalCount };
79
- }
80
- return { results, mapping: null, successCount, totalCount };
81
- }
package/src/providers.js DELETED
@@ -1,91 +0,0 @@
1
- import { spawn } from 'child_process';
2
-
3
- // ─── Provider argv constants ──────────────────────────────────────────────────
4
-
5
- export const CODEX_ARGS_PREFIX = ['exec', '--json', '--skip-git-repo-check', '--ephemeral'];
6
- export const GEMINI_ARGS_PREFIX = ['-o', 'json'];
7
- export const CLAUDE_ARGS_PREFIX = ['--output-format', 'json', '-p'];
8
-
9
- // ─── Process runner ───────────────────────────────────────────────────────────
10
-
11
- /**
12
- * Spawn a subprocess with an AbortController-based timeout.
13
- * Returns { stdout, stderr, code, error_type } — never rejects.
14
- * error_type: 'enoent' | 'timeout' | 'nonzero' | 'spawn_error' | null
15
- */
16
- export function runProcess(cmd, args, { cwd, timeoutMs } = {}) {
17
- const ac = new AbortController();
18
- const proc = spawn(cmd, args, {
19
- signal: ac.signal,
20
- stdio: ['ignore', 'pipe', 'pipe'],
21
- ...(cwd ? { cwd } : {}),
22
- });
23
-
24
- let stdout = '';
25
- let stderr = '';
26
- proc.stdout.on('data', d => { stdout += d; });
27
- proc.stderr.on('data', d => { stderr += d; });
28
-
29
- const timer = timeoutMs ? setTimeout(() => ac.abort(), timeoutMs) : null;
30
-
31
- return new Promise(resolve => {
32
- let resolved = false;
33
- const done = (code, error_type = null) => {
34
- if (resolved) return;
35
- resolved = true;
36
- if (timer) clearTimeout(timer);
37
- resolve({ stdout, stderr, code, error_type });
38
- };
39
- proc.on('close', code => done(code, code !== 0 ? 'nonzero' : null));
40
- proc.on('error', err => {
41
- if (err.name === 'AbortError') done('timeout', 'timeout');
42
- else if (err.code === 'ENOENT') done(-1, 'enoent');
43
- else done(-1, 'spawn_error');
44
- });
45
- });
46
- }
47
-
48
- // ─── Adapters ─────────────────────────────────────────────────────────────────
49
-
50
- export function fallback(rawStdout) {
51
- return { content: rawStdout.slice(-2000).trim() || '[no output]', parse_mode: 'fallback' };
52
- }
53
-
54
- export function adaptCodex(raw) {
55
- try {
56
- const events = raw.stdout.trim().split('\n').flatMap(l => {
57
- try { return [JSON.parse(l)]; } catch { return []; }
58
- });
59
- const msg = events.filter(e => e.type === 'item.completed' && e.item?.type === 'agent_message').pop()
60
- ?? events.filter(e => e.type === 'item.completed').pop();
61
- if (msg?.item?.text) return { content: msg.item.text, parse_mode: 'jsonl' };
62
- } catch { /* fall through */ }
63
- return fallback(raw.stdout);
64
- }
65
-
66
- export function parseGeminiResponse(stdout) {
67
- const jsonStart = stdout.indexOf('{');
68
- if (jsonStart === -1) return null;
69
- const j = JSON.parse(stdout.slice(jsonStart));
70
- if (typeof j.response === 'string') return j.response;
71
- for (const v of Object.values(j)) {
72
- if (v && typeof v === 'object' && typeof v.response === 'string') return v.response;
73
- }
74
- return null;
75
- }
76
-
77
- export function adaptGemini(raw) {
78
- try {
79
- const response = parseGeminiResponse(raw.stdout);
80
- if (response) return { content: response, parse_mode: 'json' };
81
- } catch { /* fall through */ }
82
- return fallback(raw.stdout);
83
- }
84
-
85
- export function adaptClaude(raw) {
86
- try {
87
- const j = JSON.parse(raw.stdout);
88
- if (typeof j.result === 'string') return { content: j.result, parse_mode: 'json' };
89
- } catch { /* fall through */ }
90
- return fallback(raw.stdout);
91
- }