dual-brain 0.2.5 → 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.
@@ -946,9 +946,7 @@ async function cmdStatus(args = []) {
946
946
  const archive = replit.getSessionArchive(cwd);
947
947
  const archiveCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
948
948
  console.log(` session archive: ${archiveCount} session${archiveCount !== 1 ? 's' : ''}`);
949
- const openaiPresent = replit.hasSecret('OPENAI_API_KEY');
950
- const anthropicPresent = replit.hasSecret('ANTHROPIC_API_KEY');
951
- console.log(` secrets : OPENAI_API_KEY=${openaiPresent ? 'set' : 'unset'} ANTHROPIC_API_KEY=${anthropicPresent ? 'set' : 'unset'}`);
949
+ // Subscription-only: no API key secrets to check
952
950
  }
953
951
  } catch { /* replit.mjs not available or not in Replit — skip silently */ }
954
952
 
@@ -1042,43 +1040,58 @@ async function installGlobal() {
1042
1040
  return;
1043
1041
  }
1044
1042
 
1045
- // Load existing settings (merge, never clobber)
1046
- let existing = {};
1047
- if (existsSync(globalSettingsPath)) {
1048
- try { existing = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch {}
1049
- }
1050
-
1051
- // Ensure hooks structure exists
1052
- if (!existing.hooks) existing.hooks = {};
1053
- if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
1054
- if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
1055
-
1056
- // Define dual-brain hooks with ownership marker
1057
- const DB_MARKER = '# dual-brain-managed';
1058
- const preToolHooks = [
1059
- { matcher: 'Edit', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
1060
- { matcher: 'Write', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
1061
- { matcher: 'NotebookEdit',hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
1062
- { matcher: 'Bash', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
1063
- { matcher: 'Agent', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'enforce-tier.mjs')} ${DB_MARKER}` }] },
1064
- ];
1065
- const postToolHooks = [
1066
- { matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'cost-logger.mjs')} ${DB_MARKER}` }] },
1067
- { matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'auto-update-wrapper.mjs')} ${DB_MARKER}` }] },
1068
- ];
1069
-
1070
- // Remove any existing dual-brain hooks (idempotent)
1071
- const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
1072
- existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(e => !isDBHook(e));
1073
- 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
+ }
1074
1062
 
1075
- // Add dual-brain hooks
1076
- existing.hooks.PreToolUse.push(...preToolHooks);
1077
- 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 = [];
1078
1067
 
1079
- // Write merged settings
1080
- mkdirSync(globalClaudeDir, { recursive: true });
1081
- 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
+ }
1082
1095
 
1083
1096
  // Write minimal global CLAUDE.md (only if none exists, or append section)
1084
1097
  const globalClaudeMd = join(globalClaudeDir, 'CLAUDE.md');
@@ -1093,12 +1106,16 @@ async function installGlobal() {
1093
1106
  }
1094
1107
  }
1095
1108
 
1096
- console.log(' + dual-brain hooks installed globally');
1097
- console.log(' hooks dir: ' + hooksDir);
1098
- console.log(' settings: ' + globalSettingsPath);
1099
- console.log('');
1100
- console.log(' All new Claude sessions will load dual-brain hooks.');
1101
- 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);
1102
1119
  }
1103
1120
 
1104
1121
  async function uninstallGlobal() {
@@ -1981,13 +1998,9 @@ function buildProviderStatusLine(profile, auth, envReport = null) {
1981
1998
  const GREEN = '\x1b[32m●\x1b[0m';
1982
1999
  const RED = '\x1b[31m●\x1b[0m';
1983
2000
 
1984
- // Use envReport secrets when available; fall back to auth detection
1985
- const claudeAvailable = envReport
1986
- ? envReport.secrets.ANTHROPIC_API_KEY || auth.claude.found
1987
- : auth.claude.found;
1988
- const openaiAvailable = envReport
1989
- ? envReport.secrets.OPENAI_API_KEY || auth.openai.found
1990
- : auth.openai.found;
2001
+ // Subscription-only detection no API key secrets
2002
+ const claudeAvailable = auth.claude.found;
2003
+ const openaiAvailable = auth.openai.found;
1991
2004
 
1992
2005
  const claudeDot = claudeAvailable ? GREEN : RED;
1993
2006
  const openaiDot = openaiAvailable ? GREEN : RED;
@@ -2169,6 +2182,15 @@ async function mainScreen(rl, ask) {
2169
2182
  dashSpinner = fx.spinner('Loading dashboard...').start();
2170
2183
  }
2171
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
+
2172
2194
  const claudeSub = profile?.providers?.claude;
2173
2195
  const openaiSub = profile?.providers?.openai;
2174
2196
 
@@ -2308,13 +2330,9 @@ async function mainScreen(rl, ask) {
2308
2330
  envReport = scanEnvironment(cwd);
2309
2331
  } catch { /* non-fatal */ }
2310
2332
 
2311
- // ── Studio Console: resolve provider availability ────────────────────────
2312
- const claudeAvail = envReport
2313
- ? envReport.secrets?.ANTHROPIC_API_KEY || auth.claude.found
2314
- : auth.claude.found;
2315
- const openaiAvail = envReport
2316
- ? envReport.secrets?.OPENAI_API_KEY || auth.openai.found
2317
- : auth.openai.found;
2333
+ // ── Studio Console: resolve provider availability (subscription-only) ───
2334
+ const claudeAvail = auth.claude.found;
2335
+ const openaiAvail = auth.openai.found;
2318
2336
 
2319
2337
  // ── Box 2 — Workspace: gather git data ───────────────────────────────────
2320
2338
  let gitBranch = 'unknown';
@@ -3994,6 +4012,62 @@ function saveWizardCredentials(cwd, detectedProviders) {
3994
4012
  * @param {object} rl readline interface
3995
4013
  * @returns {object|null} profile object to save, or null if cancelled/skipped
3996
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
+
3997
4071
  async function runOnboardingWizard(_detection, cwd, rl) {
3998
4072
  const fx = await getFx();
3999
4073
  const cl = fx.colors || {};
@@ -4108,12 +4182,10 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4108
4182
  let claudeAuthLabel = null;
4109
4183
  let claudeAuthType = null;
4110
4184
  if (claudeReady) {
4111
- if (caps.claude.source === 'claude-code') {
4185
+ if (caps.claude.source === 'claude-code' || caps.claude.source === 'claude-dir') {
4112
4186
  claudeAuthLabel = 'CLI OAuth'; claudeAuthType = 'cli_oauth';
4113
- } else if (caps.claude.source === 'env-key') {
4114
- claudeAuthLabel = 'API key'; claudeAuthType = 'api_key';
4115
4187
  } else {
4116
- claudeAuthLabel = caps.claude.source || 'detected'; claudeAuthType = 'unknown';
4188
+ claudeAuthLabel = caps.claude.source || 'detected'; claudeAuthType = 'cli_oauth';
4117
4189
  }
4118
4190
  fx.success(`Claude CLI found · ${claudeAuthLabel}`);
4119
4191
  }
@@ -4121,10 +4193,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4121
4193
  // OpenAI / Codex
4122
4194
  let openaiAuthLabel = null;
4123
4195
  let openaiAuthType = null;
4124
- if (openaiReady) {
4125
- openaiAuthLabel = 'API key'; openaiAuthType = 'api_key';
4126
- fx.success('OpenAI detected · API key');
4127
- } else if (codexAvailable) {
4196
+ if (openaiReady || codexAvailable) {
4128
4197
  openaiAuthLabel = 'CLI OAuth'; openaiAuthType = 'cli_oauth';
4129
4198
  fx.success('OpenAI Codex CLI found · authenticated');
4130
4199
  }
@@ -4167,7 +4236,6 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4167
4236
  } else if (noProvChoice === 'o') {
4168
4237
  process.stdout.write('\n');
4169
4238
  dimLine('Run: codex login');
4170
- dimLine('Or add OPENAI_API_KEY to Replit Secrets if using API key auth.');
4171
4239
  dimLine('Then re-run: dual-brain init');
4172
4240
  process.stdout.write('\n');
4173
4241
  }
@@ -4187,9 +4255,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4187
4255
  if (claudeReady) {
4188
4256
  process.stdout.write(` ${GRAY}Claude${RST} ${claudeAuthLabel} ${GREEN}✓ authenticated${RST}\n`);
4189
4257
  }
4190
- if (openaiReady) {
4191
- process.stdout.write(` ${GRAY}OpenAI${RST} API key ${GREEN}✓ OPENAI_API_KEY${RST}\n`);
4192
- } else if (codexAvailable) {
4258
+ if (openaiReady || codexAvailable) {
4193
4259
  process.stdout.write(` ${GRAY}OpenAI${RST} CLI OAuth ${GREEN}✓ authenticated${RST}\n`);
4194
4260
  }
4195
4261
 
@@ -4214,8 +4280,8 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4214
4280
  process.stdout.write('\n');
4215
4281
  } else if (provChoice === 'a') {
4216
4282
  process.stdout.write('\n');
4217
- if (!claudeReady) dimLine('Claude: run `claude auth login` to authenticate');
4218
- if (!openaiReady && !codexAvailable) dimLine('OpenAI: set OPENAI_API_KEY or run `codex login`');
4283
+ if (!claudeReady) dimLine('Claude: run `claude login` to authenticate');
4284
+ if (!openaiReady && !codexAvailable) dimLine('OpenAI: run `codex login` to authenticate');
4219
4285
  process.stdout.write('\n');
4220
4286
  process.stdout.write(` ${GRAY}[Enter]${RST} continue with current providers\n\n`);
4221
4287
  await singleKey(['\r', 'q']);
@@ -4238,10 +4304,10 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4238
4304
  }
4239
4305
  if (finalOpenaiEnabled) {
4240
4306
  credEntries.push({
4241
- id: openaiReady ? 'openai-apikey' : 'openai-codex',
4307
+ id: 'openai-codex',
4242
4308
  provider: 'openai',
4243
- auth_type: openaiAuthType || 'api_key',
4244
- source: openaiReady ? 'env_var' : 'cli_oauth',
4309
+ auth_type: 'cli_oauth',
4310
+ source: 'cli_oauth',
4245
4311
  owner: 'user',
4246
4312
  scope: 'local',
4247
4313
  plan_hint: null,
@@ -4263,12 +4329,6 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4263
4329
  const styleMap = { '1': 'auto', '2': 'quality-first', '3': 'cost-saver', '\r': 'auto' };
4264
4330
  const chosenBias = styleMap[styleKey] || 'auto';
4265
4331
 
4266
- // Metered API note (non-blocking)
4267
- if (openaiReady && caps.openai.metered) {
4268
- process.stdout.write('\n');
4269
- dimLine('OpenAI API key detected — usage is metered, guardrails enabled');
4270
- }
4271
-
4272
4332
  process.stdout.write('\n');
4273
4333
 
4274
4334
  // Init living docs (non-fatal)
@@ -4294,7 +4354,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4294
4354
 
4295
4355
  finalProfile.providers.claude = { enabled: finalClaudeEnabled };
4296
4356
  finalProfile.providers.openai = { enabled: finalOpenaiEnabled };
4297
- finalProfile.apiGuardrail = caps.openai.metered;
4357
+ finalProfile.apiGuardrail = false;
4298
4358
  finalProfile.setupComplete = true;
4299
4359
 
4300
4360
  const enabledCount = [finalClaudeEnabled, finalOpenaiEnabled].filter(Boolean).length;
@@ -4302,6 +4362,23 @@ async function runOnboardingWizard(_detection, cwd, rl) {
4302
4362
  finalProfile.bias = chosenBias;
4303
4363
  finalProfile.workStyle = chosenBias;
4304
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
+
4305
4382
  return finalProfile;
4306
4383
  }
4307
4384
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.2.5",
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": {
package/src/awareness.mjs CHANGED
@@ -49,8 +49,6 @@ function detectContainerType() {
49
49
 
50
50
  function scanSecrets() {
51
51
  const keys = [
52
- 'OPENAI_API_KEY',
53
- 'ANTHROPIC_API_KEY',
54
52
  'NPM_TOKEN',
55
53
  'DATABASE_URL',
56
54
  'GITHUB_TOKEN',
@@ -282,8 +280,6 @@ export function formatEnvironment(report) {
282
280
  if (toolEntries.length) lines.push(`Tools: ${toolEntries.join(' ')}`);
283
281
 
284
282
  const secretMap = {
285
- OPENAI_API_KEY: 'OpenAI',
286
- ANTHROPIC_API_KEY: 'Anthropic',
287
283
  NPM_TOKEN: 'npm',
288
284
  GITHUB_TOKEN: 'GitHub',
289
285
  DATABASE_URL: 'PostgreSQL',
@@ -335,8 +331,6 @@ export function getCapabilitySummary(report) {
335
331
  if (report.tools.gh?.available) caps.push('github-cli');
336
332
  if (report.tools.rg?.available) caps.push('ripgrep');
337
333
 
338
- if (report.secrets.OPENAI_API_KEY) caps.push('openai-key');
339
- if (report.secrets.ANTHROPIC_API_KEY) caps.push('anthropic-key');
340
334
 
341
335
  if (report.replitTools.installed) {
342
336
  for (const c of report.replitTools.capabilities) {
@@ -394,3 +388,38 @@ export function detectAmbiguity(prompt, context = {}) {
394
388
 
395
389
  return { isAmbiguous: false, reason: null };
396
390
  }
391
+
392
+ /**
393
+ * Detect whether a user prompt is too vague to act on confidently.
394
+ *
395
+ * Checks for:
396
+ * - Very short prompts (under 10 chars)
397
+ * - No file paths, function names, or specific identifiers
398
+ * - Pronoun-only references without antecedents ("fix that thing", "change it")
399
+ *
400
+ * @param {string} prompt — the user's raw prompt
401
+ * @returns {{ ambiguous: boolean, reason: string|null, confidence: number }}
402
+ */
403
+ export function isAmbiguous(prompt) {
404
+ if (!prompt || typeof prompt !== 'string') return { ambiguous: true, reason: 'empty-prompt', confidence: 1.0 };
405
+
406
+ const trimmed = prompt.trim();
407
+ if (trimmed.length < 10) return { ambiguous: true, reason: 'too-short', confidence: 0.9 };
408
+
409
+ // Check for file paths, function names, specific identifiers
410
+ const hasSpecifics = /[a-zA-Z_]\w*\.(mjs|js|ts|tsx|jsx|py|go|rs|java|rb|css|html|json|yaml|yml|md|sh)/.test(trimmed)
411
+ || /[a-zA-Z_]\w*\(\)/.test(trimmed) // function calls
412
+ || /`[^`]+`/.test(trimmed) // backtick-quoted identifiers
413
+ || /"[^"]{3,}"/.test(trimmed) // quoted strings
414
+ || /\b(line|function|class|method|variable|module|component|endpoint|route|table|column)\s+\w+/i.test(trimmed);
415
+
416
+ // Vague pronoun patterns
417
+ const vaguePatterns = /^(fix|change|update|do|make|help|look at|check)\s+(this|that|it|the thing|stuff|things?)$/i;
418
+ if (vaguePatterns.test(trimmed)) return { ambiguous: true, reason: 'vague-reference', confidence: 0.85 };
419
+
420
+ if (!hasSpecifics && trimmed.split(/\s+/).length < 5) {
421
+ return { ambiguous: true, reason: 'lacks-specifics', confidence: 0.7 };
422
+ }
423
+
424
+ return { ambiguous: false, reason: null, confidence: 0.1 };
425
+ }
package/src/dispatch.mjs CHANGED
@@ -345,7 +345,6 @@ async function detectRuntime() {
345
345
  claudeAvailable && codexAvailable ? 'claude-code'
346
346
  : claudeAvailable ? 'claude-code'
347
347
  : codexAvailable ? 'codex-cli'
348
- : process.env.CLAUDE_API_KEY || process.env.ANTHROPIC_API_KEY ? 'standalone'
349
348
  : 'none';
350
349
 
351
350
  _runtimeCache = { claudeAvailable, codexAvailable, runtime };
package/src/doctor.mjs CHANGED
@@ -564,13 +564,13 @@ const VERIFIERS = {
564
564
  try { execSync('which claude', { stdio: 'pipe', timeout: 2000 }); return { status: 'verified', evidence: 'claude CLI found', probe: 'which claude' }; }
565
565
  catch { return { status: 'failed', evidence: 'claude CLI not found', probe: 'which claude' }; }
566
566
  }},
