dual-brain 0.2.5 → 0.2.6
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 +17 -41
- package/package.json +1 -1
- package/src/awareness.mjs +35 -6
- package/src/dispatch.mjs +0 -1
- package/src/doctor.mjs +6 -6
- package/src/health.mjs +3 -1
- package/src/pipeline.mjs +2 -2
- package/src/profile.mjs +39 -124
- package/src/replit.mjs +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -1981,13 +1979,9 @@ function buildProviderStatusLine(profile, auth, envReport = null) {
|
|
|
1981
1979
|
const GREEN = '\x1b[32m●\x1b[0m';
|
|
1982
1980
|
const RED = '\x1b[31m●\x1b[0m';
|
|
1983
1981
|
|
|
1984
|
-
//
|
|
1985
|
-
const claudeAvailable =
|
|
1986
|
-
|
|
1987
|
-
: auth.claude.found;
|
|
1988
|
-
const openaiAvailable = envReport
|
|
1989
|
-
? envReport.secrets.OPENAI_API_KEY || auth.openai.found
|
|
1990
|
-
: auth.openai.found;
|
|
1982
|
+
// Subscription-only detection — no API key secrets
|
|
1983
|
+
const claudeAvailable = auth.claude.found;
|
|
1984
|
+
const openaiAvailable = auth.openai.found;
|
|
1991
1985
|
|
|
1992
1986
|
const claudeDot = claudeAvailable ? GREEN : RED;
|
|
1993
1987
|
const openaiDot = openaiAvailable ? GREEN : RED;
|
|
@@ -2308,13 +2302,9 @@ async function mainScreen(rl, ask) {
|
|
|
2308
2302
|
envReport = scanEnvironment(cwd);
|
|
2309
2303
|
} catch { /* non-fatal */ }
|
|
2310
2304
|
|
|
2311
|
-
// ── Studio Console: resolve provider availability
|
|
2312
|
-
const claudeAvail =
|
|
2313
|
-
|
|
2314
|
-
: auth.claude.found;
|
|
2315
|
-
const openaiAvail = envReport
|
|
2316
|
-
? envReport.secrets?.OPENAI_API_KEY || auth.openai.found
|
|
2317
|
-
: auth.openai.found;
|
|
2305
|
+
// ── Studio Console: resolve provider availability (subscription-only) ───
|
|
2306
|
+
const claudeAvail = auth.claude.found;
|
|
2307
|
+
const openaiAvail = auth.openai.found;
|
|
2318
2308
|
|
|
2319
2309
|
// ── Box 2 — Workspace: gather git data ───────────────────────────────────
|
|
2320
2310
|
let gitBranch = 'unknown';
|
|
@@ -4108,12 +4098,10 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4108
4098
|
let claudeAuthLabel = null;
|
|
4109
4099
|
let claudeAuthType = null;
|
|
4110
4100
|
if (claudeReady) {
|
|
4111
|
-
if (caps.claude.source === 'claude-code') {
|
|
4101
|
+
if (caps.claude.source === 'claude-code' || caps.claude.source === 'claude-dir') {
|
|
4112
4102
|
claudeAuthLabel = 'CLI OAuth'; claudeAuthType = 'cli_oauth';
|
|
4113
|
-
} else if (caps.claude.source === 'env-key') {
|
|
4114
|
-
claudeAuthLabel = 'API key'; claudeAuthType = 'api_key';
|
|
4115
4103
|
} else {
|
|
4116
|
-
claudeAuthLabel = caps.claude.source || 'detected'; claudeAuthType = '
|
|
4104
|
+
claudeAuthLabel = caps.claude.source || 'detected'; claudeAuthType = 'cli_oauth';
|
|
4117
4105
|
}
|
|
4118
4106
|
fx.success(`Claude CLI found · ${claudeAuthLabel}`);
|
|
4119
4107
|
}
|
|
@@ -4121,10 +4109,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4121
4109
|
// OpenAI / Codex
|
|
4122
4110
|
let openaiAuthLabel = null;
|
|
4123
4111
|
let openaiAuthType = null;
|
|
4124
|
-
if (openaiReady) {
|
|
4125
|
-
openaiAuthLabel = 'API key'; openaiAuthType = 'api_key';
|
|
4126
|
-
fx.success('OpenAI detected · API key');
|
|
4127
|
-
} else if (codexAvailable) {
|
|
4112
|
+
if (openaiReady || codexAvailable) {
|
|
4128
4113
|
openaiAuthLabel = 'CLI OAuth'; openaiAuthType = 'cli_oauth';
|
|
4129
4114
|
fx.success('OpenAI Codex CLI found · authenticated');
|
|
4130
4115
|
}
|
|
@@ -4167,7 +4152,6 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4167
4152
|
} else if (noProvChoice === 'o') {
|
|
4168
4153
|
process.stdout.write('\n');
|
|
4169
4154
|
dimLine('Run: codex login');
|
|
4170
|
-
dimLine('Or add OPENAI_API_KEY to Replit Secrets if using API key auth.');
|
|
4171
4155
|
dimLine('Then re-run: dual-brain init');
|
|
4172
4156
|
process.stdout.write('\n');
|
|
4173
4157
|
}
|
|
@@ -4187,9 +4171,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4187
4171
|
if (claudeReady) {
|
|
4188
4172
|
process.stdout.write(` ${GRAY}Claude${RST} ${claudeAuthLabel} ${GREEN}✓ authenticated${RST}\n`);
|
|
4189
4173
|
}
|
|
4190
|
-
if (openaiReady) {
|
|
4191
|
-
process.stdout.write(` ${GRAY}OpenAI${RST} API key ${GREEN}✓ OPENAI_API_KEY${RST}\n`);
|
|
4192
|
-
} else if (codexAvailable) {
|
|
4174
|
+
if (openaiReady || codexAvailable) {
|
|
4193
4175
|
process.stdout.write(` ${GRAY}OpenAI${RST} CLI OAuth ${GREEN}✓ authenticated${RST}\n`);
|
|
4194
4176
|
}
|
|
4195
4177
|
|
|
@@ -4214,8 +4196,8 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4214
4196
|
process.stdout.write('\n');
|
|
4215
4197
|
} else if (provChoice === 'a') {
|
|
4216
4198
|
process.stdout.write('\n');
|
|
4217
|
-
if (!claudeReady) dimLine('Claude: run `claude
|
|
4218
|
-
if (!openaiReady && !codexAvailable) dimLine('OpenAI:
|
|
4199
|
+
if (!claudeReady) dimLine('Claude: run `claude login` to authenticate');
|
|
4200
|
+
if (!openaiReady && !codexAvailable) dimLine('OpenAI: run `codex login` to authenticate');
|
|
4219
4201
|
process.stdout.write('\n');
|
|
4220
4202
|
process.stdout.write(` ${GRAY}[Enter]${RST} continue with current providers\n\n`);
|
|
4221
4203
|
await singleKey(['\r', 'q']);
|
|
@@ -4238,10 +4220,10 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4238
4220
|
}
|
|
4239
4221
|
if (finalOpenaiEnabled) {
|
|
4240
4222
|
credEntries.push({
|
|
4241
|
-
id:
|
|
4223
|
+
id: 'openai-codex',
|
|
4242
4224
|
provider: 'openai',
|
|
4243
|
-
auth_type:
|
|
4244
|
-
source:
|
|
4225
|
+
auth_type: 'cli_oauth',
|
|
4226
|
+
source: 'cli_oauth',
|
|
4245
4227
|
owner: 'user',
|
|
4246
4228
|
scope: 'local',
|
|
4247
4229
|
plan_hint: null,
|
|
@@ -4263,12 +4245,6 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4263
4245
|
const styleMap = { '1': 'auto', '2': 'quality-first', '3': 'cost-saver', '\r': 'auto' };
|
|
4264
4246
|
const chosenBias = styleMap[styleKey] || 'auto';
|
|
4265
4247
|
|
|
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
4248
|
process.stdout.write('\n');
|
|
4273
4249
|
|
|
4274
4250
|
// Init living docs (non-fatal)
|
|
@@ -4294,7 +4270,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4294
4270
|
|
|
4295
4271
|
finalProfile.providers.claude = { enabled: finalClaudeEnabled };
|
|
4296
4272
|
finalProfile.providers.openai = { enabled: finalOpenaiEnabled };
|
|
4297
|
-
finalProfile.apiGuardrail =
|
|
4273
|
+
finalProfile.apiGuardrail = false;
|
|
4298
4274
|
finalProfile.setupComplete = true;
|
|
4299
4275
|
|
|
4300
4276
|
const enabledCount = [finalClaudeEnabled, finalOpenaiEnabled].filter(Boolean).length;
|
package/package.json
CHANGED
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:
|
|
568
|
-
|
|
569
|
-
return { status:
|
|
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:
|
|
572
|
-
|
|
573
|
-
return { status:
|
|
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 ??
|
|
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?.
|
|
880
|
-
if (run.environment?.
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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:
|
|
204
|
-
source:
|
|
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
|
|
241
|
-
if (capabilities?.
|
|
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
|
|
262
|
-
if (capabilities?.claude?.available && !capabilities?.
|
|
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
|
|
397
|
-
if (capabilities.
|
|
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.
|
|
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.
|
|
413
|
-
const q1 = (await ask('Which AI providers do you have access to?\n (1) Claude
|
|
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;
|
|
416
|
-
else if (q1 === '3') { profile.providers.claude.enabled = true; profile.providers.openai.enabled = 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 —
|
|
522
|
+
result.warnings.push('Claude not detected — run: claude login');
|
|
550
523
|
}
|
|
551
524
|
|
|
552
525
|
// OpenAI / Codex
|
|
553
|
-
if (capabilities.
|
|
526
|
+
if (capabilities.codex.available) {
|
|
554
527
|
profile.providers.openai.enabled = true;
|
|
555
|
-
result.actions.push('
|
|
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 —
|
|
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
|
-
//
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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-
|
|
916
|
+
id: 'openai-codex-cli',
|
|
973
917
|
provider: 'openai',
|
|
974
|
-
auth_type: '
|
|
975
|
-
source: '
|
|
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:
|
|
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
|
|
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
|
|
1044
|
+
if (process.env.CLAUDE_CODE) {
|
|
1127
1045
|
claudeProvider.available = true;
|
|
1128
|
-
claudeProvider.source =
|
|
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 =
|
|
1174
|
-
openaiProvider.authenticated =
|
|
1175
|
-
openaiProvider.source =
|
|
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 :
|
|
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,
|
|
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
|
-
'
|
|
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',
|