dual-brain 0.2.6 → 0.2.7
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 +142 -41
- package/package.json +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -1040,43 +1040,58 @@ async function installGlobal() {
|
|
|
1040
1040
|
return;
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
|
-
//
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
];
|
|
1063
|
-
const postToolHooks = [
|
|
1064
|
-
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'cost-logger.mjs')} ${DB_MARKER}` }] },
|
|
1065
|
-
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'auto-update-wrapper.mjs')} ${DB_MARKER}` }] },
|
|
1066
|
-
];
|
|
1067
|
-
|
|
1068
|
-
// Remove any existing dual-brain hooks (idempotent)
|
|
1069
|
-
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1070
|
-
existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1071
|
-
existing.hooks.PostToolUse = existing.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1043
|
+
// Check if project-local hooks already exist (avoids double-firing)
|
|
1044
|
+
const projectLocalSettings = join(pkgRoot, '.claude', 'settings.local.json');
|
|
1045
|
+
const hasProjectLocalHooks = (() => {
|
|
1046
|
+
if (!existsSync(projectLocalSettings)) return false;
|
|
1047
|
+
try {
|
|
1048
|
+
const content = readFileSync(projectLocalSettings, 'utf8');
|
|
1049
|
+
return content.includes('dual-brain') || content.includes('head-guard');
|
|
1050
|
+
} catch { return false; }
|
|
1051
|
+
})();
|
|
1052
|
+
|
|
1053
|
+
if (hasProjectLocalHooks) {
|
|
1054
|
+
console.log(' hooks already configured project-locally, skipping global hooks');
|
|
1055
|
+
console.log(' (project .claude/settings.local.json already contains dual-brain hooks)');
|
|
1056
|
+
} else {
|
|
1057
|
+
// Load existing settings (merge, never clobber)
|
|
1058
|
+
let existing = {};
|
|
1059
|
+
if (existsSync(globalSettingsPath)) {
|
|
1060
|
+
try { existing = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch {}
|
|
1061
|
+
}
|
|
1072
1062
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1063
|
+
// Ensure hooks structure exists
|
|
1064
|
+
if (!existing.hooks) existing.hooks = {};
|
|
1065
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
|
|
1066
|
+
if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
|
|
1076
1067
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1068
|
+
// Define dual-brain hooks with ownership marker
|
|
1069
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
1070
|
+
const preToolHooks = [
|
|
1071
|
+
{ matcher: 'Edit', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1072
|
+
{ matcher: 'Write', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1073
|
+
{ matcher: 'NotebookEdit',hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1074
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1075
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'enforce-tier.mjs')} ${DB_MARKER}` }] },
|
|
1076
|
+
];
|
|
1077
|
+
const postToolHooks = [
|
|
1078
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'cost-logger.mjs')} ${DB_MARKER}` }] },
|
|
1079
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'auto-update-wrapper.mjs')} ${DB_MARKER}` }] },
|
|
1080
|
+
];
|
|
1081
|
+
|
|
1082
|
+
// Remove any existing dual-brain hooks (idempotent)
|
|
1083
|
+
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1084
|
+
existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1085
|
+
existing.hooks.PostToolUse = existing.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1086
|
+
|
|
1087
|
+
// Add dual-brain hooks
|
|
1088
|
+
existing.hooks.PreToolUse.push(...preToolHooks);
|
|
1089
|
+
existing.hooks.PostToolUse.push(...postToolHooks);
|
|
1090
|
+
|
|
1091
|
+
// Write merged settings
|
|
1092
|
+
mkdirSync(globalClaudeDir, { recursive: true });
|
|
1093
|
+
writeFileSync(globalSettingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
1094
|
+
}
|
|
1080
1095
|
|
|
1081
1096
|
// Write minimal global CLAUDE.md (only if none exists, or append section)
|
|
1082
1097
|
const globalClaudeMd = join(globalClaudeDir, 'CLAUDE.md');
|
|
@@ -1091,12 +1106,16 @@ async function installGlobal() {
|
|
|
1091
1106
|
}
|
|
1092
1107
|
}
|
|
1093
1108
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1109
|
+
if (!hasProjectLocalHooks) {
|
|
1110
|
+
console.log(' + dual-brain hooks installed globally');
|
|
1111
|
+
console.log(' hooks dir: ' + hooksDir);
|
|
1112
|
+
console.log(' settings: ' + globalSettingsPath);
|
|
1113
|
+
console.log('');
|
|
1114
|
+
console.log(' All new Claude sessions will load dual-brain hooks.');
|
|
1115
|
+
console.log(' Run "dual-brain uninstall --global" to remove.');
|
|
1116
|
+
}
|
|
1117
|
+
console.log(' + global CLAUDE.md updated');
|
|
1118
|
+
console.log(' path: ' + globalClaudeDir);
|
|
1100
1119
|
}
|
|
1101
1120
|
|
|
1102
1121
|
async function uninstallGlobal() {
|
|
@@ -2163,6 +2182,15 @@ async function mainScreen(rl, ask) {
|
|
|
2163
2182
|
dashSpinner = fx.spinner('Loading dashboard...').start();
|
|
2164
2183
|
}
|
|
2165
2184
|
|
|
2185
|
+
// ── One-time default shell prompt for returning users (never asked before) ─
|
|
2186
|
+
if (profile.setupComplete && !profile.defaultShellAsked) {
|
|
2187
|
+
if (dashSpinner) { dashSpinner.stop(); dashSpinner = null; }
|
|
2188
|
+
const wantsDefault = await askDefaultShell(cwd, rl, fx);
|
|
2189
|
+
profile.defaultShellAsked = true;
|
|
2190
|
+
profile.isDefaultShell = wantsDefault;
|
|
2191
|
+
saveProfile(profile, { cwd });
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2166
2194
|
const claudeSub = profile?.providers?.claude;
|
|
2167
2195
|
const openaiSub = profile?.providers?.openai;
|
|
2168
2196
|
|
|
@@ -3984,6 +4012,62 @@ function saveWizardCredentials(cwd, detectedProviders) {
|
|
|
3984
4012
|
* @param {object} rl readline interface
|
|
3985
4013
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
3986
4014
|
*/
|
|
4015
|
+
function setAsDefaultShell(cwd) {
|
|
4016
|
+
const root = cwd || process.cwd();
|
|
4017
|
+
const replitPath = join(root, '.replit');
|
|
4018
|
+
if (!existsSync(replitPath)) return;
|
|
4019
|
+
|
|
4020
|
+
let content = readFileSync(replitPath, 'utf8');
|
|
4021
|
+
const newOnBoot = 'onBoot = "source /home/runner/workspace/.replit-tools/scripts/setup-claude-code.sh 2>/dev/null || true; ln -sf /home/runner/workspace/.replit-tools/.npm-persistent/.npmrc ~/.npmrc 2>/dev/null || true; dual-brain install --global 2>/dev/null || true"';
|
|
4022
|
+
|
|
4023
|
+
if (content.match(/^onBoot\s*=/m)) {
|
|
4024
|
+
content = content.replace(/^onBoot\s*=.*$/m, newOnBoot);
|
|
4025
|
+
} else {
|
|
4026
|
+
content += '\n' + newOnBoot + '\n';
|
|
4027
|
+
}
|
|
4028
|
+
writeFileSync(replitPath, content);
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
function removeAsDefaultShell(cwd) {
|
|
4032
|
+
const root = cwd || process.cwd();
|
|
4033
|
+
const replitPath = join(root, '.replit');
|
|
4034
|
+
if (!existsSync(replitPath)) return;
|
|
4035
|
+
|
|
4036
|
+
let content = readFileSync(replitPath, 'utf8');
|
|
4037
|
+
const origOnBoot = 'onBoot = "source /home/runner/workspace/.replit-tools/scripts/setup-claude-code.sh 2>/dev/null || true"';
|
|
4038
|
+
if (content.match(/^onBoot\s*=/m)) {
|
|
4039
|
+
content = content.replace(/^onBoot\s*=.*$/m, origOnBoot);
|
|
4040
|
+
writeFileSync(replitPath, content);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
async function askDefaultShell(cwd, rl, fx) {
|
|
4045
|
+
const cl = fx.colors || {};
|
|
4046
|
+
const DIM = cl.dim || '';
|
|
4047
|
+
const BOLD = cl.bold || '';
|
|
4048
|
+
const GRAY = cl.gray || '';
|
|
4049
|
+
const GREEN = cl.green || '';
|
|
4050
|
+
const RST = cl.reset || '';
|
|
4051
|
+
|
|
4052
|
+
process.stdout.write('\n');
|
|
4053
|
+
process.stdout.write(` ${BOLD}Shell startup${RST}\n\n`);
|
|
4054
|
+
process.stdout.write(` ${DIM}dual-brain can start automatically when your shell opens.${RST}\n`);
|
|
4055
|
+
process.stdout.write(` ${DIM}This modifies .replit onBoot. You can change it anytime in Settings.${RST}\n\n`);
|
|
4056
|
+
process.stdout.write(` ${GRAY}[y]${RST} Yes, set as default ${GRAY}[n]${RST} No, I'll run it manually\n\n`);
|
|
4057
|
+
|
|
4058
|
+
const answer = await new Promise(res => rl.question(' ', (a) => res(a.trim().toLowerCase())));
|
|
4059
|
+
const yes = !answer || answer.startsWith('y');
|
|
4060
|
+
|
|
4061
|
+
if (yes) {
|
|
4062
|
+
setAsDefaultShell(cwd);
|
|
4063
|
+
process.stdout.write(` ${GREEN}+${RST} ${DIM}dual-brain will start on boot. Change anytime in Settings.${RST}\n`);
|
|
4064
|
+
} else {
|
|
4065
|
+
process.stdout.write(` ${DIM}No problem. Run dual-brain anytime from the command line.${RST}\n`);
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
return yes;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
3987
4071
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
3988
4072
|
const fx = await getFx();
|
|
3989
4073
|
const cl = fx.colors || {};
|
|
@@ -4278,6 +4362,23 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4278
4362
|
finalProfile.bias = chosenBias;
|
|
4279
4363
|
finalProfile.workStyle = chosenBias;
|
|
4280
4364
|
|
|
4365
|
+
// Ask about default shell (only on first wizard run)
|
|
4366
|
+
if (!finalProfile.defaultShellAsked) {
|
|
4367
|
+
const wantsDefault = await askDefaultShell(cwd, rl, fx);
|
|
4368
|
+
finalProfile.defaultShellAsked = true;
|
|
4369
|
+
finalProfile.isDefaultShell = wantsDefault;
|
|
4370
|
+
saveProfile(finalProfile, { cwd });
|
|
4371
|
+
|
|
4372
|
+
// Also run global install if they said yes
|
|
4373
|
+
if (wantsDefault) {
|
|
4374
|
+
try {
|
|
4375
|
+
execSync('node ' + join(dirname(fileURLToPath(import.meta.url)), 'dual-brain.mjs') + ' install --global', {
|
|
4376
|
+
cwd, stdio: 'pipe', timeout: 10000,
|
|
4377
|
+
});
|
|
4378
|
+
} catch {}
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4281
4382
|
return finalProfile;
|
|
4282
4383
|
}
|
|
4283
4384
|
|