dual-brain 0.1.23 → 0.2.1
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 +729 -262
- package/package.json +18 -2
- package/src/awareness.mjs +360 -0
- package/src/calibration.mjs +148 -0
- package/src/cost-tracker.mjs +184 -0
- package/src/decide.mjs +206 -10
- package/src/detect.mjs +119 -4
- package/src/dispatch.mjs +59 -2
- package/src/doctor.mjs +1031 -1
- package/src/fx.mjs +276 -0
- package/src/health.mjs +82 -0
- package/src/index.mjs +1 -1
- package/src/intelligence.mjs +447 -0
- package/src/ledger.mjs +196 -0
- package/src/living-docs.mjs +210 -0
- package/src/models.mjs +363 -0
- package/src/pipeline.mjs +434 -11
- package/src/profile.mjs +28 -0
- package/src/prompt-intel.mjs +325 -0
- package/src/replit.mjs +1210 -0
- package/src/session.mjs +285 -14
- package/src/think-engine.mjs +428 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -54,6 +54,63 @@ async function getFailureMem() {
|
|
|
54
54
|
return _failureMem;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
let _livingDocs = null;
|
|
58
|
+
async function getLivingDocs() {
|
|
59
|
+
if (!_livingDocs) {
|
|
60
|
+
try { _livingDocs = await import('../src/living-docs.mjs'); } catch { _livingDocs = {}; }
|
|
61
|
+
}
|
|
62
|
+
return _livingDocs;
|
|
63
|
+
}
|
|
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
|
+
|
|
57
114
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
58
115
|
|
|
59
116
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -355,6 +412,12 @@ async function cmdGo(args, opts = {}) {
|
|
|
355
412
|
const cwd = process.cwd();
|
|
356
413
|
await ensureProfile(cwd);
|
|
357
414
|
|
|
415
|
+
// ── Living docs: ensure .dual-brain/ exists on session start ─────────────
|
|
416
|
+
try {
|
|
417
|
+
const ld = await getLivingDocs();
|
|
418
|
+
if (ld.initLivingDocs) ld.initLivingDocs(cwd);
|
|
419
|
+
} catch { /* non-fatal */ }
|
|
420
|
+
|
|
358
421
|
if (verbose) console.log('\nDispatching...');
|
|
359
422
|
|
|
360
423
|
// ── Failure memory: check history before dispatching ──────────────────────
|
|
@@ -368,6 +431,13 @@ async function cmdGo(args, opts = {}) {
|
|
|
368
431
|
} catch { /* non-fatal */ }
|
|
369
432
|
}
|
|
370
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
|
+
|
|
371
441
|
const { plan, result } = await runPipeline('go', prompt, {
|
|
372
442
|
files,
|
|
373
443
|
cwd,
|
|
@@ -375,6 +445,11 @@ async function cmdGo(args, opts = {}) {
|
|
|
375
445
|
dryRun,
|
|
376
446
|
});
|
|
377
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
|
+
|
|
378
453
|
if (dryRun) {
|
|
379
454
|
// formatExecutionPlan already printed by pipeline when verbose/dryRun=true
|
|
380
455
|
console.log('\n(dry-run — not executing)');
|
|
@@ -385,6 +460,7 @@ async function cmdGo(args, opts = {}) {
|
|
|
385
460
|
|
|
386
461
|
// Display result — dual-brain vs single-provider
|
|
387
462
|
if (result.consensus) {
|
|
463
|
+
if (fxGo) fxGo.celebrate('Task complete!');
|
|
388
464
|
console.log(`\nConsensus: ${result.consensus}`);
|
|
389
465
|
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
390
466
|
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
@@ -408,6 +484,16 @@ async function cmdGo(args, opts = {}) {
|
|
|
408
484
|
nextAction: null,
|
|
409
485
|
}, cwd);
|
|
410
486
|
|
|
487
|
+
// ── Living docs: record completed session action ───────────────────────
|
|
488
|
+
try {
|
|
489
|
+
const ld = await getLivingDocs();
|
|
490
|
+
if (ld.appendAction) ld.appendAction({
|
|
491
|
+
type: 'task', intent: prompt, status: 'completed',
|
|
492
|
+
owner: plan?._decision?.provider ?? 'claude',
|
|
493
|
+
files, result: result.consensus || 'dual-brain complete',
|
|
494
|
+
}, cwd);
|
|
495
|
+
} catch { /* non-fatal */ }
|
|
496
|
+
|
|
411
497
|
// Clear failure memory on success
|
|
412
498
|
if (failureMem.clearFailures) {
|
|
413
499
|
try { await failureMem.clearFailures(prompt, cwd); } catch { /* non-fatal */ }
|
|
@@ -428,9 +514,15 @@ async function cmdGo(args, opts = {}) {
|
|
|
428
514
|
} else {
|
|
429
515
|
const succeeded = result.status === 'completed';
|
|
430
516
|
const statusLine = succeeded ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
517
|
+
if (succeeded && fxGo) {
|
|
518
|
+
fxGo.celebrate('Task complete!');
|
|
519
|
+
}
|
|
431
520
|
console.log(`\n${statusLine}${result.durationMs != null ? ` in ${(result.durationMs / 1000).toFixed(1)}s` : ''}`);
|
|
432
521
|
if (result.summary) console.log(result.summary);
|
|
433
|
-
if (result.error)
|
|
522
|
+
if (result.error) {
|
|
523
|
+
if (fxGo) fxGo.error(result.error);
|
|
524
|
+
else process.stderr.write(`${result.error}\n`);
|
|
525
|
+
}
|
|
434
526
|
|
|
435
527
|
// Receipt
|
|
436
528
|
const receipt = await getReceipt();
|
|
@@ -459,6 +551,17 @@ async function cmdGo(args, opts = {}) {
|
|
|
459
551
|
nextAction: null,
|
|
460
552
|
}, cwd);
|
|
461
553
|
|
|
554
|
+
// ── Living docs: record completed session action ───────────────────────
|
|
555
|
+
try {
|
|
556
|
+
const ld = await getLivingDocs();
|
|
557
|
+
if (ld.appendAction) ld.appendAction({
|
|
558
|
+
type: 'task', intent: prompt, status: succeeded ? 'completed' : 'failed',
|
|
559
|
+
owner: plan?._decision?.provider ?? 'claude',
|
|
560
|
+
files: result.filesChanged || files,
|
|
561
|
+
result: result.summary || (succeeded ? 'completed' : `exit ${result.exitCode}`),
|
|
562
|
+
}, cwd);
|
|
563
|
+
} catch { /* non-fatal */ }
|
|
564
|
+
|
|
462
565
|
if (!succeeded) {
|
|
463
566
|
// Record failure memory
|
|
464
567
|
if (failureMem.recordFailure) {
|
|
@@ -505,6 +608,9 @@ async function cmdThink(args) {
|
|
|
505
608
|
const cwd = process.cwd();
|
|
506
609
|
await ensureProfile(cwd);
|
|
507
610
|
|
|
611
|
+
const fxThink = await getFx();
|
|
612
|
+
if (fxThink) fxThink.info('Round 1: GPT analyzing...');
|
|
613
|
+
|
|
508
614
|
const { result, verification } = await runPipeline('think', question, {
|
|
509
615
|
cwd,
|
|
510
616
|
verbose: true,
|
|
@@ -513,12 +619,17 @@ async function cmdThink(args) {
|
|
|
513
619
|
if (!result) return;
|
|
514
620
|
|
|
515
621
|
if (result.consensus) {
|
|
622
|
+
if (fxThink) fxThink.success('Round 1 complete');
|
|
516
623
|
console.log(`\nConsensus: ${result.consensus}`);
|
|
517
624
|
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
518
625
|
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
519
626
|
} else {
|
|
627
|
+
if (fxThink) fxThink.success('Round 1 complete');
|
|
520
628
|
if (result.summary) console.log(`\n${result.summary}`);
|
|
521
|
-
if (result.error)
|
|
629
|
+
if (result.error) {
|
|
630
|
+
if (fxThink) fxThink.error(result.error);
|
|
631
|
+
else process.stderr.write(`${result.error}\n`);
|
|
632
|
+
}
|
|
522
633
|
if (result.status && result.status !== 'completed') process.exit(1);
|
|
523
634
|
}
|
|
524
635
|
|
|
@@ -670,12 +781,15 @@ async function cmdStatus(args = []) {
|
|
|
670
781
|
const { states } = getHealth(cwd);
|
|
671
782
|
const sessionStats = getSessionStats(cwd);
|
|
672
783
|
|
|
784
|
+
const fxSt = await getFx();
|
|
785
|
+
|
|
673
786
|
console.log('=== Dual-Brain Status ===\n');
|
|
674
787
|
|
|
675
788
|
// Providers + health
|
|
676
789
|
console.log('Providers:');
|
|
677
790
|
if (providers.length === 0) {
|
|
678
|
-
|
|
791
|
+
if (fxSt) fxSt.warn('(none configured — run: dual-brain init)');
|
|
792
|
+
else console.log(' (none configured — run: dual-brain init)');
|
|
679
793
|
} else {
|
|
680
794
|
for (const p of providers) {
|
|
681
795
|
const label = p.name === 'claude' ? 'Claude' : 'OpenAI';
|
|
@@ -686,7 +800,8 @@ async function cmdStatus(args = []) {
|
|
|
686
800
|
|
|
687
801
|
const planStr = p.plan ? ` plan=${p.plan}` : '';
|
|
688
802
|
if (provStates.length === 0) {
|
|
689
|
-
|
|
803
|
+
const line = ` ${label}${planStr} status=healthy calls=${sess.calls} tokens=${sess.tokens}`;
|
|
804
|
+
if (fxSt) fxSt.success(line.trim()); else console.log(line);
|
|
690
805
|
} else {
|
|
691
806
|
for (const [k, st] of provStates) {
|
|
692
807
|
const modelClass = k.split(':').slice(1).join(':');
|
|
@@ -695,7 +810,14 @@ async function cmdStatus(args = []) {
|
|
|
695
810
|
const remaining = remainingCooldownMinutes(p.name, modelClass, cwd);
|
|
696
811
|
statusStr = remaining > 0 ? `hot (retry in ${remaining}m)` : 'hot (cooling)';
|
|
697
812
|
}
|
|
698
|
-
|
|
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
|
+
}
|
|
699
821
|
}
|
|
700
822
|
}
|
|
701
823
|
}
|
|
@@ -780,6 +902,27 @@ async function cmdStatus(args = []) {
|
|
|
780
902
|
console.log(' unknown (could not read .claude/settings.json)');
|
|
781
903
|
}
|
|
782
904
|
|
|
905
|
+
// Replit section
|
|
906
|
+
try {
|
|
907
|
+
const replit = await import('../src/replit.mjs');
|
|
908
|
+
const env = replit.detectReplitEnvironment(cwd);
|
|
909
|
+
if (env.isReplit) {
|
|
910
|
+
console.log('\nReplit:');
|
|
911
|
+
const tools = replit.inspectReplitTools(cwd);
|
|
912
|
+
const verStr = tools.version ? `v${tools.version}` : 'unknown';
|
|
913
|
+
const capsCount = Array.isArray(tools.capabilities) ? tools.capabilities.length : 0;
|
|
914
|
+
console.log(` replit-tools : ${tools.installed ? `${verStr} (${capsCount} capabilities)` : 'not installed'}`);
|
|
915
|
+
const authStatus = replit.getAuthStatus(cwd);
|
|
916
|
+
console.log(` auth : ${authStatus.authenticated ? 'authenticated' : 'not authenticated'}${authStatus.method ? ` (${authStatus.method})` : ''}`);
|
|
917
|
+
const archive = replit.getSessionArchive(cwd);
|
|
918
|
+
const archiveCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
|
|
919
|
+
console.log(` session archive: ${archiveCount} session${archiveCount !== 1 ? 's' : ''}`);
|
|
920
|
+
const openaiPresent = replit.hasSecret('OPENAI_API_KEY');
|
|
921
|
+
const anthropicPresent = replit.hasSecret('ANTHROPIC_API_KEY');
|
|
922
|
+
console.log(` secrets : OPENAI_API_KEY=${openaiPresent ? 'set' : 'unset'} ANTHROPIC_API_KEY=${anthropicPresent ? 'set' : 'unset'}`);
|
|
923
|
+
}
|
|
924
|
+
} catch { /* replit.mjs not available or not in Replit — skip silently */ }
|
|
925
|
+
|
|
783
926
|
// Update check
|
|
784
927
|
try {
|
|
785
928
|
const localVer = readVersion();
|
|
@@ -1007,14 +1150,14 @@ async function welcomeScreen(rl, ask) {
|
|
|
1007
1150
|
}
|
|
1008
1151
|
console.log('');
|
|
1009
1152
|
|
|
1010
|
-
// --- Detect
|
|
1153
|
+
// --- Detect replit-tools sessions ---
|
|
1011
1154
|
const env = detectEnvironment();
|
|
1012
1155
|
const existingSessions = importReplitSessions(cwd);
|
|
1013
1156
|
if (env.hasReplitTools) {
|
|
1014
|
-
detectedLines.push(`
|
|
1157
|
+
detectedLines.push(` replit-tools detected`);
|
|
1015
1158
|
}
|
|
1016
1159
|
if (existingSessions.length > 0) {
|
|
1017
|
-
detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from
|
|
1160
|
+
detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from replit-tools`);
|
|
1018
1161
|
}
|
|
1019
1162
|
|
|
1020
1163
|
// --- Detect replit-tools ---
|
|
@@ -1054,7 +1197,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
1054
1197
|
console.log(' [Enter] Save and go');
|
|
1055
1198
|
console.log(' [c] Customize work style');
|
|
1056
1199
|
if (existingSessions.length > 0) {
|
|
1057
|
-
console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from
|
|
1200
|
+
console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from replit-tools`);
|
|
1058
1201
|
}
|
|
1059
1202
|
if (!rt.installed) {
|
|
1060
1203
|
console.log('');
|
|
@@ -1066,7 +1209,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
1066
1209
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
1067
1210
|
|
|
1068
1211
|
if (choice === 'i' && existingSessions.length > 0) {
|
|
1069
|
-
console.log(`\n Importing ${existingSessions.length} sessions from
|
|
1212
|
+
console.log(`\n Importing ${existingSessions.length} sessions from replit-tools...\n`);
|
|
1070
1213
|
const recent = existingSessions.slice(0, 5);
|
|
1071
1214
|
for (const sess of recent) {
|
|
1072
1215
|
console.log(` ${sess.age.padEnd(6)} ${sess.name}`);
|
|
@@ -1483,12 +1626,20 @@ function detectInterruptedWork(sessions, cwd) {
|
|
|
1483
1626
|
* Shows: "● Claude ● OpenAI ⚖️ Balanced"
|
|
1484
1627
|
* Uses ANSI color codes for the dots — no dollar amounts or usage bars.
|
|
1485
1628
|
*/
|
|
1486
|
-
function buildProviderStatusLine(profile, auth,
|
|
1487
|
-
const GREEN = '
|
|
1488
|
-
const RED = '
|
|
1629
|
+
function buildProviderStatusLine(profile, auth, envReport = null) {
|
|
1630
|
+
const GREEN = '\x1b[32m●\x1b[0m';
|
|
1631
|
+
const RED = '\x1b[31m●\x1b[0m';
|
|
1489
1632
|
|
|
1490
|
-
|
|
1491
|
-
const
|
|
1633
|
+
// Use envReport secrets when available; fall back to auth detection
|
|
1634
|
+
const claudeAvailable = envReport
|
|
1635
|
+
? envReport.secrets.ANTHROPIC_API_KEY || auth.claude.found
|
|
1636
|
+
: auth.claude.found;
|
|
1637
|
+
const openaiAvailable = envReport
|
|
1638
|
+
? envReport.secrets.OPENAI_API_KEY || auth.openai.found
|
|
1639
|
+
: auth.openai.found;
|
|
1640
|
+
|
|
1641
|
+
const claudeDot = claudeAvailable ? GREEN : RED;
|
|
1642
|
+
const openaiDot = openaiAvailable ? GREEN : RED;
|
|
1492
1643
|
|
|
1493
1644
|
const WORK_STYLE_LABELS = {
|
|
1494
1645
|
'auto': '⚡ Fast',
|
|
@@ -1498,30 +1649,11 @@ function buildProviderStatusLine(profile, auth, maxWidth = 54) {
|
|
|
1498
1649
|
'solo-claude': '⚡ Fast',
|
|
1499
1650
|
'solo-openai': '⚡ Fast',
|
|
1500
1651
|
};
|
|
1501
|
-
const WORK_STYLE_TIPS = {
|
|
1502
|
-
'auto': 'adapts routing by task risk',
|
|
1503
|
-
'cost-saver': 'single model, minimal reviews',
|
|
1504
|
-
'balanced': 'smart routing, reviews when needed',
|
|
1505
|
-
'quality-first': 'dual-brain on everything important',
|
|
1506
|
-
'solo-claude': 'Claude only, no GPT dispatch',
|
|
1507
|
-
'solo-openai': 'OpenAI only, no Claude dispatch',
|
|
1508
|
-
};
|
|
1509
1652
|
const bias = profile?.bias || profile?.mode || 'balanced';
|
|
1510
1653
|
const label = WORK_STYLE_LABELS[bias] || '⚖️ Balanced';
|
|
1511
|
-
const fullTip = WORK_STYLE_TIPS[bias] || 'smart routing, reviews when needed';
|
|
1512
|
-
|
|
1513
|
-
// Trim tip to fit within box width (measure visible chars: strip ANSI + variation selectors)
|
|
1514
|
-
const labelPlain = label.replace(/[︀-️]/g, '').replace(/[[0-9;]*m/g, '');
|
|
1515
|
-
const prefixLen = ('● Claude ● OpenAI ' + labelPlain + ' — ').length;
|
|
1516
|
-
const tipMax = maxWidth - prefixLen;
|
|
1517
|
-
const tip = tipMax >= 6
|
|
1518
|
-
? (fullTip.length > tipMax ? fullTip.slice(0, tipMax - 1) + '…' : fullTip)
|
|
1519
|
-
: '';
|
|
1520
1654
|
|
|
1521
|
-
|
|
1522
|
-
return `${claudeDot} Claude ${openaiDot} OpenAI ${label}${suffix}`;
|
|
1655
|
+
return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
|
|
1523
1656
|
}
|
|
1524
|
-
|
|
1525
1657
|
/**
|
|
1526
1658
|
* Render a box row padded to inner width W (stripping ANSI for length calculation).
|
|
1527
1659
|
* Returns a string like: "│ content padded to W │"
|
|
@@ -1544,6 +1676,13 @@ async function mainScreen(rl, ask) {
|
|
|
1544
1676
|
const profile = loadProfile(cwd);
|
|
1545
1677
|
const auth = await detectAuth();
|
|
1546
1678
|
|
|
1679
|
+
// ── Dashboard load animation (full mode only) ─────────────────────────────
|
|
1680
|
+
const fx = await getFx();
|
|
1681
|
+
let dashSpinner = null;
|
|
1682
|
+
if (fx && fx.getMode && fx.getMode() === 'full') {
|
|
1683
|
+
dashSpinner = fx.spinner('Loading dashboard...').start();
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1547
1686
|
const claudeSub = profile?.providers?.claude;
|
|
1548
1687
|
const openaiSub = profile?.providers?.openai;
|
|
1549
1688
|
|
|
@@ -1591,7 +1730,7 @@ async function mainScreen(rl, ask) {
|
|
|
1591
1730
|
return ageMs >= 7 * 86400000;
|
|
1592
1731
|
}).length;
|
|
1593
1732
|
|
|
1594
|
-
// Detect
|
|
1733
|
+
// Detect replit-tools version
|
|
1595
1734
|
const rtMain = detectReplitTools(cwd);
|
|
1596
1735
|
const dtVersion = (rtMain.installed && rtMain.version) ? rtMain.version : null;
|
|
1597
1736
|
|
|
@@ -1609,25 +1748,6 @@ async function mainScreen(rl, ask) {
|
|
|
1609
1748
|
|
|
1610
1749
|
const row = (content) => makeBoxRow(content, W);
|
|
1611
1750
|
|
|
1612
|
-
// ── Header: one line above the box ────────────────────────────────────────
|
|
1613
|
-
process.stdout.write(`\n🧠 dual-brain v${version}\n`);
|
|
1614
|
-
{
|
|
1615
|
-
let gitName = '';
|
|
1616
|
-
try {
|
|
1617
|
-
const { execSync } = await import('node:child_process');
|
|
1618
|
-
gitName = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
1619
|
-
} catch { /* ignore */ }
|
|
1620
|
-
if (gitName) {
|
|
1621
|
-
const hour = new Date().getHours();
|
|
1622
|
-
let greet;
|
|
1623
|
-
if (hour >= 5 && hour <= 11) greet = 'Good morning';
|
|
1624
|
-
else if (hour >= 12 && hour <= 16) greet = 'Good afternoon';
|
|
1625
|
-
else if (hour >= 17 && hour <= 21) greet = 'Good evening';
|
|
1626
|
-
else greet = 'Late night';
|
|
1627
|
-
process.stdout.write(`\x1b[2m${greet}, ${gitName}\x1b[0m\n`);
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
1751
|
// ── Continuation card (interrupted work) ─────────────────────────────────
|
|
1632
1752
|
if (interrupted) {
|
|
1633
1753
|
const ctop = `┌${'─'.repeat(boxW - 2)}┐`;
|
|
@@ -1706,15 +1826,82 @@ async function mainScreen(rl, ask) {
|
|
|
1706
1826
|
// 's' → fall through to normal dashboard
|
|
1707
1827
|
}
|
|
1708
1828
|
|
|
1709
|
-
// ──
|
|
1710
|
-
|
|
1829
|
+
// ── Environment awareness (powers Box 1 dots + Box 3) ────────────────────
|
|
1830
|
+
let envReport = null;
|
|
1831
|
+
try {
|
|
1832
|
+
const { scanEnvironment } = await import('../src/awareness.mjs');
|
|
1833
|
+
envReport = scanEnvironment(cwd);
|
|
1834
|
+
} catch { /* non-fatal */ }
|
|
1835
|
+
|
|
1836
|
+
// ── Box 1 — Header row data ─────────────────────────────────────────────
|
|
1837
|
+
const providerLine = buildProviderStatusLine(profile, auth, envReport);
|
|
1711
1838
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1839
|
+
// ── Box 2 — Workspace: gather git data ───────────────────────────────────
|
|
1840
|
+
let gitBranch = 'unknown';
|
|
1841
|
+
let gitUncommitted = 0;
|
|
1842
|
+
let gitAheadCount = 0;
|
|
1843
|
+
let gitLastMsg = '';
|
|
1844
|
+
let gitLastAgo = '';
|
|
1845
|
+
|
|
1846
|
+
try {
|
|
1847
|
+
gitBranch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
|
|
1848
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1849
|
+
}).trim() || 'unknown';
|
|
1850
|
+
} catch {}
|
|
1851
|
+
|
|
1852
|
+
try {
|
|
1853
|
+
const status = execSync('git status --porcelain 2>/dev/null', {
|
|
1854
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1855
|
+
});
|
|
1856
|
+
gitUncommitted = status.trim().split('\n').filter(Boolean).length;
|
|
1857
|
+
} catch {}
|
|
1858
|
+
|
|
1859
|
+
try {
|
|
1860
|
+
const aheadOut = execSync('git rev-list @{u}..HEAD 2>/dev/null | wc -l', {
|
|
1861
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1862
|
+
});
|
|
1863
|
+
gitAheadCount = parseInt(aheadOut.trim(), 10) || 0;
|
|
1864
|
+
} catch {}
|
|
1865
|
+
|
|
1866
|
+
try {
|
|
1867
|
+
const logOut = execSync('git log -1 --format="%s|%ct" 2>/dev/null', {
|
|
1868
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1869
|
+
}).trim();
|
|
1870
|
+
if (logOut) {
|
|
1871
|
+
const [msg, ts] = logOut.split('|');
|
|
1872
|
+
gitLastMsg = (msg || '').slice(0, 38);
|
|
1873
|
+
const ageMs = Date.now() - (parseInt(ts, 10) * 1000);
|
|
1874
|
+
const ageMin = Math.floor(ageMs / 60000);
|
|
1875
|
+
if (ageMin < 60) gitLastAgo = `${ageMin}m ago`;
|
|
1876
|
+
else if (ageMin < 1440) gitLastAgo = `${Math.floor(ageMin / 60)}h ago`;
|
|
1877
|
+
else gitLastAgo = `${Math.floor(ageMin / 1440)}d ago`;
|
|
1878
|
+
}
|
|
1879
|
+
} catch {}
|
|
1880
|
+
|
|
1881
|
+
// ── Box 2 rows ────────────────────────────────────────────────────────────
|
|
1882
|
+
const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
|
|
1883
|
+
const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
|
|
1884
|
+
const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
|
|
1885
|
+
const workspaceLine2 = gitLastMsg
|
|
1886
|
+
? `Last: ${gitLastMsg} (${gitLastAgo})`
|
|
1887
|
+
: '';
|
|
1888
|
+
|
|
1889
|
+
// Open PRs
|
|
1890
|
+
const repoState = detectRepoState(cwd);
|
|
1891
|
+
const openPRs = await detectOpenPRs(cwd);
|
|
1892
|
+
|
|
1893
|
+
const workspaceRows = [row(workspaceLine1)];
|
|
1894
|
+
if (workspaceLine2) workspaceRows.push(row(workspaceLine2));
|
|
1895
|
+
if (openPRs.length > 0) {
|
|
1896
|
+
workspaceRows.push(row(`${openPRs.length} open PR${openPRs.length === 1 ? '' : 's'}`));
|
|
1715
1897
|
}
|
|
1716
1898
|
|
|
1717
|
-
// ──
|
|
1899
|
+
// ── Box 3 — Awareness: observer + roadmap + risk ──────────────────────────
|
|
1900
|
+
let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
|
|
1901
|
+
let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
|
|
1902
|
+
let awarenessLine3 = '\x1b[32m✓\x1b[0m No risk flags';
|
|
1903
|
+
|
|
1904
|
+
// Line 1: observer data first; fall back to envReport-derived observations
|
|
1718
1905
|
let quickObservations = [];
|
|
1719
1906
|
try {
|
|
1720
1907
|
const observerMod = await import('../src/observer.mjs');
|
|
@@ -1724,64 +1911,98 @@ async function mainScreen(rl, ask) {
|
|
|
1724
1911
|
const sorted = [...quickState.observations].sort(
|
|
1725
1912
|
(a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
|
|
1726
1913
|
);
|
|
1727
|
-
quickObservations = sorted.slice(0,
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1914
|
+
quickObservations = sorted.slice(0, 3);
|
|
1915
|
+
const top = quickObservations[0];
|
|
1916
|
+
if (top) {
|
|
1917
|
+
const prefix = top.priority === 'high' ? '🔴' : top.priority === 'medium' ? '🟡' : '\x1b[2m💡\x1b[0m';
|
|
1918
|
+
awarenessLine1 = `${prefix} ${top.message}`;
|
|
1919
|
+
}
|
|
1920
|
+
const hasHighRisk = quickObservations.some(o => o.priority === 'high');
|
|
1921
|
+
if (hasHighRisk) {
|
|
1922
|
+
awarenessLine3 = '\x1b[31m⚠\x1b[0m Risk flags detected — run: dual-brain review';
|
|
1734
1923
|
}
|
|
1735
1924
|
}
|
|
1736
|
-
} catch { /* non-fatal —
|
|
1737
|
-
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1925
|
+
} catch { /* non-fatal — observer may not exist */ }
|
|
1926
|
+
|
|
1927
|
+
// If observer produced nothing, derive from envReport
|
|
1928
|
+
if (awarenessLine1 === '\x1b[2m💡\x1b[0m Ready to work' && envReport) {
|
|
1929
|
+
if (envReport.replit?.hasDatabase) {
|
|
1930
|
+
awarenessLine1 = '\x1b[2m💡\x1b[0m PostgreSQL available';
|
|
1931
|
+
} else if (gitUncommitted > 0) {
|
|
1932
|
+
awarenessLine1 = `\x1b[2m💡\x1b[0m ${gitUncommitted} file${gitUncommitted === 1 ? '' : 's'} ready to commit`;
|
|
1933
|
+
} else if (envReport.dualBrain?.hasFailureMemory) {
|
|
1934
|
+
// Check for recent failures
|
|
1935
|
+
try {
|
|
1936
|
+
const failureMem = await getFailureMem();
|
|
1937
|
+
if (failureMem.getRecentFailures) {
|
|
1938
|
+
const recent = failureMem.getRecentFailures(cwd, 2);
|
|
1939
|
+
if (recent?.length > 0) {
|
|
1940
|
+
awarenessLine1 = `\x1b[33m⚠\x1b[0m ${recent.length} recent failure${recent.length === 1 ? '' : 's'} — check before proceeding`;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
} catch { /* non-fatal */ }
|
|
1944
|
+
}
|
|
1748
1945
|
}
|
|
1749
1946
|
|
|
1750
|
-
//
|
|
1751
|
-
|
|
1947
|
+
// Line 2: roadmap file, then ledger open tasks as fallback
|
|
1948
|
+
try {
|
|
1949
|
+
const roadmapPath = join(cwd, '.dual-brain', 'roadmap.md');
|
|
1950
|
+
if (existsSync(roadmapPath)) {
|
|
1951
|
+
const roadmapText = readFileSync(roadmapPath, 'utf8');
|
|
1952
|
+
const lines = roadmapText.split('\n').filter(Boolean);
|
|
1953
|
+
// Skip heading lines, grab first non-heading line
|
|
1954
|
+
const firstItem = lines.find(l => !l.startsWith('#') && l.trim().length > 0);
|
|
1955
|
+
if (firstItem) {
|
|
1956
|
+
const clean = firstItem.replace(/^[-*>]+\s*/, '').trim().slice(0, 45);
|
|
1957
|
+
awarenessLine2 = `\x1b[2m📋\x1b[0m ${clean}`;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
} catch { /* non-fatal */ }
|
|
1961
|
+
|
|
1962
|
+
if (awarenessLine2 === '\x1b[2m📋 No roadmap yet\x1b[0m') {
|
|
1752
1963
|
try {
|
|
1753
|
-
const {
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
// Load session index to get files for the most recent session
|
|
1758
|
-
const indexPath = join(cwd, '.dualbrain', 'session-index.json');
|
|
1759
|
-
let recentFiles = [];
|
|
1760
|
-
try {
|
|
1761
|
-
const idx = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
1762
|
-
recentFiles = idx[mostRecent.id]?.files || [];
|
|
1763
|
-
} catch {}
|
|
1764
|
-
const related = findRelatedSessions(recentPrompt, recentFiles, cwd);
|
|
1765
|
-
if (related.length > 0) {
|
|
1766
|
-
const relAgeLabel = (isoDate) => {
|
|
1767
|
-
if (!isoDate) return '';
|
|
1768
|
-
const diff = Date.now() - Date.parse(isoDate);
|
|
1769
|
-
const days = Math.floor(diff / 86400000);
|
|
1770
|
-
const hours = Math.floor(diff / 3600000);
|
|
1771
|
-
if (days >= 1) return `${days}d`;
|
|
1772
|
-
return `${hours}h ago`;
|
|
1773
|
-
};
|
|
1774
|
-
const relatedParts = related.slice(0, 2).map(r => {
|
|
1775
|
-
const age = relAgeLabel(r.date);
|
|
1776
|
-
return age ? `${r.smartName} (${age})` : r.smartName;
|
|
1777
|
-
});
|
|
1778
|
-
const DIM = '\x1b[2m';
|
|
1779
|
-
const RESET = '\x1b[0m';
|
|
1780
|
-
actionRows.push(row(`${DIM}📎 Related: ${relatedParts.join(', ')}${RESET}`));
|
|
1964
|
+
const { getOpenTasks } = await import('../src/ledger.mjs');
|
|
1965
|
+
const open = getOpenTasks(cwd);
|
|
1966
|
+
if (open.length > 0) {
|
|
1967
|
+
awarenessLine2 = '📋 Next: ' + open[0].intent.slice(0, 45);
|
|
1781
1968
|
}
|
|
1782
1969
|
} catch { /* non-fatal */ }
|
|
1783
1970
|
}
|
|
1784
|
-
|
|
1971
|
+
|
|
1972
|
+
// Line 3: model registry age warning
|
|
1973
|
+
try {
|
|
1974
|
+
const { getRegistryAge } = await import('../src/models.mjs');
|
|
1975
|
+
const age = getRegistryAge();
|
|
1976
|
+
if (age > 30 && awarenessLine3 === '\x1b[32m✓\x1b[0m No risk flags') {
|
|
1977
|
+
awarenessLine3 = `\x1b[33m⚠\x1b[0m Model registry ${age} days old`;
|
|
1978
|
+
}
|
|
1979
|
+
} catch { /* non-fatal */ }
|
|
1980
|
+
|
|
1981
|
+
// Replit awareness rows (shown only when running in Replit, max 2-3 lines)
|
|
1982
|
+
const replitAwarenessRows = [];
|
|
1983
|
+
try {
|
|
1984
|
+
const replitMod = await import('../src/replit.mjs');
|
|
1985
|
+
const replitEnv = replitMod.detectReplitEnvironment(cwd);
|
|
1986
|
+
if (replitEnv.isReplit) {
|
|
1987
|
+
const rtInfo = replitMod.inspectReplitTools(cwd);
|
|
1988
|
+
const authInfo = replitMod.getAuthStatus(cwd);
|
|
1989
|
+
const archive = replitMod.getSessionArchive(cwd);
|
|
1990
|
+
const archCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
|
|
1991
|
+
const secretNames = replitMod.listSecretNames();
|
|
1992
|
+
const secretCount = Array.isArray(secretNames) ? secretNames.length : 0;
|
|
1993
|
+
const verStr = rtInfo.version ? `v${rtInfo.version}` : (rtInfo.installed ? 'installed' : 'not installed');
|
|
1994
|
+
const authStr = authInfo.authenticated ? '\x1b[32m✓\x1b[0m auth' : '\x1b[2mno auth\x1b[0m';
|
|
1995
|
+
replitAwarenessRows.push(row(`\x1b[2m🔧\x1b[0m Replit replit-tools ${verStr} ${authStr}`));
|
|
1996
|
+
replitAwarenessRows.push(row(`\x1b[2m \x1b[0m ${archCount} archived session${archCount !== 1 ? 's' : ''} ${secretCount} secret${secretCount !== 1 ? 's' : ''}`));
|
|
1997
|
+
}
|
|
1998
|
+
} catch { /* replit.mjs not available — skip */ }
|
|
1999
|
+
|
|
2000
|
+
const awarenessRows = [
|
|
2001
|
+
row(awarenessLine1),
|
|
2002
|
+
row(awarenessLine2),
|
|
2003
|
+
row(awarenessLine3),
|
|
2004
|
+
...replitAwarenessRows,
|
|
2005
|
+
];
|
|
1785
2006
|
|
|
1786
2007
|
// ── Sessions section ──────────────────────────────────────────────────────
|
|
1787
2008
|
const sessionRows = [];
|
|
@@ -1837,23 +2058,28 @@ async function mainScreen(rl, ask) {
|
|
|
1837
2058
|
});
|
|
1838
2059
|
}
|
|
1839
2060
|
|
|
1840
|
-
// ──
|
|
1841
|
-
const actionsContent = '
|
|
2061
|
+
// ── Box 5 — Input bar ──────────────────────────────────────────────────
|
|
2062
|
+
const actionsContent = '> type anything... [s] settings [t] team [q] quit';
|
|
1842
2063
|
const actionsRow = row(actionsContent);
|
|
1843
2064
|
|
|
1844
|
-
// ── Print the full box
|
|
1845
|
-
//
|
|
1846
|
-
|
|
2065
|
+
// ── Print the full 5-box layout ───────────────────────────────────────────
|
|
2066
|
+
// Box 1: header (title + provider dots + work style)
|
|
2067
|
+
// Box 2: workspace (branch · uncommitted · ahead, last commit, open PRs)
|
|
2068
|
+
// Box 3: awareness (observer, roadmap, risk)
|
|
2069
|
+
// Box 4: sessions
|
|
2070
|
+
// Box 5: input bar
|
|
1847
2071
|
const lines = [
|
|
1848
2072
|
top,
|
|
1849
|
-
|
|
1850
|
-
|
|
2073
|
+
row(`🧠 dual-brain v${version}`),
|
|
2074
|
+
row(providerLine),
|
|
2075
|
+
sep,
|
|
2076
|
+
...workspaceRows,
|
|
2077
|
+
sep,
|
|
2078
|
+
...awarenessRows,
|
|
1851
2079
|
sep,
|
|
1852
2080
|
...sessionRows,
|
|
1853
2081
|
sep,
|
|
1854
2082
|
actionsRow,
|
|
1855
|
-
sep,
|
|
1856
|
-
poweredByRow,
|
|
1857
2083
|
bot,
|
|
1858
2084
|
];
|
|
1859
2085
|
// ── Stale session hint ──────────────────────────────────────────────────
|
|
@@ -1861,6 +2087,9 @@ async function mainScreen(rl, ask) {
|
|
|
1861
2087
|
process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
|
|
1862
2088
|
}
|
|
1863
2089
|
|
|
2090
|
+
// Resolve dashboard spinner before rendering
|
|
2091
|
+
if (dashSpinner) dashSpinner.succeed('Dashboard ready');
|
|
2092
|
+
|
|
1864
2093
|
process.stdout.write(lines.join('\n') + '\n\n');
|
|
1865
2094
|
|
|
1866
2095
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
@@ -1948,7 +2177,7 @@ async function mainScreen(rl, ask) {
|
|
|
1948
2177
|
// Single-key commands only fire when buffer is empty
|
|
1949
2178
|
if (taskBuffer.length === 0) {
|
|
1950
2179
|
const lower = str.toLowerCase();
|
|
1951
|
-
const singleKeySet = new Set(['n', 's', 'q', '/', 'i']);
|
|
2180
|
+
const singleKeySet = new Set(['n', 's', 't', 'q', '/', 'i']);
|
|
1952
2181
|
if (singleKeySet.has(lower)) {
|
|
1953
2182
|
cleanup();
|
|
1954
2183
|
process.stdout.write('\n');
|
|
@@ -2054,6 +2283,7 @@ async function mainScreen(rl, ask) {
|
|
|
2054
2283
|
}
|
|
2055
2284
|
|
|
2056
2285
|
if (choice === 's') { return { next: 'settings' }; }
|
|
2286
|
+
if (choice === 't') { return { next: 'team' }; }
|
|
2057
2287
|
if (choice === 'i') { return { next: 'import-picker' }; }
|
|
2058
2288
|
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
2059
2289
|
|
|
@@ -2078,7 +2308,7 @@ async function newSessionScreen(rl, ask) {
|
|
|
2078
2308
|
async function importPickerScreen() {
|
|
2079
2309
|
const cwd = process.cwd();
|
|
2080
2310
|
|
|
2081
|
-
// Load all available sessions from
|
|
2311
|
+
// Load all available sessions from replit-tools
|
|
2082
2312
|
const allSessions = importReplitSessions(cwd);
|
|
2083
2313
|
|
|
2084
2314
|
// Load existing session meta to filter already-imported ones
|
|
@@ -2124,9 +2354,9 @@ async function importPickerScreen() {
|
|
|
2124
2354
|
if (allSessions.length === 0) {
|
|
2125
2355
|
process.stdout.write('\n');
|
|
2126
2356
|
process.stdout.write(top + '\n');
|
|
2127
|
-
process.stdout.write(row('Import from
|
|
2357
|
+
process.stdout.write(row('Import from replit-tools') + '\n');
|
|
2128
2358
|
process.stdout.write(sep + '\n');
|
|
2129
|
-
process.stdout.write(row('No
|
|
2359
|
+
process.stdout.write(row('No replit-tools sessions found.') + '\n');
|
|
2130
2360
|
process.stdout.write(row('Install replit-tools: npm i -g replit-tools') + '\n');
|
|
2131
2361
|
process.stdout.write(sep + '\n');
|
|
2132
2362
|
process.stdout.write(row('Press any key to go back...') + '\n');
|
|
@@ -2138,7 +2368,7 @@ async function importPickerScreen() {
|
|
|
2138
2368
|
if (candidates.length === 0) {
|
|
2139
2369
|
process.stdout.write('\n');
|
|
2140
2370
|
process.stdout.write(top + '\n');
|
|
2141
|
-
process.stdout.write(row('Import from
|
|
2371
|
+
process.stdout.write(row('Import from replit-tools') + '\n');
|
|
2142
2372
|
process.stdout.write(sep + '\n');
|
|
2143
2373
|
process.stdout.write(row(`All ${allSessions.length} sessions already imported.`) + '\n');
|
|
2144
2374
|
process.stdout.write(sep + '\n');
|
|
@@ -2161,7 +2391,7 @@ async function importPickerScreen() {
|
|
|
2161
2391
|
const renderPicker = () => {
|
|
2162
2392
|
process.stdout.write('\x1b[2J\x1b[H'); // clear screen
|
|
2163
2393
|
|
|
2164
|
-
const headerTitle = 'Import from
|
|
2394
|
+
const headerTitle = 'Import from replit-tools';
|
|
2165
2395
|
const footerLine = '↑↓ Navigate Space Toggle Enter Import q Back';
|
|
2166
2396
|
|
|
2167
2397
|
process.stdout.write('\n');
|
|
@@ -2298,7 +2528,7 @@ async function importPickerScreen() {
|
|
|
2298
2528
|
}
|
|
2299
2529
|
saveSessionMeta(updatedMeta, cwd);
|
|
2300
2530
|
|
|
2301
|
-
process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from
|
|
2531
|
+
process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from replit-tools\n\n`);
|
|
2302
2532
|
|
|
2303
2533
|
return { next: 'main' };
|
|
2304
2534
|
}
|
|
@@ -2560,22 +2790,89 @@ async function settingsScreen(rl, ask) {
|
|
|
2560
2790
|
'balanced': '⚖️ Balanced',
|
|
2561
2791
|
'quality-first': '🔥 Full Power',
|
|
2562
2792
|
};
|
|
2563
|
-
|
|
2793
|
+
|
|
2794
|
+
// Work style current markers
|
|
2795
|
+
const _stIsFast = ['cost-saver', 'auto', 'solo-claude', 'solo-openai'].includes(currentBias);
|
|
2796
|
+
const _stIsBal = currentBias === 'balanced';
|
|
2797
|
+
const _stIsFull = currentBias === 'quality-first';
|
|
2798
|
+
const _stMark = (active) => active ? ' ← current' : '';
|
|
2799
|
+
|
|
2800
|
+
// Provider status dots
|
|
2801
|
+
const _stAuth = await detectAuth();
|
|
2802
|
+
const _stGDOT = '\x1b[32m●\x1b[0m';
|
|
2803
|
+
const _stRDOT = '\x1b[31m●\x1b[0m';
|
|
2804
|
+
const _stClDot = _stAuth.claude.found ? _stGDOT : _stRDOT;
|
|
2805
|
+
const _stOaDot = _stAuth.openai.found ? _stGDOT : _stRDOT;
|
|
2806
|
+
const _stClStatus = _stAuth.claude.found ? 'connected' : 'not connected';
|
|
2807
|
+
const _stOaStatus = _stAuth.openai.found ? 'connected' : 'not connected';
|
|
2808
|
+
|
|
2809
|
+
// Calibration from project.json
|
|
2810
|
+
let _stCal = { specificity: 3, corrections: 3, autonomy: 3 };
|
|
2811
|
+
let _stLevel = 'intermediate';
|
|
2812
|
+
let _stStyle = 'normal';
|
|
2813
|
+
try {
|
|
2814
|
+
const _stLd = await import('../src/living-docs.mjs');
|
|
2815
|
+
const _stCm = await import('../src/calibration.mjs');
|
|
2816
|
+
const _stPs = _stLd.getProjectState(cwd);
|
|
2817
|
+
if (_stPs?.project?.userCalibration) _stCal = _stPs.project.userCalibration;
|
|
2818
|
+
const _stAd = _stCm.getAdaptation(_stCal);
|
|
2819
|
+
_stLevel = _stAd.userLevel;
|
|
2820
|
+
_stStyle = _stAd.responseStyle;
|
|
2821
|
+
} catch { /* non-fatal */ }
|
|
2822
|
+
|
|
2823
|
+
const _stS = typeof _stCal.specificity === 'number' ? _stCal.specificity.toFixed(1) : String(_stCal.specificity ?? 3);
|
|
2824
|
+
const _stC = typeof _stCal.corrections === 'number' ? _stCal.corrections.toFixed(1) : String(_stCal.corrections ?? 3);
|
|
2825
|
+
const _stA = typeof _stCal.autonomy === 'number' ? _stCal.autonomy.toFixed(1) : String(_stCal.autonomy ?? 3);
|
|
2826
|
+
|
|
2827
|
+
// Cost efficiency summary (graceful — only shown when data exists)
|
|
2828
|
+
let _stEffScore = null;
|
|
2829
|
+
let _stEffRate = null;
|
|
2830
|
+
let _stEffTrend = null;
|
|
2831
|
+
let _stEffTier = null;
|
|
2832
|
+
try {
|
|
2833
|
+
const _stCt = await import('../src/cost-tracker.mjs');
|
|
2834
|
+
const _stSummary = _stCt.getCostSummary(cwd, 7);
|
|
2835
|
+
if (_stSummary.totalActions > 0) {
|
|
2836
|
+
_stEffScore = _stCt.getEfficiencyScore(cwd);
|
|
2837
|
+
_stEffRate = Math.round(_stSummary.savingsRate * 100);
|
|
2838
|
+
_stEffTrend = _stSummary.trend;
|
|
2839
|
+
const tierOrder = ['recall', 'quick', 'standard', 'deep', 'ultra'];
|
|
2840
|
+
const _stTierKeys = tierOrder.filter(k => _stSummary.byTier[k]);
|
|
2841
|
+
_stEffTier = _stTierKeys.map(k => {
|
|
2842
|
+
const t = _stSummary.byTier[k];
|
|
2843
|
+
return `${k.padEnd(8)} ${String(t.count).padStart(3)}`;
|
|
2844
|
+
}).join(' ');
|
|
2845
|
+
}
|
|
2846
|
+
} catch { /* non-fatal */ }
|
|
2847
|
+
|
|
2848
|
+
const _stTrendIcon = _stEffTrend === 'improving' ? '↗' : _stEffTrend === 'degrading' ? '↘' : '→';
|
|
2564
2849
|
|
|
2565
2850
|
const lines = [
|
|
2566
2851
|
top,
|
|
2567
2852
|
row('Settings'),
|
|
2568
2853
|
sep,
|
|
2569
|
-
row(
|
|
2570
|
-
row(
|
|
2571
|
-
row(
|
|
2572
|
-
row(
|
|
2573
|
-
|
|
2574
|
-
row('
|
|
2575
|
-
row(
|
|
2854
|
+
row('Work Style'),
|
|
2855
|
+
row(` [1] Fast — speed over caution${_stMark(_stIsFast)}`),
|
|
2856
|
+
row(` [2] Balanced — smart routing, reviews on important${_stMark(_stIsBal)}`),
|
|
2857
|
+
row(` [3] Full Power — dual-brain everything, max quality${_stMark(_stIsFull)}`),
|
|
2858
|
+
sep,
|
|
2859
|
+
row('Providers'),
|
|
2860
|
+
row(` Claude: ${_stClDot} ${_stClStatus}`),
|
|
2861
|
+
row(` OpenAI: ${_stOaDot} ${_stOaStatus}`),
|
|
2862
|
+
sep,
|
|
2863
|
+
row('User Calibration'),
|
|
2864
|
+
row(` Specificity: ${_stS} Corrections: ${_stC} Autonomy: ${_stA}`),
|
|
2865
|
+
row(` Level: ${_stLevel} · Style: ${_stStyle}`),
|
|
2866
|
+
...(_stEffScore !== null ? [
|
|
2867
|
+
sep,
|
|
2868
|
+
row('Cost Efficiency (7 days)'),
|
|
2869
|
+
row(` Score: ${_stEffScore}/100 Savings: ${_stEffRate}% Trend: ${_stTrendIcon} ${_stEffTrend}`),
|
|
2870
|
+
...(_stEffTier ? [row(` Tiers: ${_stEffTier}`)] : []),
|
|
2871
|
+
] : []),
|
|
2872
|
+
sep,
|
|
2873
|
+
row('[1-3] change style [r] reset calibration [b] back'),
|
|
2874
|
+
row('[m] subscriptions [e] sessions [x] diagnostics'),
|
|
2576
2875
|
...(settingsPRs.length > 0 ? [row(`[p] PR triage (${settingsPRs.length} open)`)] : []),
|
|
2577
|
-
row(''),
|
|
2578
|
-
row('[Esc/b] Back to dashboard'),
|
|
2579
2876
|
bot,
|
|
2580
2877
|
];
|
|
2581
2878
|
process.stdout.write('\n' + lines.join('\n') + '\n\n');
|
|
@@ -2583,45 +2880,10 @@ async function settingsScreen(rl, ask) {
|
|
|
2583
2880
|
const raw = (await ask(' Choice: ')).trim();
|
|
2584
2881
|
const choice = raw.toLowerCase();
|
|
2585
2882
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
const
|
|
2589
|
-
const
|
|
2590
|
-
const wsBot = ` └${'─'.repeat(51)}┘`;
|
|
2591
|
-
const wsPad = (s) => {
|
|
2592
|
-
const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
2593
|
-
let vlen = 0;
|
|
2594
|
-
for (const ch of plain) {
|
|
2595
|
-
const cp = ch.codePointAt(0);
|
|
2596
|
-
if (
|
|
2597
|
-
(cp >= 0x1f300 && cp <= 0x1faff) ||
|
|
2598
|
-
(cp >= 0x2600 && cp <= 0x27bf) ||
|
|
2599
|
-
cp === 0xfe0f || cp === 0x20e3
|
|
2600
|
-
) { vlen += 2; } else { vlen += 1; }
|
|
2601
|
-
}
|
|
2602
|
-
return s + ' '.repeat(Math.max(0, 51 - vlen));
|
|
2603
|
-
};
|
|
2604
|
-
const wsRow = (s) => ` │ ${wsPad(s)}│`;
|
|
2605
|
-
|
|
2606
|
-
const isFast = currentBias === 'cost-saver' || currentBias === 'auto' || currentBias === 'solo-claude' || currentBias === 'solo-openai';
|
|
2607
|
-
const isBal = currentBias === 'balanced';
|
|
2608
|
-
const isFull = currentBias === 'quality-first';
|
|
2609
|
-
|
|
2610
|
-
console.log('');
|
|
2611
|
-
console.log(wsTop);
|
|
2612
|
-
console.log(wsRow('Work Style'));
|
|
2613
|
-
console.log(wsSep);
|
|
2614
|
-
console.log(wsRow(` 1. ⚡ Fast — quick, single model${isFast ? ' ← current' : ''}`));
|
|
2615
|
-
console.log(wsRow(` 2. ⚖️ Balanced — smart routing${isBal ? ' ← current' : ''}`));
|
|
2616
|
-
console.log(wsRow(` 3. 🔥 Full Power — dual-brain everything${isFull ? ' ← current' : ''}`));
|
|
2617
|
-
console.log(wsSep);
|
|
2618
|
-
console.log(wsRow('[Enter] Keep current'));
|
|
2619
|
-
console.log(wsBot);
|
|
2620
|
-
console.log('');
|
|
2621
|
-
|
|
2622
|
-
const wsChoice = (await ask(' Choice [1/2/3/Enter]: ')).trim();
|
|
2623
|
-
const wsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
2624
|
-
const newBias = wsMap[wsChoice];
|
|
2883
|
+
// Direct work style keys 1/2/3
|
|
2884
|
+
if (choice === '1' || choice === '2' || choice === '3') {
|
|
2885
|
+
const _stWsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
2886
|
+
const newBias = _stWsMap[choice];
|
|
2625
2887
|
if (newBias && newBias !== currentBias) {
|
|
2626
2888
|
profile.bias = newBias;
|
|
2627
2889
|
const enabledCount = [
|
|
@@ -2631,12 +2893,23 @@ async function settingsScreen(rl, ask) {
|
|
|
2631
2893
|
if (enabledCount >= 2) profile.mode = newBias;
|
|
2632
2894
|
saveProfile(profile, { cwd });
|
|
2633
2895
|
const newLabel = WORK_STYLE_DISPLAY[newBias] || newBias;
|
|
2634
|
-
|
|
2896
|
+
process.stdout.write(`\n Work style set to ${newLabel}\n\n`);
|
|
2635
2897
|
await ask(' Press Enter to continue...');
|
|
2636
2898
|
}
|
|
2637
2899
|
return { next: 'settings' };
|
|
2638
2900
|
}
|
|
2639
2901
|
|
|
2902
|
+
// Reset calibration to defaults
|
|
2903
|
+
if (choice === 'r') {
|
|
2904
|
+
try {
|
|
2905
|
+
const _stLdReset = await import('../src/living-docs.mjs');
|
|
2906
|
+
_stLdReset.updateProject({ userCalibration: { specificity: 3, corrections: 3, autonomy: 3 } }, cwd);
|
|
2907
|
+
process.stdout.write('\n Calibration reset to defaults.\n\n');
|
|
2908
|
+
await ask(' Press Enter to continue...');
|
|
2909
|
+
} catch { /* non-fatal */ }
|
|
2910
|
+
return { next: 'settings' };
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2640
2913
|
if (choice === 'm') { return { next: 'subscriptions' }; }
|
|
2641
2914
|
|
|
2642
2915
|
if (choice === 'e') { return { next: 'sessions' }; }
|
|
@@ -2655,7 +2928,7 @@ async function settingsScreen(rl, ask) {
|
|
|
2655
2928
|
if (which.status === 0) {
|
|
2656
2929
|
spawnSync('claude-menu', { stdio: 'inherit' });
|
|
2657
2930
|
} else {
|
|
2658
|
-
process.stdout.write('\n
|
|
2931
|
+
process.stdout.write('\n replit-tools not found — install with: npm i -g replit-tools\n\n');
|
|
2659
2932
|
await ask(' Press Enter to continue...');
|
|
2660
2933
|
}
|
|
2661
2934
|
return { next: 'settings' };
|
|
@@ -2689,6 +2962,105 @@ async function settingsScreen(rl, ask) {
|
|
|
2689
2962
|
return { next: 'main' };
|
|
2690
2963
|
}
|
|
2691
2964
|
|
|
2965
|
+
// ─── Screen: teamScreen ───────────────────────────────────────────────────────
|
|
2966
|
+
|
|
2967
|
+
async function teamScreen(rl, ask) {
|
|
2968
|
+
const cwd = process.cwd();
|
|
2969
|
+
|
|
2970
|
+
// Box layout matching dashboard
|
|
2971
|
+
const termW = process.stdout.columns || 60;
|
|
2972
|
+
const boxW = Math.min(termW - 2, 60);
|
|
2973
|
+
const W = boxW - 4;
|
|
2974
|
+
|
|
2975
|
+
const top = `┌${'─'.repeat(boxW - 2)}┐`;
|
|
2976
|
+
const sep = `├${'─'.repeat(boxW - 2)}┤`;
|
|
2977
|
+
const bot = `└${'─'.repeat(boxW - 2)}┘`;
|
|
2978
|
+
const row = (content) => makeBoxRow(content, W);
|
|
2979
|
+
|
|
2980
|
+
// Load team from project.json
|
|
2981
|
+
let team = [];
|
|
2982
|
+
let sharedSessions = 0;
|
|
2983
|
+
let teamDecisions = 0;
|
|
2984
|
+
try {
|
|
2985
|
+
const _tmLd = await import('../src/living-docs.mjs');
|
|
2986
|
+
const _tmPs = _tmLd.getProjectState(cwd);
|
|
2987
|
+
if (Array.isArray(_tmPs?.project?.team)) {
|
|
2988
|
+
team = _tmPs.project.team;
|
|
2989
|
+
}
|
|
2990
|
+
// Count decisions with more than one participant as team decisions
|
|
2991
|
+
if (Array.isArray(_tmPs?.recentDecisions)) {
|
|
2992
|
+
teamDecisions = _tmPs.recentDecisions.filter(
|
|
2993
|
+
d => Array.isArray(d?.participants) && d.participants.length > 1
|
|
2994
|
+
).length;
|
|
2995
|
+
}
|
|
2996
|
+
} catch { /* non-fatal */ }
|
|
2997
|
+
|
|
2998
|
+
// Fall back to git user if no team configured
|
|
2999
|
+
let ownerName = '(you)';
|
|
3000
|
+
if (team.length === 0) {
|
|
3001
|
+
try {
|
|
3002
|
+
const { execSync: _tmExec } = await import('node:child_process');
|
|
3003
|
+
const gitUser = _tmExec('git config user.name 2>/dev/null', {
|
|
3004
|
+
encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
3005
|
+
}).trim();
|
|
3006
|
+
if (gitUser) ownerName = gitUser;
|
|
3007
|
+
} catch { /* non-fatal */ }
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
const memberRows = [];
|
|
3011
|
+
if (team.length === 0) {
|
|
3012
|
+
memberRows.push(row(` ${ownerName} (owner)`));
|
|
3013
|
+
} else {
|
|
3014
|
+
for (const member of team) {
|
|
3015
|
+
const role = member.role || 'member';
|
|
3016
|
+
memberRows.push(row(` ${member.name} (${role})`));
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
const lines = [
|
|
3021
|
+
top,
|
|
3022
|
+
row('Team'),
|
|
3023
|
+
sep,
|
|
3024
|
+
row('Members'),
|
|
3025
|
+
...memberRows,
|
|
3026
|
+
sep,
|
|
3027
|
+
row(`Shared Sessions: ${sharedSessions}`),
|
|
3028
|
+
row(`Team decisions: ${teamDecisions}`),
|
|
3029
|
+
sep,
|
|
3030
|
+
row('[a] add member [b] back'),
|
|
3031
|
+
bot,
|
|
3032
|
+
];
|
|
3033
|
+
process.stdout.write('\n' + lines.join('\n') + '\n\n');
|
|
3034
|
+
|
|
3035
|
+
const raw = (await ask(' Choice: ')).trim();
|
|
3036
|
+
const choice = raw.toLowerCase();
|
|
3037
|
+
|
|
3038
|
+
if (choice === 'a') {
|
|
3039
|
+
const name = (await ask(' Member name: ')).trim();
|
|
3040
|
+
if (name) {
|
|
3041
|
+
try {
|
|
3042
|
+
const _tmLdAdd = await import('../src/living-docs.mjs');
|
|
3043
|
+
const _tmCur = _tmLdAdd.getProjectState(cwd);
|
|
3044
|
+
const _tmTeam = Array.isArray(_tmCur?.project?.team) ? [..._tmCur.project.team] : [];
|
|
3045
|
+
_tmTeam.push({ name, role: 'member', addedAt: new Date().toISOString() });
|
|
3046
|
+
_tmLdAdd.updateProject({ team: _tmTeam }, cwd);
|
|
3047
|
+
process.stdout.write(`\n Added ${name} to team.\n\n`);
|
|
3048
|
+
await ask(' Press Enter to continue...');
|
|
3049
|
+
} catch {
|
|
3050
|
+
process.stdout.write('\n Could not save team member.\n\n');
|
|
3051
|
+
await ask(' Press Enter to continue...');
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
return { next: 'team' };
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
if (choice === 'b' || choice === 'back' || choice === 'q' || raw === '\x1b') {
|
|
3058
|
+
return { next: 'main' };
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
return { next: 'main' };
|
|
3062
|
+
}
|
|
3063
|
+
|
|
2692
3064
|
|
|
2693
3065
|
// ─── Helper: aggregatePlans ───────────────────────────────────────────────────
|
|
2694
3066
|
|
|
@@ -2873,115 +3245,194 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2873
3245
|
// ─── Onboarding Wizard ───────────────────────────────────────────────────────
|
|
2874
3246
|
|
|
2875
3247
|
/**
|
|
2876
|
-
*
|
|
2877
|
-
*
|
|
2878
|
-
*
|
|
3248
|
+
* Animated first-run setup wizard.
|
|
3249
|
+
* 5 steps: welcome → env scan → replit-tools → import → work style → ready.
|
|
3250
|
+
* Uses src/fx.mjs when available; falls back to plain output stubs.
|
|
3251
|
+
*
|
|
3252
|
+
* @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
|
|
2879
3253
|
* @param {string} cwd
|
|
2880
3254
|
* @param {object} rl readline interface
|
|
2881
3255
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
2882
3256
|
*/
|
|
2883
3257
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
2884
3258
|
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
2885
|
-
const
|
|
2886
|
-
|
|
2887
|
-
//
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3259
|
+
const fx = await getFx();
|
|
3260
|
+
|
|
3261
|
+
// ─── Step 1: Welcome banner ────────────────────────────────────────────────
|
|
3262
|
+
fx.clearScreen();
|
|
3263
|
+
fx.banner('🧠 DUAL-BRAIN');
|
|
3264
|
+
fx.nl();
|
|
3265
|
+
fx.info("Welcome! Let's set up your AI work partner.");
|
|
3266
|
+
fx.nl();
|
|
3267
|
+
await fx.sleep(800);
|
|
3268
|
+
|
|
3269
|
+
// ─── Step 2: Environment detection ────────────────────────────────────────
|
|
3270
|
+
fx.step(1, 5, 'Scanning environment');
|
|
3271
|
+
fx.nl();
|
|
3272
|
+
|
|
3273
|
+
// Run capability detection in parallel with the animations
|
|
3274
|
+
const capsPromise = detectCapabilities(cwd);
|
|
3275
|
+
|
|
3276
|
+
await fx.loadingSequence([
|
|
3277
|
+
{ text: 'Detecting container...', duration: 500, successText: 'Replit container detected' },
|
|
3278
|
+
{ text: 'Checking CLI tools...', duration: 400, successText: 'CLI tools available (git, node, claude...)' },
|
|
3279
|
+
{ text: 'Scanning secrets...', duration: 350, successText: 'Environment scanned' },
|
|
3280
|
+
]);
|
|
2905
3281
|
|
|
2906
|
-
//
|
|
2907
|
-
const caps
|
|
3282
|
+
// Await actual capability data
|
|
3283
|
+
const caps = await capsPromise;
|
|
2908
3284
|
const claudeReady = caps.claude.available;
|
|
2909
3285
|
const openaiReady = caps.openai.available;
|
|
2910
3286
|
const codexAvailable = caps.codex.available;
|
|
2911
3287
|
|
|
2912
|
-
//
|
|
3288
|
+
// Override the generic "secrets" success with real data
|
|
3289
|
+
const secretsLine = claudeReady || openaiReady
|
|
3290
|
+
? 'API keys configured'
|
|
3291
|
+
: 'No API keys found — configure later';
|
|
3292
|
+
fx.info(secretsLine);
|
|
3293
|
+
fx.nl();
|
|
3294
|
+
|
|
3295
|
+
// ─── Step 3: Detect replit-tools ──────────────────────────────────────────
|
|
3296
|
+
fx.step(2, 5, 'Detecting replit-tools');
|
|
3297
|
+
fx.nl();
|
|
3298
|
+
|
|
2913
3299
|
const rt = detectReplitTools(cwd);
|
|
3300
|
+
const rtSpinner = fx.spinner('Looking for replit-tools...').start();
|
|
3301
|
+
await fx.sleep(700);
|
|
3302
|
+
|
|
3303
|
+
let rtSessionCount = 0;
|
|
3304
|
+
if (rt.installed) {
|
|
3305
|
+
const vStr = rt.version ? ` v${rt.version}` : '';
|
|
3306
|
+
rtSpinner.succeed(`replit-tools${vStr} detected`);
|
|
3307
|
+
// Count available sessions
|
|
3308
|
+
try {
|
|
3309
|
+
const sessions = importReplitSessions(cwd);
|
|
3310
|
+
rtSessionCount = sessions.length;
|
|
3311
|
+
} catch { /* non-fatal */ }
|
|
3312
|
+
} else {
|
|
3313
|
+
rtSpinner.warn('replit-tools not found — install with: npm i -g replit-tools');
|
|
3314
|
+
}
|
|
3315
|
+
fx.nl();
|
|
2914
3316
|
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
const RESET = '\x1b[0m';
|
|
3317
|
+
// ─── Step 4: Import conversations ─────────────────────────────────────────
|
|
3318
|
+
fx.step(3, 5, 'Import conversations');
|
|
3319
|
+
fx.nl();
|
|
2919
3320
|
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
console.log('');
|
|
2945
|
-
} else if (claudeReady && !openaiReady && !codexAvailable) {
|
|
2946
|
-
console.log(` ${DIM}Tip: Add OPENAI_API_KEY for dual-brain collaboration${RESET}`);
|
|
2947
|
-
console.log('');
|
|
2948
|
-
} else if (!claudeReady && (openaiReady || codexAvailable)) {
|
|
2949
|
-
console.log(` ${DIM}Note: Use within Claude Code for full dual-brain${RESET}`);
|
|
2950
|
-
console.log('');
|
|
3321
|
+
if (rt.installed && rtSessionCount > 0) {
|
|
3322
|
+
fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
|
|
3323
|
+
fx.nl();
|
|
3324
|
+
|
|
3325
|
+
// Ask user — line-based input since we may not have raw mode here
|
|
3326
|
+
process.stdout.write(' Import conversations? [y/N]: ');
|
|
3327
|
+
const importChoice = (await ask('')).trim().toLowerCase();
|
|
3328
|
+
|
|
3329
|
+
if (importChoice === 'y' || importChoice === 'yes') {
|
|
3330
|
+
const importSpinner = fx.spinner('Importing sessions...').start();
|
|
3331
|
+
await fx.sleep(600);
|
|
3332
|
+
try {
|
|
3333
|
+
// Sessions are already imported via importReplitSessions above (lazy-loaded)
|
|
3334
|
+
importSpinner.succeed(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} imported`);
|
|
3335
|
+
} catch (e) {
|
|
3336
|
+
importSpinner.fail(`Import failed: ${e.message}`);
|
|
3337
|
+
}
|
|
3338
|
+
} else {
|
|
3339
|
+
fx.dim('Skipped — you can import later from Settings → Import');
|
|
3340
|
+
}
|
|
3341
|
+
} else if (rt.installed) {
|
|
3342
|
+
fx.dim('No sessions to import');
|
|
3343
|
+
} else {
|
|
3344
|
+
fx.dim('Skipping — replit-tools not found');
|
|
2951
3345
|
}
|
|
3346
|
+
fx.nl();
|
|
2952
3347
|
|
|
2953
|
-
//
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
console.log(wRow(' 3 🔥 Full Power — deep reasoning, dual-brain when it matters'));
|
|
2962
|
-
console.log(wBottom);
|
|
2963
|
-
console.log('');
|
|
3348
|
+
// ─── Step 5: Work style selection ─────────────────────────────────────────
|
|
3349
|
+
fx.step(4, 5, 'Choose your style');
|
|
3350
|
+
fx.nl();
|
|
3351
|
+
process.stdout.write(' How do you want to work?\n\n');
|
|
3352
|
+
process.stdout.write(' [1] ⚡ Fast — speed over caution, auto-execute\n');
|
|
3353
|
+
process.stdout.write(' [2] ⚖️ Balanced — smart routing, reviews when it matters\n');
|
|
3354
|
+
process.stdout.write(' [3] 🔒 Thorough — dual-brain everything, max quality\n');
|
|
3355
|
+
fx.nl();
|
|
2964
3356
|
|
|
2965
|
-
const styleChoice = (await ask(' Choice [2]: ')).trim();
|
|
2966
3357
|
const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
2967
|
-
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': '
|
|
3358
|
+
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Thorough' };
|
|
3359
|
+
|
|
3360
|
+
let styleChoice = '2'; // default
|
|
3361
|
+
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
3362
|
+
|
|
3363
|
+
if (isTTY) {
|
|
3364
|
+
// Raw keypress — single character
|
|
3365
|
+
const { emitKeypressEvents } = await import('node:readline');
|
|
3366
|
+
emitKeypressEvents(process.stdin, rl);
|
|
3367
|
+
|
|
3368
|
+
process.stdout.write(' Choice [2]: ');
|
|
3369
|
+
styleChoice = await new Promise((resolve) => {
|
|
3370
|
+
const wasRaw = process.stdin.isRaw;
|
|
3371
|
+
process.stdin.setRawMode(true);
|
|
3372
|
+
|
|
3373
|
+
const cleanup = () => {
|
|
3374
|
+
process.stdin.removeListener('keypress', onKey);
|
|
3375
|
+
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
3376
|
+
};
|
|
3377
|
+
|
|
3378
|
+
const onKey = (str, key) => {
|
|
3379
|
+
if (!key) return;
|
|
3380
|
+
const name = key.name || '';
|
|
3381
|
+
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
3382
|
+
cleanup();
|
|
3383
|
+
process.stdout.write('\n');
|
|
3384
|
+
resolve('2');
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
if (name === 'return' || name === 'enter') {
|
|
3388
|
+
cleanup();
|
|
3389
|
+
process.stdout.write('\n');
|
|
3390
|
+
resolve('2');
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
if (str === '1' || str === '2' || str === '3') {
|
|
3394
|
+
cleanup();
|
|
3395
|
+
process.stdout.write(`${str}\n`);
|
|
3396
|
+
resolve(str);
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
};
|
|
3400
|
+
|
|
3401
|
+
process.stdin.on('keypress', onKey);
|
|
3402
|
+
});
|
|
3403
|
+
} else {
|
|
3404
|
+
// Fallback: line-based prompt
|
|
3405
|
+
process.stdout.write(' Choice [2]: ');
|
|
3406
|
+
styleChoice = (await ask('')).trim() || '2';
|
|
3407
|
+
}
|
|
3408
|
+
|
|
2968
3409
|
const chosenBias = styleMap[styleChoice] || 'balanced';
|
|
2969
3410
|
const chosenName = styleNames[chosenBias];
|
|
3411
|
+
fx.nl();
|
|
2970
3412
|
|
|
2971
|
-
//
|
|
3413
|
+
// Non-blocking note if metered API detected
|
|
2972
3414
|
if (openaiReady && caps.openai.metered) {
|
|
2973
|
-
|
|
2974
|
-
|
|
3415
|
+
const DIM = '\x1b[2m'; const RESET = '\x1b[0m';
|
|
3416
|
+
process.stdout.write(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}\n\n`);
|
|
2975
3417
|
}
|
|
2976
3418
|
|
|
2977
|
-
//
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3419
|
+
// ─── Step 6: Ready ────────────────────────────────────────────────────────
|
|
3420
|
+
fx.step(5, 5, 'Ready!');
|
|
3421
|
+
fx.nl();
|
|
3422
|
+
|
|
3423
|
+
// Init living docs
|
|
3424
|
+
try {
|
|
3425
|
+
const ld = await getLivingDocs();
|
|
3426
|
+
if (ld.initLivingDocs) ld.initLivingDocs(cwd);
|
|
3427
|
+
} catch { /* non-fatal */ }
|
|
2983
3428
|
|
|
2984
|
-
|
|
3429
|
+
await fx.sleep(400);
|
|
3430
|
+
fx.celebrate(`dual-brain is ready! (${chosenName} mode)`);
|
|
3431
|
+
fx.nl();
|
|
3432
|
+
fx.info('Type anything to get started. Your AI partner is listening.');
|
|
3433
|
+
await fx.sleep(1200);
|
|
3434
|
+
|
|
3435
|
+
// ─── Build and return the profile object ──────────────────────────────────
|
|
2985
3436
|
const finalProfile = loadProfile(cwd);
|
|
2986
3437
|
|
|
2987
3438
|
finalProfile.providers.claude = { enabled: claudeReady };
|
|
@@ -4026,6 +4477,7 @@ const SCREENS = {
|
|
|
4026
4477
|
main: mainScreen,
|
|
4027
4478
|
'new-session': newSessionScreen,
|
|
4028
4479
|
settings: settingsScreen,
|
|
4480
|
+
team: teamScreen,
|
|
4029
4481
|
'import-picker': importPickerScreen,
|
|
4030
4482
|
'pr-triage': prTriageScreen,
|
|
4031
4483
|
subscriptions: subscriptionsScreen,
|
|
@@ -4590,6 +5042,21 @@ async function main() {
|
|
|
4590
5042
|
}
|
|
4591
5043
|
|
|
4592
5044
|
if (cmd === 'init') {
|
|
5045
|
+
// init --replit: run Replit-specific integration setup
|
|
5046
|
+
if (args.includes('--replit')) {
|
|
5047
|
+
const cwd = process.cwd();
|
|
5048
|
+
const dryRun = args.includes('--dry-run');
|
|
5049
|
+
try {
|
|
5050
|
+
const replit = await import('../src/replit.mjs');
|
|
5051
|
+
const report = await replit.initReplitIntegration({ dryRun, cwd });
|
|
5052
|
+
console.log(replit.formatReplitReport(report));
|
|
5053
|
+
} catch (e) {
|
|
5054
|
+
console.error('replit.mjs not available yet — skipping Replit init');
|
|
5055
|
+
if (process.env.DEBUG) console.error(e.message);
|
|
5056
|
+
}
|
|
5057
|
+
return;
|
|
5058
|
+
}
|
|
5059
|
+
|
|
4593
5060
|
if (isInteractive) {
|
|
4594
5061
|
// Run onboarding wizard then main screen
|
|
4595
5062
|
const cwd = process.cwd();
|