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.
Files changed (2) hide show
  1. package/bin/dual-brain.mjs +142 -41
  2. package/package.json +1 -1
@@ -1040,43 +1040,58 @@ async function installGlobal() {
1040
1040
  return;
1041
1041
  }
1042
1042
 
1043
- // Load existing settings (merge, never clobber)
1044
- let existing = {};
1045
- if (existsSync(globalSettingsPath)) {
1046
- try { existing = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch {}
1047
- }
1048
-
1049
- // Ensure hooks structure exists
1050
- if (!existing.hooks) existing.hooks = {};
1051
- if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
1052
- if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
1053
-
1054
- // Define dual-brain hooks with ownership marker
1055
- const DB_MARKER = '# dual-brain-managed';
1056
- const preToolHooks = [
1057
- { matcher: 'Edit', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
1058
- { matcher: 'Write', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
1059
- { matcher: 'NotebookEdit',hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
1060
- { matcher: 'Bash', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
1061
- { matcher: 'Agent', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'enforce-tier.mjs')} ${DB_MARKER}` }] },
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
- // Add dual-brain hooks
1074
- existing.hooks.PreToolUse.push(...preToolHooks);
1075
- existing.hooks.PostToolUse.push(...postToolHooks);
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
- // Write merged settings
1078
- mkdirSync(globalClaudeDir, { recursive: true });
1079
- writeFileSync(globalSettingsPath, JSON.stringify(existing, null, 2) + '\n');
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
- console.log(' + dual-brain hooks installed globally');
1095
- console.log(' hooks dir: ' + hooksDir);
1096
- console.log(' settings: ' + globalSettingsPath);
1097
- console.log('');
1098
- console.log(' All new Claude sessions will load dual-brain hooks.');
1099
- console.log(' Run "dual-brain uninstall --global" to remove.');
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {