dual-brain 6.1.0 → 7.0.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.
@@ -0,0 +1,81 @@
1
+ # dual-brain MCP Server
2
+
3
+ Exposes dual-brain's routing engine as MCP tools so any MCP-compatible client (VS Code, Cursor, Windsurf, etc.) can use smart provider/model routing.
4
+
5
+ ## Usage
6
+
7
+ Add to your MCP client config:
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "dual-brain": {
13
+ "command": "node",
14
+ "args": ["node_modules/dual-brain/mcp-server/index.mjs"]
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ Or if installed globally:
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "dual-brain": {
26
+ "command": "node",
27
+ "args": ["/path/to/dual-brain/mcp-server/index.mjs"]
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Tools
34
+
35
+ ### `dual_brain_detect`
36
+ Classify a task into intent, risk, complexity, and tier.
37
+
38
+ ```json
39
+ { "prompt": "refactor the auth module", "files": ["src/auth.mjs"] }
40
+ ```
41
+
42
+ Returns: `{ intent, risk, complexity, effort, tier, requiresWrite, explanation }`
43
+
44
+ ### `dual_brain_decide`
45
+ Full routing decision — detect + route to provider and model.
46
+
47
+ ```json
48
+ { "prompt": "fix the login bug", "files": ["src/auth.mjs"], "profile": "quality-first" }
49
+ ```
50
+
51
+ Returns: `{ provider, model, effort, tier, dualBrain, explanation, detection }`
52
+
53
+ ### `dual_brain_status`
54
+ Provider health, routing scores, and session stats.
55
+
56
+ ```json
57
+ {}
58
+ ```
59
+
60
+ Returns: `{ providers: { claude: {...}, openai: {...} }, session, profile }`
61
+
62
+ ### `dual_brain_remember`
63
+ Save a routing preference that persists across sessions.
64
+
65
+ ```json
66
+ { "preference": "prefer claude for architecture tasks" }
67
+ ```
68
+
69
+ Returns: `{ saved: true, preference, preferences: string[] }`
70
+
71
+ ## Standalone test
72
+
73
+ ```bash
74
+ node mcp-server/index.mjs
75
+ ```
76
+
77
+ Then send JSON-RPC over stdin:
78
+
79
+ ```bash
80
+ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node mcp-server/index.mjs
81
+ ```
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mcp-server/index.mjs — MCP server for dual-brain routing engine.
4
+ *
5
+ * Exposes dual-brain capabilities as MCP tools via JSON-RPC 2.0 over stdin/stdout.
6
+ * No external dependencies — uses only Node.js built-ins and the src/ modules.
7
+ *
8
+ * Tools:
9
+ * dual_brain_detect — Classify a task (intent, risk, complexity, tier)
10
+ * dual_brain_decide — Route a task (provider, model, tier, reason)
11
+ * dual_brain_status — Provider health and budget overview
12
+ * dual_brain_remember — Save a routing preference
13
+ *
14
+ * Usage (standalone):
15
+ * node mcp-server/index.mjs
16
+ *
17
+ * MCP client config:
18
+ * {
19
+ * "mcpServers": {
20
+ * "dual-brain": {
21
+ * "command": "node",
22
+ * "args": ["node_modules/dual-brain/mcp-server/index.mjs"]
23
+ * }
24
+ * }
25
+ * }
26
+ */
27
+
28
+ import { createInterface } from 'readline';
29
+ import { fileURLToPath } from 'url';
30
+ import { dirname, join } from 'path';
31
+
32
+ const __dirname = dirname(fileURLToPath(import.meta.url));
33
+ const SRC = join(__dirname, '..', 'src');
34
+
35
+ // ─── Tool definitions (JSON Schema) ──────────────────────────────────────────
36
+
37
+ const TOOLS = [
38
+ {
39
+ name: 'dual_brain_detect',
40
+ description: 'Classify a task prompt into intent, risk, complexity, and routing tier. Returns classification data used by the dual-brain routing engine.',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ prompt: {
45
+ type: 'string',
46
+ description: 'The task description or prompt to classify.',
47
+ },
48
+ files: {
49
+ type: 'array',
50
+ items: { type: 'string' },
51
+ description: 'Optional list of file paths involved in the task. Used for risk classification.',
52
+ },
53
+ },
54
+ required: ['prompt'],
55
+ },
56
+ },
57
+ {
58
+ name: 'dual_brain_decide',
59
+ description: 'Detect a task and route it to the best provider and model. Returns provider, model, tier, explanation, and whether dual-brain review is recommended.',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ prompt: {
64
+ type: 'string',
65
+ description: 'The task description or prompt.',
66
+ },
67
+ files: {
68
+ type: 'array',
69
+ items: { type: 'string' },
70
+ description: 'Optional list of file paths involved in the task.',
71
+ },
72
+ profile: {
73
+ type: 'string',
74
+ description: 'Optional profile mode override: "auto", "balanced", "cost-saver", or "quality-first".',
75
+ },
76
+ },
77
+ required: ['prompt'],
78
+ },
79
+ },
80
+ {
81
+ name: 'dual_brain_status',
82
+ description: 'Get current provider health, routing scores, and session statistics. Shows which providers are healthy, degraded, or rate-limited.',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {},
86
+ },
87
+ },
88
+ {
89
+ name: 'dual_brain_remember',
90
+ description: 'Save a routing preference that persists across sessions. Examples: "prefer claude for architecture", "use cost-saver mode", "never dual-brain".',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ preference: {
95
+ type: 'string',
96
+ description: 'The routing preference to remember in plain English.',
97
+ },
98
+ },
99
+ required: ['preference'],
100
+ },
101
+ },
102
+ ];
103
+
104
+ // ─── Tool handlers ────────────────────────────────────────────────────────────
105
+
106
+ async function handleDetect({ prompt, files = [] }) {
107
+ const { detectTask } = await import(`${SRC}/detect.mjs`);
108
+ const result = detectTask({ prompt, files });
109
+ return {
110
+ intent: result.intent,
111
+ risk: result.risk,
112
+ complexity: result.complexity,
113
+ effort: result.effort,
114
+ tier: result.tier,
115
+ fileCount: result.fileCount,
116
+ requiresWrite: result.requiresWrite,
117
+ riskyFiles: result.riskyFiles,
118
+ explanation: result.explanation,
119
+ };
120
+ }
121
+
122
+ async function handleDecide({ prompt, files = [], profile: profileOverride }) {
123
+ const { detectTask } = await import(`${SRC}/detect.mjs`);
124
+ const { decideRoute } = await import(`${SRC}/decide.mjs`);
125
+ const { loadProfile } = await import(`${SRC}/profile.mjs`);
126
+
127
+ const cwd = process.cwd();
128
+ let profile = loadProfile(cwd);
129
+
130
+ // Apply profile override if requested
131
+ if (profileOverride) {
132
+ profile = { ...profile, mode: profileOverride, profile: profileOverride };
133
+ }
134
+
135
+ const detection = detectTask({ prompt, files });
136
+ const decision = decideRoute({ profile, detection, cwd });
137
+
138
+ return {
139
+ provider: decision.provider,
140
+ model: decision.model,
141
+ effort: decision.effort,
142
+ tier: decision.tier,
143
+ dualBrain: decision.dualBrain,
144
+ modes: decision.modes,
145
+ sandbox: decision.sandbox,
146
+ explanation: decision.explanation,
147
+ detection: {
148
+ intent: detection.intent,
149
+ risk: detection.risk,
150
+ complexity: detection.complexity,
151
+ },
152
+ };
153
+ }
154
+
155
+ async function handleStatus() {
156
+ const { loadProfile, getAvailableProviders } = await import(`${SRC}/profile.mjs`);
157
+ const { getHealth, getProviderScore, getSessionStats } = await import(`${SRC}/health.mjs`);
158
+
159
+ const cwd = process.cwd();
160
+ const profile = loadProfile(cwd);
161
+ const health = getHealth(cwd);
162
+
163
+ const providers = {};
164
+
165
+ for (const prov of ['claude', 'openai']) {
166
+ const cfg = profile?.providers?.[prov];
167
+ if (!cfg) {
168
+ providers[prov] = { enabled: false };
169
+ continue;
170
+ }
171
+
172
+ // Get scores for each tier model
173
+ const models = {
174
+ search: prov === 'claude' ? 'haiku' : 'gpt-4.1-mini',
175
+ execute: prov === 'claude' ? 'sonnet' : 'gpt-5.4',
176
+ think: prov === 'claude' ? 'opus' : 'gpt-5.5',
177
+ };
178
+
179
+ const scores = {};
180
+ const states = {};
181
+ for (const [tier, modelClass] of Object.entries(models)) {
182
+ scores[tier] = getProviderScore(prov, modelClass, cwd);
183
+ const key = `${prov}:${modelClass}`;
184
+ states[modelClass] = health.states[key] ?? { status: 'healthy' };
185
+ }
186
+
187
+ providers[prov] = {
188
+ enabled: cfg.enabled ?? false,
189
+ plan: cfg.plan ?? null,
190
+ scores,
191
+ states,
192
+ };
193
+ }
194
+
195
+ const session = getSessionStats ? getSessionStats(cwd) : (health.session ?? null);
196
+
197
+ return {
198
+ providers,
199
+ session,
200
+ profile: {
201
+ mode: profile?.mode || profile?.profile || 'auto',
202
+ dualBrainEnabled: profile?.dual_brain_enabled !== false,
203
+ preferences: (profile?.preferences || []).filter(p => p.enabled).map(p => p.text),
204
+ },
205
+ };
206
+ }
207
+
208
+ async function handleRemember({ preference }) {
209
+ const { rememberPreference, getActivePreferences } = await import(`${SRC}/profile.mjs`);
210
+ const cwd = process.cwd();
211
+ rememberPreference(preference, { cwd });
212
+ const all = getActivePreferences(cwd);
213
+ return {
214
+ saved: true,
215
+ preference,
216
+ preferences: all.map(p => p.text),
217
+ };
218
+ }
219
+
220
+ // ─── JSON-RPC dispatcher ──────────────────────────────────────────────────────
221
+
222
+ async function dispatchTool(name, args) {
223
+ switch (name) {
224
+ case 'dual_brain_detect': return handleDetect(args);
225
+ case 'dual_brain_decide': return handleDecide(args);
226
+ case 'dual_brain_status': return handleStatus();
227
+ case 'dual_brain_remember': return handleRemember(args);
228
+ default:
229
+ throw Object.assign(new Error(`Unknown tool: ${name}`), { code: -32601 });
230
+ }
231
+ }
232
+
233
+ // ─── JSON-RPC helpers ─────────────────────────────────────────────────────────
234
+
235
+ function respond(id, result) {
236
+ return JSON.stringify({ jsonrpc: '2.0', id, result });
237
+ }
238
+
239
+ function errorResponse(id, code, message, data) {
240
+ const err = { code, message };
241
+ if (data !== undefined) err.data = data;
242
+ return JSON.stringify({ jsonrpc: '2.0', id, error: err });
243
+ }
244
+
245
+ // ─── Request handlers ─────────────────────────────────────────────────────────
246
+
247
+ async function handleRequest(msg) {
248
+ const { id, method, params } = msg;
249
+
250
+ try {
251
+ switch (method) {
252
+ case 'initialize':
253
+ return respond(id, {
254
+ protocolVersion: '2024-11-05',
255
+ capabilities: { tools: {} },
256
+ serverInfo: { name: 'dual-brain', version: '1.0.0' },
257
+ });
258
+
259
+ case 'initialized':
260
+ // Notification (no id), no response needed
261
+ return null;
262
+
263
+ case 'tools/list':
264
+ return respond(id, { tools: TOOLS });
265
+
266
+ case 'tools/call': {
267
+ const { name, arguments: args = {} } = params || {};
268
+ if (!name) {
269
+ return errorResponse(id, -32602, 'Missing tool name');
270
+ }
271
+ const result = await dispatchTool(name, args);
272
+ return respond(id, {
273
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
274
+ });
275
+ }
276
+
277
+ case 'ping':
278
+ return respond(id, {});
279
+
280
+ default:
281
+ return errorResponse(id, -32601, `Method not found: ${method}`);
282
+ }
283
+ } catch (err) {
284
+ const code = err.code ?? -32000;
285
+ const message = err.message ?? 'Internal error';
286
+ return errorResponse(id, code, message, err.stack);
287
+ }
288
+ }
289
+
290
+ // ─── Main loop ────────────────────────────────────────────────────────────────
291
+
292
+ const rl = createInterface({ input: process.stdin, terminal: false });
293
+
294
+ // Track in-flight requests so we don't exit while work is pending
295
+ let pending = 0;
296
+ let stdinClosed = false;
297
+
298
+ function maybeExit() {
299
+ if (stdinClosed && pending === 0) process.exit(0);
300
+ }
301
+
302
+ rl.on('line', (line) => {
303
+ const raw = line.trim();
304
+ if (!raw) return;
305
+
306
+ let msg;
307
+ try {
308
+ msg = JSON.parse(raw);
309
+ } catch {
310
+ process.stdout.write(errorResponse(null, -32700, 'Parse error') + '\n');
311
+ return;
312
+ }
313
+
314
+ pending++;
315
+ handleRequest(msg).then((response) => {
316
+ if (response !== null) {
317
+ process.stdout.write(response + '\n');
318
+ }
319
+ }).catch((err) => {
320
+ process.stdout.write(errorResponse(msg?.id ?? null, -32000, err?.message ?? 'Internal error') + '\n');
321
+ }).finally(() => {
322
+ pending--;
323
+ maybeExit();
324
+ });
325
+ });
326
+
327
+ rl.on('close', () => {
328
+ stdinClosed = true;
329
+ maybeExit();
330
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "6.1.0",
3
+ "version": "7.0.0",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,13 +50,19 @@
50
50
  "bin/*.mjs",
51
51
  "hooks/*.mjs",
52
52
  "hooks/*.sh",
53
+ "mcp-server/*.mjs",
54
+ "mcp-server/README.md",
53
55
  "install.mjs",
54
56
  "orchestrator.json",
55
57
  "hookify.*.local.md",
56
58
  "review-rules.md",
59
+ "AGENTS.md",
57
60
  "CLAUDE.md",
58
61
  "README.md",
59
62
  "LICENSE",
60
- "playbooks/*.json"
63
+ "playbooks/*.json",
64
+ "plugin.json",
65
+ "skills/*.md",
66
+ "agents/*.md"
61
67
  ]
62
68
  }
