braintrust-lite 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HongjieRen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # braintrust-lite
2
+
3
+ Claude Code 原生的多模型军师 — 并发调用 Codex + Gemini,主 Claude 担任 Judge 融合输出。
4
+
5
+ ```
6
+ 主 Claude → parallel:
7
+ ├─ Task(subagent_type=Plan, prompt=X) ← 正常子 agent
8
+ └─ mcp__braintrust_lite__consult(prompt=X) ← Codex + Gemini 旁路咨询
9
+ → 主 Claude 融合三方视角 → 最终方案
10
+ ```
11
+
12
+ vs [`braintrust`](https://github.com/HongjieRen/braintrust): 2 次 API 调用(省 50%),无独立 Judge,无落盘,原生集成 Claude Code。
13
+
14
+ ---
15
+
16
+ ## 安装
17
+
18
+ **前置条件**:`codex` 和 `gemini` CLI 均已登录。
19
+
20
+ ```bash
21
+ # 克隆
22
+ git clone https://github.com/HongjieRen/braintrust-lite.git
23
+ cd braintrust-lite
24
+
25
+ # 安装依赖
26
+ npm install
27
+
28
+ # 可选:把 CLI 软链到 PATH
29
+ ln -sf "$(pwd)/bin/consult" ~/.local/bin/consult
30
+ chmod +x bin/consult
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 注册到 Claude Code(MCP)
36
+
37
+ ```bash
38
+ claude mcp add braintrust-lite node "$(pwd)/src/server.js"
39
+ ```
40
+
41
+ 注册后,Claude Code 会话里会出现 `mcp__braintrust_lite__consult` tool,和 `Read` / `Bash` 并列可用。
42
+
43
+ 重启 Claude Code 后生效。
44
+
45
+ ---
46
+
47
+ ## 安装 Skill 引导
48
+
49
+ 把 skill 软链到 Claude Code 全局 skill 目录,让主 Claude 知道何时该主动使用 consult:
50
+
51
+ ```bash
52
+ ln -sf "$(pwd)/skills/consult" ~/.claude/skills/consult
53
+ ```
54
+
55
+ 安装后可用 `/consult` slash command 激活"军师模式"引导。
56
+
57
+ ---
58
+
59
+ ## 使用方式
60
+
61
+ ### 在 Claude Code 里(推荐)
62
+
63
+ Claude 会在处理规划/设计类任务时自动(或在 `/consult` 引导下)并发调用:
64
+
65
+ ```
66
+ 你处理一个架构选型任务时,Claude 会同时:
67
+ 1. 启动 Plan sub-agent 做深度分析
68
+ 2. 调用 mcp__braintrust_lite__consult 获取 Codex + Gemini 的独立视角
69
+ 3. 融合三方输出给你最终方案
70
+ ```
71
+
72
+ ### 终端 CLI(fallback / 调试)
73
+
74
+ ```bash
75
+ consult "解释 CAP 定理" # 并发两模型,markdown 输出
76
+ consult --only codex "prompt" # 只跑 codex
77
+ consult --skip gemini "prompt" # 跳过 gemini
78
+ consult --timeout 60 "prompt" # 超时秒数
79
+ consult --dir ~/myproject "review" # 工作目录
80
+ cat app.ts | consult "review this code" # stdin 拼接
81
+ consult --json "prompt" # JSON 结构化输出
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 参数
87
+
88
+ | 参数 | 默认 | 说明 |
89
+ |---|---|---|
90
+ | `prompt` | 必须 | 问题文本(MCP)/ 位置参数(CLI)|
91
+ | `only` | — | 只调用: `codex` \| `gemini` |
92
+ | `skip` | — | 跳过模型列表 |
93
+ | `timeout_sec` | `90` | 每个模型超时秒数 |
94
+ | `cwd` | server cwd | 子进程工作目录 |
95
+ | `--json` | false | CLI 专用:JSON 格式输出 |
96
+
97
+ ---
98
+
99
+ ## 输出格式
100
+
101
+ ```
102
+ ## CODEX (8.2s)
103
+
104
+ <codex 完整回答>
105
+
106
+ ---
107
+
108
+ ## GEMINI (6.5s)
109
+
110
+ <gemini 完整回答>
111
+ ```
112
+
113
+ 失败的 provider 显示 `*调用失败: timeout*`,另一个照常返回(`Promise.allSettled` 容错)。
114
+
115
+ ---
116
+
117
+ ## 架构
118
+
119
+ ```
120
+ braintrust-lite/
121
+ ├── src/
122
+ │ ├── server.js MCP stdio server
123
+ │ ├── consult.js 核心并发逻辑
124
+ │ ├── providers.js spawn + Codex/Gemini 解析器
125
+ │ └── format.js Markdown / JSON 渲染
126
+ ├── bin/
127
+ │ └── consult CLI 入口
128
+ ├── skills/
129
+ │ └── consult/
130
+ │ └── SKILL.md Claude Code skill 引导
131
+ └── docs/
132
+ └── spec.md 设计文档
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 成本
138
+
139
+ | 场景 | API 调用 | 估算成本 |
140
+ |---|---|---|
141
+ | 简单问题 | 2 | $0.05–0.15 |
142
+ | 中等问题 | 2 | $0.15–0.40 |
143
+ | 复杂问题 | 2 | $0.40–0.80 |
144
+
145
+ ---
146
+
147
+ ## License
148
+
149
+ MIT
package/bin/consult ADDED
@@ -0,0 +1,78 @@
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
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 = await consult({
60
+ prompt,
61
+ only: flags.only,
62
+ skip: flags.skip,
63
+ timeoutMs: flags.timeout ? flags.timeout * 1000 : 90_000,
64
+ cwd: flags.dir ? resolve(flags.dir) : undefined,
65
+ });
66
+
67
+ // Progress summary to stderr
68
+ for (const r of results) {
69
+ const status = r.error ? `⚠ ${r.error}` : `✓ ${(r.duration_ms / 1000).toFixed(1)}s`;
70
+ process.stderr.write(`[${r.provider}: ${status}]\n`);
71
+ }
72
+
73
+ // Output to stdout
74
+ if (flags.json) {
75
+ console.log(formatAsJson(prompt, results));
76
+ } else {
77
+ console.log('\n' + formatAsMarkdown(results));
78
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "braintrust-lite",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight multi-model advisor for Claude Code — parallel Codex + Gemini consultation via MCP",
5
+ "type": "module",
6
+ "bin": {
7
+ "consult": "./bin/consult",
8
+ "braintrust-lite": "./src/server.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/server.js"
12
+ },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.10.2"
15
+ },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "keywords": ["mcp", "claude-code", "codex", "gemini", "multi-model", "ai"],
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/HongjieRen/braintrust-lite.git"
24
+ }
25
+ }
package/src/consult.js ADDED
@@ -0,0 +1,72 @@
1
+ import {
2
+ runProcess,
3
+ adaptCodex,
4
+ adaptGemini,
5
+ CODEX_ARGS_PREFIX,
6
+ GEMINI_ARGS_PREFIX,
7
+ } from './providers.js';
8
+
9
+ const SYSTEM_PROMPT = `你是一个独立思考的高级专家。请基于自己的判断给出高质量、可执行的回答。
10
+ 要求:独立思考,不假设其他专家会补充;区分结论、依据、假设、风险;简洁但完整。`;
11
+
12
+ const PROVIDERS = {
13
+ codex: {
14
+ cmd: 'codex',
15
+ buildArgs: prompt => [...CODEX_ARGS_PREFIX, `${SYSTEM_PROMPT}\n\n${prompt}`],
16
+ adapt: adaptCodex,
17
+ },
18
+ gemini: {
19
+ cmd: 'gemini',
20
+ buildArgs: prompt => ['-p', `${SYSTEM_PROMPT}\n\n${prompt}`, ...GEMINI_ARGS_PREFIX],
21
+ adapt: adaptGemini,
22
+ },
23
+ };
24
+
25
+ /**
26
+ * Run a single provider and return a normalized result object.
27
+ * Never throws — errors are captured in the `error` field.
28
+ */
29
+ async function runOne(name, prompt, { cwd, timeoutMs }) {
30
+ const p = PROVIDERS[name];
31
+ const start = Date.now();
32
+ const raw = await runProcess(p.cmd, p.buildArgs(prompt), { cwd, timeoutMs });
33
+ const duration_ms = Date.now() - start;
34
+
35
+ const error = raw.code === 'timeout' ? 'timeout'
36
+ : raw.code !== 0 ? `exit ${raw.code}`
37
+ : null;
38
+
39
+ const { content } = error ? { content: '' } : p.adapt(raw);
40
+ return { provider: name, content, duration_ms, error };
41
+ }
42
+
43
+ /**
44
+ * Consult Codex and/or Gemini in parallel.
45
+ *
46
+ * @param {object} opts
47
+ * @param {string} opts.prompt - The question to ask.
48
+ * @param {string} [opts.only] - 'codex' | 'gemini' — only run this one.
49
+ * @param {string[]} [opts.skip] - Providers to skip.
50
+ * @param {number} [opts.timeoutMs] - Per-provider timeout in ms (default 90 000).
51
+ * @param {string} [opts.cwd] - Working directory for subprocesses.
52
+ * @returns {Promise<Array<{provider, content, duration_ms, error}>>}
53
+ */
54
+ export async function consult({ prompt, only, skip = [], timeoutMs = 90_000, cwd } = {}) {
55
+ const targets = Object.keys(PROVIDERS)
56
+ .filter(name => (only ? name === only : true))
57
+ .filter(name => !skip.includes(name));
58
+
59
+ if (targets.length === 0) {
60
+ throw new Error('No providers selected — check --only / --skip flags.');
61
+ }
62
+
63
+ const settled = await Promise.allSettled(
64
+ targets.map(name => runOne(name, prompt, { cwd, timeoutMs }))
65
+ );
66
+
67
+ return targets.map((name, i) =>
68
+ settled[i].status === 'fulfilled'
69
+ ? settled[i].value
70
+ : { provider: name, content: '', duration_ms: 0, error: settled[i].reason?.message ?? 'unknown' }
71
+ );
72
+ }
package/src/format.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Format an array of provider results as human-readable Markdown.
3
+ * Each provider gets a ## header with timing (or error), then its content.
4
+ */
5
+ export function formatAsMarkdown(results) {
6
+ return results.map(r => {
7
+ const label = r.error
8
+ ? `## ${r.provider.toUpperCase()} (${r.error})`
9
+ : `## ${r.provider.toUpperCase()} (${(r.duration_ms / 1000).toFixed(1)}s)`;
10
+ const body = r.error ? `*调用失败: ${r.error}*` : r.content;
11
+ return `${label}\n\n${body}`;
12
+ }).join('\n\n---\n\n');
13
+ }
14
+
15
+ /**
16
+ * Format results as a compact JSON string for programmatic consumption.
17
+ */
18
+ export function formatAsJson(prompt, results) {
19
+ return JSON.stringify({ prompt, results }, null, 2);
20
+ }
@@ -0,0 +1,78 @@
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', '--allowed-mcp-server-names', 'sequential-thinking'];
7
+
8
+ // ─── Process runner ───────────────────────────────────────────────────────────
9
+
10
+ /**
11
+ * Spawn a subprocess with an AbortController-based timeout.
12
+ * Returns { stdout, stderr, code } — never rejects.
13
+ */
14
+ export function runProcess(cmd, args, { cwd, timeoutMs } = {}) {
15
+ const ac = new AbortController();
16
+ const proc = spawn(cmd, args, {
17
+ signal: ac.signal,
18
+ stdio: ['ignore', 'pipe', 'pipe'],
19
+ ...(cwd ? { cwd } : {}),
20
+ });
21
+
22
+ let stdout = '';
23
+ let stderr = '';
24
+ proc.stdout.on('data', d => { stdout += d; });
25
+ proc.stderr.on('data', d => { stderr += d; });
26
+
27
+ const timer = timeoutMs ? setTimeout(() => ac.abort(), timeoutMs) : null;
28
+
29
+ return new Promise(resolve => {
30
+ const done = code => {
31
+ if (timer) clearTimeout(timer);
32
+ resolve({ stdout, stderr, code });
33
+ };
34
+ proc.on('close', done);
35
+ proc.on('error', err => done(err.name === 'AbortError' ? 'timeout' : -1));
36
+ });
37
+ }
38
+
39
+ // ─── Adapters ─────────────────────────────────────────────────────────────────
40
+
41
+ /** Last-resort: take the tail of raw stdout. */
42
+ export function fallback(rawStdout) {
43
+ return { content: rawStdout.slice(-2000).trim() || '[no output]', parse_mode: 'fallback' };
44
+ }
45
+
46
+ /** Parse Codex JSONL stream → extract the last agent_message text. */
47
+ export function adaptCodex(raw) {
48
+ try {
49
+ const events = raw.stdout.trim().split('\n').flatMap(l => {
50
+ try { return [JSON.parse(l)]; } catch { return []; }
51
+ });
52
+ const msg = events.filter(e => e.type === 'item.completed' && e.item?.type === 'agent_message').pop()
53
+ ?? events.filter(e => e.type === 'item.completed').pop();
54
+ if (msg?.item?.text) return { content: msg.item.text, parse_mode: 'jsonl' };
55
+ } catch { /* fall through */ }
56
+ return fallback(raw.stdout);
57
+ }
58
+
59
+ /** Skip any MCP startup noise before the first '{', then extract .response */
60
+ export function parseGeminiResponse(stdout) {
61
+ const jsonStart = stdout.indexOf('{');
62
+ if (jsonStart === -1) return null;
63
+ const j = JSON.parse(stdout.slice(jsonStart));
64
+ if (typeof j.response === 'string') return j.response;
65
+ for (const v of Object.values(j)) {
66
+ if (v && typeof v === 'object' && typeof v.response === 'string') return v.response;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /** Parse Gemini JSON output → content string. */
72
+ export function adaptGemini(raw) {
73
+ try {
74
+ const response = parseGeminiResponse(raw.stdout);
75
+ if (response) return { content: response, parse_mode: 'json' };
76
+ } catch { /* fall through */ }
77
+ return fallback(raw.stdout);
78
+ }
package/src/server.js ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from '@modelcontextprotocol/sdk/types.js';
8
+ import { consult } from './consult.js';
9
+ import { formatAsMarkdown } from './format.js';
10
+
11
+ // ─── Tool definition ──────────────────────────────────────────────────────────
12
+
13
+ const CONSULT_TOOL = {
14
+ name: 'consult',
15
+ description:
16
+ '并发调用 Codex 和 Gemini CLI,获取两个顶尖模型对同一问题的独立视角。' +
17
+ '适合架构选型、方案设计、技术决策、复杂调研。' +
18
+ '调用方(通常是主 Claude)负责综合融合输出,自己担任 Judge。' +
19
+ '不适合:typo 修复、单行改动、只读信息查询。',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ prompt: {
24
+ type: 'string',
25
+ description: '要问两个模型的问题。建议精炼、自包含(含必要上下文)。',
26
+ },
27
+ only: {
28
+ type: 'string',
29
+ enum: ['codex', 'gemini'],
30
+ description: '只调用指定一个模型(省成本或调试)。',
31
+ },
32
+ skip: {
33
+ type: 'array',
34
+ items: { type: 'string', enum: ['codex', 'gemini'] },
35
+ description: '跳过指定模型列表。',
36
+ },
37
+ timeout_sec: {
38
+ type: 'number',
39
+ description: '每个模型的超时秒数,默认 90。',
40
+ },
41
+ cwd: {
42
+ type: 'string',
43
+ description: '子进程工作目录,默认继承 MCP server 的 cwd。',
44
+ },
45
+ },
46
+ required: ['prompt'],
47
+ },
48
+ };
49
+
50
+ // ─── Server setup ─────────────────────────────────────────────────────────────
51
+
52
+ const server = new Server(
53
+ { name: 'braintrust-lite', version: '0.1.0' },
54
+ { capabilities: { tools: {} } }
55
+ );
56
+
57
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
58
+ tools: [CONSULT_TOOL],
59
+ }));
60
+
61
+ server.setRequestHandler(CallToolRequestSchema, async req => {
62
+ if (req.params.name !== 'consult') {
63
+ throw new Error(`Unknown tool: ${req.params.name}`);
64
+ }
65
+
66
+ const args = req.params.arguments ?? {};
67
+ const results = await consult({
68
+ prompt: String(args.prompt ?? ''),
69
+ only: args.only,
70
+ skip: Array.isArray(args.skip) ? args.skip : [],
71
+ timeoutMs: args.timeout_sec ? Number(args.timeout_sec) * 1000 : 90_000,
72
+ cwd: args.cwd,
73
+ });
74
+
75
+ if (results.every(r => r.error)) {
76
+ throw new Error(
77
+ `All providers failed: ${results.map(r => `${r.provider}=${r.error}`).join(', ')}`
78
+ );
79
+ }
80
+
81
+ return {
82
+ content: [{ type: 'text', text: formatAsMarkdown(results) }],
83
+ };
84
+ });
85
+
86
+ // ─── Start ────────────────────────────────────────────────────────────────────
87
+
88
+ const transport = new StdioServerTransport();
89
+ await server.connect(transport);