dual-brain 7.1.2 → 7.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dual-brain.mjs +38 -28
- package/mcp-server/index.mjs +1 -1
- package/package.json +44 -4
- package/src/decide.mjs +32 -0
- package/src/index.mjs +1 -1
- package/src/profile.mjs +7 -4
- package/src/session.mjs +50 -10
- package/src/tui.mjs +10 -1
- package/hooks/agent-fleet.mjs +0 -659
- package/hooks/context-guard.mjs +0 -468
- package/hooks/dag-scheduler.mjs +0 -1249
- package/hooks/head-guard.sh +0 -41
- package/hooks/hook-dispatch.mjs +0 -254
- package/hooks/ledger-analysis.mjs +0 -337
- package/hooks/parallelism-scaler.mjs +0 -572
- package/hooks/quality-tiers.mjs +0 -642
- package/src/test.mjs +0 -1374
package/bin/dual-brain.mjs
CHANGED
|
@@ -82,8 +82,9 @@ Options:
|
|
|
82
82
|
/**
|
|
83
83
|
* Print a compact auth status table to stdout.
|
|
84
84
|
* @param {{ claude: object, openai: object }} auth Result from detectAuth()
|
|
85
|
+
* @param {object} [profile] Optional loaded profile to cross-check enabled state
|
|
85
86
|
*/
|
|
86
|
-
function printAuthTable(auth) {
|
|
87
|
+
function printAuthTable(auth, profile) {
|
|
87
88
|
const W = 55; // inner width (wide enough for source labels)
|
|
88
89
|
const hbar = '═'.repeat(W);
|
|
89
90
|
const pad = (s) => {
|
|
@@ -91,15 +92,21 @@ function printAuthTable(auth) {
|
|
|
91
92
|
return s + ' '.repeat(Math.max(0, W - visible.length));
|
|
92
93
|
};
|
|
93
94
|
|
|
95
|
+
const claudeDisabled = profile?.providers?.claude?.enabled === false;
|
|
96
|
+
const openaiDisabled = profile?.providers?.openai?.enabled === false;
|
|
97
|
+
|
|
98
|
+
const claudeDisabledNote = claudeDisabled ? ' (auth ok, but disabled in profile)' : '';
|
|
99
|
+
const openaiDisabledNote = openaiDisabled ? ' (auth ok, but disabled in profile)' : '';
|
|
100
|
+
|
|
94
101
|
const claudeLine1 = auth.claude.found
|
|
95
|
-
? ` Claude: ✓ found via ${auth.claude.source}`
|
|
102
|
+
? ` Claude: ✓ found via ${auth.claude.source}${claudeDisabledNote}`
|
|
96
103
|
: ` Claude: ✗ not found`;
|
|
97
104
|
const claudeLine2 = auth.claude.found
|
|
98
105
|
? ` ${auth.claude.masked}`
|
|
99
106
|
: ` run: dual-brain auth setup`;
|
|
100
107
|
|
|
101
108
|
const openaiLine1 = auth.openai.found
|
|
102
|
-
? ` OpenAI: ✓ found via ${auth.openai.source}`
|
|
109
|
+
? ` OpenAI: ✓ found via ${auth.openai.source}${openaiDisabledNote}`
|
|
103
110
|
: ` OpenAI: ✗ not found`;
|
|
104
111
|
const openaiLine2 = auth.openai.found
|
|
105
112
|
? ` ${auth.openai.masked}`
|
|
@@ -122,7 +129,7 @@ async function cmdInit(rl) {
|
|
|
122
129
|
|
|
123
130
|
// --- Step 1: Auth preflight ---
|
|
124
131
|
const auth = await detectAuth();
|
|
125
|
-
printAuthTable(auth);
|
|
132
|
+
printAuthTable(auth, loadProfile(cwd));
|
|
126
133
|
|
|
127
134
|
const noneFound = !auth.claude.found && !auth.openai.found;
|
|
128
135
|
if (noneFound) {
|
|
@@ -166,7 +173,8 @@ async function cmdAuth(subArgs = [], rl) {
|
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
const auth = await detectAuth();
|
|
169
|
-
|
|
176
|
+
const profile = loadProfile(process.cwd());
|
|
177
|
+
printAuthTable(auth, profile);
|
|
170
178
|
|
|
171
179
|
// If anything is missing, point to setup command
|
|
172
180
|
if (!auth.claude.found || !auth.openai.found) {
|
|
@@ -339,10 +347,20 @@ async function cmdStatus(args = []) {
|
|
|
339
347
|
const totalTokens = Object.values(sessionStats).reduce((s, v) => s + v.tokens, 0);
|
|
340
348
|
console.log(`\nSession: ${totalCalls} dispatch${totalCalls !== 1 ? 'es' : ''}, ${totalTokens} tokens observed`);
|
|
341
349
|
|
|
342
|
-
// Models
|
|
350
|
+
// Models — only list enabled providers
|
|
343
351
|
console.log('\nAvailable models:');
|
|
344
|
-
|
|
345
|
-
|
|
352
|
+
const claudeEnabled = profile?.providers?.claude?.enabled !== false;
|
|
353
|
+
const openaiEnabled = profile?.providers?.openai?.enabled !== false;
|
|
354
|
+
if (claudeEnabled && available.claude.length) {
|
|
355
|
+
console.log(` Claude : ${available.claude.join(', ')}`);
|
|
356
|
+
} else if (!claudeEnabled) {
|
|
357
|
+
console.log(` Claude : (disabled — run "dual-brain init" to enable)`);
|
|
358
|
+
}
|
|
359
|
+
if (openaiEnabled && available.openai.length) {
|
|
360
|
+
console.log(` OpenAI : ${available.openai.join(', ')}`);
|
|
361
|
+
} else if (!openaiEnabled) {
|
|
362
|
+
console.log(` OpenAI : (disabled — run "dual-brain init" to enable)`);
|
|
363
|
+
}
|
|
346
364
|
|
|
347
365
|
// Head model
|
|
348
366
|
console.log(`\nHead model : ${getHeadModel(profile)}`);
|
|
@@ -680,8 +698,15 @@ async function dashboardScreen(rl, ask) {
|
|
|
680
698
|
const env = detectEnvironment();
|
|
681
699
|
|
|
682
700
|
// Build status lines for box
|
|
683
|
-
|
|
684
|
-
const
|
|
701
|
+
// If auth is found but provider is disabled in profile, show warning instead of green
|
|
702
|
+
const claudeProviderEnabled = profile?.providers?.claude?.enabled !== false;
|
|
703
|
+
const openaiProviderEnabled = profile?.providers?.openai?.enabled !== false;
|
|
704
|
+
const claudeStatus = auth.claude.found
|
|
705
|
+
? (claudeProviderEnabled ? `🟢 Claude ${badge('connected')}` : `⚠️ Claude ${badge('warning')} disabled`)
|
|
706
|
+
: `🔴 Claude ${badge('missing')}`;
|
|
707
|
+
const openaiStatus = auth.openai.found
|
|
708
|
+
? (openaiProviderEnabled ? `🟢 OpenAI ${badge('connected')}` : `⚠️ OpenAI ${badge('warning')} disabled`)
|
|
709
|
+
: `🔴 OpenAI ${badge('missing')}`;
|
|
685
710
|
const envLabel = env.hasReplitTools ? 'Replit + replit-tools' : env.isReplit ? 'Replit' : 'local';
|
|
686
711
|
|
|
687
712
|
// Enforcement check
|
|
@@ -734,13 +759,11 @@ async function dashboardScreen(rl, ask) {
|
|
|
734
759
|
}
|
|
735
760
|
|
|
736
761
|
console.log(menu([
|
|
737
|
-
{ key: '
|
|
738
|
-
{ key: 's', label: 'Status — detailed provider info', section: 'Actions' },
|
|
762
|
+
{ key: 's', label: 'Status — detailed provider info', section: 'Info' },
|
|
739
763
|
{ key: 'p', label: 'Profile & preferences', section: 'Settings' },
|
|
740
764
|
{ key: 'a', label: 'Auth management', section: 'Settings' },
|
|
741
|
-
{ key: 'd', label: 'Diagnostics',
|
|
742
|
-
{ key: '
|
|
743
|
-
{ key: 'q', label: 'Exit', section: 'Session' },
|
|
765
|
+
{ key: 'd', label: 'Diagnostics & repair', section: 'Settings' },
|
|
766
|
+
{ key: 'q', label: 'Exit to shell', section: '' },
|
|
744
767
|
]));
|
|
745
768
|
console.log('');
|
|
746
769
|
|
|
@@ -752,18 +775,6 @@ async function dashboardScreen(rl, ask) {
|
|
|
752
775
|
return { next: 'session-detail', session: recentSessions[numChoice - 1] };
|
|
753
776
|
}
|
|
754
777
|
|
|
755
|
-
if (choice === 'g') {
|
|
756
|
-
const taskDesc = (await ask(' Task description: ')).trim();
|
|
757
|
-
if (taskDesc) {
|
|
758
|
-
try {
|
|
759
|
-
await cmdGo([taskDesc]);
|
|
760
|
-
} catch (e) {
|
|
761
|
-
console.error(`Error: ${e.message}`);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return { next: 'dashboard' };
|
|
765
|
-
}
|
|
766
|
-
|
|
767
778
|
if (choice === 's') {
|
|
768
779
|
await cmdStatus([]);
|
|
769
780
|
await ask('\n Press Enter to return to dashboard...');
|
|
@@ -773,7 +784,6 @@ async function dashboardScreen(rl, ask) {
|
|
|
773
784
|
if (choice === 'p') { return { next: 'profile' }; }
|
|
774
785
|
if (choice === 'a') { return { next: 'auth' }; }
|
|
775
786
|
if (choice === 'd') { return { next: 'diagnostics' }; }
|
|
776
|
-
if (choice === 'c') { return { next: 'repl' }; }
|
|
777
787
|
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
778
788
|
|
|
779
789
|
// Unknown choice — stay on dashboard
|
package/mcp-server/index.mjs
CHANGED
|
@@ -283,7 +283,7 @@ async function handleRequest(msg) {
|
|
|
283
283
|
} catch (err) {
|
|
284
284
|
const code = err.code ?? -32000;
|
|
285
285
|
const message = err.message ?? 'Internal error';
|
|
286
|
-
return errorResponse(id, code, message
|
|
286
|
+
return errorResponse(id, code, message);
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.4",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,10 +46,50 @@
|
|
|
46
46
|
"node": ">=20.0.0"
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
|
-
"src
|
|
49
|
+
"src/profile.mjs",
|
|
50
|
+
"src/detect.mjs",
|
|
51
|
+
"src/decide.mjs",
|
|
52
|
+
"src/dispatch.mjs",
|
|
53
|
+
"src/playbook.mjs",
|
|
54
|
+
"src/health.mjs",
|
|
55
|
+
"src/repo.mjs",
|
|
56
|
+
"src/session.mjs",
|
|
57
|
+
"src/decompose.mjs",
|
|
58
|
+
"src/brief.mjs",
|
|
59
|
+
"src/redact.mjs",
|
|
60
|
+
"src/index.mjs",
|
|
61
|
+
"src/tui.mjs",
|
|
62
|
+
"src/install-hooks.mjs",
|
|
63
|
+
"src/update-check.mjs",
|
|
50
64
|
"bin/*.mjs",
|
|
51
|
-
"hooks
|
|
52
|
-
"hooks
|
|
65
|
+
"hooks/enforce-tier.mjs",
|
|
66
|
+
"hooks/cost-logger.mjs",
|
|
67
|
+
"hooks/cost-report.mjs",
|
|
68
|
+
"hooks/dual-brain-review.mjs",
|
|
69
|
+
"hooks/dual-brain-think.mjs",
|
|
70
|
+
"hooks/quality-gate.mjs",
|
|
71
|
+
"hooks/test-orchestrator.mjs",
|
|
72
|
+
"hooks/setup-wizard.mjs",
|
|
73
|
+
"hooks/health-check.mjs",
|
|
74
|
+
"hooks/install-git-hooks.mjs",
|
|
75
|
+
"hooks/session-report.mjs",
|
|
76
|
+
"hooks/budget-balancer.mjs",
|
|
77
|
+
"hooks/gpt-work-dispatcher.mjs",
|
|
78
|
+
"hooks/profiles.mjs",
|
|
79
|
+
"hooks/summary-checkpoint.mjs",
|
|
80
|
+
"hooks/decision-ledger.mjs",
|
|
81
|
+
"hooks/control-panel.mjs",
|
|
82
|
+
"hooks/risk-classifier.mjs",
|
|
83
|
+
"hooks/failure-detector.mjs",
|
|
84
|
+
"hooks/vibe-router.mjs",
|
|
85
|
+
"hooks/plan-generator.mjs",
|
|
86
|
+
"hooks/vibe-memory.mjs",
|
|
87
|
+
"hooks/wave-orchestrator.mjs",
|
|
88
|
+
"hooks/task-classifier.mjs",
|
|
89
|
+
"hooks/model-registry.mjs",
|
|
90
|
+
"hooks/auto-update-wrapper.mjs",
|
|
91
|
+
"hooks/head-guard.mjs",
|
|
92
|
+
"hooks/auto-update.sh",
|
|
53
93
|
"mcp-server/*.mjs",
|
|
54
94
|
"mcp-server/README.md",
|
|
55
95
|
"install.mjs",
|
package/src/decide.mjs
CHANGED
|
@@ -449,6 +449,35 @@ export function parsePreferences(preferences) {
|
|
|
449
449
|
return signals;
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
// ─── Internal: safety floor for critical-risk tasks ───────────────────────────
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Ensure critical-risk tasks are never handled by the cheapest (haiku/gpt-4.1-mini) model.
|
|
456
|
+
* Cost-saver mode is the main culprit; escalate silently but emit a stderr warning.
|
|
457
|
+
* @param {string} model
|
|
458
|
+
* @param {string} provider
|
|
459
|
+
* @param {string[]} available
|
|
460
|
+
* @param {'low'|'medium'|'high'|'critical'} risk
|
|
461
|
+
* @returns {string}
|
|
462
|
+
*/
|
|
463
|
+
function applyCriticalRiskFloor(model, provider, available, risk) {
|
|
464
|
+
if (risk !== 'critical') return model;
|
|
465
|
+
|
|
466
|
+
const cheapModels = { claude: 'haiku', openai: 'gpt-4.1-mini' };
|
|
467
|
+
const floorModels = { claude: 'sonnet', openai: 'gpt-4.1' };
|
|
468
|
+
|
|
469
|
+
if (model === cheapModels[provider]) {
|
|
470
|
+
const floor = floorModels[provider];
|
|
471
|
+
const escalated = available.includes(floor) ? floor : available[available.length - 1] ?? model;
|
|
472
|
+
process.stderr.write(
|
|
473
|
+
`[dual-brain] Warning: cost-saver selected ${model} for a critical-risk task. ` +
|
|
474
|
+
`Escalating to ${escalated} (safety floor).\n`
|
|
475
|
+
);
|
|
476
|
+
return escalated;
|
|
477
|
+
}
|
|
478
|
+
return model;
|
|
479
|
+
}
|
|
480
|
+
|
|
452
481
|
// ─── Exported: decideRoute ────────────────────────────────────────────────────
|
|
453
482
|
|
|
454
483
|
/**
|
|
@@ -508,6 +537,9 @@ export function decideRoute({ profile = {}, detection = {}, cwd } = {}) {
|
|
|
508
537
|
// Apply profile mode bias (cost-saver / quality-first / preferences) using patched profile
|
|
509
538
|
model = applyProfileBias(model, profileWithEffectiveBias, provider, available[provider]);
|
|
510
539
|
|
|
540
|
+
// Safety floor: critical-risk tasks must never use haiku/gpt-4.1-mini even in cost-saver mode
|
|
541
|
+
model = applyCriticalRiskFloor(model, provider, available[provider], detection.risk);
|
|
542
|
+
|
|
511
543
|
// Apply preferModel signal from preferences (override after all other picks)
|
|
512
544
|
if (prefSignals.preferModel) {
|
|
513
545
|
const wantedModel = prefSignals.preferModel;
|
package/src/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export { loadProfile, saveProfile, ensureProfile, runOnboarding, rememberPreference, forgetPreference, getActivePreferences, getAvailableProviders, isSoloBrain, getHeadModel, detectAuth, detectEnvironment, setupAuth, getActiveKey, removeAuthKey, disableKey, rotateToNextKey } from './profile.mjs';
|
|
10
10
|
export { detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths } from './detect.mjs';
|
|
11
|
-
export { decideRoute, getModelCapabilities, getAvailableModels,
|
|
11
|
+
export { decideRoute, getModelCapabilities, getAvailableModels, shouldDualBrain, explainDecision } from './decide.mjs';
|
|
12
12
|
export { dispatch, buildCommand, detectRuntime, compressResult, dispatchDualBrain } from './dispatch.mjs';
|
|
13
13
|
export { loadPlaybook, listPlaybooks, executePlaybook, createRunArtifact } from './playbook.mjs';
|
|
14
14
|
export { getHealth, markHot, markDegraded, markHealthy, checkCooldown, getProviderScore, recordDispatch, getSessionStats, resetHealth, remainingCooldownMinutes } from './health.mjs';
|
package/src/profile.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { createInterface } from 'readline';
|
|
26
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
26
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
27
27
|
import { homedir } from 'os';
|
|
28
28
|
import { dirname, join } from 'path';
|
|
29
29
|
|
|
@@ -348,7 +348,7 @@ function saveAuthKey(provider, key, opts = {}) {
|
|
|
348
348
|
const cwd = opts.cwd || process.cwd();
|
|
349
349
|
const authFile = AUTH_FILE(cwd);
|
|
350
350
|
const dir = dirname(authFile);
|
|
351
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
351
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
352
352
|
|
|
353
353
|
const auth = loadAuthKeys(cwd);
|
|
354
354
|
if (!Array.isArray(auth[provider])) auth[provider] = [];
|
|
@@ -370,6 +370,7 @@ function saveAuthKey(provider, key, opts = {}) {
|
|
|
370
370
|
});
|
|
371
371
|
|
|
372
372
|
writeFileSync(authFile, JSON.stringify(auth, null, 2));
|
|
373
|
+
chmodSync(authFile, 0o600);
|
|
373
374
|
|
|
374
375
|
// Inject highest-priority valid key into process.env for this session
|
|
375
376
|
const active = getActiveKey(provider, cwd);
|
|
@@ -388,13 +389,14 @@ function saveAuthKey(provider, key, opts = {}) {
|
|
|
388
389
|
function removeAuthKey(provider, index, cwd) {
|
|
389
390
|
const authFile = AUTH_FILE(cwd);
|
|
390
391
|
const dir = dirname(authFile);
|
|
391
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
392
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
392
393
|
|
|
393
394
|
const auth = loadAuthKeys(cwd);
|
|
394
395
|
if (!Array.isArray(auth[provider])) return;
|
|
395
396
|
|
|
396
397
|
auth[provider].splice(index, 1);
|
|
397
398
|
writeFileSync(authFile, JSON.stringify(auth, null, 2));
|
|
399
|
+
chmodSync(authFile, 0o600);
|
|
398
400
|
}
|
|
399
401
|
|
|
400
402
|
/**
|
|
@@ -406,13 +408,14 @@ function removeAuthKey(provider, index, cwd) {
|
|
|
406
408
|
function disableKey(provider, index, cwd) {
|
|
407
409
|
const authFile = AUTH_FILE(cwd);
|
|
408
410
|
const dir = dirname(authFile);
|
|
409
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
411
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
410
412
|
|
|
411
413
|
const auth = loadAuthKeys(cwd);
|
|
412
414
|
if (!Array.isArray(auth[provider]) || !auth[provider][index]) return;
|
|
413
415
|
|
|
414
416
|
auth[provider][index].enabled = false;
|
|
415
417
|
writeFileSync(authFile, JSON.stringify(auth, null, 2));
|
|
418
|
+
chmodSync(authFile, 0o600);
|
|
416
419
|
}
|
|
417
420
|
|
|
418
421
|
/**
|
package/src/session.mjs
CHANGED
|
@@ -122,9 +122,10 @@ export function clearSession(cwd = process.cwd()) {
|
|
|
122
122
|
* @param {object|null} session — from loadSession()
|
|
123
123
|
* @param {object} repo — from detectRepo() / loadRepoCache()
|
|
124
124
|
* @param {object} health — from getHealth() (shape: { states: {}, session: {} })
|
|
125
|
+
* @param {object} [profile] — optional profile for enabled-state checks
|
|
125
126
|
* @returns {string}
|
|
126
127
|
*/
|
|
127
|
-
export function formatSessionCard(session, repo, health) {
|
|
128
|
+
export function formatSessionCard(session, repo, health, profile) {
|
|
128
129
|
const lines = [];
|
|
129
130
|
|
|
130
131
|
// Line 1: Repo identity
|
|
@@ -157,8 +158,11 @@ export function formatSessionCard(session, repo, health) {
|
|
|
157
158
|
lines.push(`Branch: ${repo.branch}${dirtyNote}`);
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
// Line 4: Health summary
|
|
161
|
+
// Line 4: Health summary — only show enabled providers
|
|
161
162
|
const { states = {} } = health || {};
|
|
163
|
+
const claudeProviderEnabled = profile?.providers?.claude?.enabled !== false;
|
|
164
|
+
const openaiProviderEnabled = profile?.providers?.openai?.enabled !== false;
|
|
165
|
+
|
|
162
166
|
function providerStatus(name) {
|
|
163
167
|
const entries = Object.entries(states).filter(([k]) => k.startsWith(`${name}:`));
|
|
164
168
|
if (entries.length === 0) return 'healthy';
|
|
@@ -168,11 +172,21 @@ export function formatSessionCard(session, repo, health) {
|
|
|
168
172
|
if (statuses.includes('probing')) return 'probing';
|
|
169
173
|
return 'healthy';
|
|
170
174
|
}
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
|
|
176
|
+
const healthParts = [];
|
|
177
|
+
if (claudeProviderEnabled) {
|
|
178
|
+
const claudeStatus = providerStatus('claude');
|
|
179
|
+
healthParts.push(claudeStatus === 'healthy' ? 'Claude healthy' : `Claude ${claudeStatus}`);
|
|
180
|
+
} else {
|
|
181
|
+
healthParts.push('Claude disabled');
|
|
182
|
+
}
|
|
183
|
+
if (openaiProviderEnabled) {
|
|
184
|
+
const openaiStatus = providerStatus('openai');
|
|
185
|
+
healthParts.push(openaiStatus === 'healthy' ? 'OpenAI healthy' : `OpenAI ${openaiStatus}`);
|
|
186
|
+
} else {
|
|
187
|
+
healthParts.push('OpenAI disabled');
|
|
188
|
+
}
|
|
189
|
+
lines.push(`Health: ${healthParts.join(', ')}`);
|
|
176
190
|
|
|
177
191
|
// Line 5: Last task summary (only if session exists)
|
|
178
192
|
if (session) {
|
|
@@ -194,6 +208,9 @@ export function formatSessionCard(session, repo, health) {
|
|
|
194
208
|
}
|
|
195
209
|
}
|
|
196
210
|
|
|
211
|
+
// Tip line: always show a call-to-action so non-TTY output is actionable
|
|
212
|
+
lines.push(`Tip: run "dual-brain --help" or "dual-brain go \\"task\\""`);
|
|
213
|
+
|
|
197
214
|
return lines.join('\n');
|
|
198
215
|
}
|
|
199
216
|
|
|
@@ -229,11 +246,32 @@ function timeAgo(timestamp) {
|
|
|
229
246
|
*/
|
|
230
247
|
export function importReplitSessions(cwd = process.cwd()) {
|
|
231
248
|
const sessions = [];
|
|
232
|
-
|
|
249
|
+
|
|
250
|
+
// Check multiple possible locations for replit-tools
|
|
251
|
+
const candidates = [
|
|
252
|
+
join(cwd, '.replit-tools', '.claude-persistent'),
|
|
253
|
+
join('/home/runner/workspace', '.replit-tools', '.claude-persistent'),
|
|
254
|
+
];
|
|
255
|
+
// Deduplicate
|
|
256
|
+
const seen = new Set();
|
|
257
|
+
const replitBases = candidates.filter(p => {
|
|
258
|
+
const norm = p.replace(/\/+$/, '');
|
|
259
|
+
if (seen.has(norm)) return false;
|
|
260
|
+
seen.add(norm);
|
|
261
|
+
return true;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
let replitBase = null;
|
|
265
|
+
for (const candidate of replitBases) {
|
|
266
|
+
if (existsSync(join(candidate, 'history.jsonl'))) {
|
|
267
|
+
replitBase = candidate;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!replitBase) return sessions;
|
|
233
272
|
|
|
234
273
|
// Read history.jsonl
|
|
235
274
|
const historyPath = join(replitBase, 'history.jsonl');
|
|
236
|
-
if (!existsSync(historyPath)) return sessions;
|
|
237
275
|
|
|
238
276
|
let lines;
|
|
239
277
|
try {
|
|
@@ -272,7 +310,9 @@ export function importReplitSessions(cwd = process.cwd()) {
|
|
|
272
310
|
}
|
|
273
311
|
|
|
274
312
|
// Read active terminal sessions
|
|
275
|
-
|
|
313
|
+
// Use the same root as replitBase (go up one level from .claude-persistent)
|
|
314
|
+
const replitRoot = join(replitBase, '..');
|
|
315
|
+
const sessionsDir = join(replitRoot, '..', '.claude-sessions');
|
|
276
316
|
const activeSessionIds = new Set();
|
|
277
317
|
if (existsSync(sessionsDir)) {
|
|
278
318
|
try {
|
package/src/tui.mjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
7
9
|
|
|
8
10
|
// ─── Unicode / ASCII mode ─────────────────────────────────────────────────────
|
|
9
11
|
|
|
@@ -172,7 +174,14 @@ export function menu(options, opts = {}) {
|
|
|
172
174
|
// ─── Self-test ────────────────────────────────────────────────────────────────
|
|
173
175
|
|
|
174
176
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
175
|
-
|
|
177
|
+
// Read version dynamically from package.json
|
|
178
|
+
let selfTestVersion = '0.0.0';
|
|
179
|
+
try {
|
|
180
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
181
|
+
selfTestVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
182
|
+
} catch { /* fallback to 0.0.0 */ }
|
|
183
|
+
|
|
184
|
+
console.log(box(`🧠 Dual-Brain v${selfTestVersion}`, [
|
|
176
185
|
'🟢 Claude ✅ 🟢 OpenAI ✅',
|
|
177
186
|
'🌀 Replit + replit-tools',
|
|
178
187
|
]));
|