dual-brain 0.2.3 → 0.2.5
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 +1004 -427
- package/package.json +10 -2
- package/src/awareness.mjs +36 -0
- package/src/checkpoint.mjs +109 -0
- package/src/ci-triage.mjs +191 -0
- package/src/continuity.mjs +291 -0
- package/src/detect.mjs +38 -0
- package/src/dispatch.mjs +73 -6
- package/src/health.mjs +35 -0
- package/src/pipeline.mjs +58 -1
- package/src/pr-agent.mjs +214 -0
- package/src/profile.mjs +259 -10
- package/src/repo.mjs +153 -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';
|
|
@@ -241,11 +242,19 @@ Commands:
|
|
|
241
242
|
init First-time setup → flows into interactive REPL
|
|
242
243
|
auth Show provider login and plan status
|
|
243
244
|
install Install Claude Code hooks into the current project
|
|
245
|
+
install --global Write hooks into ~/.claude/settings.json (absolute paths,
|
|
246
|
+
fires from any working directory after shell restart)
|
|
247
|
+
uninstall --global Remove dual-brain hooks from ~/.claude/settings.json
|
|
244
248
|
go "task description" Detect → decide → dispatch (alias for do)
|
|
245
249
|
--dry-run Show routing decision without executing
|
|
246
250
|
--files a.mjs,b.mjs Provide file context for risk classification
|
|
247
251
|
--verbose, -v Print routing trace (intent, risk, health, model selection)
|
|
248
252
|
think "question" Multi-round architecture decision with dual-brain
|
|
253
|
+
pr Show PR status for current branch
|
|
254
|
+
pr create Create PR from current branch with auto-generated description
|
|
255
|
+
--draft Create as a draft PR
|
|
256
|
+
pr list List open PRs (--closed, --all for other states)
|
|
257
|
+
pr view <N> View PR #N details
|
|
249
258
|
status Provider health, session stats, available models
|
|
250
259
|
--verbose, -v Also print profile file path and raw profile object
|
|
251
260
|
hot <provider> Manually mark all model classes for provider as hot
|
|
@@ -372,6 +381,26 @@ async function cmdInit(rl) {
|
|
|
372
381
|
// --- Step 2b: Install hooks ---
|
|
373
382
|
await cmdInstall(cwd);
|
|
374
383
|
|
|
384
|
+
// --- Step 2c: Suggest global install if not already done ---
|
|
385
|
+
try {
|
|
386
|
+
const { homedir } = await import('node:os');
|
|
387
|
+
const globalSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
388
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
389
|
+
let alreadyGlobal = false;
|
|
390
|
+
if (existsSync(globalSettingsPath)) {
|
|
391
|
+
try {
|
|
392
|
+
const gs = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
|
|
393
|
+
const allHooks = [...(gs.hooks?.PreToolUse || []), ...(gs.hooks?.PostToolUse || [])];
|
|
394
|
+
alreadyGlobal = allHooks.some(e => e.hooks?.some(h => h.command?.includes(DB_MARKER)));
|
|
395
|
+
} catch {}
|
|
396
|
+
}
|
|
397
|
+
if (!alreadyGlobal) {
|
|
398
|
+
console.log('');
|
|
399
|
+
console.log(' Tip: run "dual-brain install --global" to load these hooks from');
|
|
400
|
+
console.log(' any directory — so dual-brain works when Replit restarts a shell.');
|
|
401
|
+
}
|
|
402
|
+
} catch {}
|
|
403
|
+
|
|
375
404
|
// --- Step 3: Show dashboard ---
|
|
376
405
|
console.log('');
|
|
377
406
|
const repo = loadRepoCache(cwd);
|
|
@@ -990,6 +1019,139 @@ async function cmdInstall(cwd) {
|
|
|
990
1019
|
}
|
|
991
1020
|
}
|
|
992
1021
|
|
|
1022
|
+
async function installGlobal() {
|
|
1023
|
+
const { homedir } = await import('node:os');
|
|
1024
|
+
const globalClaudeDir = join(homedir(), '.claude');
|
|
1025
|
+
const globalSettingsPath = join(globalClaudeDir, 'settings.json');
|
|
1026
|
+
|
|
1027
|
+
// Resolve absolute path to hooks directory via import.meta.url
|
|
1028
|
+
const pkgRoot = join(__dirname, '..');
|
|
1029
|
+
const hooksDir = join(pkgRoot, '.claude', 'hooks');
|
|
1030
|
+
|
|
1031
|
+
// Warn if running from npx (ephemeral path)
|
|
1032
|
+
if (pkgRoot.includes('.npm/_npx') || pkgRoot.includes('npx-')) {
|
|
1033
|
+
console.log(' Warning: Running from npx — paths will break after this session.');
|
|
1034
|
+
console.log(' Install globally first: npm i -g dual-brain');
|
|
1035
|
+
console.log(' Then run: dual-brain install --global');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Verify hooks exist at resolved path
|
|
1040
|
+
if (!existsSync(join(hooksDir, 'head-guard.mjs'))) {
|
|
1041
|
+
console.log(' Error: Could not resolve hook files at: ' + hooksDir);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Load existing settings (merge, never clobber)
|
|
1046
|
+
let existing = {};
|
|
1047
|
+
if (existsSync(globalSettingsPath)) {
|
|
1048
|
+
try { existing = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch {}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Ensure hooks structure exists
|
|
1052
|
+
if (!existing.hooks) existing.hooks = {};
|
|
1053
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
|
|
1054
|
+
if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
|
|
1055
|
+
|
|
1056
|
+
// Define dual-brain hooks with ownership marker
|
|
1057
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
1058
|
+
const preToolHooks = [
|
|
1059
|
+
{ matcher: 'Edit', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1060
|
+
{ matcher: 'Write', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1061
|
+
{ matcher: 'NotebookEdit',hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1062
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1063
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'enforce-tier.mjs')} ${DB_MARKER}` }] },
|
|
1064
|
+
];
|
|
1065
|
+
const postToolHooks = [
|
|
1066
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'cost-logger.mjs')} ${DB_MARKER}` }] },
|
|
1067
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'auto-update-wrapper.mjs')} ${DB_MARKER}` }] },
|
|
1068
|
+
];
|
|
1069
|
+
|
|
1070
|
+
// Remove any existing dual-brain hooks (idempotent)
|
|
1071
|
+
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1072
|
+
existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1073
|
+
existing.hooks.PostToolUse = existing.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1074
|
+
|
|
1075
|
+
// Add dual-brain hooks
|
|
1076
|
+
existing.hooks.PreToolUse.push(...preToolHooks);
|
|
1077
|
+
existing.hooks.PostToolUse.push(...postToolHooks);
|
|
1078
|
+
|
|
1079
|
+
// Write merged settings
|
|
1080
|
+
mkdirSync(globalClaudeDir, { recursive: true });
|
|
1081
|
+
writeFileSync(globalSettingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
1082
|
+
|
|
1083
|
+
// Write minimal global CLAUDE.md (only if none exists, or append section)
|
|
1084
|
+
const globalClaudeMd = join(globalClaudeDir, 'CLAUDE.md');
|
|
1085
|
+
const dbSection = `\n## Dual-Brain Global Hooks\n\nThis machine has dual-brain hooks installed globally.\nProject-local .claude/CLAUDE.md and settings take precedence.\nManaged by: dual-brain install --global\n`;
|
|
1086
|
+
|
|
1087
|
+
if (!existsSync(globalClaudeMd)) {
|
|
1088
|
+
writeFileSync(globalClaudeMd, dbSection);
|
|
1089
|
+
} else {
|
|
1090
|
+
const content = readFileSync(globalClaudeMd, 'utf8');
|
|
1091
|
+
if (!content.includes('Dual-Brain Global Hooks')) {
|
|
1092
|
+
writeFileSync(globalClaudeMd, content + '\n' + dbSection);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
console.log(' + dual-brain hooks installed globally');
|
|
1097
|
+
console.log(' hooks dir: ' + hooksDir);
|
|
1098
|
+
console.log(' settings: ' + globalSettingsPath);
|
|
1099
|
+
console.log('');
|
|
1100
|
+
console.log(' All new Claude sessions will load dual-brain hooks.');
|
|
1101
|
+
console.log(' Run "dual-brain uninstall --global" to remove.');
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async function uninstallGlobal() {
|
|
1105
|
+
const { homedir } = await import('node:os');
|
|
1106
|
+
const globalSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
1107
|
+
|
|
1108
|
+
if (!existsSync(globalSettingsPath)) {
|
|
1109
|
+
console.log(' No global settings found.');
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
let settings = {};
|
|
1114
|
+
try { settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch { return; }
|
|
1115
|
+
|
|
1116
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
1117
|
+
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1118
|
+
|
|
1119
|
+
let removed = 0;
|
|
1120
|
+
if (settings.hooks?.PreToolUse) {
|
|
1121
|
+
const before = settings.hooks.PreToolUse.length;
|
|
1122
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1123
|
+
removed += before - settings.hooks.PreToolUse.length;
|
|
1124
|
+
}
|
|
1125
|
+
if (settings.hooks?.PostToolUse) {
|
|
1126
|
+
const before = settings.hooks.PostToolUse.length;
|
|
1127
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1128
|
+
removed += before - settings.hooks.PostToolUse.length;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Clean up empty arrays/objects
|
|
1132
|
+
if (settings.hooks?.PreToolUse?.length === 0) delete settings.hooks.PreToolUse;
|
|
1133
|
+
if (settings.hooks?.PostToolUse?.length === 0) delete settings.hooks.PostToolUse;
|
|
1134
|
+
if (Object.keys(settings.hooks || {}).length === 0) delete settings.hooks;
|
|
1135
|
+
|
|
1136
|
+
writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1137
|
+
|
|
1138
|
+
// Remove dual-brain section from global CLAUDE.md
|
|
1139
|
+
const globalClaudeMd = join(homedir(), '.claude', 'CLAUDE.md');
|
|
1140
|
+
if (existsSync(globalClaudeMd)) {
|
|
1141
|
+
let content = readFileSync(globalClaudeMd, 'utf8');
|
|
1142
|
+
const dbSectionRegex = /\n## Dual-Brain Global Hooks\n[\s\S]*?Managed by: dual-brain install --global\n/;
|
|
1143
|
+
content = content.replace(dbSectionRegex, '');
|
|
1144
|
+
if (content.trim()) {
|
|
1145
|
+
writeFileSync(globalClaudeMd, content);
|
|
1146
|
+
} else {
|
|
1147
|
+
unlinkSync(globalClaudeMd);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
console.log(` - removed ${removed} dual-brain hook${removed === 1 ? '' : 's'} from global settings`);
|
|
1152
|
+
console.log(' Other settings preserved.');
|
|
1153
|
+
}
|
|
1154
|
+
|
|
993
1155
|
function cmdRemember(text) {
|
|
994
1156
|
if (!text) err('Usage: dual-brain remember "preference text"');
|
|
995
1157
|
const profile = rememberPreference(text, { scope: 'project', cwd: process.cwd() });
|
|
@@ -1049,6 +1211,182 @@ function cmdBreakGlass(reason) {
|
|
|
1049
1211
|
console.log('└' + '─'.repeat(inner) + '┘');
|
|
1050
1212
|
}
|
|
1051
1213
|
|
|
1214
|
+
// ─── PR command ───────────────────────────────────────────────────────────────
|
|
1215
|
+
|
|
1216
|
+
async function cmdPR(args) {
|
|
1217
|
+
const cwd = process.cwd();
|
|
1218
|
+
const sub = args[0] ?? '';
|
|
1219
|
+
|
|
1220
|
+
// Lazy import — only loaded when 'pr' is invoked
|
|
1221
|
+
let prAgent;
|
|
1222
|
+
try {
|
|
1223
|
+
prAgent = await import('../src/pr-agent.mjs');
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
console.error('pr-agent module not available:', e.message);
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const gh = prAgent.hasGitHub();
|
|
1230
|
+
if (!gh.available) {
|
|
1231
|
+
console.error('gh CLI not found. Install GitHub CLI: https://cli.github.com');
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ── dual-brain pr (show current branch PR status) ──────────────────────────
|
|
1236
|
+
if (!sub || sub === 'status') {
|
|
1237
|
+
if (!gh.authenticated) {
|
|
1238
|
+
console.log('gh CLI is available but not authenticated. Run: gh auth login');
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const info = prAgent.getBranchInfo(cwd);
|
|
1242
|
+
console.log('\n── PR status ─────────────────────────────────────────────\n');
|
|
1243
|
+
console.log(` Branch: ${info.branch ?? '(unknown)'}`);
|
|
1244
|
+
console.log(` Base: ${info.defaultBranch}`);
|
|
1245
|
+
console.log(` Ahead: ${info.ahead} commit(s)`);
|
|
1246
|
+
console.log(` Behind: ${info.behind} commit(s)`);
|
|
1247
|
+
|
|
1248
|
+
if (info.isDefault) {
|
|
1249
|
+
console.log('\n On default branch — create a feature branch first.');
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Check for an existing PR on this branch
|
|
1254
|
+
try {
|
|
1255
|
+
const { execSync: _exec } = await import('node:child_process');
|
|
1256
|
+
const json = _exec(`gh pr list --head "${info.branch}" --json number,title,state,url`, {
|
|
1257
|
+
cwd, encoding: 'utf8', timeout: 10000,
|
|
1258
|
+
});
|
|
1259
|
+
const prs = JSON.parse(json);
|
|
1260
|
+
if (prs.length > 0) {
|
|
1261
|
+
const pr = prs[0];
|
|
1262
|
+
console.log(`\n PR #${pr.number}: ${pr.title}`);
|
|
1263
|
+
console.log(` State: ${pr.state}`);
|
|
1264
|
+
console.log(` URL: ${pr.url}`);
|
|
1265
|
+
} else {
|
|
1266
|
+
console.log('\n No PR open for this branch.');
|
|
1267
|
+
console.log(` Create one: dual-brain pr create`);
|
|
1268
|
+
}
|
|
1269
|
+
} catch {
|
|
1270
|
+
console.log('\n (Could not check for existing PR — run: gh pr status)');
|
|
1271
|
+
}
|
|
1272
|
+
console.log('');
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// ── dual-brain pr list ───────────────────────────────────────────────────────
|
|
1277
|
+
if (sub === 'list') {
|
|
1278
|
+
if (!gh.authenticated) {
|
|
1279
|
+
console.log('gh CLI not authenticated. Run: gh auth login');
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const state = args.includes('--closed') ? 'closed' : args.includes('--all') ? 'all' : 'open';
|
|
1283
|
+
const prs = prAgent.listPRs(cwd, { state, limit: 20 });
|
|
1284
|
+
if (prs.length === 0) {
|
|
1285
|
+
console.log(`No ${state} PRs found.`);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
console.log(`\n── ${state} PRs ──────────────────────────────────────────────\n`);
|
|
1289
|
+
for (const pr of prs) {
|
|
1290
|
+
const draft = pr.isDraft ? ' [draft]' : '';
|
|
1291
|
+
const date = pr.createdAt ? new Date(pr.createdAt).toLocaleDateString() : '';
|
|
1292
|
+
console.log(` #${pr.number} ${pr.title}${draft}`);
|
|
1293
|
+
console.log(` ${pr.headRefName} by ${pr.author?.login ?? '?'} ${date}`);
|
|
1294
|
+
}
|
|
1295
|
+
console.log('');
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// ── dual-brain pr view <N> ───────────────────────────────────────────────────
|
|
1300
|
+
if (sub === 'view') {
|
|
1301
|
+
const prNum = args[1];
|
|
1302
|
+
if (!prNum || isNaN(Number(prNum))) {
|
|
1303
|
+
console.error('Usage: dual-brain pr view <PR-number>');
|
|
1304
|
+
process.exit(1);
|
|
1305
|
+
}
|
|
1306
|
+
const details = prAgent.getPRDetails(prNum, cwd);
|
|
1307
|
+
if (!details) {
|
|
1308
|
+
console.error(`PR #${prNum} not found or gh CLI error.`);
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
console.log(`\n── PR #${prNum}: ${details.title} ─────────────────────────────\n`);
|
|
1312
|
+
console.log(` State: ${details.state}`);
|
|
1313
|
+
console.log(` Branch: ${details.headRefName} → ${details.baseRefName}`);
|
|
1314
|
+
console.log(` Changes: +${details.additions} -${details.deletions} (${details.changedFiles} files)`);
|
|
1315
|
+
if (details.statusCheckRollup?.length) {
|
|
1316
|
+
const passing = details.statusCheckRollup.filter(c => c.conclusion === 'SUCCESS').length;
|
|
1317
|
+
const total = details.statusCheckRollup.length;
|
|
1318
|
+
console.log(` Checks: ${passing}/${total} passing`);
|
|
1319
|
+
}
|
|
1320
|
+
if (details.body) {
|
|
1321
|
+
console.log('\n Body:\n');
|
|
1322
|
+
console.log(details.body.split('\n').map(l => ` ${l}`).join('\n').slice(0, 1500));
|
|
1323
|
+
}
|
|
1324
|
+
console.log('');
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ── dual-brain pr create ─────────────────────────────────────────────────────
|
|
1329
|
+
if (sub === 'create') {
|
|
1330
|
+
if (!gh.authenticated) {
|
|
1331
|
+
console.log('gh CLI not authenticated. Run: gh auth login');
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const info = prAgent.getBranchInfo(cwd);
|
|
1335
|
+
if (info.isDefault || !info.branch) {
|
|
1336
|
+
console.error('You are on the default branch. Switch to a feature branch before creating a PR.');
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
if (info.ahead === 0) {
|
|
1340
|
+
console.error('No commits ahead of the base branch. Make changes and commit first.');
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const diff = prAgent.getDiffSummary(info.defaultBranch, cwd);
|
|
1345
|
+
const draft = args.includes('--draft');
|
|
1346
|
+
|
|
1347
|
+
// Auto-generate a title from the branch name
|
|
1348
|
+
const rawTitle = info.branch.replace(/^db\//, '').replace(/-/g, ' ');
|
|
1349
|
+
const title = rawTitle.charAt(0).toUpperCase() + rawTitle.slice(1);
|
|
1350
|
+
|
|
1351
|
+
// Auto-generate body from diff
|
|
1352
|
+
const body = prAgent.buildPRBody(title, {
|
|
1353
|
+
filesChanged: diff.files,
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
console.log(`\n── Creating PR from ${info.branch} → ${info.defaultBranch} ────────────\n`);
|
|
1357
|
+
console.log(` Title: ${title}`);
|
|
1358
|
+
console.log(` Files: ${diff.fileCount} changed`);
|
|
1359
|
+
if (diff.summary) console.log(` Diff: ${diff.summary}`);
|
|
1360
|
+
if (draft) console.log(' Mode: draft');
|
|
1361
|
+
console.log('');
|
|
1362
|
+
|
|
1363
|
+
const result = prAgent.createPR({
|
|
1364
|
+
title,
|
|
1365
|
+
body,
|
|
1366
|
+
baseBranch: info.defaultBranch,
|
|
1367
|
+
draft,
|
|
1368
|
+
cwd,
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
if (result.success) {
|
|
1372
|
+
console.log(` PR created: ${result.url}`);
|
|
1373
|
+
} else {
|
|
1374
|
+
console.error(` Failed to create PR: ${result.error}`);
|
|
1375
|
+
process.exit(1);
|
|
1376
|
+
}
|
|
1377
|
+
console.log('');
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Unknown sub-subcommand
|
|
1382
|
+
console.log(`Unknown pr subcommand: "${sub}"`);
|
|
1383
|
+
console.log('Usage:');
|
|
1384
|
+
console.log(' dual-brain pr Show PR status for current branch');
|
|
1385
|
+
console.log(' dual-brain pr create Create PR from current branch');
|
|
1386
|
+
console.log(' dual-brain pr list List open PRs');
|
|
1387
|
+
console.log(' dual-brain pr view <N> View PR details');
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1052
1390
|
// ─── Screen helpers ───────────────────────────────────────────────────────────
|
|
1053
1391
|
|
|
1054
1392
|
/**
|
|
@@ -1089,7 +1427,20 @@ function profileExists(cwd) {
|
|
|
1089
1427
|
const dir = cwd || process.cwd();
|
|
1090
1428
|
const globalPath = join(process.env.HOME || '/root', '.config', 'dual-brain', 'profile.json');
|
|
1091
1429
|
const projectPath = join(dir, '.dualbrain', 'profile.json');
|
|
1092
|
-
|
|
1430
|
+
// Check file existence AND that setup wizard completed (setupComplete flag)
|
|
1431
|
+
if (existsSync(projectPath)) {
|
|
1432
|
+
try {
|
|
1433
|
+
const p = JSON.parse(readFileSync(projectPath, 'utf8'));
|
|
1434
|
+
return p.setupComplete === true;
|
|
1435
|
+
} catch { return true; } // malformed but exists — treat as complete
|
|
1436
|
+
}
|
|
1437
|
+
if (existsSync(globalPath)) {
|
|
1438
|
+
try {
|
|
1439
|
+
const p = JSON.parse(readFileSync(globalPath, 'utf8'));
|
|
1440
|
+
return p.setupComplete === true;
|
|
1441
|
+
} catch { return true; }
|
|
1442
|
+
}
|
|
1443
|
+
return false;
|
|
1093
1444
|
}
|
|
1094
1445
|
|
|
1095
1446
|
// ─── Plan label helpers ───────────────────────────────────────────────────────
|
|
@@ -1872,31 +2223,20 @@ async function mainScreen(rl, ask) {
|
|
|
1872
2223
|
// ── Interrupted work detection ────────────────────────────────────────────
|
|
1873
2224
|
const interrupted = detectInterruptedWork(allSessions, cwd);
|
|
1874
2225
|
|
|
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);
|
|
2226
|
+
// ── Studio Console layout ─────────────────────────────────────────────────
|
|
2227
|
+
const termW = process.stdout.columns || 80;
|
|
2228
|
+
const W = Math.min(termW - 2, 78); // usable content width
|
|
1885
2229
|
|
|
1886
2230
|
// ── Continuation card (interrupted work) ─────────────────────────────────
|
|
1887
2231
|
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');
|
|
2232
|
+
const DIM = '\x1b[2m', RST = '\x1b[0m', YLW = '\x1b[33m';
|
|
2233
|
+
process.stdout.write(`\n ${YLW}Continue:${RST} ${interrupted.sessionName}\n`);
|
|
2234
|
+
if (interrupted.lastState) {
|
|
2235
|
+
process.stdout.write(` ${DIM}Last: ${interrupted.lastState} · ${interrupted.ageLabel}${RST}\n`);
|
|
2236
|
+
} else {
|
|
2237
|
+
process.stdout.write(` ${DIM}${interrupted.reason} · ${interrupted.ageLabel}${RST}\n`);
|
|
2238
|
+
}
|
|
2239
|
+
process.stdout.write(` ${DIM}[Enter] resume [n] new [s] skip${RST}\n\n`);
|
|
1900
2240
|
|
|
1901
2241
|
// Wait for a keypress to decide what to do with the card
|
|
1902
2242
|
const readline2 = await import('node:readline');
|
|
@@ -1968,8 +2308,13 @@ async function mainScreen(rl, ask) {
|
|
|
1968
2308
|
envReport = scanEnvironment(cwd);
|
|
1969
2309
|
} catch { /* non-fatal */ }
|
|
1970
2310
|
|
|
1971
|
-
// ──
|
|
1972
|
-
const
|
|
2311
|
+
// ── Studio Console: resolve provider availability ────────────────────────
|
|
2312
|
+
const claudeAvail = envReport
|
|
2313
|
+
? envReport.secrets?.ANTHROPIC_API_KEY || auth.claude.found
|
|
2314
|
+
: auth.claude.found;
|
|
2315
|
+
const openaiAvail = envReport
|
|
2316
|
+
? envReport.secrets?.OPENAI_API_KEY || auth.openai.found
|
|
2317
|
+
: auth.openai.found;
|
|
1973
2318
|
|
|
1974
2319
|
// ── Box 2 — Workspace: gather git data ───────────────────────────────────
|
|
1975
2320
|
let gitBranch = 'unknown';
|
|
@@ -2013,24 +2358,14 @@ async function mainScreen(rl, ask) {
|
|
|
2013
2358
|
}
|
|
2014
2359
|
} catch {}
|
|
2015
2360
|
|
|
2016
|
-
// ──
|
|
2361
|
+
// ── Workspace data ────────────────────────────────────────────────────────
|
|
2017
2362
|
const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
|
|
2018
2363
|
const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
|
|
2019
|
-
const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
|
|
2020
|
-
const workspaceLine2 = gitLastMsg
|
|
2021
|
-
? `Last: ${gitLastMsg} (${gitLastAgo})`
|
|
2022
|
-
: '';
|
|
2023
2364
|
|
|
2024
2365
|
// Open PRs
|
|
2025
2366
|
const repoState = detectRepoState(cwd);
|
|
2026
2367
|
const openPRs = await detectOpenPRs(cwd);
|
|
2027
2368
|
|
|
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
2369
|
// ── Box 3 — Awareness: observer + roadmap + risk ──────────────────────────
|
|
2035
2370
|
let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
|
|
2036
2371
|
let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
|
|
@@ -2128,131 +2463,151 @@ async function mainScreen(rl, ask) {
|
|
|
2128
2463
|
const verStr = rtInfo.version ? `v${rtInfo.version}` : (rtInfo.installed ? 'installed' : 'not installed');
|
|
2129
2464
|
const isAuthenticated = authInfo.authenticated ?? (authInfo.available && authInfo.tokenStatus === 'valid');
|
|
2130
2465
|
const authStr = isAuthenticated ? '\x1b[32m✓\x1b[0m auth' : '\x1b[2mno auth\x1b[0m';
|
|
2131
|
-
replitAwarenessRows.push(
|
|
2132
|
-
replitAwarenessRows.push(
|
|
2466
|
+
replitAwarenessRows.push(`Replit replit-tools ${verStr} ${authStr}`);
|
|
2467
|
+
replitAwarenessRows.push(`${archCount} archived session${archCount !== 1 ? 's' : ''} ${secretCount} secret${secretCount !== 1 ? 's' : ''}`);
|
|
2133
2468
|
}
|
|
2134
2469
|
} catch { /* replit.mjs not available — skip */ }
|
|
2135
2470
|
|
|
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
|
-
});
|
|
2471
|
+
// ── Recent work items (from awareness + sessions) — max 3 lines, dim ──────
|
|
2472
|
+
const recentWorkItems = [];
|
|
2473
|
+
// Add awareness observations as recent work if meaningful
|
|
2474
|
+
if (awarenessLine1 && !awarenessLine1.includes('Ready to work')) {
|
|
2475
|
+
const plainAware1 = awarenessLine1.replace(/\x1b\[[0-9;]*m/g, '').replace(/[︀-️]/g, '').trim();
|
|
2476
|
+
if (plainAware1) recentWorkItems.push({ ok: !plainAware1.startsWith('⚠') && !plainAware1.startsWith('🔴'), text: plainAware1.replace(/^[🔴🟡💡]\s*/, '') });
|
|
2477
|
+
}
|
|
2478
|
+
// Add last commit as a recent work item
|
|
2479
|
+
if (gitLastMsg) {
|
|
2480
|
+
recentWorkItems.push({ ok: true, text: `${gitLastMsg} (${gitLastAgo})` });
|
|
2481
|
+
}
|
|
2482
|
+
// Fill from sessions if still room
|
|
2483
|
+
if (recentWorkItems.length < 3 && recentSessions.length > 0) {
|
|
2484
|
+
const sess = recentSessions[0];
|
|
2485
|
+
let rawName = sess.name || '';
|
|
2486
|
+
if (/^Session [0-9a-f]{8,}$/i.test(rawName)) rawName = sess.id.slice(0, 8);
|
|
2487
|
+
if (rawName) recentWorkItems.push({ ok: true, text: rawName.slice(0, 50) });
|
|
2195
2488
|
}
|
|
2196
2489
|
|
|
2197
2490
|
// ── Resume state detection ────────────────────────────────────────────────
|
|
2198
|
-
let resumeStateRows = [];
|
|
2199
2491
|
const resumeState = await detectResumeState(cwd);
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2492
|
+
|
|
2493
|
+
// ── Determine layout mode ─────────────────────────────────────────────────
|
|
2494
|
+
const anyProviderAvail = claudeAvail || openaiAvail;
|
|
2495
|
+
const isReturning = resumeState.type === 'resumable';
|
|
2496
|
+
|
|
2497
|
+
// ── ANSI color shorthands ─────────────────────────────────────────────────
|
|
2498
|
+
const DIM = '\x1b[2m';
|
|
2499
|
+
const RST = '\x1b[0m';
|
|
2500
|
+
const BOLD = '\x1b[1m';
|
|
2501
|
+
const GRN = '\x1b[32m';
|
|
2502
|
+
const YLW = '\x1b[33m';
|
|
2503
|
+
const RED = '\x1b[31m';
|
|
2504
|
+
const GRY = '\x1b[90m';
|
|
2505
|
+
|
|
2506
|
+
// ── Provider dots ─────────────────────────────────────────────────────────
|
|
2507
|
+
const claudeDot = claudeAvail ? `${GRN}●${RST}` : `${GRY}○${RST}`;
|
|
2508
|
+
const openaiDot = openaiAvail ? `${GRN}●${RST}` : `${GRY}○${RST}`;
|
|
2509
|
+
|
|
2510
|
+
// ── Project name (from package.json or cwd basename) ─────────────────────
|
|
2511
|
+
let projectName = basename(cwd);
|
|
2512
|
+
try {
|
|
2513
|
+
const pkgRaw = readFileSync(join(cwd, 'package.json'), 'utf8');
|
|
2514
|
+
const pkgJson = JSON.parse(pkgRaw);
|
|
2515
|
+
if (pkgJson.name) projectName = pkgJson.name;
|
|
2516
|
+
} catch { /* no package.json */ }
|
|
2517
|
+
|
|
2518
|
+
// ── Separator line ────────────────────────────────────────────────────────
|
|
2519
|
+
const sepW = Math.min(W, 72);
|
|
2520
|
+
const sepLine = `${DIM}${'━'.repeat(sepW)}${RST}`;
|
|
2521
|
+
|
|
2522
|
+
// ── Strip ANSI for width calc ─────────────────────────────────────────────
|
|
2523
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').replace(/[︀-️]/g, '');
|
|
2524
|
+
|
|
2525
|
+
// ── Line 1: status bar ───────────────────────────────────────────────────
|
|
2526
|
+
// " project branch Claude ● GPT ● v0.2.3"
|
|
2527
|
+
const branchStr = `${gitBranch}${uncommittedPart}${aheadPart}`;
|
|
2528
|
+
const providerStr = `Claude ${claudeDot} GPT ${openaiDot}`;
|
|
2529
|
+
const verStr2 = `${DIM}v${version}${RST}`;
|
|
2530
|
+
const statusLeft = ` ${projectName} ${DIM}${branchStr}${RST} ${providerStr}`;
|
|
2531
|
+
const statusRight = verStr2;
|
|
2532
|
+
const statusLeftW = stripAnsi(statusLeft).length;
|
|
2533
|
+
const statusRightW = stripAnsi(statusRight).length;
|
|
2534
|
+
const statusGap = Math.max(1, sepW + 1 - statusLeftW - statusRightW);
|
|
2535
|
+
const statusBar = `${statusLeft}${' '.repeat(statusGap)}${statusRight}`;
|
|
2536
|
+
|
|
2537
|
+
// ── Line 2-3: contextual question + last summary ─────────────────────────
|
|
2538
|
+
let mainQuestion, lastSummary;
|
|
2539
|
+
if (!anyProviderAvail) {
|
|
2540
|
+
mainQuestion = ` ${BOLD}Connect a provider to start working${RST}`;
|
|
2541
|
+
lastSummary = null;
|
|
2542
|
+
} else if (isReturning) {
|
|
2543
|
+
mainQuestion = ` ${BOLD}Resume previous work?${RST}`;
|
|
2544
|
+
const labelTrunc = (resumeState.label || 'last session').slice(0, 45);
|
|
2545
|
+
const agePart = resumeState.ageLabel ? ` · ${resumeState.ageLabel}` : '';
|
|
2207
2546
|
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`);
|
|
2547
|
+
lastSummary = ` ${DIM}Last: ${labelTrunc}${agePart}${nextPart}${RST}`;
|
|
2548
|
+
} else {
|
|
2549
|
+
mainQuestion = ` ${BOLD}What do you want to build?${RST}`;
|
|
2550
|
+
lastSummary = null;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
// ── Suggestions (max 3, bright) ───────────────────────────────────────────
|
|
2554
|
+
let suggestions;
|
|
2555
|
+
const claudeExpiredNow = claudeSub?.expiresAt && Date.parse(claudeSub.expiresAt) < Date.now();
|
|
2556
|
+
const openaiExpiredNow = openaiSub?.expiresAt && Date.parse(openaiSub.expiresAt) < Date.now();
|
|
2557
|
+
if (!anyProviderAvail) {
|
|
2558
|
+
suggestions = ['configure Claude', 'configure GPT', 'browse project'];
|
|
2559
|
+
} else if (claudeExpiredNow || openaiExpiredNow) {
|
|
2560
|
+
const resumeOrBuild = isReturning ? 'resume last session' : 'start building';
|
|
2561
|
+
suggestions = ['refresh auth', resumeOrBuild, 'check project health'];
|
|
2562
|
+
} else if (isReturning) {
|
|
2563
|
+
const openTasks = [];
|
|
2564
|
+
try {
|
|
2565
|
+
const { getOpenTasks } = await import('../src/ledger.mjs');
|
|
2566
|
+
const open = getOpenTasks(cwd);
|
|
2567
|
+
if (open.length > 0) openTasks.push(`continue: ${open[0].intent.slice(0, 30)}`);
|
|
2568
|
+
} catch {}
|
|
2569
|
+
suggestions = openTasks.length > 0
|
|
2570
|
+
? [openTasks[0], 'review changes', 'run tests']
|
|
2571
|
+
: ['resume last session', 'review changes', 'run tests'];
|
|
2572
|
+
} else {
|
|
2573
|
+
suggestions = ['start building', 'explore codebase', 'check project health'];
|
|
2250
2574
|
}
|
|
2575
|
+
const suggestLine = ` ${suggestions.join(' ')}`;
|
|
2251
2576
|
|
|
2252
|
-
//
|
|
2577
|
+
// ── Recent work items (dim, max 3) ────────────────────────────────────────
|
|
2578
|
+
const recentLines = recentWorkItems.slice(0, 3).map(item => {
|
|
2579
|
+
const prefix = item.ok ? `${GRN}✓${RST}` : `${RED}!${RST}`;
|
|
2580
|
+
return ` ${DIM}${prefix} ${item.text}${RST}`;
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2583
|
+
// ── Resolve dashboard spinner before rendering ────────────────────────────
|
|
2253
2584
|
if (dashSpinner) dashSpinner.succeed('Dashboard ready');
|
|
2254
2585
|
|
|
2255
|
-
|
|
2586
|
+
// ── Stale hint ────────────────────────────────────────────────────────────
|
|
2587
|
+
if (staleCount >= 3) {
|
|
2588
|
+
process.stdout.write(`${DIM}${staleCount} stale sessions (>7d) — type "sessions" to manage${RST}\n`);
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// ── Render Studio Console ─────────────────────────────────────────────────
|
|
2592
|
+
const out = [];
|
|
2593
|
+
out.push(''); // breathing room
|
|
2594
|
+
out.push(statusBar); // project branch Claude ● GPT ● v0.2.3
|
|
2595
|
+
out.push('');
|
|
2596
|
+
out.push(mainQuestion); // Resume previous work? / What do you want to build?
|
|
2597
|
+
if (lastSummary) out.push(lastSummary);
|
|
2598
|
+
out.push(` \x1b[1m›\x1b[0m`); // bright prompt cursor
|
|
2599
|
+
out.push('');
|
|
2600
|
+
out.push(suggestLine); // contextual suggestions
|
|
2601
|
+
if (recentLines.length > 0) {
|
|
2602
|
+
out.push('');
|
|
2603
|
+
out.push(...recentLines); // ✓ / ! recent work items
|
|
2604
|
+
}
|
|
2605
|
+
out.push('');
|
|
2606
|
+
out.push(` ${sepLine}`); // ━━━━ separator
|
|
2607
|
+
// Input bar rendered inline — the key handler will overwrite this line
|
|
2608
|
+
out.push(` ${DIM}> task or command...${RST}${' '.repeat(Math.max(1, sepW - 22))}${DIM}[?] help${RST}`);
|
|
2609
|
+
|
|
2610
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
2256
2611
|
|
|
2257
2612
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
2258
2613
|
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
@@ -3081,15 +3436,14 @@ async function prTriageScreen(rl, ask, ctx = {}) {
|
|
|
3081
3436
|
async function settingsScreen(rl, ask) {
|
|
3082
3437
|
const cwd = process.cwd();
|
|
3083
3438
|
|
|
3084
|
-
|
|
3085
|
-
const
|
|
3086
|
-
const
|
|
3087
|
-
const
|
|
3439
|
+
const DIM = '\x1b[2m';
|
|
3440
|
+
const RESET = '\x1b[0m';
|
|
3441
|
+
const GREEN = '\x1b[32m';
|
|
3442
|
+
const RED = '\x1b[31m';
|
|
3443
|
+
const BOLD = '\x1b[1m';
|
|
3088
3444
|
|
|
3089
|
-
const
|
|
3090
|
-
const
|
|
3091
|
-
const bot = `└${'─'.repeat(boxW - 2)}┘`;
|
|
3092
|
-
const row = (content) => makeBoxRow(content, W);
|
|
3445
|
+
const chk = `${GREEN}✓${RESET}`;
|
|
3446
|
+
const xmark = `${RED}✗${RESET}`;
|
|
3093
3447
|
|
|
3094
3448
|
// Detect if gh is available + has PRs for the PR triage option
|
|
3095
3449
|
const settingsPRs = await detectOpenPRs(cwd);
|
|
@@ -3097,108 +3451,131 @@ async function settingsScreen(rl, ask) {
|
|
|
3097
3451
|
// Load current work style
|
|
3098
3452
|
const profile = loadProfile(cwd);
|
|
3099
3453
|
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
3454
|
|
|
3109
3455
|
// Work style current markers
|
|
3110
3456
|
const _stIsFast = ['cost-saver', 'auto', 'solo-claude', 'solo-openai'].includes(currentBias);
|
|
3111
3457
|
const _stIsBal = currentBias === 'balanced';
|
|
3112
3458
|
const _stIsFull = currentBias === 'quality-first';
|
|
3113
|
-
const
|
|
3114
|
-
|
|
3115
|
-
//
|
|
3116
|
-
const
|
|
3117
|
-
const
|
|
3118
|
-
const
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3459
|
+
const dot = (active) => active ? `${GREEN}●${RESET}` : `${DIM}○${RESET}`;
|
|
3460
|
+
|
|
3461
|
+
// ── Subscriptions / credentials ──────────────────────────────────────────
|
|
3462
|
+
const credData = loadCredentials(cwd);
|
|
3463
|
+
const credList = credData.credentials || [];
|
|
3464
|
+
const hasCredRegistry = credList.length > 0;
|
|
3465
|
+
|
|
3466
|
+
// Fall back to detectAuth() when no registry entries yet
|
|
3467
|
+
let subsLines = [];
|
|
3468
|
+
if (hasCredRegistry) {
|
|
3469
|
+
for (const c of credList.filter(c => c.enabled !== false)) {
|
|
3470
|
+
const provLabel = c.provider === 'claude' ? 'Claude' : 'OpenAI';
|
|
3471
|
+
const authLabel = c.auth_type === 'cli_oauth' ? 'CLI OAuth' : 'API key';
|
|
3472
|
+
const planLabel = c.plan_hint || '';
|
|
3473
|
+
const healthMark = c.health === 'healthy' ? chk : c.health === 'degraded' ? `${RED}~${RESET}` : `${DIM}?${RESET}`;
|
|
3474
|
+
const scopeTag = `[${c.scope || 'local'}]`;
|
|
3475
|
+
const planPart = planLabel ? ` ${DIM}${planLabel}${RESET}` : '';
|
|
3476
|
+
subsLines.push(` ${DIM}${provLabel.padEnd(6)}${RESET} ${authLabel.padEnd(10)}${planPart} ${healthMark}${c.health === 'healthy' ? ' healthy' : ' ' + (c.health || 'unknown')} ${DIM}${scopeTag}${RESET}`);
|
|
3477
|
+
}
|
|
3478
|
+
if (subsLines.length === 0) subsLines.push(` ${DIM}none registered${RESET}`);
|
|
3479
|
+
} else {
|
|
3480
|
+
const _stAuth = await detectAuth();
|
|
3481
|
+
const _clStatus = _stAuth.claude.found ? `${chk} connected` : `${xmark} not connected`;
|
|
3482
|
+
const _oaStatus = _stAuth.openai.found ? `${chk} connected` : `${xmark} not connected`;
|
|
3483
|
+
subsLines.push(` ${DIM}Claude${RESET} CLI OAuth ${_clStatus}`);
|
|
3484
|
+
subsLines.push(` ${DIM}OpenAI${RESET} API key ${_oaStatus}`);
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
// ── Work style ───────────────────────────────────────────────────────────
|
|
3488
|
+
const wsLines = [
|
|
3489
|
+
` ${dot(_stIsFast)} ${_stIsFast ? BOLD : DIM}Fast${RESET} speed over caution`,
|
|
3490
|
+
` ${dot(_stIsBal)} ${_stIsBal ? BOLD : DIM}Balanced${RESET} smart routing, reviews on important`,
|
|
3491
|
+
` ${dot(_stIsFull)} ${_stIsFull ? BOLD : DIM}Full Power${RESET} dual-brain everything, max quality`,
|
|
3492
|
+
];
|
|
3493
|
+
|
|
3494
|
+
// ── System info ──────────────────────────────────────────────────────────
|
|
3495
|
+
const rt = detectReplitTools(cwd);
|
|
3496
|
+
const rtLabel = rt.installed ? `v${rt.version || '?'}` : 'not installed';
|
|
3497
|
+
const rtMark = rt.installed ? chk : xmark;
|
|
3498
|
+
|
|
3499
|
+
let sessionCount = 0;
|
|
3128
3500
|
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 */ }
|
|
3501
|
+
const idxPath = join(cwd, '.dualbrain', 'session-index.json');
|
|
3502
|
+
const idx = existsSync(idxPath) ? JSON.parse(readFileSync(idxPath, 'utf8')) : {};
|
|
3503
|
+
sessionCount = Object.keys(idx).length;
|
|
3504
|
+
} catch { /* ignore */ }
|
|
3137
3505
|
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3506
|
+
let pluginCount = 0;
|
|
3507
|
+
try {
|
|
3508
|
+
const settingsJson = join(cwd, '.claude', 'settings.json');
|
|
3509
|
+
if (existsSync(settingsJson)) {
|
|
3510
|
+
const s = JSON.parse(readFileSync(settingsJson, 'utf8'));
|
|
3511
|
+
pluginCount = Object.keys(s?.mcpServers || {}).length;
|
|
3512
|
+
}
|
|
3513
|
+
} catch { /* ignore */ }
|
|
3141
3514
|
|
|
3142
|
-
|
|
3143
|
-
let _stEffScore = null;
|
|
3144
|
-
let _stEffRate = null;
|
|
3145
|
-
let _stEffTrend = null;
|
|
3146
|
-
let _stEffTier = null;
|
|
3515
|
+
let doctorStr = `${DIM}not run${RESET}`;
|
|
3147
3516
|
try {
|
|
3148
|
-
const
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3155
|
-
const
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3517
|
+
const hooksDir = join(cwd, '.claude', 'hooks');
|
|
3518
|
+
const headGuard = existsSync(join(hooksDir, 'head-guard.mjs'));
|
|
3519
|
+
const enforceTier = existsSync(join(hooksDir, 'enforce-tier.mjs'));
|
|
3520
|
+
const settingsFile = join(cwd, '.claude', 'settings.json');
|
|
3521
|
+
let guardCount = 0;
|
|
3522
|
+
if (existsSync(settingsFile)) {
|
|
3523
|
+
const s = JSON.parse(readFileSync(settingsFile, 'utf8'));
|
|
3524
|
+
const ptu = s?.hooks?.PreToolUse ?? [];
|
|
3525
|
+
const gCmd = 'node .claude/hooks/head-guard.mjs';
|
|
3526
|
+
const tCmd = 'node .claude/hooks/enforce-tier.mjs';
|
|
3527
|
+
guardCount = [
|
|
3528
|
+
ptu.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === gCmd)),
|
|
3529
|
+
ptu.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === gCmd)),
|
|
3530
|
+
ptu.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === gCmd)),
|
|
3531
|
+
ptu.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tCmd)),
|
|
3532
|
+
].filter(Boolean).length;
|
|
3160
3533
|
}
|
|
3161
|
-
|
|
3534
|
+
const checks = [headGuard, enforceTier, guardCount >= 4].filter(Boolean).length + 7; // base 7 always pass
|
|
3535
|
+
const total = 10;
|
|
3536
|
+
doctorStr = checks >= total
|
|
3537
|
+
? `${chk} ${checks}/${total} checks passing`
|
|
3538
|
+
: `${RED}${checks}/${total} checks passing${RESET}`;
|
|
3539
|
+
} catch { /* ignore */ }
|
|
3162
3540
|
|
|
3163
|
-
const
|
|
3541
|
+
const sysLines = [
|
|
3542
|
+
` ${DIM}replit-tools${RESET} ${rtLabel} ${rtMark} ${rt.installed ? 'connected' : 'not connected'}`,
|
|
3543
|
+
` ${DIM}Sessions${RESET} ${sessionCount} archived`,
|
|
3544
|
+
` ${DIM}Plugins${RESET} ${pluginCount} configured`,
|
|
3545
|
+
` ${DIM}Doctor${RESET} ${doctorStr}`,
|
|
3546
|
+
];
|
|
3164
3547
|
|
|
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,
|
|
3548
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
3549
|
+
const out = [
|
|
3550
|
+
'',
|
|
3551
|
+
` ${BOLD}Settings${RESET}`,
|
|
3552
|
+
'',
|
|
3553
|
+
` ${DIM}Subscriptions${RESET}`,
|
|
3554
|
+
...subsLines,
|
|
3555
|
+
` ${DIM}[a] add [r] remove [h] health check${RESET}`,
|
|
3556
|
+
'',
|
|
3557
|
+
` ${DIM}Work style${RESET}`,
|
|
3558
|
+
...wsLines,
|
|
3559
|
+
` ${DIM}[1-3] change${RESET}`,
|
|
3560
|
+
'',
|
|
3561
|
+
` ${DIM}System${RESET}`,
|
|
3562
|
+
...sysLines,
|
|
3563
|
+
` ${DIM}[d] run doctor [x] diagnostics${RESET}`,
|
|
3564
|
+
'',
|
|
3565
|
+
` ${DIM}[e] sessions [m] subscriptions [b] back${RESET}`,
|
|
3566
|
+
...(settingsPRs.length > 0 ? [` ${DIM}[p] PR triage (${settingsPRs.length} open)${RESET}`] : []),
|
|
3567
|
+
'',
|
|
3192
3568
|
];
|
|
3193
|
-
process.stdout.write(
|
|
3569
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
3194
3570
|
|
|
3195
3571
|
const raw = (await ask(' Choice: ')).trim();
|
|
3196
3572
|
const choice = raw.toLowerCase();
|
|
3197
3573
|
|
|
3198
|
-
//
|
|
3574
|
+
// Work style 1/2/3
|
|
3199
3575
|
if (choice === '1' || choice === '2' || choice === '3') {
|
|
3200
|
-
const
|
|
3201
|
-
const
|
|
3576
|
+
const wsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
3577
|
+
const wsDisp = { '1': 'Fast', '2': 'Balanced', '3': 'Full Power' };
|
|
3578
|
+
const newBias = wsMap[choice];
|
|
3202
3579
|
if (newBias && newBias !== currentBias) {
|
|
3203
3580
|
profile.bias = newBias;
|
|
3204
3581
|
const enabledCount = [
|
|
@@ -3207,71 +3584,91 @@ async function settingsScreen(rl, ask) {
|
|
|
3207
3584
|
].filter(Boolean).length;
|
|
3208
3585
|
if (enabledCount >= 2) profile.mode = newBias;
|
|
3209
3586
|
saveProfile(profile, { cwd });
|
|
3210
|
-
|
|
3211
|
-
process.stdout.write(`\n Work style set to ${newLabel}\n\n`);
|
|
3587
|
+
process.stdout.write(`\n Work style set to ${wsDisp[choice]}\n\n`);
|
|
3212
3588
|
await ask(' Press Enter to continue...');
|
|
3213
3589
|
}
|
|
3214
3590
|
return { next: 'settings' };
|
|
3215
3591
|
}
|
|
3216
3592
|
|
|
3217
|
-
//
|
|
3218
|
-
if (choice === '
|
|
3593
|
+
// Add credential
|
|
3594
|
+
if (choice === 'a') {
|
|
3595
|
+
process.stdout.write('\n Auto-detecting credentials...\n');
|
|
3219
3596
|
try {
|
|
3220
|
-
const
|
|
3221
|
-
|
|
3222
|
-
|
|
3597
|
+
const discovered = await detectCredentials(cwd);
|
|
3598
|
+
const existing = loadCredentials(cwd).credentials.map(c => c.id);
|
|
3599
|
+
const newOnes = discovered.filter(c => !existing.includes(c.id));
|
|
3600
|
+
if (newOnes.length === 0) {
|
|
3601
|
+
process.stdout.write(' No new credentials detected.\n\n');
|
|
3602
|
+
} else {
|
|
3603
|
+
for (const c of newOnes) {
|
|
3604
|
+
addCredential(c, cwd);
|
|
3605
|
+
process.stdout.write(` Added: ${c.id} (${c.provider} / ${c.auth_type})\n`);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
} catch (e) {
|
|
3609
|
+
process.stdout.write(` Detection failed: ${e.message}\n`);
|
|
3610
|
+
}
|
|
3611
|
+
await ask(' Press Enter to continue...');
|
|
3612
|
+
return { next: 'settings' };
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
// Remove credential
|
|
3616
|
+
if (choice === 'r') {
|
|
3617
|
+
const creds = loadCredentials(cwd).credentials;
|
|
3618
|
+
if (creds.length === 0) {
|
|
3619
|
+
process.stdout.write('\n No credentials registered.\n\n');
|
|
3223
3620
|
await ask(' Press Enter to continue...');
|
|
3224
|
-
|
|
3621
|
+
return { next: 'settings' };
|
|
3622
|
+
}
|
|
3623
|
+
process.stdout.write('\n');
|
|
3624
|
+
creds.forEach((c, i) => process.stdout.write(` [${i + 1}] ${c.id} (${c.provider})\n`));
|
|
3625
|
+
const pick = (await ask('\n Number to remove (or Enter to cancel): ')).trim();
|
|
3626
|
+
const idx = parseInt(pick, 10) - 1;
|
|
3627
|
+
if (idx >= 0 && idx < creds.length) {
|
|
3628
|
+
removeCredential(creds[idx].id, cwd);
|
|
3629
|
+
process.stdout.write(` Removed ${creds[idx].id}\n\n`);
|
|
3630
|
+
}
|
|
3631
|
+
await ask(' Press Enter to continue...');
|
|
3225
3632
|
return { next: 'settings' };
|
|
3226
3633
|
}
|
|
3227
3634
|
|
|
3228
|
-
|
|
3635
|
+
// Health check credentials
|
|
3636
|
+
if (choice === 'h') {
|
|
3637
|
+
process.stdout.write('\n Checking credential health...\n');
|
|
3638
|
+
try {
|
|
3639
|
+
const data = loadCredentials(cwd);
|
|
3640
|
+
const creds = data.credentials || [];
|
|
3641
|
+
if (creds.length === 0) {
|
|
3642
|
+
process.stdout.write(' No credentials to check.\n');
|
|
3643
|
+
} else {
|
|
3644
|
+
const updated = [];
|
|
3645
|
+
for (const c of creds) {
|
|
3646
|
+
const checked = await checkCredentialHealth(c, cwd);
|
|
3647
|
+
const mark = checked.health === 'healthy' ? chk : xmark;
|
|
3648
|
+
process.stdout.write(` ${mark} ${c.id}: ${checked.health}\n`);
|
|
3649
|
+
updated.push(checked);
|
|
3650
|
+
}
|
|
3651
|
+
saveCredentials({ ...data, credentials: updated }, cwd);
|
|
3652
|
+
}
|
|
3653
|
+
} catch (e) {
|
|
3654
|
+
process.stdout.write(` Health check failed: ${e.message}\n`);
|
|
3655
|
+
}
|
|
3656
|
+
await ask('\n Press Enter to continue...');
|
|
3657
|
+
return { next: 'settings' };
|
|
3658
|
+
}
|
|
3229
3659
|
|
|
3660
|
+
if (choice === 'm') { return { next: 'subscriptions' }; }
|
|
3230
3661
|
if (choice === 'e') { return { next: 'sessions' }; }
|
|
3231
|
-
|
|
3232
|
-
if (choice === 'i') {
|
|
3233
|
-
return { next: 'import-picker' };
|
|
3234
|
-
}
|
|
3662
|
+
if (choice === 'x') { return { next: 'diagnostics' }; }
|
|
3235
3663
|
|
|
3236
3664
|
if (choice === 'p' && settingsPRs.length > 0) {
|
|
3237
3665
|
return { next: 'pr-triage', openPRs: settingsPRs };
|
|
3238
3666
|
}
|
|
3239
3667
|
|
|
3240
3668
|
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' };
|
|
3669
|
+
return { next: 'diagnostics' };
|
|
3271
3670
|
}
|
|
3272
3671
|
|
|
3273
|
-
if (choice === 'x') { return { next: 'diagnostics' }; }
|
|
3274
|
-
|
|
3275
3672
|
if (choice === 'b' || choice === 'back' || raw === '\x1b') { return { next: 'main' }; }
|
|
3276
3673
|
|
|
3277
3674
|
return { next: 'main' };
|
|
@@ -3560,9 +3957,37 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
3560
3957
|
// ─── Onboarding Wizard ───────────────────────────────────────────────────────
|
|
3561
3958
|
|
|
3562
3959
|
/**
|
|
3563
|
-
*
|
|
3564
|
-
*
|
|
3565
|
-
|
|
3960
|
+
* Write .dualbrain/credentials.json with detected providers.
|
|
3961
|
+
* Non-destructive: never overwrites entries with the same id.
|
|
3962
|
+
*/
|
|
3963
|
+
function saveWizardCredentials(cwd, detectedProviders) {
|
|
3964
|
+
const dir = join(cwd, '.dualbrain');
|
|
3965
|
+
try { mkdirSync(dir, { recursive: true }); } catch { /* exists */ }
|
|
3966
|
+
|
|
3967
|
+
const credPath = join(dir, 'credentials.json');
|
|
3968
|
+
let existing = { version: 1, credentials: [] };
|
|
3969
|
+
try {
|
|
3970
|
+
const raw = readFileSync(credPath, 'utf8');
|
|
3971
|
+
existing = JSON.parse(raw);
|
|
3972
|
+
if (!Array.isArray(existing.credentials)) existing.credentials = [];
|
|
3973
|
+
} catch { /* fresh start */ }
|
|
3974
|
+
|
|
3975
|
+
const existingIds = new Set(existing.credentials.map(c => c.id));
|
|
3976
|
+
const now = new Date().toISOString();
|
|
3977
|
+
|
|
3978
|
+
for (const cred of detectedProviders) {
|
|
3979
|
+
if (!existingIds.has(cred.id)) {
|
|
3980
|
+
existing.credentials.push({ ...cred, last_checked_at: now });
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
writeFileSync(credPath, JSON.stringify(existing, null, 2), 'utf8');
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
/**
|
|
3988
|
+
* Animated first-run setup wizard — detection-first, 3-interaction flow.
|
|
3989
|
+
* Detection IS the home screen loading: scan → confirm providers → pick style → done.
|
|
3990
|
+
* Uses src/fx.mjs; falls back to plain output stubs.
|
|
3566
3991
|
*
|
|
3567
3992
|
* @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
|
|
3568
3993
|
* @param {string} cwd
|
|
@@ -3570,192 +3995,310 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
3570
3995
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
3571
3996
|
*/
|
|
3572
3997
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
3573
|
-
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
3574
3998
|
const fx = await getFx();
|
|
3999
|
+
const cl = fx.colors || {};
|
|
4000
|
+
const DIM = cl.dim || '';
|
|
4001
|
+
const BOLD = cl.bold || '';
|
|
4002
|
+
const GREEN = cl.green || '';
|
|
4003
|
+
const CYAN = cl.cyan || '';
|
|
4004
|
+
const GRAY = cl.gray || '';
|
|
4005
|
+
const RST = cl.reset || '';
|
|
3575
4006
|
|
|
3576
|
-
|
|
3577
|
-
fx.clearScreen();
|
|
3578
|
-
fx.banner('🧠 DUAL-BRAIN');
|
|
3579
|
-
fx.nl();
|
|
3580
|
-
fx.info("Welcome! Let's set up your AI work partner.");
|
|
3581
|
-
fx.nl();
|
|
3582
|
-
await fx.sleep(800);
|
|
4007
|
+
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
3583
4008
|
|
|
3584
|
-
//
|
|
3585
|
-
|
|
3586
|
-
|
|
4009
|
+
// Helper: print a single dim line (indented with one space)
|
|
4010
|
+
function dimLine(text) {
|
|
4011
|
+
process.stdout.write(` ${GRAY}${text}${RST}\n`);
|
|
4012
|
+
}
|
|
3587
4013
|
|
|
3588
|
-
//
|
|
4014
|
+
// Helper: single-key prompt; falls back to readline if not a real TTY
|
|
4015
|
+
async function singleKey(validKeys) {
|
|
4016
|
+
if (!isTTY) {
|
|
4017
|
+
const line = await new Promise(res => rl.question('', res));
|
|
4018
|
+
return (line.trim().toLowerCase()[0]) || '\r';
|
|
4019
|
+
}
|
|
4020
|
+
const { emitKeypressEvents } = await import('node:readline');
|
|
4021
|
+
emitKeypressEvents(process.stdin, rl);
|
|
4022
|
+
return new Promise((resolve) => {
|
|
4023
|
+
const wasRaw = process.stdin.isRaw;
|
|
4024
|
+
process.stdin.setRawMode(true);
|
|
4025
|
+
const cleanup = () => {
|
|
4026
|
+
process.stdin.removeListener('keypress', onKey);
|
|
4027
|
+
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
4028
|
+
};
|
|
4029
|
+
const onKey = (str, key) => {
|
|
4030
|
+
if (!key) return;
|
|
4031
|
+
const name = key.name || '';
|
|
4032
|
+
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
4033
|
+
cleanup(); process.stdout.write('\n'); resolve('q'); return;
|
|
4034
|
+
}
|
|
4035
|
+
const ch = (str || '').toLowerCase();
|
|
4036
|
+
if (name === 'return' || name === 'enter') {
|
|
4037
|
+
cleanup(); process.stdout.write('\n'); resolve('\r'); return;
|
|
4038
|
+
}
|
|
4039
|
+
if (validKeys.includes(ch)) {
|
|
4040
|
+
cleanup(); process.stdout.write(`${ch}\n`); resolve(ch); return;
|
|
4041
|
+
}
|
|
4042
|
+
};
|
|
4043
|
+
process.stdin.on('keypress', onKey);
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// ─── Clear screen + header ─────────────────────────────────────────────────
|
|
4048
|
+
const version = readVersion();
|
|
4049
|
+
fx.clearScreen();
|
|
4050
|
+
process.stdout.write(`\n ${BOLD}dual-brain${RST}${GRAY} v${version}${RST}\n\n`);
|
|
4051
|
+
process.stdout.write(` ${DIM}Setting up your workspace...${RST}\n\n`);
|
|
4052
|
+
|
|
4053
|
+
// ─── Env scan — run detection in parallel with animated output ────────────
|
|
3589
4054
|
const capsPromise = detectCapabilities(cwd);
|
|
3590
4055
|
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
4056
|
+
// Replit workspace
|
|
4057
|
+
const isReplit = !!(process.env.REPL_ID || process.env.REPL_SLUG);
|
|
4058
|
+
if (isReplit) {
|
|
4059
|
+
await fx.sleep(150);
|
|
4060
|
+
fx.success('Replit workspace detected');
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
// Node version
|
|
4064
|
+
try {
|
|
4065
|
+
const major = process.version.replace(/^v/, '').split('.')[0];
|
|
4066
|
+
await fx.sleep(100);
|
|
4067
|
+
fx.success(`Node ${major}.x found`);
|
|
4068
|
+
} catch { /* non-fatal */ }
|
|
4069
|
+
|
|
4070
|
+
// Git repo name, branch, file count
|
|
4071
|
+
let repoName = null;
|
|
4072
|
+
let branchName = null;
|
|
4073
|
+
let fileCount = 0;
|
|
4074
|
+
try {
|
|
4075
|
+
const { spawnSync: sp } = await import('node:child_process');
|
|
4076
|
+
const topLevel = sp('git', ['rev-parse', '--show-toplevel'], {
|
|
4077
|
+
cwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 3000,
|
|
4078
|
+
});
|
|
4079
|
+
if (topLevel.status === 0) repoName = basename((topLevel.stdout || '').trim());
|
|
4080
|
+
|
|
4081
|
+
const branch = sp('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
4082
|
+
cwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 2000,
|
|
4083
|
+
});
|
|
4084
|
+
branchName = (branch.stdout || '').trim() || null;
|
|
4085
|
+
|
|
4086
|
+
const count = sp('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
|
|
4087
|
+
cwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 3000,
|
|
4088
|
+
});
|
|
4089
|
+
fileCount = (count.stdout || '').trim().split('\n').filter(Boolean).length;
|
|
4090
|
+
} catch { /* not a git repo or git unavailable */ }
|
|
3596
4091
|
|
|
3597
|
-
|
|
3598
|
-
|
|
4092
|
+
if (repoName) {
|
|
4093
|
+
const fileLabel = fileCount > 0 ? `, ${fileCount} file${fileCount === 1 ? '' : 's'}` : '';
|
|
4094
|
+
const branchLabel = branchName ? ` (${branchName} branch${fileLabel})` : '';
|
|
4095
|
+
await fx.sleep(100);
|
|
4096
|
+
fx.success(`Git repository: ${repoName}${branchLabel}`);
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
// Provider spinner while awaiting detection
|
|
4100
|
+
const provSpinner = fx.spinner('Checking providers...').start();
|
|
4101
|
+
const caps = await capsPromise;
|
|
3599
4102
|
const claudeReady = caps.claude.available;
|
|
3600
4103
|
const openaiReady = caps.openai.available;
|
|
3601
4104
|
const codexAvailable = caps.codex.available;
|
|
4105
|
+
provSpinner.stop();
|
|
3602
4106
|
|
|
3603
|
-
//
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
4107
|
+
// Claude
|
|
4108
|
+
let claudeAuthLabel = null;
|
|
4109
|
+
let claudeAuthType = null;
|
|
4110
|
+
if (claudeReady) {
|
|
4111
|
+
if (caps.claude.source === 'claude-code') {
|
|
4112
|
+
claudeAuthLabel = 'CLI OAuth'; claudeAuthType = 'cli_oauth';
|
|
4113
|
+
} else if (caps.claude.source === 'env-key') {
|
|
4114
|
+
claudeAuthLabel = 'API key'; claudeAuthType = 'api_key';
|
|
4115
|
+
} else {
|
|
4116
|
+
claudeAuthLabel = caps.claude.source || 'detected'; claudeAuthType = 'unknown';
|
|
4117
|
+
}
|
|
4118
|
+
fx.success(`Claude CLI found · ${claudeAuthLabel}`);
|
|
4119
|
+
}
|
|
3609
4120
|
|
|
3610
|
-
//
|
|
3611
|
-
|
|
3612
|
-
|
|
4121
|
+
// OpenAI / Codex
|
|
4122
|
+
let openaiAuthLabel = null;
|
|
4123
|
+
let openaiAuthType = null;
|
|
4124
|
+
if (openaiReady) {
|
|
4125
|
+
openaiAuthLabel = 'API key'; openaiAuthType = 'api_key';
|
|
4126
|
+
fx.success('OpenAI detected · API key');
|
|
4127
|
+
} else if (codexAvailable) {
|
|
4128
|
+
openaiAuthLabel = 'CLI OAuth'; openaiAuthType = 'cli_oauth';
|
|
4129
|
+
fx.success('OpenAI Codex CLI found · authenticated');
|
|
4130
|
+
}
|
|
3613
4131
|
|
|
4132
|
+
// replit-tools — auto-import sessions (non-destructive read-only indexing, no prompt)
|
|
3614
4133
|
const rt = detectReplitTools(cwd);
|
|
3615
|
-
const rtSpinner = fx.spinner('Looking for replit-tools...').start();
|
|
3616
|
-
await fx.sleep(700);
|
|
3617
|
-
|
|
3618
4134
|
let rtSessionCount = 0;
|
|
3619
4135
|
if (rt.installed) {
|
|
3620
|
-
const vStr = rt.version ? ` v${rt.version}` : '';
|
|
3621
|
-
rtSpinner.succeed(`replit-tools${vStr} detected`);
|
|
3622
|
-
// Count available sessions
|
|
3623
4136
|
try {
|
|
3624
4137
|
const sessions = importReplitSessions(cwd);
|
|
3625
4138
|
rtSessionCount = sessions.length;
|
|
3626
4139
|
} catch { /* non-fatal */ }
|
|
3627
|
-
|
|
3628
|
-
|
|
4140
|
+
if (rtSessionCount > 0) {
|
|
4141
|
+
fx.success(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} found in replit-tools`);
|
|
4142
|
+
}
|
|
4143
|
+
const vStr = rt.version ? `v${rt.version}` : 'installed';
|
|
4144
|
+
fx.success(`replit-tools ${vStr} detected`);
|
|
3629
4145
|
}
|
|
3630
|
-
fx.nl();
|
|
3631
|
-
|
|
3632
|
-
// ─── Step 4: Import conversations ─────────────────────────────────────────
|
|
3633
|
-
fx.step(3, 5, 'Import conversations');
|
|
3634
|
-
fx.nl();
|
|
3635
4146
|
|
|
3636
|
-
|
|
3637
|
-
fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
|
|
3638
|
-
fx.nl();
|
|
4147
|
+
process.stdout.write('\n');
|
|
3639
4148
|
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
const importChoice = (await ask('')).trim().toLowerCase();
|
|
4149
|
+
// ─── Step 1: Confirm providers ────────────────────────────────────────────
|
|
4150
|
+
const hasAnyProvider = claudeReady || openaiReady || codexAvailable;
|
|
3643
4151
|
|
|
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();
|
|
4152
|
+
if (!hasAnyProvider) {
|
|
4153
|
+
// No-providers path
|
|
4154
|
+
process.stdout.write(` ${BOLD}No providers detected${RST}\n\n`);
|
|
4155
|
+
dimLine('dual-brain needs Claude or OpenAI to run coding tasks.');
|
|
4156
|
+
dimLine('You can still browse your project and configure settings.');
|
|
4157
|
+
process.stdout.write('\n');
|
|
4158
|
+
process.stdout.write(` ${GRAY}[c]${RST} set up Claude ${GRAY}[o]${RST} set up OpenAI ${GRAY}[s]${RST} skip for now\n\n`);
|
|
3662
4159
|
|
|
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();
|
|
4160
|
+
const noProvChoice = await singleKey(['c', 'o', 's', '\r']);
|
|
3671
4161
|
|
|
3672
|
-
|
|
3673
|
-
|
|
4162
|
+
if (noProvChoice === 'c') {
|
|
4163
|
+
process.stdout.write('\n');
|
|
4164
|
+
dimLine('Run: claude login');
|
|
4165
|
+
dimLine('Then re-run: dual-brain init');
|
|
4166
|
+
process.stdout.write('\n');
|
|
4167
|
+
} else if (noProvChoice === 'o') {
|
|
4168
|
+
process.stdout.write('\n');
|
|
4169
|
+
dimLine('Run: codex login');
|
|
4170
|
+
dimLine('Or add OPENAI_API_KEY to Replit Secrets if using API key auth.');
|
|
4171
|
+
dimLine('Then re-run: dual-brain init');
|
|
4172
|
+
process.stdout.write('\n');
|
|
4173
|
+
}
|
|
3674
4174
|
|
|
3675
|
-
|
|
3676
|
-
|
|
4175
|
+
const minProfile = loadProfile(cwd);
|
|
4176
|
+
minProfile.setupComplete = true;
|
|
4177
|
+
minProfile.providers.claude = { enabled: false };
|
|
4178
|
+
minProfile.providers.openai = { enabled: false };
|
|
4179
|
+
minProfile.mode = 'solo-claude';
|
|
4180
|
+
minProfile.bias = 'balanced';
|
|
4181
|
+
minProfile.workStyle = 'balanced';
|
|
4182
|
+
return minProfile;
|
|
4183
|
+
}
|
|
3677
4184
|
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
4185
|
+
// Show provider table
|
|
4186
|
+
process.stdout.write(` ${BOLD}Providers detected:${RST}\n\n`);
|
|
4187
|
+
if (claudeReady) {
|
|
4188
|
+
process.stdout.write(` ${GRAY}Claude${RST} ${claudeAuthLabel} ${GREEN}✓ authenticated${RST}\n`);
|
|
4189
|
+
}
|
|
4190
|
+
if (openaiReady) {
|
|
4191
|
+
process.stdout.write(` ${GRAY}OpenAI${RST} API key ${GREEN}✓ OPENAI_API_KEY${RST}\n`);
|
|
4192
|
+
} else if (codexAvailable) {
|
|
4193
|
+
process.stdout.write(` ${GRAY}OpenAI${RST} CLI OAuth ${GREEN}✓ authenticated${RST}\n`);
|
|
4194
|
+
}
|
|
3682
4195
|
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
const wasRaw = process.stdin.isRaw;
|
|
3686
|
-
process.stdin.setRawMode(true);
|
|
4196
|
+
process.stdout.write('\n');
|
|
4197
|
+
process.stdout.write(` ${GRAY}Correct?${RST} ${GRAY}[Enter]${RST} yes ${GRAY}[n]${RST} change ${GRAY}[a]${RST} add more\n\n`);
|
|
3687
4198
|
|
|
3688
|
-
|
|
3689
|
-
process.stdin.removeListener('keypress', onKey);
|
|
3690
|
-
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
3691
|
-
};
|
|
4199
|
+
const provChoice = await singleKey(['n', 'a', '\r', 'y']);
|
|
3692
4200
|
|
|
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
|
-
};
|
|
4201
|
+
let finalClaudeEnabled = claudeReady;
|
|
4202
|
+
let finalOpenaiEnabled = openaiReady || codexAvailable;
|
|
3715
4203
|
|
|
3716
|
-
|
|
4204
|
+
if (provChoice === 'n') {
|
|
4205
|
+
process.stdout.write('\n');
|
|
4206
|
+
const toggleOpts = [];
|
|
4207
|
+
if (claudeReady) toggleOpts.push(`${GRAY}[c]${RST} disable Claude`);
|
|
4208
|
+
if (openaiReady || codexAvailable) toggleOpts.push(`${GRAY}[o]${RST} disable OpenAI`);
|
|
4209
|
+
toggleOpts.push(`${GRAY}[Enter]${RST} keep`);
|
|
4210
|
+
process.stdout.write(` ${toggleOpts.join(' ')}\n\n`);
|
|
4211
|
+
const toggleChoice = await singleKey(['c', 'o', '\r']);
|
|
4212
|
+
if (toggleChoice === 'c') finalClaudeEnabled = false;
|
|
4213
|
+
if (toggleChoice === 'o') finalOpenaiEnabled = false;
|
|
4214
|
+
process.stdout.write('\n');
|
|
4215
|
+
} else if (provChoice === 'a') {
|
|
4216
|
+
process.stdout.write('\n');
|
|
4217
|
+
if (!claudeReady) dimLine('Claude: run `claude auth login` to authenticate');
|
|
4218
|
+
if (!openaiReady && !codexAvailable) dimLine('OpenAI: set OPENAI_API_KEY or run `codex login`');
|
|
4219
|
+
process.stdout.write('\n');
|
|
4220
|
+
process.stdout.write(` ${GRAY}[Enter]${RST} continue with current providers\n\n`);
|
|
4221
|
+
await singleKey(['\r', 'q']);
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
// Write credentials.json
|
|
4225
|
+
const credEntries = [];
|
|
4226
|
+
if (finalClaudeEnabled) {
|
|
4227
|
+
credEntries.push({
|
|
4228
|
+
id: 'claude-local',
|
|
4229
|
+
provider: 'claude',
|
|
4230
|
+
auth_type: claudeAuthType || 'cli_oauth',
|
|
4231
|
+
source: 'local_cli',
|
|
4232
|
+
owner: 'user',
|
|
4233
|
+
scope: 'local',
|
|
4234
|
+
plan_hint: null,
|
|
4235
|
+
enabled: true,
|
|
4236
|
+
health: 'healthy',
|
|
3717
4237
|
});
|
|
3718
|
-
} else {
|
|
3719
|
-
// Fallback: line-based prompt
|
|
3720
|
-
process.stdout.write(' Choice [2]: ');
|
|
3721
|
-
styleChoice = (await ask('')).trim() || '2';
|
|
3722
4238
|
}
|
|
4239
|
+
if (finalOpenaiEnabled) {
|
|
4240
|
+
credEntries.push({
|
|
4241
|
+
id: openaiReady ? 'openai-apikey' : 'openai-codex',
|
|
4242
|
+
provider: 'openai',
|
|
4243
|
+
auth_type: openaiAuthType || 'api_key',
|
|
4244
|
+
source: openaiReady ? 'env_var' : 'cli_oauth',
|
|
4245
|
+
owner: 'user',
|
|
4246
|
+
scope: 'local',
|
|
4247
|
+
plan_hint: null,
|
|
4248
|
+
enabled: true,
|
|
4249
|
+
health: 'healthy',
|
|
4250
|
+
});
|
|
4251
|
+
}
|
|
4252
|
+
try { saveWizardCredentials(cwd, credEntries); } catch { /* non-fatal */ }
|
|
4253
|
+
|
|
4254
|
+
// ─── Step 2: Work style ───────────────────────────────────────────────────
|
|
4255
|
+
process.stdout.write(` ${BOLD}Choose your work style:${RST}\n\n`);
|
|
4256
|
+
process.stdout.write(` ${CYAN}●${RST} Auto (recommended) — adapts to each task\n`);
|
|
4257
|
+
process.stdout.write(` ${GRAY}○${RST} Quality-first — deeper review, stronger models\n`);
|
|
4258
|
+
process.stdout.write(` ${GRAY}○${RST} Cost-saver — lighter models, lower cost\n`);
|
|
4259
|
+
process.stdout.write('\n');
|
|
4260
|
+
process.stdout.write(` ${GRAY}[Enter]${RST} Auto ${GRAY}[1-3]${RST} select\n\n`);
|
|
3723
4261
|
|
|
3724
|
-
const
|
|
3725
|
-
const
|
|
3726
|
-
|
|
4262
|
+
const styleKey = await singleKey(['1', '2', '3', '\r']);
|
|
4263
|
+
const styleMap = { '1': 'auto', '2': 'quality-first', '3': 'cost-saver', '\r': 'auto' };
|
|
4264
|
+
const chosenBias = styleMap[styleKey] || 'auto';
|
|
3727
4265
|
|
|
3728
|
-
//
|
|
4266
|
+
// Metered API note (non-blocking)
|
|
3729
4267
|
if (openaiReady && caps.openai.metered) {
|
|
3730
|
-
|
|
3731
|
-
|
|
4268
|
+
process.stdout.write('\n');
|
|
4269
|
+
dimLine('OpenAI API key detected — usage is metered, guardrails enabled');
|
|
3732
4270
|
}
|
|
3733
4271
|
|
|
3734
|
-
|
|
3735
|
-
fx.step(5, 5, 'Ready!');
|
|
3736
|
-
fx.nl();
|
|
4272
|
+
process.stdout.write('\n');
|
|
3737
4273
|
|
|
3738
|
-
// Init living docs
|
|
4274
|
+
// Init living docs (non-fatal)
|
|
3739
4275
|
try {
|
|
3740
4276
|
const ld = await getLivingDocs();
|
|
3741
4277
|
if (ld.initLivingDocs) ld.initLivingDocs(cwd);
|
|
3742
4278
|
} catch { /* non-fatal */ }
|
|
3743
4279
|
|
|
4280
|
+
// ─── Step 3: Done — seamless transition line before dashboard renders ─────
|
|
4281
|
+
const termWidth = process.stdout.columns || 72;
|
|
4282
|
+
const divider = '━'.repeat(Math.min(termWidth - 2, 57));
|
|
4283
|
+
process.stdout.write(` ${GRAY}${divider}${RST}\n`);
|
|
4284
|
+
|
|
4285
|
+
const providerCount = [finalClaudeEnabled, finalOpenaiEnabled].filter(Boolean).length;
|
|
4286
|
+
const sessionLabel = rtSessionCount > 0 ? ` · ${rtSessionCount} sessions imported` : '';
|
|
4287
|
+
process.stdout.write(` ${GREEN}✓${RST} Setup complete · ${providerCount} provider${providerCount === 1 ? '' : 's'}${sessionLabel}\n`);
|
|
4288
|
+
process.stdout.write('\n');
|
|
4289
|
+
|
|
3744
4290
|
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
4291
|
|
|
3750
4292
|
// ─── Build and return the profile object ──────────────────────────────────
|
|
3751
4293
|
const finalProfile = loadProfile(cwd);
|
|
3752
4294
|
|
|
3753
|
-
finalProfile.providers.claude = { enabled:
|
|
3754
|
-
finalProfile.providers.openai = { enabled:
|
|
4295
|
+
finalProfile.providers.claude = { enabled: finalClaudeEnabled };
|
|
4296
|
+
finalProfile.providers.openai = { enabled: finalOpenaiEnabled };
|
|
3755
4297
|
finalProfile.apiGuardrail = caps.openai.metered;
|
|
4298
|
+
finalProfile.setupComplete = true;
|
|
3756
4299
|
|
|
3757
|
-
const enabledCount = [
|
|
3758
|
-
finalProfile.mode = enabledCount >= 2 ? 'dual' :
|
|
4300
|
+
const enabledCount = [finalClaudeEnabled, finalOpenaiEnabled].filter(Boolean).length;
|
|
4301
|
+
finalProfile.mode = enabledCount >= 2 ? 'dual' : finalClaudeEnabled ? 'solo-claude' : 'solo-openai';
|
|
3759
4302
|
finalProfile.bias = chosenBias;
|
|
3760
4303
|
finalProfile.workStyle = chosenBias;
|
|
3761
4304
|
|
|
@@ -5341,7 +5884,7 @@ async function main() {
|
|
|
5341
5884
|
if (wizardProfile) {
|
|
5342
5885
|
saveProfile(wizardProfile, { cwd });
|
|
5343
5886
|
await cmdInstall(cwd);
|
|
5344
|
-
|
|
5887
|
+
// (wizard already printed setup-complete line)
|
|
5345
5888
|
}
|
|
5346
5889
|
rl.close();
|
|
5347
5890
|
await runScreens('main');
|
|
@@ -5372,6 +5915,30 @@ async function main() {
|
|
|
5372
5915
|
}
|
|
5373
5916
|
|
|
5374
5917
|
if (cmd === 'init') {
|
|
5918
|
+
// init --reset: clear credentials.json and re-run wizard
|
|
5919
|
+
if (args.includes('--reset')) {
|
|
5920
|
+
const cwd = process.cwd();
|
|
5921
|
+
const credPath = join(cwd, '.dualbrain', 'credentials.json');
|
|
5922
|
+
try {
|
|
5923
|
+
if (existsSync(credPath)) {
|
|
5924
|
+
unlinkSync(credPath);
|
|
5925
|
+
console.log(' ✓ credentials.json cleared');
|
|
5926
|
+
}
|
|
5927
|
+
// Also clear setupComplete so wizard re-runs
|
|
5928
|
+
const profilePath = join(cwd, '.dualbrain', 'profile.json');
|
|
5929
|
+
if (existsSync(profilePath)) {
|
|
5930
|
+
const p = JSON.parse(readFileSync(profilePath, 'utf8'));
|
|
5931
|
+
delete p.setupComplete;
|
|
5932
|
+
writeFileSync(profilePath, JSON.stringify(p, null, 2), 'utf8');
|
|
5933
|
+
console.log(' ✓ profile reset — wizard will re-run');
|
|
5934
|
+
}
|
|
5935
|
+
} catch (e) {
|
|
5936
|
+
console.error(' Error during reset:', e.message);
|
|
5937
|
+
}
|
|
5938
|
+
if (!isInteractive) return;
|
|
5939
|
+
// Fall through to run the wizard interactively
|
|
5940
|
+
}
|
|
5941
|
+
|
|
5375
5942
|
// init --replit: run Replit-specific integration setup
|
|
5376
5943
|
if (args.includes('--replit')) {
|
|
5377
5944
|
const cwd = process.cwd();
|
|
@@ -5399,7 +5966,7 @@ async function main() {
|
|
|
5399
5966
|
if (wizardProfile) {
|
|
5400
5967
|
saveProfile(wizardProfile, { cwd });
|
|
5401
5968
|
await cmdInstall(cwd);
|
|
5402
|
-
|
|
5969
|
+
// (wizard already printed setup-complete line)
|
|
5403
5970
|
}
|
|
5404
5971
|
rl.close();
|
|
5405
5972
|
await runScreens('main');
|
|
@@ -5410,7 +5977,16 @@ async function main() {
|
|
|
5410
5977
|
}
|
|
5411
5978
|
|
|
5412
5979
|
// One-shot commands — run and exit
|
|
5413
|
-
if (cmd === 'install')
|
|
5980
|
+
if (cmd === 'install') {
|
|
5981
|
+
if (args.includes('--global')) { await installGlobal(); return; }
|
|
5982
|
+
await cmdInstall();
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
if (cmd === 'uninstall') {
|
|
5986
|
+
if (args.includes('--global')) { await uninstallGlobal(); return; }
|
|
5987
|
+
console.log('Usage: dual-brain uninstall --global');
|
|
5988
|
+
return;
|
|
5989
|
+
}
|
|
5414
5990
|
if (cmd === 'auth') {
|
|
5415
5991
|
await cmdAuth(args.slice(1));
|
|
5416
5992
|
return;
|
|
@@ -5421,6 +5997,7 @@ async function main() {
|
|
|
5421
5997
|
if (cmd === 'think') { await cmdThink(args.slice(1)); return; }
|
|
5422
5998
|
if (cmd === 'review') { await cmdReview(args.slice(1)); return; }
|
|
5423
5999
|
if (cmd === 'ship') { await cmdShip(); return; }
|
|
6000
|
+
if (cmd === 'pr') { await cmdPR(args.slice(1)); return; }
|
|
5424
6001
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
5425
6002
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
5426
6003
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
|
@@ -5485,7 +6062,7 @@ fi
|
|
|
5485
6062
|
// If cmd is not a recognized subcommand, treat the entire arg list as a task.
|
|
5486
6063
|
// e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
|
|
5487
6064
|
const KNOWN_COMMANDS = new Set([
|
|
5488
|
-
'init', 'install', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'status', 'hot', 'cool',
|
|
6065
|
+
'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'hot', 'cool',
|
|
5489
6066
|
'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch',
|
|
5490
6067
|
'--help', '-h', '--version', '-v',
|
|
5491
6068
|
...Object.keys(loadSpecialistRegistry()),
|