dual-brain 0.2.3 → 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 +655 -425
- package/package.json +1 -1
- package/src/profile.mjs +259 -10
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 ───────────────────────────────────────────────────────
|
|
@@ -1872,31 +1886,20 @@ async function mainScreen(rl, ask) {
|
|
|
1872
1886
|
// ── Interrupted work detection ────────────────────────────────────────────
|
|
1873
1887
|
const interrupted = detectInterruptedWork(allSessions, cwd);
|
|
1874
1888
|
|
|
1875
|
-
// ──
|
|
1876
|
-
const termW = process.stdout.columns ||
|
|
1877
|
-
const
|
|
1878
|
-
const W = boxW - 4; // inner content width (│ {content} │)
|
|
1879
|
-
|
|
1880
|
-
const top = `┌${'─'.repeat(boxW - 2)}┐`;
|
|
1881
|
-
const sep = `├${'─'.repeat(boxW - 2)}┤`;
|
|
1882
|
-
const bot = `└${'─'.repeat(boxW - 2)}┘`;
|
|
1883
|
-
|
|
1884
|
-
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
|
|
1885
1892
|
|
|
1886
1893
|
// ── Continuation card (interrupted work) ─────────────────────────────────
|
|
1887
1894
|
if (interrupted) {
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
: ` ${interrupted.reason} · ${interrupted.ageLabel}`;
|
|
1897
|
-
const actLine = ' [Enter] Resume [n] New session [s] Skip';
|
|
1898
|
-
|
|
1899
|
-
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`);
|
|
1900
1903
|
|
|
1901
1904
|
// Wait for a keypress to decide what to do with the card
|
|
1902
1905
|
const readline2 = await import('node:readline');
|
|
@@ -1968,8 +1971,13 @@ async function mainScreen(rl, ask) {
|
|
|
1968
1971
|
envReport = scanEnvironment(cwd);
|
|
1969
1972
|
} catch { /* non-fatal */ }
|
|
1970
1973
|
|
|
1971
|
-
// ──
|
|
1972
|
-
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;
|
|
1973
1981
|
|
|
1974
1982
|
// ── Box 2 — Workspace: gather git data ───────────────────────────────────
|
|
1975
1983
|
let gitBranch = 'unknown';
|
|
@@ -2013,24 +2021,14 @@ async function mainScreen(rl, ask) {
|
|
|
2013
2021
|
}
|
|
2014
2022
|
} catch {}
|
|
2015
2023
|
|
|
2016
|
-
// ──
|
|
2024
|
+
// ── Workspace data ────────────────────────────────────────────────────────
|
|
2017
2025
|
const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
|
|
2018
2026
|
const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
|
|
2019
|
-
const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
|
|
2020
|
-
const workspaceLine2 = gitLastMsg
|
|
2021
|
-
? `Last: ${gitLastMsg} (${gitLastAgo})`
|
|
2022
|
-
: '';
|
|
2023
2027
|
|
|
2024
2028
|
// Open PRs
|
|
2025
2029
|
const repoState = detectRepoState(cwd);
|
|
2026
2030
|
const openPRs = await detectOpenPRs(cwd);
|
|
2027
2031
|
|
|
2028
|
-
const workspaceRows = [row(workspaceLine1)];
|
|
2029
|
-
if (workspaceLine2) workspaceRows.push(row(workspaceLine2));
|
|
2030
|
-
if (openPRs.length > 0) {
|
|
2031
|
-
workspaceRows.push(row(`${openPRs.length} open PR${openPRs.length === 1 ? '' : 's'}`));
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
2032
|
// ── Box 3 — Awareness: observer + roadmap + risk ──────────────────────────
|
|
2035
2033
|
let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
|
|
2036
2034
|
let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
|
|
@@ -2128,131 +2126,151 @@ async function mainScreen(rl, ask) {
|
|
|
2128
2126
|
const verStr = rtInfo.version ? `v${rtInfo.version}` : (rtInfo.installed ? 'installed' : 'not installed');
|
|
2129
2127
|
const isAuthenticated = authInfo.authenticated ?? (authInfo.available && authInfo.tokenStatus === 'valid');
|
|
2130
2128
|
const authStr = isAuthenticated ? '\x1b[32m✓\x1b[0m auth' : '\x1b[2mno auth\x1b[0m';
|
|
2131
|
-
replitAwarenessRows.push(
|
|
2132
|
-
replitAwarenessRows.push(
|
|
2129
|
+
replitAwarenessRows.push(`Replit replit-tools ${verStr} ${authStr}`);
|
|
2130
|
+
replitAwarenessRows.push(`${archCount} archived session${archCount !== 1 ? 's' : ''} ${secretCount} secret${secretCount !== 1 ? 's' : ''}`);
|
|
2133
2131
|
}
|
|
2134
2132
|
} catch { /* replit.mjs not available — skip */ }
|
|
2135
2133
|
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
//
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
rawName = sess.project
|
|
2154
|
-
? sess.project.replace(/^-/, '/').replace(/-/g, '/')
|
|
2155
|
-
: sess.id.slice(0, 8);
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
// Build badges (ANSI color; track visible width separately)
|
|
2159
|
-
const badges = [];
|
|
2160
|
-
const badgeVisible = [];
|
|
2161
|
-
if (sess.isActive) {
|
|
2162
|
-
badges.push('\x1b[32m[active]\x1b[0m');
|
|
2163
|
-
badgeVisible.push('[active]'.length);
|
|
2164
|
-
}
|
|
2165
|
-
const ageMs = sess.lastActive ? Date.now() - new Date(sess.lastActive).getTime() : 0;
|
|
2166
|
-
if (ageMs > 7 * 24 * 3600 * 1000) {
|
|
2167
|
-
badges.push('\x1b[2m[stale]\x1b[0m');
|
|
2168
|
-
badgeVisible.push('[stale]'.length);
|
|
2169
|
-
}
|
|
2170
|
-
const msgCount = sess.messageCount ?? sess.promptCount ?? 0;
|
|
2171
|
-
// Human-readable: "4 tasks" instead of "(4)"
|
|
2172
|
-
const taskLabel = msgCount === 1 ? '1 task' : `${msgCount} tasks`;
|
|
2173
|
-
const taskBadge = `\x1b[2m${taskLabel}\x1b[0m`;
|
|
2174
|
-
const taskBadgeW = taskLabel.length;
|
|
2175
|
-
|
|
2176
|
-
const badgeStr = badges.join('');
|
|
2177
|
-
const badgesW = badgeVisible.reduce((s, n) => s + n, 0);
|
|
2178
|
-
|
|
2179
|
-
// Layout: "{num} {name...}{badges} {age} {tasks}"
|
|
2180
|
-
// Use basename for name — strip full paths for readability
|
|
2181
|
-
const displayName = rawName.startsWith('/')
|
|
2182
|
-
? rawName.split('/').filter(Boolean).pop() || rawName
|
|
2183
|
-
: rawName;
|
|
2184
|
-
|
|
2185
|
-
const numStr = String(i + 1);
|
|
2186
|
-
const ageStr = sess.age || '';
|
|
2187
|
-
// Available for name: W minus fixed chrome, badge widths, and task badge
|
|
2188
|
-
const nameMax = W - numStr.length - 2 - badgesW - 2 - ageStr.length - 2 - taskBadgeW;
|
|
2189
|
-
const truncName = displayName.length > nameMax
|
|
2190
|
-
? displayName.slice(0, Math.max(0, nameMax - 3)) + '...'
|
|
2191
|
-
: displayName.padEnd(nameMax);
|
|
2192
|
-
const content = `${numStr} ${truncName}${badgeStr} ${ageStr} ${taskBadge}`;
|
|
2193
|
-
sessionRows.push(row(content));
|
|
2194
|
-
});
|
|
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) });
|
|
2195
2151
|
}
|
|
2196
2152
|
|
|
2197
2153
|
// ── Resume state detection ────────────────────────────────────────────────
|
|
2198
|
-
let resumeStateRows = [];
|
|
2199
2154
|
const resumeState = await detectResumeState(cwd);
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2155
|
+
|
|
2156
|
+
// ── Determine layout mode ─────────────────────────────────────────────────
|
|
2157
|
+
const anyProviderAvail = claudeAvail || openaiAvail;
|
|
2158
|
+
const isReturning = resumeState.type === 'resumable';
|
|
2159
|
+
|
|
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';
|
|
2168
|
+
|
|
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}` : '';
|
|
2207
2209
|
const nextPart = resumeState.nextAction ? ` · next: ${resumeState.nextAction}` : '';
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
row(providerLine),
|
|
2236
|
-
sep,
|
|
2237
|
-
...workspaceRows,
|
|
2238
|
-
sep,
|
|
2239
|
-
...awarenessRows,
|
|
2240
|
-
sep,
|
|
2241
|
-
...sessionRows,
|
|
2242
|
-
...(hasResumeHint ? [sep, ...resumeStateRows] : []),
|
|
2243
|
-
sep,
|
|
2244
|
-
actionsRow,
|
|
2245
|
-
bot,
|
|
2246
|
-
];
|
|
2247
|
-
// ── Stale session hint ──────────────────────────────────────────────────
|
|
2248
|
-
if (staleCount >= 3) {
|
|
2249
|
-
process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
|
|
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'];
|
|
2250
2237
|
}
|
|
2238
|
+
const suggestLine = ` ${suggestions.join(' ')}`;
|
|
2251
2239
|
|
|
2252
|
-
//
|
|
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 ────────────────────────────
|
|
2253
2247
|
if (dashSpinner) dashSpinner.succeed('Dashboard ready');
|
|
2254
2248
|
|
|
2255
|
-
|
|
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');
|
|
2256
2274
|
|
|
2257
2275
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
2258
2276
|
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
@@ -3081,15 +3099,14 @@ async function prTriageScreen(rl, ask, ctx = {}) {
|
|
|
3081
3099
|
async function settingsScreen(rl, ask) {
|
|
3082
3100
|
const cwd = process.cwd();
|
|
3083
3101
|
|
|
3084
|
-
|
|
3085
|
-
const
|
|
3086
|
-
const
|
|
3087
|
-
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';
|
|
3088
3107
|
|
|
3089
|
-
const
|
|
3090
|
-
const
|
|
3091
|
-
const bot = `└${'─'.repeat(boxW - 2)}┘`;
|
|
3092
|
-
const row = (content) => makeBoxRow(content, W);
|
|
3108
|
+
const chk = `${GREEN}✓${RESET}`;
|
|
3109
|
+
const xmark = `${RED}✗${RESET}`;
|
|
3093
3110
|
|
|
3094
3111
|
// Detect if gh is available + has PRs for the PR triage option
|
|
3095
3112
|
const settingsPRs = await detectOpenPRs(cwd);
|
|
@@ -3097,108 +3114,131 @@ async function settingsScreen(rl, ask) {
|
|
|
3097
3114
|
// Load current work style
|
|
3098
3115
|
const profile = loadProfile(cwd);
|
|
3099
3116
|
const currentBias = profile?.bias || profile?.mode || 'balanced';
|
|
3100
|
-
const WORK_STYLE_DISPLAY = {
|
|
3101
|
-
'cost-saver': '⚡ Fast',
|
|
3102
|
-
'auto': '⚡ Fast',
|
|
3103
|
-
'solo-claude': '⚡ Fast',
|
|
3104
|
-
'solo-openai': '⚡ Fast',
|
|
3105
|
-
'balanced': '⚖️ Balanced',
|
|
3106
|
-
'quality-first': '🔥 Full Power',
|
|
3107
|
-
};
|
|
3108
3117
|
|
|
3109
3118
|
// Work style current markers
|
|
3110
3119
|
const _stIsFast = ['cost-saver', 'auto', 'solo-claude', 'solo-openai'].includes(currentBias);
|
|
3111
3120
|
const _stIsBal = currentBias === 'balanced';
|
|
3112
3121
|
const _stIsFull = currentBias === 'quality-first';
|
|
3113
|
-
const
|
|
3114
|
-
|
|
3115
|
-
//
|
|
3116
|
-
const
|
|
3117
|
-
const
|
|
3118
|
-
const
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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;
|
|
3128
3163
|
try {
|
|
3129
|
-
const
|
|
3130
|
-
const
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
const _stAd = _stCm.getAdaptation(_stCal);
|
|
3134
|
-
_stLevel = _stAd.userLevel;
|
|
3135
|
-
_stStyle = _stAd.responseStyle;
|
|
3136
|
-
} 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 */ }
|
|
3137
3168
|
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
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 */ }
|
|
3141
3177
|
|
|
3142
|
-
|
|
3143
|
-
let _stEffScore = null;
|
|
3144
|
-
let _stEffRate = null;
|
|
3145
|
-
let _stEffTrend = null;
|
|
3146
|
-
let _stEffTier = null;
|
|
3178
|
+
let doctorStr = `${DIM}not run${RESET}`;
|
|
3147
3179
|
try {
|
|
3148
|
-
const
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3155
|
-
const
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
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;
|
|
3160
3196
|
}
|
|
3161
|
-
|
|
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 */ }
|
|
3162
3203
|
|
|
3163
|
-
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
|
+
];
|
|
3164
3210
|
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
...(_stEffTier ? [row(` Tiers: ${_stEffTier}`)] : []),
|
|
3186
|
-
] : []),
|
|
3187
|
-
sep,
|
|
3188
|
-
row('[1-3] change style [r] reset calibration [b] back'),
|
|
3189
|
-
row('[m] subscriptions [e] sessions [x] diagnostics'),
|
|
3190
|
-
...(settingsPRs.length > 0 ? [row(`[p] PR triage (${settingsPRs.length} open)`)] : []),
|
|
3191
|
-
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
|
+
'',
|
|
3192
3231
|
];
|
|
3193
|
-
process.stdout.write(
|
|
3232
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
3194
3233
|
|
|
3195
3234
|
const raw = (await ask(' Choice: ')).trim();
|
|
3196
3235
|
const choice = raw.toLowerCase();
|
|
3197
3236
|
|
|
3198
|
-
//
|
|
3237
|
+
// Work style 1/2/3
|
|
3199
3238
|
if (choice === '1' || choice === '2' || choice === '3') {
|
|
3200
|
-
const
|
|
3201
|
-
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];
|
|
3202
3242
|
if (newBias && newBias !== currentBias) {
|
|
3203
3243
|
profile.bias = newBias;
|
|
3204
3244
|
const enabledCount = [
|
|
@@ -3207,71 +3247,91 @@ async function settingsScreen(rl, ask) {
|
|
|
3207
3247
|
].filter(Boolean).length;
|
|
3208
3248
|
if (enabledCount >= 2) profile.mode = newBias;
|
|
3209
3249
|
saveProfile(profile, { cwd });
|
|
3210
|
-
|
|
3211
|
-
process.stdout.write(`\n Work style set to ${newLabel}\n\n`);
|
|
3250
|
+
process.stdout.write(`\n Work style set to ${wsDisp[choice]}\n\n`);
|
|
3212
3251
|
await ask(' Press Enter to continue...');
|
|
3213
3252
|
}
|
|
3214
3253
|
return { next: 'settings' };
|
|
3215
3254
|
}
|
|
3216
3255
|
|
|
3217
|
-
//
|
|
3218
|
-
if (choice === '
|
|
3256
|
+
// Add credential
|
|
3257
|
+
if (choice === 'a') {
|
|
3258
|
+
process.stdout.write('\n Auto-detecting credentials...\n');
|
|
3219
3259
|
try {
|
|
3220
|
-
const
|
|
3221
|
-
|
|
3222
|
-
|
|
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');
|
|
3223
3283
|
await ask(' Press Enter to continue...');
|
|
3224
|
-
|
|
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...');
|
|
3225
3295
|
return { next: 'settings' };
|
|
3226
3296
|
}
|
|
3227
3297
|
|
|
3228
|
-
|
|
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
|
+
}
|
|
3229
3322
|
|
|
3323
|
+
if (choice === 'm') { return { next: 'subscriptions' }; }
|
|
3230
3324
|
if (choice === 'e') { return { next: 'sessions' }; }
|
|
3231
|
-
|
|
3232
|
-
if (choice === 'i') {
|
|
3233
|
-
return { next: 'import-picker' };
|
|
3234
|
-
}
|
|
3325
|
+
if (choice === 'x') { return { next: 'diagnostics' }; }
|
|
3235
3326
|
|
|
3236
3327
|
if (choice === 'p' && settingsPRs.length > 0) {
|
|
3237
3328
|
return { next: 'pr-triage', openPRs: settingsPRs };
|
|
3238
3329
|
}
|
|
3239
3330
|
|
|
3240
3331
|
if (choice === 'd') {
|
|
3241
|
-
|
|
3242
|
-
const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
|
|
3243
|
-
if (which.status === 0) {
|
|
3244
|
-
spawnSync('claude-menu', { stdio: 'inherit' });
|
|
3245
|
-
} else {
|
|
3246
|
-
process.stdout.write('\n replit-tools not found — install with: npm i -g replit-tools\n\n');
|
|
3247
|
-
await ask(' Press Enter to continue...');
|
|
3248
|
-
}
|
|
3249
|
-
return { next: 'settings' };
|
|
3250
|
-
}
|
|
3251
|
-
|
|
3252
|
-
if (choice === '?') {
|
|
3253
|
-
const W2 = 37;
|
|
3254
|
-
const helpTop = ` ┌${'─'.repeat(W2)}┐`;
|
|
3255
|
-
const helpSep = ` ├${'─'.repeat(W2)}┤`;
|
|
3256
|
-
const helpBottom = ` └${'─'.repeat(W2)}┘`;
|
|
3257
|
-
const helpPad = (s) => s + ' '.repeat(Math.max(0, W2 - s.length));
|
|
3258
|
-
process.stdout.write('\n');
|
|
3259
|
-
process.stdout.write(helpTop + '\n');
|
|
3260
|
-
process.stdout.write(` │ ${helpPad('At ~/workspace$ prompt:')}│\n`);
|
|
3261
|
-
process.stdout.write(` │ ${helpPad('db = show this menu')}│\n`);
|
|
3262
|
-
process.stdout.write(` │ ${helpPad('j = login to claude')}│\n`);
|
|
3263
|
-
process.stdout.write(` │ ${helpPad('k = login to codex')}│\n`);
|
|
3264
|
-
process.stdout.write(helpSep + '\n');
|
|
3265
|
-
process.stdout.write(` │ ${helpPad('In Claude:')}│\n`);
|
|
3266
|
-
process.stdout.write(` │ ${helpPad('Ctrl+C x2 = back to menu')}│\n`);
|
|
3267
|
-
process.stdout.write(` │ ${helpPad('Ctrl+C x3 = exit to shell')}│\n`);
|
|
3268
|
-
process.stdout.write(helpBottom + '\n\n');
|
|
3269
|
-
await ask(' Press Enter to continue...');
|
|
3270
|
-
return { next: 'settings' };
|
|
3332
|
+
return { next: 'diagnostics' };
|
|
3271
3333
|
}
|
|
3272
3334
|
|
|
3273
|
-
if (choice === 'x') { return { next: 'diagnostics' }; }
|
|
3274
|
-
|
|
3275
3335
|
if (choice === 'b' || choice === 'back' || raw === '\x1b') { return { next: 'main' }; }
|
|
3276
3336
|
|
|
3277
3337
|
return { next: 'main' };
|
|
@@ -3560,9 +3620,37 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
3560
3620
|
// ─── Onboarding Wizard ───────────────────────────────────────────────────────
|
|
3561
3621
|
|
|
3562
3622
|
/**
|
|
3563
|
-
*
|
|
3564
|
-
*
|
|
3565
|
-
|
|
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.
|
|
3566
3654
|
*
|
|
3567
3655
|
* @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
|
|
3568
3656
|
* @param {string} cwd
|
|
@@ -3570,192 +3658,310 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
3570
3658
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
3571
3659
|
*/
|
|
3572
3660
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
3573
|
-
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
3574
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 || '';
|
|
3575
3669
|
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3670
|
+
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
3671
|
+
|
|
3672
|
+
// Helper: print a single dim line (indented with one space)
|
|
3673
|
+
function dimLine(text) {
|
|
3674
|
+
process.stdout.write(` ${GRAY}${text}${RST}\n`);
|
|
3675
|
+
}
|
|
3676
|
+
|
|
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
|
+
}
|
|
3583
3709
|
|
|
3584
|
-
// ───
|
|
3585
|
-
|
|
3586
|
-
fx.
|
|
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`);
|
|
3587
3715
|
|
|
3588
|
-
//
|
|
3716
|
+
// ─── Env scan — run detection in parallel with animated output ────────────
|
|
3589
3717
|
const capsPromise = detectCapabilities(cwd);
|
|
3590
3718
|
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
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 */ }
|
|
3754
|
+
|
|
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
|
+
}
|
|
3596
3761
|
|
|
3597
|
-
//
|
|
3598
|
-
const
|
|
3762
|
+
// Provider spinner while awaiting detection
|
|
3763
|
+
const provSpinner = fx.spinner('Checking providers...').start();
|
|
3764
|
+
const caps = await capsPromise;
|
|
3599
3765
|
const claudeReady = caps.claude.available;
|
|
3600
3766
|
const openaiReady = caps.openai.available;
|
|
3601
3767
|
const codexAvailable = caps.codex.available;
|
|
3768
|
+
provSpinner.stop();
|
|
3602
3769
|
|
|
3603
|
-
//
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
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
|
+
}
|
|
3609
3783
|
|
|
3610
|
-
//
|
|
3611
|
-
|
|
3612
|
-
|
|
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
|
+
}
|
|
3613
3794
|
|
|
3795
|
+
// replit-tools — auto-import sessions (non-destructive read-only indexing, no prompt)
|
|
3614
3796
|
const rt = detectReplitTools(cwd);
|
|
3615
|
-
const rtSpinner = fx.spinner('Looking for replit-tools...').start();
|
|
3616
|
-
await fx.sleep(700);
|
|
3617
|
-
|
|
3618
3797
|
let rtSessionCount = 0;
|
|
3619
3798
|
if (rt.installed) {
|
|
3620
|
-
const vStr = rt.version ? ` v${rt.version}` : '';
|
|
3621
|
-
rtSpinner.succeed(`replit-tools${vStr} detected`);
|
|
3622
|
-
// Count available sessions
|
|
3623
3799
|
try {
|
|
3624
3800
|
const sessions = importReplitSessions(cwd);
|
|
3625
3801
|
rtSessionCount = sessions.length;
|
|
3626
3802
|
} catch { /* non-fatal */ }
|
|
3627
|
-
|
|
3628
|
-
|
|
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`);
|
|
3629
3808
|
}
|
|
3630
|
-
fx.nl();
|
|
3631
|
-
|
|
3632
|
-
// ─── Step 4: Import conversations ─────────────────────────────────────────
|
|
3633
|
-
fx.step(3, 5, 'Import conversations');
|
|
3634
|
-
fx.nl();
|
|
3635
3809
|
|
|
3636
|
-
|
|
3637
|
-
fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
|
|
3638
|
-
fx.nl();
|
|
3810
|
+
process.stdout.write('\n');
|
|
3639
3811
|
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
const importChoice = (await ask('')).trim().toLowerCase();
|
|
3812
|
+
// ─── Step 1: Confirm providers ────────────────────────────────────────────
|
|
3813
|
+
const hasAnyProvider = claudeReady || openaiReady || codexAvailable;
|
|
3643
3814
|
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
importSpinner.fail(`Import failed: ${e.message}`);
|
|
3652
|
-
}
|
|
3653
|
-
} else {
|
|
3654
|
-
fx.dim('Skipped — you can import later from Settings → Import');
|
|
3655
|
-
}
|
|
3656
|
-
} else if (rt.installed) {
|
|
3657
|
-
fx.dim('No sessions to import');
|
|
3658
|
-
} else {
|
|
3659
|
-
fx.dim('Skipping — replit-tools not found');
|
|
3660
|
-
}
|
|
3661
|
-
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`);
|
|
3662
3822
|
|
|
3663
|
-
|
|
3664
|
-
fx.step(4, 5, 'Choose your style');
|
|
3665
|
-
fx.nl();
|
|
3666
|
-
process.stdout.write(' How do you want to work?\n\n');
|
|
3667
|
-
process.stdout.write(' [1] ⚡ Fast — speed over caution, auto-execute\n');
|
|
3668
|
-
process.stdout.write(' [2] ⚖️ Balanced — smart routing, reviews when it matters\n');
|
|
3669
|
-
process.stdout.write(' [3] 🔒 Thorough — dual-brain everything, max quality\n');
|
|
3670
|
-
fx.nl();
|
|
3823
|
+
const noProvChoice = await singleKey(['c', 'o', 's', '\r']);
|
|
3671
3824
|
|
|
3672
|
-
|
|
3673
|
-
|
|
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
|
+
}
|
|
3674
3837
|
|
|
3675
|
-
|
|
3676
|
-
|
|
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
|
+
}
|
|
3677
3847
|
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
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
|
+
}
|
|
3682
3858
|
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
const wasRaw = process.stdin.isRaw;
|
|
3686
|
-
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`);
|
|
3687
3861
|
|
|
3688
|
-
|
|
3689
|
-
process.stdin.removeListener('keypress', onKey);
|
|
3690
|
-
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
3691
|
-
};
|
|
3862
|
+
const provChoice = await singleKey(['n', 'a', '\r', 'y']);
|
|
3692
3863
|
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
const name = key.name || '';
|
|
3696
|
-
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
3697
|
-
cleanup();
|
|
3698
|
-
process.stdout.write('\n');
|
|
3699
|
-
resolve('2');
|
|
3700
|
-
return;
|
|
3701
|
-
}
|
|
3702
|
-
if (name === 'return' || name === 'enter') {
|
|
3703
|
-
cleanup();
|
|
3704
|
-
process.stdout.write('\n');
|
|
3705
|
-
resolve('2');
|
|
3706
|
-
return;
|
|
3707
|
-
}
|
|
3708
|
-
if (str === '1' || str === '2' || str === '3') {
|
|
3709
|
-
cleanup();
|
|
3710
|
-
process.stdout.write(`${str}\n`);
|
|
3711
|
-
resolve(str);
|
|
3712
|
-
return;
|
|
3713
|
-
}
|
|
3714
|
-
};
|
|
3864
|
+
let finalClaudeEnabled = claudeReady;
|
|
3865
|
+
let finalOpenaiEnabled = openaiReady || codexAvailable;
|
|
3715
3866
|
|
|
3716
|
-
|
|
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',
|
|
3900
|
+
});
|
|
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',
|
|
3717
3913
|
});
|
|
3718
|
-
} else {
|
|
3719
|
-
// Fallback: line-based prompt
|
|
3720
|
-
process.stdout.write(' Choice [2]: ');
|
|
3721
|
-
styleChoice = (await ask('')).trim() || '2';
|
|
3722
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`);
|
|
3723
3924
|
|
|
3724
|
-
const
|
|
3725
|
-
const
|
|
3726
|
-
|
|
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';
|
|
3727
3928
|
|
|
3728
|
-
//
|
|
3929
|
+
// Metered API note (non-blocking)
|
|
3729
3930
|
if (openaiReady && caps.openai.metered) {
|
|
3730
|
-
|
|
3731
|
-
|
|
3931
|
+
process.stdout.write('\n');
|
|
3932
|
+
dimLine('OpenAI API key detected — usage is metered, guardrails enabled');
|
|
3732
3933
|
}
|
|
3733
3934
|
|
|
3734
|
-
|
|
3735
|
-
fx.step(5, 5, 'Ready!');
|
|
3736
|
-
fx.nl();
|
|
3935
|
+
process.stdout.write('\n');
|
|
3737
3936
|
|
|
3738
|
-
// Init living docs
|
|
3937
|
+
// Init living docs (non-fatal)
|
|
3739
3938
|
try {
|
|
3740
3939
|
const ld = await getLivingDocs();
|
|
3741
3940
|
if (ld.initLivingDocs) ld.initLivingDocs(cwd);
|
|
3742
3941
|
} catch { /* non-fatal */ }
|
|
3743
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
|
+
|
|
3744
3953
|
await fx.sleep(400);
|
|
3745
|
-
fx.celebrate(`dual-brain is ready! (${chosenName} mode)`);
|
|
3746
|
-
fx.nl();
|
|
3747
|
-
fx.info('Type anything to get started. Your AI partner is listening.');
|
|
3748
|
-
await fx.sleep(1200);
|
|
3749
3954
|
|
|
3750
3955
|
// ─── Build and return the profile object ──────────────────────────────────
|
|
3751
3956
|
const finalProfile = loadProfile(cwd);
|
|
3752
3957
|
|
|
3753
|
-
finalProfile.providers.claude = { enabled:
|
|
3754
|
-
finalProfile.providers.openai = { enabled:
|
|
3958
|
+
finalProfile.providers.claude = { enabled: finalClaudeEnabled };
|
|
3959
|
+
finalProfile.providers.openai = { enabled: finalOpenaiEnabled };
|
|
3755
3960
|
finalProfile.apiGuardrail = caps.openai.metered;
|
|
3961
|
+
finalProfile.setupComplete = true;
|
|
3756
3962
|
|
|
3757
|
-
const enabledCount = [
|
|
3758
|
-
finalProfile.mode = enabledCount >= 2 ? 'dual' :
|
|
3963
|
+
const enabledCount = [finalClaudeEnabled, finalOpenaiEnabled].filter(Boolean).length;
|
|
3964
|
+
finalProfile.mode = enabledCount >= 2 ? 'dual' : finalClaudeEnabled ? 'solo-claude' : 'solo-openai';
|
|
3759
3965
|
finalProfile.bias = chosenBias;
|
|
3760
3966
|
finalProfile.workStyle = chosenBias;
|
|
3761
3967
|
|
|
@@ -5341,7 +5547,7 @@ async function main() {
|
|
|
5341
5547
|
if (wizardProfile) {
|
|
5342
5548
|
saveProfile(wizardProfile, { cwd });
|
|
5343
5549
|
await cmdInstall(cwd);
|
|
5344
|
-
|
|
5550
|
+
// (wizard already printed setup-complete line)
|
|
5345
5551
|
}
|
|
5346
5552
|
rl.close();
|
|
5347
5553
|
await runScreens('main');
|
|
@@ -5372,6 +5578,30 @@ async function main() {
|
|
|
5372
5578
|
}
|
|
5373
5579
|
|
|
5374
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
|
+
|
|
5375
5605
|
// init --replit: run Replit-specific integration setup
|
|
5376
5606
|
if (args.includes('--replit')) {
|
|
5377
5607
|
const cwd = process.cwd();
|
|
@@ -5399,7 +5629,7 @@ async function main() {
|
|
|
5399
5629
|
if (wizardProfile) {
|
|
5400
5630
|
saveProfile(wizardProfile, { cwd });
|
|
5401
5631
|
await cmdInstall(cwd);
|
|
5402
|
-
|
|
5632
|
+
// (wizard already printed setup-complete line)
|
|
5403
5633
|
}
|
|
5404
5634
|
rl.close();
|
|
5405
5635
|
await runScreens('main');
|