dual-brain 0.1.12 → 0.1.14
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 +229 -167
- package/package.json +1 -1
- package/src/decide.mjs +2 -3
- package/src/profile.mjs +4 -4
package/bin/dual-brain.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// dual-brain — CLI entry point. Commands: init, go, status, remember, forget
|
|
2
|
+
// dual-brain — CLI entry point. Commands: init, go, think, review, status, remember, forget
|
|
3
3
|
|
|
4
4
|
import { appendFileSync, existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync, unlinkSync, watch as fsWatch } from 'node:fs';
|
|
5
5
|
import { join, dirname, basename, extname } from 'node:path';
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
rememberPreference, forgetPreference, getActivePreferences,
|
|
13
13
|
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
14
14
|
detectAuth, detectEnvironment, detectPlans,
|
|
15
|
+
detectCapabilities,
|
|
15
16
|
saveSubscription, listSubscriptions,
|
|
16
17
|
autoSetup,
|
|
17
18
|
} from '../src/profile.mjs';
|
|
@@ -28,6 +29,8 @@ import {
|
|
|
28
29
|
|
|
29
30
|
import { dispatch, detectRuntime, dispatchDualBrain } from '../src/dispatch.mjs';
|
|
30
31
|
|
|
32
|
+
import { runPipeline, buildExecutionPlan, formatExecutionPlan } from '../src/pipeline.mjs';
|
|
33
|
+
|
|
31
34
|
import { loadRepoCache } from '../src/repo.mjs';
|
|
32
35
|
import { loadSession, saveSession, formatSessionCard, importReplitSessions, getSessionMeta, saveSessionMeta, renameSession, pinSession, unpinSession, categorizeSession, enrichSessions, archiveSession, getArchivedSessions } from '../src/session.mjs';
|
|
33
36
|
|
|
@@ -156,7 +159,7 @@ dual-brain <command> [options]
|
|
|
156
159
|
|
|
157
160
|
Commands:
|
|
158
161
|
init First-time setup → flows into interactive REPL
|
|
159
|
-
auth Show
|
|
162
|
+
auth Show provider login and plan status
|
|
160
163
|
install Install Claude Code hooks into the current project
|
|
161
164
|
go "task description" Detect → decide → dispatch a task
|
|
162
165
|
--dry-run Show routing decision without executing
|
|
@@ -220,7 +223,7 @@ function detectReplitTools(cwd) {
|
|
|
220
223
|
// ─── Subscription status table ────────────────────────────────────────────────
|
|
221
224
|
|
|
222
225
|
/**
|
|
223
|
-
* Print a
|
|
226
|
+
* Print a provider status table to stdout.
|
|
224
227
|
*/
|
|
225
228
|
function printSubscriptionTable(auth, profile) {
|
|
226
229
|
const W = 55;
|
|
@@ -234,10 +237,10 @@ function printSubscriptionTable(auth, profile) {
|
|
|
234
237
|
const openaiSub = profile?.providers?.openai;
|
|
235
238
|
|
|
236
239
|
const claudePlanLabel = claudeSub?.enabled
|
|
237
|
-
? ({ pro: 'Pro
|
|
240
|
+
? ({ pro: 'Pro', max5: 'Max x5', max20: 'Max x20', '$20': 'Pro', '$100': 'Max x5', '$200': 'Max x20' }[claudeSub.plan] ?? claudeSub.plan) // doctor:verified — config value lookup
|
|
238
241
|
: 'disabled';
|
|
239
242
|
const openaiPlanLabel = openaiSub?.enabled
|
|
240
|
-
? ({ plus: 'Plus
|
|
243
|
+
? ({ plus: 'Plus', pro: 'Pro', pro100: 'Pro', pro200: 'Pro (higher limits)', '$20': 'Plus', '$100': 'Pro', '$200': 'Pro (higher limits)' }[openaiSub.plan] ?? openaiSub.plan) // doctor:verified — config value lookup
|
|
241
244
|
: 'disabled';
|
|
242
245
|
|
|
243
246
|
const claudeLabel = claudeSub?.label ? ` [${claudeSub.label}]` : '';
|
|
@@ -254,7 +257,7 @@ function printSubscriptionTable(auth, profile) {
|
|
|
254
257
|
const openaiLine2 = ` plan: ${openaiPlanLabel}${openaiLabel}`;
|
|
255
258
|
|
|
256
259
|
console.log(`╔${hbar}╗`);
|
|
257
|
-
console.log(`║${pad('
|
|
260
|
+
console.log(`║${pad(' Provider Status')}║`);
|
|
258
261
|
console.log(`╠${hbar}╣`);
|
|
259
262
|
console.log(`║${pad(claudeLine1)}║`);
|
|
260
263
|
console.log(`║${pad(claudeLine2)}║`);
|
|
@@ -299,7 +302,7 @@ async function cmdInit(rl) {
|
|
|
299
302
|
}
|
|
300
303
|
|
|
301
304
|
/**
|
|
302
|
-
* Show
|
|
305
|
+
* Show provider login and plan status.
|
|
303
306
|
*/
|
|
304
307
|
async function cmdAuth(subArgs = []) {
|
|
305
308
|
const auth = await detectAuth();
|
|
@@ -325,89 +328,57 @@ async function cmdGo(args) {
|
|
|
325
328
|
const prompt = args.find(a => !a.startsWith('--') && !a.startsWith('-') && a !== (filesRaw ?? ''));
|
|
326
329
|
if (!prompt) err('Usage: dual-brain go "task description" [--dry-run] [--files a,b] [--verbose]');
|
|
327
330
|
|
|
328
|
-
const cwd
|
|
329
|
-
|
|
330
|
-
const detection = detectTask({ prompt, files });
|
|
331
|
-
|
|
332
|
-
// Print the one-sentence classification
|
|
333
|
-
console.log(detection.explanation);
|
|
334
|
-
|
|
335
|
-
// Verbose: emit detection trace before routing decision
|
|
336
|
-
if (verbose) {
|
|
337
|
-
vtrace(`Intent: ${detection.intent} | Risk: ${detection.risk} | Complexity: ${detection.complexity} | Effort: ${detection.effort ?? 'n/a'}`);
|
|
338
|
-
vtrace(`Tier: ${detection.tier} | Files: ${detection.fileCount ?? files.length} | Requires write: ${detection.requiresWrite}`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Verbose: emit provider health scores before dispatch
|
|
342
|
-
if (verbose) {
|
|
343
|
-
const providers = getAvailableProviders(profile);
|
|
344
|
-
const { states } = getHealth(cwd);
|
|
345
|
-
const providerScores = ['claude', 'openai'].map(name => {
|
|
346
|
-
const enabled = providers.some(p => p.name === name);
|
|
347
|
-
if (!enabled) return `${name}=unavailable`;
|
|
348
|
-
// Find any state entry for this provider
|
|
349
|
-
const statuses = Object.entries(states)
|
|
350
|
-
.filter(([k]) => k.startsWith(`${name}:`))
|
|
351
|
-
.map(([, v]) => v.status);
|
|
352
|
-
const worst = statuses.includes('hot') ? 'hot'
|
|
353
|
-
: statuses.includes('probing') ? 'probing'
|
|
354
|
-
: statuses.includes('degraded') ? 'degraded'
|
|
355
|
-
: 'healthy';
|
|
356
|
-
return `${name}=${worst}`;
|
|
357
|
-
}).join(' ');
|
|
358
|
-
vtrace(`Provider health: ${providerScores}`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const decision = decideRoute({ profile, detection, cwd });
|
|
331
|
+
const cwd = process.cwd();
|
|
332
|
+
await ensureProfile(cwd);
|
|
362
333
|
|
|
363
|
-
|
|
364
|
-
if (verbose) {
|
|
365
|
-
const modelLabel = decision.effort ? `${decision.model} (${decision.effort})` : decision.model;
|
|
366
|
-
const modelStatus = getAvailableModels(profile)[decision.provider]?.includes(decision.model)
|
|
367
|
-
? 'available, matches tier'
|
|
368
|
-
: 'selected';
|
|
369
|
-
vtrace(`Model selection: ${modelLabel} (${modelStatus})`);
|
|
370
|
-
vtrace(`Dual-brain: ${decision.dualBrain ? 'yes' : 'no'} (${isSoloBrain(profile) ? 'solo provider' : 'dual provider'}, ${detection.risk} risk)`);
|
|
371
|
-
}
|
|
334
|
+
if (verbose) console.log('\nDispatching...');
|
|
372
335
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
console.log(` reason : ${decision.explanation}`);
|
|
380
|
-
}
|
|
336
|
+
const { plan, result } = await runPipeline('go', prompt, {
|
|
337
|
+
files,
|
|
338
|
+
cwd,
|
|
339
|
+
verbose,
|
|
340
|
+
dryRun,
|
|
341
|
+
});
|
|
381
342
|
|
|
382
343
|
if (dryRun) {
|
|
344
|
+
// formatExecutionPlan already printed by pipeline when verbose/dryRun=true
|
|
383
345
|
console.log('\n(dry-run — not executing)');
|
|
384
346
|
return;
|
|
385
347
|
}
|
|
386
348
|
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
349
|
+
if (!result) return;
|
|
350
|
+
|
|
351
|
+
// Display result — dual-brain vs single-provider
|
|
352
|
+
if (result.consensus) {
|
|
391
353
|
console.log(`\nConsensus: ${result.consensus}`);
|
|
392
354
|
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
393
355
|
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
394
|
-
// Save session state
|
|
395
356
|
saveSession({
|
|
396
357
|
objective: prompt,
|
|
397
358
|
branch: null,
|
|
398
359
|
filesChanged: files,
|
|
399
360
|
commandsRun: [`dual-brain go "${prompt}"`],
|
|
400
361
|
lastResult: { status: 'success', summary: result.consensus || 'dual-brain complete' },
|
|
401
|
-
provider:
|
|
362
|
+
provider: plan?._decision?.provider ?? 'claude',
|
|
402
363
|
nextAction: null,
|
|
403
364
|
}, cwd);
|
|
365
|
+
// ── Next steps suggestions (dual-brain consensus path) ──────────────────
|
|
366
|
+
try {
|
|
367
|
+
const { suggestNextSteps, formatNextSteps } = await import('../src/nextstep.mjs');
|
|
368
|
+
const steps = await suggestNextSteps(
|
|
369
|
+
{ prompt, tier: plan?._decision?.tier ?? 'think', files, trigger: 'go' },
|
|
370
|
+
{ success: true, filesChanged: files, error: null, duration: null },
|
|
371
|
+
cwd
|
|
372
|
+
);
|
|
373
|
+
if (steps?.steps?.length > 0) {
|
|
374
|
+
console.log('\n' + formatNextSteps(steps.steps, 3));
|
|
375
|
+
}
|
|
376
|
+
} catch { /* non-fatal */ }
|
|
404
377
|
} else {
|
|
405
|
-
result = await dispatch({ decision, prompt, files, cwd, verbose });
|
|
406
378
|
const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
407
|
-
console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
379
|
+
console.log(`\n${statusLine}${result.durationMs != null ? ` in ${(result.durationMs / 1000).toFixed(1)}s` : ''}`);
|
|
408
380
|
if (result.summary) console.log(result.summary);
|
|
409
381
|
if (result.error) process.stderr.write(`${result.error}\n`);
|
|
410
|
-
// Save session state regardless of success/failure
|
|
411
382
|
saveSession({
|
|
412
383
|
objective: prompt,
|
|
413
384
|
branch: null,
|
|
@@ -417,14 +388,92 @@ async function cmdGo(args) {
|
|
|
417
388
|
status: result.status === 'completed' ? 'success' : 'failure',
|
|
418
389
|
summary: result.summary || (result.status === 'completed' ? 'completed' : `exit ${result.exitCode}`),
|
|
419
390
|
},
|
|
420
|
-
provider:
|
|
391
|
+
provider: plan?._decision?.provider ?? 'claude',
|
|
421
392
|
nextAction: null,
|
|
422
393
|
}, cwd);
|
|
423
394
|
if (result.status !== 'completed') process.exit(1);
|
|
424
395
|
await offerAutoCommit(cwd);
|
|
396
|
+
// ── Next steps suggestions ──────────────────────────────────────────────
|
|
397
|
+
try {
|
|
398
|
+
const { suggestNextSteps, formatNextSteps } = await import('../src/nextstep.mjs');
|
|
399
|
+
const steps = await suggestNextSteps(
|
|
400
|
+
{
|
|
401
|
+
prompt,
|
|
402
|
+
tier: plan?._decision?.tier ?? 'execute',
|
|
403
|
+
files: result.filesChanged || files,
|
|
404
|
+
trigger: 'go',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
success: result.status === 'completed',
|
|
408
|
+
filesChanged: result.filesChanged || files,
|
|
409
|
+
error: result.error,
|
|
410
|
+
duration: result.durationMs,
|
|
411
|
+
},
|
|
412
|
+
cwd
|
|
413
|
+
);
|
|
414
|
+
if (steps?.steps?.length > 0) {
|
|
415
|
+
console.log('\n' + formatNextSteps(steps.steps, 3));
|
|
416
|
+
}
|
|
417
|
+
} catch { /* non-fatal — module may not exist yet */ }
|
|
425
418
|
}
|
|
426
419
|
}
|
|
427
420
|
|
|
421
|
+
async function cmdThink(args) {
|
|
422
|
+
const question = args.find(a => !a.startsWith('--') && !a.startsWith('-'));
|
|
423
|
+
if (!question) err('Usage: dual-brain think "architecture question or design decision"');
|
|
424
|
+
|
|
425
|
+
const cwd = process.cwd();
|
|
426
|
+
await ensureProfile(cwd);
|
|
427
|
+
|
|
428
|
+
const { result, verification } = await runPipeline('think', question, {
|
|
429
|
+
cwd,
|
|
430
|
+
verbose: true,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (!result) return;
|
|
434
|
+
|
|
435
|
+
if (result.consensus) {
|
|
436
|
+
console.log(`\nConsensus: ${result.consensus}`);
|
|
437
|
+
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
438
|
+
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
439
|
+
} else {
|
|
440
|
+
if (result.summary) console.log(`\n${result.summary}`);
|
|
441
|
+
if (result.error) process.stderr.write(`${result.error}\n`);
|
|
442
|
+
if (result.status && result.status !== 'completed') process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (verification && !verification.ok) {
|
|
446
|
+
for (const note of verification.notes) process.stderr.write(` note: ${note}\n`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function cmdReview(_args) {
|
|
451
|
+
const cwd = process.cwd();
|
|
452
|
+
await ensureProfile(cwd);
|
|
453
|
+
|
|
454
|
+
const { result, verification } = await runPipeline('review', 'review current diff', {
|
|
455
|
+
cwd,
|
|
456
|
+
verbose: true,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (!result) return;
|
|
460
|
+
|
|
461
|
+
if (result.consensus) {
|
|
462
|
+
console.log(`\nConsensus: ${result.consensus}`);
|
|
463
|
+
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
464
|
+
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
465
|
+
} else {
|
|
466
|
+
if (result.summary) console.log(`\n${result.summary}`);
|
|
467
|
+
if (result.error) process.stderr.write(`${result.error}\n`);
|
|
468
|
+
if (result.status && result.status !== 'completed') process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (verification && !verification.ok) {
|
|
472
|
+
for (const note of verification.notes) process.stderr.write(` note: ${note}\n`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
428
477
|
async function cmdStatus(args = []) {
|
|
429
478
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
430
479
|
const cwd = process.cwd();
|
|
@@ -717,21 +766,21 @@ function profileExists(cwd) {
|
|
|
717
766
|
// ─── Plan label helpers ───────────────────────────────────────────────────────
|
|
718
767
|
|
|
719
768
|
const CLAUDE_PLAN_LABELS = {
|
|
720
|
-
pro: 'Pro
|
|
721
|
-
max5: 'Max x5
|
|
722
|
-
max20: 'Max x20
|
|
723
|
-
'$20': 'Pro
|
|
724
|
-
'$100': 'Max x5
|
|
725
|
-
'$200': 'Max x20
|
|
769
|
+
pro: 'Pro',
|
|
770
|
+
max5: 'Max x5',
|
|
771
|
+
max20: 'Max x20',
|
|
772
|
+
'$20': 'Pro', // doctor:verified — backward-compat key for legacy stored plan value
|
|
773
|
+
'$100': 'Max x5', // doctor:verified — backward-compat key for legacy stored plan value
|
|
774
|
+
'$200': 'Max x20', // doctor:verified — backward-compat key for legacy stored plan value
|
|
726
775
|
};
|
|
727
776
|
const OPENAI_PLAN_LABELS = {
|
|
728
|
-
plus: 'Plus
|
|
729
|
-
pro: 'Pro
|
|
730
|
-
pro100: 'Pro
|
|
731
|
-
pro200: 'Pro (
|
|
732
|
-
'$20': 'Plus
|
|
733
|
-
'$100': 'Pro
|
|
734
|
-
'$200': 'Pro (
|
|
777
|
+
plus: 'Plus',
|
|
778
|
+
pro: 'Pro',
|
|
779
|
+
pro100: 'Pro',
|
|
780
|
+
pro200: 'Pro (higher limits)',
|
|
781
|
+
'$20': 'Plus', // doctor:verified — backward-compat key for legacy stored plan value
|
|
782
|
+
'$100': 'Pro', // doctor:verified — backward-compat key for legacy stored plan value
|
|
783
|
+
'$200': 'Pro (higher limits)', // doctor:verified — backward-compat key for legacy stored plan value
|
|
735
784
|
};
|
|
736
785
|
|
|
737
786
|
// ─── Screen: welcomeScreen ────────────────────────────────────────────────────
|
|
@@ -817,7 +866,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
817
866
|
}
|
|
818
867
|
|
|
819
868
|
console.log(' [Enter] Save and go');
|
|
820
|
-
console.log(' [c] Customize
|
|
869
|
+
console.log(' [c] Customize work style');
|
|
821
870
|
if (existingSessions.length > 0) {
|
|
822
871
|
console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from data-tools`);
|
|
823
872
|
}
|
|
@@ -880,10 +929,10 @@ async function welcomeScreen(rl, ask) {
|
|
|
880
929
|
// Claude plan picker
|
|
881
930
|
if (claudeReady) {
|
|
882
931
|
console.log('');
|
|
883
|
-
console.log(separator('Claude
|
|
884
|
-
console.log(' (1) Pro
|
|
885
|
-
console.log(' (2) Max x5
|
|
886
|
-
console.log(' (3) Max x20
|
|
932
|
+
console.log(separator('Claude plan'));
|
|
933
|
+
console.log(' (1) Pro');
|
|
934
|
+
console.log(' (2) Max x5');
|
|
935
|
+
console.log(' (3) Max x20');
|
|
887
936
|
console.log(' (4) Skip');
|
|
888
937
|
const claudeChoice = (await ask('> ')).trim();
|
|
889
938
|
const claudePlanMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
|
|
@@ -900,10 +949,10 @@ async function welcomeScreen(rl, ask) {
|
|
|
900
949
|
// OpenAI plan picker
|
|
901
950
|
if (openaiReady) {
|
|
902
951
|
console.log('');
|
|
903
|
-
console.log(separator('OpenAI
|
|
904
|
-
console.log(' (1) Plus
|
|
905
|
-
console.log(' (2) Pro
|
|
906
|
-
console.log(' (3) Pro (
|
|
952
|
+
console.log(separator('OpenAI plan'));
|
|
953
|
+
console.log(' (1) Plus');
|
|
954
|
+
console.log(' (2) Pro');
|
|
955
|
+
console.log(' (3) Pro (higher limits)');
|
|
907
956
|
console.log(' (4) Skip');
|
|
908
957
|
const openaiChoice = (await ask('> ')).trim();
|
|
909
958
|
const openaiPlanMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
|
|
@@ -928,8 +977,8 @@ async function welcomeScreen(rl, ask) {
|
|
|
928
977
|
|
|
929
978
|
// Team setup
|
|
930
979
|
console.log('');
|
|
931
|
-
console.log(' Team auth: label
|
|
932
|
-
console.log(' When a
|
|
980
|
+
console.log(' Team auth: label providers and set expiry for auto-refresh.');
|
|
981
|
+
console.log(' When a provider link expires, dual-brain will prompt re-login automatically.');
|
|
933
982
|
console.log('');
|
|
934
983
|
console.log(' [Enter] Skip [t] Set up team auth');
|
|
935
984
|
const teamChoice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
@@ -937,7 +986,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
937
986
|
for (const provider of ['claude', 'openai']) {
|
|
938
987
|
if (!existingProfile.providers[provider]?.enabled) continue;
|
|
939
988
|
const provLabel = provider === 'claude' ? 'Claude' : 'OpenAI';
|
|
940
|
-
const label = (await ask(` ${provLabel} label (e.g. "Josh's
|
|
989
|
+
const label = (await ask(` ${provLabel} label (e.g. "Josh's work account"): `)).trim();
|
|
941
990
|
if (label) existingProfile.providers[provider].label = label;
|
|
942
991
|
const expiry = await askExpiry(ask, provLabel);
|
|
943
992
|
if (expiry) existingProfile.providers[provider].expiresAt = expiry;
|
|
@@ -1457,11 +1506,39 @@ async function mainScreen(rl, ask) {
|
|
|
1457
1506
|
statusRows.push(row(`\x1b[2m📦 data-tools v${dtVersion}\x1b[0m`));
|
|
1458
1507
|
}
|
|
1459
1508
|
|
|
1509
|
+
// ── Observer observations (top 2, high priority first) ───────────────────
|
|
1510
|
+
let quickObservations = [];
|
|
1511
|
+
try {
|
|
1512
|
+
const observerMod = await import('../src/observer.mjs');
|
|
1513
|
+
const quickState = await observerMod.getQuickState(cwd);
|
|
1514
|
+
if (quickState?.observations?.length > 0) {
|
|
1515
|
+
const PRIO = { high: 0, medium: 1, low: 2 };
|
|
1516
|
+
const sorted = [...quickState.observations].sort(
|
|
1517
|
+
(a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
|
|
1518
|
+
);
|
|
1519
|
+
quickObservations = sorted.slice(0, 2);
|
|
1520
|
+
for (const obs of quickObservations) {
|
|
1521
|
+
let prefix;
|
|
1522
|
+
if (obs.priority === 'high') prefix = '🔴';
|
|
1523
|
+
else if (obs.priority === 'medium') prefix = '🟡';
|
|
1524
|
+
else prefix = '\x1b[2m💡\x1b[0m';
|
|
1525
|
+
statusRows.push(row(`${prefix} ${obs.message}`));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
} catch { /* non-fatal — module may not exist yet */ }
|
|
1529
|
+
|
|
1460
1530
|
// ── Action cards (git state + open PRs) ──────────────────────────────────
|
|
1461
1531
|
const repoState = detectRepoState(cwd);
|
|
1462
1532
|
const openPRs = await detectOpenPRs(cwd);
|
|
1463
1533
|
const actionRows = buildActionRows(repoState, row, openPRs);
|
|
1464
1534
|
|
|
1535
|
+
// ── High-priority observer action cards ───────────────────────────────────
|
|
1536
|
+
if (quickObservations.some(o => o.priority === 'high')) {
|
|
1537
|
+
const DIM = '\x1b[2m';
|
|
1538
|
+
const RESET = '\x1b[0m';
|
|
1539
|
+
actionRows.push(row(`${DIM}[r] Security review [t] Run tests [c] Commit${RESET}`));
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1465
1542
|
// ── Related sessions hint (only when no continuation card is showing) ─────
|
|
1466
1543
|
if (!interrupted && recentSessions.length > 0) {
|
|
1467
1544
|
try {
|
|
@@ -2423,21 +2500,15 @@ async function settingsScreen(rl, ask) {
|
|
|
2423
2500
|
|
|
2424
2501
|
// ─── Helper: aggregatePlans ───────────────────────────────────────────────────
|
|
2425
2502
|
|
|
2426
|
-
const PLAN_PRICES = {
|
|
2427
|
-
pro: '$20', max5: '$100', max20: '$200',
|
|
2428
|
-
plus: '$20', pro100: '$100', pro200: '$200',
|
|
2429
|
-
};
|
|
2430
|
-
|
|
2431
2503
|
function aggregatePlans(subs) {
|
|
2432
2504
|
if (!subs || subs.length === 0) return '';
|
|
2433
2505
|
const counts = {};
|
|
2434
2506
|
for (const s of subs) {
|
|
2435
|
-
const
|
|
2436
|
-
counts[
|
|
2507
|
+
const label = s.plan || 'unknown';
|
|
2508
|
+
counts[label] = (counts[label] || 0) + 1;
|
|
2437
2509
|
}
|
|
2438
2510
|
return Object.entries(counts)
|
|
2439
|
-
.
|
|
2440
|
-
.map(([price, count]) => `${price}×${count}`)
|
|
2511
|
+
.map(([label, count]) => count > 1 ? `${label}×${count}` : label)
|
|
2441
2512
|
.join(' ');
|
|
2442
2513
|
}
|
|
2443
2514
|
|
|
@@ -2499,13 +2570,13 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2499
2570
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
2500
2571
|
|
|
2501
2572
|
if (choice === '1') {
|
|
2502
|
-
console.log('\n Linking Claude
|
|
2573
|
+
console.log('\n Linking Claude account...');
|
|
2503
2574
|
console.log(' A browser window will open — paste the code below when prompted.\n');
|
|
2504
2575
|
const { spawnSync } = await import('node:child_process');
|
|
2505
2576
|
const r = spawnSync('claude', ['auth', 'login'], { stdio: 'inherit', timeout: 60000 });
|
|
2506
2577
|
if (r.status === 0) {
|
|
2507
2578
|
console.log('\n ✅ Claude linked successfully!\n');
|
|
2508
|
-
const label = (await ask(" Label (e.g. \"Josh's
|
|
2579
|
+
const label = (await ask(" Label (e.g. \"Josh's work account\", or Enter to skip): ")).trim();
|
|
2509
2580
|
const expiry = await askExpiry(ask, 'Claude');
|
|
2510
2581
|
const newPlans = detectPlans();
|
|
2511
2582
|
const plan = newPlans.claude?.plan || 'pro';
|
|
@@ -2527,7 +2598,7 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2527
2598
|
}
|
|
2528
2599
|
|
|
2529
2600
|
if (choice === '2') {
|
|
2530
|
-
console.log('\n Linking Codex
|
|
2601
|
+
console.log('\n Linking Codex account...');
|
|
2531
2602
|
console.log(' A browser window will open — paste the code below when prompted.\n');
|
|
2532
2603
|
const { spawnSync } = await import('node:child_process');
|
|
2533
2604
|
const r = spawnSync('codex', ['login'], { stdio: 'inherit', timeout: 60000 });
|
|
@@ -2565,12 +2636,12 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2565
2636
|
}
|
|
2566
2637
|
|
|
2567
2638
|
if (allSubs.length === 0) {
|
|
2568
|
-
console.log('\n No
|
|
2639
|
+
console.log('\n No linked accounts to remove.\n');
|
|
2569
2640
|
await ask(' Press Enter to continue...');
|
|
2570
2641
|
return { next: 'subscriptions' };
|
|
2571
2642
|
}
|
|
2572
2643
|
|
|
2573
|
-
console.log('\n Remove a
|
|
2644
|
+
console.log('\n Remove a linked account:\n');
|
|
2574
2645
|
allSubs.forEach(({ displayName, sub }, i) => {
|
|
2575
2646
|
const planLabels = displayName === 'Claude' ? CLAUDE_PLAN_LABELS : OPENAI_PLAN_LABELS;
|
|
2576
2647
|
const planLabel = planLabels[sub.plan] ?? sub.plan ?? 'unknown';
|
|
@@ -2617,14 +2688,13 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2617
2688
|
* @param {object} rl readline interface
|
|
2618
2689
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
2619
2690
|
*/
|
|
2620
|
-
async function runOnboardingWizard(
|
|
2691
|
+
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
2621
2692
|
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
2622
2693
|
const version = readVersion();
|
|
2623
2694
|
|
|
2624
2695
|
// ── Rounded box helpers (matching mainScreen style) ────────────────────────
|
|
2625
2696
|
const W = 51;
|
|
2626
2697
|
const wTop = ` ┌${'─'.repeat(W)}┐`;
|
|
2627
|
-
const wSep = ` ├${'─'.repeat(W)}┤`;
|
|
2628
2698
|
const wBottom = ` └${'─'.repeat(W)}┘`;
|
|
2629
2699
|
const wPad = (s) => {
|
|
2630
2700
|
const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -2641,105 +2711,95 @@ async function runOnboardingWizard(detection, cwd, rl) {
|
|
|
2641
2711
|
};
|
|
2642
2712
|
const wRow = (s) => ` │ ${wPad(s)}│`;
|
|
2643
2713
|
|
|
2644
|
-
|
|
2645
|
-
const
|
|
2646
|
-
const
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
let codexAvailable = false;
|
|
2650
|
-
try {
|
|
2651
|
-
const { spawnSync } = await import('node:child_process');
|
|
2652
|
-
const r = spawnSync('which', ['codex'], { encoding: 'utf8' });
|
|
2653
|
-
codexAvailable = r.status === 0;
|
|
2654
|
-
} catch {}
|
|
2714
|
+
// ── Use detectCapabilities for broad detection (env vars, ~/.claude, CLI) ──
|
|
2715
|
+
const caps = await detectCapabilities(cwd);
|
|
2716
|
+
const claudeReady = caps.claude.available;
|
|
2717
|
+
const openaiReady = caps.openai.available;
|
|
2718
|
+
const codexAvailable = caps.codex.available;
|
|
2655
2719
|
|
|
2656
2720
|
// ── Detect replit-tools ────────────────────────────────────────────────────
|
|
2657
2721
|
const rt = detectReplitTools(cwd);
|
|
2658
2722
|
|
|
2659
2723
|
const GREEN = '\x1b[32m✓\x1b[0m';
|
|
2660
2724
|
const RED = '\x1b[31m✗\x1b[0m';
|
|
2725
|
+
const DIM = '\x1b[2m';
|
|
2726
|
+
const RESET = '\x1b[0m';
|
|
2661
2727
|
|
|
2662
2728
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2663
|
-
// Step 1 — Auto-detect capabilities (no
|
|
2729
|
+
// Step 1 — Auto-detect capabilities (instant, no spinner)
|
|
2664
2730
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2665
2731
|
console.log('');
|
|
2666
2732
|
console.log(wTop);
|
|
2667
2733
|
console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
|
|
2668
|
-
console.log(wSep);
|
|
2669
|
-
console.log(wRow('Checking your setup...'));
|
|
2670
|
-
console.log(wSep);
|
|
2671
2734
|
console.log(wRow(claudeReady
|
|
2672
|
-
? `${GREEN} Claude Code
|
|
2735
|
+
? `${GREEN} Claude Code`
|
|
2673
2736
|
: `${RED} Claude Code — not found`));
|
|
2674
2737
|
console.log(wRow(openaiReady
|
|
2675
|
-
? `${GREEN} OpenAI API
|
|
2676
|
-
:
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
: `${RED} Codex CLI — not found`));
|
|
2738
|
+
? `${GREEN} OpenAI API`
|
|
2739
|
+
: codexAvailable
|
|
2740
|
+
? `${GREEN} OpenAI / Codex CLI`
|
|
2741
|
+
: `${DIM}○ OpenAI — not configured${RESET}`));
|
|
2680
2742
|
console.log(wRow(rt.installed
|
|
2681
|
-
? `${GREEN} replit-tools
|
|
2682
|
-
: `${
|
|
2743
|
+
? `${GREEN} replit-tools`
|
|
2744
|
+
: `${DIM}○ replit-tools — not found${RESET}`));
|
|
2683
2745
|
console.log(wBottom);
|
|
2684
2746
|
|
|
2685
|
-
|
|
2747
|
+
// ── Edge cases: communicate honestly, but always let them proceed ──────────
|
|
2748
|
+
console.log('');
|
|
2749
|
+
if (!claudeReady && !openaiReady && !codexAvailable) {
|
|
2750
|
+
console.log(' No AI providers detected — configure OPENAI_API_KEY or use');
|
|
2751
|
+
console.log(' within Claude Code. You can still continue and set up later.');
|
|
2752
|
+
console.log('');
|
|
2753
|
+
} else if (claudeReady && !openaiReady && !codexAvailable) {
|
|
2754
|
+
console.log(` ${DIM}Tip: Add OPENAI_API_KEY for dual-brain collaboration${RESET}`);
|
|
2755
|
+
console.log('');
|
|
2756
|
+
} else if (!claudeReady && (openaiReady || codexAvailable)) {
|
|
2757
|
+
console.log(` ${DIM}Note: Use within Claude Code for full dual-brain${RESET}`);
|
|
2686
2758
|
console.log('');
|
|
2687
|
-
console.log(' No AI provider found. Log in first:');
|
|
2688
|
-
console.log(' claude auth login — for Claude');
|
|
2689
|
-
console.log(' codex login — for OpenAI/Codex');
|
|
2690
|
-
console.log(' Then re-run: dual-brain init\n');
|
|
2691
|
-
return null;
|
|
2692
2759
|
}
|
|
2693
2760
|
|
|
2694
2761
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2695
2762
|
// Step 2 — ONE question: work style
|
|
2696
2763
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2697
|
-
console.log('');
|
|
2698
2764
|
console.log(wTop);
|
|
2699
|
-
console.log(wRow(`🧠 Dual-Brain v${version} — Work Style`));
|
|
2700
|
-
console.log(wSep);
|
|
2701
2765
|
console.log(wRow('How do you want to work?'));
|
|
2702
|
-
console.log(
|
|
2703
|
-
console.log(wRow(' 1
|
|
2704
|
-
console.log(wRow(' 2
|
|
2705
|
-
console.log(wRow('
|
|
2706
|
-
console.log(wRow(' 3. 🔥 Full Power — deep reasoning, dual-brain on everything'));
|
|
2707
|
-
console.log(wRow(' that matters'));
|
|
2708
|
-
console.log(wSep);
|
|
2709
|
-
console.log(wRow('[Enter] Balanced'));
|
|
2766
|
+
console.log(wRow(''));
|
|
2767
|
+
console.log(wRow(' 1 ⚡ Fast — single model, quick tasks, skip reviews'));
|
|
2768
|
+
console.log(wRow(' 2 ⚖️ Balanced — smart routing, reviews on important changes'));
|
|
2769
|
+
console.log(wRow(' 3 🔥 Full Power — deep reasoning, dual-brain when it matters'));
|
|
2710
2770
|
console.log(wBottom);
|
|
2711
2771
|
console.log('');
|
|
2712
2772
|
|
|
2713
|
-
const styleChoice = (await ask(' Choice [
|
|
2714
|
-
const styleMap
|
|
2773
|
+
const styleChoice = (await ask(' Choice [2]: ')).trim();
|
|
2774
|
+
const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
2775
|
+
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Full Power' };
|
|
2715
2776
|
const chosenBias = styleMap[styleChoice] || 'balanced';
|
|
2777
|
+
const chosenName = styleNames[chosenBias];
|
|
2716
2778
|
|
|
2717
2779
|
// ── Non-blocking note if metered API detected ──────────────────────────────
|
|
2718
|
-
if (openaiReady &&
|
|
2780
|
+
if (openaiReady && caps.openai.metered) {
|
|
2781
|
+
console.log(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}`);
|
|
2719
2782
|
console.log('');
|
|
2720
|
-
console.log(' 💡 OpenAI API detected — will confirm before expensive operations');
|
|
2721
2783
|
}
|
|
2722
2784
|
|
|
2723
2785
|
// ── Done ───────────────────────────────────────────────────────────────────
|
|
2724
|
-
console.log('');
|
|
2725
2786
|
console.log(wTop);
|
|
2726
|
-
console.log(wRow(
|
|
2787
|
+
console.log(wRow(`${GREEN} Ready — ${chosenName} mode`));
|
|
2788
|
+
console.log(wRow(` Type a task to start, or press Enter for dashboard`));
|
|
2727
2789
|
console.log(wBottom);
|
|
2728
2790
|
console.log('');
|
|
2729
2791
|
|
|
2730
2792
|
// ── Build and return the profile object ────────────────────────────────────
|
|
2731
2793
|
const finalProfile = loadProfile(cwd);
|
|
2732
2794
|
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
if (openaiReady) {
|
|
2737
|
-
finalProfile.providers.openai = { enabled: true };
|
|
2738
|
-
}
|
|
2795
|
+
finalProfile.providers.claude = { enabled: claudeReady };
|
|
2796
|
+
finalProfile.providers.openai = { enabled: openaiReady || codexAvailable };
|
|
2797
|
+
finalProfile.apiGuardrail = caps.openai.metered;
|
|
2739
2798
|
|
|
2740
|
-
const enabledCount = [claudeReady, openaiReady].filter(Boolean).length;
|
|
2741
|
-
finalProfile.mode
|
|
2742
|
-
finalProfile.bias
|
|
2799
|
+
const enabledCount = [claudeReady, openaiReady || codexAvailable].filter(Boolean).length;
|
|
2800
|
+
finalProfile.mode = enabledCount >= 2 ? 'dual' : claudeReady ? 'solo-claude' : 'solo-openai';
|
|
2801
|
+
finalProfile.bias = chosenBias;
|
|
2802
|
+
finalProfile.workStyle = chosenBias;
|
|
2743
2803
|
|
|
2744
2804
|
return finalProfile;
|
|
2745
2805
|
}
|
|
@@ -2780,11 +2840,11 @@ async function authScreen(rl, ask) {
|
|
|
2780
2840
|
` plan: ${openaiPlanLabel}${openaiSub?.label ? ` [${openaiSub.label}]` : ''}`,
|
|
2781
2841
|
];
|
|
2782
2842
|
|
|
2783
|
-
console.log(box('
|
|
2843
|
+
console.log(box('Provider Status', authLines));
|
|
2784
2844
|
console.log('');
|
|
2785
2845
|
console.log(menu([
|
|
2786
|
-
{ key: 'a', label: 'Manage
|
|
2787
|
-
{ key: 'b', label: 'Back to dashboard',
|
|
2846
|
+
{ key: 'a', label: 'Manage linked accounts', section: '' },
|
|
2847
|
+
{ key: 'b', label: 'Back to dashboard', section: '' },
|
|
2788
2848
|
]));
|
|
2789
2849
|
console.log('');
|
|
2790
2850
|
|
|
@@ -4383,6 +4443,8 @@ async function main() {
|
|
|
4383
4443
|
return;
|
|
4384
4444
|
}
|
|
4385
4445
|
if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
|
|
4446
|
+
if (cmd === 'think') { await cmdThink(args.slice(1)); return; }
|
|
4447
|
+
if (cmd === 'review') { await cmdReview(args.slice(1)); return; }
|
|
4386
4448
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
4387
4449
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
4388
4450
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
package/package.json
CHANGED
package/src/decide.mjs
CHANGED
|
@@ -26,8 +26,7 @@ const WORKSPACE = join(__dirname, '..');
|
|
|
26
26
|
/**
|
|
27
27
|
* Work styles control how aggressively the router uses stronger models,
|
|
28
28
|
* challenger (dual-brain) reviews, and checkpoints.
|
|
29
|
-
*
|
|
30
|
-
* regardless of what plan they are on.
|
|
29
|
+
* The user picks a style regardless of provider or plan — no price gating.
|
|
31
30
|
*/
|
|
32
31
|
export const WORK_STYLES = {
|
|
33
32
|
fast: {
|
|
@@ -193,7 +192,7 @@ export function getModelCapabilities(model) {
|
|
|
193
192
|
* Return which models the user can access.
|
|
194
193
|
* All known models are available by default; providers can explicitly restrict
|
|
195
194
|
* via profile.providers.<provider>.models (array of allowed model short names).
|
|
196
|
-
* This does NOT gate on
|
|
195
|
+
* This does NOT gate on price or configured plan — we cannot verify those from here.
|
|
197
196
|
* @param {{ providers?: { claude?: { enabled?: boolean, models?: string[] }, openai?: { enabled?: boolean, models?: string[] } } }} profile
|
|
198
197
|
* @returns {{ claude: string[], openai: string[] }}
|
|
199
198
|
*/
|
package/src/profile.mjs
CHANGED
|
@@ -131,7 +131,7 @@ function detectEnvironment() {
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Detect what providers and tools are actually available.
|
|
134
|
-
* Never makes network calls, never claims to know
|
|
134
|
+
* Never makes network calls, never claims to know configured plan or price.
|
|
135
135
|
*
|
|
136
136
|
* @param {string} [cwd]
|
|
137
137
|
* @returns {Promise<{
|
|
@@ -338,7 +338,7 @@ function migrateProfile(profile) {
|
|
|
338
338
|
}
|
|
339
339
|
delete profile.plan;
|
|
340
340
|
delete profile.price;
|
|
341
|
-
delete profile.subscription;
|
|
341
|
+
delete profile.subscription; // doctor:verified — removing legacy field from stored config
|
|
342
342
|
delete profile.budget;
|
|
343
343
|
delete profile.detectedPlan;
|
|
344
344
|
}
|
|
@@ -771,7 +771,7 @@ function detectPlans() {
|
|
|
771
771
|
return { claude: null, openai: null };
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
-
/** @deprecated Plan
|
|
774
|
+
/** @deprecated Plan tracking removed. Use provider enabled flag instead. */
|
|
775
775
|
function saveSubscription(provider, config, cwd) {
|
|
776
776
|
const profile = loadProfile(cwd);
|
|
777
777
|
if (!profile.providers[provider]) profile.providers[provider] = { enabled: true };
|
|
@@ -780,7 +780,7 @@ function saveSubscription(provider, config, cwd) {
|
|
|
780
780
|
return profile;
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
-
/** @deprecated Plan
|
|
783
|
+
/** @deprecated Plan tracking removed. Use getAvailableProviders() instead. */
|
|
784
784
|
function listSubscriptions(cwd) {
|
|
785
785
|
const profile = loadProfile(cwd);
|
|
786
786
|
return profile.providers || {};
|