package/plugin.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "dual-brain",
3
+ "version": "7.0.0",
4
+ "description": "Dual-provider AI orchestration — smart routing between Claude and OpenAI",
5
+ "skills": [
6
+ { "name": "go", "description": "Route and dispatch a task", "file": "skills/go.md" },
7
+ { "name": "status", "description": "Show provider health and budget", "file": "skills/status.md" },
8
+ { "name": "think", "description": "Dual-brain architecture discussion", "file": "skills/think.md" },
9
+ { "name": "review", "description": "Dual-brain code review", "file": "skills/review.md" }
10
+ ],
11
+ "hooks": {
12
+ "PreToolUse": [
13
+ { "matcher": "Edit|Write|NotebookEdit|Bash", "command": "node hooks/head-guard.mjs" },
14
+ { "matcher": "Agent", "command": "node hooks/enforce-tier.mjs" }
15
+ ]
16
+ },
17
+ "agents": [
18
+ { "name": "researcher", "file": "agents/researcher.md" },
19
+ { "name": "implementer", "file": "agents/implementer.md" },
20
+ { "name": "verifier", "file": "agents/verifier.md" }
21
+ ]
22
+ }
package/skills/go.md ADDED
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: go
3
+ description: Route and dispatch a task through dual-brain
4
+ arguments:
5
+ - name: task
6
+ description: The task description to route
7
+ required: true
8
+ - name: dry-run
9
+ description: Show routing without executing
10
+ required: false
11
+ - name: files
12
+ description: Comma-separated file paths for risk classification
13
+ required: false
14
+ ---
15
+
16
+ Run `dual-brain go` with the provided arguments. Execute:
17
+
18
+ ```bash
19
+ dual-brain go [--dry-run] [--files <files>] "<task>"
20
+ ```
21
+
22
+ Report the routing decision (provider, model, tier) and dispatch result to the user.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: review
3
+ description: Dual-brain code review — two-round Claude + GPT review of the current diff
4
+ arguments: []
5
+ ---
6
+
7
+ Run the dual-brain review flow (2 rounds) on the current git diff:
8
+
9
+ **Round 1** — get GPT's independent review:
10
+ ```bash
11
+ node hooks/dual-brain-review.mjs
12
+ ```
13
+
14
+ Review the same diff independently, then run **Round 2** — share your findings and get GPT's response:
15
+ ```bash
16
+ node hooks/dual-brain-review.mjs --round 2 --claude-review "<your findings>"
17
+ ```
18
+
19
+ Synthesize both rounds into a final review verdict: shared findings, unique catches from each side, and any items that need human attention.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: status
3
+ description: Show provider health, budget pressure, and enforcement status
4
+ arguments: []
5
+ ---
6
+
7
+ Run `dual-brain status` and report the results to the user:
8
+
9
+ ```bash
10
+ dual-brain status
11
+ ```
12
+
13
+ Show provider health (Claude, OpenAI), current budget pressure for each provider, active profile, and enforcement status.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: think
3
+ description: Dual-brain architecture discussion — two-round Claude + GPT analysis
4
+ arguments:
5
+ - name: question
6
+ description: The architecture question or decision to analyze
7
+ required: true
8
+ ---
9
+
10
+ Run the dual-brain think flow (2 rounds) using the provided question:
11
+
12
+ **Round 1** — get GPT's independent analysis:
13
+ ```bash
14
+ node hooks/dual-brain-think.mjs --question "<question>"
15
+ ```
16
+
17
+ Analyze the same question independently, then run **Round 2** — share your analysis and get GPT's response:
18
+ ```bash
19
+ node hooks/dual-brain-think.mjs --question "<question>" --round 2 --claude-says "<your analysis>"
20
+ ```
21
+
22
+ Synthesize both rounds into a final decision, noting agreements, disagreements, and the chosen recommendation.