dual-brain 0.2.3 → 0.2.4
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 +655 -425
- package/package.json +1 -1
- package/src/profile.mjs +259 -10
package/package.json
CHANGED
package/src/profile.mjs
CHANGED
|
@@ -151,9 +151,9 @@ async function detectCapabilities(cwd) {
|
|
|
151
151
|
if (process.env.CLAUDE_CODE) {
|
|
152
152
|
claudeAvailable = true;
|
|
153
153
|
claudeSource = 'claude-code';
|
|
154
|
-
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
154
|
+
} else if (process.env.ANTHROPIC_API_KEY?.length > 0) {
|
|
155
155
|
claudeAvailable = true;
|
|
156
|
-
claudeSource = '
|
|
156
|
+
claudeSource = 'api-key';
|
|
157
157
|
} else {
|
|
158
158
|
// Check for ~/.claude directory (Claude Code installation)
|
|
159
159
|
const claudeDir = join(homedir(), '.claude');
|
|
@@ -164,9 +164,8 @@ async function detectCapabilities(cwd) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// --- OpenAI: check for OPENAI_API_KEY (metered billing) ---
|
|
168
|
-
const
|
|
169
|
-
const openaiAvailable = !!(openaiKey && openaiKey.length > 0);
|
|
167
|
+
// --- OpenAI: check for OPENAI_API_KEY presence (metered billing) ---
|
|
168
|
+
const openaiAvailable = !!process.env.OPENAI_API_KEY?.length;
|
|
170
169
|
|
|
171
170
|
// --- Codex: check if 'codex' is in PATH ---
|
|
172
171
|
let codexAvailable = false;
|
|
@@ -201,9 +200,9 @@ async function detectCapabilities(cwd) {
|
|
|
201
200
|
source: claudeSource,
|
|
202
201
|
},
|
|
203
202
|
openai: {
|
|
204
|
-
available: openaiAvailable,
|
|
205
|
-
source: openaiAvailable ? '
|
|
206
|
-
metered: openaiAvailable
|
|
203
|
+
available: openaiAvailable || codexAvailable,
|
|
204
|
+
source: openaiAvailable ? 'api-key' : codexAvailable ? 'codex-cli' : null,
|
|
205
|
+
metered: openaiAvailable && !codexAvailable,
|
|
207
206
|
},
|
|
208
207
|
codex: {
|
|
209
208
|
available: codexAvailable,
|
|
@@ -252,7 +251,7 @@ function getOnboardingMessage(capabilities, workStyle = 'balanced') {
|
|
|
252
251
|
const lines = [];
|
|
253
252
|
if (found.length === 0) {
|
|
254
253
|
lines.push('No providers detected');
|
|
255
|
-
lines.push('
|
|
254
|
+
lines.push(' Run: claude login or install Claude Code to get started');
|
|
256
255
|
return lines.join('\n');
|
|
257
256
|
}
|
|
258
257
|
|
|
@@ -261,7 +260,7 @@ function getOnboardingMessage(capabilities, workStyle = 'balanced') {
|
|
|
261
260
|
|
|
262
261
|
// Tip: suggest OpenAI if only Claude is available
|
|
263
262
|
if (capabilities?.claude?.available && !capabilities?.openai?.available && !capabilities?.codex?.available) {
|
|
264
|
-
lines.push(' Tip:
|
|
263
|
+
lines.push(' Tip: Run codex login for dual-brain collaboration');
|
|
265
264
|
}
|
|
266
265
|
|
|
267
266
|
// Warn about metered billing
|
|
@@ -814,6 +813,223 @@ function listSubscriptions(cwd) {
|
|
|
814
813
|
return profile.providers || {};
|
|
815
814
|
}
|
|
816
815
|
|
|
816
|
+
// ---------------------------------------------------------------------------
|
|
817
|
+
// Credential Registry
|
|
818
|
+
// ---------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
const credentialsPath = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'credentials.json');
|
|
821
|
+
|
|
822
|
+
function defaultCredentials() {
|
|
823
|
+
return { version: 1, credentials: [] };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function loadCredentials(cwd = process.cwd()) {
|
|
827
|
+
try {
|
|
828
|
+
const p = credentialsPath(cwd);
|
|
829
|
+
if (!existsSync(p)) return defaultCredentials();
|
|
830
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
831
|
+
} catch {
|
|
832
|
+
return defaultCredentials();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export function saveCredentials(data, cwd = process.cwd()) {
|
|
837
|
+
try {
|
|
838
|
+
const p = credentialsPath(cwd);
|
|
839
|
+
const dir = p.slice(0, p.lastIndexOf('/'));
|
|
840
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
841
|
+
// Ensure no raw secret values are stored
|
|
842
|
+
const safe = {
|
|
843
|
+
...data,
|
|
844
|
+
credentials: (data.credentials || []).map(c => {
|
|
845
|
+
const clean = { ...c };
|
|
846
|
+
delete clean.secret;
|
|
847
|
+
delete clean.token;
|
|
848
|
+
delete clean.api_key;
|
|
849
|
+
delete clean.password;
|
|
850
|
+
return clean;
|
|
851
|
+
}),
|
|
852
|
+
};
|
|
853
|
+
const tmp = p + '.tmp.' + process.pid;
|
|
854
|
+
writeFileSync(tmp, JSON.stringify(safe, null, 2) + '\n');
|
|
855
|
+
renameSync(tmp, p);
|
|
856
|
+
return p;
|
|
857
|
+
} catch { /* non-fatal */ }
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export function addCredential(cred, cwd = process.cwd()) {
|
|
861
|
+
const required = ['id', 'provider', 'auth_type', 'source'];
|
|
862
|
+
for (const f of required) {
|
|
863
|
+
if (!cred[f]) throw new Error(`addCredential: missing required field '${f}'`);
|
|
864
|
+
}
|
|
865
|
+
const data = loadCredentials(cwd);
|
|
866
|
+
const idx = data.credentials.findIndex(c => c.id === cred.id);
|
|
867
|
+
const entry = {
|
|
868
|
+
id: cred.id,
|
|
869
|
+
provider: cred.provider,
|
|
870
|
+
auth_type: cred.auth_type,
|
|
871
|
+
source: cred.source,
|
|
872
|
+
owner: cred.owner || 'user',
|
|
873
|
+
scope: cred.scope || 'local',
|
|
874
|
+
plan_hint: cred.plan_hint || null,
|
|
875
|
+
enabled: cred.enabled !== false,
|
|
876
|
+
health: cred.health || 'unknown',
|
|
877
|
+
last_checked_at: cred.last_checked_at || null,
|
|
878
|
+
};
|
|
879
|
+
if (idx >= 0) data.credentials[idx] = entry;
|
|
880
|
+
else data.credentials.push(entry);
|
|
881
|
+
saveCredentials(data, cwd);
|
|
882
|
+
return entry;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function removeCredential(id, cwd = process.cwd()) {
|
|
886
|
+
const data = loadCredentials(cwd);
|
|
887
|
+
data.credentials = data.credentials.filter(c => c.id !== id);
|
|
888
|
+
saveCredentials(data, cwd);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export function getHealthyCredentials(provider = null, cwd = process.cwd()) {
|
|
892
|
+
const data = loadCredentials(cwd);
|
|
893
|
+
return data.credentials.filter(c =>
|
|
894
|
+
c.enabled !== false &&
|
|
895
|
+
c.health !== 'unhealthy' &&
|
|
896
|
+
(provider === null || c.provider === provider)
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
export async function checkCredentialHealth(cred, cwd = process.cwd()) {
|
|
901
|
+
let health = 'unknown';
|
|
902
|
+
try {
|
|
903
|
+
if (cred.auth_type === 'cli_oauth') {
|
|
904
|
+
try { execSync('claude --version', { stdio: 'pipe', timeout: 3000 }); } catch { return { ...cred, health: 'unhealthy', last_checked_at: new Date().toISOString() }; }
|
|
905
|
+
try {
|
|
906
|
+
const { getAuthStatus } = await import('./replit.mjs');
|
|
907
|
+
const status = getAuthStatus(cwd);
|
|
908
|
+
health = (status.available && status.tokenStatus !== 'expired') ? 'healthy' : 'degraded';
|
|
909
|
+
} catch {
|
|
910
|
+
health = 'healthy'; // cli works, auth check unavailable
|
|
911
|
+
}
|
|
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
|
+
}
|
|
924
|
+
} catch { health = 'unknown'; }
|
|
925
|
+
return { ...cred, health, last_checked_at: new Date().toISOString() };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export async function detectCredentials(cwd = process.cwd()) {
|
|
929
|
+
const found = [];
|
|
930
|
+
|
|
931
|
+
// Claude CLI / oauth
|
|
932
|
+
const claudeDir = join(homedir(), '.claude');
|
|
933
|
+
const replitClaudeDir = join(cwd, '.replit-tools', '.claude-persistent');
|
|
934
|
+
const claudeAvail = process.env.CLAUDE_CODE || existsSync(claudeDir) || existsSync(replitClaudeDir);
|
|
935
|
+
if (claudeAvail) {
|
|
936
|
+
let health = 'unknown';
|
|
937
|
+
try { execSync('claude --version', { stdio: 'pipe', timeout: 3000 }); health = 'healthy'; } catch { health = 'degraded'; }
|
|
938
|
+
found.push({
|
|
939
|
+
id: 'claude-local-user',
|
|
940
|
+
provider: 'claude',
|
|
941
|
+
auth_type: 'cli_oauth',
|
|
942
|
+
source: 'local_cli',
|
|
943
|
+
owner: 'user',
|
|
944
|
+
scope: 'local',
|
|
945
|
+
plan_hint: null,
|
|
946
|
+
enabled: true,
|
|
947
|
+
health,
|
|
948
|
+
last_checked_at: new Date().toISOString(),
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
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) {
|
|
971
|
+
found.push({
|
|
972
|
+
id: 'openai-api-key',
|
|
973
|
+
provider: 'openai',
|
|
974
|
+
auth_type: 'api_key',
|
|
975
|
+
source: 'env',
|
|
976
|
+
env_var: 'OPENAI_API_KEY',
|
|
977
|
+
owner: 'user',
|
|
978
|
+
scope: 'local',
|
|
979
|
+
plan_hint: null,
|
|
980
|
+
enabled: true,
|
|
981
|
+
health: 'healthy',
|
|
982
|
+
last_checked_at: new Date().toISOString(),
|
|
983
|
+
});
|
|
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 */ }
|
|
1010
|
+
|
|
1011
|
+
return found;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export function getCredentialSummary(cwd = process.cwd()) {
|
|
1015
|
+
const data = loadCredentials(cwd);
|
|
1016
|
+
const creds = data.credentials || [];
|
|
1017
|
+
const byProvider = { claude: 0, openai: 0 };
|
|
1018
|
+
let healthy = 0, degraded = 0;
|
|
1019
|
+
for (const c of creds) {
|
|
1020
|
+
if (c.enabled === false) continue;
|
|
1021
|
+
if (byProvider[c.provider] !== undefined) byProvider[c.provider]++;
|
|
1022
|
+
if (c.health === 'healthy') healthy++;
|
|
1023
|
+
else if (c.health === 'degraded' || c.health === 'unknown') degraded++;
|
|
1024
|
+
}
|
|
1025
|
+
const total = creds.filter(c => c.enabled !== false).length;
|
|
1026
|
+
let teamCapacity = 'none';
|
|
1027
|
+
if (healthy >= 4) teamCapacity = 'high';
|
|
1028
|
+
else if (healthy >= 2) teamCapacity = 'medium';
|
|
1029
|
+
else if (healthy >= 1) teamCapacity = 'low';
|
|
1030
|
+
return { total, byProvider, healthy, degraded, teamCapacity };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
817
1033
|
// ---------------------------------------------------------------------------
|
|
818
1034
|
// CLI
|
|
819
1035
|
// ---------------------------------------------------------------------------
|
|
@@ -894,6 +1110,10 @@ export async function getCapabilityManifest(cwd = process.cwd()) {
|
|
|
894
1110
|
return { pressure5h: pressure, pressure7d: pressure * 0.6 };
|
|
895
1111
|
}
|
|
896
1112
|
|
|
1113
|
+
// ── Credential registry (when available, overrides detection) ─────────
|
|
1114
|
+
const _credData = loadCredentials(cwd);
|
|
1115
|
+
const _hasCreds = _credData.credentials && _credData.credentials.length > 0;
|
|
1116
|
+
|
|
897
1117
|
// ── Claude provider ────────────────────────────────────────────────────
|
|
898
1118
|
const claudeProvider = { available: false, authenticated: false, plan: 'unknown',
|
|
899
1119
|
models: ['opus', 'sonnet', 'haiku'], health: 'healthy',
|
|
@@ -924,6 +1144,20 @@ export async function getCapabilityManifest(cwd = process.cwd()) {
|
|
|
924
1144
|
claudeProvider.health = claudeProvider.authenticated ? deriveHealth('claude') : 'down';
|
|
925
1145
|
claudeProvider.budget = deriveBudget('claude');
|
|
926
1146
|
|
|
1147
|
+
// Override with registry data when credentials.json exists
|
|
1148
|
+
if (_hasCreds) {
|
|
1149
|
+
const claudeCreds = _credData.credentials.filter(c => c.provider === 'claude' && c.enabled !== false);
|
|
1150
|
+
if (claudeCreds.length > 0) {
|
|
1151
|
+
claudeProvider.available = true;
|
|
1152
|
+
claudeProvider.authenticated = claudeCreds.some(c => c.health === 'healthy');
|
|
1153
|
+
claudeProvider.health = claudeCreds.some(c => c.health === 'healthy') ? deriveHealth('claude')
|
|
1154
|
+
: claudeCreds.some(c => c.health === 'degraded') ? 'degraded' : 'down';
|
|
1155
|
+
const planHint = claudeCreds.find(c => c.plan_hint)?.plan_hint;
|
|
1156
|
+
if (planHint) claudeProvider.plan = normalizePlan(planHint);
|
|
1157
|
+
claudeProvider.source = claudeCreds[0].source;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
927
1161
|
// ── OpenAI provider ────────────────────────────────────────────────────
|
|
928
1162
|
const openaiProvider = { available: false, authenticated: false, plan: 'unknown',
|
|
929
1163
|
models: ['gpt-5.5', 'o3', 'gpt-4o', 'gpt-4o-mini'], health: 'healthy',
|
|
@@ -945,6 +1179,20 @@ export async function getCapabilityManifest(cwd = process.cwd()) {
|
|
|
945
1179
|
openaiProvider.health = openaiProvider.authenticated ? deriveHealth('openai') : 'down';
|
|
946
1180
|
openaiProvider.budget = deriveBudget('openai');
|
|
947
1181
|
|
|
1182
|
+
// Override with registry data when credentials.json exists
|
|
1183
|
+
if (_hasCreds) {
|
|
1184
|
+
const openaiCreds = _credData.credentials.filter(c => c.provider === 'openai' && c.enabled !== false);
|
|
1185
|
+
if (openaiCreds.length > 0) {
|
|
1186
|
+
openaiProvider.available = true;
|
|
1187
|
+
openaiProvider.authenticated = openaiCreds.some(c => c.health === 'healthy');
|
|
1188
|
+
openaiProvider.health = openaiCreds.some(c => c.health === 'healthy') ? deriveHealth('openai')
|
|
1189
|
+
: openaiCreds.some(c => c.health === 'degraded') ? 'degraded' : 'down';
|
|
1190
|
+
const planHint = openaiCreds.find(c => c.plan_hint)?.plan_hint;
|
|
1191
|
+
if (planHint) openaiProvider.plan = normalizePlan(planHint);
|
|
1192
|
+
openaiProvider.source = openaiCreds[0].source;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
948
1196
|
// ── Preferences ────────────────────────────────────────────────────────
|
|
949
1197
|
let preferences = { bias: 'auto', forbiddenModels: [], preferredModels: [],
|
|
950
1198
|
costBias: 0.5, confirmBeforeExpensive: false };
|
|
@@ -1163,4 +1411,5 @@ export {
|
|
|
1163
1411
|
// backward-compat stubs (deprecated)
|
|
1164
1412
|
detectExistingAuth, detectPlans, saveSubscription, listSubscriptions,
|
|
1165
1413
|
defaultProfile,
|
|
1414
|
+
// credential registry (functions already exported inline above)
|
|
1166
1415
|
};
|