dual-brain 7.1.4 → 7.1.6
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/CLAUDE.md +5 -5
- package/README.md +3 -3
- package/bin/dual-brain.mjs +212 -89
- package/hooks/head-guard.mjs +43 -7
- package/hooks/test-orchestrator.mjs +6 -6
- package/mcp-server/index.mjs +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/decide.mjs +36 -6
- package/src/dispatch.mjs +4 -3
- package/src/index.mjs +1 -1
- package/src/profile.mjs +3 -103
package/CLAUDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This project uses dual-provider orchestration. Config: `.claude/orchestrator.json`.
|
|
4
4
|
|
|
5
|
-
## Core Architecture (
|
|
5
|
+
## Core Architecture (v7)
|
|
6
6
|
|
|
7
7
|
Four modules in `src/` form the decision pipeline:
|
|
8
8
|
|
|
@@ -71,21 +71,21 @@ Dual-brain is a multi-round conversation between Claude and GPT — not a single
|
|
|
71
71
|
## Quality Gate
|
|
72
72
|
|
|
73
73
|
Before ending a session with code changes:
|
|
74
|
-
1.
|
|
75
|
-
2.
|
|
74
|
+
1. `node .claude/hooks/session-report.mjs` (allowed by head-guard for hook scripts)
|
|
75
|
+
2. `node .claude/hooks/quality-gate.mjs`
|
|
76
76
|
|
|
77
77
|
Gate statuses: `pass` (safe to end), `issues_found` (fix first), `needs_human_review` (GPT unavailable).
|
|
78
78
|
|
|
79
79
|
## Profiles
|
|
80
80
|
|
|
81
|
-
Profile persists to `.
|
|
81
|
+
Profile persists to `.dualbrain/profile.json` (project-scoped, gitignored).
|
|
82
82
|
|
|
83
83
|
- **auto** (default): Adapts routing based on task risk, provider health, and outcomes
|
|
84
84
|
- **balanced**: Best model per tier, normal budgets, reviews at medium+ risk
|
|
85
85
|
- **cost-saver**: Prefer cheaper models, lower budgets, skip GPT for non-critical
|
|
86
86
|
- **quality-first**: Dual-brain for medium+ risk, higher budgets, stricter reviews
|
|
87
87
|
|
|
88
|
-
Switch
|
|
88
|
+
Switch via the interactive Profile screen in `dual-brain`, or set `bias` in `.dualbrain/profile.json`.
|
|
89
89
|
|
|
90
90
|
## Adaptive Routing (Auto Mode)
|
|
91
91
|
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ dual-brain detects the intent and risk of your task, picks the best model based
|
|
|
24
24
|
|
|
25
25
|
### `dual-brain init`
|
|
26
26
|
|
|
27
|
-
First-time setup. Three questions: which providers you have, subscription tiers, and optimization preference.
|
|
27
|
+
First-time setup. Three questions: which providers you have, subscription tiers, and optimization preference. The actual flow auto-detects existing auth and adapts — you may see fewer prompts if credentials are already configured.
|
|
28
28
|
|
|
29
29
|
```
|
|
30
30
|
Dual-Brain Orchestrator — First-time setup
|
|
@@ -121,7 +121,7 @@ Preferences are stored in `.dualbrain/profile.json` and applied on every `go` in
|
|
|
121
121
|
import { orchestrate } from 'dual-brain';
|
|
122
122
|
|
|
123
123
|
const result = await orchestrate({ prompt: "fix the bug", cwd: "." });
|
|
124
|
-
console.log(result.summary);
|
|
124
|
+
console.log(result.result?.summary);
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
Individual modules are also exported:
|
|
@@ -174,7 +174,7 @@ For Claude Code users, a hooks layer provides deeper integration. Hooks fire on
|
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
176
|
# Install hooks into .claude/settings.json
|
|
177
|
-
npx
|
|
177
|
+
npx dual-brain install
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
The installer auto-detects your environment (Claude CLI, Codex CLI, Replit), registers `enforce-tier.mjs` and `cost-logger.mjs` hooks, and writes `orchestrator.json` with your subscription config. Re-run anytime — it's idempotent.
|
package/bin/dual-brain.mjs
CHANGED
|
@@ -65,10 +65,8 @@ Commands:
|
|
|
65
65
|
forget "preference" Remove a preference by fuzzy match
|
|
66
66
|
|
|
67
67
|
Interactive mode (entered with no args on a TTY):
|
|
68
|
-
|
|
69
|
-
[
|
|
70
|
-
[s] Status, [p] Profile, [a] Auth, [d] Diagnostics
|
|
71
|
-
[c] Command mode (REPL), [q] Exit
|
|
68
|
+
Session manager with recent sessions and routing.
|
|
69
|
+
[n] New session, [c] Continue last, [1-9] Resume, [s] Settings, [q] Exit
|
|
72
70
|
|
|
73
71
|
Options:
|
|
74
72
|
--version Print version
|
|
@@ -155,6 +153,9 @@ async function cmdInit(rl) {
|
|
|
155
153
|
const profile = await runOnboarding({ interactive: true, detectedAuth: auth, rl });
|
|
156
154
|
saveProfile(profile, { cwd });
|
|
157
155
|
|
|
156
|
+
// --- Step 2b: Install hooks so enforcement is active from first run ---
|
|
157
|
+
await cmdInstall(cwd);
|
|
158
|
+
|
|
158
159
|
// --- Step 3: Show dashboard ---
|
|
159
160
|
console.log('');
|
|
160
161
|
const repo = loadRepoCache(cwd);
|
|
@@ -280,6 +281,27 @@ async function cmdGo(args) {
|
|
|
280
281
|
}, cwd);
|
|
281
282
|
} else {
|
|
282
283
|
result = await dispatch({ decision, prompt, files, cwd });
|
|
284
|
+
if (result.status === 'completed' && result.type === 'native-agent') {
|
|
285
|
+
const nd = result.nativeDispatch || {};
|
|
286
|
+
const promptPreview = (nd.prompt || prompt).slice(0, 100);
|
|
287
|
+
const promptSuffix = (nd.prompt || prompt).length > 100 ? '...' : '';
|
|
288
|
+
console.log(`\nRouted: ${decision.provider}/${nd.model || decision.model} (${decision.tier})`);
|
|
289
|
+
console.log('To dispatch, use the Agent tool with:');
|
|
290
|
+
console.log(` model: ${nd.model || decision.model}`);
|
|
291
|
+
console.log(` prompt: ${promptPreview}${promptSuffix}`);
|
|
292
|
+
if (nd.isolation) console.log(` isolation: ${nd.isolation}`);
|
|
293
|
+
if (nd.maxTurns) console.log(` maxTurns: ${nd.maxTurns}`);
|
|
294
|
+
saveSession({
|
|
295
|
+
objective: prompt,
|
|
296
|
+
branch: null,
|
|
297
|
+
filesChanged: files,
|
|
298
|
+
commandsRun: [`dual-brain go "${prompt}"`],
|
|
299
|
+
lastResult: { status: 'success', summary: `native-agent routed to ${nd.model || decision.model}` },
|
|
300
|
+
provider: decision.provider,
|
|
301
|
+
nextAction: null,
|
|
302
|
+
}, cwd);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
283
305
|
const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
284
306
|
console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
285
307
|
if (result.summary) console.log(result.summary);
|
|
@@ -443,7 +465,7 @@ async function cmdStatus(args = []) {
|
|
|
443
465
|
|
|
444
466
|
const PROVIDER_MODEL_CLASSES = {
|
|
445
467
|
claude: ['haiku', 'sonnet', 'opus'],
|
|
446
|
-
openai: ['
|
|
468
|
+
openai: ['gpt-4.1-mini', 'gpt-4.1', 'gpt-5.2', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.4', 'gpt-5.5'],
|
|
447
469
|
};
|
|
448
470
|
|
|
449
471
|
function cmdHot(providerArg) {
|
|
@@ -466,8 +488,8 @@ function cmdCool(providerArg) {
|
|
|
466
488
|
console.log(`Cleared hot state for all ${provider} model classes.`);
|
|
467
489
|
}
|
|
468
490
|
|
|
469
|
-
async function cmdInstall() {
|
|
470
|
-
|
|
491
|
+
async function cmdInstall(cwd) {
|
|
492
|
+
if (!cwd) cwd = process.cwd();
|
|
471
493
|
|
|
472
494
|
// Run the main install.mjs (orchestrator config, all hooks, CLAUDE.md, etc.)
|
|
473
495
|
const { spawnSync } = await import('child_process');
|
|
@@ -486,8 +508,6 @@ async function cmdInstall() {
|
|
|
486
508
|
console.log(`Enforcement hooks already present (${skipped.length}):`);
|
|
487
509
|
for (const item of skipped) console.log(` = ${item}`);
|
|
488
510
|
}
|
|
489
|
-
|
|
490
|
-
process.exit(0);
|
|
491
511
|
}
|
|
492
512
|
|
|
493
513
|
function cmdRemember(text) {
|
|
@@ -559,7 +579,8 @@ async function welcomeScreen(rl, ask) {
|
|
|
559
579
|
} else {
|
|
560
580
|
// Enter or anything else → save and go to dashboard
|
|
561
581
|
saveProfile(setup.profile, { cwd });
|
|
562
|
-
|
|
582
|
+
await cmdInstall(cwd);
|
|
583
|
+
return { next: 'main' };
|
|
563
584
|
}
|
|
564
585
|
} else {
|
|
565
586
|
// Not confident — show what's missing before falling through to wizard
|
|
@@ -685,109 +706,209 @@ async function welcomeScreen(rl, ask) {
|
|
|
685
706
|
console.log(box('Setup Complete', summaryLines));
|
|
686
707
|
console.log('');
|
|
687
708
|
|
|
688
|
-
|
|
709
|
+
await cmdInstall(cwd);
|
|
710
|
+
|
|
711
|
+
return { next: 'main' };
|
|
689
712
|
}
|
|
690
713
|
|
|
691
|
-
// ─── Screen:
|
|
714
|
+
// ─── Screen: mainScreen ───────────────────────────────────────────────────────
|
|
692
715
|
|
|
693
|
-
async function
|
|
716
|
+
async function mainScreen(rl, ask) {
|
|
694
717
|
const cwd = process.cwd();
|
|
695
718
|
const version = readVersion();
|
|
696
719
|
const profile = loadProfile(cwd);
|
|
697
720
|
const auth = await detectAuth();
|
|
698
|
-
const env = detectEnvironment();
|
|
699
721
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
722
|
+
const claudePlan = profile?.providers?.claude?.plan ?? 'Pro';
|
|
723
|
+
const openaiPlan = profile?.providers?.openai?.plan ?? 'Plus';
|
|
724
|
+
const claudeStatus = auth.claude.found ? `Claude: ${claudePlan} ✓` : `Claude: missing`;
|
|
725
|
+
const openaiStatus = auth.openai.found ? `OpenAI: ${openaiPlan} ✓` : `OpenAI: missing`;
|
|
726
|
+
|
|
727
|
+
console.log(`\ndual-brain v${version}`);
|
|
728
|
+
console.log(`${claudeStatus} · ${openaiStatus}\n`);
|
|
729
|
+
|
|
730
|
+
const recentSessions = importReplitSessions(cwd).slice(0, 5);
|
|
731
|
+
|
|
732
|
+
if (recentSessions.length > 0) {
|
|
733
|
+
console.log('Recent:');
|
|
734
|
+
recentSessions.forEach((sess, i) => {
|
|
735
|
+
const activeIndicator = sess.isActive ? ' ●' : '';
|
|
736
|
+
console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${sess.name}${activeIndicator}`);
|
|
737
|
+
});
|
|
738
|
+
console.log('');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
console.log(' [c] Continue last session');
|
|
742
|
+
console.log(' [n] New session');
|
|
743
|
+
if (recentSessions.length > 0) {
|
|
744
|
+
console.log(' [1-9] Resume numbered above');
|
|
745
|
+
}
|
|
746
|
+
console.log(' [d] Switch to data-tools');
|
|
747
|
+
if (!auth.claude.found) console.log(' [j] Login to Claude');
|
|
748
|
+
if (!auth.openai.found) console.log(' [k] Login to Codex');
|
|
749
|
+
console.log(' [s] Settings [q] Exit');
|
|
750
|
+
console.log('');
|
|
751
|
+
|
|
752
|
+
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
753
|
+
|
|
754
|
+
if (choice === 'n') { return { next: 'new-session' }; }
|
|
755
|
+
|
|
756
|
+
if (choice === 'c') {
|
|
757
|
+
const sessions = importReplitSessions(cwd);
|
|
758
|
+
if (sessions.length === 0) {
|
|
759
|
+
console.log('\n No recent sessions found.\n');
|
|
760
|
+
await ask(' Press Enter to continue...');
|
|
761
|
+
return { next: 'main' };
|
|
762
|
+
}
|
|
763
|
+
const { spawnSync } = await import('node:child_process');
|
|
764
|
+
console.log(`\n Launching: claude --resume ${sessions[0].id}\n`);
|
|
765
|
+
spawnSync('claude', ['--resume', sessions[0].id], { stdio: 'inherit' });
|
|
766
|
+
return { next: 'main' };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const numChoice = parseInt(choice, 10);
|
|
770
|
+
if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
|
|
771
|
+
const sess = recentSessions[numChoice - 1];
|
|
772
|
+
const { spawnSync } = await import('node:child_process');
|
|
773
|
+
console.log(`\n Launching: claude --resume ${sess.id}\n`);
|
|
774
|
+
spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
|
|
775
|
+
return { next: 'main' };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (choice === 'd') {
|
|
779
|
+
const { spawnSync } = await import('node:child_process');
|
|
780
|
+
const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
|
|
781
|
+
if (which.status === 0) {
|
|
782
|
+
spawnSync('claude-menu', { stdio: 'inherit' });
|
|
783
|
+
} else {
|
|
784
|
+
console.log('\n data-tools not found — install with: npm i -g replit-tools\n');
|
|
785
|
+
await ask(' Press Enter to continue...');
|
|
786
|
+
}
|
|
787
|
+
return { next: 'main' };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (choice === 'j') {
|
|
791
|
+
const { spawnSync } = await import('node:child_process');
|
|
792
|
+
spawnSync('claude', ['login'], { stdio: 'inherit' });
|
|
793
|
+
return { next: 'main' };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (choice === 'k') {
|
|
797
|
+
const { spawnSync } = await import('node:child_process');
|
|
798
|
+
spawnSync('codex', ['login'], { stdio: 'inherit' });
|
|
799
|
+
return { next: 'main' };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (choice === 's') { return { next: 'settings' }; }
|
|
803
|
+
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
804
|
+
|
|
805
|
+
return { next: 'main' };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ─── Screen: newSessionScreen ─────────────────────────────────────────────────
|
|
809
|
+
|
|
810
|
+
async function newSessionScreen(rl, ask) {
|
|
811
|
+
const cwd = process.cwd();
|
|
812
|
+
const input = (await ask('\n What do you want to do? ')).trim();
|
|
813
|
+
if (!input) { return { next: 'main' }; }
|
|
814
|
+
|
|
815
|
+
const profile = loadProfile(cwd);
|
|
816
|
+
const detection = detectTask({ prompt: input });
|
|
817
|
+
const decision = decideRoute({ profile, detection, cwd });
|
|
818
|
+
|
|
819
|
+
console.log(`\n Routing: ${decision.provider}/${decision.model} (${decision.tier})`);
|
|
820
|
+
console.log(` Reason: ${decision.explanation}\n`);
|
|
821
|
+
|
|
822
|
+
const { spawnSync } = await import('node:child_process');
|
|
823
|
+
if (decision.provider === 'openai') {
|
|
824
|
+
spawnSync('codex', [input], { stdio: 'inherit' });
|
|
825
|
+
} else {
|
|
826
|
+
spawnSync('claude', ['-p', input], { stdio: 'inherit' });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return { next: 'main' };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ─── Screen: settingsScreen ───────────────────────────────────────────────────
|
|
833
|
+
|
|
834
|
+
async function settingsScreen(rl, ask) {
|
|
835
|
+
const cwd = process.cwd();
|
|
836
|
+
const profile = loadProfile(cwd);
|
|
837
|
+
const auth = await detectAuth();
|
|
838
|
+
|
|
713
839
|
let guardCount = 0;
|
|
714
840
|
try {
|
|
715
841
|
const settingsFile = join(cwd, '.claude', 'settings.json');
|
|
716
842
|
if (existsSync(settingsFile)) {
|
|
717
843
|
const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
|
|
718
844
|
const preToolUse = settings?.hooks?.PreToolUse ?? [];
|
|
719
|
-
const guardCmd
|
|
720
|
-
const tierCmd
|
|
721
|
-
const hasEdit
|
|
722
|
-
const hasWrite
|
|
723
|
-
const hasBash
|
|
724
|
-
const hasAgent
|
|
845
|
+
const guardCmd = 'node .claude/hooks/head-guard.mjs';
|
|
846
|
+
const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
|
|
847
|
+
const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
|
|
848
|
+
const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
|
|
849
|
+
const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
|
|
850
|
+
const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
|
|
725
851
|
guardCount = [hasEdit, hasWrite, hasBash, hasAgent].filter(Boolean).length;
|
|
726
852
|
}
|
|
727
853
|
} catch { /* ignore */ }
|
|
728
854
|
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
855
|
+
const modeLabel = (m) => m === profile.mode ? `${m} (active)` : m;
|
|
856
|
+
|
|
857
|
+
const settingsLines = [
|
|
858
|
+
`Mode:`,
|
|
859
|
+
` [1] ${modeLabel('cost-saver')}`,
|
|
860
|
+
` [2] ${modeLabel('balanced')}`,
|
|
861
|
+
` [3] ${modeLabel('quality-first')}`,
|
|
862
|
+
'',
|
|
863
|
+
`Auth:`,
|
|
864
|
+
` Claude: ${auth.claude.found ? `connected (${auth.claude.source})` : 'missing'}`,
|
|
865
|
+
` OpenAI: ${auth.openai.found ? `connected (${auth.openai.source})` : 'missing'}`,
|
|
740
866
|
'',
|
|
741
|
-
|
|
742
|
-
`✓ Enforcement: ${guardCount} guards active`,
|
|
743
|
-
`✓ Auth: ${authSummary}`,
|
|
867
|
+
`Enforcement: ${guardCount}/4 guards active`,
|
|
744
868
|
];
|
|
745
869
|
|
|
746
|
-
console.log(box(`🧠 Dual-Brain v${version}`, dashLines));
|
|
747
870
|
console.log('');
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const recentSessions = importReplitSessions(cwd).slice(0, 5);
|
|
751
|
-
if (recentSessions.length > 0) {
|
|
752
|
-
console.log(separator('Recent Sessions'));
|
|
753
|
-
recentSessions.forEach((sess, i) => {
|
|
754
|
-
const activeIndicator = sess.isActive ? '●' : ' ';
|
|
755
|
-
const promptsLabel = `(${sess.promptCount} prompt${sess.promptCount !== 1 ? 's' : ''})`;
|
|
756
|
-
console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${activeIndicator} ${sess.name} ${promptsLabel}`);
|
|
757
|
-
});
|
|
758
|
-
console.log('');
|
|
759
|
-
}
|
|
760
|
-
|
|
871
|
+
console.log(box('Settings', settingsLines));
|
|
872
|
+
console.log('');
|
|
761
873
|
console.log(menu([
|
|
762
|
-
{ key: '
|
|
763
|
-
{ key: '
|
|
764
|
-
{ key: '
|
|
765
|
-
{ key: '
|
|
766
|
-
{ key: '
|
|
874
|
+
{ key: '1', label: 'Switch to cost-saver', section: 'Mode' },
|
|
875
|
+
{ key: '2', label: 'Switch to balanced', section: 'Mode' },
|
|
876
|
+
{ key: '3', label: 'Switch to quality-first', section: 'Mode' },
|
|
877
|
+
{ key: 'a', label: 'Add API key', section: 'Auth' },
|
|
878
|
+
{ key: 'i', label: 'Reinstall hooks', section: 'Enforcement' },
|
|
879
|
+
{ key: 'b', label: 'Back', section: '' },
|
|
767
880
|
]));
|
|
768
881
|
console.log('');
|
|
769
882
|
|
|
770
883
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
771
884
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
885
|
+
if (choice === '1' || choice === '2' || choice === '3') {
|
|
886
|
+
const modeMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
887
|
+
profile.mode = modeMap[choice];
|
|
888
|
+
saveProfile(profile, { cwd });
|
|
889
|
+
console.log(` Mode set to: ${profile.mode}`);
|
|
890
|
+
return { next: 'settings' };
|
|
776
891
|
}
|
|
777
892
|
|
|
778
|
-
if (choice === '
|
|
779
|
-
await
|
|
780
|
-
|
|
781
|
-
return { next: 'dashboard' };
|
|
893
|
+
if (choice === 'a') {
|
|
894
|
+
await setupAuth(rl);
|
|
895
|
+
return { next: 'settings' };
|
|
782
896
|
}
|
|
783
897
|
|
|
784
|
-
if (choice === '
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
898
|
+
if (choice === 'i') {
|
|
899
|
+
await cmdInstall();
|
|
900
|
+
return { next: 'settings' };
|
|
901
|
+
}
|
|
788
902
|
|
|
789
|
-
|
|
790
|
-
|
|
903
|
+
if (choice === 'b' || choice === 'back') { return { next: 'main' }; }
|
|
904
|
+
|
|
905
|
+
return { next: 'settings' };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
|
|
909
|
+
|
|
910
|
+
async function dashboardScreen(rl, ask) {
|
|
911
|
+
return { next: 'main' };
|
|
791
912
|
}
|
|
792
913
|
|
|
793
914
|
// ─── Screen: authScreen ───────────────────────────────────────────────────────
|
|
@@ -1278,12 +1399,15 @@ async function sessionDetailScreen(rl, ask, ctx = {}) {
|
|
|
1278
1399
|
// ─── Screen state machine ─────────────────────────────────────────────────────
|
|
1279
1400
|
|
|
1280
1401
|
const SCREENS = {
|
|
1281
|
-
welcome:
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1402
|
+
welcome: welcomeScreen,
|
|
1403
|
+
main: mainScreen,
|
|
1404
|
+
'new-session': newSessionScreen,
|
|
1405
|
+
settings: settingsScreen,
|
|
1406
|
+
dashboard: dashboardScreen,
|
|
1407
|
+
auth: authScreen,
|
|
1408
|
+
profile: profileScreen,
|
|
1409
|
+
diagnostics: diagnosticsScreen,
|
|
1410
|
+
repl: replScreen,
|
|
1287
1411
|
'session-detail': sessionDetailScreen,
|
|
1288
1412
|
};
|
|
1289
1413
|
|
|
@@ -1303,7 +1427,7 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
1303
1427
|
ctx = result?.session ? { session: result.session } : {};
|
|
1304
1428
|
} catch (e) {
|
|
1305
1429
|
console.error(`Error: ${e.message}`);
|
|
1306
|
-
current = '
|
|
1430
|
+
current = 'main';
|
|
1307
1431
|
ctx = {};
|
|
1308
1432
|
}
|
|
1309
1433
|
}
|
|
@@ -1326,8 +1450,7 @@ async function main() {
|
|
|
1326
1450
|
if (isInteractive) {
|
|
1327
1451
|
const cwd = process.cwd();
|
|
1328
1452
|
if (profileExists(cwd)) {
|
|
1329
|
-
|
|
1330
|
-
await runScreens('dashboard');
|
|
1453
|
+
await runScreens('main');
|
|
1331
1454
|
} else {
|
|
1332
1455
|
// First run: welcomeScreen handles auto-setup detection internally,
|
|
1333
1456
|
// then falls through to manual wizard if needed.
|
package/hooks/head-guard.mjs
CHANGED
|
@@ -15,7 +15,15 @@
|
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from 'fs';
|
|
17
17
|
|
|
18
|
-
const BLOCKED_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit'
|
|
18
|
+
const BLOCKED_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
19
|
+
|
|
20
|
+
// Patterns that indicate a Bash command is writing/mutating the filesystem.
|
|
21
|
+
// Anchored to avoid false positives on grep/find output containing these words.
|
|
22
|
+
const WRITE_BASH_RE = /\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bdd\b|\binstall\b|\btruncate\b|\btee\b|\bsed\s+-i\b|\bawk\s+-i\b|>>|(?<![><])>(?![>=])/;
|
|
23
|
+
|
|
24
|
+
function isBashWriteIntent(command) {
|
|
25
|
+
return WRITE_BASH_RE.test(command);
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
// Read stdin JSON payload
|
|
21
29
|
let input;
|
|
@@ -23,9 +31,16 @@ try {
|
|
|
23
31
|
const raw = readFileSync('/dev/stdin', 'utf8');
|
|
24
32
|
input = JSON.parse(raw);
|
|
25
33
|
} catch {
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
// Can't parse input — fail closed to avoid guard bypass.
|
|
35
|
+
const output = {
|
|
36
|
+
hookSpecificOutput: {
|
|
37
|
+
hookEventName: 'PreToolUse',
|
|
38
|
+
permissionDecision: 'deny',
|
|
39
|
+
permissionDecisionReason: '[dual-brain] head-guard could not parse hook input — blocking as a safety measure.',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
process.stdout.write(JSON.stringify(output));
|
|
43
|
+
process.exit(2);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
const toolName = input.tool_name || '';
|
|
@@ -50,9 +65,30 @@ if (BLOCKED_TOOLS.has(toolName)) {
|
|
|
50
65
|
process.exit(2);
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
if (toolName
|
|
68
|
+
// Bash: allow read-only commands; block write-intent ones.
|
|
69
|
+
// Always allow node .claude/hooks/ and node hooks/ — CLAUDE.md instructs HEAD to run these.
|
|
70
|
+
if (toolName === 'Bash') {
|
|
71
|
+
const command = (input.tool_input && input.tool_input.command) || '';
|
|
72
|
+
if (/^node\s+\.?(?:\.claude\/)?hooks\//.test(command.trimStart())) {
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
if (isBashWriteIntent(command)) {
|
|
76
|
+
const output = {
|
|
77
|
+
hookSpecificOutput: {
|
|
78
|
+
hookEventName: 'PreToolUse',
|
|
79
|
+
permissionDecision: 'deny',
|
|
80
|
+
permissionDecisionReason:
|
|
81
|
+
'[dual-brain] HEAD cannot run write-intent Bash commands. Dispatch via: dual-brain go "task description"',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
process.stdout.write(JSON.stringify(output));
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Block MCP filesystem write tools by name.
|
|
91
|
+
if (toolName.startsWith('mcp__') && /write|create|delete|remove|move|rename|append|patch|truncate|copy|commit|push|stage|merge|update|overwrite/i.test(toolName)) {
|
|
56
92
|
const output = {
|
|
57
93
|
hookSpecificOutput: {
|
|
58
94
|
hookEventName: 'PreToolUse',
|
|
@@ -445,7 +445,7 @@ test('enforce-tier: cost-saver demotes think', () => {
|
|
|
445
445
|
// cost-saver's demote_think=true demotes think→execute when text lacks think words
|
|
446
446
|
const payload = JSON.stringify({
|
|
447
447
|
tool_name: 'Agent',
|
|
448
|
-
tool_input: { prompt: 'edit the README file', model: 'opus' },
|
|
448
|
+
tool_input: { prompt: '<!-- dual-brain-dispatch: test23 -->edit the README file', model: 'opus' },
|
|
449
449
|
});
|
|
450
450
|
const { parsed, status } = run(ENFORCE_TIER, payload);
|
|
451
451
|
if (status !== 0) return `non-zero exit: ${status}`;
|
|
@@ -494,7 +494,7 @@ test('enforce-tier: auto profile with high-risk file', () => {
|
|
|
494
494
|
// Description with auth/credentials path → risk classifier detects critical risk → promote to think
|
|
495
495
|
const payload = JSON.stringify({
|
|
496
496
|
tool_name: 'Agent',
|
|
497
|
-
tool_input: { description: 'update src/auth/credentials.mjs', prompt: 'change the token logic', model: 'sonnet' },
|
|
497
|
+
tool_input: { description: 'update src/auth/credentials.mjs', prompt: '<!-- dual-brain-dispatch: test25 -->change the token logic', model: 'sonnet' },
|
|
498
498
|
});
|
|
499
499
|
const { parsed, status } = run(ENFORCE_TIER, payload);
|
|
500
500
|
if (status !== 0) return `non-zero exit: ${status}`;
|
|
@@ -740,9 +740,9 @@ test('install: preserves existing hooks', () => {
|
|
|
740
740
|
if (!installSrc.includes('.filter'))
|
|
741
741
|
return 'install.mjs missing .filter() call — may clobber non-dual-brain hooks';
|
|
742
742
|
|
|
743
|
-
// The merge logic should
|
|
744
|
-
if (!installSrc.includes('existingEntries'))
|
|
745
|
-
return 'install.mjs missing
|
|
743
|
+
// The merge logic should filter existing hooks before merging dual-brain hooks
|
|
744
|
+
if (!installSrc.includes('existingPre') && !installSrc.includes('existingEntries'))
|
|
745
|
+
return 'install.mjs missing existing hook preservation — may not preserve other hooks';
|
|
746
746
|
|
|
747
747
|
// Verify it reads existing settings before overwriting
|
|
748
748
|
if (!installSrc.includes('existing') || !installSrc.includes('settings.json'))
|
|
@@ -1017,7 +1017,7 @@ test('adaptive loop: end-to-end hash match', () => {
|
|
|
1017
1017
|
writeFileSync(LEDGER, '', 'utf8');
|
|
1018
1018
|
|
|
1019
1019
|
// Step 1: Define a specific Agent payload used consistently across all steps
|
|
1020
|
-
const toolInput = { prompt: 'fix the auth bug', description: 'patch auth module' };
|
|
1020
|
+
const toolInput = { prompt: '<!-- dual-brain-dispatch: test40 -->fix the auth bug', description: 'patch auth module' };
|
|
1021
1021
|
const agentPayload = JSON.stringify({ tool_name: 'Agent', tool_input: toolInput });
|
|
1022
1022
|
|
|
1023
1023
|
// Step 2: Run enforce-tier with this payload (computes and may log a promptHash)
|
package/mcp-server/index.mjs
CHANGED
|
@@ -253,7 +253,7 @@ async function handleRequest(msg) {
|
|
|
253
253
|
return respond(id, {
|
|
254
254
|
protocolVersion: '2024-11-05',
|
|
255
255
|
capabilities: { tools: {} },
|
|
256
|
-
serverInfo: { name: 'dual-brain', version: '7.1.
|
|
256
|
+
serverInfo: { name: 'dual-brain', version: '7.1.4' },
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
case 'initialized':
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/src/decide.mjs
CHANGED
|
@@ -71,10 +71,37 @@ const MODEL_CAPABILITIES = {
|
|
|
71
71
|
effortLevels: ['low', 'medium', 'high'],
|
|
72
72
|
costTier: 'medium',
|
|
73
73
|
},
|
|
74
|
+
'gpt-5.2': {
|
|
75
|
+
provider: 'openai',
|
|
76
|
+
tierFit: ['search', 'execute'],
|
|
77
|
+
contextWindow: 200_000,
|
|
78
|
+
costTier: 'medium',
|
|
79
|
+
strengths: ['code-generation', 'analysis'],
|
|
80
|
+
weaknesses: [],
|
|
81
|
+
effortLevels: null,
|
|
82
|
+
},
|
|
83
|
+
'gpt-5.4-mini': {
|
|
84
|
+
provider: 'openai',
|
|
85
|
+
tierFit: ['search'],
|
|
86
|
+
contextWindow: 200_000,
|
|
87
|
+
costTier: 'low',
|
|
88
|
+
strengths: ['quick-tasks', 'search'],
|
|
89
|
+
weaknesses: ['complex-edits', 'architecture'],
|
|
90
|
+
effortLevels: null,
|
|
91
|
+
},
|
|
92
|
+
'gpt-5.3-codex': {
|
|
93
|
+
provider: 'openai',
|
|
94
|
+
tierFit: ['execute'],
|
|
95
|
+
contextWindow: 200_000,
|
|
96
|
+
costTier: 'medium',
|
|
97
|
+
strengths: ['code-generation', 'refactoring'],
|
|
98
|
+
weaknesses: ['architecture', 'security'],
|
|
99
|
+
effortLevels: null,
|
|
100
|
+
},
|
|
74
101
|
'gpt-5.4': {
|
|
75
102
|
provider: 'openai',
|
|
76
103
|
tierFit: ['execute', 'think'],
|
|
77
|
-
contextWindow:
|
|
104
|
+
contextWindow: 1_050_000,
|
|
78
105
|
strengths: ['refactor', 'debug', 'code-generation', 'test'],
|
|
79
106
|
weaknesses: ['cost'],
|
|
80
107
|
effortLevels: ['low', 'medium', 'high', 'xhigh'],
|
|
@@ -83,7 +110,7 @@ const MODEL_CAPABILITIES = {
|
|
|
83
110
|
'gpt-5.5': {
|
|
84
111
|
provider: 'openai',
|
|
85
112
|
tierFit: ['think'],
|
|
86
|
-
contextWindow:
|
|
113
|
+
contextWindow: 1_000_000,
|
|
87
114
|
strengths: ['architecture', 'security', 'review', 'planning', 'complex-debug'],
|
|
88
115
|
weaknesses: ['cost', 'latency'],
|
|
89
116
|
effortLevels: ['low', 'medium', 'high', 'xhigh'],
|
|
@@ -264,16 +291,19 @@ function applyHealthDowngrade(model, score, provider, available, isHighStakes) {
|
|
|
264
291
|
}
|
|
265
292
|
}
|
|
266
293
|
|
|
267
|
-
function applyProfileBias(model, profile, provider, available) {
|
|
294
|
+
function applyProfileBias(model, profile, provider, available, tier) {
|
|
268
295
|
const mode = profile?.mode || profile?.profile || 'auto';
|
|
269
296
|
if (mode === 'cost-saver') {
|
|
270
|
-
// Prefer cheapest available
|
|
297
|
+
// Prefer cheapest available that also fits the required tier
|
|
271
298
|
const ranks = {
|
|
272
299
|
claude: ['haiku', 'sonnet', 'opus'],
|
|
273
300
|
openai: ['gpt-4.1-mini', 'gpt-4.1', 'gpt-5.2', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.4', 'gpt-5.5'],
|
|
274
301
|
};
|
|
275
302
|
for (const m of ranks[provider]) {
|
|
276
|
-
if (available.includes(m))
|
|
303
|
+
if (!available.includes(m)) continue;
|
|
304
|
+
const caps = MODEL_CAPABILITIES[m];
|
|
305
|
+
if (tier && caps && !caps.tierFit.includes(tier)) continue;
|
|
306
|
+
return m;
|
|
277
307
|
}
|
|
278
308
|
}
|
|
279
309
|
if (mode === 'quality-first') {
|
|
@@ -535,7 +565,7 @@ export function decideRoute({ profile = {}, detection = {}, cwd } = {}) {
|
|
|
535
565
|
model = applyHealthDowngrade(model, healthScores[provider], provider, available[provider], isHighStakes);
|
|
536
566
|
|
|
537
567
|
// Apply profile mode bias (cost-saver / quality-first / preferences) using patched profile
|
|
538
|
-
model = applyProfileBias(model, profileWithEffectiveBias, provider, available[provider]);
|
|
568
|
+
model = applyProfileBias(model, profileWithEffectiveBias, provider, available[provider], detection.tier);
|
|
539
569
|
|
|
540
570
|
// Safety floor: critical-risk tasks must never use haiku/gpt-4.1-mini even in cost-saver mode
|
|
541
571
|
model = applyCriticalRiskFloor(model, provider, available[provider], detection.risk);
|
package/src/dispatch.mjs
CHANGED
|
@@ -647,13 +647,14 @@ async function dispatch(input = {}) {
|
|
|
647
647
|
}
|
|
648
648
|
_recordDispatchBudget(prompt);
|
|
649
649
|
return {
|
|
650
|
-
status: '
|
|
650
|
+
status: 'completed',
|
|
651
|
+
type: 'native-agent',
|
|
651
652
|
provider: effectiveProvider,
|
|
652
653
|
model: effectiveModel,
|
|
653
654
|
command: null,
|
|
654
655
|
nativeDispatch: nativeDescriptor,
|
|
655
|
-
exitCode:
|
|
656
|
-
summary: `
|
|
656
|
+
exitCode: 0,
|
|
657
|
+
summary: `Routed to ${effectiveProvider}/${effectiveModel} (${effectiveDecision.tier})`,
|
|
657
658
|
durationMs: 0,
|
|
658
659
|
usage: null,
|
|
659
660
|
error: null,
|
package/src/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* orchestrate() convenience function for programmatic use.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export { loadProfile, saveProfile, ensureProfile, runOnboarding, rememberPreference, forgetPreference, getActivePreferences, getAvailableProviders, isSoloBrain, getHeadModel, detectAuth, detectEnvironment, setupAuth, getActiveKey
|
|
9
|
+
export { loadProfile, saveProfile, ensureProfile, runOnboarding, rememberPreference, forgetPreference, getActivePreferences, getAvailableProviders, isSoloBrain, getHeadModel, detectAuth, detectEnvironment, setupAuth, getActiveKey } from './profile.mjs';
|
|
10
10
|
export { detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths } from './detect.mjs';
|
|
11
11
|
export { decideRoute, getModelCapabilities, getAvailableModels, shouldDualBrain, explainDecision } from './decide.mjs';
|
|
12
12
|
export { dispatch, buildCommand, detectRuntime, compressResult, dispatchDualBrain } from './dispatch.mjs';
|
package/src/profile.mjs
CHANGED
|
@@ -269,46 +269,13 @@ async function detectAuth() {
|
|
|
269
269
|
const AUTH_FILE = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'auth.json');
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
*
|
|
273
|
-
* Old: { claude: { key, savedAt, expiresAt }, openai: { ... } }
|
|
274
|
-
* New: { claude: [{ key, label, savedAt, expiresAt, priority, enabled }], openai: [...] }
|
|
275
|
-
* @param {object} auth
|
|
276
|
-
* @returns {object} migrated auth object
|
|
277
|
-
*/
|
|
278
|
-
function _migrateAuthFormat(auth) {
|
|
279
|
-
const migrated = {};
|
|
280
|
-
for (const [provider, value] of Object.entries(auth)) {
|
|
281
|
-
if (Array.isArray(value)) {
|
|
282
|
-
// Already new format
|
|
283
|
-
migrated[provider] = value;
|
|
284
|
-
} else if (value && typeof value === 'object' && value.key) {
|
|
285
|
-
// Old single-key format — wrap in array
|
|
286
|
-
migrated[provider] = [
|
|
287
|
-
{
|
|
288
|
-
key: value.key,
|
|
289
|
-
label: 'primary',
|
|
290
|
-
savedAt: value.savedAt || new Date().toISOString(),
|
|
291
|
-
expiresAt: value.expiresAt || null,
|
|
292
|
-
priority: 1,
|
|
293
|
-
enabled: true,
|
|
294
|
-
},
|
|
295
|
-
];
|
|
296
|
-
} else {
|
|
297
|
-
migrated[provider] = value;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return migrated;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Load .dualbrain/auth.json, migrating old single-key format to array format.
|
|
272
|
+
* Load .dualbrain/auth.json.
|
|
305
273
|
* @param {string} [cwd]
|
|
306
274
|
* @returns {object} auth object with arrays per provider
|
|
307
275
|
*/
|
|
308
276
|
function loadAuthKeys(cwd) {
|
|
309
277
|
try {
|
|
310
|
-
|
|
311
|
-
return _migrateAuthFormat(raw);
|
|
278
|
+
return JSON.parse(readFileSync(AUTH_FILE(cwd), 'utf8'));
|
|
312
279
|
} catch {
|
|
313
280
|
return {};
|
|
314
281
|
}
|
|
@@ -380,73 +347,6 @@ function saveAuthKey(provider, key, opts = {}) {
|
|
|
380
347
|
}
|
|
381
348
|
}
|
|
382
349
|
|
|
383
|
-
/**
|
|
384
|
-
* Remove a key by index from the provider's array.
|
|
385
|
-
* @param {string} provider
|
|
386
|
-
* @param {number} index
|
|
387
|
-
* @param {string} [cwd]
|
|
388
|
-
*/
|
|
389
|
-
function removeAuthKey(provider, index, cwd) {
|
|
390
|
-
const authFile = AUTH_FILE(cwd);
|
|
391
|
-
const dir = dirname(authFile);
|
|
392
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
393
|
-
|
|
394
|
-
const auth = loadAuthKeys(cwd);
|
|
395
|
-
if (!Array.isArray(auth[provider])) return;
|
|
396
|
-
|
|
397
|
-
auth[provider].splice(index, 1);
|
|
398
|
-
writeFileSync(authFile, JSON.stringify(auth, null, 2));
|
|
399
|
-
chmodSync(authFile, 0o600);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Mark a key as enabled:false (used during failover when a key hits rate limits).
|
|
404
|
-
* @param {string} provider
|
|
405
|
-
* @param {number} index
|
|
406
|
-
* @param {string} [cwd]
|
|
407
|
-
*/
|
|
408
|
-
function disableKey(provider, index, cwd) {
|
|
409
|
-
const authFile = AUTH_FILE(cwd);
|
|
410
|
-
const dir = dirname(authFile);
|
|
411
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
412
|
-
|
|
413
|
-
const auth = loadAuthKeys(cwd);
|
|
414
|
-
if (!Array.isArray(auth[provider]) || !auth[provider][index]) return;
|
|
415
|
-
|
|
416
|
-
auth[provider][index].enabled = false;
|
|
417
|
-
writeFileSync(authFile, JSON.stringify(auth, null, 2));
|
|
418
|
-
chmodSync(authFile, 0o600);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Called when the active key hits a rate limit. Disables the current active key
|
|
423
|
-
* temporarily and returns the next valid key, or null if none available.
|
|
424
|
-
* @param {string} provider
|
|
425
|
-
* @param {string} [cwd]
|
|
426
|
-
* @returns {{ key: string, label: string, priority: number, enabled: boolean, expiresAt: string|null }|null}
|
|
427
|
-
*/
|
|
428
|
-
function rotateToNextKey(provider, cwd) {
|
|
429
|
-
const auth = loadAuthKeys(cwd);
|
|
430
|
-
const keys = auth[provider] || [];
|
|
431
|
-
const now = new Date();
|
|
432
|
-
|
|
433
|
-
// Find current active key index
|
|
434
|
-
const sortedValid = keys
|
|
435
|
-
.map((k, i) => ({ ...k, _idx: i }))
|
|
436
|
-
.filter(k => k.enabled)
|
|
437
|
-
.filter(k => !k.expiresAt || new Date(k.expiresAt) > now)
|
|
438
|
-
.sort((a, b) => (a.priority || 99) - (b.priority || 99));
|
|
439
|
-
|
|
440
|
-
if (sortedValid.length === 0) return null;
|
|
441
|
-
|
|
442
|
-
// Disable the current active key
|
|
443
|
-
const currentIdx = sortedValid[0]._idx;
|
|
444
|
-
disableKey(provider, currentIdx, cwd);
|
|
445
|
-
|
|
446
|
-
// Reload and get the next valid key
|
|
447
|
-
return getActiveKey(provider, cwd);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
350
|
/**
|
|
451
351
|
* Interactive setup flow: walks user through entering API keys for missing providers.
|
|
452
352
|
* Accepts an existing readline Interface (rl) — does NOT close it.
|
|
@@ -974,6 +874,6 @@ export {
|
|
|
974
874
|
detectPlans, syncPreferencesToMemory,
|
|
975
875
|
detectAuth, detectEnvironment,
|
|
976
876
|
setupAuth, saveAuthKey, loadAuthKeys,
|
|
977
|
-
getActiveKey,
|
|
877
|
+
getActiveKey,
|
|
978
878
|
defaultProfile, autoSetup,
|
|
979
879
|
};
|