dual-brain 5.1.0 → 6.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,200 @@
1
+ #!/usr/bin/env node
2
+ // dual-brain — CLI entry point. Commands: init, go, status, remember, forget
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { join, dirname } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { execSync } from 'node:child_process';
8
+
9
+ import {
10
+ ensureProfile, loadProfile, runOnboarding,
11
+ rememberPreference, forgetPreference, getActivePreferences,
12
+ getAvailableProviders, isSoloBrain, getHeadModel,
13
+ } from '../src/profile.mjs';
14
+
15
+ import { detectTask } from '../src/detect.mjs';
16
+
17
+ import {
18
+ decideRoute, getAvailableModels, estimateBudgetPressure,
19
+ } from '../src/decide.mjs';
20
+
21
+ import { dispatch, detectRuntime, dispatchDualBrain } from '../src/dispatch.mjs';
22
+
23
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ const PKG_PATH = join(__dirname, '..', 'package.json');
27
+
28
+ function readVersion() {
29
+ try { return JSON.parse(readFileSync(PKG_PATH, 'utf8')).version; } catch { return '0.0.0'; }
30
+ }
31
+ function flag(args, name) { const i = args.indexOf(name); return i !== -1 ? (args[i + 1] ?? true) : null; }
32
+ function err(msg) { process.stderr.write(`Error: ${msg}\n`); process.exit(1); }
33
+
34
+ function printHelp() {
35
+ console.log(`
36
+ dual-brain <command> [options]
37
+
38
+ Commands:
39
+ init First-time setup (providers, plans, optimization)
40
+ go "task description" Detect → decide → dispatch a task
41
+ --dry-run Show routing decision without executing
42
+ --files a.mjs,b.mjs Provide file context for risk classification
43
+ status Provider health, budget pressure, available models
44
+ remember "preference" Save a project-scoped preference
45
+ forget "preference" Remove a preference by fuzzy match
46
+
47
+ Options:
48
+ --version Print version
49
+ --help Show this help
50
+ `.trim());
51
+ }
52
+
53
+ // ─── Commands ─────────────────────────────────────────────────────────────────
54
+
55
+ async function cmdInit() {
56
+ const profile = await runOnboarding({ interactive: true });
57
+ const rt = await detectRuntime();
58
+ const providers = getAvailableProviders(profile);
59
+ const providerSummary = providers.length
60
+ ? providers.map(p => `${p.name === 'claude' ? 'Claude' : 'OpenAI'} (${p.plan})`).join(', ')
61
+ : 'none';
62
+ console.log(`Profile saved. Providers: ${providerSummary}. Mode: ${profile.mode}. Runtime: ${rt.runtime}`);
63
+ }
64
+
65
+ async function cmdGo(args) {
66
+ const dryRun = args.includes('--dry-run');
67
+ const filesRaw = flag(args, '--files');
68
+ const files = filesRaw && typeof filesRaw === 'string'
69
+ ? filesRaw.split(',').map(f => f.trim()).filter(Boolean)
70
+ : [];
71
+
72
+ // prompt is the first non-flag argument (or value after --dry-run which is boolean)
73
+ const prompt = args.find(a => !a.startsWith('--') && a !== (filesRaw ?? ''));
74
+ if (!prompt) err('Usage: dual-brain go "task description" [--dry-run] [--files a,b]');
75
+
76
+ const cwd = process.cwd();
77
+ const profile = await ensureProfile(cwd);
78
+ const detection = detectTask({ prompt, files });
79
+
80
+ // Print the one-sentence classification
81
+ console.log(detection.explanation);
82
+
83
+ const decision = decideRoute({ profile, detection, cwd });
84
+
85
+ // Print routing table
86
+ console.log(` provider : ${decision.provider}`);
87
+ console.log(` model : ${decision.model}${decision.effort ? ' (' + decision.effort + ')' : ''}`);
88
+ console.log(` tier : ${decision.tier}`);
89
+ console.log(` dual-brain : ${decision.dualBrain ? 'yes' : 'no'}`);
90
+ console.log(` reason : ${decision.explanation}`);
91
+
92
+ if (dryRun) {
93
+ console.log('\n(dry-run — not executing)');
94
+ return;
95
+ }
96
+
97
+ console.log('\nDispatching...');
98
+ let result;
99
+ if (decision.dualBrain) {
100
+ result = await dispatchDualBrain({ decision, prompt, files, cwd });
101
+ console.log(`\nConsensus: ${result.consensus}`);
102
+ if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
103
+ if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
104
+ } else {
105
+ result = await dispatch({ decision, prompt, files, cwd });
106
+ const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
107
+ console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
108
+ if (result.summary) console.log(result.summary);
109
+ if (result.error) process.stderr.write(`${result.error}\n`);
110
+ if (result.status !== 'completed') process.exit(1); }
111
+ }
112
+
113
+ async function cmdStatus() {
114
+ const cwd = process.cwd();
115
+ const profile = loadProfile(cwd);
116
+ const rt = await detectRuntime();
117
+ const providers = getAvailableProviders(profile);
118
+ const pressure = estimateBudgetPressure(profile, cwd);
119
+ const available = getAvailableModels(profile);
120
+ const prefs = getActivePreferences(cwd);
121
+
122
+ console.log('=== Dual-Brain Status ===\n');
123
+
124
+ // Providers
125
+ console.log('Providers:');
126
+ if (providers.length === 0) {
127
+ console.log(' (none configured — run: dual-brain init)');
128
+ } else {
129
+ for (const p of providers) {
130
+ const label = p.name === 'claude' ? 'Claude' : 'OpenAI';
131
+ const pct = Math.round((pressure[p.name] ?? 0) * 100);
132
+ console.log(` ${label} plan=${p.plan} budget=${pct}% used`);
133
+ }
134
+ }
135
+
136
+ // Models
137
+ console.log('\nAvailable models:');
138
+ if (available.claude.length) console.log(` Claude : ${available.claude.join(', ')}`);
139
+ if (available.openai.length) console.log(` OpenAI : ${available.openai.join(', ')}`);
140
+
141
+ // Head model
142
+ console.log(`\nHead model : ${getHeadModel(profile)}`);
143
+ console.log(`Mode : ${profile.mode}`);
144
+ console.log(`Solo brain : ${isSoloBrain(profile) ? 'yes' : 'no'}`);
145
+
146
+ // Runtime
147
+ console.log('\nRuntime:');
148
+ console.log(` claude CLI : ${rt.claudeAvailable ? 'available' : 'not found'}`);
149
+ console.log(` codex CLI : ${rt.codexAvailable ? 'available' : 'not found'}`);
150
+ console.log(` detected : ${rt.runtime}`);
151
+
152
+ // Preferences
153
+ console.log(`\nPreferences: ${prefs.length ? '' : '(none)'}`);
154
+ for (const p of prefs) console.log(` [${p.scope}] ${p.text}`);
155
+
156
+ // Update check
157
+ try {
158
+ const localVer = readVersion();
159
+ const remoteVer = execSync('npm view dual-brain version 2>/dev/null', { timeout: 5000 }).toString().trim();
160
+ if (remoteVer && remoteVer !== localVer) {
161
+ console.log(`\nUpdate available: npm i -g dual-brain@latest (${localVer} → ${remoteVer})`);
162
+ }
163
+ } catch { /* network unavailable — skip */ }
164
+ }
165
+
166
+ function cmdRemember(text) {
167
+ if (!text) err('Usage: dual-brain remember "preference text"');
168
+ const profile = rememberPreference(text, { scope: 'project', cwd: process.cwd() });
169
+ console.log(`Preference saved. Total active: ${profile.preferences.filter(p => p.enabled).length}`);
170
+ }
171
+
172
+ function cmdForget(text) {
173
+ if (!text) err('Usage: dual-brain forget "preference text"');
174
+ forgetPreference(text, process.cwd());
175
+ console.log('Preference removed (if matched).');
176
+ }
177
+
178
+ // ─── Entry point ─────────────────────────────────────────────────────────────
179
+
180
+ async function main() {
181
+ const args = process.argv.slice(2);
182
+ const cmd = args[0];
183
+
184
+ if (!cmd || cmd === '--help' || cmd === '-h') { printHelp(); return; }
185
+ if (cmd === '--version' || cmd === '-v') { console.log(readVersion()); return; }
186
+
187
+ if (cmd === 'init') { await cmdInit(); return; }
188
+ if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
189
+ if (cmd === 'status') { await cmdStatus(); return; }
190
+ if (cmd === 'remember') { cmdRemember(args[1]); return; }
191
+ if (cmd === 'forget') { cmdForget(args[1]); return; }
192
+
193
+ process.stderr.write(`Unknown command: ${cmd}\nRun "dual-brain --help" for usage.\n`);
194
+ process.exit(1);
195
+ }
196
+
197
+ main().catch(e => {
198
+ process.stderr.write(`${e.message}\n`);
199
+ process.exit(1);
200
+ });