dual-brain 0.2.2 → 0.2.4
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 +971 -411
- package/package.json +1 -1
- package/src/dispatch.mjs +14 -0
- package/src/pipeline.mjs +6 -0
- package/src/profile.mjs +535 -10
- package/src/receipt.mjs +213 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
detectCapabilities,
|
|
16
16
|
saveSubscription, listSubscriptions,
|
|
17
17
|
autoSetup,
|
|
18
|
+
loadCredentials, saveCredentials, getCredentialSummary, detectCredentials, addCredential, removeCredential, checkCredentialHealth,
|
|
18
19
|
} from '../src/profile.mjs';
|
|
19
20
|
|
|
20
21
|
import { detectTask } from '../src/detect.mjs';
|
|
@@ -1089,7 +1090,20 @@ function profileExists(cwd) {
|
|
|
1089
1090
|
const dir = cwd || process.cwd();
|
|
1090
1091
|
const globalPath = join(process.env.HOME || '/root', '.config', 'dual-brain', 'profile.json');
|
|
1091
1092
|
const projectPath = join(dir, '.dualbrain', 'profile.json');
|
|
1092
|
-
|
|
1093
|
+
// Check file existence AND that setup wizard completed (setupComplete flag)
|
|
1094
|
+
if (existsSync(projectPath)) {
|
|
1095
|
+
try {
|
|
1096
|
+
const p = JSON.parse(readFileSync(projectPath, 'utf8'));
|
|
1097
|
+
return p.setupComplete === true;
|
|
1098
|
+
} catch { return true; } // malformed but exists — treat as complete
|
|
1099
|
+
}
|
|
1100
|
+
if (existsSync(globalPath)) {
|
|
1101
|
+
try {
|
|
1102
|
+
const p = JSON.parse(readFileSync(globalPath, 'utf8'));
|
|
1103
|
+
return p.setupComplete === true;
|
|
1104
|
+
} catch { return true; }
|
|
1105
|
+
}
|
|
1106
|
+
return false;
|
|
1093
1107
|
}
|
|
1094
1108
|
|
|
1095
1109
|
// ─── Plan label helpers ───────────────────────────────────────────────────────
|
|
@@ -1668,6 +1682,141 @@ function makeBoxRow(content, W) {
|
|
|
1668
1682
|
return `│ ${content}${' '.repeat(padding)} │`;
|
|
1669
1683
|
}
|
|
1670
1684
|
|
|
1685
|
+
// ─── Command palette: input classifier ───────────────────────────────────────
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Classify user input into one of three tiers:
|
|
1689
|
+
* { tier: 'free', command, args } — deterministic, zero tokens
|
|
1690
|
+
* { tier: 'cheap' } — question → haiku
|
|
1691
|
+
* { tier: 'full' } — work task → confirm then dispatch
|
|
1692
|
+
*/
|
|
1693
|
+
function classifyInput(input) {
|
|
1694
|
+
const trimmed = input.trim();
|
|
1695
|
+
const lower = trimmed.toLowerCase();
|
|
1696
|
+
const parts = trimmed.split(/\s+/);
|
|
1697
|
+
const cmd = parts[0].toLowerCase();
|
|
1698
|
+
const args = parts.slice(1);
|
|
1699
|
+
|
|
1700
|
+
// Tier 1: FREE — exact command matches
|
|
1701
|
+
const FREE_COMMANDS = new Map([
|
|
1702
|
+
['resume', 'resume'],
|
|
1703
|
+
['r', 'resume'],
|
|
1704
|
+
['status', 'status'],
|
|
1705
|
+
['sessions', 'sessions'],
|
|
1706
|
+
['ss', 'sessions'],
|
|
1707
|
+
['settings', 'settings'],
|
|
1708
|
+
['s', 'settings'],
|
|
1709
|
+
['team', 'team'],
|
|
1710
|
+
['t', 'team'],
|
|
1711
|
+
['doctor', 'doctor'],
|
|
1712
|
+
['d', 'doctor'],
|
|
1713
|
+
['health', 'health'],
|
|
1714
|
+
['h', 'health'],
|
|
1715
|
+
['projects', 'projects'],
|
|
1716
|
+
['p', 'projects'],
|
|
1717
|
+
['help', 'help'],
|
|
1718
|
+
['?', 'help'],
|
|
1719
|
+
['quit', 'quit'],
|
|
1720
|
+
['q', 'quit'],
|
|
1721
|
+
['exit', 'quit'],
|
|
1722
|
+
['budget', 'budget'],
|
|
1723
|
+
['b', 'budget'],
|
|
1724
|
+
]);
|
|
1725
|
+
|
|
1726
|
+
if (FREE_COMMANDS.has(cmd)) {
|
|
1727
|
+
return { tier: 'free', command: FREE_COMMANDS.get(cmd), args };
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// Multi-word free commands
|
|
1731
|
+
if (lower.startsWith('search ')) {
|
|
1732
|
+
return { tier: 'free', command: 'search', args: parts.slice(1) };
|
|
1733
|
+
}
|
|
1734
|
+
if (lower === 'init --replit') {
|
|
1735
|
+
return { tier: 'free', command: 'init --replit', args: [] };
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Tier 2: CHEAP — question / diagnostic patterns → haiku
|
|
1739
|
+
const QUESTION_WORDS = /^(why|what|how|where|when|who|is my|check|show me|explain|tell me|list|am i|are there|does|did|can i|will|should i)/i;
|
|
1740
|
+
const QUESTION_CONTAINS = /\b(why|what|how is|how are|where is|where are|explain|tell me|show me)\b/i;
|
|
1741
|
+
if (QUESTION_WORDS.test(lower) || QUESTION_CONTAINS.test(lower)) {
|
|
1742
|
+
return { tier: 'cheap' };
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// Tier 3: FULL — everything else is a work task
|
|
1746
|
+
return { tier: 'full' };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// ─── Dashboard: resume state detection ───────────────────────────────────────
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Detect resumable state for dashboard contextual hint.
|
|
1753
|
+
* Returns an object with type ('resumable' | 'fresh' | 'none') and detail fields.
|
|
1754
|
+
* All checks are best-effort and fail silent.
|
|
1755
|
+
*/
|
|
1756
|
+
async function detectResumeState(cwd) {
|
|
1757
|
+
const result = { type: 'none', label: null, ageLabel: null, nextAction: null };
|
|
1758
|
+
|
|
1759
|
+
// Check for recent receipt (< 24h)
|
|
1760
|
+
try {
|
|
1761
|
+
const { getLatestReceipt } = await import('../src/receipt.mjs');
|
|
1762
|
+
const receipt = getLatestReceipt(cwd);
|
|
1763
|
+
if (receipt) {
|
|
1764
|
+
const ageMs = Date.now() - Date.parse(receipt.timestamp);
|
|
1765
|
+
if (ageMs < 24 * 60 * 60 * 1000) {
|
|
1766
|
+
const mins = Math.round(ageMs / 60000);
|
|
1767
|
+
const age = mins < 60
|
|
1768
|
+
? `${mins}m ago`
|
|
1769
|
+
: mins < 1440
|
|
1770
|
+
? `${Math.round(mins / 60)}h ago`
|
|
1771
|
+
: `${Math.round(mins / 1440)}d ago`;
|
|
1772
|
+
const fileCount = (receipt.filesChanged || []).length;
|
|
1773
|
+
const filePart = fileCount > 0 ? ` · ${fileCount} file${fileCount !== 1 ? 's' : ''}` : '';
|
|
1774
|
+
result.type = 'resumable';
|
|
1775
|
+
result.label = (receipt.goal || 'last session').slice(0, 40);
|
|
1776
|
+
result.ageLabel = age;
|
|
1777
|
+
result.filePart = filePart;
|
|
1778
|
+
result.nextAction = (receipt.nextAction || '').slice(0, 35);
|
|
1779
|
+
return result;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
} catch { /* non-fatal */ }
|
|
1783
|
+
|
|
1784
|
+
// Check for open tasks in ledger
|
|
1785
|
+
try {
|
|
1786
|
+
const { getOpenTasks } = await import('../src/ledger.mjs');
|
|
1787
|
+
const open = getOpenTasks(cwd);
|
|
1788
|
+
if (open.length > 0) {
|
|
1789
|
+
result.type = 'resumable';
|
|
1790
|
+
result.label = (open[0].intent || 'open task').slice(0, 40);
|
|
1791
|
+
result.ageLabel = null;
|
|
1792
|
+
result.filePart = '';
|
|
1793
|
+
result.nextAction = `${open.length} open task${open.length !== 1 ? 's' : ''}`;
|
|
1794
|
+
return result;
|
|
1795
|
+
}
|
|
1796
|
+
} catch { /* non-fatal */ }
|
|
1797
|
+
|
|
1798
|
+
// Check if this is a fresh project (package.json but no dual-brain history)
|
|
1799
|
+
try {
|
|
1800
|
+
const { existsSync: exists } = await import('node:fs');
|
|
1801
|
+
const { join: pjoin } = await import('node:path');
|
|
1802
|
+
const hasPkg = exists(pjoin(cwd, 'package.json'));
|
|
1803
|
+
const hasHistory = exists(pjoin(cwd, '.dual-brain', 'receipts'));
|
|
1804
|
+
if (hasPkg && !hasHistory) {
|
|
1805
|
+
let pkgName = 'this project';
|
|
1806
|
+
try {
|
|
1807
|
+
const { readFileSync: rfs } = await import('node:fs');
|
|
1808
|
+
const pkg = JSON.parse(rfs(pjoin(cwd, 'package.json'), 'utf8'));
|
|
1809
|
+
if (pkg.name) pkgName = pkg.name;
|
|
1810
|
+
} catch {}
|
|
1811
|
+
result.type = 'fresh';
|
|
1812
|
+
result.label = pkgName;
|
|
1813
|
+
return result;
|
|
1814
|
+
}
|
|
1815
|
+
} catch { /* non-fatal */ }
|
|
1816
|
+
|
|
1817
|
+
return result;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1671
1820
|
// ─── Screen: mainScreen ───────────────────────────────────────────────────────
|
|
1672
1821
|
|
|
1673
1822
|
async function mainScreen(rl, ask) {
|
|
@@ -1737,31 +1886,20 @@ async function mainScreen(rl, ask) {
|
|
|
1737
1886
|
// ── Interrupted work detection ────────────────────────────────────────────
|
|
1738
1887
|
const interrupted = detectInterruptedWork(allSessions, cwd);
|
|
1739
1888
|
|
|
1740
|
-
// ──
|
|
1741
|
-
const termW = process.stdout.columns ||
|
|
1742
|
-
const
|
|
1743
|
-
const W = boxW - 4; // inner content width (│ {content} │)
|
|
1744
|
-
|
|
1745
|
-
const top = `┌${'─'.repeat(boxW - 2)}┐`;
|
|
1746
|
-
const sep = `├${'─'.repeat(boxW - 2)}┤`;
|
|
1747
|
-
const bot = `└${'─'.repeat(boxW - 2)}┘`;
|
|
1748
|
-
|
|
1749
|
-
const row = (content) => makeBoxRow(content, W);
|
|
1889
|
+
// ── Studio Console layout ─────────────────────────────────────────────────
|
|
1890
|
+
const termW = process.stdout.columns || 80;
|
|
1891
|
+
const W = Math.min(termW - 2, 78); // usable content width
|
|
1750
1892
|
|
|
1751
1893
|
// ── Continuation card (interrupted work) ─────────────────────────────────
|
|
1752
1894
|
if (interrupted) {
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
: ` ${interrupted.reason} · ${interrupted.ageLabel}`;
|
|
1762
|
-
const actLine = ' [Enter] Resume [n] New session [s] Skip';
|
|
1763
|
-
|
|
1764
|
-
process.stdout.write([ctop, crow(titleLine), csep, crow(lastLine), crow(actLine), cbot].join('\n') + '\n\n');
|
|
1895
|
+
const DIM = '\x1b[2m', RST = '\x1b[0m', YLW = '\x1b[33m';
|
|
1896
|
+
process.stdout.write(`\n ${YLW}Continue:${RST} ${interrupted.sessionName}\n`);
|
|
1897
|
+
if (interrupted.lastState) {
|
|
1898
|
+
process.stdout.write(` ${DIM}Last: ${interrupted.lastState} · ${interrupted.ageLabel}${RST}\n`);
|
|
1899
|
+
} else {
|
|
1900
|
+
process.stdout.write(` ${DIM}${interrupted.reason} · ${interrupted.ageLabel}${RST}\n`);
|
|
1901
|
+
}
|
|
1902
|
+
process.stdout.write(` ${DIM}[Enter] resume [n] new [s] skip${RST}\n\n`);
|
|
1765
1903
|
|
|
1766
1904
|
// Wait for a keypress to decide what to do with the card
|
|
1767
1905
|
const readline2 = await import('node:readline');
|
|
@@ -1833,8 +1971,13 @@ async function mainScreen(rl, ask) {
|
|
|
1833
1971
|
envReport = scanEnvironment(cwd);
|
|
1834
1972
|
} catch { /* non-fatal */ }
|
|
1835
1973
|
|
|
1836
|
-
// ──
|
|
1837
|
-
const
|
|
1974
|
+
// ── Studio Console: resolve provider availability ────────────────────────
|
|
1975
|
+
const claudeAvail = envReport
|
|
1976
|
+
? envReport.secrets?.ANTHROPIC_API_KEY || auth.claude.found
|
|
1977
|
+
: auth.claude.found;
|
|
1978
|
+
const openaiAvail = envReport
|
|
1979
|
+
? envReport.secrets?.OPENAI_API_KEY || auth.openai.found
|
|
1980
|
+
: auth.openai.found;
|
|
1838
1981
|
|
|
1839
1982
|
// ── Box 2 — Workspace: gather git data ───────────────────────────────────
|
|
1840
1983
|
let gitBranch = 'unknown';
|
|
@@ -1878,24 +2021,14 @@ async function mainScreen(rl, ask) {
|
|
|
1878
2021
|
}
|
|
1879
2022
|
} catch {}
|
|
1880
2023
|
|
|
1881
|
-
// ──
|
|
2024
|
+
// ── Workspace data ────────────────────────────────────────────────────────
|
|
1882
2025
|
const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
|
|
1883
2026
|
const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
|
|
1884
|
-
const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
|
|
1885
|
-
const workspaceLine2 = gitLastMsg
|
|
1886
|
-
? `Last: ${gitLastMsg} (${gitLastAgo})`
|
|
1887
|
-
: '';
|
|
1888
2027
|
|
|
1889
2028
|
// Open PRs
|
|
1890
2029
|
const repoState = detectRepoState(cwd);
|
|
1891
2030
|
const openPRs = await detectOpenPRs(cwd);
|
|
1892
2031
|
|
|
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'}`));
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
2032
|
// ── Box 3 — Awareness: observer + roadmap + risk ──────────────────────────
|
|
1900
2033
|
let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
|
|
1901
2034
|
let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
|
|
@@ -1987,110 +2120,157 @@ async function mainScreen(rl, ask) {
|
|
|
1987
2120
|
const rtInfo = replitMod.inspectReplitTools(cwd);
|
|
1988
2121
|
const authInfo = replitMod.getAuthStatus(cwd);
|
|
1989
2122
|
const archive = replitMod.getSessionArchive(cwd);
|
|
1990
|
-
const archCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
|
|
2123
|
+
const archCount = Array.isArray(archive) ? archive.length : (archive?.totalSessions ?? archive?.count ?? 0);
|
|
1991
2124
|
const secretNames = replitMod.listSecretNames();
|
|
1992
2125
|
const secretCount = Array.isArray(secretNames) ? secretNames.length : 0;
|
|
1993
2126
|
const verStr = rtInfo.version ? `v${rtInfo.version}` : (rtInfo.installed ? 'installed' : 'not installed');
|
|
1994
|
-
const
|
|
1995
|
-
|
|
1996
|
-
replitAwarenessRows.push(
|
|
2127
|
+
const isAuthenticated = authInfo.authenticated ?? (authInfo.available && authInfo.tokenStatus === 'valid');
|
|
2128
|
+
const authStr = isAuthenticated ? '\x1b[32m✓\x1b[0m auth' : '\x1b[2mno auth\x1b[0m';
|
|
2129
|
+
replitAwarenessRows.push(`Replit replit-tools ${verStr} ${authStr}`);
|
|
2130
|
+
replitAwarenessRows.push(`${archCount} archived session${archCount !== 1 ? 's' : ''} ${secretCount} secret${secretCount !== 1 ? 's' : ''}`);
|
|
1997
2131
|
}
|
|
1998
2132
|
} catch { /* replit.mjs not available — skip */ }
|
|
1999
2133
|
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2134
|
+
// ── Recent work items (from awareness + sessions) — max 3 lines, dim ──────
|
|
2135
|
+
const recentWorkItems = [];
|
|
2136
|
+
// Add awareness observations as recent work if meaningful
|
|
2137
|
+
if (awarenessLine1 && !awarenessLine1.includes('Ready to work')) {
|
|
2138
|
+
const plainAware1 = awarenessLine1.replace(/\x1b\[[0-9;]*m/g, '').replace(/[︀-️]/g, '').trim();
|
|
2139
|
+
if (plainAware1) recentWorkItems.push({ ok: !plainAware1.startsWith('⚠') && !plainAware1.startsWith('🔴'), text: plainAware1.replace(/^[🔴🟡💡]\s*/, '') });
|
|
2140
|
+
}
|
|
2141
|
+
// Add last commit as a recent work item
|
|
2142
|
+
if (gitLastMsg) {
|
|
2143
|
+
recentWorkItems.push({ ok: true, text: `${gitLastMsg} (${gitLastAgo})` });
|
|
2144
|
+
}
|
|
2145
|
+
// Fill from sessions if still room
|
|
2146
|
+
if (recentWorkItems.length < 3 && recentSessions.length > 0) {
|
|
2147
|
+
const sess = recentSessions[0];
|
|
2148
|
+
let rawName = sess.name || '';
|
|
2149
|
+
if (/^Session [0-9a-f]{8,}$/i.test(rawName)) rawName = sess.id.slice(0, 8);
|
|
2150
|
+
if (rawName) recentWorkItems.push({ ok: true, text: rawName.slice(0, 50) });
|
|
2151
|
+
}
|
|
2006
2152
|
|
|
2007
|
-
// ──
|
|
2008
|
-
const
|
|
2009
|
-
if (recentSessions.length === 0) {
|
|
2010
|
-
const noSessMsg = 'No sessions yet. Press n to start.';
|
|
2011
|
-
sessionRows.push(row(noSessMsg));
|
|
2012
|
-
} else {
|
|
2013
|
-
recentSessions.forEach((sess, i) => {
|
|
2014
|
-
// Normalize name: strip "Session XXXXXXXX" fallbacks
|
|
2015
|
-
let rawName = sess.name || '';
|
|
2016
|
-
if (/^Session [0-9a-f]{8,}$/i.test(rawName)) {
|
|
2017
|
-
rawName = sess.project
|
|
2018
|
-
? sess.project.replace(/^-/, '/').replace(/-/g, '/')
|
|
2019
|
-
: sess.id.slice(0, 8);
|
|
2020
|
-
}
|
|
2153
|
+
// ── Resume state detection ────────────────────────────────────────────────
|
|
2154
|
+
const resumeState = await detectResumeState(cwd);
|
|
2021
2155
|
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
if (sess.isActive) {
|
|
2026
|
-
badges.push('\x1b[32m[active]\x1b[0m');
|
|
2027
|
-
badgeVisible.push('[active]'.length);
|
|
2028
|
-
}
|
|
2029
|
-
const ageMs = sess.lastActive ? Date.now() - new Date(sess.lastActive).getTime() : 0;
|
|
2030
|
-
if (ageMs > 7 * 24 * 3600 * 1000) {
|
|
2031
|
-
badges.push('\x1b[2m[stale]\x1b[0m');
|
|
2032
|
-
badgeVisible.push('[stale]'.length);
|
|
2033
|
-
}
|
|
2034
|
-
const msgCount = sess.messageCount ?? sess.promptCount ?? 0;
|
|
2035
|
-
// Human-readable: "4 tasks" instead of "(4)"
|
|
2036
|
-
const taskLabel = msgCount === 1 ? '1 task' : `${msgCount} tasks`;
|
|
2037
|
-
const taskBadge = `\x1b[2m${taskLabel}\x1b[0m`;
|
|
2038
|
-
const taskBadgeW = taskLabel.length;
|
|
2039
|
-
|
|
2040
|
-
const badgeStr = badges.join('');
|
|
2041
|
-
const badgesW = badgeVisible.reduce((s, n) => s + n, 0);
|
|
2042
|
-
|
|
2043
|
-
// Layout: "{num} {name...}{badges} {age} {tasks}"
|
|
2044
|
-
// Use basename for name — strip full paths for readability
|
|
2045
|
-
const displayName = rawName.startsWith('/')
|
|
2046
|
-
? rawName.split('/').filter(Boolean).pop() || rawName
|
|
2047
|
-
: rawName;
|
|
2048
|
-
|
|
2049
|
-
const numStr = String(i + 1);
|
|
2050
|
-
const ageStr = sess.age || '';
|
|
2051
|
-
// Available for name: W minus fixed chrome, badge widths, and task badge
|
|
2052
|
-
const nameMax = W - numStr.length - 2 - badgesW - 2 - ageStr.length - 2 - taskBadgeW;
|
|
2053
|
-
const truncName = displayName.length > nameMax
|
|
2054
|
-
? displayName.slice(0, Math.max(0, nameMax - 3)) + '...'
|
|
2055
|
-
: displayName.padEnd(nameMax);
|
|
2056
|
-
const content = `${numStr} ${truncName}${badgeStr} ${ageStr} ${taskBadge}`;
|
|
2057
|
-
sessionRows.push(row(content));
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2156
|
+
// ── Determine layout mode ─────────────────────────────────────────────────
|
|
2157
|
+
const anyProviderAvail = claudeAvail || openaiAvail;
|
|
2158
|
+
const isReturning = resumeState.type === 'resumable';
|
|
2060
2159
|
|
|
2061
|
-
// ──
|
|
2062
|
-
const
|
|
2063
|
-
const
|
|
2160
|
+
// ── ANSI color shorthands ─────────────────────────────────────────────────
|
|
2161
|
+
const DIM = '\x1b[2m';
|
|
2162
|
+
const RST = '\x1b[0m';
|
|
2163
|
+
const BOLD = '\x1b[1m';
|
|
2164
|
+
const GRN = '\x1b[32m';
|
|
2165
|
+
const YLW = '\x1b[33m';
|
|
2166
|
+
const RED = '\x1b[31m';
|
|
2167
|
+
const GRY = '\x1b[90m';
|
|
2064
2168
|
|
|
2065
|
-
// ──
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
//
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
//
|
|
2086
|
-
|
|
2087
|
-
|
|
2169
|
+
// ── Provider dots ─────────────────────────────────────────────────────────
|
|
2170
|
+
const claudeDot = claudeAvail ? `${GRN}●${RST}` : `${GRY}○${RST}`;
|
|
2171
|
+
const openaiDot = openaiAvail ? `${GRN}●${RST}` : `${GRY}○${RST}`;
|
|
2172
|
+
|
|
2173
|
+
// ── Project name (from package.json or cwd basename) ─────────────────────
|
|
2174
|
+
let projectName = basename(cwd);
|
|
2175
|
+
try {
|
|
2176
|
+
const pkgRaw = readFileSync(join(cwd, 'package.json'), 'utf8');
|
|
2177
|
+
const pkgJson = JSON.parse(pkgRaw);
|
|
2178
|
+
if (pkgJson.name) projectName = pkgJson.name;
|
|
2179
|
+
} catch { /* no package.json */ }
|
|
2180
|
+
|
|
2181
|
+
// ── Separator line ────────────────────────────────────────────────────────
|
|
2182
|
+
const sepW = Math.min(W, 72);
|
|
2183
|
+
const sepLine = `${DIM}${'━'.repeat(sepW)}${RST}`;
|
|
2184
|
+
|
|
2185
|
+
// ── Strip ANSI for width calc ─────────────────────────────────────────────
|
|
2186
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').replace(/[︀-️]/g, '');
|
|
2187
|
+
|
|
2188
|
+
// ── Line 1: status bar ───────────────────────────────────────────────────
|
|
2189
|
+
// " project branch Claude ● GPT ● v0.2.3"
|
|
2190
|
+
const branchStr = `${gitBranch}${uncommittedPart}${aheadPart}`;
|
|
2191
|
+
const providerStr = `Claude ${claudeDot} GPT ${openaiDot}`;
|
|
2192
|
+
const verStr2 = `${DIM}v${version}${RST}`;
|
|
2193
|
+
const statusLeft = ` ${projectName} ${DIM}${branchStr}${RST} ${providerStr}`;
|
|
2194
|
+
const statusRight = verStr2;
|
|
2195
|
+
const statusLeftW = stripAnsi(statusLeft).length;
|
|
2196
|
+
const statusRightW = stripAnsi(statusRight).length;
|
|
2197
|
+
const statusGap = Math.max(1, sepW + 1 - statusLeftW - statusRightW);
|
|
2198
|
+
const statusBar = `${statusLeft}${' '.repeat(statusGap)}${statusRight}`;
|
|
2199
|
+
|
|
2200
|
+
// ── Line 2-3: contextual question + last summary ─────────────────────────
|
|
2201
|
+
let mainQuestion, lastSummary;
|
|
2202
|
+
if (!anyProviderAvail) {
|
|
2203
|
+
mainQuestion = ` ${BOLD}Connect a provider to start working${RST}`;
|
|
2204
|
+
lastSummary = null;
|
|
2205
|
+
} else if (isReturning) {
|
|
2206
|
+
mainQuestion = ` ${BOLD}Resume previous work?${RST}`;
|
|
2207
|
+
const labelTrunc = (resumeState.label || 'last session').slice(0, 45);
|
|
2208
|
+
const agePart = resumeState.ageLabel ? ` · ${resumeState.ageLabel}` : '';
|
|
2209
|
+
const nextPart = resumeState.nextAction ? ` · next: ${resumeState.nextAction}` : '';
|
|
2210
|
+
lastSummary = ` ${DIM}Last: ${labelTrunc}${agePart}${nextPart}${RST}`;
|
|
2211
|
+
} else {
|
|
2212
|
+
mainQuestion = ` ${BOLD}What do you want to build?${RST}`;
|
|
2213
|
+
lastSummary = null;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// ── Suggestions (max 3, bright) ───────────────────────────────────────────
|
|
2217
|
+
let suggestions;
|
|
2218
|
+
const claudeExpiredNow = claudeSub?.expiresAt && Date.parse(claudeSub.expiresAt) < Date.now();
|
|
2219
|
+
const openaiExpiredNow = openaiSub?.expiresAt && Date.parse(openaiSub.expiresAt) < Date.now();
|
|
2220
|
+
if (!anyProviderAvail) {
|
|
2221
|
+
suggestions = ['configure Claude', 'configure GPT', 'browse project'];
|
|
2222
|
+
} else if (claudeExpiredNow || openaiExpiredNow) {
|
|
2223
|
+
const resumeOrBuild = isReturning ? 'resume last session' : 'start building';
|
|
2224
|
+
suggestions = ['refresh auth', resumeOrBuild, 'check project health'];
|
|
2225
|
+
} else if (isReturning) {
|
|
2226
|
+
const openTasks = [];
|
|
2227
|
+
try {
|
|
2228
|
+
const { getOpenTasks } = await import('../src/ledger.mjs');
|
|
2229
|
+
const open = getOpenTasks(cwd);
|
|
2230
|
+
if (open.length > 0) openTasks.push(`continue: ${open[0].intent.slice(0, 30)}`);
|
|
2231
|
+
} catch {}
|
|
2232
|
+
suggestions = openTasks.length > 0
|
|
2233
|
+
? [openTasks[0], 'review changes', 'run tests']
|
|
2234
|
+
: ['resume last session', 'review changes', 'run tests'];
|
|
2235
|
+
} else {
|
|
2236
|
+
suggestions = ['start building', 'explore codebase', 'check project health'];
|
|
2088
2237
|
}
|
|
2238
|
+
const suggestLine = ` ${suggestions.join(' ')}`;
|
|
2089
2239
|
|
|
2090
|
-
//
|
|
2240
|
+
// ── Recent work items (dim, max 3) ────────────────────────────────────────
|
|
2241
|
+
const recentLines = recentWorkItems.slice(0, 3).map(item => {
|
|
2242
|
+
const prefix = item.ok ? `${GRN}✓${RST}` : `${RED}!${RST}`;
|
|
2243
|
+
return ` ${DIM}${prefix} ${item.text}${RST}`;
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
// ── Resolve dashboard spinner before rendering ────────────────────────────
|
|
2091
2247
|
if (dashSpinner) dashSpinner.succeed('Dashboard ready');
|
|
2092
2248
|
|
|
2093
|
-
|
|
2249
|
+
// ── Stale hint ────────────────────────────────────────────────────────────
|
|
2250
|
+
if (staleCount >= 3) {
|
|
2251
|
+
process.stdout.write(`${DIM}${staleCount} stale sessions (>7d) — type "sessions" to manage${RST}\n`);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// ── Render Studio Console ─────────────────────────────────────────────────
|
|
2255
|
+
const out = [];
|
|
2256
|
+
out.push(''); // breathing room
|
|
2257
|
+
out.push(statusBar); // project branch Claude ● GPT ● v0.2.3
|
|
2258
|
+
out.push('');
|
|
2259
|
+
out.push(mainQuestion); // Resume previous work? / What do you want to build?
|
|
2260
|
+
if (lastSummary) out.push(lastSummary);
|
|
2261
|
+
out.push(` \x1b[1m›\x1b[0m`); // bright prompt cursor
|
|
2262
|
+
out.push('');
|
|
2263
|
+
out.push(suggestLine); // contextual suggestions
|
|
2264
|
+
if (recentLines.length > 0) {
|
|
2265
|
+
out.push('');
|
|
2266
|
+
out.push(...recentLines); // ✓ / ! recent work items
|
|
2267
|
+
}
|
|
2268
|
+
out.push('');
|
|
2269
|
+
out.push(` ${sepLine}`); // ━━━━ separator
|
|
2270
|
+
// Input bar rendered inline — the key handler will overwrite this line
|
|
2271
|
+
out.push(` ${DIM}> task or command...${RST}${' '.repeat(Math.max(1, sepW - 22))}${DIM}[?] help${RST}`);
|
|
2272
|
+
|
|
2273
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
2094
2274
|
|
|
2095
2275
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
2096
2276
|
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
@@ -2177,7 +2357,7 @@ async function mainScreen(rl, ask) {
|
|
|
2177
2357
|
// Single-key commands only fire when buffer is empty
|
|
2178
2358
|
if (taskBuffer.length === 0) {
|
|
2179
2359
|
const lower = str.toLowerCase();
|
|
2180
|
-
const singleKeySet = new Set(['n', 's', 't', 'q', '/', 'i']);
|
|
2360
|
+
const singleKeySet = new Set(['n', 's', 't', 'q', '/', 'i', '?', 'h', 'd']);
|
|
2181
2361
|
if (singleKeySet.has(lower)) {
|
|
2182
2362
|
cleanup();
|
|
2183
2363
|
process.stdout.write('\n');
|
|
@@ -2203,15 +2383,124 @@ async function mainScreen(rl, ask) {
|
|
|
2203
2383
|
|
|
2204
2384
|
const choice = typeof raw === 'string' ? raw.toLowerCase() : '';
|
|
2205
2385
|
|
|
2206
|
-
// Typed
|
|
2386
|
+
// ── Typed input — run through command palette ─────────────────────────────
|
|
2207
2387
|
if (raw.startsWith('__task__:')) {
|
|
2208
|
-
const
|
|
2209
|
-
if (
|
|
2210
|
-
|
|
2388
|
+
const input = raw.slice('__task__:'.length).trim();
|
|
2389
|
+
if (!input) return { next: 'main' };
|
|
2390
|
+
|
|
2391
|
+
const classified = classifyInput(input);
|
|
2392
|
+
|
|
2393
|
+
// Tier 1: FREE — deterministic, zero tokens
|
|
2394
|
+
if (classified.tier === 'free') {
|
|
2395
|
+
const cmd = classified.command;
|
|
2396
|
+
const args = classified.args;
|
|
2397
|
+
|
|
2398
|
+
if (cmd === 'resume' || cmd === 'r') {
|
|
2399
|
+
if (recentSessions.length === 0) return { next: 'new-session' };
|
|
2400
|
+
return { next: 'sessions' };
|
|
2401
|
+
}
|
|
2402
|
+
if (cmd === 'status' || cmd === 's') {
|
|
2403
|
+
await cmdStatus([]);
|
|
2404
|
+
await ask('\n Press Enter to continue...');
|
|
2405
|
+
return { next: 'main' };
|
|
2406
|
+
}
|
|
2407
|
+
if (cmd === 'sessions' || cmd === 'ss') {
|
|
2408
|
+
return { next: 'sessions' };
|
|
2409
|
+
}
|
|
2410
|
+
if (cmd === 'settings') {
|
|
2411
|
+
return { next: 'settings' };
|
|
2412
|
+
}
|
|
2413
|
+
if (cmd === 'team' || cmd === 't') {
|
|
2414
|
+
return { next: 'team' };
|
|
2415
|
+
}
|
|
2416
|
+
if (cmd === 'doctor' || cmd === 'd') {
|
|
2417
|
+
return { next: 'diagnostics' };
|
|
2418
|
+
}
|
|
2419
|
+
if (cmd === 'health' || cmd === 'h') {
|
|
2420
|
+
const hooksDir = join(cwd, '.claude', 'hooks');
|
|
2421
|
+
const healthScript = join(hooksDir, 'health-check.mjs');
|
|
2422
|
+
const { spawnSync: sp } = await import('node:child_process');
|
|
2423
|
+
if (existsSync(healthScript)) {
|
|
2424
|
+
sp('node', [healthScript], { stdio: 'inherit', cwd });
|
|
2425
|
+
} else {
|
|
2426
|
+
process.stdout.write('\n health-check.mjs not found — run: dual-brain install\n');
|
|
2427
|
+
}
|
|
2428
|
+
await ask('\n Press Enter to continue...');
|
|
2429
|
+
return { next: 'main' };
|
|
2430
|
+
}
|
|
2431
|
+
if (cmd === 'help' || cmd === '?') {
|
|
2432
|
+
return { next: 'palette-help' };
|
|
2433
|
+
}
|
|
2434
|
+
if (cmd === 'quit' || cmd === 'q') {
|
|
2435
|
+
return { next: 'exit' };
|
|
2436
|
+
}
|
|
2437
|
+
if (cmd === 'search') {
|
|
2438
|
+
const query = args.join(' ');
|
|
2439
|
+
if (!query) {
|
|
2440
|
+
const q2 = (await ask(' Search: ')).trim();
|
|
2441
|
+
if (!q2) return { next: 'main' };
|
|
2442
|
+
args.push(q2);
|
|
2443
|
+
}
|
|
2444
|
+
const { searchSessions, buildSessionIndex } = await import('../src/session.mjs');
|
|
2445
|
+
try { buildSessionIndex(cwd); } catch {}
|
|
2446
|
+
const results = searchSessions(args.join(' '), cwd);
|
|
2447
|
+
if (results.length === 0) {
|
|
2448
|
+
process.stdout.write(`\n No sessions matching "${args.join(' ')}"\n\n`);
|
|
2449
|
+
await ask(' Press Enter to continue...');
|
|
2450
|
+
return { next: 'main' };
|
|
2451
|
+
}
|
|
2452
|
+
process.stdout.write(`\n Found ${results.length} session${results.length === 1 ? '' : 's'}:\n`);
|
|
2453
|
+
results.slice(0, 9).forEach((sess, i) => {
|
|
2454
|
+
const tool = sess.tool === 'codex' ? 'cdx' : 'cld';
|
|
2455
|
+
const date = sess.date ? new Date(sess.date).toLocaleDateString() : '?';
|
|
2456
|
+
process.stdout.write(` [${i + 1}] ${tool} ${date} ${sess.prompts.first || sess.id.slice(0, 8)}\n`);
|
|
2457
|
+
});
|
|
2458
|
+
process.stdout.write('\n');
|
|
2459
|
+
const pick = (await ask(' Enter number to resume (or Enter to cancel): ')).trim();
|
|
2460
|
+
const num = parseInt(pick, 10);
|
|
2461
|
+
if (!isNaN(num) && num >= 1 && num <= Math.min(results.length, 9)) {
|
|
2462
|
+
const sess = results[num - 1];
|
|
2463
|
+
const { spawnSync: sp2 } = await import('node:child_process');
|
|
2464
|
+
const tool = sess.tool === 'codex' ? 'codex' : 'claude';
|
|
2465
|
+
process.stdout.write(`\n Launching: ${tool} --resume ${sess.id}\n\n`);
|
|
2466
|
+
sp2(tool, ['--resume', sess.id], { stdio: 'inherit' });
|
|
2467
|
+
}
|
|
2468
|
+
return { next: 'main' };
|
|
2469
|
+
}
|
|
2470
|
+
if (cmd === 'budget') {
|
|
2471
|
+
await cmdStatus([]);
|
|
2472
|
+
await ask('\n Press Enter to continue...');
|
|
2473
|
+
return { next: 'main' };
|
|
2474
|
+
}
|
|
2475
|
+
if (cmd === 'init --replit') {
|
|
2476
|
+
await cmdInit(rl);
|
|
2477
|
+
return { next: 'main' };
|
|
2478
|
+
}
|
|
2479
|
+
// fallthrough: unknown free command → treat as full task
|
|
2211
2480
|
}
|
|
2212
|
-
|
|
2481
|
+
|
|
2482
|
+
// Tier 2: CHEAP — question/diagnostic, route to haiku
|
|
2483
|
+
if (classified.tier === 'cheap') {
|
|
2484
|
+
process.stdout.write(`\n Routing to haiku for quick answer...\n`);
|
|
2485
|
+
return { next: 'go', prompt: input, model: 'haiku' };
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// Tier 3: FULL — work task, confirm before dispatching
|
|
2489
|
+
if (classified.tier === 'full') {
|
|
2490
|
+
const summary = input.length > 60 ? input.slice(0, 57) + '...' : input;
|
|
2491
|
+
process.stdout.write(`\n Launch coding session: ${summary}\n`);
|
|
2492
|
+
process.stdout.write(` Model: sonnet [Enter] to proceed, [n] to cancel\n\n`);
|
|
2493
|
+
const confirm = (await ask(' > ')).trim().toLowerCase();
|
|
2494
|
+
if (confirm === 'n' || confirm === 'no') return { next: 'main' };
|
|
2495
|
+
return { next: 'go', prompt: input };
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// Default fallback
|
|
2499
|
+
return { next: 'go', prompt: input };
|
|
2213
2500
|
}
|
|
2214
2501
|
|
|
2502
|
+
// ── Single-key shortcuts ───────────────────────────────────────────────────
|
|
2503
|
+
|
|
2215
2504
|
// Enter (empty) → resume most recent session
|
|
2216
2505
|
if (raw === '' || choice === '\r') {
|
|
2217
2506
|
if (recentSessions.length === 0) {
|
|
@@ -2225,7 +2514,7 @@ async function mainScreen(rl, ask) {
|
|
|
2225
2514
|
return { next: 'main' };
|
|
2226
2515
|
}
|
|
2227
2516
|
|
|
2228
|
-
// Number 1-
|
|
2517
|
+
// Number 1-9 → resume that session
|
|
2229
2518
|
const numChoice = parseInt(raw, 10);
|
|
2230
2519
|
if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
|
|
2231
2520
|
const sess = recentSessions[numChoice - 1];
|
|
@@ -2245,6 +2534,8 @@ async function mainScreen(rl, ask) {
|
|
|
2245
2534
|
}
|
|
2246
2535
|
|
|
2247
2536
|
if (choice === 'n') { return { next: 'new-session' }; }
|
|
2537
|
+
if (choice === '?' || choice === 'h') { return { next: 'palette-help' }; }
|
|
2538
|
+
if (choice === 'd') { return { next: 'diagnostics' }; }
|
|
2248
2539
|
|
|
2249
2540
|
if (choice === '/') {
|
|
2250
2541
|
const query = (await ask(' Search: ')).trim();
|
|
@@ -2303,6 +2594,48 @@ async function newSessionScreen(rl, ask) {
|
|
|
2303
2594
|
return { next: 'main' };
|
|
2304
2595
|
}
|
|
2305
2596
|
|
|
2597
|
+
// ─── Screen: paletteHelpScreen ───────────────────────────────────────────────
|
|
2598
|
+
|
|
2599
|
+
async function paletteHelpScreen(rl, ask) {
|
|
2600
|
+
const termW = process.stdout.columns || 60;
|
|
2601
|
+
const boxW = Math.min(termW - 2, 60);
|
|
2602
|
+
const W = boxW - 4;
|
|
2603
|
+
const top = `┌${'─'.repeat(boxW - 2)}┐`;
|
|
2604
|
+
const sep = `├${'─'.repeat(boxW - 2)}┤`;
|
|
2605
|
+
const bot = `└${'─'.repeat(boxW - 2)}┘`;
|
|
2606
|
+
const row = (content) => makeBoxRow(content, W);
|
|
2607
|
+
const DIM = '\x1b[2m';
|
|
2608
|
+
const RESET = '\x1b[0m';
|
|
2609
|
+
|
|
2610
|
+
const lines = [
|
|
2611
|
+
top,
|
|
2612
|
+
row('Command Palette'),
|
|
2613
|
+
sep,
|
|
2614
|
+
row(`${DIM}resume r${RESET} Resume last session`),
|
|
2615
|
+
row(`${DIM}status${RESET} Provider health + budget`),
|
|
2616
|
+
row(`${DIM}sessions ss${RESET} List recent sessions`),
|
|
2617
|
+
row(`${DIM}search <query>${RESET} Search session history`),
|
|
2618
|
+
row(`${DIM}budget b${RESET} Token usage + routing`),
|
|
2619
|
+
row(`${DIM}health h${RESET} System health check`),
|
|
2620
|
+
row(`${DIM}doctor d${RESET} Repo diagnostics`),
|
|
2621
|
+
row(`${DIM}settings s${RESET} Settings screen`),
|
|
2622
|
+
row(`${DIM}team t${RESET} Team screen`),
|
|
2623
|
+
row(`${DIM}help ?${RESET} Show this help`),
|
|
2624
|
+
row(`${DIM}quit q${RESET} Exit`),
|
|
2625
|
+
sep,
|
|
2626
|
+
row('Or type any task to launch a coding session'),
|
|
2627
|
+
row(`${DIM}Questions (why/how/what) → haiku (cheap)${RESET}`),
|
|
2628
|
+
row(`${DIM}Work tasks → confirm then dispatch${RESET}`),
|
|
2629
|
+
sep,
|
|
2630
|
+
row(`${DIM}[Enter] go back${RESET}`),
|
|
2631
|
+
bot,
|
|
2632
|
+
];
|
|
2633
|
+
|
|
2634
|
+
process.stdout.write('\n' + lines.join('\n') + '\n\n');
|
|
2635
|
+
await ask('');
|
|
2636
|
+
return { next: 'main' };
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2306
2639
|
// ─── Screen: importPickerScreen ──────────────────────────────────────────────
|
|
2307
2640
|
|
|
2308
2641
|
async function importPickerScreen() {
|
|
@@ -2766,15 +3099,14 @@ async function prTriageScreen(rl, ask, ctx = {}) {
|
|
|
2766
3099
|
async function settingsScreen(rl, ask) {
|
|
2767
3100
|
const cwd = process.cwd();
|
|
2768
3101
|
|
|
2769
|
-
|
|
2770
|
-
const
|
|
2771
|
-
const
|
|
2772
|
-
const
|
|
3102
|
+
const DIM = '\x1b[2m';
|
|
3103
|
+
const RESET = '\x1b[0m';
|
|
3104
|
+
const GREEN = '\x1b[32m';
|
|
3105
|
+
const RED = '\x1b[31m';
|
|
3106
|
+
const BOLD = '\x1b[1m';
|
|
2773
3107
|
|
|
2774
|
-
const
|
|
2775
|
-
const
|
|
2776
|
-
const bot = `└${'─'.repeat(boxW - 2)}┘`;
|
|
2777
|
-
const row = (content) => makeBoxRow(content, W);
|
|
3108
|
+
const chk = `${GREEN}✓${RESET}`;
|
|
3109
|
+
const xmark = `${RED}✗${RESET}`;
|
|
2778
3110
|
|
|
2779
3111
|
// Detect if gh is available + has PRs for the PR triage option
|
|
2780
3112
|
const settingsPRs = await detectOpenPRs(cwd);
|
|
@@ -2782,108 +3114,131 @@ async function settingsScreen(rl, ask) {
|
|
|
2782
3114
|
// Load current work style
|
|
2783
3115
|
const profile = loadProfile(cwd);
|
|
2784
3116
|
const currentBias = profile?.bias || profile?.mode || 'balanced';
|
|
2785
|
-
const WORK_STYLE_DISPLAY = {
|
|
2786
|
-
'cost-saver': '⚡ Fast',
|
|
2787
|
-
'auto': '⚡ Fast',
|
|
2788
|
-
'solo-claude': '⚡ Fast',
|
|
2789
|
-
'solo-openai': '⚡ Fast',
|
|
2790
|
-
'balanced': '⚖️ Balanced',
|
|
2791
|
-
'quality-first': '🔥 Full Power',
|
|
2792
|
-
};
|
|
2793
3117
|
|
|
2794
3118
|
// Work style current markers
|
|
2795
3119
|
const _stIsFast = ['cost-saver', 'auto', 'solo-claude', 'solo-openai'].includes(currentBias);
|
|
2796
3120
|
const _stIsBal = currentBias === 'balanced';
|
|
2797
3121
|
const _stIsFull = currentBias === 'quality-first';
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
//
|
|
2801
|
-
const
|
|
2802
|
-
const
|
|
2803
|
-
const
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3122
|
+
const dot = (active) => active ? `${GREEN}●${RESET}` : `${DIM}○${RESET}`;
|
|
3123
|
+
|
|
3124
|
+
// ── Subscriptions / credentials ──────────────────────────────────────────
|
|
3125
|
+
const credData = loadCredentials(cwd);
|
|
3126
|
+
const credList = credData.credentials || [];
|
|
3127
|
+
const hasCredRegistry = credList.length > 0;
|
|
3128
|
+
|
|
3129
|
+
// Fall back to detectAuth() when no registry entries yet
|
|
3130
|
+
let subsLines = [];
|
|
3131
|
+
if (hasCredRegistry) {
|
|
3132
|
+
for (const c of credList.filter(c => c.enabled !== false)) {
|
|
3133
|
+
const provLabel = c.provider === 'claude' ? 'Claude' : 'OpenAI';
|
|
3134
|
+
const authLabel = c.auth_type === 'cli_oauth' ? 'CLI OAuth' : 'API key';
|
|
3135
|
+
const planLabel = c.plan_hint || '';
|
|
3136
|
+
const healthMark = c.health === 'healthy' ? chk : c.health === 'degraded' ? `${RED}~${RESET}` : `${DIM}?${RESET}`;
|
|
3137
|
+
const scopeTag = `[${c.scope || 'local'}]`;
|
|
3138
|
+
const planPart = planLabel ? ` ${DIM}${planLabel}${RESET}` : '';
|
|
3139
|
+
subsLines.push(` ${DIM}${provLabel.padEnd(6)}${RESET} ${authLabel.padEnd(10)}${planPart} ${healthMark}${c.health === 'healthy' ? ' healthy' : ' ' + (c.health || 'unknown')} ${DIM}${scopeTag}${RESET}`);
|
|
3140
|
+
}
|
|
3141
|
+
if (subsLines.length === 0) subsLines.push(` ${DIM}none registered${RESET}`);
|
|
3142
|
+
} else {
|
|
3143
|
+
const _stAuth = await detectAuth();
|
|
3144
|
+
const _clStatus = _stAuth.claude.found ? `${chk} connected` : `${xmark} not connected`;
|
|
3145
|
+
const _oaStatus = _stAuth.openai.found ? `${chk} connected` : `${xmark} not connected`;
|
|
3146
|
+
subsLines.push(` ${DIM}Claude${RESET} CLI OAuth ${_clStatus}`);
|
|
3147
|
+
subsLines.push(` ${DIM}OpenAI${RESET} API key ${_oaStatus}`);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// ── Work style ───────────────────────────────────────────────────────────
|
|
3151
|
+
const wsLines = [
|
|
3152
|
+
` ${dot(_stIsFast)} ${_stIsFast ? BOLD : DIM}Fast${RESET} speed over caution`,
|
|
3153
|
+
` ${dot(_stIsBal)} ${_stIsBal ? BOLD : DIM}Balanced${RESET} smart routing, reviews on important`,
|
|
3154
|
+
` ${dot(_stIsFull)} ${_stIsFull ? BOLD : DIM}Full Power${RESET} dual-brain everything, max quality`,
|
|
3155
|
+
];
|
|
3156
|
+
|
|
3157
|
+
// ── System info ──────────────────────────────────────────────────────────
|
|
3158
|
+
const rt = detectReplitTools(cwd);
|
|
3159
|
+
const rtLabel = rt.installed ? `v${rt.version || '?'}` : 'not installed';
|
|
3160
|
+
const rtMark = rt.installed ? chk : xmark;
|
|
3161
|
+
|
|
3162
|
+
let sessionCount = 0;
|
|
2813
3163
|
try {
|
|
2814
|
-
const
|
|
2815
|
-
const
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
const _stAd = _stCm.getAdaptation(_stCal);
|
|
2819
|
-
_stLevel = _stAd.userLevel;
|
|
2820
|
-
_stStyle = _stAd.responseStyle;
|
|
2821
|
-
} catch { /* non-fatal */ }
|
|
3164
|
+
const idxPath = join(cwd, '.dualbrain', 'session-index.json');
|
|
3165
|
+
const idx = existsSync(idxPath) ? JSON.parse(readFileSync(idxPath, 'utf8')) : {};
|
|
3166
|
+
sessionCount = Object.keys(idx).length;
|
|
3167
|
+
} catch { /* ignore */ }
|
|
2822
3168
|
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3169
|
+
let pluginCount = 0;
|
|
3170
|
+
try {
|
|
3171
|
+
const settingsJson = join(cwd, '.claude', 'settings.json');
|
|
3172
|
+
if (existsSync(settingsJson)) {
|
|
3173
|
+
const s = JSON.parse(readFileSync(settingsJson, 'utf8'));
|
|
3174
|
+
pluginCount = Object.keys(s?.mcpServers || {}).length;
|
|
3175
|
+
}
|
|
3176
|
+
} catch { /* ignore */ }
|
|
2826
3177
|
|
|
2827
|
-
|
|
2828
|
-
let _stEffScore = null;
|
|
2829
|
-
let _stEffRate = null;
|
|
2830
|
-
let _stEffTrend = null;
|
|
2831
|
-
let _stEffTier = null;
|
|
3178
|
+
let doctorStr = `${DIM}not run${RESET}`;
|
|
2832
3179
|
try {
|
|
2833
|
-
const
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
const
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3180
|
+
const hooksDir = join(cwd, '.claude', 'hooks');
|
|
3181
|
+
const headGuard = existsSync(join(hooksDir, 'head-guard.mjs'));
|
|
3182
|
+
const enforceTier = existsSync(join(hooksDir, 'enforce-tier.mjs'));
|
|
3183
|
+
const settingsFile = join(cwd, '.claude', 'settings.json');
|
|
3184
|
+
let guardCount = 0;
|
|
3185
|
+
if (existsSync(settingsFile)) {
|
|
3186
|
+
const s = JSON.parse(readFileSync(settingsFile, 'utf8'));
|
|
3187
|
+
const ptu = s?.hooks?.PreToolUse ?? [];
|
|
3188
|
+
const gCmd = 'node .claude/hooks/head-guard.mjs';
|
|
3189
|
+
const tCmd = 'node .claude/hooks/enforce-tier.mjs';
|
|
3190
|
+
guardCount = [
|
|
3191
|
+
ptu.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === gCmd)),
|
|
3192
|
+
ptu.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === gCmd)),
|
|
3193
|
+
ptu.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === gCmd)),
|
|
3194
|
+
ptu.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tCmd)),
|
|
3195
|
+
].filter(Boolean).length;
|
|
2845
3196
|
}
|
|
2846
|
-
|
|
3197
|
+
const checks = [headGuard, enforceTier, guardCount >= 4].filter(Boolean).length + 7; // base 7 always pass
|
|
3198
|
+
const total = 10;
|
|
3199
|
+
doctorStr = checks >= total
|
|
3200
|
+
? `${chk} ${checks}/${total} checks passing`
|
|
3201
|
+
: `${RED}${checks}/${total} checks passing${RESET}`;
|
|
3202
|
+
} catch { /* ignore */ }
|
|
2847
3203
|
|
|
2848
|
-
const
|
|
3204
|
+
const sysLines = [
|
|
3205
|
+
` ${DIM}replit-tools${RESET} ${rtLabel} ${rtMark} ${rt.installed ? 'connected' : 'not connected'}`,
|
|
3206
|
+
` ${DIM}Sessions${RESET} ${sessionCount} archived`,
|
|
3207
|
+
` ${DIM}Plugins${RESET} ${pluginCount} configured`,
|
|
3208
|
+
` ${DIM}Doctor${RESET} ${doctorStr}`,
|
|
3209
|
+
];
|
|
2849
3210
|
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
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'),
|
|
2875
|
-
...(settingsPRs.length > 0 ? [row(`[p] PR triage (${settingsPRs.length} open)`)] : []),
|
|
2876
|
-
bot,
|
|
3211
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
3212
|
+
const out = [
|
|
3213
|
+
'',
|
|
3214
|
+
` ${BOLD}Settings${RESET}`,
|
|
3215
|
+
'',
|
|
3216
|
+
` ${DIM}Subscriptions${RESET}`,
|
|
3217
|
+
...subsLines,
|
|
3218
|
+
` ${DIM}[a] add [r] remove [h] health check${RESET}`,
|
|
3219
|
+
'',
|
|
3220
|
+
` ${DIM}Work style${RESET}`,
|
|
3221
|
+
...wsLines,
|
|
3222
|
+
` ${DIM}[1-3] change${RESET}`,
|
|
3223
|
+
'',
|
|
3224
|
+
` ${DIM}System${RESET}`,
|
|
3225
|
+
...sysLines,
|
|
3226
|
+
` ${DIM}[d] run doctor [x] diagnostics${RESET}`,
|
|
3227
|
+
'',
|
|
3228
|
+
` ${DIM}[e] sessions [m] subscriptions [b] back${RESET}`,
|
|
3229
|
+
...(settingsPRs.length > 0 ? [` ${DIM}[p] PR triage (${settingsPRs.length} open)${RESET}`] : []),
|
|
3230
|
+
'',
|
|
2877
3231
|
];
|
|
2878
|
-
process.stdout.write(
|
|
3232
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
2879
3233
|
|
|
2880
3234
|
const raw = (await ask(' Choice: ')).trim();
|
|
2881
3235
|
const choice = raw.toLowerCase();
|
|
2882
3236
|
|
|
2883
|
-
//
|
|
3237
|
+
// Work style 1/2/3
|
|
2884
3238
|
if (choice === '1' || choice === '2' || choice === '3') {
|
|
2885
|
-
const
|
|
2886
|
-
const
|
|
3239
|
+
const wsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
3240
|
+
const wsDisp = { '1': 'Fast', '2': 'Balanced', '3': 'Full Power' };
|
|
3241
|
+
const newBias = wsMap[choice];
|
|
2887
3242
|
if (newBias && newBias !== currentBias) {
|
|
2888
3243
|
profile.bias = newBias;
|
|
2889
3244
|
const enabledCount = [
|
|
@@ -2892,71 +3247,91 @@ async function settingsScreen(rl, ask) {
|
|
|
2892
3247
|
].filter(Boolean).length;
|
|
2893
3248
|
if (enabledCount >= 2) profile.mode = newBias;
|
|
2894
3249
|
saveProfile(profile, { cwd });
|
|
2895
|
-
|
|
2896
|
-
process.stdout.write(`\n Work style set to ${newLabel}\n\n`);
|
|
3250
|
+
process.stdout.write(`\n Work style set to ${wsDisp[choice]}\n\n`);
|
|
2897
3251
|
await ask(' Press Enter to continue...');
|
|
2898
3252
|
}
|
|
2899
3253
|
return { next: 'settings' };
|
|
2900
3254
|
}
|
|
2901
3255
|
|
|
2902
|
-
//
|
|
2903
|
-
if (choice === '
|
|
3256
|
+
// Add credential
|
|
3257
|
+
if (choice === 'a') {
|
|
3258
|
+
process.stdout.write('\n Auto-detecting credentials...\n');
|
|
2904
3259
|
try {
|
|
2905
|
-
const
|
|
2906
|
-
|
|
2907
|
-
|
|
3260
|
+
const discovered = await detectCredentials(cwd);
|
|
3261
|
+
const existing = loadCredentials(cwd).credentials.map(c => c.id);
|
|
3262
|
+
const newOnes = discovered.filter(c => !existing.includes(c.id));
|
|
3263
|
+
if (newOnes.length === 0) {
|
|
3264
|
+
process.stdout.write(' No new credentials detected.\n\n');
|
|
3265
|
+
} else {
|
|
3266
|
+
for (const c of newOnes) {
|
|
3267
|
+
addCredential(c, cwd);
|
|
3268
|
+
process.stdout.write(` Added: ${c.id} (${c.provider} / ${c.auth_type})\n`);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
} catch (e) {
|
|
3272
|
+
process.stdout.write(` Detection failed: ${e.message}\n`);
|
|
3273
|
+
}
|
|
3274
|
+
await ask(' Press Enter to continue...');
|
|
3275
|
+
return { next: 'settings' };
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// Remove credential
|
|
3279
|
+
if (choice === 'r') {
|
|
3280
|
+
const creds = loadCredentials(cwd).credentials;
|
|
3281
|
+
if (creds.length === 0) {
|
|
3282
|
+
process.stdout.write('\n No credentials registered.\n\n');
|
|
2908
3283
|
await ask(' Press Enter to continue...');
|
|
2909
|
-
|
|
3284
|
+
return { next: 'settings' };
|
|
3285
|
+
}
|
|
3286
|
+
process.stdout.write('\n');
|
|
3287
|
+
creds.forEach((c, i) => process.stdout.write(` [${i + 1}] ${c.id} (${c.provider})\n`));
|
|
3288
|
+
const pick = (await ask('\n Number to remove (or Enter to cancel): ')).trim();
|
|
3289
|
+
const idx = parseInt(pick, 10) - 1;
|
|
3290
|
+
if (idx >= 0 && idx < creds.length) {
|
|
3291
|
+
removeCredential(creds[idx].id, cwd);
|
|
3292
|
+
process.stdout.write(` Removed ${creds[idx].id}\n\n`);
|
|
3293
|
+
}
|
|
3294
|
+
await ask(' Press Enter to continue...');
|
|
2910
3295
|
return { next: 'settings' };
|
|
2911
3296
|
}
|
|
2912
3297
|
|
|
2913
|
-
|
|
3298
|
+
// Health check credentials
|
|
3299
|
+
if (choice === 'h') {
|
|
3300
|
+
process.stdout.write('\n Checking credential health...\n');
|
|
3301
|
+
try {
|
|
3302
|
+
const data = loadCredentials(cwd);
|
|
3303
|
+
const creds = data.credentials || [];
|
|
3304
|
+
if (creds.length === 0) {
|
|
3305
|
+
process.stdout.write(' No credentials to check.\n');
|
|
3306
|
+
} else {
|
|
3307
|
+
const updated = [];
|
|
3308
|
+
for (const c of creds) {
|
|
3309
|
+
const checked = await checkCredentialHealth(c, cwd);
|
|
3310
|
+
const mark = checked.health === 'healthy' ? chk : xmark;
|
|
3311
|
+
process.stdout.write(` ${mark} ${c.id}: ${checked.health}\n`);
|
|
3312
|
+
updated.push(checked);
|
|
3313
|
+
}
|
|
3314
|
+
saveCredentials({ ...data, credentials: updated }, cwd);
|
|
3315
|
+
}
|
|
3316
|
+
} catch (e) {
|
|
3317
|
+
process.stdout.write(` Health check failed: ${e.message}\n`);
|
|
3318
|
+
}
|
|
3319
|
+
await ask('\n Press Enter to continue...');
|
|
3320
|
+
return { next: 'settings' };
|
|
3321
|
+
}
|
|
2914
3322
|
|
|
3323
|
+
if (choice === 'm') { return { next: 'subscriptions' }; }
|
|
2915
3324
|
if (choice === 'e') { return { next: 'sessions' }; }
|
|
2916
|
-
|
|
2917
|
-
if (choice === 'i') {
|
|
2918
|
-
return { next: 'import-picker' };
|
|
2919
|
-
}
|
|
3325
|
+
if (choice === 'x') { return { next: 'diagnostics' }; }
|
|
2920
3326
|
|
|
2921
3327
|
if (choice === 'p' && settingsPRs.length > 0) {
|
|
2922
3328
|
return { next: 'pr-triage', openPRs: settingsPRs };
|
|
2923
3329
|
}
|
|
2924
3330
|
|
|
2925
3331
|
if (choice === 'd') {
|
|
2926
|
-
|
|
2927
|
-
const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
|
|
2928
|
-
if (which.status === 0) {
|
|
2929
|
-
spawnSync('claude-menu', { stdio: 'inherit' });
|
|
2930
|
-
} else {
|
|
2931
|
-
process.stdout.write('\n replit-tools not found — install with: npm i -g replit-tools\n\n');
|
|
2932
|
-
await ask(' Press Enter to continue...');
|
|
2933
|
-
}
|
|
2934
|
-
return { next: 'settings' };
|
|
2935
|
-
}
|
|
2936
|
-
|
|
2937
|
-
if (choice === '?') {
|
|
2938
|
-
const W2 = 37;
|
|
2939
|
-
const helpTop = ` ┌${'─'.repeat(W2)}┐`;
|
|
2940
|
-
const helpSep = ` ├${'─'.repeat(W2)}┤`;
|
|
2941
|
-
const helpBottom = ` └${'─'.repeat(W2)}┘`;
|
|
2942
|
-
const helpPad = (s) => s + ' '.repeat(Math.max(0, W2 - s.length));
|
|
2943
|
-
process.stdout.write('\n');
|
|
2944
|
-
process.stdout.write(helpTop + '\n');
|
|
2945
|
-
process.stdout.write(` │ ${helpPad('At ~/workspace$ prompt:')}│\n`);
|
|
2946
|
-
process.stdout.write(` │ ${helpPad('db = show this menu')}│\n`);
|
|
2947
|
-
process.stdout.write(` │ ${helpPad('j = login to claude')}│\n`);
|
|
2948
|
-
process.stdout.write(` │ ${helpPad('k = login to codex')}│\n`);
|
|
2949
|
-
process.stdout.write(helpSep + '\n');
|
|
2950
|
-
process.stdout.write(` │ ${helpPad('In Claude:')}│\n`);
|
|
2951
|
-
process.stdout.write(` │ ${helpPad('Ctrl+C x2 = back to menu')}│\n`);
|
|
2952
|
-
process.stdout.write(` │ ${helpPad('Ctrl+C x3 = exit to shell')}│\n`);
|
|
2953
|
-
process.stdout.write(helpBottom + '\n\n');
|
|
2954
|
-
await ask(' Press Enter to continue...');
|
|
2955
|
-
return { next: 'settings' };
|
|
3332
|
+
return { next: 'diagnostics' };
|
|
2956
3333
|
}
|
|
2957
3334
|
|
|
2958
|
-
if (choice === 'x') { return { next: 'diagnostics' }; }
|
|
2959
|
-
|
|
2960
3335
|
if (choice === 'b' || choice === 'back' || raw === '\x1b') { return { next: 'main' }; }
|
|
2961
3336
|
|
|
2962
3337
|
return { next: 'main' };
|
|
@@ -3245,9 +3620,37 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
3245
3620
|
// ─── Onboarding Wizard ───────────────────────────────────────────────────────
|
|
3246
3621
|
|
|
3247
3622
|
/**
|
|
3248
|
-
*
|
|
3249
|
-
*
|
|
3250
|
-
|
|
3623
|
+
* Write .dualbrain/credentials.json with detected providers.
|
|
3624
|
+
* Non-destructive: never overwrites entries with the same id.
|
|
3625
|
+
*/
|
|
3626
|
+
function saveWizardCredentials(cwd, detectedProviders) {
|
|
3627
|
+
const dir = join(cwd, '.dualbrain');
|
|
3628
|
+
try { mkdirSync(dir, { recursive: true }); } catch { /* exists */ }
|
|
3629
|
+
|
|
3630
|
+
const credPath = join(dir, 'credentials.json');
|
|
3631
|
+
let existing = { version: 1, credentials: [] };
|
|
3632
|
+
try {
|
|
3633
|
+
const raw = readFileSync(credPath, 'utf8');
|
|
3634
|
+
existing = JSON.parse(raw);
|
|
3635
|
+
if (!Array.isArray(existing.credentials)) existing.credentials = [];
|
|
3636
|
+
} catch { /* fresh start */ }
|
|
3637
|
+
|
|
3638
|
+
const existingIds = new Set(existing.credentials.map(c => c.id));
|
|
3639
|
+
const now = new Date().toISOString();
|
|
3640
|
+
|
|
3641
|
+
for (const cred of detectedProviders) {
|
|
3642
|
+
if (!existingIds.has(cred.id)) {
|
|
3643
|
+
existing.credentials.push({ ...cred, last_checked_at: now });
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
writeFileSync(credPath, JSON.stringify(existing, null, 2), 'utf8');
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
/**
|
|
3651
|
+
* Animated first-run setup wizard — detection-first, 3-interaction flow.
|
|
3652
|
+
* Detection IS the home screen loading: scan → confirm providers → pick style → done.
|
|
3653
|
+
* Uses src/fx.mjs; falls back to plain output stubs.
|
|
3251
3654
|
*
|
|
3252
3655
|
* @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
|
|
3253
3656
|
* @param {string} cwd
|
|
@@ -3255,192 +3658,310 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
3255
3658
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
3256
3659
|
*/
|
|
3257
3660
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
3258
|
-
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
3259
3661
|
const fx = await getFx();
|
|
3662
|
+
const cl = fx.colors || {};
|
|
3663
|
+
const DIM = cl.dim || '';
|
|
3664
|
+
const BOLD = cl.bold || '';
|
|
3665
|
+
const GREEN = cl.green || '';
|
|
3666
|
+
const CYAN = cl.cyan || '';
|
|
3667
|
+
const GRAY = cl.gray || '';
|
|
3668
|
+
const RST = cl.reset || '';
|
|
3260
3669
|
|
|
3261
|
-
|
|
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);
|
|
3670
|
+
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
3268
3671
|
|
|
3269
|
-
//
|
|
3270
|
-
|
|
3271
|
-
|
|
3672
|
+
// Helper: print a single dim line (indented with one space)
|
|
3673
|
+
function dimLine(text) {
|
|
3674
|
+
process.stdout.write(` ${GRAY}${text}${RST}\n`);
|
|
3675
|
+
}
|
|
3272
3676
|
|
|
3273
|
-
//
|
|
3677
|
+
// Helper: single-key prompt; falls back to readline if not a real TTY
|
|
3678
|
+
async function singleKey(validKeys) {
|
|
3679
|
+
if (!isTTY) {
|
|
3680
|
+
const line = await new Promise(res => rl.question('', res));
|
|
3681
|
+
return (line.trim().toLowerCase()[0]) || '\r';
|
|
3682
|
+
}
|
|
3683
|
+
const { emitKeypressEvents } = await import('node:readline');
|
|
3684
|
+
emitKeypressEvents(process.stdin, rl);
|
|
3685
|
+
return new Promise((resolve) => {
|
|
3686
|
+
const wasRaw = process.stdin.isRaw;
|
|
3687
|
+
process.stdin.setRawMode(true);
|
|
3688
|
+
const cleanup = () => {
|
|
3689
|
+
process.stdin.removeListener('keypress', onKey);
|
|
3690
|
+
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
3691
|
+
};
|
|
3692
|
+
const onKey = (str, key) => {
|
|
3693
|
+
if (!key) return;
|
|
3694
|
+
const name = key.name || '';
|
|
3695
|
+
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
3696
|
+
cleanup(); process.stdout.write('\n'); resolve('q'); return;
|
|
3697
|
+
}
|
|
3698
|
+
const ch = (str || '').toLowerCase();
|
|
3699
|
+
if (name === 'return' || name === 'enter') {
|
|
3700
|
+
cleanup(); process.stdout.write('\n'); resolve('\r'); return;
|
|
3701
|
+
}
|
|
3702
|
+
if (validKeys.includes(ch)) {
|
|
3703
|
+
cleanup(); process.stdout.write(`${ch}\n`); resolve(ch); return;
|
|
3704
|
+
}
|
|
3705
|
+
};
|
|
3706
|
+
process.stdin.on('keypress', onKey);
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
// ─── Clear screen + header ─────────────────────────────────────────────────
|
|
3711
|
+
const version = readVersion();
|
|
3712
|
+
fx.clearScreen();
|
|
3713
|
+
process.stdout.write(`\n ${BOLD}dual-brain${RST}${GRAY} v${version}${RST}\n\n`);
|
|
3714
|
+
process.stdout.write(` ${DIM}Setting up your workspace...${RST}\n\n`);
|
|
3715
|
+
|
|
3716
|
+
// ─── Env scan — run detection in parallel with animated output ────────────
|
|
3274
3717
|
const capsPromise = detectCapabilities(cwd);
|
|
3275
3718
|
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3719
|
+
// Replit workspace
|
|
3720
|
+
const isReplit = !!(process.env.REPL_ID || process.env.REPL_SLUG);
|
|
3721
|
+
if (isReplit) {
|
|
3722
|
+
await fx.sleep(150);
|
|
3723
|
+
fx.success('Replit workspace detected');
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
// Node version
|
|
3727
|
+
try {
|
|
3728
|
+
const major = process.version.replace(/^v/, '').split('.')[0];
|
|
3729
|
+
await fx.sleep(100);
|
|
3730
|
+
fx.success(`Node ${major}.x found`);
|
|
3731
|
+
} catch { /* non-fatal */ }
|
|
3732
|
+
|
|
3733
|
+
// Git repo name, branch, file count
|
|
3734
|
+
let repoName = null;
|
|
3735
|
+
let branchName = null;
|
|
3736
|
+
let fileCount = 0;
|
|
3737
|
+
try {
|
|
3738
|
+
const { spawnSync: sp } = await import('node:child_process');
|
|
3739
|
+
const topLevel = sp('git', ['rev-parse', '--show-toplevel'], {
|
|
3740
|
+
cwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 3000,
|
|
3741
|
+
});
|
|
3742
|
+
if (topLevel.status === 0) repoName = basename((topLevel.stdout || '').trim());
|
|
3743
|
+
|
|
3744
|
+
const branch = sp('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
3745
|
+
cwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 2000,
|
|
3746
|
+
});
|
|
3747
|
+
branchName = (branch.stdout || '').trim() || null;
|
|
3748
|
+
|
|
3749
|
+
const count = sp('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
|
|
3750
|
+
cwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 3000,
|
|
3751
|
+
});
|
|
3752
|
+
fileCount = (count.stdout || '').trim().split('\n').filter(Boolean).length;
|
|
3753
|
+
} catch { /* not a git repo or git unavailable */ }
|
|
3281
3754
|
|
|
3282
|
-
|
|
3283
|
-
|
|
3755
|
+
if (repoName) {
|
|
3756
|
+
const fileLabel = fileCount > 0 ? `, ${fileCount} file${fileCount === 1 ? '' : 's'}` : '';
|
|
3757
|
+
const branchLabel = branchName ? ` (${branchName} branch${fileLabel})` : '';
|
|
3758
|
+
await fx.sleep(100);
|
|
3759
|
+
fx.success(`Git repository: ${repoName}${branchLabel}`);
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// Provider spinner while awaiting detection
|
|
3763
|
+
const provSpinner = fx.spinner('Checking providers...').start();
|
|
3764
|
+
const caps = await capsPromise;
|
|
3284
3765
|
const claudeReady = caps.claude.available;
|
|
3285
3766
|
const openaiReady = caps.openai.available;
|
|
3286
3767
|
const codexAvailable = caps.codex.available;
|
|
3768
|
+
provSpinner.stop();
|
|
3287
3769
|
|
|
3288
|
-
//
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3770
|
+
// Claude
|
|
3771
|
+
let claudeAuthLabel = null;
|
|
3772
|
+
let claudeAuthType = null;
|
|
3773
|
+
if (claudeReady) {
|
|
3774
|
+
if (caps.claude.source === 'claude-code') {
|
|
3775
|
+
claudeAuthLabel = 'CLI OAuth'; claudeAuthType = 'cli_oauth';
|
|
3776
|
+
} else if (caps.claude.source === 'env-key') {
|
|
3777
|
+
claudeAuthLabel = 'API key'; claudeAuthType = 'api_key';
|
|
3778
|
+
} else {
|
|
3779
|
+
claudeAuthLabel = caps.claude.source || 'detected'; claudeAuthType = 'unknown';
|
|
3780
|
+
}
|
|
3781
|
+
fx.success(`Claude CLI found · ${claudeAuthLabel}`);
|
|
3782
|
+
}
|
|
3294
3783
|
|
|
3295
|
-
//
|
|
3296
|
-
|
|
3297
|
-
|
|
3784
|
+
// OpenAI / Codex
|
|
3785
|
+
let openaiAuthLabel = null;
|
|
3786
|
+
let openaiAuthType = null;
|
|
3787
|
+
if (openaiReady) {
|
|
3788
|
+
openaiAuthLabel = 'API key'; openaiAuthType = 'api_key';
|
|
3789
|
+
fx.success('OpenAI detected · API key');
|
|
3790
|
+
} else if (codexAvailable) {
|
|
3791
|
+
openaiAuthLabel = 'CLI OAuth'; openaiAuthType = 'cli_oauth';
|
|
3792
|
+
fx.success('OpenAI Codex CLI found · authenticated');
|
|
3793
|
+
}
|
|
3298
3794
|
|
|
3795
|
+
// replit-tools — auto-import sessions (non-destructive read-only indexing, no prompt)
|
|
3299
3796
|
const rt = detectReplitTools(cwd);
|
|
3300
|
-
const rtSpinner = fx.spinner('Looking for replit-tools...').start();
|
|
3301
|
-
await fx.sleep(700);
|
|
3302
|
-
|
|
3303
3797
|
let rtSessionCount = 0;
|
|
3304
3798
|
if (rt.installed) {
|
|
3305
|
-
const vStr = rt.version ? ` v${rt.version}` : '';
|
|
3306
|
-
rtSpinner.succeed(`replit-tools${vStr} detected`);
|
|
3307
|
-
// Count available sessions
|
|
3308
3799
|
try {
|
|
3309
3800
|
const sessions = importReplitSessions(cwd);
|
|
3310
3801
|
rtSessionCount = sessions.length;
|
|
3311
3802
|
} catch { /* non-fatal */ }
|
|
3312
|
-
|
|
3313
|
-
|
|
3803
|
+
if (rtSessionCount > 0) {
|
|
3804
|
+
fx.success(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} found in replit-tools`);
|
|
3805
|
+
}
|
|
3806
|
+
const vStr = rt.version ? `v${rt.version}` : 'installed';
|
|
3807
|
+
fx.success(`replit-tools ${vStr} detected`);
|
|
3314
3808
|
}
|
|
3315
|
-
fx.nl();
|
|
3316
|
-
|
|
3317
|
-
// ─── Step 4: Import conversations ─────────────────────────────────────────
|
|
3318
|
-
fx.step(3, 5, 'Import conversations');
|
|
3319
|
-
fx.nl();
|
|
3320
3809
|
|
|
3321
|
-
|
|
3322
|
-
fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
|
|
3323
|
-
fx.nl();
|
|
3810
|
+
process.stdout.write('\n');
|
|
3324
3811
|
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
const importChoice = (await ask('')).trim().toLowerCase();
|
|
3812
|
+
// ─── Step 1: Confirm providers ────────────────────────────────────────────
|
|
3813
|
+
const hasAnyProvider = claudeReady || openaiReady || codexAvailable;
|
|
3328
3814
|
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
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');
|
|
3345
|
-
}
|
|
3346
|
-
fx.nl();
|
|
3815
|
+
if (!hasAnyProvider) {
|
|
3816
|
+
// No-providers path
|
|
3817
|
+
process.stdout.write(` ${BOLD}No providers detected${RST}\n\n`);
|
|
3818
|
+
dimLine('dual-brain needs Claude or OpenAI to run coding tasks.');
|
|
3819
|
+
dimLine('You can still browse your project and configure settings.');
|
|
3820
|
+
process.stdout.write('\n');
|
|
3821
|
+
process.stdout.write(` ${GRAY}[c]${RST} set up Claude ${GRAY}[o]${RST} set up OpenAI ${GRAY}[s]${RST} skip for now\n\n`);
|
|
3347
3822
|
|
|
3348
|
-
|
|
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();
|
|
3823
|
+
const noProvChoice = await singleKey(['c', 'o', 's', '\r']);
|
|
3356
3824
|
|
|
3357
|
-
|
|
3358
|
-
|
|
3825
|
+
if (noProvChoice === 'c') {
|
|
3826
|
+
process.stdout.write('\n');
|
|
3827
|
+
dimLine('Run: claude login');
|
|
3828
|
+
dimLine('Then re-run: dual-brain init');
|
|
3829
|
+
process.stdout.write('\n');
|
|
3830
|
+
} else if (noProvChoice === 'o') {
|
|
3831
|
+
process.stdout.write('\n');
|
|
3832
|
+
dimLine('Run: codex login');
|
|
3833
|
+
dimLine('Or add OPENAI_API_KEY to Replit Secrets if using API key auth.');
|
|
3834
|
+
dimLine('Then re-run: dual-brain init');
|
|
3835
|
+
process.stdout.write('\n');
|
|
3836
|
+
}
|
|
3359
3837
|
|
|
3360
|
-
|
|
3361
|
-
|
|
3838
|
+
const minProfile = loadProfile(cwd);
|
|
3839
|
+
minProfile.setupComplete = true;
|
|
3840
|
+
minProfile.providers.claude = { enabled: false };
|
|
3841
|
+
minProfile.providers.openai = { enabled: false };
|
|
3842
|
+
minProfile.mode = 'solo-claude';
|
|
3843
|
+
minProfile.bias = 'balanced';
|
|
3844
|
+
minProfile.workStyle = 'balanced';
|
|
3845
|
+
return minProfile;
|
|
3846
|
+
}
|
|
3362
3847
|
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3848
|
+
// Show provider table
|
|
3849
|
+
process.stdout.write(` ${BOLD}Providers detected:${RST}\n\n`);
|
|
3850
|
+
if (claudeReady) {
|
|
3851
|
+
process.stdout.write(` ${GRAY}Claude${RST} ${claudeAuthLabel} ${GREEN}✓ authenticated${RST}\n`);
|
|
3852
|
+
}
|
|
3853
|
+
if (openaiReady) {
|
|
3854
|
+
process.stdout.write(` ${GRAY}OpenAI${RST} API key ${GREEN}✓ OPENAI_API_KEY${RST}\n`);
|
|
3855
|
+
} else if (codexAvailable) {
|
|
3856
|
+
process.stdout.write(` ${GRAY}OpenAI${RST} CLI OAuth ${GREEN}✓ authenticated${RST}\n`);
|
|
3857
|
+
}
|
|
3367
3858
|
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
const wasRaw = process.stdin.isRaw;
|
|
3371
|
-
process.stdin.setRawMode(true);
|
|
3859
|
+
process.stdout.write('\n');
|
|
3860
|
+
process.stdout.write(` ${GRAY}Correct?${RST} ${GRAY}[Enter]${RST} yes ${GRAY}[n]${RST} change ${GRAY}[a]${RST} add more\n\n`);
|
|
3372
3861
|
|
|
3373
|
-
|
|
3374
|
-
process.stdin.removeListener('keypress', onKey);
|
|
3375
|
-
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
3376
|
-
};
|
|
3862
|
+
const provChoice = await singleKey(['n', 'a', '\r', 'y']);
|
|
3377
3863
|
|
|
3378
|
-
|
|
3379
|
-
|
|
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
|
-
};
|
|
3864
|
+
let finalClaudeEnabled = claudeReady;
|
|
3865
|
+
let finalOpenaiEnabled = openaiReady || codexAvailable;
|
|
3400
3866
|
|
|
3401
|
-
|
|
3867
|
+
if (provChoice === 'n') {
|
|
3868
|
+
process.stdout.write('\n');
|
|
3869
|
+
const toggleOpts = [];
|
|
3870
|
+
if (claudeReady) toggleOpts.push(`${GRAY}[c]${RST} disable Claude`);
|
|
3871
|
+
if (openaiReady || codexAvailable) toggleOpts.push(`${GRAY}[o]${RST} disable OpenAI`);
|
|
3872
|
+
toggleOpts.push(`${GRAY}[Enter]${RST} keep`);
|
|
3873
|
+
process.stdout.write(` ${toggleOpts.join(' ')}\n\n`);
|
|
3874
|
+
const toggleChoice = await singleKey(['c', 'o', '\r']);
|
|
3875
|
+
if (toggleChoice === 'c') finalClaudeEnabled = false;
|
|
3876
|
+
if (toggleChoice === 'o') finalOpenaiEnabled = false;
|
|
3877
|
+
process.stdout.write('\n');
|
|
3878
|
+
} else if (provChoice === 'a') {
|
|
3879
|
+
process.stdout.write('\n');
|
|
3880
|
+
if (!claudeReady) dimLine('Claude: run `claude auth login` to authenticate');
|
|
3881
|
+
if (!openaiReady && !codexAvailable) dimLine('OpenAI: set OPENAI_API_KEY or run `codex login`');
|
|
3882
|
+
process.stdout.write('\n');
|
|
3883
|
+
process.stdout.write(` ${GRAY}[Enter]${RST} continue with current providers\n\n`);
|
|
3884
|
+
await singleKey(['\r', 'q']);
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
// Write credentials.json
|
|
3888
|
+
const credEntries = [];
|
|
3889
|
+
if (finalClaudeEnabled) {
|
|
3890
|
+
credEntries.push({
|
|
3891
|
+
id: 'claude-local',
|
|
3892
|
+
provider: 'claude',
|
|
3893
|
+
auth_type: claudeAuthType || 'cli_oauth',
|
|
3894
|
+
source: 'local_cli',
|
|
3895
|
+
owner: 'user',
|
|
3896
|
+
scope: 'local',
|
|
3897
|
+
plan_hint: null,
|
|
3898
|
+
enabled: true,
|
|
3899
|
+
health: 'healthy',
|
|
3402
3900
|
});
|
|
3403
|
-
} else {
|
|
3404
|
-
// Fallback: line-based prompt
|
|
3405
|
-
process.stdout.write(' Choice [2]: ');
|
|
3406
|
-
styleChoice = (await ask('')).trim() || '2';
|
|
3407
3901
|
}
|
|
3902
|
+
if (finalOpenaiEnabled) {
|
|
3903
|
+
credEntries.push({
|
|
3904
|
+
id: openaiReady ? 'openai-apikey' : 'openai-codex',
|
|
3905
|
+
provider: 'openai',
|
|
3906
|
+
auth_type: openaiAuthType || 'api_key',
|
|
3907
|
+
source: openaiReady ? 'env_var' : 'cli_oauth',
|
|
3908
|
+
owner: 'user',
|
|
3909
|
+
scope: 'local',
|
|
3910
|
+
plan_hint: null,
|
|
3911
|
+
enabled: true,
|
|
3912
|
+
health: 'healthy',
|
|
3913
|
+
});
|
|
3914
|
+
}
|
|
3915
|
+
try { saveWizardCredentials(cwd, credEntries); } catch { /* non-fatal */ }
|
|
3916
|
+
|
|
3917
|
+
// ─── Step 2: Work style ───────────────────────────────────────────────────
|
|
3918
|
+
process.stdout.write(` ${BOLD}Choose your work style:${RST}\n\n`);
|
|
3919
|
+
process.stdout.write(` ${CYAN}●${RST} Auto (recommended) — adapts to each task\n`);
|
|
3920
|
+
process.stdout.write(` ${GRAY}○${RST} Quality-first — deeper review, stronger models\n`);
|
|
3921
|
+
process.stdout.write(` ${GRAY}○${RST} Cost-saver — lighter models, lower cost\n`);
|
|
3922
|
+
process.stdout.write('\n');
|
|
3923
|
+
process.stdout.write(` ${GRAY}[Enter]${RST} Auto ${GRAY}[1-3]${RST} select\n\n`);
|
|
3408
3924
|
|
|
3409
|
-
const
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3925
|
+
const styleKey = await singleKey(['1', '2', '3', '\r']);
|
|
3926
|
+
const styleMap = { '1': 'auto', '2': 'quality-first', '3': 'cost-saver', '\r': 'auto' };
|
|
3927
|
+
const chosenBias = styleMap[styleKey] || 'auto';
|
|
3412
3928
|
|
|
3413
|
-
//
|
|
3929
|
+
// Metered API note (non-blocking)
|
|
3414
3930
|
if (openaiReady && caps.openai.metered) {
|
|
3415
|
-
|
|
3416
|
-
|
|
3931
|
+
process.stdout.write('\n');
|
|
3932
|
+
dimLine('OpenAI API key detected — usage is metered, guardrails enabled');
|
|
3417
3933
|
}
|
|
3418
3934
|
|
|
3419
|
-
|
|
3420
|
-
fx.step(5, 5, 'Ready!');
|
|
3421
|
-
fx.nl();
|
|
3935
|
+
process.stdout.write('\n');
|
|
3422
3936
|
|
|
3423
|
-
// Init living docs
|
|
3937
|
+
// Init living docs (non-fatal)
|
|
3424
3938
|
try {
|
|
3425
3939
|
const ld = await getLivingDocs();
|
|
3426
3940
|
if (ld.initLivingDocs) ld.initLivingDocs(cwd);
|
|
3427
3941
|
} catch { /* non-fatal */ }
|
|
3428
3942
|
|
|
3943
|
+
// ─── Step 3: Done — seamless transition line before dashboard renders ─────
|
|
3944
|
+
const termWidth = process.stdout.columns || 72;
|
|
3945
|
+
const divider = '━'.repeat(Math.min(termWidth - 2, 57));
|
|
3946
|
+
process.stdout.write(` ${GRAY}${divider}${RST}\n`);
|
|
3947
|
+
|
|
3948
|
+
const providerCount = [finalClaudeEnabled, finalOpenaiEnabled].filter(Boolean).length;
|
|
3949
|
+
const sessionLabel = rtSessionCount > 0 ? ` · ${rtSessionCount} sessions imported` : '';
|
|
3950
|
+
process.stdout.write(` ${GREEN}✓${RST} Setup complete · ${providerCount} provider${providerCount === 1 ? '' : 's'}${sessionLabel}\n`);
|
|
3951
|
+
process.stdout.write('\n');
|
|
3952
|
+
|
|
3429
3953
|
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
3954
|
|
|
3435
3955
|
// ─── Build and return the profile object ──────────────────────────────────
|
|
3436
3956
|
const finalProfile = loadProfile(cwd);
|
|
3437
3957
|
|
|
3438
|
-
finalProfile.providers.claude = { enabled:
|
|
3439
|
-
finalProfile.providers.openai = { enabled:
|
|
3958
|
+
finalProfile.providers.claude = { enabled: finalClaudeEnabled };
|
|
3959
|
+
finalProfile.providers.openai = { enabled: finalOpenaiEnabled };
|
|
3440
3960
|
finalProfile.apiGuardrail = caps.openai.metered;
|
|
3961
|
+
finalProfile.setupComplete = true;
|
|
3441
3962
|
|
|
3442
|
-
const enabledCount = [
|
|
3443
|
-
finalProfile.mode = enabledCount >= 2 ? 'dual' :
|
|
3963
|
+
const enabledCount = [finalClaudeEnabled, finalOpenaiEnabled].filter(Boolean).length;
|
|
3964
|
+
finalProfile.mode = enabledCount >= 2 ? 'dual' : finalClaudeEnabled ? 'solo-claude' : 'solo-openai';
|
|
3444
3965
|
finalProfile.bias = chosenBias;
|
|
3445
3966
|
finalProfile.workStyle = chosenBias;
|
|
3446
3967
|
|
|
@@ -4476,6 +4997,7 @@ const SCREENS = {
|
|
|
4476
4997
|
welcome: welcomeScreen,
|
|
4477
4998
|
main: mainScreen,
|
|
4478
4999
|
'new-session': newSessionScreen,
|
|
5000
|
+
'palette-help': paletteHelpScreen,
|
|
4479
5001
|
settings: settingsScreen,
|
|
4480
5002
|
team: teamScreen,
|
|
4481
5003
|
'import-picker': importPickerScreen,
|
|
@@ -4502,7 +5024,21 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
4502
5024
|
if (current === 'go' && ctx.prompt) {
|
|
4503
5025
|
const prompt = ctx.prompt;
|
|
4504
5026
|
const dryRun = ctx.dryRun || false;
|
|
4505
|
-
|
|
5027
|
+
// Haiku tier: dispatch with model override for cheap question answers
|
|
5028
|
+
if (ctx.model === 'haiku') {
|
|
5029
|
+
process.stdout.write('\n');
|
|
5030
|
+
try {
|
|
5031
|
+
const { runPipeline: rp } = await import('../src/pipeline.mjs');
|
|
5032
|
+
const { result } = await rp('go', prompt, { cwd: process.cwd(), dryRun, forceDepth: 'shallow' });
|
|
5033
|
+
if (result?.output) process.stdout.write('\n' + String(result.output).trim() + '\n\n');
|
|
5034
|
+
else process.stdout.write(' (no output)\n\n');
|
|
5035
|
+
} catch (e) {
|
|
5036
|
+
// Fall back to normal dispatch on error
|
|
5037
|
+
await cmdGo([prompt], { dryRun });
|
|
5038
|
+
}
|
|
5039
|
+
} else {
|
|
5040
|
+
await cmdGo([prompt], { dryRun });
|
|
5041
|
+
}
|
|
4506
5042
|
current = 'main';
|
|
4507
5043
|
ctx = {};
|
|
4508
5044
|
continue;
|
|
@@ -4515,7 +5051,7 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
4515
5051
|
current = result?.next || 'exit';
|
|
4516
5052
|
// Pass through context (e.g. selected session, typed prompt, openPRs) to next screen
|
|
4517
5053
|
ctx = result?.session ? { session: result.session }
|
|
4518
|
-
: result?.prompt ? { prompt: result.prompt }
|
|
5054
|
+
: result?.prompt ? { prompt: result.prompt, model: result.model }
|
|
4519
5055
|
: result?.openPRs ? { openPRs: result.openPRs }
|
|
4520
5056
|
: {};
|
|
4521
5057
|
} catch (e) {
|
|
@@ -5011,7 +5547,7 @@ async function main() {
|
|
|
5011
5547
|
if (wizardProfile) {
|
|
5012
5548
|
saveProfile(wizardProfile, { cwd });
|
|
5013
5549
|
await cmdInstall(cwd);
|
|
5014
|
-
|
|
5550
|
+
// (wizard already printed setup-complete line)
|
|
5015
5551
|
}
|
|
5016
5552
|
rl.close();
|
|
5017
5553
|
await runScreens('main');
|
|
@@ -5042,6 +5578,30 @@ async function main() {
|
|
|
5042
5578
|
}
|
|
5043
5579
|
|
|
5044
5580
|
if (cmd === 'init') {
|
|
5581
|
+
// init --reset: clear credentials.json and re-run wizard
|
|
5582
|
+
if (args.includes('--reset')) {
|
|
5583
|
+
const cwd = process.cwd();
|
|
5584
|
+
const credPath = join(cwd, '.dualbrain', 'credentials.json');
|
|
5585
|
+
try {
|
|
5586
|
+
if (existsSync(credPath)) {
|
|
5587
|
+
unlinkSync(credPath);
|
|
5588
|
+
console.log(' ✓ credentials.json cleared');
|
|
5589
|
+
}
|
|
5590
|
+
// Also clear setupComplete so wizard re-runs
|
|
5591
|
+
const profilePath = join(cwd, '.dualbrain', 'profile.json');
|
|
5592
|
+
if (existsSync(profilePath)) {
|
|
5593
|
+
const p = JSON.parse(readFileSync(profilePath, 'utf8'));
|
|
5594
|
+
delete p.setupComplete;
|
|
5595
|
+
writeFileSync(profilePath, JSON.stringify(p, null, 2), 'utf8');
|
|
5596
|
+
console.log(' ✓ profile reset — wizard will re-run');
|
|
5597
|
+
}
|
|
5598
|
+
} catch (e) {
|
|
5599
|
+
console.error(' Error during reset:', e.message);
|
|
5600
|
+
}
|
|
5601
|
+
if (!isInteractive) return;
|
|
5602
|
+
// Fall through to run the wizard interactively
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5045
5605
|
// init --replit: run Replit-specific integration setup
|
|
5046
5606
|
if (args.includes('--replit')) {
|
|
5047
5607
|
const cwd = process.cwd();
|
|
@@ -5069,7 +5629,7 @@ async function main() {
|
|
|
5069
5629
|
if (wizardProfile) {
|
|
5070
5630
|
saveProfile(wizardProfile, { cwd });
|
|
5071
5631
|
await cmdInstall(cwd);
|
|
5072
|
-
|
|
5632
|
+
// (wizard already printed setup-complete line)
|
|
5073
5633
|
}
|
|
5074
5634
|
rl.close();
|
|
5075
5635
|
await runScreens('main');
|