dual-brain 7.1.28 → 7.1.29
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 +265 -89
- package/package.json +2 -1
- package/src/fx.mjs +276 -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 ──────────────────────────────────────────────────────────
|
|
@@ -3078,115 +3175,194 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
3078
3175
|
// ─── Onboarding Wizard ───────────────────────────────────────────────────────
|
|
3079
3176
|
|
|
3080
3177
|
/**
|
|
3081
|
-
*
|
|
3082
|
-
*
|
|
3083
|
-
*
|
|
3178
|
+
* Animated first-run setup wizard.
|
|
3179
|
+
* 5 steps: welcome → env scan → replit-tools → import → work style → ready.
|
|
3180
|
+
* Uses src/fx.mjs when available; falls back to plain output stubs.
|
|
3181
|
+
*
|
|
3182
|
+
* @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
|
|
3084
3183
|
* @param {string} cwd
|
|
3085
3184
|
* @param {object} rl readline interface
|
|
3086
3185
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
3087
3186
|
*/
|
|
3088
3187
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
3089
3188
|
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
|
-
|
|
3189
|
+
const fx = await getFx();
|
|
3190
|
+
|
|
3191
|
+
// ─── Step 1: Welcome banner ────────────────────────────────────────────────
|
|
3192
|
+
fx.clearScreen();
|
|
3193
|
+
fx.banner('🧠 DUAL-BRAIN');
|
|
3194
|
+
fx.nl();
|
|
3195
|
+
fx.info("Welcome! Let's set up your AI work partner.");
|
|
3196
|
+
fx.nl();
|
|
3197
|
+
await fx.sleep(800);
|
|
3198
|
+
|
|
3199
|
+
// ─── Step 2: Environment detection ────────────────────────────────────────
|
|
3200
|
+
fx.step(1, 5, 'Scanning environment');
|
|
3201
|
+
fx.nl();
|
|
3202
|
+
|
|
3203
|
+
// Run capability detection in parallel with the animations
|
|
3204
|
+
const capsPromise = detectCapabilities(cwd);
|
|
3205
|
+
|
|
3206
|
+
await fx.loadingSequence([
|
|
3207
|
+
{ text: 'Detecting container...', duration: 500, successText: 'Replit container detected' },
|
|
3208
|
+
{ text: 'Checking CLI tools...', duration: 400, successText: 'CLI tools available (git, node, claude...)' },
|
|
3209
|
+
{ text: 'Scanning secrets...', duration: 350, successText: 'Environment scanned' },
|
|
3210
|
+
]);
|
|
3110
3211
|
|
|
3111
|
-
//
|
|
3112
|
-
const caps
|
|
3212
|
+
// Await actual capability data
|
|
3213
|
+
const caps = await capsPromise;
|
|
3113
3214
|
const claudeReady = caps.claude.available;
|
|
3114
3215
|
const openaiReady = caps.openai.available;
|
|
3115
3216
|
const codexAvailable = caps.codex.available;
|
|
3116
3217
|
|
|
3117
|
-
//
|
|
3218
|
+
// Override the generic "secrets" success with real data
|
|
3219
|
+
const secretsLine = claudeReady || openaiReady
|
|
3220
|
+
? 'API keys configured'
|
|
3221
|
+
: 'No API keys found — configure later';
|
|
3222
|
+
fx.info(secretsLine);
|
|
3223
|
+
fx.nl();
|
|
3224
|
+
|
|
3225
|
+
// ─── Step 3: Detect replit-tools ──────────────────────────────────────────
|
|
3226
|
+
fx.step(2, 5, 'Detecting replit-tools');
|
|
3227
|
+
fx.nl();
|
|
3228
|
+
|
|
3118
3229
|
const rt = detectReplitTools(cwd);
|
|
3230
|
+
const rtSpinner = fx.spinner('Looking for replit-tools...').start();
|
|
3231
|
+
await fx.sleep(700);
|
|
3119
3232
|
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3233
|
+
let rtSessionCount = 0;
|
|
3234
|
+
if (rt.installed) {
|
|
3235
|
+
const vStr = rt.version ? ` v${rt.version}` : '';
|
|
3236
|
+
rtSpinner.succeed(`replit-tools${vStr} detected`);
|
|
3237
|
+
// Count available sessions
|
|
3238
|
+
try {
|
|
3239
|
+
const sessions = importReplitSessions(cwd);
|
|
3240
|
+
rtSessionCount = sessions.length;
|
|
3241
|
+
} catch { /* non-fatal */ }
|
|
3242
|
+
} else {
|
|
3243
|
+
rtSpinner.warn('replit-tools not found — install with: npm i -g replit-tools');
|
|
3244
|
+
}
|
|
3245
|
+
fx.nl();
|
|
3124
3246
|
|
|
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('');
|
|
3247
|
+
// ─── Step 4: Import conversations ─────────────────────────────────────────
|
|
3248
|
+
fx.step(3, 5, 'Import conversations');
|
|
3249
|
+
fx.nl();
|
|
3250
|
+
|
|
3251
|
+
if (rt.installed && rtSessionCount > 0) {
|
|
3252
|
+
fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
|
|
3253
|
+
fx.nl();
|
|
3254
|
+
|
|
3255
|
+
// Ask user — line-based input since we may not have raw mode here
|
|
3256
|
+
process.stdout.write(' Import conversations? [y/N]: ');
|
|
3257
|
+
const importChoice = (await ask('')).trim().toLowerCase();
|
|
3258
|
+
|
|
3259
|
+
if (importChoice === 'y' || importChoice === 'yes') {
|
|
3260
|
+
const importSpinner = fx.spinner('Importing sessions...').start();
|
|
3261
|
+
await fx.sleep(600);
|
|
3262
|
+
try {
|
|
3263
|
+
// Sessions are already imported via importReplitSessions above (lazy-loaded)
|
|
3264
|
+
importSpinner.succeed(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} imported`);
|
|
3265
|
+
} catch (e) {
|
|
3266
|
+
importSpinner.fail(`Import failed: ${e.message}`);
|
|
3267
|
+
}
|
|
3268
|
+
} else {
|
|
3269
|
+
fx.dim('Skipped — you can import later from Settings → Import');
|
|
3270
|
+
}
|
|
3271
|
+
} else if (rt.installed) {
|
|
3272
|
+
fx.dim('No sessions to import');
|
|
3273
|
+
} else {
|
|
3274
|
+
fx.dim('Skipping — replit-tools not found');
|
|
3156
3275
|
}
|
|
3276
|
+
fx.nl();
|
|
3157
3277
|
|
|
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('');
|
|
3278
|
+
// ─── Step 5: Work style selection ─────────────────────────────────────────
|
|
3279
|
+
fx.step(4, 5, 'Choose your style');
|
|
3280
|
+
fx.nl();
|
|
3281
|
+
process.stdout.write(' How do you want to work?\n\n');
|
|
3282
|
+
process.stdout.write(' [1] ⚡ Fast — speed over caution, auto-execute\n');
|
|
3283
|
+
process.stdout.write(' [2] ⚖️ Balanced — smart routing, reviews when it matters\n');
|
|
3284
|
+
process.stdout.write(' [3] 🔒 Thorough — dual-brain everything, max quality\n');
|
|
3285
|
+
fx.nl();
|
|
3169
3286
|
|
|
3170
|
-
const styleChoice = (await ask(' Choice [2]: ')).trim();
|
|
3171
3287
|
const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
3172
|
-
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': '
|
|
3288
|
+
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Thorough' };
|
|
3289
|
+
|
|
3290
|
+
let styleChoice = '2'; // default
|
|
3291
|
+
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
3292
|
+
|
|
3293
|
+
if (isTTY) {
|
|
3294
|
+
// Raw keypress — single character
|
|
3295
|
+
const { emitKeypressEvents } = await import('node:readline');
|
|
3296
|
+
emitKeypressEvents(process.stdin, rl);
|
|
3297
|
+
|
|
3298
|
+
process.stdout.write(' Choice [2]: ');
|
|
3299
|
+
styleChoice = await new Promise((resolve) => {
|
|
3300
|
+
const wasRaw = process.stdin.isRaw;
|
|
3301
|
+
process.stdin.setRawMode(true);
|
|
3302
|
+
|
|
3303
|
+
const cleanup = () => {
|
|
3304
|
+
process.stdin.removeListener('keypress', onKey);
|
|
3305
|
+
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
3306
|
+
};
|
|
3307
|
+
|
|
3308
|
+
const onKey = (str, key) => {
|
|
3309
|
+
if (!key) return;
|
|
3310
|
+
const name = key.name || '';
|
|
3311
|
+
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
3312
|
+
cleanup();
|
|
3313
|
+
process.stdout.write('\n');
|
|
3314
|
+
resolve('2');
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
if (name === 'return' || name === 'enter') {
|
|
3318
|
+
cleanup();
|
|
3319
|
+
process.stdout.write('\n');
|
|
3320
|
+
resolve('2');
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
if (str === '1' || str === '2' || str === '3') {
|
|
3324
|
+
cleanup();
|
|
3325
|
+
process.stdout.write(`${str}\n`);
|
|
3326
|
+
resolve(str);
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
|
|
3331
|
+
process.stdin.on('keypress', onKey);
|
|
3332
|
+
});
|
|
3333
|
+
} else {
|
|
3334
|
+
// Fallback: line-based prompt
|
|
3335
|
+
process.stdout.write(' Choice [2]: ');
|
|
3336
|
+
styleChoice = (await ask('')).trim() || '2';
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3173
3339
|
const chosenBias = styleMap[styleChoice] || 'balanced';
|
|
3174
3340
|
const chosenName = styleNames[chosenBias];
|
|
3341
|
+
fx.nl();
|
|
3175
3342
|
|
|
3176
|
-
//
|
|
3343
|
+
// Non-blocking note if metered API detected
|
|
3177
3344
|
if (openaiReady && caps.openai.metered) {
|
|
3178
|
-
|
|
3179
|
-
|
|
3345
|
+
const DIM = '\x1b[2m'; const RESET = '\x1b[0m';
|
|
3346
|
+
process.stdout.write(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}\n\n`);
|
|
3180
3347
|
}
|
|
3181
3348
|
|
|
3182
|
-
//
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3349
|
+
// ─── Step 6: Ready ────────────────────────────────────────────────────────
|
|
3350
|
+
fx.step(5, 5, 'Ready!');
|
|
3351
|
+
fx.nl();
|
|
3352
|
+
|
|
3353
|
+
// Init living docs
|
|
3354
|
+
try {
|
|
3355
|
+
const ld = await getLivingDocs();
|
|
3356
|
+
if (ld.initLivingDocs) ld.initLivingDocs(cwd);
|
|
3357
|
+
} catch { /* non-fatal */ }
|
|
3358
|
+
|
|
3359
|
+
await fx.sleep(400);
|
|
3360
|
+
fx.celebrate(`dual-brain is ready! (${chosenName} mode)`);
|
|
3361
|
+
fx.nl();
|
|
3362
|
+
fx.info('Type anything to get started. Your AI partner is listening.');
|
|
3363
|
+
await fx.sleep(1200);
|
|
3188
3364
|
|
|
3189
|
-
//
|
|
3365
|
+
// ─── Build and return the profile object ──────────────────────────────────
|
|
3190
3366
|
const finalProfile = loadProfile(cwd);
|
|
3191
3367
|
|
|
3192
3368
|
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.29",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"src/install-hooks.mjs",
|
|
81
81
|
"src/update-check.mjs",
|
|
82
82
|
"src/prompt-intel.mjs",
|
|
83
|
+
"src/fx.mjs",
|
|
83
84
|
"bin/*.mjs",
|
|
84
85
|
"hooks/enforce-tier.mjs",
|
|
85
86
|
"hooks/cost-logger.mjs",
|
package/src/fx.mjs
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// fx.mjs — zero-dependency animated shell effects for dual-brain CLI
|
|
2
|
+
|
|
3
|
+
const isTTY = process.stdout.isTTY && !process.env.CI;
|
|
4
|
+
const hasColor = isTTY && !process.env.NO_COLOR;
|
|
5
|
+
const isUnicode = process.platform !== 'win32' || process.env.WT_SESSION;
|
|
6
|
+
|
|
7
|
+
const c = {
|
|
8
|
+
reset: '\x1b[0m',
|
|
9
|
+
bold: '\x1b[1m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
11
|
+
green: '\x1b[32m',
|
|
12
|
+
red: '\x1b[31m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
blue: '\x1b[34m',
|
|
15
|
+
magenta: '\x1b[35m',
|
|
16
|
+
cyan: '\x1b[36m',
|
|
17
|
+
white: '\x1b[37m',
|
|
18
|
+
gray: '\x1b[90m',
|
|
19
|
+
bgGreen: '\x1b[42m',
|
|
20
|
+
bgRed: '\x1b[41m',
|
|
21
|
+
bgYellow: '\x1b[43m',
|
|
22
|
+
bgBlue: '\x1b[44m',
|
|
23
|
+
bgMagenta: '\x1b[45m',
|
|
24
|
+
clearLine: '\x1b[2K',
|
|
25
|
+
cursorUp: '\x1b[1A',
|
|
26
|
+
cursorHide: '\x1b[?25l',
|
|
27
|
+
cursorShow: '\x1b[?25h',
|
|
28
|
+
saveCursor: '\x1b[s',
|
|
29
|
+
restoreCursor: '\x1b[u'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function color(text, ...styles) {
|
|
33
|
+
if (!hasColor) return text;
|
|
34
|
+
return styles.join('') + text + c.reset;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const colors = c;
|
|
38
|
+
|
|
39
|
+
export function sleep(ms) {
|
|
40
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function clearScreen() {
|
|
44
|
+
if (isTTY) process.stdout.write('\x1b[2J\x1b[H');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function nl(n = 1) {
|
|
48
|
+
process.stdout.write('\n'.repeat(n));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getMode() {
|
|
52
|
+
if (process.env.CI) return 'ci';
|
|
53
|
+
if (!process.stdout.isTTY) return 'plain';
|
|
54
|
+
if (process.env.DUAL_BRAIN_FX === 'subtle') return 'subtle';
|
|
55
|
+
return 'full';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function spinner(text) {
|
|
59
|
+
const frames = isUnicode ? ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'] : ['|','/','-','\\'];
|
|
60
|
+
let i = 0, interval = null, currentText = text;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
start() {
|
|
64
|
+
if (!isTTY) { process.stdout.write(currentText + '\n'); return this; }
|
|
65
|
+
process.stdout.write(c.cursorHide);
|
|
66
|
+
interval = setInterval(() => {
|
|
67
|
+
process.stdout.write(`\r${c.clearLine} ${color(frames[i % frames.length], c.cyan)} ${currentText}`);
|
|
68
|
+
i++;
|
|
69
|
+
}, 80);
|
|
70
|
+
return this;
|
|
71
|
+
},
|
|
72
|
+
update(newText) { currentText = newText; return this; },
|
|
73
|
+
succeed(msg) {
|
|
74
|
+
if (interval) clearInterval(interval);
|
|
75
|
+
const sym = isUnicode ? '✓' : '+';
|
|
76
|
+
process.stdout.write(`\r${c.clearLine} ${color(sym, c.green)} ${msg || currentText}\n`);
|
|
77
|
+
if (isTTY) process.stdout.write(c.cursorShow);
|
|
78
|
+
return this;
|
|
79
|
+
},
|
|
80
|
+
fail(msg) {
|
|
81
|
+
if (interval) clearInterval(interval);
|
|
82
|
+
const sym = isUnicode ? '✗' : 'x';
|
|
83
|
+
process.stdout.write(`\r${c.clearLine} ${color(sym, c.red)} ${msg || currentText}\n`);
|
|
84
|
+
if (isTTY) process.stdout.write(c.cursorShow);
|
|
85
|
+
return this;
|
|
86
|
+
},
|
|
87
|
+
warn(msg) {
|
|
88
|
+
if (interval) clearInterval(interval);
|
|
89
|
+
const sym = isUnicode ? '⚠' : '!';
|
|
90
|
+
process.stdout.write(`\r${c.clearLine} ${color(sym, c.yellow)} ${msg || currentText}\n`);
|
|
91
|
+
if (isTTY) process.stdout.write(c.cursorShow);
|
|
92
|
+
return this;
|
|
93
|
+
},
|
|
94
|
+
stop() {
|
|
95
|
+
if (interval) clearInterval(interval);
|
|
96
|
+
process.stdout.write(`\r${c.clearLine}`);
|
|
97
|
+
if (isTTY) process.stdout.write(c.cursorShow);
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function progress(current, total, label = '', width = 30) {
|
|
104
|
+
const pct = Math.min(1, current / total);
|
|
105
|
+
if (!isTTY) {
|
|
106
|
+
process.stdout.write(`${Math.round(pct * 100)}% ${label}\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const filled = Math.round(pct * width);
|
|
110
|
+
const empty = width - filled;
|
|
111
|
+
const bar = isUnicode
|
|
112
|
+
? color('█'.repeat(filled) + '░'.repeat(empty), c.cyan)
|
|
113
|
+
: color('#'.repeat(filled) + '-'.repeat(empty), c.cyan);
|
|
114
|
+
const pctStr = String(Math.round(pct * 100)).padStart(3) + '%';
|
|
115
|
+
process.stdout.write(`\r${c.clearLine} ${bar} ${color(pctStr, c.bold)} ${label}`);
|
|
116
|
+
if (current >= total) process.stdout.write('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function success(text) {
|
|
120
|
+
const sym = isUnicode ? '✓' : '+';
|
|
121
|
+
process.stdout.write(` ${color(sym, c.green)} ${text}\n`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function error(text) {
|
|
125
|
+
const sym = isUnicode ? '✗' : 'x';
|
|
126
|
+
process.stdout.write(` ${color(sym, c.red)} ${text}\n`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function warn(text) {
|
|
130
|
+
const sym = isUnicode ? '⚠' : '!';
|
|
131
|
+
process.stdout.write(` ${color(sym, c.yellow)} ${text}\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function info(text) {
|
|
135
|
+
const sym = isUnicode ? 'ℹ' : 'i';
|
|
136
|
+
process.stdout.write(` ${color(sym, c.blue)} ${text}\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function dim(text) {
|
|
140
|
+
process.stdout.write(`${color(text, c.gray)}\n`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function step(current, total, text) {
|
|
144
|
+
if (!isUnicode) {
|
|
145
|
+
process.stdout.write(` [${current}/${total}] ${text}\n`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const dots = [];
|
|
149
|
+
for (let i = 1; i <= total; i++) {
|
|
150
|
+
if (i < current) dots.push(color('●', c.green));
|
|
151
|
+
else if (i === current) dots.push(color('●', c.cyan));
|
|
152
|
+
else dots.push(color('○', c.gray));
|
|
153
|
+
}
|
|
154
|
+
process.stdout.write(` ${dots.join(' ')} ${color(`Step ${current} of ${total}`, c.bold)} · ${text}\n`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function banner(text) {
|
|
158
|
+
const pkg = 'DUAL-BRAIN';
|
|
159
|
+
const inner = ` ${isUnicode ? '🧠' : '**'} ${pkg} ${text} `;
|
|
160
|
+
const width = inner.length + 2;
|
|
161
|
+
if (!isUnicode || !hasColor) {
|
|
162
|
+
process.stdout.write(`\n +${'='.repeat(width - 2)}+\n | ${inner} |\n +${'='.repeat(width - 2)}+\n\n`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const top = ` ╔${'═'.repeat(width)}╗`;
|
|
166
|
+
const mid = ` ║${inner}║`;
|
|
167
|
+
const bot = ` ╚${'═'.repeat(width)}╝`;
|
|
168
|
+
process.stdout.write(`\n${color(top, c.cyan, c.bold)}\n${color(mid, c.cyan, c.bold)}\n${color(bot, c.cyan, c.bold)}\n\n`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function box(content, options = {}) {
|
|
172
|
+
const { color: colorName = 'cyan', padding = 1, title = '' } = options;
|
|
173
|
+
const ansiColor = c[colorName] || c.cyan;
|
|
174
|
+
const lines = Array.isArray(content) ? content : content.split('\n');
|
|
175
|
+
const innerWidth = Math.max(...lines.map(l => stripAnsi(l).length), title ? stripAnsi(title).length : 0) + padding * 2;
|
|
176
|
+
|
|
177
|
+
function draw(text, ansi) {
|
|
178
|
+
if (!hasColor) return text;
|
|
179
|
+
return ansi + text + c.reset;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const titleStr = title ? ` ${title} ` : '';
|
|
183
|
+
const topFill = '─'.repeat(Math.max(0, innerWidth - stripAnsi(titleStr).length));
|
|
184
|
+
const top = isUnicode
|
|
185
|
+
? draw(`┌${titleStr}${'─'.repeat(Math.floor(topFill.length / 2))}${'─'.repeat(Math.ceil(topFill.length / 2))}┐`, ansiColor)
|
|
186
|
+
: draw(`+${titleStr}${'-'.repeat(topFill.length)}+`, ansiColor);
|
|
187
|
+
const bot = isUnicode
|
|
188
|
+
? draw(`└${'─'.repeat(innerWidth)}┘`, ansiColor)
|
|
189
|
+
: draw(`+${'-'.repeat(innerWidth)}+`, ansiColor);
|
|
190
|
+
|
|
191
|
+
process.stdout.write(` ${top}\n`);
|
|
192
|
+
for (const line of lines) {
|
|
193
|
+
const pad = ' '.repeat(padding);
|
|
194
|
+
const visible = stripAnsi(line).length;
|
|
195
|
+
const right = ' '.repeat(Math.max(0, innerWidth - padding - visible));
|
|
196
|
+
const border = isUnicode ? draw('│', ansiColor) : draw('|', ansiColor);
|
|
197
|
+
process.stdout.write(` ${border}${pad}${line}${right}${border}\n`);
|
|
198
|
+
}
|
|
199
|
+
process.stdout.write(` ${bot}\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stripAnsi(str) {
|
|
203
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function gradient(text, fromColor = 196, toColor = 226) {
|
|
207
|
+
if (!hasColor) return process.stdout.write(text + '\n');
|
|
208
|
+
const chars = [...text];
|
|
209
|
+
const result = chars.map((ch, i) => {
|
|
210
|
+
const t = chars.length <= 1 ? 0 : i / (chars.length - 1);
|
|
211
|
+
const colorIdx = Math.round(fromColor + t * (toColor - fromColor));
|
|
212
|
+
return `\x1b[38;5;${colorIdx}m${ch}`;
|
|
213
|
+
}).join('') + c.reset;
|
|
214
|
+
process.stdout.write(result + '\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function celebrate(text) {
|
|
218
|
+
const sym = isUnicode ? '✨' : '*';
|
|
219
|
+
if (!isTTY || getMode() === 'ci' || getMode() === 'plain') {
|
|
220
|
+
process.stdout.write(` ${sym} ${text} ${sym}\n`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
process.stdout.write(`\r${c.clearLine} ${color(`${sym} ${text} ${sym}`, c.bgGreen, c.bold)}`);
|
|
224
|
+
await sleep(100);
|
|
225
|
+
process.stdout.write(`\r${c.clearLine} ${color(`${sym} ${text} ${sym}`, c.green, c.bold)}\n`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function loadingSequence(steps) {
|
|
229
|
+
for (const s of steps) {
|
|
230
|
+
const sp = spinner(s.text).start();
|
|
231
|
+
await sleep(s.duration || 800);
|
|
232
|
+
sp.succeed(s.successText || s.text);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function agentDispatch(model, task) {
|
|
237
|
+
const mode = getMode();
|
|
238
|
+
if (mode === 'ci' || mode === 'plain') {
|
|
239
|
+
process.stdout.write(`Dispatching ${model}...\n`);
|
|
240
|
+
process.stdout.write(`Agent dispatched: ${task}\n`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const sp = spinner(`Dispatching ${color(model, c.cyan)}...`).start();
|
|
244
|
+
await sleep(mode === 'subtle' ? 0 : 600);
|
|
245
|
+
sp.succeed(`Agent dispatched: ${task}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function thinkRound(round, provider, question) {
|
|
249
|
+
const mode = getMode();
|
|
250
|
+
const providerLabel = color(provider, c.magenta);
|
|
251
|
+
const roundLabel = color(`Round ${round}`, c.bold);
|
|
252
|
+
|
|
253
|
+
if (mode === 'ci' || mode === 'plain') {
|
|
254
|
+
process.stdout.write(`Dual-Brain Think · ${roundLabel} · ${provider} analyzing: ${question}\n`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const title = `Dual-Brain Think · ${roundLabel}`;
|
|
259
|
+
const titleVisible = stripAnsi(title);
|
|
260
|
+
const width = Math.max(titleVisible.length + 4, question.length + 4, 36);
|
|
261
|
+
const topFill = '─'.repeat(Math.max(0, width - titleVisible.length - 2));
|
|
262
|
+
|
|
263
|
+
if (isUnicode && hasColor) {
|
|
264
|
+
process.stdout.write(` ${color(`╭─ ${title} ${'─'.repeat(topFill.length)}╮`, c.cyan)}\n`);
|
|
265
|
+
process.stdout.write(` ${color('│', c.cyan)} ${isUnicode ? '🤖' : '>>'} ${providerLabel} analyzing...${' '.repeat(Math.max(0, width - 4 - stripAnsi(provider).length - 13))}${color('│', c.cyan)}\n`);
|
|
266
|
+
process.stdout.write(` ${color(`╰${'─'.repeat(width)}╯`, c.cyan)}\n`);
|
|
267
|
+
} else {
|
|
268
|
+
process.stdout.write(` +-- ${title} --+\n`);
|
|
269
|
+
process.stdout.write(` | ${provider} analyzing: ${question}\n`);
|
|
270
|
+
process.stdout.write(` +${'─'.repeat(width + 2)}+\n`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const sp = spinner(`${provider} thinking on: ${question}`).start();
|
|
274
|
+
await sleep(mode === 'subtle' ? 0 : 900);
|
|
275
|
+
sp.succeed(`${provider} analysis complete`);
|
|
276
|
+
}
|