dual-brain 0.2.29 → 0.3.0
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/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +122 -51
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -85
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -386
- package/src/pipeline.mjs +0 -1689
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -291
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -200
- package/src/self-correct.mjs +0 -146
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -215
- package/src/signal.mjs +0 -114
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/profile.mjs
DELETED
|
@@ -1,1411 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* profile.mjs — User profile module for the Dual-Brain Orchestrator.
|
|
4
|
-
*
|
|
5
|
-
* Exported API:
|
|
6
|
-
* loadProfile(cwd) → profile (or defaults)
|
|
7
|
-
* saveProfile(profile, opts) → write project or global file
|
|
8
|
-
* ensureProfile(cwd, opts) → load or onboard
|
|
9
|
-
* runOnboarding(opts) → interactive 3-question setup
|
|
10
|
-
* rememberPreference(text, opts) → add/update preference
|
|
11
|
-
* forgetPreference(text, cwd) → remove preference by fuzzy match
|
|
12
|
-
* getActivePreferences(cwd) → enabled global + project preferences
|
|
13
|
-
* getAvailableProviders(profile) → enabled providers
|
|
14
|
-
* isSoloBrain(profile) → true if only one provider enabled
|
|
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
|
-
* detectCapabilities(cwd) → available providers (subscription-based only)
|
|
19
|
-
*
|
|
20
|
-
* CLI:
|
|
21
|
-
* node src/profile.mjs # show current profile
|
|
22
|
-
* node src/profile.mjs --init # run onboarding
|
|
23
|
-
* node src/profile.mjs --remember "…" # add preference
|
|
24
|
-
* node src/profile.mjs --forget "…" # remove preference
|
|
25
|
-
* node src/profile.mjs --providers # show available providers
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { createInterface } from 'readline';
|
|
29
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'fs';
|
|
30
|
-
import { homedir } from 'os';
|
|
31
|
-
import { join } from 'path';
|
|
32
|
-
import { execSync } from 'child_process';
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Claude Code memory integration
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
const MEMORY_FILE_NAME = 'dual_brain_preferences.md';
|
|
39
|
-
const MEMORY_INDEX_ENTRY =
|
|
40
|
-
'- [Dual-brain preferences](dual_brain_preferences.md) — Active routing preferences for model/provider selection';
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Derive the Claude Code memory directory for the given project root.
|
|
44
|
-
* Returns null when the directory doesn't exist (i.e. not running on Replit).
|
|
45
|
-
*/
|
|
46
|
-
function _memoryDir(cwd) {
|
|
47
|
-
const root = cwd || process.cwd();
|
|
48
|
-
// Replit persistent memory lives at a fixed path derived from the workspace root.
|
|
49
|
-
// Convert e.g. /home/runner/workspace → -home-runner-workspace
|
|
50
|
-
const encoded = root.replace(/\//g, '-');
|
|
51
|
-
const candidate = join(
|
|
52
|
-
root,
|
|
53
|
-
'.replit-tools',
|
|
54
|
-
'.claude-persistent',
|
|
55
|
-
'projects',
|
|
56
|
-
encoded,
|
|
57
|
-
'memory',
|
|
58
|
-
);
|
|
59
|
-
return existsSync(candidate) ? candidate : null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Write (or update) the dual_brain_preferences.md file in the Claude Code
|
|
64
|
-
* memory directory, and ensure MEMORY.md has an index entry for it.
|
|
65
|
-
* Fails silently if the memory directory is absent or unwritable.
|
|
66
|
-
*/
|
|
67
|
-
function syncPreferencesToMemory(profile, cwd) {
|
|
68
|
-
try {
|
|
69
|
-
const memDir = _memoryDir(cwd);
|
|
70
|
-
if (!memDir) return; // not on Replit / memory dir missing — skip silently
|
|
71
|
-
|
|
72
|
-
const prefs = (profile.preferences || []).filter(p => p.enabled);
|
|
73
|
-
|
|
74
|
-
// Build markdown body
|
|
75
|
-
const prefLines = prefs.length
|
|
76
|
-
? prefs.map(p => `- ${p.text} (scope: ${p.scope || 'project'})`).join('\n')
|
|
77
|
-
: '_(no active preferences)_';
|
|
78
|
-
|
|
79
|
-
const content = [
|
|
80
|
-
'---',
|
|
81
|
-
'name: dual-brain-preferences',
|
|
82
|
-
'description: Active dual-brain routing preferences — affects model selection, provider choice, and dual-brain consensus',
|
|
83
|
-
'metadata:',
|
|
84
|
-
' type: project',
|
|
85
|
-
'---',
|
|
86
|
-
'',
|
|
87
|
-
'Active dual-brain preferences:',
|
|
88
|
-
'',
|
|
89
|
-
prefLines,
|
|
90
|
-
'',
|
|
91
|
-
'These preferences are enforced by the dual-brain orchestrator routing engine.',
|
|
92
|
-
'Provider routing, model selection, and dual-brain consensus decisions',
|
|
93
|
-
'respect these preferences automatically via src/decide.mjs.',
|
|
94
|
-
'',
|
|
95
|
-
].join('\n');
|
|
96
|
-
|
|
97
|
-
const prefFile = join(memDir, MEMORY_FILE_NAME);
|
|
98
|
-
writeFileSync(prefFile, content, 'utf8');
|
|
99
|
-
|
|
100
|
-
// Update MEMORY.md index — add entry only if not already present
|
|
101
|
-
const indexFile = join(memDir, 'MEMORY.md');
|
|
102
|
-
if (existsSync(indexFile)) {
|
|
103
|
-
const existing = readFileSync(indexFile, 'utf8');
|
|
104
|
-
if (!existing.includes(MEMORY_FILE_NAME)) {
|
|
105
|
-
writeFileSync(indexFile, existing.trimEnd() + '\n' + MEMORY_INDEX_ENTRY + '\n', 'utf8');
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
// Non-fatal — the profile JSON remains the source of truth
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
// Environment detection
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Detect the runtime environment.
|
|
119
|
-
* Returns { isReplit, hasReplitTools, isCI }.
|
|
120
|
-
*/
|
|
121
|
-
function detectEnvironment() {
|
|
122
|
-
const isReplit = !!(process.env.REPL_ID || process.env.REPLIT_DB_URL);
|
|
123
|
-
const hasReplitTools = existsSync(join(process.cwd(), '.replit-tools'));
|
|
124
|
-
const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS);
|
|
125
|
-
return { isReplit, hasReplitTools, isCI };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
// Capability detection — only what we can actually verify
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Detect what providers and tools are actually available.
|
|
134
|
-
* Never makes network calls, never claims to know configured plan or price.
|
|
135
|
-
*
|
|
136
|
-
* @param {string} [cwd]
|
|
137
|
-
* @returns {Promise<{
|
|
138
|
-
* claude: { available: boolean, source: string|null },
|
|
139
|
-
* openai: { available: boolean, source: string|null },
|
|
140
|
-
* codex: { available: boolean, source: string|null },
|
|
141
|
-
* replitTools: { available: boolean, checkpoints: boolean },
|
|
142
|
-
* }>}
|
|
143
|
-
*/
|
|
144
|
-
async function detectCapabilities(cwd) {
|
|
145
|
-
const root = cwd || process.cwd();
|
|
146
|
-
|
|
147
|
-
// --- Claude: running inside Claude Code session or CLI installed ---
|
|
148
|
-
let claudeAvailable = false;
|
|
149
|
-
let claudeSource = null;
|
|
150
|
-
|
|
151
|
-
if (process.env.CLAUDE_CODE) {
|
|
152
|
-
claudeAvailable = true;
|
|
153
|
-
claudeSource = 'claude-code';
|
|
154
|
-
} else {
|
|
155
|
-
// Check for ~/.claude directory (Claude Code installation) or Replit Claude
|
|
156
|
-
const claudeDir = join(homedir(), '.claude');
|
|
157
|
-
const replitClaudeDir = join(root, '.replit-tools', '.claude-persistent');
|
|
158
|
-
if (existsSync(claudeDir) || existsSync(replitClaudeDir)) {
|
|
159
|
-
claudeAvailable = true;
|
|
160
|
-
claudeSource = existsSync(replitClaudeDir) ? 'claude-code' : 'claude-dir';
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// --- Codex: check if 'codex' is in PATH ---
|
|
165
|
-
let codexAvailable = false;
|
|
166
|
-
let codexSource = null;
|
|
167
|
-
try {
|
|
168
|
-
execSync('which codex', { stdio: 'pipe', timeout: 2000 });
|
|
169
|
-
codexAvailable = true;
|
|
170
|
-
codexSource = 'cli';
|
|
171
|
-
} catch {
|
|
172
|
-
// not in PATH
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// --- replit-tools: check if directory exists or binary in PATH ---
|
|
176
|
-
const replitToolsDir = join(root, '.replit-tools');
|
|
177
|
-
let replitToolsAvailable = existsSync(replitToolsDir);
|
|
178
|
-
if (!replitToolsAvailable) {
|
|
179
|
-
try {
|
|
180
|
-
execSync('which replit-tools', { stdio: 'pipe', timeout: 2000 });
|
|
181
|
-
replitToolsAvailable = true;
|
|
182
|
-
} catch {
|
|
183
|
-
// not in PATH
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Check for checkpoint capability (replit-specific)
|
|
188
|
-
const checkpointsBin = existsSync(join(replitToolsDir, 'checkpoints'))
|
|
189
|
-
|| existsSync('/usr/local/bin/replit-checkpoint');
|
|
190
|
-
|
|
191
|
-
// --- MCP servers: check Claude settings files ---
|
|
192
|
-
let mcpServers = [];
|
|
193
|
-
try {
|
|
194
|
-
const claudeSettings = join(homedir(), '.claude', 'settings.json');
|
|
195
|
-
if (existsSync(claudeSettings)) {
|
|
196
|
-
const settings = JSON.parse(readFileSync(claudeSettings, 'utf8'));
|
|
197
|
-
if (settings.mcpServers) {
|
|
198
|
-
mcpServers = Object.keys(settings.mcpServers);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// Also check project-local
|
|
202
|
-
const localSettings = join(root, '.claude', 'settings.json');
|
|
203
|
-
if (existsSync(localSettings)) {
|
|
204
|
-
const local = JSON.parse(readFileSync(localSettings, 'utf8'));
|
|
205
|
-
if (local.mcpServers) {
|
|
206
|
-
mcpServers.push(...Object.keys(local.mcpServers));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
} catch {}
|
|
210
|
-
|
|
211
|
-
// --- Claude plugins: check installed plugin marketplaces ---
|
|
212
|
-
let claudePlugins = [];
|
|
213
|
-
try {
|
|
214
|
-
const pluginDir = join(root, '.replit-tools', '.claude-persistent', 'plugins', 'marketplaces');
|
|
215
|
-
if (existsSync(pluginDir)) {
|
|
216
|
-
const marketplaces = readdirSync(pluginDir);
|
|
217
|
-
for (const m of marketplaces) {
|
|
218
|
-
const mDir = join(pluginDir, m, 'plugins');
|
|
219
|
-
if (existsSync(mDir)) {
|
|
220
|
-
claudePlugins.push(...readdirSync(mDir));
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
} catch {}
|
|
225
|
-
|
|
226
|
-
// --- Codex plugins: check available plugins ---
|
|
227
|
-
let codexPlugins = [];
|
|
228
|
-
try {
|
|
229
|
-
const pluginDir = join(root, '.replit-tools', '.codex-persistent', '.tmp', 'plugins', 'plugins');
|
|
230
|
-
if (existsSync(pluginDir)) {
|
|
231
|
-
codexPlugins = readdirSync(pluginDir).filter(f => !f.startsWith('.'));
|
|
232
|
-
}
|
|
233
|
-
} catch {}
|
|
234
|
-
|
|
235
|
-
// --- Shell snapshots: count .sh files ---
|
|
236
|
-
let shellSnapshots = 0;
|
|
237
|
-
try {
|
|
238
|
-
const snapDir = join(root, '.replit-tools', '.claude-persistent', 'shell-snapshots');
|
|
239
|
-
if (existsSync(snapDir)) {
|
|
240
|
-
shellSnapshots = readdirSync(snapDir).filter(f => f.endsWith('.sh')).length;
|
|
241
|
-
}
|
|
242
|
-
} catch {}
|
|
243
|
-
|
|
244
|
-
// --- Configured hooks: count by type from settings.local.json ---
|
|
245
|
-
let configuredHooks = { PreToolUse: 0, PostToolUse: 0, Stop: 0, Notification: 0 };
|
|
246
|
-
try {
|
|
247
|
-
const localSettings = join(root, '.claude', 'settings.local.json');
|
|
248
|
-
if (existsSync(localSettings)) {
|
|
249
|
-
const s = JSON.parse(readFileSync(localSettings, 'utf8'));
|
|
250
|
-
for (const hookType of Object.keys(configuredHooks)) {
|
|
251
|
-
configuredHooks[hookType] = s.hooks?.[hookType]?.length || 0;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
} catch {}
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
claude: {
|
|
258
|
-
available: claudeAvailable,
|
|
259
|
-
source: claudeSource,
|
|
260
|
-
},
|
|
261
|
-
openai: {
|
|
262
|
-
available: codexAvailable,
|
|
263
|
-
source: codexAvailable ? 'codex-cli' : null,
|
|
264
|
-
},
|
|
265
|
-
codex: {
|
|
266
|
-
available: codexAvailable,
|
|
267
|
-
source: codexSource,
|
|
268
|
-
},
|
|
269
|
-
replitTools: {
|
|
270
|
-
available: replitToolsAvailable,
|
|
271
|
-
checkpoints: checkpointsBin,
|
|
272
|
-
},
|
|
273
|
-
mcpServers,
|
|
274
|
-
claudePlugins,
|
|
275
|
-
codexPlugins,
|
|
276
|
-
shellSnapshots,
|
|
277
|
-
configuredHooks,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Generate an honest 2-3 line onboarding/status message based on
|
|
283
|
-
* what we can actually verify.
|
|
284
|
-
*
|
|
285
|
-
* @param {object} capabilities — result of detectCapabilities()
|
|
286
|
-
* @param {string} [workStyle] — 'balanced' | 'cost-saver' | 'quality-first'
|
|
287
|
-
* @returns {string}
|
|
288
|
-
*/
|
|
289
|
-
function getOnboardingMessage(capabilities, workStyle = 'balanced') {
|
|
290
|
-
const found = [];
|
|
291
|
-
if (capabilities?.claude?.available) found.push('Claude · subscription');
|
|
292
|
-
if (capabilities?.codex?.available) found.push('OpenAI · Codex subscription');
|
|
293
|
-
|
|
294
|
-
const styleLabels = {
|
|
295
|
-
'balanced': 'Balanced — smart routing, reviews on important changes',
|
|
296
|
-
'cost-saver': 'Cost-saver — prefers faster models, skips dual-brain for low-risk tasks',
|
|
297
|
-
'quality-first': 'Quality-first — dual-brain for medium+ risk, stricter reviews',
|
|
298
|
-
};
|
|
299
|
-
const modeLabel = styleLabels[workStyle] || styleLabels['balanced'];
|
|
300
|
-
|
|
301
|
-
const lines = [];
|
|
302
|
-
if (found.length === 0) {
|
|
303
|
-
lines.push('No providers detected');
|
|
304
|
-
lines.push(' Run: claude login or install Claude Code to get started');
|
|
305
|
-
return lines.join('\n');
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
lines.push(`Found: ${found.join(', ')}`);
|
|
309
|
-
lines.push(` Mode: ${modeLabel}`);
|
|
310
|
-
|
|
311
|
-
// Tip: suggest Codex if only Claude is available
|
|
312
|
-
if (capabilities?.claude?.available && !capabilities?.codex?.available) {
|
|
313
|
-
lines.push(' Tip: Run codex login for dual-brain collaboration');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return lines.join('\n');
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ---------------------------------------------------------------------------
|
|
320
|
-
// Paths & defaults
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
|
|
323
|
-
const GLOBAL_DIR = join(homedir(), '.config', 'dual-brain');
|
|
324
|
-
const GLOBAL_PATH = join(GLOBAL_DIR, 'profile.json');
|
|
325
|
-
const projectPath = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'profile.json');
|
|
326
|
-
|
|
327
|
-
function defaultProfile() {
|
|
328
|
-
const now = new Date().toISOString();
|
|
329
|
-
return {
|
|
330
|
-
schemaVersion: 2,
|
|
331
|
-
createdAt: now,
|
|
332
|
-
updatedAt: now,
|
|
333
|
-
workStyle: 'balanced',
|
|
334
|
-
providers: {
|
|
335
|
-
claude: { enabled: true },
|
|
336
|
-
openai: { enabled: false },
|
|
337
|
-
},
|
|
338
|
-
mode: 'auto',
|
|
339
|
-
bias: 'balanced',
|
|
340
|
-
preferences: [],
|
|
341
|
-
apiGuardrail: false,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ---------------------------------------------------------------------------
|
|
346
|
-
// Schema migration
|
|
347
|
-
// ---------------------------------------------------------------------------
|
|
348
|
-
|
|
349
|
-
function migrateProfile(profile) {
|
|
350
|
-
// v5.x compat: convert old `subscriptions` field to `providers`
|
|
351
|
-
if (profile.subscriptions && !profile.providers) {
|
|
352
|
-
profile.providers = {};
|
|
353
|
-
for (const [key, sub] of Object.entries(profile.subscriptions)) {
|
|
354
|
-
profile.providers[key] = { enabled: true };
|
|
355
|
-
// Drop plan/price fields — we no longer track subscription tier
|
|
356
|
-
void sub;
|
|
357
|
-
}
|
|
358
|
-
delete profile.subscriptions;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (!profile.schemaVersion || profile.schemaVersion < 1) {
|
|
362
|
-
// v0 → v1: add missing fields with defaults
|
|
363
|
-
profile.schemaVersion = 1;
|
|
364
|
-
profile.mode = profile.mode || 'auto';
|
|
365
|
-
profile.bias = profile.bias || 'balanced';
|
|
366
|
-
profile.preferences = profile.preferences || [];
|
|
367
|
-
profile.providers = profile.providers || {};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (profile.schemaVersion < 2) {
|
|
371
|
-
// v1 → v2: remove fake subscription fields, add workStyle + apiGuardrail
|
|
372
|
-
profile.schemaVersion = 2;
|
|
373
|
-
profile.workStyle = profile.workStyle || profile.bias || 'balanced';
|
|
374
|
-
profile.apiGuardrail = profile.apiGuardrail ?? false;
|
|
375
|
-
|
|
376
|
-
// Strip price/plan/budget fields — they were never accurate
|
|
377
|
-
for (const prov of Object.values(profile.providers || {})) {
|
|
378
|
-
delete prov.plan;
|
|
379
|
-
delete prov.label;
|
|
380
|
-
delete prov.expiresAt;
|
|
381
|
-
delete prov.subs;
|
|
382
|
-
}
|
|
383
|
-
delete profile.plan;
|
|
384
|
-
delete profile.price;
|
|
385
|
-
delete profile.subscription; // doctor:verified — removing legacy field from stored config
|
|
386
|
-
delete profile.budget;
|
|
387
|
-
delete profile.detectedPlan;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return profile;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// ---------------------------------------------------------------------------
|
|
394
|
-
// Load / save
|
|
395
|
-
// ---------------------------------------------------------------------------
|
|
396
|
-
|
|
397
|
-
function loadProfile(cwd) {
|
|
398
|
-
let profile;
|
|
399
|
-
for (const p of [projectPath(cwd), GLOBAL_PATH]) {
|
|
400
|
-
if (existsSync(p)) {
|
|
401
|
-
try { profile = migrateProfile(JSON.parse(readFileSync(p, 'utf8'))); break; } catch { /* skip */ }
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (!profile) profile = defaultProfile();
|
|
405
|
-
return profile;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function saveProfile(profile, opts = {}) {
|
|
409
|
-
const target = opts.global ? GLOBAL_PATH : projectPath(opts.cwd);
|
|
410
|
-
const dir = target.slice(0, target.lastIndexOf('/'));
|
|
411
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
412
|
-
profile.updatedAt = new Date().toISOString();
|
|
413
|
-
const tmp = target + '.tmp.' + process.pid;
|
|
414
|
-
writeFileSync(tmp, JSON.stringify(profile, null, 2) + '\n');
|
|
415
|
-
renameSync(tmp, target);
|
|
416
|
-
return target;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// ---------------------------------------------------------------------------
|
|
420
|
-
// Onboarding
|
|
421
|
-
// ---------------------------------------------------------------------------
|
|
422
|
-
|
|
423
|
-
async function runOnboarding(opts = {}) {
|
|
424
|
-
if (!opts.interactive) return defaultProfile();
|
|
425
|
-
|
|
426
|
-
// Accept an externally-provided readline instance (shared with REPL/auth setup)
|
|
427
|
-
// or create one internally if not provided. Only close if we created it.
|
|
428
|
-
const rlProvided = !!opts.rl;
|
|
429
|
-
const rl = opts.rl || createInterface({ input: process.stdin, output: process.stdout });
|
|
430
|
-
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
431
|
-
const profile = defaultProfile();
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
process.stdout.write('\nDual-Brain Orchestrator — First-time setup\n\n');
|
|
435
|
-
|
|
436
|
-
// Detect what's actually available
|
|
437
|
-
const capabilities = await detectCapabilities(opts.cwd);
|
|
438
|
-
|
|
439
|
-
// Show what we found honestly
|
|
440
|
-
const foundProviders = [];
|
|
441
|
-
if (capabilities.claude.available) foundProviders.push('Claude · subscription');
|
|
442
|
-
if (capabilities.codex.available) foundProviders.push('OpenAI · Codex subscription');
|
|
443
|
-
|
|
444
|
-
if (foundProviders.length > 0) {
|
|
445
|
-
process.stdout.write(`Detected: ${foundProviders.join(', ')}\n\n`);
|
|
446
|
-
} else {
|
|
447
|
-
process.stdout.write('No providers detected automatically.\n\n');
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Enable providers based on what's available
|
|
451
|
-
profile.providers.claude.enabled = capabilities.claude.available;
|
|
452
|
-
profile.providers.openai.enabled = capabilities.codex.available;
|
|
453
|
-
|
|
454
|
-
// If detection missed something, ask
|
|
455
|
-
if (!capabilities.claude.available && !capabilities.codex.available) {
|
|
456
|
-
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();
|
|
457
|
-
if (q1 === '1') { profile.providers.claude.enabled = true; }
|
|
458
|
-
else if (q1 === '2') { profile.providers.claude.enabled = false; profile.providers.openai.enabled = true; }
|
|
459
|
-
else if (q1 === '3') { profile.providers.claude.enabled = true; profile.providers.openai.enabled = true; }
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const q3 = (await ask('\nDefault work style?\n (1) Save usage (2) Balanced (3) Best quality\n> ')).trim();
|
|
463
|
-
profile.bias = ({ '1': 'cost-saver', '3': 'quality-first' })[q3] || 'balanced';
|
|
464
|
-
profile.workStyle = profile.bias;
|
|
465
|
-
|
|
466
|
-
const n = Object.values(profile.providers).filter(p => p.enabled).length;
|
|
467
|
-
profile.mode = n >= 2 ? 'dual' : profile.providers.claude.enabled ? 'solo-claude' : 'solo-openai';
|
|
468
|
-
process.stdout.write('\nProfile saved.\n');
|
|
469
|
-
} finally {
|
|
470
|
-
// Only close if we created the rl instance (not if it was passed in)
|
|
471
|
-
if (!rlProvided) rl.close();
|
|
472
|
-
}
|
|
473
|
-
return profile;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
async function ensureProfile(cwd, opts = {}) {
|
|
477
|
-
for (const p of [projectPath(cwd), GLOBAL_PATH]) {
|
|
478
|
-
if (existsSync(p)) {
|
|
479
|
-
try { return migrateProfile(JSON.parse(readFileSync(p, 'utf8'))); } catch { /* skip */ }
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
const profile = await runOnboarding(opts);
|
|
483
|
-
saveProfile(profile, { cwd, global: opts.global });
|
|
484
|
-
return profile;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// ---------------------------------------------------------------------------
|
|
488
|
-
// Preferences
|
|
489
|
-
// ---------------------------------------------------------------------------
|
|
490
|
-
|
|
491
|
-
const VALID_SCOPES = ['one-off', 'project', 'global'];
|
|
492
|
-
|
|
493
|
-
function rememberPreference(text, opts = {}) {
|
|
494
|
-
const scope = VALID_SCOPES.includes(opts.scope) ? opts.scope : 'project';
|
|
495
|
-
const cwd = opts.cwd || process.cwd();
|
|
496
|
-
const profile = loadProfile(cwd);
|
|
497
|
-
const needle = text.toLowerCase();
|
|
498
|
-
const idx = profile.preferences.findIndex(p =>
|
|
499
|
-
p.text.toLowerCase().includes(needle) || needle.includes(p.text.toLowerCase()));
|
|
500
|
-
if (idx >= 0) profile.preferences[idx] = { text, enabled: true, scope };
|
|
501
|
-
else profile.preferences.push({ text, enabled: true, scope });
|
|
502
|
-
saveProfile(profile, { cwd, global: opts.global || scope === 'global' });
|
|
503
|
-
syncPreferencesToMemory(profile, cwd);
|
|
504
|
-
return profile;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function forgetPreference(text, cwd) {
|
|
508
|
-
const profile = loadProfile(cwd);
|
|
509
|
-
const needle = text.toLowerCase();
|
|
510
|
-
profile.preferences = profile.preferences.filter(p => !p.text.toLowerCase().includes(needle));
|
|
511
|
-
saveProfile(profile, { cwd });
|
|
512
|
-
syncPreferencesToMemory(profile, cwd);
|
|
513
|
-
return profile;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function getActivePreferences(cwd) {
|
|
517
|
-
const seen = new Set();
|
|
518
|
-
const result = [];
|
|
519
|
-
for (const p of [GLOBAL_PATH, projectPath(cwd)]) {
|
|
520
|
-
if (!existsSync(p)) continue;
|
|
521
|
-
try {
|
|
522
|
-
for (const pref of JSON.parse(readFileSync(p, 'utf8')).preferences || []) {
|
|
523
|
-
if (pref.enabled && !seen.has(pref.text)) { seen.add(pref.text); result.push(pref); }
|
|
524
|
-
}
|
|
525
|
-
} catch { /* skip */ }
|
|
526
|
-
}
|
|
527
|
-
return result;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// ---------------------------------------------------------------------------
|
|
531
|
-
// Provider helpers
|
|
532
|
-
// ---------------------------------------------------------------------------
|
|
533
|
-
|
|
534
|
-
function getAvailableProviders(profile) {
|
|
535
|
-
return Object.entries(profile.providers || {})
|
|
536
|
-
.filter(([, p]) => p.enabled)
|
|
537
|
-
.map(([name, p]) => ({ name, ...p }));
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function isSoloBrain(profile) {
|
|
541
|
-
return getAvailableProviders(profile).length === 1;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function getHeadModel(profile) {
|
|
545
|
-
const providers = getAvailableProviders(profile);
|
|
546
|
-
if (providers.length === 0) return 'sonnet';
|
|
547
|
-
if (providers.length === 1) return providers[0].name === 'openai' ? 'gpt-4o' : 'sonnet';
|
|
548
|
-
// Both available — default to Claude (we're running in Claude Code)
|
|
549
|
-
return 'sonnet';
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// ---------------------------------------------------------------------------
|
|
553
|
-
// Capability-based auto-setup (replaces subscription-based autoSetup)
|
|
554
|
-
// ---------------------------------------------------------------------------
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Silently configure a profile from detected capabilities — no user input.
|
|
558
|
-
*
|
|
559
|
-
* Returns:
|
|
560
|
-
* {
|
|
561
|
-
* confident: boolean, // true when at least one provider was found
|
|
562
|
-
* profile: object|null, // fully-built profile ready to save, or null
|
|
563
|
-
* warnings: string[], // non-fatal issues
|
|
564
|
-
* actions: string[], // human-readable lines for the summary box
|
|
565
|
-
* }
|
|
566
|
-
*/
|
|
567
|
-
async function autoSetup(cwd) {
|
|
568
|
-
const capabilities = await detectCapabilities(cwd);
|
|
569
|
-
const env = detectEnvironment();
|
|
570
|
-
|
|
571
|
-
const result = {
|
|
572
|
-
confident: false,
|
|
573
|
-
profile: null,
|
|
574
|
-
warnings: [],
|
|
575
|
-
actions: [],
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
// Need at least one provider
|
|
579
|
-
if (!capabilities.claude.available && !capabilities.openai.available && !capabilities.codex.available) {
|
|
580
|
-
result.warnings.push('No provider credentials found');
|
|
581
|
-
return result;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const profile = defaultProfile();
|
|
585
|
-
|
|
586
|
-
// Claude
|
|
587
|
-
if (capabilities.claude.available) {
|
|
588
|
-
profile.providers.claude.enabled = true;
|
|
589
|
-
result.actions.push(`Claude: available (${capabilities.claude.source})`);
|
|
590
|
-
} else {
|
|
591
|
-
profile.providers.claude.enabled = false;
|
|
592
|
-
result.warnings.push('Claude not detected — run: claude login');
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// OpenAI / Codex
|
|
596
|
-
if (capabilities.codex.available) {
|
|
597
|
-
profile.providers.openai.enabled = true;
|
|
598
|
-
result.actions.push('Codex CLI: available (subscription)');
|
|
599
|
-
} else {
|
|
600
|
-
profile.providers.openai.enabled = false;
|
|
601
|
-
result.warnings.push('OpenAI not detected — run: codex login');
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Mode
|
|
605
|
-
const enabledCount = Object.values(profile.providers).filter(p => p.enabled).length;
|
|
606
|
-
profile.mode = enabledCount >= 2 ? 'dual'
|
|
607
|
-
: profile.providers.claude.enabled ? 'solo-claude'
|
|
608
|
-
: 'solo-openai';
|
|
609
|
-
profile.bias = 'balanced';
|
|
610
|
-
profile.workStyle = 'balanced';
|
|
611
|
-
profile.capabilities = capabilities;
|
|
612
|
-
profile.detectedAt = new Date().toISOString();
|
|
613
|
-
|
|
614
|
-
// Environment note
|
|
615
|
-
if (env.isReplit && env.hasReplitTools) {
|
|
616
|
-
result.actions.push('Replit + replit-tools detected');
|
|
617
|
-
} else if (env.isReplit) {
|
|
618
|
-
result.actions.push('Replit environment detected');
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
result.confident = true;
|
|
622
|
-
result.profile = profile;
|
|
623
|
-
return result;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// ---------------------------------------------------------------------------
|
|
627
|
-
// OAuth token auto-refresh (unchanged — token refresh is still valid)
|
|
628
|
-
// ---------------------------------------------------------------------------
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Silently refresh the Claude OAuth token before it expires.
|
|
632
|
-
*
|
|
633
|
-
* Returns one of:
|
|
634
|
-
* { status: 'valid', hoursRemaining }
|
|
635
|
-
* { status: 'refreshed', hoursRemaining }
|
|
636
|
-
* { status: 'expiring_no_refresh' | 'expired', hoursRemaining }
|
|
637
|
-
* { status: 'no_credentials' | 'parse_error' | 'no_expiry' }
|
|
638
|
-
* { status: 'refresh_failed', error }
|
|
639
|
-
*
|
|
640
|
-
* @param {string} [cwd]
|
|
641
|
-
*/
|
|
642
|
-
async function autoRefreshToken(cwd) {
|
|
643
|
-
// Delegate to replit-tools auth refresh script when available,
|
|
644
|
-
// to avoid competing token refreshes from two different code paths.
|
|
645
|
-
try {
|
|
646
|
-
const { getAuthStatus, inspectReplitTools } = await import('./replit.mjs');
|
|
647
|
-
const tools = inspectReplitTools(cwd || process.cwd());
|
|
648
|
-
if (tools.authRefresh?.available) {
|
|
649
|
-
const status = getAuthStatus(cwd || process.cwd());
|
|
650
|
-
if (status.available) {
|
|
651
|
-
// replit-tools owns the refresh cycle — report current status and exit
|
|
652
|
-
const hoursRemaining = status.expiresAt
|
|
653
|
-
? Math.max(0, Math.floor((Date.parse(status.expiresAt) - Date.now()) / 3_600_000))
|
|
654
|
-
: null;
|
|
655
|
-
if (status.tokenStatus === 'valid') {
|
|
656
|
-
return { status: 'valid', hoursRemaining: hoursRemaining ?? 999, delegatedTo: 'replit-tools' };
|
|
657
|
-
}
|
|
658
|
-
if (status.tokenStatus === 'expired') {
|
|
659
|
-
// replit-tools will handle the actual refresh on its own schedule;
|
|
660
|
-
// we note the state but do not attempt our own refresh.
|
|
661
|
-
return { status: 'expiring_no_refresh', hoursRemaining: 0, delegatedTo: 'replit-tools' };
|
|
662
|
-
}
|
|
663
|
-
// expiring or unknown — note delegation and skip our own refresh attempt
|
|
664
|
-
return { status: 'valid', hoursRemaining: hoursRemaining ?? 1, delegatedTo: 'replit-tools' };
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
} catch {
|
|
668
|
-
// replit.mjs unavailable — fall through to direct refresh
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const home = process.env.HOME || '/root';
|
|
672
|
-
const credPaths = [
|
|
673
|
-
join(home, '.claude', '.credentials.json'),
|
|
674
|
-
join(cwd || '.', '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
675
|
-
];
|
|
676
|
-
|
|
677
|
-
let credPath = null;
|
|
678
|
-
for (const p of credPaths) {
|
|
679
|
-
if (existsSync(p)) { credPath = p; break; }
|
|
680
|
-
}
|
|
681
|
-
if (!credPath) return { status: 'no_credentials' };
|
|
682
|
-
|
|
683
|
-
let creds;
|
|
684
|
-
try {
|
|
685
|
-
creds = JSON.parse(readFileSync(credPath, 'utf8'));
|
|
686
|
-
} catch { return { status: 'parse_error' }; }
|
|
687
|
-
|
|
688
|
-
const oauth = creds?.claudeAiOauth;
|
|
689
|
-
if (!oauth?.expiresAt) return { status: 'no_expiry' };
|
|
690
|
-
|
|
691
|
-
const now = Date.now();
|
|
692
|
-
const remainingMs = oauth.expiresAt - now;
|
|
693
|
-
const remainingHours = Math.floor(remainingMs / 1000 / 60 / 60);
|
|
694
|
-
|
|
695
|
-
// More than 2 hours left — no refresh needed
|
|
696
|
-
if (remainingHours >= 2) {
|
|
697
|
-
return { status: 'valid', hoursRemaining: remainingHours };
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Need refresh
|
|
701
|
-
if (!oauth.refreshToken) {
|
|
702
|
-
return { status: remainingMs > 0 ? 'expiring_no_refresh' : 'expired', hoursRemaining: remainingHours };
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
const res = await fetch('https://console.anthropic.com/v1/oauth/token', {
|
|
707
|
-
method: 'POST',
|
|
708
|
-
headers: { 'Content-Type': 'application/json' },
|
|
709
|
-
body: JSON.stringify({
|
|
710
|
-
grant_type: 'refresh_token',
|
|
711
|
-
refresh_token: oauth.refreshToken,
|
|
712
|
-
client_id: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
713
|
-
}),
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
if (!res.ok) return { status: 'refresh_failed', error: `HTTP ${res.status}` };
|
|
717
|
-
|
|
718
|
-
const data = await res.json();
|
|
719
|
-
if (!data.access_token) return { status: 'refresh_failed', error: 'no access_token' };
|
|
720
|
-
|
|
721
|
-
// Update credentials
|
|
722
|
-
const newExpiresAt = now + (data.expires_in * 1000);
|
|
723
|
-
creds.claudeAiOauth.accessToken = data.access_token;
|
|
724
|
-
if (data.refresh_token) creds.claudeAiOauth.refreshToken = data.refresh_token;
|
|
725
|
-
creds.claudeAiOauth.expiresAt = newExpiresAt;
|
|
726
|
-
|
|
727
|
-
// Backup then write
|
|
728
|
-
try { writeFileSync(credPath + '.backup', readFileSync(credPath)); } catch {}
|
|
729
|
-
writeFileSync(credPath, JSON.stringify(creds));
|
|
730
|
-
|
|
731
|
-
const newHours = Math.floor((data.expires_in) / 60 / 60);
|
|
732
|
-
return { status: 'refreshed', hoursRemaining: newHours };
|
|
733
|
-
} catch (e) {
|
|
734
|
-
return { status: 'refresh_failed', error: e.message };
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// ---------------------------------------------------------------------------
|
|
739
|
-
// detectAuth — kept for backward compat, now delegates to detectCapabilities
|
|
740
|
-
// ---------------------------------------------------------------------------
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Detect CLI login status for Claude and Codex.
|
|
744
|
-
* Checks config files on disk — never makes network calls.
|
|
745
|
-
*
|
|
746
|
-
* @returns {{ claude: AuthEntry, openai: AuthEntry }}
|
|
747
|
-
* @typedef {{ found: boolean, source: string|null, loginType: 'oauth'|'cli'|null }} AuthEntry
|
|
748
|
-
*/
|
|
749
|
-
async function detectAuth() {
|
|
750
|
-
const results = {
|
|
751
|
-
claude: { found: false, source: null, loginType: null },
|
|
752
|
-
openai: { found: false, source: null, loginType: null },
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
// --- Claude: check .claude.json for oauthAccount (CLI login) ---
|
|
756
|
-
const claudePaths = [
|
|
757
|
-
'/home/runner/workspace/.replit-tools/.claude-persistent/.claude.json',
|
|
758
|
-
join(homedir(), '.claude', '.claude.json'),
|
|
759
|
-
];
|
|
760
|
-
for (const p of claudePaths) {
|
|
761
|
-
try {
|
|
762
|
-
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
763
|
-
if (data?.oauthAccount) {
|
|
764
|
-
results.claude.found = true;
|
|
765
|
-
results.claude.source = p.includes('.replit-tools') ? 'claude CLI (replit-tools)' : 'claude CLI';
|
|
766
|
-
results.claude.loginType = 'oauth';
|
|
767
|
-
break;
|
|
768
|
-
}
|
|
769
|
-
if (data?.apiKey && typeof data.apiKey === 'string') {
|
|
770
|
-
results.claude.found = true;
|
|
771
|
-
results.claude.source = p.includes('.replit-tools') ? 'claude CLI (replit-tools)' : 'claude CLI';
|
|
772
|
-
results.claude.loginType = 'cli';
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
} catch { continue; }
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// --- OpenAI/Codex: check auth.json for access_token or id_token (CLI login) ---
|
|
779
|
-
const codexPaths = [
|
|
780
|
-
'/home/runner/workspace/.replit-tools/.codex-persistent/auth.json',
|
|
781
|
-
join(homedir(), '.codex', 'auth.json'),
|
|
782
|
-
];
|
|
783
|
-
for (const p of codexPaths) {
|
|
784
|
-
try {
|
|
785
|
-
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
786
|
-
const accessToken = data?.tokens?.access_token || data?.access_token;
|
|
787
|
-
const idToken = data?.tokens?.id_token || data?.id_token;
|
|
788
|
-
|
|
789
|
-
if (accessToken || idToken) {
|
|
790
|
-
results.openai.found = true;
|
|
791
|
-
results.openai.source = p.includes('.replit-tools') ? 'codex CLI (replit-tools)' : 'codex CLI';
|
|
792
|
-
results.openai.loginType = 'oauth';
|
|
793
|
-
break;
|
|
794
|
-
}
|
|
795
|
-
} catch { continue; }
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
return results;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// ---------------------------------------------------------------------------
|
|
802
|
-
// Removed: detectExistingAuth, detectPlans, decodeJwtPayload, saveSubscription,
|
|
803
|
-
// listSubscriptions, _planLabel, _runWithTimeout
|
|
804
|
-
// These claimed to detect subscription tier/price from auth files — that was
|
|
805
|
-
// never accurate. Use detectCapabilities() instead for honest detection.
|
|
806
|
-
// ---------------------------------------------------------------------------
|
|
807
|
-
|
|
808
|
-
// Thin stubs retained only so any callers that weren't updated yet
|
|
809
|
-
// fail gracefully with a clear message rather than a crash.
|
|
810
|
-
|
|
811
|
-
/** @deprecated Use detectCapabilities() instead. */
|
|
812
|
-
async function detectExistingAuth(cwd) {
|
|
813
|
-
const caps = await detectCapabilities(cwd);
|
|
814
|
-
return {
|
|
815
|
-
claude: {
|
|
816
|
-
found: caps.claude.available,
|
|
817
|
-
source: caps.claude.source,
|
|
818
|
-
plan: null, // not detectable
|
|
819
|
-
expiresAt: null,
|
|
820
|
-
},
|
|
821
|
-
openai: {
|
|
822
|
-
found: caps.openai.available || caps.codex.available,
|
|
823
|
-
source: caps.openai.source || caps.codex.source,
|
|
824
|
-
plan: null, // not detectable
|
|
825
|
-
},
|
|
826
|
-
existingProfile: [projectPath(cwd), GLOBAL_PATH].some(p => existsSync(p)),
|
|
827
|
-
recommendations: {
|
|
828
|
-
headModel: caps.claude.available ? 'claude-sonnet-4-6' : 'gpt-4o',
|
|
829
|
-
// budget field removed — we don't track subscription price
|
|
830
|
-
profile: 'balanced',
|
|
831
|
-
},
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/** @deprecated Price-based plan tiers removed. Returns null for all providers. */
|
|
836
|
-
function detectPlans() {
|
|
837
|
-
return { claude: null, openai: null };
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/** @deprecated Plan tracking removed. Use provider enabled flag instead. */
|
|
841
|
-
function saveSubscription(provider, config, cwd) {
|
|
842
|
-
const profile = loadProfile(cwd);
|
|
843
|
-
if (!profile.providers[provider]) profile.providers[provider] = { enabled: true };
|
|
844
|
-
profile.providers[provider].enabled = true;
|
|
845
|
-
saveProfile(profile, { cwd: cwd || process.cwd() });
|
|
846
|
-
return profile;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/** @deprecated Plan tracking removed. Use getAvailableProviders() instead. */
|
|
850
|
-
function listSubscriptions(cwd) {
|
|
851
|
-
const profile = loadProfile(cwd);
|
|
852
|
-
return profile.providers || {};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// ---------------------------------------------------------------------------
|
|
856
|
-
// Credential Registry
|
|
857
|
-
// ---------------------------------------------------------------------------
|
|
858
|
-
|
|
859
|
-
const credentialsPath = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'credentials.json');
|
|
860
|
-
|
|
861
|
-
function defaultCredentials() {
|
|
862
|
-
return { version: 1, credentials: [] };
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
export function loadCredentials(cwd = process.cwd()) {
|
|
866
|
-
try {
|
|
867
|
-
const p = credentialsPath(cwd);
|
|
868
|
-
if (!existsSync(p)) return defaultCredentials();
|
|
869
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
870
|
-
} catch {
|
|
871
|
-
return defaultCredentials();
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
export function saveCredentials(data, cwd = process.cwd()) {
|
|
876
|
-
try {
|
|
877
|
-
const p = credentialsPath(cwd);
|
|
878
|
-
const dir = p.slice(0, p.lastIndexOf('/'));
|
|
879
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
880
|
-
// Ensure no raw secret values are stored
|
|
881
|
-
const safe = {
|
|
882
|
-
...data,
|
|
883
|
-
credentials: (data.credentials || []).map(c => {
|
|
884
|
-
const clean = { ...c };
|
|
885
|
-
delete clean.secret;
|
|
886
|
-
delete clean.token;
|
|
887
|
-
delete clean.api_key;
|
|
888
|
-
delete clean.password;
|
|
889
|
-
return clean;
|
|
890
|
-
}),
|
|
891
|
-
};
|
|
892
|
-
const tmp = p + '.tmp.' + process.pid;
|
|
893
|
-
writeFileSync(tmp, JSON.stringify(safe, null, 2) + '\n');
|
|
894
|
-
renameSync(tmp, p);
|
|
895
|
-
return p;
|
|
896
|
-
} catch { /* non-fatal */ }
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
export function addCredential(cred, cwd = process.cwd()) {
|
|
900
|
-
const required = ['id', 'provider', 'auth_type', 'source'];
|
|
901
|
-
for (const f of required) {
|
|
902
|
-
if (!cred[f]) throw new Error(`addCredential: missing required field '${f}'`);
|
|
903
|
-
}
|
|
904
|
-
const data = loadCredentials(cwd);
|
|
905
|
-
const idx = data.credentials.findIndex(c => c.id === cred.id);
|
|
906
|
-
const entry = {
|
|
907
|
-
id: cred.id,
|
|
908
|
-
provider: cred.provider,
|
|
909
|
-
auth_type: cred.auth_type,
|
|
910
|
-
source: cred.source,
|
|
911
|
-
owner: cred.owner || 'user',
|
|
912
|
-
scope: cred.scope || 'local',
|
|
913
|
-
plan_hint: cred.plan_hint || null,
|
|
914
|
-
enabled: cred.enabled !== false,
|
|
915
|
-
health: cred.health || 'unknown',
|
|
916
|
-
last_checked_at: cred.last_checked_at || null,
|
|
917
|
-
};
|
|
918
|
-
if (idx >= 0) data.credentials[idx] = entry;
|
|
919
|
-
else data.credentials.push(entry);
|
|
920
|
-
saveCredentials(data, cwd);
|
|
921
|
-
return entry;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
export function removeCredential(id, cwd = process.cwd()) {
|
|
925
|
-
const data = loadCredentials(cwd);
|
|
926
|
-
data.credentials = data.credentials.filter(c => c.id !== id);
|
|
927
|
-
saveCredentials(data, cwd);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
export function getHealthyCredentials(provider = null, cwd = process.cwd()) {
|
|
931
|
-
const data = loadCredentials(cwd);
|
|
932
|
-
return data.credentials.filter(c =>
|
|
933
|
-
c.enabled !== false &&
|
|
934
|
-
c.health !== 'unhealthy' &&
|
|
935
|
-
(provider === null || c.provider === provider)
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
export async function checkCredentialHealth(cred, cwd = process.cwd()) {
|
|
940
|
-
let health = 'unknown';
|
|
941
|
-
try {
|
|
942
|
-
if (cred.auth_type === 'cli_oauth') {
|
|
943
|
-
try { execSync('claude --version', { stdio: 'pipe', timeout: 3000 }); } catch { return { ...cred, health: 'unhealthy', last_checked_at: new Date().toISOString() }; }
|
|
944
|
-
try {
|
|
945
|
-
const { getAuthStatus } = await import('./replit.mjs');
|
|
946
|
-
const status = getAuthStatus(cwd);
|
|
947
|
-
health = (status.available && status.tokenStatus !== 'expired') ? 'healthy' : 'degraded';
|
|
948
|
-
} catch {
|
|
949
|
-
health = 'healthy'; // cli works, auth check unavailable
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
} catch { health = 'unknown'; }
|
|
953
|
-
return { ...cred, health, last_checked_at: new Date().toISOString() };
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
export async function detectCredentials(cwd = process.cwd()) {
|
|
957
|
-
const found = [];
|
|
958
|
-
|
|
959
|
-
// Claude CLI / oauth
|
|
960
|
-
const claudeDir = join(homedir(), '.claude');
|
|
961
|
-
const replitClaudeDir = join(cwd, '.replit-tools', '.claude-persistent');
|
|
962
|
-
const claudeAvail = process.env.CLAUDE_CODE || existsSync(claudeDir) || existsSync(replitClaudeDir);
|
|
963
|
-
if (claudeAvail) {
|
|
964
|
-
let health = 'unknown';
|
|
965
|
-
try { execSync('claude --version', { stdio: 'pipe', timeout: 3000 }); health = 'healthy'; } catch { health = 'degraded'; }
|
|
966
|
-
found.push({
|
|
967
|
-
id: 'claude-local-user',
|
|
968
|
-
provider: 'claude',
|
|
969
|
-
auth_type: 'cli_oauth',
|
|
970
|
-
source: 'local_cli',
|
|
971
|
-
owner: 'user',
|
|
972
|
-
scope: 'local',
|
|
973
|
-
plan_hint: null,
|
|
974
|
-
enabled: true,
|
|
975
|
-
health,
|
|
976
|
-
last_checked_at: new Date().toISOString(),
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// Codex CLI (subscription-based OpenAI access)
|
|
981
|
-
try {
|
|
982
|
-
execSync('which codex', { stdio: 'pipe', timeout: 2000 });
|
|
983
|
-
let codexHealth = 'unknown';
|
|
984
|
-
try { execSync('codex --version', { stdio: 'pipe', timeout: 3000 }); codexHealth = 'healthy'; } catch { codexHealth = 'degraded'; }
|
|
985
|
-
found.push({
|
|
986
|
-
id: 'openai-codex-cli',
|
|
987
|
-
provider: 'openai',
|
|
988
|
-
auth_type: 'cli_oauth',
|
|
989
|
-
source: 'local_cli',
|
|
990
|
-
owner: 'user',
|
|
991
|
-
scope: 'local',
|
|
992
|
-
plan_hint: null,
|
|
993
|
-
enabled: true,
|
|
994
|
-
health: codexHealth,
|
|
995
|
-
last_checked_at: new Date().toISOString(),
|
|
996
|
-
});
|
|
997
|
-
} catch { /* codex not in PATH */ }
|
|
998
|
-
|
|
999
|
-
return found;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
export function getCredentialSummary(cwd = process.cwd()) {
|
|
1003
|
-
const data = loadCredentials(cwd);
|
|
1004
|
-
const creds = data.credentials || [];
|
|
1005
|
-
const byProvider = { claude: 0, openai: 0 };
|
|
1006
|
-
let healthy = 0, degraded = 0;
|
|
1007
|
-
for (const c of creds) {
|
|
1008
|
-
if (c.enabled === false) continue;
|
|
1009
|
-
if (byProvider[c.provider] !== undefined) byProvider[c.provider]++;
|
|
1010
|
-
if (c.health === 'healthy') healthy++;
|
|
1011
|
-
else if (c.health === 'degraded' || c.health === 'unknown') degraded++;
|
|
1012
|
-
}
|
|
1013
|
-
const total = creds.filter(c => c.enabled !== false).length;
|
|
1014
|
-
let teamCapacity = 'none';
|
|
1015
|
-
if (healthy >= 4) teamCapacity = 'high';
|
|
1016
|
-
else if (healthy >= 2) teamCapacity = 'medium';
|
|
1017
|
-
else if (healthy >= 1) teamCapacity = 'low';
|
|
1018
|
-
return { total, byProvider, healthy, degraded, teamCapacity };
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// ---------------------------------------------------------------------------
|
|
1022
|
-
// CLI
|
|
1023
|
-
// ---------------------------------------------------------------------------
|
|
1024
|
-
|
|
1025
|
-
// ---------------------------------------------------------------------------
|
|
1026
|
-
// Capability Manifest — single runtime view of all provider/subscription state
|
|
1027
|
-
// ---------------------------------------------------------------------------
|
|
1028
|
-
|
|
1029
|
-
/** 60-second in-process cache for the manifest. */
|
|
1030
|
-
let _manifestCache = null;
|
|
1031
|
-
let _manifestCachedAt = 0;
|
|
1032
|
-
const MANIFEST_TTL_MS = 60_000;
|
|
1033
|
-
|
|
1034
|
-
/**
|
|
1035
|
-
* Build a normalized capability manifest that consolidates provider health,
|
|
1036
|
-
* subscription config, user preferences, policy, and learning data.
|
|
1037
|
-
*
|
|
1038
|
-
* @param {string} [cwd]
|
|
1039
|
-
* @returns {Promise<object>}
|
|
1040
|
-
*/
|
|
1041
|
-
export async function getCapabilityManifest(cwd = process.cwd()) {
|
|
1042
|
-
const now = Date.now();
|
|
1043
|
-
if (_manifestCache && now - _manifestCachedAt < MANIFEST_TTL_MS) {
|
|
1044
|
-
return _manifestCache;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// ── Read orchestrator.json for subscription config ─────────────────────
|
|
1048
|
-
let orchConfig = {};
|
|
1049
|
-
try {
|
|
1050
|
-
const orchPath = join(cwd, 'orchestrator.json');
|
|
1051
|
-
orchConfig = JSON.parse(readFileSync(orchPath, 'utf8'));
|
|
1052
|
-
} catch { /* missing or malformed — fall through */ }
|
|
1053
|
-
|
|
1054
|
-
const orchSubs = orchConfig.subscriptions ?? {};
|
|
1055
|
-
const orchProv = orchConfig.providers ?? {};
|
|
1056
|
-
|
|
1057
|
-
// ── Plan normalizer (orchestrator.json uses "$100", "max-5x", "pro" etc) ─
|
|
1058
|
-
function normalizePlan(raw) {
|
|
1059
|
-
if (!raw) return 'unknown';
|
|
1060
|
-
const s = String(raw).toLowerCase();
|
|
1061
|
-
if (s.includes('max') && s.includes('20')) return 'max20';
|
|
1062
|
-
if (s.includes('max') && (s.includes('5') || s.includes('5x'))) return 'max5';
|
|
1063
|
-
if (s.includes('pro')) return 'pro';
|
|
1064
|
-
if (s.includes('plus')) return 'plus';
|
|
1065
|
-
if (s === '$20' || s === '20') return 'pro';
|
|
1066
|
-
if (s === '$100' || s === '100') return 'max5';
|
|
1067
|
-
if (s === '$200' || s === '200') return 'max20';
|
|
1068
|
-
return 'unknown';
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// ── Environment capabilities (MCP, plugins, hooks, snapshots) ─────────
|
|
1072
|
-
const envCaps = await detectCapabilities(cwd);
|
|
1073
|
-
|
|
1074
|
-
// ── Health states ──────────────────────────────────────────────────────
|
|
1075
|
-
let healthStates = {};
|
|
1076
|
-
try {
|
|
1077
|
-
const { getHealth } = await import('./health.mjs');
|
|
1078
|
-
healthStates = getHealth(cwd).states ?? {};
|
|
1079
|
-
} catch { /* health.mjs unavailable */ }
|
|
1080
|
-
|
|
1081
|
-
function deriveHealth(providerKey) {
|
|
1082
|
-
// Aggregate across all model classes for the provider
|
|
1083
|
-
const entries = Object.entries(healthStates)
|
|
1084
|
-
.filter(([k]) => k.startsWith(providerKey + ':'))
|
|
1085
|
-
.map(([, v]) => v?.status ?? 'healthy');
|
|
1086
|
-
if (entries.length === 0) return 'healthy';
|
|
1087
|
-
if (entries.some(s => s === 'hot')) return 'rate-limited';
|
|
1088
|
-
if (entries.some(s => s === 'degraded')) return 'degraded';
|
|
1089
|
-
if (entries.some(s => s === 'probing')) return 'degraded';
|
|
1090
|
-
return 'healthy';
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// ── Budget pressure from health file (simple proxy) ────────────────────
|
|
1094
|
-
function deriveBudget(providerKey) {
|
|
1095
|
-
const hotEntries = Object.entries(healthStates)
|
|
1096
|
-
.filter(([k]) => k.startsWith(providerKey + ':'))
|
|
1097
|
-
.filter(([, v]) => v?.status === 'hot');
|
|
1098
|
-
if (hotEntries.length === 0) return { pressure5h: 0, pressure7d: 0 };
|
|
1099
|
-
// Clamp to 0.9 when hot — we don't have real token data here
|
|
1100
|
-
const pressure = Math.min(0.9, 0.5 + hotEntries.length * 0.15);
|
|
1101
|
-
return { pressure5h: pressure, pressure7d: pressure * 0.6 };
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// ── Credential registry (when available, overrides detection) ─────────
|
|
1105
|
-
const _credData = loadCredentials(cwd);
|
|
1106
|
-
const _hasCreds = _credData.credentials && _credData.credentials.length > 0;
|
|
1107
|
-
|
|
1108
|
-
// ── Claude provider ────────────────────────────────────────────────────
|
|
1109
|
-
const claudeProvider = { available: false, authenticated: false, plan: 'unknown',
|
|
1110
|
-
models: ['opus', 'sonnet', 'haiku'], health: 'healthy',
|
|
1111
|
-
budget: { pressure5h: 0, pressure7d: 0 }, source: 'none' };
|
|
1112
|
-
|
|
1113
|
-
try {
|
|
1114
|
-
// available: CLAUDE_CODE env, claude CLI, or replit-tools claude dir
|
|
1115
|
-
const claudeDir = join(homedir(), '.claude');
|
|
1116
|
-
const replitClaudeDir = join(cwd, '.replit-tools', '.claude-persistent');
|
|
1117
|
-
if (process.env.CLAUDE_CODE) {
|
|
1118
|
-
claudeProvider.available = true;
|
|
1119
|
-
claudeProvider.source = 'credentials';
|
|
1120
|
-
} else if (existsSync(claudeDir) || existsSync(replitClaudeDir)) {
|
|
1121
|
-
claudeProvider.available = true;
|
|
1122
|
-
claudeProvider.source = existsSync(replitClaudeDir) ? 'replit-tools' : 'credentials';
|
|
1123
|
-
} else {
|
|
1124
|
-
try { execSync('which claude', { stdio: 'pipe', timeout: 2000 }); claudeProvider.available = true; claudeProvider.source = 'credentials'; } catch { /* not found */ }
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// authenticated: use getAuthHealthStatus
|
|
1128
|
-
const { getAuthHealthStatus } = await import('./health.mjs');
|
|
1129
|
-
const authStatus = await getAuthHealthStatus(cwd);
|
|
1130
|
-
claudeProvider.authenticated = authStatus.ok;
|
|
1131
|
-
if (authStatus.source === 'replit-tools') claudeProvider.source = 'replit-tools';
|
|
1132
|
-
} catch { /* getAuthHealthStatus unavailable */ }
|
|
1133
|
-
|
|
1134
|
-
claudeProvider.plan = normalizePlan(orchProv.claude?.subscription ?? orchSubs.claude?.plan);
|
|
1135
|
-
claudeProvider.health = claudeProvider.authenticated ? deriveHealth('claude') : 'down';
|
|
1136
|
-
claudeProvider.budget = deriveBudget('claude');
|
|
1137
|
-
|
|
1138
|
-
// Override with registry data when credentials.json exists
|
|
1139
|
-
if (_hasCreds) {
|
|
1140
|
-
const claudeCreds = _credData.credentials.filter(c => c.provider === 'claude' && c.enabled !== false);
|
|
1141
|
-
if (claudeCreds.length > 0) {
|
|
1142
|
-
claudeProvider.available = true;
|
|
1143
|
-
claudeProvider.authenticated = claudeCreds.some(c => c.health === 'healthy');
|
|
1144
|
-
claudeProvider.health = claudeCreds.some(c => c.health === 'healthy') ? deriveHealth('claude')
|
|
1145
|
-
: claudeCreds.some(c => c.health === 'degraded') ? 'degraded' : 'down';
|
|
1146
|
-
const planHint = claudeCreds.find(c => c.plan_hint)?.plan_hint;
|
|
1147
|
-
if (planHint) claudeProvider.plan = normalizePlan(planHint);
|
|
1148
|
-
claudeProvider.source = claudeCreds[0].source;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// ── OpenAI provider ────────────────────────────────────────────────────
|
|
1153
|
-
const openaiProvider = { available: false, authenticated: false, plan: 'unknown',
|
|
1154
|
-
models: ['gpt-5.5', 'o3', 'gpt-4o', 'gpt-4o-mini'], health: 'healthy',
|
|
1155
|
-
budget: { pressure5h: 0, pressure7d: 0 }, source: 'none' };
|
|
1156
|
-
|
|
1157
|
-
try {
|
|
1158
|
-
let codexAvailable = false;
|
|
1159
|
-
try { execSync('which codex', { stdio: 'pipe', timeout: 2000 }); codexAvailable = true; } catch { /* not in PATH */ }
|
|
1160
|
-
|
|
1161
|
-
openaiProvider.available = codexAvailable;
|
|
1162
|
-
openaiProvider.authenticated = codexAvailable;
|
|
1163
|
-
openaiProvider.source = codexAvailable ? 'codex-cli' : 'none';
|
|
1164
|
-
} catch { /* detection failed */ }
|
|
1165
|
-
|
|
1166
|
-
openaiProvider.plan = normalizePlan(orchProv.openai?.subscription ?? orchSubs.openai?.plan);
|
|
1167
|
-
openaiProvider.health = openaiProvider.authenticated ? deriveHealth('openai') : 'down';
|
|
1168
|
-
openaiProvider.budget = deriveBudget('openai');
|
|
1169
|
-
|
|
1170
|
-
// Override with registry data when credentials.json exists
|
|
1171
|
-
if (_hasCreds) {
|
|
1172
|
-
const openaiCreds = _credData.credentials.filter(c => c.provider === 'openai' && c.enabled !== false);
|
|
1173
|
-
if (openaiCreds.length > 0) {
|
|
1174
|
-
openaiProvider.available = true;
|
|
1175
|
-
openaiProvider.authenticated = openaiCreds.some(c => c.health === 'healthy');
|
|
1176
|
-
openaiProvider.health = openaiCreds.some(c => c.health === 'healthy') ? deriveHealth('openai')
|
|
1177
|
-
: openaiCreds.some(c => c.health === 'degraded') ? 'degraded' : 'down';
|
|
1178
|
-
const planHint = openaiCreds.find(c => c.plan_hint)?.plan_hint;
|
|
1179
|
-
if (planHint) openaiProvider.plan = normalizePlan(planHint);
|
|
1180
|
-
openaiProvider.source = openaiCreds[0].source;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// ── Preferences ────────────────────────────────────────────────────────
|
|
1185
|
-
let preferences = { bias: 'auto', forbiddenModels: [], preferredModels: [],
|
|
1186
|
-
costBias: 0.5, confirmBeforeExpensive: false };
|
|
1187
|
-
try {
|
|
1188
|
-
const profile = loadProfile(cwd);
|
|
1189
|
-
const bias = profile.bias ?? profile.workStyle ?? 'auto';
|
|
1190
|
-
preferences.bias = ['auto','balanced','cost-saver','quality-first'].includes(bias) ? bias : 'auto';
|
|
1191
|
-
preferences.forbiddenModels = profile.forbiddenModels ?? [];
|
|
1192
|
-
preferences.preferredModels = profile.preferredModels ?? [];
|
|
1193
|
-
preferences.costBias = profile.costBias ?? (bias === 'cost-saver' ? 0.8 : bias === 'quality-first' ? 0.1 : 0.5);
|
|
1194
|
-
preferences.confirmBeforeExpensive = profile.apiGuardrail ?? false;
|
|
1195
|
-
} catch { /* profile unavailable */ }
|
|
1196
|
-
|
|
1197
|
-
// ── Policy ─────────────────────────────────────────────────────────────
|
|
1198
|
-
const policy = {
|
|
1199
|
-
highRiskRequiresBestAvailable: true,
|
|
1200
|
-
failoverMode: 'tell',
|
|
1201
|
-
dualBrainThreshold: 'high',
|
|
1202
|
-
};
|
|
1203
|
-
|
|
1204
|
-
// ── Learning ───────────────────────────────────────────────────────────
|
|
1205
|
-
let learning = {};
|
|
1206
|
-
try {
|
|
1207
|
-
const { getModelSuccessRates } = await import('./doctor.mjs');
|
|
1208
|
-
learning = getModelSuccessRates(cwd);
|
|
1209
|
-
} catch { /* doctor.mjs unavailable */ }
|
|
1210
|
-
|
|
1211
|
-
// ── Setup summary ──────────────────────────────────────────────────────
|
|
1212
|
-
const hasAnyProvider = (claudeProvider.available && claudeProvider.authenticated) ||
|
|
1213
|
-
(openaiProvider.available && openaiProvider.authenticated);
|
|
1214
|
-
|
|
1215
|
-
let recommendedAction = null;
|
|
1216
|
-
if (!claudeProvider.available && !openaiProvider.available) {
|
|
1217
|
-
recommendedAction = 'connect-claude';
|
|
1218
|
-
} else if (!claudeProvider.authenticated && !openaiProvider.authenticated) {
|
|
1219
|
-
recommendedAction = 'refresh-auth';
|
|
1220
|
-
} else if (!openaiProvider.available) {
|
|
1221
|
-
recommendedAction = 'connect-openai';
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
const manifest = {
|
|
1225
|
-
providers: { claude: claudeProvider, openai: openaiProvider },
|
|
1226
|
-
preferences,
|
|
1227
|
-
policy,
|
|
1228
|
-
learning,
|
|
1229
|
-
setup: {
|
|
1230
|
-
hasAnyProvider,
|
|
1231
|
-
recommendedAction,
|
|
1232
|
-
zeroProviderMode: !hasAnyProvider,
|
|
1233
|
-
},
|
|
1234
|
-
environment: {
|
|
1235
|
-
mcpServers: envCaps.mcpServers,
|
|
1236
|
-
claudePlugins: envCaps.claudePlugins,
|
|
1237
|
-
codexPlugins: envCaps.codexPlugins,
|
|
1238
|
-
shellSnapshots: envCaps.shellSnapshots,
|
|
1239
|
-
configuredHooks: envCaps.configuredHooks,
|
|
1240
|
-
replitTools: envCaps.replitTools,
|
|
1241
|
-
},
|
|
1242
|
-
timestamp: new Date().toISOString(),
|
|
1243
|
-
};
|
|
1244
|
-
|
|
1245
|
-
_manifestCache = manifest;
|
|
1246
|
-
_manifestCachedAt = now;
|
|
1247
|
-
return manifest;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
/**
|
|
1251
|
-
* Compute the effective routing policy for a specific task, applying rules in order:
|
|
1252
|
-
* 1. Safety constraints (high-risk → best available model)
|
|
1253
|
-
* 2. Provider availability
|
|
1254
|
-
* 3. Task tier fit (search→haiku, execute→sonnet, think→opus)
|
|
1255
|
-
* 4. User preferences (cost bias, forbidden models)
|
|
1256
|
-
* 5. Learning (prefer models with ≥90% success rate for this task type)
|
|
1257
|
-
*
|
|
1258
|
-
* @param {object} manifest — from getCapabilityManifest()
|
|
1259
|
-
* @param {{ tier?: string, risk?: string, taskType?: string }} taskContext
|
|
1260
|
-
* @returns {{ provider: string, model: string, tier: string, reason: string, overrides: string[] }}
|
|
1261
|
-
*/
|
|
1262
|
-
export function getEffectivePolicy(manifest, taskContext = {}) {
|
|
1263
|
-
const { providers, preferences, policy, learning } = manifest;
|
|
1264
|
-
const tier = taskContext.tier ?? 'execute';
|
|
1265
|
-
const risk = taskContext.risk ?? 'medium';
|
|
1266
|
-
const taskType = taskContext.taskType ?? 'general';
|
|
1267
|
-
const overrides = [];
|
|
1268
|
-
|
|
1269
|
-
// Tier → default model mapping
|
|
1270
|
-
const tierModelMap = { search: 'haiku', execute: 'sonnet', think: 'opus' };
|
|
1271
|
-
let preferredModel = tierModelMap[tier] ?? 'sonnet';
|
|
1272
|
-
let preferredProvider = 'claude';
|
|
1273
|
-
|
|
1274
|
-
// 1. Safety: high/critical risk → best available model
|
|
1275
|
-
if (policy.highRiskRequiresBestAvailable && (risk === 'high' || risk === 'critical')) {
|
|
1276
|
-
preferredModel = 'opus';
|
|
1277
|
-
overrides.push(`risk=${risk} → upgraded to opus`);
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// 2. Provider availability — fall back to openai if claude is down
|
|
1281
|
-
const claudeOk = providers.claude.available && providers.claude.authenticated &&
|
|
1282
|
-
providers.claude.health !== 'down';
|
|
1283
|
-
const openaiOk = providers.openai.available && providers.openai.authenticated &&
|
|
1284
|
-
providers.openai.health !== 'down';
|
|
1285
|
-
|
|
1286
|
-
if (!claudeOk && openaiOk) {
|
|
1287
|
-
preferredProvider = 'openai';
|
|
1288
|
-
// Remap model names for openai
|
|
1289
|
-
const openaiTierMap = { search: 'gpt-4o-mini', execute: 'gpt-4o', think: 'gpt-5.5' };
|
|
1290
|
-
preferredModel = risk === 'high' || risk === 'critical' ? 'gpt-5.5' : (openaiTierMap[tier] ?? 'gpt-4o');
|
|
1291
|
-
overrides.push('claude unavailable → routed to openai');
|
|
1292
|
-
} else if (!claudeOk && !openaiOk) {
|
|
1293
|
-
return { provider: 'none', model: 'none', tier, reason: 'no providers available', overrides };
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// 3. Task fit already applied via tierModelMap above
|
|
1297
|
-
|
|
1298
|
-
// 4. User preferences: forbidden models, cost bias
|
|
1299
|
-
const forbidden = preferences.forbiddenModels ?? [];
|
|
1300
|
-
if (forbidden.includes(preferredModel)) {
|
|
1301
|
-
// Downgrade one step
|
|
1302
|
-
const fallback = preferredProvider === 'claude'
|
|
1303
|
-
? (preferredModel === 'opus' ? 'sonnet' : 'haiku')
|
|
1304
|
-
: (preferredModel === 'gpt-5.5' ? 'gpt-4o' : 'gpt-4o-mini');
|
|
1305
|
-
overrides.push(`${preferredModel} forbidden → downgraded to ${fallback}`);
|
|
1306
|
-
preferredModel = fallback;
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
if (preferences.costBias > 0.7 && preferredModel === 'opus' && risk !== 'high' && risk !== 'critical') {
|
|
1310
|
-
preferredModel = 'sonnet';
|
|
1311
|
-
overrides.push('cost-bias > 0.7 → downgraded from opus to sonnet');
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// 5. Learning: if another model has ≥90% success for this task type, prefer it
|
|
1315
|
-
const successRates = learning ?? {};
|
|
1316
|
-
let bestLearnedModel = null;
|
|
1317
|
-
let bestRate = 0.9; // threshold
|
|
1318
|
-
for (const [model, stats] of Object.entries(successRates)) {
|
|
1319
|
-
if (stats.rate >= bestRate && stats.total >= 5 && !forbidden.includes(model)) {
|
|
1320
|
-
// Only prefer if it's on the right provider
|
|
1321
|
-
const isClaudeModel = ['opus', 'sonnet', 'haiku'].includes(model);
|
|
1322
|
-
if ((preferredProvider === 'claude' && isClaudeModel) ||
|
|
1323
|
-
(preferredProvider === 'openai' && !isClaudeModel)) {
|
|
1324
|
-
bestLearnedModel = model;
|
|
1325
|
-
bestRate = stats.rate;
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
if (bestLearnedModel && bestLearnedModel !== preferredModel) {
|
|
1330
|
-
overrides.push(`learning: ${bestLearnedModel} has ${Math.round(bestRate * 100)}% success → preferred`);
|
|
1331
|
-
preferredModel = bestLearnedModel;
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
const reason = overrides.length > 0
|
|
1335
|
-
? overrides[0]
|
|
1336
|
-
: `tier=${tier} → ${preferredProvider}/${preferredModel}`;
|
|
1337
|
-
|
|
1338
|
-
return { provider: preferredProvider, model: preferredModel, tier, reason, overrides };
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
async function main() {
|
|
1342
|
-
const args = process.argv.slice(2);
|
|
1343
|
-
const cwd = process.cwd();
|
|
1344
|
-
const flag = args[0];
|
|
1345
|
-
const val = args[1];
|
|
1346
|
-
|
|
1347
|
-
if (flag === '--init') {
|
|
1348
|
-
const profile = await runOnboarding({ interactive: true, cwd });
|
|
1349
|
-
saveProfile(profile, { cwd });
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
|
-
if (flag === '--remember') {
|
|
1353
|
-
if (!val) { process.stderr.write('Usage: --remember "text"\n'); process.exit(1); }
|
|
1354
|
-
const p = rememberPreference(val, { cwd });
|
|
1355
|
-
process.stdout.write(`Preference saved. Total: ${p.preferences.length}\n`);
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
if (flag === '--forget') {
|
|
1359
|
-
if (!val) { process.stderr.write('Usage: --forget "text"\n'); process.exit(1); }
|
|
1360
|
-
forgetPreference(val, cwd);
|
|
1361
|
-
process.stdout.write('Preference removed (if matched).\n');
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
if (flag === '--providers') {
|
|
1365
|
-
const providers = getAvailableProviders(loadProfile(cwd));
|
|
1366
|
-
if (!providers.length) { process.stdout.write('No providers enabled.\n'); return; }
|
|
1367
|
-
providers.forEach(p => process.stdout.write(`${p.name} enabled=${p.enabled}\n`));
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
if (flag === '--capabilities') {
|
|
1371
|
-
const caps = await detectCapabilities(cwd);
|
|
1372
|
-
process.stdout.write(JSON.stringify(caps, null, 2) + '\n');
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// default: show profile
|
|
1377
|
-
const profile = loadProfile(cwd);
|
|
1378
|
-
const providers = getAvailableProviders(profile);
|
|
1379
|
-
const caps = await detectCapabilities(cwd);
|
|
1380
|
-
[
|
|
1381
|
-
`mode : ${profile.mode}`,
|
|
1382
|
-
`workStyle : ${profile.workStyle || profile.bias}`,
|
|
1383
|
-
`head model : ${getHeadModel(profile)}`,
|
|
1384
|
-
`providers : ${providers.map(p => p.name).join(', ') || 'none'}`,
|
|
1385
|
-
`prefs : ${profile.preferences?.filter(p => p.enabled).length || 0} active`,
|
|
1386
|
-
`guardrail : off`,
|
|
1387
|
-
'',
|
|
1388
|
-
getOnboardingMessage(caps, profile.workStyle || profile.bias),
|
|
1389
|
-
].forEach(l => process.stdout.write(l + '\n'));
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
const isMain = process.argv[1]?.endsWith('profile.mjs');
|
|
1393
|
-
if (isMain) main().catch(e => { process.stderr.write(e.message + '\n'); process.exit(1); });
|
|
1394
|
-
|
|
1395
|
-
// ---------------------------------------------------------------------------
|
|
1396
|
-
// Exports
|
|
1397
|
-
// ---------------------------------------------------------------------------
|
|
1398
|
-
|
|
1399
|
-
export {
|
|
1400
|
-
loadProfile, saveProfile, ensureProfile, runOnboarding,
|
|
1401
|
-
rememberPreference, forgetPreference, getActivePreferences,
|
|
1402
|
-
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
1403
|
-
detectCapabilities, getOnboardingMessage,
|
|
1404
|
-
syncPreferencesToMemory,
|
|
1405
|
-
detectAuth, detectEnvironment,
|
|
1406
|
-
autoSetup, autoRefreshToken,
|
|
1407
|
-
// backward-compat stubs (deprecated)
|
|
1408
|
-
detectExistingAuth, detectPlans, saveSubscription, listSubscriptions,
|
|
1409
|
-
defaultProfile,
|
|
1410
|
-
// credential registry (functions already exported inline above)
|
|
1411
|
-
};
|