dual-brain 7.1.28 → 7.1.30
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/dual-brain.mjs +294 -89
- package/package.json +4 -1
- package/src/cost-tracker.mjs +184 -0
- package/src/decide.mjs +47 -2
- package/src/fx.mjs +276 -0
- package/src/pipeline.mjs +53 -2
- package/src/think-engine.mjs +428 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -62,6 +62,55 @@ async function getLivingDocs() {
|
|
|
62
62
|
return _livingDocs;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
let _fx = null;
|
|
66
|
+
async function getFx() {
|
|
67
|
+
if (_fx !== null) return _fx;
|
|
68
|
+
try {
|
|
69
|
+
_fx = await import('../src/fx.mjs');
|
|
70
|
+
} catch {
|
|
71
|
+
// Fallback stubs when fx.mjs is not yet present
|
|
72
|
+
const _noop = () => {};
|
|
73
|
+
const _spinnerStub = (text) => {
|
|
74
|
+
let _t = text;
|
|
75
|
+
const _o = {
|
|
76
|
+
start() { process.stdout.write(` … ${_t}\n`); return _o; },
|
|
77
|
+
succeed(msg) { process.stdout.write(` ✓ ${msg || _t}\n`); return _o; },
|
|
78
|
+
fail(msg) { process.stdout.write(` ✗ ${msg || _t}\n`); return _o; },
|
|
79
|
+
warn(msg) { process.stdout.write(` ⚠ ${msg || _t}\n`); return _o; },
|
|
80
|
+
stop() { return _o; },
|
|
81
|
+
update(t) { _t = t; return _o; },
|
|
82
|
+
};
|
|
83
|
+
return _o;
|
|
84
|
+
};
|
|
85
|
+
_fx = {
|
|
86
|
+
spinner: _spinnerStub,
|
|
87
|
+
success: (t) => process.stdout.write(` ✓ ${t}\n`),
|
|
88
|
+
error: (t) => process.stdout.write(` ✗ ${t}\n`),
|
|
89
|
+
warn: (t) => process.stdout.write(` ⚠ ${t}\n`),
|
|
90
|
+
info: (t) => process.stdout.write(` ${t}\n`),
|
|
91
|
+
dim: (t) => process.stdout.write(` ${t}\n`),
|
|
92
|
+
step: (cur, tot, t) => process.stdout.write(`\n [${cur}/${tot}] ${t}\n`),
|
|
93
|
+
banner: (t) => process.stdout.write(`\n ═══ ${t} ═══\n\n`),
|
|
94
|
+
box: (content) => process.stdout.write(`${content}\n`),
|
|
95
|
+
celebrate: (t) => process.stdout.write(` ✨ ${t}\n`),
|
|
96
|
+
loadingSequence: async (steps) => {
|
|
97
|
+
for (const s of steps) {
|
|
98
|
+
process.stdout.write(` … ${s.text}\n`);
|
|
99
|
+
await new Promise(r => setTimeout(r, Math.min(s.duration || 300, 300)));
|
|
100
|
+
process.stdout.write(` ✓ ${s.successText || s.text}\n`);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
gradient: (t) => t,
|
|
104
|
+
sleep: (ms) => new Promise(r => setTimeout(r, ms)),
|
|
105
|
+
clearScreen: _noop,
|
|
106
|
+
nl: () => process.stdout.write('\n'),
|
|
107
|
+
getMode: () => 'plain',
|
|
108
|
+
colors: {},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return _fx;
|
|
112
|
+
}
|
|
113
|
+
|
|
65
114
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
66
115
|
|
|
67
116
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -382,6 +431,13 @@ async function cmdGo(args, opts = {}) {
|
|
|
382
431
|
} catch { /* non-fatal */ }
|
|
383
432
|
}
|
|
384
433
|
|
|
434
|
+
// ── Dispatch visualization ─────────────────────────────────────────────────
|
|
435
|
+
const fxGo = await getFx();
|
|
436
|
+
let dispatchSpinner = null;
|
|
437
|
+
if (fxGo) {
|
|
438
|
+
dispatchSpinner = fxGo.spinner(`Dispatching agent...`).start();
|
|
439
|
+
}
|
|
440
|
+
|
|
385
441
|
const { plan, result } = await runPipeline('go', prompt, {
|
|
386
442
|
files,
|
|
387
443
|
cwd,
|
|
@@ -389,6 +445,11 @@ async function cmdGo(args, opts = {}) {
|
|
|
389
445
|
dryRun,
|
|
390
446
|
});
|
|
391
447
|
|
|
448
|
+
if (dispatchSpinner) {
|
|
449
|
+
const model = plan?._decision?.model || plan?._decision?.provider || 'agent';
|
|
450
|
+
dispatchSpinner.succeed(`Agent dispatched: ${prompt.slice(0, 50)}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
392
453
|
if (dryRun) {
|
|
393
454
|
// formatExecutionPlan already printed by pipeline when verbose/dryRun=true
|
|
394
455
|
console.log('\n(dry-run — not executing)');
|
|
@@ -399,6 +460,7 @@ async function cmdGo(args, opts = {}) {
|
|
|
399
460
|
|
|
400
461
|
// Display result — dual-brain vs single-provider
|
|
401
462
|
if (result.consensus) {
|
|
463
|
+
if (fxGo) fxGo.celebrate('Task complete!');
|
|
402
464
|
console.log(`\nConsensus: ${result.consensus}`);
|
|
403
465
|
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
404
466
|
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
@@ -452,9 +514,15 @@ async function cmdGo(args, opts = {}) {
|
|
|
452
514
|
} else {
|
|
453
515
|
const succeeded = result.status === 'completed';
|
|
454
516
|
const statusLine = succeeded ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
517
|
+
if (succeeded && fxGo) {
|
|
518
|
+
fxGo.celebrate('Task complete!');
|
|
519
|
+
}
|
|
455
520
|
console.log(`\n${statusLine}${result.durationMs != null ? ` in ${(result.durationMs / 1000).toFixed(1)}s` : ''}`);
|
|
456
521
|
if (result.summary) console.log(result.summary);
|
|
457
|
-
if (result.error)
|
|
522
|
+
if (result.error) {
|
|
523
|
+
if (fxGo) fxGo.error(result.error);
|
|
524
|
+
else process.stderr.write(`${result.error}\n`);
|
|
525
|
+
}
|
|
458
526
|
|
|
459
527
|
// Receipt
|
|
460
528
|
const receipt = await getReceipt();
|
|
@@ -540,6 +608,9 @@ async function cmdThink(args) {
|
|
|
540
608
|
const cwd = process.cwd();
|
|
541
609
|
await ensureProfile(cwd);
|
|
542
610
|
|
|
611
|
+
const fxThink = await getFx();
|
|
612
|
+
if (fxThink) fxThink.info('Round 1: GPT analyzing...');
|
|
613
|
+
|
|
543
614
|
const { result, verification } = await runPipeline('think', question, {
|
|
544
615
|
cwd,
|
|
545
616
|
verbose: true,
|
|
@@ -548,12 +619,17 @@ async function cmdThink(args) {
|
|
|
548
619
|
if (!result) return;
|
|
549
620
|
|
|
550
621
|
if (result.consensus) {
|
|
622
|
+
if (fxThink) fxThink.success('Round 1 complete');
|
|
551
623
|
console.log(`\nConsensus: ${result.consensus}`);
|
|
552
624
|
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
553
625
|
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
554
626
|
} else {
|
|
627
|
+
if (fxThink) fxThink.success('Round 1 complete');
|
|
555
628
|
if (result.summary) console.log(`\n${result.summary}`);
|
|
556
|
-
if (result.error)
|
|
629
|
+
if (result.error) {
|
|
630
|
+
if (fxThink) fxThink.error(result.error);
|
|
631
|
+
else process.stderr.write(`${result.error}\n`);
|
|
632
|
+
}
|
|
557
633
|
if (result.status && result.status !== 'completed') process.exit(1);
|
|
558
634
|
}
|
|
559
635
|
|
|
@@ -705,12 +781,15 @@ async function cmdStatus(args = []) {
|
|
|
705
781
|
const { states } = getHealth(cwd);
|
|
706
782
|
const sessionStats = getSessionStats(cwd);
|
|
707
783
|
|
|
784
|
+
const fxSt = await getFx();
|
|
785
|
+
|
|
708
786
|
console.log('=== Dual-Brain Status ===\n');
|
|
709
787
|
|
|
710
788
|
// Providers + health
|
|
711
789
|
console.log('Providers:');
|
|
712
790
|
if (providers.length === 0) {
|
|
713
|
-
|
|
791
|
+
if (fxSt) fxSt.warn('(none configured — run: dual-brain init)');
|
|
792
|
+
else console.log(' (none configured — run: dual-brain init)');
|
|
714
793
|
} else {
|
|
715
794
|
for (const p of providers) {
|
|
716
795
|
const label = p.name === 'claude' ? 'Claude' : 'OpenAI';
|
|
@@ -721,7 +800,8 @@ async function cmdStatus(args = []) {
|
|
|
721
800
|
|
|
722
801
|
const planStr = p.plan ? ` plan=${p.plan}` : '';
|
|
723
802
|
if (provStates.length === 0) {
|
|
724
|
-
|
|
803
|
+
const line = ` ${label}${planStr} status=healthy calls=${sess.calls} tokens=${sess.tokens}`;
|
|
804
|
+
if (fxSt) fxSt.success(line.trim()); else console.log(line);
|
|
725
805
|
} else {
|
|
726
806
|
for (const [k, st] of provStates) {
|
|
727
807
|
const modelClass = k.split(':').slice(1).join(':');
|
|
@@ -730,7 +810,14 @@ async function cmdStatus(args = []) {
|
|
|
730
810
|
const remaining = remainingCooldownMinutes(p.name, modelClass, cwd);
|
|
731
811
|
statusStr = remaining > 0 ? `hot (retry in ${remaining}m)` : 'hot (cooling)';
|
|
732
812
|
}
|
|
733
|
-
|
|
813
|
+
const line = ` ${label}${planStr} model=${modelClass} status=${statusStr} calls=${sess.calls} tokens=${sess.tokens}`;
|
|
814
|
+
if (fxSt) {
|
|
815
|
+
if (st.status === 'hot') fxSt.warn(line.trim());
|
|
816
|
+
else if (st.status === 'down') fxSt.error(line.trim());
|
|
817
|
+
else fxSt.success(line.trim());
|
|
818
|
+
} else {
|
|
819
|
+
console.log(line);
|
|
820
|
+
}
|
|
734
821
|
}
|
|
735
822
|
}
|
|
736
823
|
}
|
|
@@ -1568,6 +1655,13 @@ async function mainScreen(rl, ask) {
|
|
|
1568
1655
|
const profile = loadProfile(cwd);
|
|
1569
1656
|
const auth = await detectAuth();
|
|
1570
1657
|
|
|
1658
|
+
// ── Dashboard load animation (full mode only) ─────────────────────────────
|
|
1659
|
+
const fx = await getFx();
|
|
1660
|
+
let dashSpinner = null;
|
|
1661
|
+
if (fx && fx.getMode && fx.getMode() === 'full') {
|
|
1662
|
+
dashSpinner = fx.spinner('Loading dashboard...').start();
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1571
1665
|
const claudeSub = profile?.providers?.claude;
|
|
1572
1666
|
const openaiSub = profile?.providers?.openai;
|
|
1573
1667
|
|
|
@@ -1952,6 +2046,9 @@ async function mainScreen(rl, ask) {
|
|
|
1952
2046
|
process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
|
|
1953
2047
|
}
|
|
1954
2048
|
|
|
2049
|
+
// Resolve dashboard spinner before rendering
|
|
2050
|
+
if (dashSpinner) dashSpinner.succeed('Dashboard ready');
|
|
2051
|
+
|
|
1955
2052
|
process.stdout.write(lines.join('\n') + '\n\n');
|
|
1956
2053
|
|
|
1957
2054
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
@@ -2686,6 +2783,29 @@ async function settingsScreen(rl, ask) {
|
|
|
2686
2783
|
const _stC = typeof _stCal.corrections === 'number' ? _stCal.corrections.toFixed(1) : String(_stCal.corrections ?? 3);
|
|
2687
2784
|
const _stA = typeof _stCal.autonomy === 'number' ? _stCal.autonomy.toFixed(1) : String(_stCal.autonomy ?? 3);
|
|
2688
2785
|
|
|
2786
|
+
// Cost efficiency summary (graceful — only shown when data exists)
|
|
2787
|
+
let _stEffScore = null;
|
|
2788
|
+
let _stEffRate = null;
|
|
2789
|
+
let _stEffTrend = null;
|
|
2790
|
+
let _stEffTier = null;
|
|
2791
|
+
try {
|
|
2792
|
+
const _stCt = await import('../src/cost-tracker.mjs');
|
|
2793
|
+
const _stSummary = _stCt.getCostSummary(cwd, 7);
|
|
2794
|
+
if (_stSummary.totalActions > 0) {
|
|
2795
|
+
_stEffScore = _stCt.getEfficiencyScore(cwd);
|
|
2796
|
+
_stEffRate = Math.round(_stSummary.savingsRate * 100);
|
|
2797
|
+
_stEffTrend = _stSummary.trend;
|
|
2798
|
+
const tierOrder = ['recall', 'quick', 'standard', 'deep', 'ultra'];
|
|
2799
|
+
const _stTierKeys = tierOrder.filter(k => _stSummary.byTier[k]);
|
|
2800
|
+
_stEffTier = _stTierKeys.map(k => {
|
|
2801
|
+
const t = _stSummary.byTier[k];
|
|
2802
|
+
return `${k.padEnd(8)} ${String(t.count).padStart(3)}`;
|
|
2803
|
+
}).join(' ');
|
|
2804
|
+
}
|
|
2805
|
+
} catch { /* non-fatal */ }
|
|
2806
|
+
|
|
2807
|
+
const _stTrendIcon = _stEffTrend === 'improving' ? '↗' : _stEffTrend === 'degrading' ? '↘' : '→';
|
|
2808
|
+
|
|
2689
2809
|
const lines = [
|
|
2690
2810
|
top,
|
|
2691
2811
|
row('Settings'),
|
|
@@ -2702,6 +2822,12 @@ async function settingsScreen(rl, ask) {
|
|
|
2702
2822
|
row('User Calibration'),
|
|
2703
2823
|
row(` Specificity: ${_stS} Corrections: ${_stC} Autonomy: ${_stA}`),
|
|
2704
2824
|
row(` Level: ${_stLevel} · Style: ${_stStyle}`),
|
|
2825
|
+
...(_stEffScore !== null ? [
|
|
2826
|
+
sep,
|
|
2827
|
+
row('Cost Efficiency (7 days)'),
|
|
2828
|
+
row(` Score: ${_stEffScore}/100 Savings: ${_stEffRate}% Trend: ${_stTrendIcon} ${_stEffTrend}`),
|
|
2829
|
+
...(_stEffTier ? [row(` Tiers: ${_stEffTier}`)] : []),
|
|
2830
|
+
] : []),
|
|
2705
2831
|
sep,
|
|
2706
2832
|
row('[1-3] change style [r] reset calibration [b] back'),
|
|
2707
2833
|
row('[m] subscriptions [e] sessions [x] diagnostics'),
|
|
@@ -3078,115 +3204,194 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
3078
3204
|
// ─── Onboarding Wizard ───────────────────────────────────────────────────────
|
|
3079
3205
|
|
|
3080
3206
|
/**
|
|
3081
|
-
*
|
|
3082
|
-
*
|
|
3083
|
-
*
|
|
3207
|
+
* Animated first-run setup wizard.
|
|
3208
|
+
* 5 steps: welcome → env scan → replit-tools → import → work style → ready.
|
|
3209
|
+
* Uses src/fx.mjs when available; falls back to plain output stubs.
|
|
3210
|
+
*
|
|
3211
|
+
* @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
|
|
3084
3212
|
* @param {string} cwd
|
|
3085
3213
|
* @param {object} rl readline interface
|
|
3086
3214
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
3087
3215
|
*/
|
|
3088
3216
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
3089
3217
|
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
3090
|
-
const
|
|
3091
|
-
|
|
3092
|
-
//
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3218
|
+
const fx = await getFx();
|
|
3219
|
+
|
|
3220
|
+
// ─── Step 1: Welcome banner ────────────────────────────────────────────────
|
|
3221
|
+
fx.clearScreen();
|
|
3222
|
+
fx.banner('🧠 DUAL-BRAIN');
|
|
3223
|
+
fx.nl();
|
|
3224
|
+
fx.info("Welcome! Let's set up your AI work partner.");
|
|
3225
|
+
fx.nl();
|
|
3226
|
+
await fx.sleep(800);
|
|
3227
|
+
|
|
3228
|
+
// ─── Step 2: Environment detection ────────────────────────────────────────
|
|
3229
|
+
fx.step(1, 5, 'Scanning environment');
|
|
3230
|
+
fx.nl();
|
|
3231
|
+
|
|
3232
|
+
// Run capability detection in parallel with the animations
|
|
3233
|
+
const capsPromise = detectCapabilities(cwd);
|
|
3234
|
+
|
|
3235
|
+
await fx.loadingSequence([
|
|
3236
|
+
{ text: 'Detecting container...', duration: 500, successText: 'Replit container detected' },
|
|
3237
|
+
{ text: 'Checking CLI tools...', duration: 400, successText: 'CLI tools available (git, node, claude...)' },
|
|
3238
|
+
{ text: 'Scanning secrets...', duration: 350, successText: 'Environment scanned' },
|
|
3239
|
+
]);
|
|
3110
3240
|
|
|
3111
|
-
//
|
|
3112
|
-
const caps
|
|
3241
|
+
// Await actual capability data
|
|
3242
|
+
const caps = await capsPromise;
|
|
3113
3243
|
const claudeReady = caps.claude.available;
|
|
3114
3244
|
const openaiReady = caps.openai.available;
|
|
3115
3245
|
const codexAvailable = caps.codex.available;
|
|
3116
3246
|
|
|
3117
|
-
//
|
|
3247
|
+
// Override the generic "secrets" success with real data
|
|
3248
|
+
const secretsLine = claudeReady || openaiReady
|
|
3249
|
+
? 'API keys configured'
|
|
3250
|
+
: 'No API keys found — configure later';
|
|
3251
|
+
fx.info(secretsLine);
|
|
3252
|
+
fx.nl();
|
|
3253
|
+
|
|
3254
|
+
// ─── Step 3: Detect replit-tools ──────────────────────────────────────────
|
|
3255
|
+
fx.step(2, 5, 'Detecting replit-tools');
|
|
3256
|
+
fx.nl();
|
|
3257
|
+
|
|
3118
3258
|
const rt = detectReplitTools(cwd);
|
|
3259
|
+
const rtSpinner = fx.spinner('Looking for replit-tools...').start();
|
|
3260
|
+
await fx.sleep(700);
|
|
3119
3261
|
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3262
|
+
let rtSessionCount = 0;
|
|
3263
|
+
if (rt.installed) {
|
|
3264
|
+
const vStr = rt.version ? ` v${rt.version}` : '';
|
|
3265
|
+
rtSpinner.succeed(`replit-tools${vStr} detected`);
|
|
3266
|
+
// Count available sessions
|
|
3267
|
+
try {
|
|
3268
|
+
const sessions = importReplitSessions(cwd);
|
|
3269
|
+
rtSessionCount = sessions.length;
|
|
3270
|
+
} catch { /* non-fatal */ }
|
|
3271
|
+
} else {
|
|
3272
|
+
rtSpinner.warn('replit-tools not found — install with: npm i -g replit-tools');
|
|
3273
|
+
}
|
|
3274
|
+
fx.nl();
|
|
3124
3275
|
|
|
3125
|
-
//
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
} else if (!claudeReady && (openaiReady || codexAvailable)) {
|
|
3154
|
-
console.log(` ${DIM}Note: Use within Claude Code for full dual-brain${RESET}`);
|
|
3155
|
-
console.log('');
|
|
3276
|
+
// ─── Step 4: Import conversations ─────────────────────────────────────────
|
|
3277
|
+
fx.step(3, 5, 'Import conversations');
|
|
3278
|
+
fx.nl();
|
|
3279
|
+
|
|
3280
|
+
if (rt.installed && rtSessionCount > 0) {
|
|
3281
|
+
fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
|
|
3282
|
+
fx.nl();
|
|
3283
|
+
|
|
3284
|
+
// Ask user — line-based input since we may not have raw mode here
|
|
3285
|
+
process.stdout.write(' Import conversations? [y/N]: ');
|
|
3286
|
+
const importChoice = (await ask('')).trim().toLowerCase();
|
|
3287
|
+
|
|
3288
|
+
if (importChoice === 'y' || importChoice === 'yes') {
|
|
3289
|
+
const importSpinner = fx.spinner('Importing sessions...').start();
|
|
3290
|
+
await fx.sleep(600);
|
|
3291
|
+
try {
|
|
3292
|
+
// Sessions are already imported via importReplitSessions above (lazy-loaded)
|
|
3293
|
+
importSpinner.succeed(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} imported`);
|
|
3294
|
+
} catch (e) {
|
|
3295
|
+
importSpinner.fail(`Import failed: ${e.message}`);
|
|
3296
|
+
}
|
|
3297
|
+
} else {
|
|
3298
|
+
fx.dim('Skipped — you can import later from Settings → Import');
|
|
3299
|
+
}
|
|
3300
|
+
} else if (rt.installed) {
|
|
3301
|
+
fx.dim('No sessions to import');
|
|
3302
|
+
} else {
|
|
3303
|
+
fx.dim('Skipping — replit-tools not found');
|
|
3156
3304
|
}
|
|
3305
|
+
fx.nl();
|
|
3157
3306
|
|
|
3158
|
-
//
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
console.log(wRow(' 3 🔥 Full Power — deep reasoning, dual-brain when it matters'));
|
|
3167
|
-
console.log(wBottom);
|
|
3168
|
-
console.log('');
|
|
3307
|
+
// ─── Step 5: Work style selection ─────────────────────────────────────────
|
|
3308
|
+
fx.step(4, 5, 'Choose your style');
|
|
3309
|
+
fx.nl();
|
|
3310
|
+
process.stdout.write(' How do you want to work?\n\n');
|
|
3311
|
+
process.stdout.write(' [1] ⚡ Fast — speed over caution, auto-execute\n');
|
|
3312
|
+
process.stdout.write(' [2] ⚖️ Balanced — smart routing, reviews when it matters\n');
|
|
3313
|
+
process.stdout.write(' [3] 🔒 Thorough — dual-brain everything, max quality\n');
|
|
3314
|
+
fx.nl();
|
|
3169
3315
|
|
|
3170
|
-
const styleChoice = (await ask(' Choice [2]: ')).trim();
|
|
3171
3316
|
const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
3172
|
-
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': '
|
|
3317
|
+
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Thorough' };
|
|
3318
|
+
|
|
3319
|
+
let styleChoice = '2'; // default
|
|
3320
|
+
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
3321
|
+
|
|
3322
|
+
if (isTTY) {
|
|
3323
|
+
// Raw keypress — single character
|
|
3324
|
+
const { emitKeypressEvents } = await import('node:readline');
|
|
3325
|
+
emitKeypressEvents(process.stdin, rl);
|
|
3326
|
+
|
|
3327
|
+
process.stdout.write(' Choice [2]: ');
|
|
3328
|
+
styleChoice = await new Promise((resolve) => {
|
|
3329
|
+
const wasRaw = process.stdin.isRaw;
|
|
3330
|
+
process.stdin.setRawMode(true);
|
|
3331
|
+
|
|
3332
|
+
const cleanup = () => {
|
|
3333
|
+
process.stdin.removeListener('keypress', onKey);
|
|
3334
|
+
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
3335
|
+
};
|
|
3336
|
+
|
|
3337
|
+
const onKey = (str, key) => {
|
|
3338
|
+
if (!key) return;
|
|
3339
|
+
const name = key.name || '';
|
|
3340
|
+
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
3341
|
+
cleanup();
|
|
3342
|
+
process.stdout.write('\n');
|
|
3343
|
+
resolve('2');
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
if (name === 'return' || name === 'enter') {
|
|
3347
|
+
cleanup();
|
|
3348
|
+
process.stdout.write('\n');
|
|
3349
|
+
resolve('2');
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
if (str === '1' || str === '2' || str === '3') {
|
|
3353
|
+
cleanup();
|
|
3354
|
+
process.stdout.write(`${str}\n`);
|
|
3355
|
+
resolve(str);
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
|
|
3360
|
+
process.stdin.on('keypress', onKey);
|
|
3361
|
+
});
|
|
3362
|
+
} else {
|
|
3363
|
+
// Fallback: line-based prompt
|
|
3364
|
+
process.stdout.write(' Choice [2]: ');
|
|
3365
|
+
styleChoice = (await ask('')).trim() || '2';
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3173
3368
|
const chosenBias = styleMap[styleChoice] || 'balanced';
|
|
3174
3369
|
const chosenName = styleNames[chosenBias];
|
|
3370
|
+
fx.nl();
|
|
3175
3371
|
|
|
3176
|
-
//
|
|
3372
|
+
// Non-blocking note if metered API detected
|
|
3177
3373
|
if (openaiReady && caps.openai.metered) {
|
|
3178
|
-
|
|
3179
|
-
|
|
3374
|
+
const DIM = '\x1b[2m'; const RESET = '\x1b[0m';
|
|
3375
|
+
process.stdout.write(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}\n\n`);
|
|
3180
3376
|
}
|
|
3181
3377
|
|
|
3182
|
-
//
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3378
|
+
// ─── Step 6: Ready ────────────────────────────────────────────────────────
|
|
3379
|
+
fx.step(5, 5, 'Ready!');
|
|
3380
|
+
fx.nl();
|
|
3381
|
+
|
|
3382
|
+
// Init living docs
|
|
3383
|
+
try {
|
|
3384
|
+
const ld = await getLivingDocs();
|
|
3385
|
+
if (ld.initLivingDocs) ld.initLivingDocs(cwd);
|
|
3386
|
+
} catch { /* non-fatal */ }
|
|
3387
|
+
|
|
3388
|
+
await fx.sleep(400);
|
|
3389
|
+
fx.celebrate(`dual-brain is ready! (${chosenName} mode)`);
|
|
3390
|
+
fx.nl();
|
|
3391
|
+
fx.info('Type anything to get started. Your AI partner is listening.');
|
|
3392
|
+
await fx.sleep(1200);
|
|
3188
3393
|
|
|
3189
|
-
//
|
|
3394
|
+
// ─── Build and return the profile object ──────────────────────────────────
|
|
3190
3395
|
const finalProfile = loadProfile(cwd);
|
|
3191
3396
|
|
|
3192
3397
|
finalProfile.providers.claude = { enabled: claudeReady };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.30",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -77,9 +77,12 @@
|
|
|
77
77
|
"src/awareness.mjs",
|
|
78
78
|
"src/tui.mjs",
|
|
79
79
|
"src/living-docs.mjs",
|
|
80
|
+
"src/cost-tracker.mjs",
|
|
81
|
+
"src/think-engine.mjs",
|
|
80
82
|
"src/install-hooks.mjs",
|
|
81
83
|
"src/update-check.mjs",
|
|
82
84
|
"src/prompt-intel.mjs",
|
|
85
|
+
"src/fx.mjs",
|
|
83
86
|
"bin/*.mjs",
|
|
84
87
|
"hooks/enforce-tier.mjs",
|
|
85
88
|
"hooks/cost-logger.mjs",
|