dual-brain 7.1.21 → 7.1.22
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 +2580 -717
- package/hooks/budget-balancer.mjs +104 -266
- package/hooks/wave-orchestrator.mjs +29 -26
- package/package.json +13 -3
- package/scripts/verify-publish.mjs +26 -0
- package/src/context.mjs +389 -0
- package/src/decide.mjs +283 -60
- package/src/detect.mjs +133 -1
- package/src/dispatch.mjs +175 -30
- package/src/doctor.mjs +577 -0
- package/src/failure-memory.mjs +178 -0
- package/src/nextstep.mjs +100 -0
- package/src/observer.mjs +241 -0
- package/src/outcome.mjs +256 -0
- package/src/pipeline.mjs +759 -0
- package/src/profile.mjs +357 -485
- package/src/receipt.mjs +131 -0
- package/src/session.mjs +358 -10
package/src/profile.mjs
CHANGED
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
* rememberPreference(text, opts) → add/update preference
|
|
11
11
|
* forgetPreference(text, cwd) → remove preference by fuzzy match
|
|
12
12
|
* getActivePreferences(cwd) → enabled global + project preferences
|
|
13
|
-
* getAvailableProviders(profile) → enabled providers
|
|
13
|
+
* getAvailableProviders(profile) → enabled providers
|
|
14
14
|
* isSoloBrain(profile) → true if only one provider enabled
|
|
15
15
|
* getHeadModel(profile) → suggested head model string
|
|
16
|
+
* detectCapabilities(cwd) → what we can actually verify
|
|
17
|
+
* getOnboardingMessage(caps, ws) → honest 2-3 line status message
|
|
18
|
+
* needsApiGuardrail(caps) → true if metered API key detected
|
|
16
19
|
*
|
|
17
20
|
* CLI:
|
|
18
21
|
* node src/profile.mjs # show current profile
|
|
@@ -26,7 +29,7 @@ import { createInterface } from 'readline';
|
|
|
26
29
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
27
30
|
import { homedir } from 'os';
|
|
28
31
|
import { join } from 'path';
|
|
29
|
-
import {
|
|
32
|
+
import { execSync } from 'child_process';
|
|
30
33
|
|
|
31
34
|
// ---------------------------------------------------------------------------
|
|
32
35
|
// Claude Code memory integration
|
|
@@ -123,190 +126,150 @@ function detectEnvironment() {
|
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
// ---------------------------------------------------------------------------
|
|
126
|
-
//
|
|
129
|
+
// Capability detection — only what we can actually verify
|
|
127
130
|
// ---------------------------------------------------------------------------
|
|
128
131
|
|
|
129
132
|
/**
|
|
130
|
-
* Detect
|
|
131
|
-
*
|
|
133
|
+
* Detect what providers and tools are actually available.
|
|
134
|
+
* Never makes network calls, never claims to know configured plan or price.
|
|
132
135
|
*
|
|
133
|
-
* @
|
|
134
|
-
* @
|
|
136
|
+
* @param {string} [cwd]
|
|
137
|
+
* @returns {Promise<{
|
|
138
|
+
* claude: { available: boolean, source: string|null },
|
|
139
|
+
* openai: { available: boolean, source: string|null, metered: boolean },
|
|
140
|
+
* codex: { available: boolean, source: string|null },
|
|
141
|
+
* replitTools: { available: boolean, checkpoints: boolean },
|
|
142
|
+
* }>}
|
|
135
143
|
*/
|
|
136
|
-
async function
|
|
137
|
-
const
|
|
138
|
-
claude: { found: false, source: null, loginType: null },
|
|
139
|
-
openai: { found: false, source: null, loginType: null },
|
|
140
|
-
};
|
|
144
|
+
async function detectCapabilities(cwd) {
|
|
145
|
+
const root = cwd || process.cwd();
|
|
141
146
|
|
|
142
|
-
// --- Claude:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
results.claude.loginType = 'cli';
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
} catch { continue; }
|
|
147
|
+
// --- Claude: running inside Claude Code or has ANTHROPIC_API_KEY or ~/.claude dir ---
|
|
148
|
+
let claudeAvailable = false;
|
|
149
|
+
let claudeSource = null;
|
|
150
|
+
|
|
151
|
+
if (process.env.CLAUDE_CODE) {
|
|
152
|
+
claudeAvailable = true;
|
|
153
|
+
claudeSource = 'claude-code';
|
|
154
|
+
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
155
|
+
claudeAvailable = true;
|
|
156
|
+
claudeSource = 'env-key';
|
|
157
|
+
} else {
|
|
158
|
+
// Check for ~/.claude directory (Claude Code installation)
|
|
159
|
+
const claudeDir = join(homedir(), '.claude');
|
|
160
|
+
const replitClaudeDir = join(root, '.replit-tools', '.claude-persistent');
|
|
161
|
+
if (existsSync(claudeDir) || existsSync(replitClaudeDir)) {
|
|
162
|
+
claudeAvailable = true;
|
|
163
|
+
claudeSource = existsSync(replitClaudeDir) ? 'claude-code' : 'claude-dir';
|
|
164
|
+
}
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
// --- OpenAI
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
join(homedir(), '.codex', 'auth.json'),
|
|
170
|
-
];
|
|
171
|
-
for (const p of codexPaths) {
|
|
172
|
-
try {
|
|
173
|
-
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
174
|
-
const accessToken = data?.tokens?.access_token || data?.access_token;
|
|
175
|
-
const idToken = data?.tokens?.id_token || data?.id_token;
|
|
167
|
+
// --- OpenAI: check for OPENAI_API_KEY (metered billing) ---
|
|
168
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
169
|
+
const openaiAvailable = !!(openaiKey && openaiKey.length > 0);
|
|
176
170
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
// --- Codex: check if 'codex' is in PATH ---
|
|
172
|
+
let codexAvailable = false;
|
|
173
|
+
let codexSource = null;
|
|
174
|
+
try {
|
|
175
|
+
execSync('which codex', { stdio: 'pipe', timeout: 2000 });
|
|
176
|
+
codexAvailable = true;
|
|
177
|
+
codexSource = 'cli';
|
|
178
|
+
} catch {
|
|
179
|
+
// not in PATH
|
|
184
180
|
}
|
|
185
181
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
182
|
+
// --- replit-tools: check if directory exists or binary in PATH ---
|
|
183
|
+
const replitToolsDir = join(root, '.replit-tools');
|
|
184
|
+
let replitToolsAvailable = existsSync(replitToolsDir);
|
|
185
|
+
if (!replitToolsAvailable) {
|
|
186
|
+
try {
|
|
187
|
+
execSync('which replit-tools', { stdio: 'pipe', timeout: 2000 });
|
|
188
|
+
replitToolsAvailable = true;
|
|
189
|
+
} catch {
|
|
190
|
+
// not in PATH
|
|
191
|
+
}
|
|
192
|
+
}
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
* @param {{ plan: string, label?: string, expiresAt?: string }} config
|
|
197
|
-
* @param {string} [cwd]
|
|
198
|
-
*/
|
|
199
|
-
function saveSubscription(provider, config, cwd) {
|
|
200
|
-
const profile = loadProfile(cwd);
|
|
201
|
-
if (!profile.providers[provider]) profile.providers[provider] = { enabled: true };
|
|
202
|
-
profile.providers[provider].plan = config.plan;
|
|
203
|
-
profile.providers[provider].enabled = true;
|
|
204
|
-
if (config.label) profile.providers[provider].label = config.label;
|
|
205
|
-
if (config.expiresAt) profile.providers[provider].expiresAt = config.expiresAt;
|
|
206
|
-
saveProfile(profile, { cwd: cwd || process.cwd() });
|
|
207
|
-
return profile;
|
|
208
|
-
}
|
|
194
|
+
// Check for checkpoint capability (replit-specific)
|
|
195
|
+
const checkpointsBin = existsSync(join(replitToolsDir, 'checkpoints'))
|
|
196
|
+
|| existsSync('/usr/local/bin/replit-checkpoint');
|
|
209
197
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
198
|
+
return {
|
|
199
|
+
claude: {
|
|
200
|
+
available: claudeAvailable,
|
|
201
|
+
source: claudeSource,
|
|
202
|
+
},
|
|
203
|
+
openai: {
|
|
204
|
+
available: openaiAvailable,
|
|
205
|
+
source: openaiAvailable ? 'env-key' : null,
|
|
206
|
+
metered: openaiAvailable, // API key = metered billing
|
|
207
|
+
},
|
|
208
|
+
codex: {
|
|
209
|
+
available: codexAvailable,
|
|
210
|
+
source: codexSource,
|
|
211
|
+
},
|
|
212
|
+
replitTools: {
|
|
213
|
+
available: replitToolsAvailable,
|
|
214
|
+
checkpoints: checkpointsBin,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
218
217
|
}
|
|
219
218
|
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
// Auto-detect subscription plans from provider config files
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
|
|
224
219
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
220
|
+
* Return true if any metered API key is detected.
|
|
221
|
+
* When true, the system defaults to conservative API usage and should
|
|
222
|
+
* confirm before expensive operations.
|
|
223
|
+
*
|
|
224
|
+
* @param {ReturnType<typeof detectCapabilities> extends Promise<infer T> ? T : never} capabilities
|
|
225
|
+
* @returns {boolean}
|
|
228
226
|
*/
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
const parts = token.split('.');
|
|
232
|
-
if (parts.length < 2) return null;
|
|
233
|
-
// Base64url → base64 → Buffer
|
|
234
|
-
const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
235
|
-
const json = Buffer.from(b64, 'base64').toString('utf8');
|
|
236
|
-
return JSON.parse(json);
|
|
237
|
-
} catch {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
227
|
+
function needsApiGuardrail(capabilities) {
|
|
228
|
+
return !!(capabilities?.openai?.metered);
|
|
240
229
|
}
|
|
241
230
|
|
|
242
231
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* Returns nulls for any provider whose config cannot be read — never throws.
|
|
232
|
+
* Generate an honest 2-3 line onboarding/status message based on
|
|
233
|
+
* what we can actually verify.
|
|
246
234
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* labels like "Max x5" or "Pro" are our own interpretations of those signals.
|
|
235
|
+
* @param {object} capabilities — result of detectCapabilities()
|
|
236
|
+
* @param {string} [workStyle] — 'balanced' | 'cost-saver' | 'quality-first'
|
|
237
|
+
* @returns {string}
|
|
251
238
|
*/
|
|
252
|
-
function
|
|
253
|
-
const
|
|
239
|
+
function getOnboardingMessage(capabilities, workStyle = 'balanced') {
|
|
240
|
+
const found = [];
|
|
241
|
+
if (capabilities?.claude?.available) found.push('Claude Code');
|
|
242
|
+
if (capabilities?.openai?.available) found.push('OpenAI API');
|
|
243
|
+
if (capabilities?.codex?.available && !capabilities?.openai?.available) found.push('Codex CLI');
|
|
244
|
+
|
|
245
|
+
const styleLabels = {
|
|
246
|
+
'balanced': 'Balanced — smart routing, reviews on important changes',
|
|
247
|
+
'cost-saver': 'Cost-saver — prefers faster models, skips dual-brain for low-risk tasks',
|
|
248
|
+
'quality-first': 'Quality-first — dual-brain for medium+ risk, stricter reviews',
|
|
249
|
+
};
|
|
250
|
+
const modeLabel = styleLabels[workStyle] || styleLabels['balanced'];
|
|
254
251
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
join(
|
|
260
|
-
];
|
|
261
|
-
for (const p of claudePaths) {
|
|
262
|
-
try {
|
|
263
|
-
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
264
|
-
const tier = data?.oauthAccount?.organizationRateLimitTier;
|
|
265
|
-
if (tier) {
|
|
266
|
-
if (tier.includes('max_20x')) plans.claude = '$200';
|
|
267
|
-
else if (tier.includes('max_5x')) plans.claude = '$100';
|
|
268
|
-
else plans.claude = '$20';
|
|
269
|
-
}
|
|
270
|
-
break;
|
|
271
|
-
} catch { continue; }
|
|
252
|
+
const lines = [];
|
|
253
|
+
if (found.length === 0) {
|
|
254
|
+
lines.push('No providers detected');
|
|
255
|
+
lines.push(' Set ANTHROPIC_API_KEY or install Claude Code to get started');
|
|
256
|
+
return lines.join('\n');
|
|
272
257
|
}
|
|
273
258
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// Replit-tools persistent path (takes precedence)
|
|
277
|
-
'/home/runner/workspace/.replit-tools/.codex-persistent/auth.json',
|
|
278
|
-
join(homedir(), '.codex', 'auth.json'),
|
|
279
|
-
];
|
|
280
|
-
for (const p of codexPaths) {
|
|
281
|
-
try {
|
|
282
|
-
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
259
|
+
lines.push(`Found: ${found.join(', ')}`);
|
|
260
|
+
lines.push(` Mode: ${modeLabel}`);
|
|
283
261
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (!planType) {
|
|
289
|
-
for (const key of ['id_token', 'access_token']) {
|
|
290
|
-
const token = data?.tokens?.[key];
|
|
291
|
-
if (!token) continue;
|
|
292
|
-
const payload = decodeJwtPayload(token);
|
|
293
|
-
planType =
|
|
294
|
-
payload?.['https://api.openai.com/auth']?.chatgpt_plan_type ?? null;
|
|
295
|
-
if (planType) break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
262
|
+
// Tip: suggest OpenAI if only Claude is available
|
|
263
|
+
if (capabilities?.claude?.available && !capabilities?.openai?.available && !capabilities?.codex?.available) {
|
|
264
|
+
lines.push(' Tip: Add OPENAI_API_KEY for dual-brain collaboration');
|
|
265
|
+
}
|
|
298
266
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
else if (planType === 'pro' || planType === 'prolite') plans.openai = '$100';
|
|
303
|
-
else plans.openai = '$20';
|
|
304
|
-
}
|
|
305
|
-
break;
|
|
306
|
-
} catch { continue; }
|
|
267
|
+
// Warn about metered billing
|
|
268
|
+
if (capabilities?.openai?.metered) {
|
|
269
|
+
lines.push(' Note: OpenAI API key detected — usage is metered, guardrails enabled');
|
|
307
270
|
}
|
|
308
271
|
|
|
309
|
-
return
|
|
272
|
+
return lines.join('\n');
|
|
310
273
|
}
|
|
311
274
|
|
|
312
275
|
// ---------------------------------------------------------------------------
|
|
@@ -320,16 +283,18 @@ const projectPath = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'profile.j
|
|
|
320
283
|
function defaultProfile() {
|
|
321
284
|
const now = new Date().toISOString();
|
|
322
285
|
return {
|
|
323
|
-
schemaVersion:
|
|
286
|
+
schemaVersion: 2,
|
|
324
287
|
createdAt: now,
|
|
325
288
|
updatedAt: now,
|
|
289
|
+
workStyle: 'balanced',
|
|
326
290
|
providers: {
|
|
327
|
-
claude: {
|
|
328
|
-
openai: {
|
|
291
|
+
claude: { enabled: true },
|
|
292
|
+
openai: { enabled: false },
|
|
329
293
|
},
|
|
330
294
|
mode: 'auto',
|
|
331
295
|
bias: 'balanced',
|
|
332
296
|
preferences: [],
|
|
297
|
+
apiGuardrail: false,
|
|
333
298
|
};
|
|
334
299
|
}
|
|
335
300
|
|
|
@@ -342,10 +307,9 @@ function migrateProfile(profile) {
|
|
|
342
307
|
if (profile.subscriptions && !profile.providers) {
|
|
343
308
|
profile.providers = {};
|
|
344
309
|
for (const [key, sub] of Object.entries(profile.subscriptions)) {
|
|
345
|
-
profile.providers[key] = {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
};
|
|
310
|
+
profile.providers[key] = { enabled: true };
|
|
311
|
+
// Drop plan/price fields — we no longer track subscription tier
|
|
312
|
+
void sub;
|
|
349
313
|
}
|
|
350
314
|
delete profile.subscriptions;
|
|
351
315
|
}
|
|
@@ -358,8 +322,27 @@ function migrateProfile(profile) {
|
|
|
358
322
|
profile.preferences = profile.preferences || [];
|
|
359
323
|
profile.providers = profile.providers || {};
|
|
360
324
|
}
|
|
361
|
-
|
|
362
|
-
|
|
325
|
+
|
|
326
|
+
if (profile.schemaVersion < 2) {
|
|
327
|
+
// v1 → v2: remove fake subscription fields, add workStyle + apiGuardrail
|
|
328
|
+
profile.schemaVersion = 2;
|
|
329
|
+
profile.workStyle = profile.workStyle || profile.bias || 'balanced';
|
|
330
|
+
profile.apiGuardrail = profile.apiGuardrail ?? false;
|
|
331
|
+
|
|
332
|
+
// Strip price/plan/budget fields — they were never accurate
|
|
333
|
+
for (const prov of Object.values(profile.providers || {})) {
|
|
334
|
+
delete prov.plan;
|
|
335
|
+
delete prov.label;
|
|
336
|
+
delete prov.expiresAt;
|
|
337
|
+
delete prov.subs;
|
|
338
|
+
}
|
|
339
|
+
delete profile.plan;
|
|
340
|
+
delete profile.price;
|
|
341
|
+
delete profile.subscription; // doctor:verified — removing legacy field from stored config
|
|
342
|
+
delete profile.budget;
|
|
343
|
+
delete profile.detectedPlan;
|
|
344
|
+
}
|
|
345
|
+
|
|
363
346
|
return profile;
|
|
364
347
|
}
|
|
365
348
|
|
|
@@ -375,24 +358,6 @@ function loadProfile(cwd) {
|
|
|
375
358
|
}
|
|
376
359
|
}
|
|
377
360
|
if (!profile) profile = defaultProfile();
|
|
378
|
-
|
|
379
|
-
// Read plan tier from auth config files (JWT or organizationRateLimitTier) and
|
|
380
|
-
// apply if it differs from the stored profile value.
|
|
381
|
-
// NOTE: detectPlans() reads rate-limit tier data from the auth config — it infers
|
|
382
|
-
// a price tier ($20/$100/$200) from that signal, not from the subscription name itself.
|
|
383
|
-
// The plan label (e.g. "Max x5") comes from our own mapping, not from Claude/OpenAI.
|
|
384
|
-
const detected = detectPlans();
|
|
385
|
-
for (const [provider, detectedPlan] of Object.entries(detected)) {
|
|
386
|
-
if (!detectedPlan) continue;
|
|
387
|
-
if (!profile.providers[provider]) continue;
|
|
388
|
-
const stored = profile.providers[provider].plan;
|
|
389
|
-
if (stored !== detectedPlan) {
|
|
390
|
-
const providerName = provider === 'claude' ? 'Claude' : 'OpenAI';
|
|
391
|
-
process.stderr.write(`[dual-brain] ${providerName}: plan updated to ${detectedPlan} (from auth config)\n`);
|
|
392
|
-
profile.providers[provider].plan = detectedPlan;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
361
|
return profile;
|
|
397
362
|
}
|
|
398
363
|
|
|
@@ -424,20 +389,37 @@ async function runOnboarding(opts = {}) {
|
|
|
424
389
|
try {
|
|
425
390
|
process.stdout.write('\nDual-Brain Orchestrator — First-time setup\n\n');
|
|
426
391
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
392
|
+
// Detect what's actually available
|
|
393
|
+
const capabilities = await detectCapabilities(opts.cwd);
|
|
394
|
+
|
|
395
|
+
// Show what we found honestly
|
|
396
|
+
const foundProviders = [];
|
|
397
|
+
if (capabilities.claude.available) foundProviders.push('Claude Code');
|
|
398
|
+
if (capabilities.openai.available) foundProviders.push('OpenAI API (metered)');
|
|
399
|
+
if (capabilities.codex.available && !capabilities.openai.available) foundProviders.push('Codex CLI');
|
|
430
400
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
401
|
+
if (foundProviders.length > 0) {
|
|
402
|
+
process.stdout.write(`Detected: ${foundProviders.join(', ')}\n\n`);
|
|
403
|
+
} else {
|
|
404
|
+
process.stdout.write('No providers detected automatically.\n\n');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Enable providers based on what's available
|
|
408
|
+
profile.providers.claude.enabled = capabilities.claude.available;
|
|
409
|
+
profile.providers.openai.enabled = capabilities.openai.available || capabilities.codex.available;
|
|
410
|
+
profile.apiGuardrail = needsApiGuardrail(capabilities);
|
|
411
|
+
|
|
412
|
+
// If detection missed something, ask
|
|
413
|
+
if (!capabilities.claude.available && !capabilities.openai.available && !capabilities.codex.available) {
|
|
414
|
+
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();
|
|
415
|
+
if (q1 === '1') { profile.providers.claude.enabled = true; }
|
|
416
|
+
else if (q1 === '2') { profile.providers.claude.enabled = false; profile.providers.openai.enabled = true; profile.apiGuardrail = true; }
|
|
417
|
+
else if (q1 === '3') { profile.providers.claude.enabled = true; profile.providers.openai.enabled = true; profile.apiGuardrail = true; }
|
|
437
418
|
}
|
|
438
419
|
|
|
439
|
-
const q3 = (await ask('\nDefault
|
|
420
|
+
const q3 = (await ask('\nDefault work style?\n (1) Save usage (2) Balanced (3) Best quality\n> ')).trim();
|
|
440
421
|
profile.bias = ({ '1': 'cost-saver', '3': 'quality-first' })[q3] || 'balanced';
|
|
422
|
+
profile.workStyle = profile.bias;
|
|
441
423
|
|
|
442
424
|
const n = Object.values(profile.providers).filter(p => p.enabled).length;
|
|
443
425
|
profile.mode = n >= 2 ? 'dual' : profile.providers.claude.enabled ? 'solo-claude' : 'solo-openai';
|
|
@@ -507,12 +489,10 @@ function getActivePreferences(cwd) {
|
|
|
507
489
|
// Provider helpers
|
|
508
490
|
// ---------------------------------------------------------------------------
|
|
509
491
|
|
|
510
|
-
const PLAN_RANK = { '$20': 1, '$100': 2, '$200': 3 };
|
|
511
|
-
|
|
512
492
|
function getAvailableProviders(profile) {
|
|
513
493
|
return Object.entries(profile.providers || {})
|
|
514
494
|
.filter(([, p]) => p.enabled)
|
|
515
|
-
.map(([name, p]) => ({ name,
|
|
495
|
+
.map(([name, p]) => ({ name, ...p }));
|
|
516
496
|
}
|
|
517
497
|
|
|
518
498
|
function isSoloBrain(profile) {
|
|
@@ -523,85 +503,28 @@ function getHeadModel(profile) {
|
|
|
523
503
|
const providers = getAvailableProviders(profile);
|
|
524
504
|
if (providers.length === 0) return 'sonnet';
|
|
525
505
|
if (providers.length === 1) return providers[0].name === 'openai' ? 'gpt-4o' : 'sonnet';
|
|
526
|
-
|
|
527
|
-
return
|
|
506
|
+
// Both available — default to Claude (we're running in Claude Code)
|
|
507
|
+
return 'sonnet';
|
|
528
508
|
}
|
|
529
509
|
|
|
530
510
|
// ---------------------------------------------------------------------------
|
|
531
|
-
//
|
|
532
|
-
// ---------------------------------------------------------------------------
|
|
533
|
-
|
|
534
|
-
async function main() {
|
|
535
|
-
const args = process.argv.slice(2);
|
|
536
|
-
const cwd = process.cwd();
|
|
537
|
-
const flag = args[0];
|
|
538
|
-
const val = args[1];
|
|
539
|
-
|
|
540
|
-
if (flag === '--init') {
|
|
541
|
-
const profile = await runOnboarding({ interactive: true });
|
|
542
|
-
saveProfile(profile, { cwd });
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
if (flag === '--remember') {
|
|
546
|
-
if (!val) { process.stderr.write('Usage: --remember "text"\n'); process.exit(1); }
|
|
547
|
-
const p = rememberPreference(val, { cwd });
|
|
548
|
-
process.stdout.write(`Preference saved. Total: ${p.preferences.length}\n`);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
if (flag === '--forget') {
|
|
552
|
-
if (!val) { process.stderr.write('Usage: --forget "text"\n'); process.exit(1); }
|
|
553
|
-
forgetPreference(val, cwd);
|
|
554
|
-
process.stdout.write('Preference removed (if matched).\n');
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (flag === '--providers') {
|
|
558
|
-
const providers = getAvailableProviders(loadProfile(cwd));
|
|
559
|
-
if (!providers.length) { process.stdout.write('No providers enabled.\n'); return; }
|
|
560
|
-
providers.forEach(p => process.stdout.write(`${p.name} plan=${p.plan}\n`));
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// default: show profile
|
|
565
|
-
const profile = loadProfile(cwd);
|
|
566
|
-
const providers = getAvailableProviders(profile);
|
|
567
|
-
[
|
|
568
|
-
`mode : ${profile.mode}`,
|
|
569
|
-
`bias : ${profile.bias}`,
|
|
570
|
-
`head model : ${getHeadModel(profile)}`,
|
|
571
|
-
`providers : ${providers.map(p => `${p.name} (${p.plan})`).join(', ') || 'none'}`,
|
|
572
|
-
`prefs : ${profile.preferences?.filter(p => p.enabled).length || 0} active`,
|
|
573
|
-
].forEach(l => process.stdout.write(l + '\n'));
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const isMain = process.argv[1]?.endsWith('profile.mjs');
|
|
577
|
-
if (isMain) main().catch(e => { process.stderr.write(e.message + '\n'); process.exit(1); });
|
|
578
|
-
|
|
579
|
-
// ---------------------------------------------------------------------------
|
|
580
|
-
// Exports
|
|
581
|
-
// ---------------------------------------------------------------------------
|
|
582
|
-
|
|
583
|
-
// ---------------------------------------------------------------------------
|
|
584
|
-
// Auto-setup (1-click, no user input required)
|
|
511
|
+
// Capability-based auto-setup (replaces subscription-based autoSetup)
|
|
585
512
|
// ---------------------------------------------------------------------------
|
|
586
513
|
|
|
587
514
|
/**
|
|
588
|
-
*
|
|
515
|
+
* Silently configure a profile from detected capabilities — no user input.
|
|
589
516
|
*
|
|
590
517
|
* Returns:
|
|
591
518
|
* {
|
|
592
519
|
* confident: boolean, // true when at least one provider was found
|
|
593
520
|
* profile: object|null, // fully-built profile ready to save, or null
|
|
594
|
-
* warnings: string[], // non-fatal issues
|
|
521
|
+
* warnings: string[], // non-fatal issues
|
|
595
522
|
* actions: string[], // human-readable lines for the summary box
|
|
596
523
|
* }
|
|
597
|
-
*
|
|
598
|
-
* IMPORTANT: this function NEVER stores credentials — it only reads what's
|
|
599
|
-
* already present on disk / in environment variables.
|
|
600
524
|
*/
|
|
601
525
|
async function autoSetup(cwd) {
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
const plans = detectPlans();
|
|
526
|
+
const capabilities = await detectCapabilities(cwd);
|
|
527
|
+
const env = detectEnvironment();
|
|
605
528
|
|
|
606
529
|
const result = {
|
|
607
530
|
confident: false,
|
|
@@ -610,47 +533,45 @@ async function autoSetup(cwd) {
|
|
|
610
533
|
actions: [],
|
|
611
534
|
};
|
|
612
535
|
|
|
613
|
-
// Need at least one provider
|
|
614
|
-
if (!
|
|
536
|
+
// Need at least one provider
|
|
537
|
+
if (!capabilities.claude.available && !capabilities.openai.available && !capabilities.codex.available) {
|
|
615
538
|
result.warnings.push('No provider credentials found');
|
|
616
539
|
return result;
|
|
617
540
|
}
|
|
618
541
|
|
|
619
|
-
// Build profile from detected state
|
|
620
542
|
const profile = defaultProfile();
|
|
621
543
|
|
|
622
544
|
// Claude
|
|
623
|
-
if (
|
|
545
|
+
if (capabilities.claude.available) {
|
|
624
546
|
profile.providers.claude.enabled = true;
|
|
625
|
-
|
|
626
|
-
// Plan tier is inferred from auth config signal — show tier with "configured",
|
|
627
|
-
// not a plan name we didn't actually detect.
|
|
628
|
-
const claudeTierLabel = plans.claude ? `${plans.claude} configured` : 'connected';
|
|
629
|
-
result.actions.push(`Claude: ${claudeTierLabel} (${auth.claude.source})`);
|
|
547
|
+
result.actions.push(`Claude: available (${capabilities.claude.source})`);
|
|
630
548
|
} else {
|
|
631
549
|
profile.providers.claude.enabled = false;
|
|
632
|
-
result.warnings.push('Claude
|
|
550
|
+
result.warnings.push('Claude not detected — install Claude Code or set ANTHROPIC_API_KEY');
|
|
633
551
|
}
|
|
634
552
|
|
|
635
|
-
// OpenAI
|
|
636
|
-
if (
|
|
553
|
+
// OpenAI / Codex
|
|
554
|
+
if (capabilities.openai.available) {
|
|
555
|
+
profile.providers.openai.enabled = true;
|
|
556
|
+
result.actions.push('OpenAI: API key detected (metered billing — guardrails enabled)');
|
|
557
|
+
} else if (capabilities.codex.available) {
|
|
637
558
|
profile.providers.openai.enabled = true;
|
|
638
|
-
|
|
639
|
-
// Plan tier is inferred from JWT claim in auth config — show tier with "configured",
|
|
640
|
-
// not a plan name we didn't actually detect.
|
|
641
|
-
const openaiTierLabel = plans.openai ? `${plans.openai} configured` : 'connected';
|
|
642
|
-
result.actions.push(`OpenAI: ${openaiTierLabel} (${auth.openai.source})`);
|
|
559
|
+
result.actions.push('Codex CLI: available');
|
|
643
560
|
} else {
|
|
644
561
|
profile.providers.openai.enabled = false;
|
|
645
|
-
result.warnings.push('
|
|
562
|
+
result.warnings.push('OpenAI not detected — add OPENAI_API_KEY or install Codex CLI');
|
|
646
563
|
}
|
|
647
564
|
|
|
648
565
|
// Mode
|
|
649
|
-
const enabledCount =
|
|
566
|
+
const enabledCount = Object.values(profile.providers).filter(p => p.enabled).length;
|
|
650
567
|
profile.mode = enabledCount >= 2 ? 'dual'
|
|
651
|
-
:
|
|
568
|
+
: profile.providers.claude.enabled ? 'solo-claude'
|
|
652
569
|
: 'solo-openai';
|
|
653
570
|
profile.bias = 'balanced';
|
|
571
|
+
profile.workStyle = 'balanced';
|
|
572
|
+
profile.apiGuardrail = needsApiGuardrail(capabilities);
|
|
573
|
+
profile.capabilities = capabilities;
|
|
574
|
+
profile.detectedAt = new Date().toISOString();
|
|
654
575
|
|
|
655
576
|
// Environment note
|
|
656
577
|
if (env.isReplit && env.hasReplitTools) {
|
|
@@ -665,13 +586,11 @@ async function autoSetup(cwd) {
|
|
|
665
586
|
}
|
|
666
587
|
|
|
667
588
|
// ---------------------------------------------------------------------------
|
|
668
|
-
// OAuth token auto-refresh
|
|
589
|
+
// OAuth token auto-refresh (unchanged — token refresh is still valid)
|
|
669
590
|
// ---------------------------------------------------------------------------
|
|
670
591
|
|
|
671
592
|
/**
|
|
672
593
|
* Silently refresh the Claude OAuth token before it expires.
|
|
673
|
-
* Mirrors the approach used by replit-tools/data-tools claude-auth-refresh.sh,
|
|
674
|
-
* but implemented in JavaScript.
|
|
675
594
|
*
|
|
676
595
|
* Returns one of:
|
|
677
596
|
* { status: 'valid', hoursRemaining }
|
|
@@ -751,240 +670,193 @@ async function autoRefreshToken(cwd) {
|
|
|
751
670
|
}
|
|
752
671
|
|
|
753
672
|
// ---------------------------------------------------------------------------
|
|
754
|
-
//
|
|
673
|
+
// detectAuth — kept for backward compat, now delegates to detectCapabilities
|
|
755
674
|
// ---------------------------------------------------------------------------
|
|
756
675
|
|
|
757
676
|
/**
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
* @param {string} cmd
|
|
761
|
-
* @param {string[]} args
|
|
762
|
-
* @param {number} timeoutMs
|
|
763
|
-
* @returns {Promise<string|null>}
|
|
764
|
-
*/
|
|
765
|
-
function _runWithTimeout(cmd, args, timeoutMs) {
|
|
766
|
-
return new Promise(resolve => {
|
|
767
|
-
let settled = false;
|
|
768
|
-
const done = (val) => { if (!settled) { settled = true; resolve(val); } };
|
|
769
|
-
|
|
770
|
-
let child;
|
|
771
|
-
try {
|
|
772
|
-
child = execFile(cmd, args, { timeout: timeoutMs, windowsHide: true }, (err, stdout) => {
|
|
773
|
-
done(err ? null : (stdout || '').trim());
|
|
774
|
-
});
|
|
775
|
-
} catch {
|
|
776
|
-
done(null);
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Belt-and-suspenders timeout fallback
|
|
781
|
-
const timer = setTimeout(() => {
|
|
782
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
783
|
-
done(null);
|
|
784
|
-
}, timeoutMs + 500);
|
|
785
|
-
|
|
786
|
-
if (child?.on) {
|
|
787
|
-
child.on('close', () => clearTimeout(timer));
|
|
788
|
-
}
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* Derive a human-readable plan label from a plan tier string.
|
|
794
|
-
* @param {'claude'|'openai'} provider
|
|
795
|
-
* @param {string} plan e.g. '$20' | '$100' | '$200'
|
|
796
|
-
*/
|
|
797
|
-
function _planLabel(provider, plan) {
|
|
798
|
-
const labels = {
|
|
799
|
-
claude: { '$20': 'Claude Pro ($20)', '$100': 'Claude Max x5 ($100)', '$200': 'Claude Max x20 ($200)' },
|
|
800
|
-
openai: { '$20': 'ChatGPT Plus ($20)', '$100': 'ChatGPT Pro ($100)', '$200': 'ChatGPT Pro ($200)' },
|
|
801
|
-
};
|
|
802
|
-
return labels[provider]?.[plan] ?? `${provider} ${plan}`;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Silently scan for existing auth from all known sources and return what was
|
|
807
|
-
* found, together with smart setup recommendations.
|
|
808
|
-
*
|
|
809
|
-
* Checks (in order, all non-throwing):
|
|
810
|
-
* 1. data-tools / replit-tools — ~/.claude/credentials.json or
|
|
811
|
-
* .replit-tools/.claude-persistent/.credentials.json for a session key
|
|
812
|
-
* 2. Claude CLI — `claude auth status` with 3 s timeout
|
|
813
|
-
* 3. Codex CLI — `codex auth status` with 3 s timeout or
|
|
814
|
-
* ~/.codex/ config files
|
|
815
|
-
* 4. Existing dual-brain config — .dualbrain/profile.json
|
|
816
|
-
*
|
|
817
|
-
* Returns:
|
|
818
|
-
* {
|
|
819
|
-
* claude: { found: boolean, source: string|null, plan: string|null, expiresAt: string|null },
|
|
820
|
-
* openai: { found: boolean, source: string|null, plan: string|null },
|
|
821
|
-
* existingProfile: boolean,
|
|
822
|
-
* recommendations: { headModel: string, budget: string, profile: string },
|
|
823
|
-
* }
|
|
677
|
+
* Detect CLI login status for Claude and Codex.
|
|
678
|
+
* Checks config files on disk — never makes network calls.
|
|
824
679
|
*
|
|
825
|
-
* @
|
|
680
|
+
* @returns {{ claude: AuthEntry, openai: AuthEntry }}
|
|
681
|
+
* @typedef {{ found: boolean, source: string|null, loginType: 'oauth'|'cli'|null }} AuthEntry
|
|
826
682
|
*/
|
|
827
|
-
async function
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
// -------------------------------------------------------------------------
|
|
832
|
-
// Result skeleton
|
|
833
|
-
// -------------------------------------------------------------------------
|
|
834
|
-
const result = {
|
|
835
|
-
claude: { found: false, source: null, plan: null, expiresAt: null },
|
|
836
|
-
openai: { found: false, source: null, plan: null },
|
|
837
|
-
existingProfile: false,
|
|
838
|
-
recommendations: { headModel: 'claude-sonnet-4-6', budget: '$20', profile: 'balanced' },
|
|
683
|
+
async function detectAuth() {
|
|
684
|
+
const results = {
|
|
685
|
+
claude: { found: false, source: null, loginType: null },
|
|
686
|
+
openai: { found: false, source: null, loginType: null },
|
|
839
687
|
};
|
|
840
688
|
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
join(root, '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
846
|
-
join(home, '.claude', '.credentials.json'),
|
|
847
|
-
// legacy replit persistent path
|
|
848
|
-
'/home/runner/workspace/.replit-tools/.claude-persistent/.credentials.json',
|
|
689
|
+
// --- Claude: check .claude.json for oauthAccount (CLI login) ---
|
|
690
|
+
const claudePaths = [
|
|
691
|
+
'/home/runner/workspace/.replit-tools/.claude-persistent/.claude.json',
|
|
692
|
+
join(homedir(), '.claude', '.claude.json'),
|
|
849
693
|
];
|
|
850
|
-
for (const
|
|
694
|
+
for (const p of claudePaths) {
|
|
851
695
|
try {
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
// Expiry
|
|
858
|
-
if (oauth.expiresAt) {
|
|
859
|
-
try { result.claude.expiresAt = new Date(oauth.expiresAt).toISOString(); } catch {}
|
|
860
|
-
}
|
|
696
|
+
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
697
|
+
if (data?.oauthAccount) {
|
|
698
|
+
results.claude.found = true;
|
|
699
|
+
results.claude.source = p.includes('.replit-tools') ? 'claude CLI (replit-tools)' : 'claude CLI';
|
|
700
|
+
results.claude.loginType = 'oauth';
|
|
861
701
|
break;
|
|
862
702
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
// -------------------------------------------------------------------------
|
|
869
|
-
if (!result.claude.found) {
|
|
870
|
-
// Config-file scan (same paths as detectAuth)
|
|
871
|
-
const claudeConfigPaths = [
|
|
872
|
-
join(root, '.replit-tools', '.claude-persistent', '.claude.json'),
|
|
873
|
-
'/home/runner/workspace/.replit-tools/.claude-persistent/.claude.json',
|
|
874
|
-
join(home, '.claude', '.claude.json'),
|
|
875
|
-
];
|
|
876
|
-
for (const p of claudeConfigPaths) {
|
|
877
|
-
try {
|
|
878
|
-
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
879
|
-
if (data?.oauthAccount || (data?.apiKey && typeof data.apiKey === 'string')) {
|
|
880
|
-
result.claude.found = true;
|
|
881
|
-
result.claude.source = p.includes('.replit-tools') ? 'claude CLI (replit-tools)' : 'claude CLI';
|
|
882
|
-
break;
|
|
883
|
-
}
|
|
884
|
-
} catch { /* non-fatal */ }
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// CLI fallback: `claude auth status`
|
|
888
|
-
if (!result.claude.found) {
|
|
889
|
-
const out = await _runWithTimeout('claude', ['auth', 'status'], 3000);
|
|
890
|
-
if (out && /logged.in|authenticated|signed.in/i.test(out)) {
|
|
891
|
-
result.claude.found = true;
|
|
892
|
-
result.claude.source = 'claude CLI (auth status)';
|
|
703
|
+
if (data?.apiKey && typeof data.apiKey === 'string') {
|
|
704
|
+
results.claude.found = true;
|
|
705
|
+
results.claude.source = p.includes('.replit-tools') ? 'claude CLI (replit-tools)' : 'claude CLI';
|
|
706
|
+
results.claude.loginType = 'cli';
|
|
707
|
+
break;
|
|
893
708
|
}
|
|
894
|
-
}
|
|
709
|
+
} catch { continue; }
|
|
895
710
|
}
|
|
896
711
|
|
|
897
|
-
//
|
|
898
|
-
|
|
899
|
-
// -------------------------------------------------------------------------
|
|
900
|
-
const codexConfigPaths = [
|
|
901
|
-
join(root, '.replit-tools', '.codex-persistent', 'auth.json'),
|
|
712
|
+
// --- OpenAI/Codex: check auth.json for access_token or id_token (CLI login) ---
|
|
713
|
+
const codexPaths = [
|
|
902
714
|
'/home/runner/workspace/.replit-tools/.codex-persistent/auth.json',
|
|
903
|
-
join(
|
|
715
|
+
join(homedir(), '.codex', 'auth.json'),
|
|
904
716
|
];
|
|
905
|
-
for (const p of
|
|
717
|
+
for (const p of codexPaths) {
|
|
906
718
|
try {
|
|
907
|
-
const data
|
|
719
|
+
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
908
720
|
const accessToken = data?.tokens?.access_token || data?.access_token;
|
|
909
721
|
const idToken = data?.tokens?.id_token || data?.id_token;
|
|
722
|
+
|
|
910
723
|
if (accessToken || idToken) {
|
|
911
|
-
|
|
912
|
-
|
|
724
|
+
results.openai.found = true;
|
|
725
|
+
results.openai.source = p.includes('.replit-tools') ? 'codex CLI (replit-tools)' : 'codex CLI';
|
|
726
|
+
results.openai.loginType = 'oauth';
|
|
913
727
|
break;
|
|
914
728
|
}
|
|
915
|
-
} catch {
|
|
729
|
+
} catch { continue; }
|
|
916
730
|
}
|
|
917
731
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const out = await _runWithTimeout('codex', ['auth', 'status'], 3000);
|
|
921
|
-
if (out && /logged.in|authenticated|signed.in/i.test(out)) {
|
|
922
|
-
result.openai.found = true;
|
|
923
|
-
result.openai.source = 'codex CLI (auth status)';
|
|
924
|
-
}
|
|
925
|
-
}
|
|
732
|
+
return results;
|
|
733
|
+
}
|
|
926
734
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
break;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
// Removed: detectExistingAuth, detectPlans, decodeJwtPayload, saveSubscription,
|
|
737
|
+
// listSubscriptions, _planLabel, _runWithTimeout
|
|
738
|
+
// These claimed to detect subscription tier/price from auth files — that was
|
|
739
|
+
// never accurate. Use detectCapabilities() instead for honest detection.
|
|
740
|
+
// ---------------------------------------------------------------------------
|
|
936
741
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
742
|
+
// Thin stubs retained only so any callers that weren't updated yet
|
|
743
|
+
// fail gracefully with a clear message rather than a crash.
|
|
744
|
+
|
|
745
|
+
/** @deprecated Use detectCapabilities() instead. */
|
|
746
|
+
async function detectExistingAuth(cwd) {
|
|
747
|
+
const caps = await detectCapabilities(cwd);
|
|
748
|
+
return {
|
|
749
|
+
claude: {
|
|
750
|
+
found: caps.claude.available,
|
|
751
|
+
source: caps.claude.source,
|
|
752
|
+
plan: null, // not detectable
|
|
753
|
+
expiresAt: null,
|
|
754
|
+
},
|
|
755
|
+
openai: {
|
|
756
|
+
found: caps.openai.available || caps.codex.available,
|
|
757
|
+
source: caps.openai.source || caps.codex.source,
|
|
758
|
+
plan: null, // not detectable
|
|
759
|
+
},
|
|
760
|
+
existingProfile: [projectPath(cwd), GLOBAL_PATH].some(p => existsSync(p)),
|
|
761
|
+
recommendations: {
|
|
762
|
+
headModel: caps.claude.available ? 'claude-sonnet-4-6' : 'gpt-4o',
|
|
763
|
+
// budget field removed — we don't track subscription price
|
|
764
|
+
profile: 'balanced',
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** @deprecated Price-based plan tiers removed. Returns null for all providers. */
|
|
770
|
+
function detectPlans() {
|
|
771
|
+
return { claude: null, openai: null };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** @deprecated Plan tracking removed. Use provider enabled flag instead. */
|
|
775
|
+
function saveSubscription(provider, config, cwd) {
|
|
776
|
+
const profile = loadProfile(cwd);
|
|
777
|
+
if (!profile.providers[provider]) profile.providers[provider] = { enabled: true };
|
|
778
|
+
profile.providers[provider].enabled = true;
|
|
779
|
+
saveProfile(profile, { cwd: cwd || process.cwd() });
|
|
780
|
+
return profile;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** @deprecated Plan tracking removed. Use getAvailableProviders() instead. */
|
|
784
|
+
function listSubscriptions(cwd) {
|
|
785
|
+
const profile = loadProfile(cwd);
|
|
786
|
+
return profile.providers || {};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ---------------------------------------------------------------------------
|
|
790
|
+
// CLI
|
|
791
|
+
// ---------------------------------------------------------------------------
|
|
792
|
+
|
|
793
|
+
async function main() {
|
|
794
|
+
const args = process.argv.slice(2);
|
|
795
|
+
const cwd = process.cwd();
|
|
796
|
+
const flag = args[0];
|
|
797
|
+
const val = args[1];
|
|
798
|
+
|
|
799
|
+
if (flag === '--init') {
|
|
800
|
+
const profile = await runOnboarding({ interactive: true, cwd });
|
|
801
|
+
saveProfile(profile, { cwd });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (flag === '--remember') {
|
|
805
|
+
if (!val) { process.stderr.write('Usage: --remember "text"\n'); process.exit(1); }
|
|
806
|
+
const p = rememberPreference(val, { cwd });
|
|
807
|
+
process.stdout.write(`Preference saved. Total: ${p.preferences.length}\n`);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (flag === '--forget') {
|
|
811
|
+
if (!val) { process.stderr.write('Usage: --forget "text"\n'); process.exit(1); }
|
|
812
|
+
forgetPreference(val, cwd);
|
|
813
|
+
process.stdout.write('Preference removed (if matched).\n');
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (flag === '--providers') {
|
|
817
|
+
const providers = getAvailableProviders(loadProfile(cwd));
|
|
818
|
+
if (!providers.length) { process.stdout.write('No providers enabled.\n'); return; }
|
|
819
|
+
providers.forEach(p => process.stdout.write(`${p.name} enabled=${p.enabled}\n`));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (flag === '--capabilities') {
|
|
823
|
+
const caps = await detectCapabilities(cwd);
|
|
824
|
+
process.stdout.write(JSON.stringify(caps, null, 2) + '\n');
|
|
825
|
+
return;
|
|
975
826
|
}
|
|
976
|
-
// else: no auth found — defaults remain (claude-sonnet-4-6 / $20 / balanced)
|
|
977
827
|
|
|
978
|
-
|
|
828
|
+
// default: show profile
|
|
829
|
+
const profile = loadProfile(cwd);
|
|
830
|
+
const providers = getAvailableProviders(profile);
|
|
831
|
+
const caps = await detectCapabilities(cwd);
|
|
832
|
+
[
|
|
833
|
+
`mode : ${profile.mode}`,
|
|
834
|
+
`workStyle : ${profile.workStyle || profile.bias}`,
|
|
835
|
+
`head model : ${getHeadModel(profile)}`,
|
|
836
|
+
`providers : ${providers.map(p => p.name).join(', ') || 'none'}`,
|
|
837
|
+
`prefs : ${profile.preferences?.filter(p => p.enabled).length || 0} active`,
|
|
838
|
+
`guardrail : ${needsApiGuardrail(caps) ? 'enabled (metered API key detected)' : 'off'}`,
|
|
839
|
+
'',
|
|
840
|
+
getOnboardingMessage(caps, profile.workStyle || profile.bias),
|
|
841
|
+
].forEach(l => process.stdout.write(l + '\n'));
|
|
979
842
|
}
|
|
980
843
|
|
|
844
|
+
const isMain = process.argv[1]?.endsWith('profile.mjs');
|
|
845
|
+
if (isMain) main().catch(e => { process.stderr.write(e.message + '\n'); process.exit(1); });
|
|
846
|
+
|
|
847
|
+
// ---------------------------------------------------------------------------
|
|
848
|
+
// Exports
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
|
|
981
851
|
export {
|
|
982
852
|
loadProfile, saveProfile, ensureProfile, runOnboarding,
|
|
983
853
|
rememberPreference, forgetPreference, getActivePreferences,
|
|
984
854
|
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
985
|
-
|
|
855
|
+
detectCapabilities, getOnboardingMessage, needsApiGuardrail,
|
|
856
|
+
syncPreferencesToMemory,
|
|
986
857
|
detectAuth, detectEnvironment,
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
detectExistingAuth,
|
|
858
|
+
autoSetup, autoRefreshToken,
|
|
859
|
+
// backward-compat stubs (deprecated)
|
|
860
|
+
detectExistingAuth, detectPlans, saveSubscription, listSubscriptions,
|
|
861
|
+
defaultProfile,
|
|
990
862
|
};
|