dual-brain 7.1.15 → 7.1.17
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 +514 -29
- package/install.mjs +37 -0
- package/package.json +2 -2
- package/src/decide.mjs +56 -56
- package/src/detect.mjs +110 -6
- package/src/dispatch.mjs +155 -20
- package/src/profile.mjs +228 -2
- package/src/session.mjs +5 -3
package/bin/dual-brain.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// dual-brain — CLI entry point. Commands: init, go, status, remember, forget
|
|
3
3
|
|
|
4
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { appendFileSync, existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync, unlinkSync } from 'node:fs';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { execSync, spawnSync as _spawnSyncTop } from 'node:child_process';
|
|
@@ -169,6 +169,14 @@ Commands:
|
|
|
169
169
|
remember "preference" Save a project-scoped preference
|
|
170
170
|
forget "preference" Remove a preference by fuzzy match
|
|
171
171
|
search "keyword" Search across all sessions
|
|
172
|
+
specialists List available specialist agents with descriptions
|
|
173
|
+
python "task" Force Python specialist for the task
|
|
174
|
+
typescript "task" Force TypeScript specialist for the task
|
|
175
|
+
html "task" Force HTML/CSS specialist for the task
|
|
176
|
+
linux "task" Force Linux/DevOps specialist for the task
|
|
177
|
+
security "task" Force Security specialist for the task
|
|
178
|
+
--dry-run (specialist commands) Show routing without executing
|
|
179
|
+
--files a,b (specialist commands) Provide file context
|
|
172
180
|
shell-hook Output bash snippet to add dual-brain to your shell
|
|
173
181
|
Usage: dual-brain shell-hook >> ~/.bashrc
|
|
174
182
|
|
|
@@ -391,27 +399,6 @@ async function cmdGo(args) {
|
|
|
391
399
|
}, cwd);
|
|
392
400
|
} else {
|
|
393
401
|
result = await dispatch({ decision, prompt, files, cwd });
|
|
394
|
-
if (result.status === 'completed' && result.type === 'native-agent') {
|
|
395
|
-
const nd = result.nativeDispatch || {};
|
|
396
|
-
const promptPreview = (nd.prompt || prompt).slice(0, 100);
|
|
397
|
-
const promptSuffix = (nd.prompt || prompt).length > 100 ? '...' : '';
|
|
398
|
-
console.log(`\nRouted: ${decision.provider}/${nd.model || decision.model} (${decision.tier})`);
|
|
399
|
-
console.log('To dispatch, use the Agent tool with:');
|
|
400
|
-
console.log(` model: ${nd.model || decision.model}`);
|
|
401
|
-
console.log(` prompt: ${promptPreview}${promptSuffix}`);
|
|
402
|
-
if (nd.isolation) console.log(` isolation: ${nd.isolation}`);
|
|
403
|
-
if (nd.maxTurns) console.log(` maxTurns: ${nd.maxTurns}`);
|
|
404
|
-
saveSession({
|
|
405
|
-
objective: prompt,
|
|
406
|
-
branch: null,
|
|
407
|
-
filesChanged: files,
|
|
408
|
-
commandsRun: [`dual-brain go "${prompt}"`],
|
|
409
|
-
lastResult: { status: 'success', summary: `native-agent routed to ${nd.model || decision.model}` },
|
|
410
|
-
provider: decision.provider,
|
|
411
|
-
nextAction: null,
|
|
412
|
-
}, cwd);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
402
|
const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
416
403
|
console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
417
404
|
if (result.summary) console.log(result.summary);
|
|
@@ -632,6 +619,53 @@ function cmdForget(text) {
|
|
|
632
619
|
console.log('Preference removed (if matched).');
|
|
633
620
|
}
|
|
634
621
|
|
|
622
|
+
function cmdBreakGlass(reason) {
|
|
623
|
+
if (!reason) err('Usage: dual-brain break-glass "reason"');
|
|
624
|
+
const cwd = process.cwd();
|
|
625
|
+
const dualbrain = join(cwd, '.dualbrain');
|
|
626
|
+
const tokenPath = join(dualbrain, 'break-glass.json');
|
|
627
|
+
const auditDir = join(dualbrain, 'audit');
|
|
628
|
+
const auditFile = join(auditDir, 'head-audit.jsonl');
|
|
629
|
+
const TTL_MINUTES = 5;
|
|
630
|
+
|
|
631
|
+
mkdirSync(dualbrain, { recursive: true });
|
|
632
|
+
mkdirSync(auditDir, { recursive: true });
|
|
633
|
+
|
|
634
|
+
const token = {
|
|
635
|
+
createdAt: Date.now(),
|
|
636
|
+
ttlMinutes: TTL_MINUTES,
|
|
637
|
+
reason,
|
|
638
|
+
};
|
|
639
|
+
writeFileSync(tokenPath, JSON.stringify(token, null, 2));
|
|
640
|
+
|
|
641
|
+
// Audit entry
|
|
642
|
+
const auditEntry = {
|
|
643
|
+
ts: new Date().toISOString(),
|
|
644
|
+
event: 'break-glass-activated',
|
|
645
|
+
reason,
|
|
646
|
+
ttlMinutes: TTL_MINUTES,
|
|
647
|
+
expiresAt: new Date(token.createdAt + TTL_MINUTES * 60 * 1000).toISOString(),
|
|
648
|
+
};
|
|
649
|
+
try {
|
|
650
|
+
appendFileSync(auditFile, JSON.stringify(auditEntry) + '\n');
|
|
651
|
+
} catch { /* non-fatal */ }
|
|
652
|
+
|
|
653
|
+
const width = 51;
|
|
654
|
+
const inner = width - 2;
|
|
655
|
+
const pad = (s) => ' ' + s + ' '.repeat(inner - 1 - s.length);
|
|
656
|
+
const reasonLine = `Reason: ${reason}`;
|
|
657
|
+
const expiresLine = `Expires: ${TTL_MINUTES} minutes`;
|
|
658
|
+
const auditLine = 'All tool calls logged to audit.';
|
|
659
|
+
|
|
660
|
+
console.log('┌' + '─'.repeat(inner) + '┐');
|
|
661
|
+
console.log('│' + pad('🔓 Break-Glass Activated') + '│');
|
|
662
|
+
console.log('├' + '─'.repeat(inner) + '┤');
|
|
663
|
+
console.log('│' + pad(reasonLine) + '│');
|
|
664
|
+
console.log('│' + pad(expiresLine) + '│');
|
|
665
|
+
console.log('│' + pad(auditLine) + '│');
|
|
666
|
+
console.log('└' + '─'.repeat(inner) + '┘');
|
|
667
|
+
}
|
|
668
|
+
|
|
635
669
|
// ─── Screen helpers ───────────────────────────────────────────────────────────
|
|
636
670
|
|
|
637
671
|
/**
|
|
@@ -1603,6 +1637,282 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
1603
1637
|
return { next: 'main' };
|
|
1604
1638
|
}
|
|
1605
1639
|
|
|
1640
|
+
// ─── Onboarding Wizard ───────────────────────────────────────────────────────
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* 5-step onboarding wizard shown on first run (no .dualbrain/profile.json).
|
|
1644
|
+
* Matches the rounded ┌─┐ box style used in mainScreen / renderHeader.
|
|
1645
|
+
* @param {{ auth, plans, existingSessions }} detection
|
|
1646
|
+
* @param {string} cwd
|
|
1647
|
+
* @param {object} rl readline interface
|
|
1648
|
+
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
1649
|
+
*/
|
|
1650
|
+
async function runOnboardingWizard(detection, cwd, rl) {
|
|
1651
|
+
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
1652
|
+
const version = readVersion();
|
|
1653
|
+
|
|
1654
|
+
// ── Rounded box helpers (matching mainScreen style) ────────────────────────
|
|
1655
|
+
const W = 51;
|
|
1656
|
+
const wTop = ` ┌${'─'.repeat(W)}┐`;
|
|
1657
|
+
const wSep = ` ├${'─'.repeat(W)}┤`;
|
|
1658
|
+
const wBottom = ` └${'─'.repeat(W)}┘`;
|
|
1659
|
+
const wPad = (s) => {
|
|
1660
|
+
const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1661
|
+
let vlen = 0;
|
|
1662
|
+
for (const ch of plain) {
|
|
1663
|
+
const cp = ch.codePointAt(0);
|
|
1664
|
+
if (
|
|
1665
|
+
(cp >= 0x1f300 && cp <= 0x1faff) ||
|
|
1666
|
+
(cp >= 0x2600 && cp <= 0x27bf) ||
|
|
1667
|
+
cp === 0xfe0f || cp === 0x20e3
|
|
1668
|
+
) { vlen += 2; } else { vlen += 1; }
|
|
1669
|
+
}
|
|
1670
|
+
return s + ' '.repeat(Math.max(0, W - vlen));
|
|
1671
|
+
};
|
|
1672
|
+
const wRow = (s) => ` │ ${wPad(s)}│`;
|
|
1673
|
+
|
|
1674
|
+
// ── Collected wizard state ─────────────────────────────────────────────────
|
|
1675
|
+
const state = {
|
|
1676
|
+
claudePlan: null,
|
|
1677
|
+
openaiPlan: null,
|
|
1678
|
+
headModel: null,
|
|
1679
|
+
importSessions: false,
|
|
1680
|
+
profile: 'auto',
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
const { auth, plans, existingSessions } = detection;
|
|
1684
|
+
const claudeReady = auth.claude.found;
|
|
1685
|
+
const openaiReady = auth.openai.found;
|
|
1686
|
+
|
|
1687
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1688
|
+
// Step 1 — Welcome & provider detection
|
|
1689
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1690
|
+
console.log('');
|
|
1691
|
+
console.log(wTop);
|
|
1692
|
+
console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
|
|
1693
|
+
console.log(wSep);
|
|
1694
|
+
console.log(wRow(`Step 1 of 5: Detected providers`));
|
|
1695
|
+
console.log(wSep);
|
|
1696
|
+
|
|
1697
|
+
const claudePlanLabel = claudeReady
|
|
1698
|
+
? (CLAUDE_PLAN_LABELS[plans.claude] ?? plans.claude ?? 'plan unknown')
|
|
1699
|
+
: null;
|
|
1700
|
+
const openaiPlanLabel = openaiReady
|
|
1701
|
+
? (OPENAI_PLAN_LABELS[plans.openai] ?? plans.openai ?? 'plan unknown')
|
|
1702
|
+
: null;
|
|
1703
|
+
|
|
1704
|
+
console.log(wRow(claudeReady
|
|
1705
|
+
? `✓ Claude CLI ${claudePlanLabel ? `(${claudePlanLabel})` : ''}`
|
|
1706
|
+
: `✗ Claude CLI not logged in`));
|
|
1707
|
+
console.log(wRow(openaiReady
|
|
1708
|
+
? `✓ Codex CLI ${openaiPlanLabel ? `(${openaiPlanLabel})` : ''}`
|
|
1709
|
+
: `✗ Codex CLI not logged in`));
|
|
1710
|
+
if (existingSessions.length > 0) {
|
|
1711
|
+
console.log(wRow(`✓ ${existingSessions.length} data-tools session${existingSessions.length !== 1 ? 's' : ''} found`));
|
|
1712
|
+
}
|
|
1713
|
+
console.log(wSep);
|
|
1714
|
+
console.log(wRow(`[Enter] Continue setup [s] Skip wizard`));
|
|
1715
|
+
console.log(wBottom);
|
|
1716
|
+
console.log('');
|
|
1717
|
+
|
|
1718
|
+
if (!claudeReady && !openaiReady) {
|
|
1719
|
+
console.log(' No AI provider found. Log in first:');
|
|
1720
|
+
console.log(' claude auth login — for Claude');
|
|
1721
|
+
console.log(' codex login — for OpenAI/Codex');
|
|
1722
|
+
console.log(' Then re-run: dual-brain init\n');
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
const step1 = (await ask(' > ')).trim().toLowerCase();
|
|
1727
|
+
if (step1 === 's') {
|
|
1728
|
+
// Skip: auto-save detected plans and proceed directly
|
|
1729
|
+
const skippedProfile = loadProfile(cwd);
|
|
1730
|
+
if (claudeReady) skippedProfile.providers.claude = { enabled: true, plan: plans.claude || 'pro' };
|
|
1731
|
+
if (openaiReady) skippedProfile.providers.openai = { enabled: true, plan: plans.openai || 'plus' };
|
|
1732
|
+
const enabledCount = [claudeReady, openaiReady].filter(Boolean).length;
|
|
1733
|
+
skippedProfile.mode = enabledCount >= 2 ? 'auto' : claudeReady ? 'solo-claude' : 'solo-openai';
|
|
1734
|
+
return skippedProfile;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1738
|
+
// Step 2 — Budget / plan selection
|
|
1739
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1740
|
+
console.log('');
|
|
1741
|
+
console.log(wTop);
|
|
1742
|
+
console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
|
|
1743
|
+
console.log(wSep);
|
|
1744
|
+
console.log(wRow(`Step 2 of 5: Subscription plans`));
|
|
1745
|
+
console.log(wSep);
|
|
1746
|
+
|
|
1747
|
+
if (claudeReady) {
|
|
1748
|
+
const detectedClaudePlan = plans.claude || 'pro';
|
|
1749
|
+
const detectedClaudeLabel = CLAUDE_PLAN_LABELS[detectedClaudePlan] ?? detectedClaudePlan;
|
|
1750
|
+
console.log(wRow(`Claude — detected: ${detectedClaudeLabel}`));
|
|
1751
|
+
console.log(wRow(` [1] Pro ($20/mo)`));
|
|
1752
|
+
console.log(wRow(` [2] Max x5 ($100/mo)`));
|
|
1753
|
+
console.log(wRow(` [3] Max x20 ($200/mo)`));
|
|
1754
|
+
console.log(wRow(` [Enter] Keep detected (${detectedClaudeLabel})`));
|
|
1755
|
+
console.log(wSep);
|
|
1756
|
+
const claudeChoice = (await ask(' Claude plan [1/2/3/Enter]: ')).trim();
|
|
1757
|
+
const claudePlanMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
|
|
1758
|
+
state.claudePlan = claudePlanMap[claudeChoice] || detectedClaudePlan;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
if (openaiReady) {
|
|
1762
|
+
const detectedOpenaiPlan = plans.openai || 'plus';
|
|
1763
|
+
const detectedOpenaiLabel = OPENAI_PLAN_LABELS[detectedOpenaiPlan] ?? detectedOpenaiPlan;
|
|
1764
|
+
console.log(wRow(`OpenAI — detected: ${detectedOpenaiLabel}`));
|
|
1765
|
+
console.log(wRow(` [1] Plus ($20/mo)`));
|
|
1766
|
+
console.log(wRow(` [2] Pro ($100/mo)`));
|
|
1767
|
+
console.log(wRow(` [3] Pro ($200/mo higher limits)`));
|
|
1768
|
+
console.log(wRow(` [Enter] Keep detected (${detectedOpenaiLabel})`));
|
|
1769
|
+
console.log(wSep);
|
|
1770
|
+
const openaiChoice = (await ask(' OpenAI plan [1/2/3/Enter]: ')).trim();
|
|
1771
|
+
const openaiPlanMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
|
|
1772
|
+
state.openaiPlan = openaiPlanMap[openaiChoice] || detectedOpenaiPlan;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
console.log(wBottom);
|
|
1776
|
+
|
|
1777
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1778
|
+
// Step 3 — HEAD model selection
|
|
1779
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1780
|
+
const hasBigPlan = state.claudePlan === 'max5' || state.claudePlan === 'max20';
|
|
1781
|
+
const recommendedModel = hasBigPlan ? 'claude-opus-4-5' : 'claude-sonnet-4-5';
|
|
1782
|
+
const recommendedLabel = hasBigPlan
|
|
1783
|
+
? 'Opus (Max plan — best quality)'
|
|
1784
|
+
: 'Sonnet (Pro plan — balanced speed/quality)';
|
|
1785
|
+
|
|
1786
|
+
console.log('');
|
|
1787
|
+
console.log(wTop);
|
|
1788
|
+
console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
|
|
1789
|
+
console.log(wSep);
|
|
1790
|
+
console.log(wRow(`Step 3 of 5: HEAD model (think-tier)`));
|
|
1791
|
+
console.log(wSep);
|
|
1792
|
+
console.log(wRow(`Recommended: ${recommendedLabel}`));
|
|
1793
|
+
console.log(wSep);
|
|
1794
|
+
console.log(wRow(` [1] Haiku — fastest, lowest cost`));
|
|
1795
|
+
console.log(wRow(` [2] Sonnet — balanced (recommended for Pro)`));
|
|
1796
|
+
console.log(wRow(` [3] Opus — best quality (recommended for Max)`));
|
|
1797
|
+
console.log(wRow(` [Enter] Use recommended`));
|
|
1798
|
+
console.log(wBottom);
|
|
1799
|
+
console.log('');
|
|
1800
|
+
|
|
1801
|
+
const step3 = (await ask(' HEAD model [1/2/3/Enter]: ')).trim();
|
|
1802
|
+
const modelMap = {
|
|
1803
|
+
'1': 'claude-haiku-4-5',
|
|
1804
|
+
'2': 'claude-sonnet-4-5',
|
|
1805
|
+
'3': 'claude-opus-4-5',
|
|
1806
|
+
};
|
|
1807
|
+
state.headModel = modelMap[step3] || recommendedModel;
|
|
1808
|
+
|
|
1809
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1810
|
+
// Step 4 — Import sessions + profile selection
|
|
1811
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1812
|
+
console.log('');
|
|
1813
|
+
console.log(wTop);
|
|
1814
|
+
console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
|
|
1815
|
+
console.log(wSep);
|
|
1816
|
+
console.log(wRow(`Step 4 of 5: Sessions & routing profile`));
|
|
1817
|
+
console.log(wSep);
|
|
1818
|
+
|
|
1819
|
+
if (existingSessions.length > 0) {
|
|
1820
|
+
console.log(wRow(`Import ${existingSessions.length} data-tools session${existingSessions.length !== 1 ? 's' : ''}?`));
|
|
1821
|
+
console.log(wRow(` [y] Yes [Enter/n] Skip`));
|
|
1822
|
+
console.log(wSep);
|
|
1823
|
+
const importChoice = (await ask(' Import sessions [y/Enter]: ')).trim().toLowerCase();
|
|
1824
|
+
state.importSessions = importChoice === 'y';
|
|
1825
|
+
if (state.importSessions) {
|
|
1826
|
+
console.log('');
|
|
1827
|
+
console.log(` Importing ${existingSessions.length} sessions...`);
|
|
1828
|
+
const recent = existingSessions.slice(0, 5);
|
|
1829
|
+
for (const sess of recent) {
|
|
1830
|
+
console.log(` ${sess.age.padEnd(6)} ${sess.name}`);
|
|
1831
|
+
}
|
|
1832
|
+
if (existingSessions.length > 5) {
|
|
1833
|
+
console.log(` ... and ${existingSessions.length - 5} more`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
console.log(wSep);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
console.log(wRow(`Routing profile:`));
|
|
1840
|
+
console.log(wRow(` [1] auto — adapts based on task risk & outcomes`));
|
|
1841
|
+
console.log(wRow(` [2] balanced — best model per tier, normal budgets`));
|
|
1842
|
+
console.log(wRow(` [3] cost-saver — prefer cheaper models, skip GPT`));
|
|
1843
|
+
console.log(wRow(` [4] quality-first — dual-brain for medium+ risk`));
|
|
1844
|
+
console.log(wRow(` [Enter] auto (recommended)`));
|
|
1845
|
+
console.log(wBottom);
|
|
1846
|
+
console.log('');
|
|
1847
|
+
|
|
1848
|
+
const step4 = (await ask(' Profile [1/2/3/4/Enter]: ')).trim();
|
|
1849
|
+
const profileMap = { '1': 'auto', '2': 'balanced', '3': 'cost-saver', '4': 'quality-first' };
|
|
1850
|
+
state.profile = profileMap[step4] || 'auto';
|
|
1851
|
+
|
|
1852
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1853
|
+
// Step 5 — Summary & confirm
|
|
1854
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1855
|
+
const claudeSummary = state.claudePlan
|
|
1856
|
+
? `Claude: ${CLAUDE_PLAN_LABELS[state.claudePlan] ?? state.claudePlan}`
|
|
1857
|
+
: `Claude: not configured`;
|
|
1858
|
+
const openaiSummary = state.openaiPlan
|
|
1859
|
+
? `OpenAI: ${OPENAI_PLAN_LABELS[state.openaiPlan] ?? state.openaiPlan}`
|
|
1860
|
+
: `OpenAI: not configured`;
|
|
1861
|
+
const modelSummary = `HEAD model: ${state.headModel}`;
|
|
1862
|
+
const profileSummary = `Profile: ${state.profile}`;
|
|
1863
|
+
const sessionSummary = existingSessions.length > 0
|
|
1864
|
+
? `Sessions: ${state.importSessions ? `${existingSessions.length} imported` : 'skipped'}`
|
|
1865
|
+
: null;
|
|
1866
|
+
|
|
1867
|
+
console.log('');
|
|
1868
|
+
console.log(wTop);
|
|
1869
|
+
console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
|
|
1870
|
+
console.log(wSep);
|
|
1871
|
+
console.log(wRow(`Step 5 of 5: Summary`));
|
|
1872
|
+
console.log(wSep);
|
|
1873
|
+
console.log(wRow(claudeSummary));
|
|
1874
|
+
console.log(wRow(openaiSummary));
|
|
1875
|
+
console.log(wRow(modelSummary));
|
|
1876
|
+
console.log(wRow(profileSummary));
|
|
1877
|
+
if (sessionSummary) console.log(wRow(sessionSummary));
|
|
1878
|
+
console.log(wSep);
|
|
1879
|
+
console.log(wRow(`[Enter] Save and start [q] Quit without saving`));
|
|
1880
|
+
console.log(wBottom);
|
|
1881
|
+
console.log('');
|
|
1882
|
+
|
|
1883
|
+
const step5 = (await ask(' > ')).trim().toLowerCase();
|
|
1884
|
+
if (step5 === 'q') {
|
|
1885
|
+
console.log('\n Setup cancelled.\n');
|
|
1886
|
+
return null;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// ── Build and return the profile object ────────────────────────────────────
|
|
1890
|
+
const finalProfile = loadProfile(cwd);
|
|
1891
|
+
|
|
1892
|
+
if (state.claudePlan) {
|
|
1893
|
+
finalProfile.providers.claude = { enabled: true, plan: state.claudePlan };
|
|
1894
|
+
} else if (claudeReady) {
|
|
1895
|
+
finalProfile.providers.claude = { enabled: true, plan: plans.claude || 'pro' };
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (state.openaiPlan) {
|
|
1899
|
+
finalProfile.providers.openai = { enabled: true, plan: state.openaiPlan };
|
|
1900
|
+
} else if (openaiReady) {
|
|
1901
|
+
finalProfile.providers.openai = { enabled: true, plan: plans.openai || 'plus' };
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const enabledCount = [
|
|
1905
|
+
finalProfile.providers?.claude?.enabled,
|
|
1906
|
+
finalProfile.providers?.openai?.enabled,
|
|
1907
|
+
].filter(Boolean).length;
|
|
1908
|
+
|
|
1909
|
+
finalProfile.mode = enabledCount >= 2 ? state.profile : claudeReady ? 'solo-claude' : 'solo-openai';
|
|
1910
|
+
finalProfile.headModel = state.headModel;
|
|
1911
|
+
finalProfile.bias = state.profile;
|
|
1912
|
+
|
|
1913
|
+
return finalProfile;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1606
1916
|
// ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
|
|
1607
1917
|
|
|
1608
1918
|
async function dashboardScreen(rl, ask) {
|
|
@@ -2244,6 +2554,151 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
2244
2554
|
rl.close();
|
|
2245
2555
|
}
|
|
2246
2556
|
|
|
2557
|
+
// ─── Specialist commands ──────────────────────────────────────────────────────
|
|
2558
|
+
|
|
2559
|
+
const SPECIALIST_DEFAULTS = {
|
|
2560
|
+
python: { name: 'Python', description: 'Python stdlib, typing, async' },
|
|
2561
|
+
typescript: { name: 'TypeScript', description: 'TS type system, React, Node' },
|
|
2562
|
+
html: { name: 'HTML/CSS', description: 'Semantic HTML, CSS, accessibility' },
|
|
2563
|
+
linux: { name: 'Linux/DevOps', description: 'Sysadmin, Docker, nginx, shell' },
|
|
2564
|
+
security: { name: 'Security', description: 'Auth, crypto, OWASP, threat model' },
|
|
2565
|
+
};
|
|
2566
|
+
|
|
2567
|
+
function loadSpecialistRegistry() {
|
|
2568
|
+
const regPath = join(__dirname, '..', 'agents', 'specialists', 'registry.json');
|
|
2569
|
+
try {
|
|
2570
|
+
const raw = JSON.parse(readFileSync(regPath, 'utf8'));
|
|
2571
|
+
const out = {};
|
|
2572
|
+
for (const [key, val] of Object.entries(raw.specialists || {})) {
|
|
2573
|
+
out[key] = { name: val.name || key, description: val.description || '' };
|
|
2574
|
+
}
|
|
2575
|
+
return out;
|
|
2576
|
+
} catch {
|
|
2577
|
+
return SPECIALIST_DEFAULTS;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function cmdSpecialists() {
|
|
2582
|
+
const registry = loadSpecialistRegistry();
|
|
2583
|
+
const entries = Object.entries(registry);
|
|
2584
|
+
|
|
2585
|
+
// Build padded rows
|
|
2586
|
+
const rows = entries.map(([key, val]) => {
|
|
2587
|
+
const k = key.padEnd(12);
|
|
2588
|
+
const d = val.description;
|
|
2589
|
+
return `│ ${k}${d}`;
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
// Find longest row for width
|
|
2593
|
+
const inner = Math.max(
|
|
2594
|
+
...rows.map(r => r.length),
|
|
2595
|
+
'│ Usage: dual-brain python "task description" │'.length - 2,
|
|
2596
|
+
);
|
|
2597
|
+
const width = inner + 1; // account for trailing │
|
|
2598
|
+
|
|
2599
|
+
function pad(str) {
|
|
2600
|
+
return str + ' '.repeat(Math.max(0, width - str.length - 1)) + '│';
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
const top = '┌' + '─'.repeat(width - 1) + '┐';
|
|
2604
|
+
const title = pad('│ 🎯 Available Specialists');
|
|
2605
|
+
const divTop = '├' + '─'.repeat(width - 1) + '┤';
|
|
2606
|
+
const divBot = '├' + '─'.repeat(width - 1) + '┤';
|
|
2607
|
+
const bot = '└' + '─'.repeat(width - 1) + '┘';
|
|
2608
|
+
|
|
2609
|
+
console.log(top);
|
|
2610
|
+
console.log(title);
|
|
2611
|
+
console.log(divTop);
|
|
2612
|
+
for (const row of rows) console.log(pad(row));
|
|
2613
|
+
console.log(divBot);
|
|
2614
|
+
console.log(pad('│ Usage: dual-brain python "task description"'));
|
|
2615
|
+
console.log(pad('│ Auto-routing: off (use dual-brain go for auto)'));
|
|
2616
|
+
console.log(bot);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
async function cmdSpecialistGo(specialist, args) {
|
|
2620
|
+
const dryRun = args.includes('--dry-run');
|
|
2621
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
2622
|
+
const filesRaw = flag(args, '--files');
|
|
2623
|
+
const files = filesRaw && typeof filesRaw === 'string'
|
|
2624
|
+
? filesRaw.split(',').map(f => f.trim()).filter(Boolean)
|
|
2625
|
+
: [];
|
|
2626
|
+
|
|
2627
|
+
const prompt = args.find(a => !a.startsWith('--') && !a.startsWith('-') && a !== (filesRaw ?? ''));
|
|
2628
|
+
if (!prompt) err(`Usage: dual-brain ${specialist} "task description" [--dry-run] [--files a,b]`);
|
|
2629
|
+
|
|
2630
|
+
const cwd = process.cwd();
|
|
2631
|
+
const profile = await ensureProfile(cwd);
|
|
2632
|
+
const detection = detectTask({ prompt, files });
|
|
2633
|
+
|
|
2634
|
+
// Override specialist, preserve everything else
|
|
2635
|
+
detection.specialist = specialist;
|
|
2636
|
+
|
|
2637
|
+
console.log(`[specialist: ${specialist}] ${detection.explanation}`);
|
|
2638
|
+
|
|
2639
|
+
if (verbose) {
|
|
2640
|
+
vtrace(`Intent: ${detection.intent} | Risk: ${detection.risk} | Complexity: ${detection.complexity} | Effort: ${detection.effort ?? 'n/a'}`);
|
|
2641
|
+
vtrace(`Tier: ${detection.tier} | Specialist override: ${specialist}`);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
const decision = decideRoute({ profile, detection, cwd });
|
|
2645
|
+
|
|
2646
|
+
if (verbose) {
|
|
2647
|
+
const modelLabel = decision.effort ? `${decision.model} (${decision.effort})` : decision.model;
|
|
2648
|
+
vtrace(`Model selection: ${modelLabel}`);
|
|
2649
|
+
vtrace(`Dual-brain: ${decision.dualBrain ? 'yes' : 'no'}`);
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
console.log(` specialist : ${specialist}`);
|
|
2653
|
+
console.log(` provider : ${decision.provider}`);
|
|
2654
|
+
console.log(` model : ${decision.model}${decision.effort ? ' (' + decision.effort + ')' : ''}`);
|
|
2655
|
+
console.log(` tier : ${decision.tier}`);
|
|
2656
|
+
console.log(` dual-brain : ${decision.dualBrain ? 'yes' : 'no'}`);
|
|
2657
|
+
console.log(` reason : ${decision.explanation}`);
|
|
2658
|
+
|
|
2659
|
+
if (dryRun) {
|
|
2660
|
+
console.log('\n(dry-run — not executing)');
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
console.log('\nDispatching...');
|
|
2665
|
+
let result;
|
|
2666
|
+
if (decision.dualBrain) {
|
|
2667
|
+
result = await dispatchDualBrain({ decision, prompt, files, cwd });
|
|
2668
|
+
console.log(`\nConsensus: ${result.consensus}`);
|
|
2669
|
+
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
2670
|
+
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
2671
|
+
saveSession({
|
|
2672
|
+
objective: prompt,
|
|
2673
|
+
branch: null,
|
|
2674
|
+
filesChanged: files,
|
|
2675
|
+
commandsRun: [`dual-brain ${specialist} "${prompt}"`],
|
|
2676
|
+
lastResult: { status: 'success', summary: result.consensus || 'dual-brain complete' },
|
|
2677
|
+
provider: decision.provider,
|
|
2678
|
+
nextAction: null,
|
|
2679
|
+
}, cwd);
|
|
2680
|
+
} else {
|
|
2681
|
+
result = await dispatch({ decision, prompt, files, cwd });
|
|
2682
|
+
const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
2683
|
+
console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
2684
|
+
if (result.summary) console.log(result.summary);
|
|
2685
|
+
if (result.error) process.stderr.write(`${result.error}\n`);
|
|
2686
|
+
saveSession({
|
|
2687
|
+
objective: prompt,
|
|
2688
|
+
branch: null,
|
|
2689
|
+
filesChanged: files,
|
|
2690
|
+
commandsRun: [`dual-brain ${specialist} "${prompt}"`],
|
|
2691
|
+
lastResult: {
|
|
2692
|
+
status: result.status === 'completed' ? 'success' : 'failure',
|
|
2693
|
+
summary: result.summary || (result.status === 'completed' ? 'completed' : `exit ${result.exitCode}`),
|
|
2694
|
+
},
|
|
2695
|
+
provider: decision.provider,
|
|
2696
|
+
nextAction: null,
|
|
2697
|
+
}, cwd);
|
|
2698
|
+
if (result.status !== 'completed') process.exit(1);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2247
2702
|
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
2248
2703
|
|
|
2249
2704
|
async function main() {
|
|
@@ -2267,9 +2722,20 @@ async function main() {
|
|
|
2267
2722
|
if (profileExists(cwd)) {
|
|
2268
2723
|
await runScreens('main');
|
|
2269
2724
|
} else {
|
|
2270
|
-
// First run:
|
|
2271
|
-
|
|
2272
|
-
await
|
|
2725
|
+
// First run: run the 5-step onboarding wizard, then go to main.
|
|
2726
|
+
process.stdout.write(`\ndual-brain v${readVersion()} — Setup\n\nDetecting your setup...\n`);
|
|
2727
|
+
const auth = await detectAuth();
|
|
2728
|
+
const plans = detectPlans();
|
|
2729
|
+
const existingSessions = importReplitSessions(cwd);
|
|
2730
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2731
|
+
const wizardProfile = await runOnboardingWizard({ auth, plans, existingSessions }, cwd, rl);
|
|
2732
|
+
if (wizardProfile) {
|
|
2733
|
+
saveProfile(wizardProfile, { cwd });
|
|
2734
|
+
await cmdInstall(cwd);
|
|
2735
|
+
console.log('\n ✅ Setup complete! Starting dual-brain...\n');
|
|
2736
|
+
}
|
|
2737
|
+
rl.close();
|
|
2738
|
+
await runScreens('main');
|
|
2273
2739
|
}
|
|
2274
2740
|
} else {
|
|
2275
2741
|
// Non-TTY: print status card and exit
|
|
@@ -2285,8 +2751,21 @@ async function main() {
|
|
|
2285
2751
|
|
|
2286
2752
|
if (cmd === 'init') {
|
|
2287
2753
|
if (isInteractive) {
|
|
2288
|
-
// Run
|
|
2289
|
-
|
|
2754
|
+
// Run 5-step onboarding wizard then main screen
|
|
2755
|
+
const cwd = process.cwd();
|
|
2756
|
+
process.stdout.write(`\ndual-brain v${readVersion()} — Setup\n\nDetecting your setup...\n`);
|
|
2757
|
+
const auth = await detectAuth();
|
|
2758
|
+
const plans = detectPlans();
|
|
2759
|
+
const existingSessions = importReplitSessions(cwd);
|
|
2760
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2761
|
+
const wizardProfile = await runOnboardingWizard({ auth, plans, existingSessions }, cwd, rl);
|
|
2762
|
+
if (wizardProfile) {
|
|
2763
|
+
saveProfile(wizardProfile, { cwd });
|
|
2764
|
+
await cmdInstall(cwd);
|
|
2765
|
+
console.log('\n ✅ Setup complete! Starting dual-brain...\n');
|
|
2766
|
+
}
|
|
2767
|
+
rl.close();
|
|
2768
|
+
await runScreens('main');
|
|
2290
2769
|
} else {
|
|
2291
2770
|
await cmdInit();
|
|
2292
2771
|
}
|
|
@@ -2303,8 +2782,14 @@ async function main() {
|
|
|
2303
2782
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
2304
2783
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
2305
2784
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
|
2306
|
-
if (cmd === 'remember')
|
|
2307
|
-
if (cmd === 'forget')
|
|
2785
|
+
if (cmd === 'remember') { cmdRemember(args[1]); return; }
|
|
2786
|
+
if (cmd === 'forget') { cmdForget(args[1]); return; }
|
|
2787
|
+
if (cmd === 'break-glass') { cmdBreakGlass(args.slice(1).join(' ')); return; }
|
|
2788
|
+
|
|
2789
|
+
if (cmd === 'specialists') { cmdSpecialists(); return; }
|
|
2790
|
+
|
|
2791
|
+
const SPECIALIST_CMDS = new Set(Object.keys(loadSpecialistRegistry()));
|
|
2792
|
+
if (SPECIALIST_CMDS.has(cmd)) { await cmdSpecialistGo(cmd, args.slice(1)); return; }
|
|
2308
2793
|
|
|
2309
2794
|
if (cmd === 'search') {
|
|
2310
2795
|
const query = args.slice(1).filter(a => !a.startsWith('--')).join(' ');
|
package/install.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { createInterface } from 'readline';
|
|
|
14
14
|
import { dirname, join, resolve } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { spawnSync } from 'child_process';
|
|
17
|
+
import { createHash } from 'crypto';
|
|
17
18
|
|
|
18
19
|
// Skip hook installation during global npm install — hooks are installed
|
|
19
20
|
// when the user runs 'dual-brain install' in their project directory.
|
|
@@ -863,6 +864,39 @@ function generateGitignoreEntries(workspace) {
|
|
|
863
864
|
return { existing, needed };
|
|
864
865
|
}
|
|
865
866
|
|
|
867
|
+
// ─── Hook Manifest ──────────────────────────────────────────────────────────
|
|
868
|
+
|
|
869
|
+
function hashString(s) {
|
|
870
|
+
return createHash('sha256').update(s).digest('hex');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function generateHookManifest(settings) {
|
|
874
|
+
const hooks = settings.hooks || {};
|
|
875
|
+
const preHooks = (hooks.PreToolUse || []).flatMap(entry =>
|
|
876
|
+
(entry.hooks || []).map(h => hashString(h.command || ''))
|
|
877
|
+
);
|
|
878
|
+
const postHooks = (hooks.PostToolUse || []).flatMap(entry =>
|
|
879
|
+
(entry.hooks || []).map(h => hashString(h.command || ''))
|
|
880
|
+
);
|
|
881
|
+
const settingsHash = hashString(JSON.stringify(hooks));
|
|
882
|
+
return {
|
|
883
|
+
generatedAt: new Date().toISOString(),
|
|
884
|
+
expectedHooks: {
|
|
885
|
+
PreToolUse: preHooks,
|
|
886
|
+
PostToolUse: postHooks,
|
|
887
|
+
},
|
|
888
|
+
settingsHash,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function writeHookManifest(workspace, settings) {
|
|
893
|
+
const dualbrain = join(workspace, '.dualbrain');
|
|
894
|
+
mkdirSync(dualbrain, { recursive: true });
|
|
895
|
+
const manifest = generateHookManifest(settings);
|
|
896
|
+
writeFileSync(join(dualbrain, 'hook-manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
897
|
+
return manifest;
|
|
898
|
+
}
|
|
899
|
+
|
|
866
900
|
// ─── Installation ───────────────────────────────────────────────────────────
|
|
867
901
|
|
|
868
902
|
function install(workspace, env, mode) {
|
|
@@ -915,6 +949,9 @@ function install(workspace, env, mode) {
|
|
|
915
949
|
writeFileSync(join(target, 'settings.json'), JSON.stringify(settings, null, 2) + '\n');
|
|
916
950
|
actions.push('✓ settings.json (hooks registered)');
|
|
917
951
|
|
|
952
|
+
writeHookManifest(workspace, settings);
|
|
953
|
+
actions.push('✓ .dualbrain/hook-manifest.json (integrity manifest)');
|
|
954
|
+
|
|
918
955
|
const claudeMd = generateClaudeMd(mode);
|
|
919
956
|
writeClaudeMd(join(target, 'CLAUDE.md'), claudeMd);
|
|
920
957
|
actions.push('✓ CLAUDE.md (session instructions)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.17",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"url": "https://github.com/1xmint/dual-brain.git"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
-
"test": "node hooks/test-orchestrator.mjs",
|
|
41
|
+
"test": "node .claude/hooks/test-orchestrator.mjs",
|
|
42
42
|
"test:core": "node --test src/test.mjs",
|
|
43
43
|
"postinstall": "echo 'dual-brain installed. Run: dual-brain install (in your project) to set up hooks.'"
|
|
44
44
|
},
|