567
- 'openai-key': { ttl: TTL_RUNTIME, fn: () => {
568
- const has = !!process.env.OPENAI_API_KEY;
569
- return { status: has ? 'verified' : 'failed', evidence: has ? 'OPENAI_API_KEY present' : 'OPENAI_API_KEY missing', probe: 'env check' };
567
+ 'openai-key': { ttl: TTL_TOOL, fn: () => {
568
+ try { execSync('which codex', { stdio: 'pipe', timeout: 2000 }); return { status: 'verified', evidence: 'codex CLI found (subscription auth)', probe: 'which codex' }; }
569
+ catch { return { status: 'failed', evidence: 'codex CLI not found — run: codex login', probe: 'which codex' }; }
570
570
  }},
571
- 'anthropic-key': { ttl: TTL_RUNTIME, fn: () => {
572
- const has = !!(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY);
573
- return { status: has ? 'verified' : 'failed', evidence: has ? 'API key present' : 'API key missing', probe: 'env check' };
571
+ 'anthropic-key': { ttl: TTL_TOOL, fn: () => {
572
+ try { execSync('which claude', { stdio: 'pipe', timeout: 2000 }); return { status: 'verified', evidence: 'claude CLI found (subscription auth)', probe: 'which claude' }; }
573
+ catch { return { status: 'failed', evidence: 'claude CLI not found run: claude login', probe: 'which claude' }; }
574
574
  }},
575
575
  'git-available': { ttl: TTL_TOOL, fn: () => {
576
576
  try { const v = execSync('git --version', { stdio: 'pipe', timeout: 2000 }).toString().trim(); return { status: 'verified', evidence: v, probe: 'git --version' }; }
package/src/health.mjs CHANGED
@@ -94,6 +94,8 @@ export async function getAuthHealthStatus(cwd) {
94
94
  return { ok: false, detail: 'Auth: no credentials found (direct check)', source: 'unknown' };
95
95
  }
96
96
 
97
+ const HEALTH_CHECK_TIMEOUT_MS = 5000;
98
+
97
99
  const HEALTH_FILE = '.dualbrain/health.json';
98
100
 
99
101
  // Cooldown ladder in minutes: index = attempts - 1, capped at last entry
@@ -327,7 +329,7 @@ export function resetHealth(cwd) {
327
329
  * @returns {Promise<{ ok: boolean, status: 'ok'|'timeout'|'error', detail?: string }>}
328
330
  */
329
331
  export async function pingProvider(url, opts = {}) {
330
- const timeoutMs = opts.timeoutMs ?? 5000;
332
+ const timeoutMs = opts.timeoutMs ?? HEALTH_CHECK_TIMEOUT_MS;
331
333
  const controller = new AbortController();
332
334
  const timer = setTimeout(() => controller.abort(), timeoutMs);
333
335
  try {
package/src/pipeline.mjs CHANGED
@@ -876,8 +876,8 @@ export async function runPipeline(trigger, prompt, options = {}) {
876
876
  try {
877
877
  const { suggestModel, getRegistryAge } = await import('./models.mjs');
878
878
  const availableProviders = [];
879
- if (run.environment?.secrets?.ANTHROPIC_API_KEY || run.environment?.claudeCode?.isInsideClaude) availableProviders.push('anthropic');
880
- if (run.environment?.secrets?.OPENAI_API_KEY) availableProviders.push('openai');
879
+ if (run.environment?.claudeCode?.isInsideClaude || run.environment?.tools?.claude?.available) availableProviders.push('anthropic');
880
+ if (run.environment?.tools?.codex?.available) availableProviders.push('openai');
881
881
 
882
882
  const intent = run.promptAnalysis?.intent?.type || 'execute';
883
883
  const risk = run.plan?.risk || 'medium';
package/src/profile.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  * getHeadModel(profile) → suggested head model string
16
16
  * detectCapabilities(cwd) → what we can actually verify
17
17
  * getOnboardingMessage(caps, ws) → honest 2-3 line status message
18
- * needsApiGuardrail(caps) → true if metered API key detected
18
+ * detectCapabilities(cwd) → available providers (subscription-based only)
19
19
  *
20
20
  * CLI:
21
21
  * node src/profile.mjs # show current profile
@@ -136,7 +136,7 @@ function detectEnvironment() {
136
136
  * @param {string} [cwd]
137
137
  * @returns {Promise<{
138
138
  * claude: { available: boolean, source: string|null },
139
- * openai: { available: boolean, source: string|null, metered: boolean },
139
+ * openai: { available: boolean, source: string|null },
140
140
  * codex: { available: boolean, source: string|null },
141
141
  * replitTools: { available: boolean, checkpoints: boolean },
142
142
  * }>}
@@ -144,18 +144,15 @@ function detectEnvironment() {
144
144
  async function detectCapabilities(cwd) {
145
145
  const root = cwd || process.cwd();
146
146
 
147
- // --- Claude: running inside Claude Code or has ANTHROPIC_API_KEY or ~/.claude dir ---
147
+ // --- Claude: running inside Claude Code session or CLI installed ---
148
148
  let claudeAvailable = false;
149
149
  let claudeSource = null;
150
150
 
151
151
  if (process.env.CLAUDE_CODE) {
152
152
  claudeAvailable = true;
153
153
  claudeSource = 'claude-code';
154
- } else if (process.env.ANTHROPIC_API_KEY?.length > 0) {
155
- claudeAvailable = true;
156
- claudeSource = 'api-key';
157
154
  } else {
158
- // Check for ~/.claude directory (Claude Code installation)
155
+ // Check for ~/.claude directory (Claude Code installation) or Replit Claude
159
156
  const claudeDir = join(homedir(), '.claude');
160
157
  const replitClaudeDir = join(root, '.replit-tools', '.claude-persistent');
161
158
  if (existsSync(claudeDir) || existsSync(replitClaudeDir)) {
@@ -164,9 +161,6 @@ async function detectCapabilities(cwd) {
164
161
  }
165
162
  }
166
163
 
167
- // --- OpenAI: check for OPENAI_API_KEY presence (metered billing) ---
168
- const openaiAvailable = !!process.env.OPENAI_API_KEY?.length;
169
-
170
164
  // --- Codex: check if 'codex' is in PATH ---
171
165
  let codexAvailable = false;
172
166
  let codexSource = null;
@@ -200,9 +194,8 @@ async function detectCapabilities(cwd) {
200
194
  source: claudeSource,
201
195
  },
202
196
  openai: {
203
- available: openaiAvailable || codexAvailable,
204
- source: openaiAvailable ? 'api-key' : codexAvailable ? 'codex-cli' : null,
205
- metered: openaiAvailable && !codexAvailable,
197
+ available: codexAvailable,
198
+ source: codexAvailable ? 'codex-cli' : null,
206
199
  },
207
200
  codex: {
208
201
  available: codexAvailable,
@@ -215,18 +208,6 @@ async function detectCapabilities(cwd) {
215
208
  };
216
209
  }
217
210
 
218
- /**
219
- * Return true if any metered API key is detected.
220
- * When true, the system defaults to conservative API usage and should
221
- * confirm before expensive operations.
222
- *
223
- * @param {ReturnType<typeof detectCapabilities> extends Promise<infer T> ? T : never} capabilities
224
- * @returns {boolean}
225
- */
226
- function needsApiGuardrail(capabilities) {
227
- return !!(capabilities?.openai?.metered);
228
- }
229
-
230
211
  /**
231
212
  * Generate an honest 2-3 line onboarding/status message based on
232
213
  * what we can actually verify.
@@ -237,9 +218,8 @@ function needsApiGuardrail(capabilities) {
237
218
  */
238
219
  function getOnboardingMessage(capabilities, workStyle = 'balanced') {
239
220
  const found = [];
240
- if (capabilities?.claude?.available) found.push('Claude Code');
241
- if (capabilities?.openai?.available) found.push('OpenAI API');
242
- if (capabilities?.codex?.available && !capabilities?.openai?.available) found.push('Codex CLI');
221
+ if (capabilities?.claude?.available) found.push('Claude · subscription');
222
+ if (capabilities?.codex?.available) found.push('OpenAI · Codex subscription');
243
223
 
244
224
  const styleLabels = {
245
225
  'balanced': 'Balanced — smart routing, reviews on important changes',
@@ -258,16 +238,11 @@ function getOnboardingMessage(capabilities, workStyle = 'balanced') {
258
238
  lines.push(`Found: ${found.join(', ')}`);
259
239
  lines.push(` Mode: ${modeLabel}`);
260
240
 
261
- // Tip: suggest OpenAI if only Claude is available
262
- if (capabilities?.claude?.available && !capabilities?.openai?.available && !capabilities?.codex?.available) {
241
+ // Tip: suggest Codex if only Claude is available
242
+ if (capabilities?.claude?.available && !capabilities?.codex?.available) {
263
243
  lines.push(' Tip: Run codex login for dual-brain collaboration');
264
244
  }
265
245
 
266
- // Warn about metered billing
267
- if (capabilities?.openai?.metered) {
268
- lines.push(' Note: OpenAI API key detected — usage is metered, guardrails enabled');
269
- }
270
-
271
246
  return lines.join('\n');
272
247
  }
273
248
 
@@ -393,9 +368,8 @@ async function runOnboarding(opts = {}) {
393
368
 
394
369
  // Show what we found honestly
395
370
  const foundProviders = [];
396
- if (capabilities.claude.available) foundProviders.push('Claude Code');
397
- if (capabilities.openai.available) foundProviders.push('OpenAI API (metered)');
398
- if (capabilities.codex.available && !capabilities.openai.available) foundProviders.push('Codex CLI');
371
+ if (capabilities.claude.available) foundProviders.push('Claude · subscription');
372
+ if (capabilities.codex.available) foundProviders.push('OpenAI · Codex subscription');
399
373
 
400
374
  if (foundProviders.length > 0) {
401
375
  process.stdout.write(`Detected: ${foundProviders.join(', ')}\n\n`);
@@ -405,15 +379,14 @@ async function runOnboarding(opts = {}) {
405
379
 
406
380
  // Enable providers based on what's available
407
381
  profile.providers.claude.enabled = capabilities.claude.available;
408
- profile.providers.openai.enabled = capabilities.openai.available || capabilities.codex.available;
409
- profile.apiGuardrail = needsApiGuardrail(capabilities);
382
+ profile.providers.openai.enabled = capabilities.codex.available;
410
383
 
411
384
  // If detection missed something, ask
412
- if (!capabilities.claude.available && !capabilities.openai.available && !capabilities.codex.available) {
413
- const q1 = (await ask('Which AI providers do you have access to?\n (1) Claude Code only (2) OpenAI API only (3) Both (4) Neither\n> ')).trim();
385
+ if (!capabilities.claude.available && !capabilities.codex.available) {
386
+ const q1 = (await ask('Which AI providers do you have access to?\n (1) Claude only (2) OpenAI Codex only (3) Both (4) Neither\n> ')).trim();
414
387
  if (q1 === '1') { profile.providers.claude.enabled = true; }
415
- else if (q1 === '2') { profile.providers.claude.enabled = false; profile.providers.openai.enabled = true; profile.apiGuardrail = true; }
416
- else if (q1 === '3') { profile.providers.claude.enabled = true; profile.providers.openai.enabled = true; profile.apiGuardrail = true; }
388
+ else if (q1 === '2') { profile.providers.claude.enabled = false; profile.providers.openai.enabled = true; }
389
+ else if (q1 === '3') { profile.providers.claude.enabled = true; profile.providers.openai.enabled = true; }
417
390
  }
418
391
 
419
392
  const q3 = (await ask('\nDefault work style?\n (1) Save usage (2) Balanced (3) Best quality\n> ')).trim();
@@ -546,19 +519,16 @@ async function autoSetup(cwd) {
546
519
  result.actions.push(`Claude: available (${capabilities.claude.source})`);
547
520
  } else {
548
521
  profile.providers.claude.enabled = false;
549
- result.warnings.push('Claude not detected — install Claude Code or set ANTHROPIC_API_KEY');
522
+ result.warnings.push('Claude not detected — run: claude login');
550
523
  }
551
524
 
552
525
  // OpenAI / Codex
553
- if (capabilities.openai.available) {
526
+ if (capabilities.codex.available) {
554
527
  profile.providers.openai.enabled = true;
555
- result.actions.push('OpenAI: API key detected (metered billing — guardrails enabled)');
556
- } else if (capabilities.codex.available) {
557
- profile.providers.openai.enabled = true;
558
- result.actions.push('Codex CLI: available');
528
+ result.actions.push('Codex CLI: available (subscription)');
559
529
  } else {
560
530
  profile.providers.openai.enabled = false;
561
- result.warnings.push('OpenAI not detected — add OPENAI_API_KEY or install Codex CLI');
531
+ result.warnings.push('OpenAI not detected — run: codex login');
562
532
  }
563
533
 
564
534
  // Mode
@@ -568,7 +538,6 @@ async function autoSetup(cwd) {
568
538
  : 'solo-openai';
569
539
  profile.bias = 'balanced';
570
540
  profile.workStyle = 'balanced';
571
- profile.apiGuardrail = needsApiGuardrail(capabilities);
572
541
  profile.capabilities = capabilities;
573
542
  profile.detectedAt = new Date().toISOString();
574
543
 
@@ -909,17 +878,6 @@ export async function checkCredentialHealth(cred, cwd = process.cwd()) {
909
878
  } catch {
910
879
  health = 'healthy'; // cli works, auth check unavailable
911
880
  }
912
- } else if (cred.auth_type === 'api_key') {
913
- if (cred.source === 'replit_secret') {
914
- try {
915
- const { hasSecret } = await import('./replit.mjs');
916
- health = hasSecret(cred.env_var || cred.id.toUpperCase().replace(/-/g, '_')) ? 'healthy' : 'unhealthy';
917
- } catch { health = 'unknown'; }
918
- } else {
919
- // env source
920
- const varName = cred.env_var || cred.id.toUpperCase().replace(/-/g, '_');
921
- health = (process.env[varName] && process.env[varName].length > 0) ? 'healthy' : 'unhealthy';
922
- }
923
881
  }
924
882
  } catch { health = 'unknown'; }
925
883
  return { ...cred, health, last_checked_at: new Date().toISOString() };
@@ -949,64 +907,24 @@ export async function detectCredentials(cwd = process.cwd()) {
949
907
  });
950
908
  }
951
909
 
952
- // ANTHROPIC_API_KEY
953
- if (process.env.ANTHROPIC_API_KEY) {
954
- found.push({
955
- id: 'anthropic-api-key',
956
- provider: 'claude',
957
- auth_type: 'api_key',
958
- source: 'env',
959
- env_var: 'ANTHROPIC_API_KEY',
960
- owner: 'user',
961
- scope: 'local',
962
- plan_hint: null,
963
- enabled: true,
964
- health: 'healthy',
965
- last_checked_at: new Date().toISOString(),
966
- });
967
- }
968
-
969
- // OPENAI_API_KEY (env)
970
- if (process.env.OPENAI_API_KEY) {
910
+ // Codex CLI (subscription-based OpenAI access)
911
+ try {
912
+ execSync('which codex', { stdio: 'pipe', timeout: 2000 });
913
+ let codexHealth = 'unknown';
914
+ try { execSync('codex --version', { stdio: 'pipe', timeout: 3000 }); codexHealth = 'healthy'; } catch { codexHealth = 'degraded'; }
971
915
  found.push({
972
- id: 'openai-api-key',
916
+ id: 'openai-codex-cli',
973
917
  provider: 'openai',
974
- auth_type: 'api_key',
975
- source: 'env',
976
- env_var: 'OPENAI_API_KEY',
918
+ auth_type: 'cli_oauth',
919
+ source: 'local_cli',
977
920
  owner: 'user',
978
921
  scope: 'local',
979
922
  plan_hint: null,
980
923
  enabled: true,
981
- health: 'healthy',
924
+ health: codexHealth,
982
925
  last_checked_at: new Date().toISOString(),
983
926
  });
984
- }
985
-
986
- // Replit secrets
987
- try {
988
- const { hasSecret } = await import('./replit.mjs');
989
- const secretsToCheck = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY'];
990
- for (const name of secretsToCheck) {
991
- if (!process.env[name] && hasSecret(name)) {
992
- const provider = name.startsWith('OPENAI') ? 'openai' : 'claude';
993
- const id = name.toLowerCase().replace(/_/g, '-') + '-replit';
994
- found.push({
995
- id,
996
- provider,
997
- auth_type: 'api_key',
998
- source: 'replit_secret',
999
- env_var: name,
1000
- owner: 'user',
1001
- scope: 'workspace',
1002
- plan_hint: null,
1003
- enabled: true,
1004
- health: 'healthy',
1005
- last_checked_at: new Date().toISOString(),
1006
- });
1007
- }
1008
- }
1009
- } catch { /* replit.mjs unavailable */ }
927
+ } catch { /* codex not in PATH */ }
1010
928
 
1011
929
  return found;
1012
930
  }
@@ -1120,12 +1038,12 @@ export async function getCapabilityManifest(cwd = process.cwd()) {
1120
1038
  budget: { pressure5h: 0, pressure7d: 0 }, source: 'none' };
1121
1039
 
1122
1040
  try {
1123
- // available: claude CLI or CLAUDE_CODE env or replit-tools claude dir
1041
+ // available: CLAUDE_CODE env, claude CLI, or replit-tools claude dir
1124
1042
  const claudeDir = join(homedir(), '.claude');
1125
1043
  const replitClaudeDir = join(cwd, '.replit-tools', '.claude-persistent');
1126
- if (process.env.CLAUDE_CODE || process.env.ANTHROPIC_API_KEY) {
1044
+ if (process.env.CLAUDE_CODE) {
1127
1045
  claudeProvider.available = true;
1128
- claudeProvider.source = process.env.ANTHROPIC_API_KEY ? 'env' : 'credentials';
1046
+ claudeProvider.source = 'credentials';
1129
1047
  } else if (existsSync(claudeDir) || existsSync(replitClaudeDir)) {
1130
1048
  claudeProvider.available = true;
1131
1049
  claudeProvider.source = existsSync(replitClaudeDir) ? 'replit-tools' : 'credentials';
@@ -1164,15 +1082,12 @@ export async function getCapabilityManifest(cwd = process.cwd()) {
1164
1082
  budget: { pressure5h: 0, pressure7d: 0 }, source: 'none' };
1165
1083
 
1166
1084
  try {
1167
- let hasSecret = false;
1168
- try { const { hasSecret: hs } = await import('./replit.mjs'); hasSecret = hs('OPENAI_API_KEY'); } catch { hasSecret = !!(process.env.OPENAI_API_KEY); }
1169
-
1170
1085
  let codexAvailable = false;
1171
1086
  try { execSync('which codex', { stdio: 'pipe', timeout: 2000 }); codexAvailable = true; } catch { /* not in PATH */ }
1172
1087
 
1173
- openaiProvider.available = hasSecret || codexAvailable;
1174
- openaiProvider.authenticated = hasSecret;
1175
- openaiProvider.source = hasSecret ? 'env' : codexAvailable ? 'codex-config' : 'none';
1088
+ openaiProvider.available = codexAvailable;
1089
+ openaiProvider.authenticated = codexAvailable;
1090
+ openaiProvider.source = codexAvailable ? 'codex-cli' : 'none';
1176
1091
  } catch { /* detection failed */ }
1177
1092
 
1178
1093
  openaiProvider.plan = normalizePlan(orchProv.openai?.subscription ?? orchSubs.openai?.plan);
@@ -1387,7 +1302,7 @@ async function main() {
1387
1302
  `head model : ${getHeadModel(profile)}`,
1388
1303
  `providers : ${providers.map(p => p.name).join(', ') || 'none'}`,
1389
1304
  `prefs : ${profile.preferences?.filter(p => p.enabled).length || 0} active`,
1390
- `guardrail : ${needsApiGuardrail(caps) ? 'enabled (metered API key detected)' : 'off'}`,
1305
+ `guardrail : off`,
1391
1306
  '',
1392
1307
  getOnboardingMessage(caps, profile.workStyle || profile.bias),
1393
1308
  ].forEach(l => process.stdout.write(l + '\n'));
@@ -1404,7 +1319,7 @@ export {
1404
1319
  loadProfile, saveProfile, ensureProfile, runOnboarding,
1405
1320
  rememberPreference, forgetPreference, getActivePreferences,
1406
1321
  getAvailableProviders, isSoloBrain, getHeadModel,
1407
- detectCapabilities, getOnboardingMessage, needsApiGuardrail,
1322
+ detectCapabilities, getOnboardingMessage,
1408
1323
  syncPreferencesToMemory,
1409
1324
  detectAuth, detectEnvironment,
1410
1325
  autoSetup, autoRefreshToken,
package/src/replit.mjs CHANGED
@@ -339,7 +339,7 @@ const SYSTEM_PREFIXES = [
339
339
  ];
340
340
 
341
341
  const KNOWN_SECRET_NAMES = [
342
- 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'DATABASE_URL', 'REPLIT_DB_URL',
342
+ 'DATABASE_URL', 'REPLIT_DB_URL',
343
343
  'GITHUB_TOKEN', 'GITHUB_API_TOKEN', 'NPM_TOKEN', 'NPM_AUTH_TOKEN',
344
344
  'STRIPE_SECRET_KEY', 'STRIPE_API_KEY', 'AWS_ACCESS_KEY_ID',
345
345
  'AWS_SECRET_ACCESS_KEY', 'GOOGLE_API_KEY', 'GOOGLE_APPLICATION_CREDENTIALS',