dual-brain 0.2.30 → 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.
Files changed (309) hide show
  1. package/.dual-brain/docs/claude-code-extension-points.md +32 -0
  2. package/.dual-brain/docs/data-tools-capabilities.md +181 -0
  3. package/.dual-brain/docs/ecosystem-tools.md +91 -0
  4. package/.dual-brain/docs/panel-handoff.md +124 -0
  5. package/.dual-brain/docs/ruflo-analysis.md +48 -0
  6. package/bin/dual-brain.mjs +56 -56
  7. package/dist/mcp-server/index.d.ts +27 -0
  8. package/dist/mcp-server/index.js +359 -0
  9. package/dist/mcp-server/index.js.map +1 -0
  10. package/dist/src/agent-protocol.d.ts +163 -0
  11. package/dist/src/agent-protocol.js +368 -0
  12. package/dist/src/agent-protocol.js.map +1 -0
  13. package/dist/src/agents/registry.d.ts +52 -0
  14. package/dist/src/agents/registry.js +393 -0
  15. package/dist/src/agents/registry.js.map +1 -0
  16. package/dist/src/awareness.d.ts +93 -0
  17. package/dist/src/awareness.js +406 -0
  18. package/dist/src/awareness.js.map +1 -0
  19. package/dist/src/brief.d.ts +48 -0
  20. package/dist/src/brief.js +179 -0
  21. package/dist/src/brief.js.map +1 -0
  22. package/dist/src/calibration.d.ts +32 -0
  23. package/dist/src/calibration.js +133 -0
  24. package/dist/src/calibration.js.map +1 -0
  25. package/dist/src/checkpoint.d.ts +33 -0
  26. package/dist/src/checkpoint.js +99 -0
  27. package/dist/src/checkpoint.js.map +1 -0
  28. package/dist/src/ci-triage.d.ts +33 -0
  29. package/dist/src/ci-triage.js +193 -0
  30. package/dist/src/ci-triage.js.map +1 -0
  31. package/dist/src/cognitive-loop.d.ts +56 -0
  32. package/dist/src/cognitive-loop.js +495 -0
  33. package/dist/src/cognitive-loop.js.map +1 -0
  34. package/dist/src/collaboration.d.ts +147 -0
  35. package/dist/src/collaboration.js +438 -0
  36. package/dist/src/collaboration.js.map +1 -0
  37. package/dist/src/context-intel.d.ts +47 -0
  38. package/dist/src/context-intel.js +156 -0
  39. package/dist/src/context-intel.js.map +1 -0
  40. package/dist/src/context.d.ts +53 -0
  41. package/dist/src/context.js +332 -0
  42. package/dist/src/context.js.map +1 -0
  43. package/dist/src/continuity.d.ts +89 -0
  44. package/dist/src/continuity.js +230 -0
  45. package/dist/src/continuity.js.map +1 -0
  46. package/dist/src/cost-tracker.d.ts +47 -0
  47. package/dist/src/cost-tracker.js +170 -0
  48. package/dist/src/cost-tracker.js.map +1 -0
  49. package/dist/src/debrief.d.ts +53 -0
  50. package/dist/src/debrief.js +222 -0
  51. package/dist/src/debrief.js.map +1 -0
  52. package/dist/src/decide.d.ts +96 -0
  53. package/dist/src/decide.js +744 -0
  54. package/dist/src/decide.js.map +1 -0
  55. package/dist/src/decompose.d.ts +39 -0
  56. package/dist/src/decompose.js +218 -0
  57. package/dist/src/decompose.js.map +1 -0
  58. package/dist/src/detect.d.ts +91 -0
  59. package/dist/src/detect.js +544 -0
  60. package/dist/src/detect.js.map +1 -0
  61. package/dist/src/dispatch.d.ts +154 -0
  62. package/dist/src/dispatch.js +1306 -0
  63. package/dist/src/dispatch.js.map +1 -0
  64. package/dist/src/doctor.d.ts +421 -0
  65. package/dist/src/doctor.js +1689 -0
  66. package/dist/src/doctor.js.map +1 -0
  67. package/dist/src/engine.d.ts +70 -0
  68. package/dist/src/engine.js +155 -0
  69. package/dist/src/engine.js.map +1 -0
  70. package/dist/src/envelope.d.ts +36 -0
  71. package/dist/src/envelope.js +80 -0
  72. package/dist/src/envelope.js.map +1 -0
  73. package/dist/src/failure-memory.d.ts +55 -0
  74. package/dist/src/failure-memory.js +175 -0
  75. package/dist/src/failure-memory.js.map +1 -0
  76. package/dist/src/fx.d.ts +87 -0
  77. package/dist/src/fx.js +272 -0
  78. package/dist/src/fx.js.map +1 -0
  79. package/dist/src/governance.d.ts +93 -0
  80. package/dist/src/governance.js +261 -0
  81. package/dist/src/governance.js.map +1 -0
  82. package/dist/src/handoff.d.ts +11 -0
  83. package/dist/src/handoff.js +90 -0
  84. package/dist/src/handoff.js.map +1 -0
  85. package/dist/src/head-protocol.d.ts +76 -0
  86. package/dist/src/head-protocol.js +109 -0
  87. package/dist/src/head-protocol.js.map +1 -0
  88. package/dist/src/head.d.ts +222 -0
  89. package/dist/src/head.js +765 -0
  90. package/dist/src/head.js.map +1 -0
  91. package/dist/src/health.d.ts +132 -0
  92. package/dist/src/health.js +435 -0
  93. package/dist/src/health.js.map +1 -0
  94. package/dist/src/inbox.d.ts +70 -0
  95. package/dist/src/inbox.js +218 -0
  96. package/dist/src/inbox.js.map +1 -0
  97. package/dist/src/index.d.ts +33 -0
  98. package/dist/src/index.js +38 -0
  99. package/dist/src/index.js.map +1 -0
  100. package/dist/src/install-hooks.d.ts +13 -0
  101. package/dist/src/install-hooks.js +88 -0
  102. package/dist/src/install-hooks.js.map +1 -0
  103. package/dist/src/integrity.d.ts +59 -0
  104. package/dist/src/integrity.js +206 -0
  105. package/dist/src/integrity.js.map +1 -0
  106. package/dist/src/intelligence.d.ts +104 -0
  107. package/dist/src/intelligence.js +391 -0
  108. package/dist/src/intelligence.js.map +1 -0
  109. package/dist/src/ledger.d.ts +54 -0
  110. package/dist/src/ledger.js +179 -0
  111. package/dist/src/ledger.js.map +1 -0
  112. package/dist/src/living-docs.d.ts +14 -0
  113. package/dist/src/living-docs.js +197 -0
  114. package/dist/src/living-docs.js.map +1 -0
  115. package/dist/src/memory-tiers.d.ts +37 -0
  116. package/dist/src/memory-tiers.js +160 -0
  117. package/dist/src/memory-tiers.js.map +1 -0
  118. package/dist/src/model-profiles.d.ts +65 -0
  119. package/dist/src/model-profiles.js +568 -0
  120. package/dist/src/model-profiles.js.map +1 -0
  121. package/dist/src/models.d.ts +58 -0
  122. package/dist/src/models.js +327 -0
  123. package/dist/src/models.js.map +1 -0
  124. package/dist/src/narrative.d.ts +54 -0
  125. package/dist/src/narrative.js +163 -0
  126. package/dist/src/narrative.js.map +1 -0
  127. package/dist/src/nextstep.d.ts +16 -0
  128. package/dist/src/nextstep.js +103 -0
  129. package/dist/src/nextstep.js.map +1 -0
  130. package/dist/src/observer.d.ts +18 -0
  131. package/dist/src/observer.js +251 -0
  132. package/dist/src/observer.js.map +1 -0
  133. package/dist/src/outcome.d.ts +110 -0
  134. package/dist/src/outcome.js +377 -0
  135. package/dist/src/outcome.js.map +1 -0
  136. package/dist/src/pipeline.d.ts +167 -0
  137. package/dist/src/pipeline.js +1503 -0
  138. package/dist/src/pipeline.js.map +1 -0
  139. package/dist/src/playbook.d.ts +59 -0
  140. package/dist/src/playbook.js +238 -0
  141. package/dist/src/playbook.js.map +1 -0
  142. package/dist/src/pr-agent.d.ts +97 -0
  143. package/dist/src/pr-agent.js +195 -0
  144. package/dist/src/pr-agent.js.map +1 -0
  145. package/dist/src/predictive.d.ts +57 -0
  146. package/dist/src/predictive.js +230 -0
  147. package/dist/src/predictive.js.map +1 -0
  148. package/dist/src/profile.d.ts +294 -0
  149. package/dist/src/profile.js +1347 -0
  150. package/dist/src/profile.js.map +1 -0
  151. package/dist/src/prompt-audit.d.ts +22 -0
  152. package/dist/src/prompt-audit.js +194 -0
  153. package/dist/src/prompt-audit.js.map +1 -0
  154. package/dist/src/prompt-intel.d.ts +12 -0
  155. package/dist/src/prompt-intel.js +321 -0
  156. package/dist/src/prompt-intel.js.map +1 -0
  157. package/dist/src/provider-context.d.ts +121 -0
  158. package/dist/src/provider-context.js +222 -0
  159. package/dist/src/provider-context.js.map +1 -0
  160. package/dist/src/provider-manager.d.ts +92 -0
  161. package/dist/src/provider-manager.js +428 -0
  162. package/dist/src/provider-manager.js.map +1 -0
  163. package/dist/src/receipt.d.ts +87 -0
  164. package/dist/src/receipt.js +326 -0
  165. package/dist/src/receipt.js.map +1 -0
  166. package/dist/src/recommendations.d.ts +13 -0
  167. package/dist/src/recommendations.js +291 -0
  168. package/dist/src/recommendations.js.map +1 -0
  169. package/dist/src/redact.d.ts +15 -0
  170. package/dist/src/redact.js +129 -0
  171. package/dist/src/redact.js.map +1 -0
  172. package/dist/src/replit.d.ts +397 -0
  173. package/dist/src/replit.js +1160 -0
  174. package/dist/src/replit.js.map +1 -0
  175. package/dist/src/repo.d.ts +149 -0
  176. package/dist/src/repo.js +416 -0
  177. package/dist/src/repo.js.map +1 -0
  178. package/dist/src/revert.d.ts +30 -0
  179. package/dist/src/revert.js +166 -0
  180. package/dist/src/revert.js.map +1 -0
  181. package/dist/src/room.d.ts +102 -0
  182. package/dist/src/room.js +212 -0
  183. package/dist/src/room.js.map +1 -0
  184. package/dist/src/routing-advisor.d.ts +57 -0
  185. package/dist/src/routing-advisor.js +221 -0
  186. package/dist/src/routing-advisor.js.map +1 -0
  187. package/dist/src/self-correct.d.ts +40 -0
  188. package/dist/src/self-correct.js +137 -0
  189. package/dist/src/self-correct.js.map +1 -0
  190. package/dist/src/session-lock.d.ts +35 -0
  191. package/dist/src/session-lock.js +134 -0
  192. package/dist/src/session-lock.js.map +1 -0
  193. package/dist/src/session.d.ts +267 -0
  194. package/dist/src/session.js +1660 -0
  195. package/dist/src/session.js.map +1 -0
  196. package/dist/src/settings-tui.d.ts +5 -0
  197. package/dist/src/settings-tui.js +422 -0
  198. package/dist/src/settings-tui.js.map +1 -0
  199. package/dist/src/setup-flow.d.ts +63 -0
  200. package/dist/src/setup-flow.js +233 -0
  201. package/dist/src/setup-flow.js.map +1 -0
  202. package/dist/src/signal.d.ts +19 -0
  203. package/dist/src/signal.js +122 -0
  204. package/dist/src/signal.js.map +1 -0
  205. package/dist/src/simmer.d.ts +85 -0
  206. package/dist/src/simmer.js +224 -0
  207. package/dist/src/simmer.js.map +1 -0
  208. package/dist/src/state-export.d.ts +129 -0
  209. package/dist/src/state-export.js +233 -0
  210. package/dist/src/state-export.js.map +1 -0
  211. package/dist/src/strategy.d.ts +54 -0
  212. package/dist/src/strategy.js +95 -0
  213. package/dist/src/strategy.js.map +1 -0
  214. package/dist/src/subscription.d.ts +40 -0
  215. package/dist/src/subscription.js +189 -0
  216. package/dist/src/subscription.js.map +1 -0
  217. package/dist/src/templates.d.ts +208 -0
  218. package/dist/src/templates.js +238 -0
  219. package/dist/src/templates.js.map +1 -0
  220. package/dist/src/test.d.ts +9 -0
  221. package/dist/src/test.js +1173 -0
  222. package/dist/src/test.js.map +1 -0
  223. package/dist/src/think-engine.d.ts +67 -0
  224. package/dist/src/think-engine.js +412 -0
  225. package/dist/src/think-engine.js.map +1 -0
  226. package/dist/src/tui.d.ts +71 -0
  227. package/dist/src/tui.js +242 -0
  228. package/dist/src/tui.js.map +1 -0
  229. package/dist/src/types.d.ts +177 -0
  230. package/dist/src/types.js +6 -0
  231. package/dist/src/types.js.map +1 -0
  232. package/dist/src/update-check.d.ts +7 -0
  233. package/dist/src/update-check.js +36 -0
  234. package/dist/src/update-check.js.map +1 -0
  235. package/dist/src/wave-planner.d.ts +30 -0
  236. package/dist/src/wave-planner.js +281 -0
  237. package/dist/src/wave-planner.js.map +1 -0
  238. package/hooks/head-guard.sh +41 -0
  239. package/hooks/task-classifier.mjs +328 -0
  240. package/hooks/vibe-router.mjs +387 -0
  241. package/package.json +29 -153
  242. package/src/agents/registry.mjs +0 -405
  243. package/src/awareness.mjs +0 -425
  244. package/src/brief.mjs +0 -266
  245. package/src/calibration.mjs +0 -148
  246. package/src/checkpoint.mjs +0 -109
  247. package/src/ci-triage.mjs +0 -191
  248. package/src/cognitive-loop.mjs +0 -562
  249. package/src/collaboration.mjs +0 -545
  250. package/src/context-intel.mjs +0 -158
  251. package/src/context.mjs +0 -389
  252. package/src/continuity.mjs +0 -298
  253. package/src/cost-tracker.mjs +0 -184
  254. package/src/debrief.mjs +0 -228
  255. package/src/decide.mjs +0 -1099
  256. package/src/decompose.mjs +0 -331
  257. package/src/detect.mjs +0 -702
  258. package/src/dispatch.mjs +0 -1447
  259. package/src/doctor.mjs +0 -1607
  260. package/src/envelope.mjs +0 -139
  261. package/src/failure-memory.mjs +0 -178
  262. package/src/fx.mjs +0 -276
  263. package/src/governance.mjs +0 -279
  264. package/src/handoff.mjs +0 -87
  265. package/src/head-protocol.mjs +0 -128
  266. package/src/head.mjs +0 -952
  267. package/src/health.mjs +0 -528
  268. package/src/inbox.mjs +0 -195
  269. package/src/index.mjs +0 -44
  270. package/src/install-hooks.mjs +0 -100
  271. package/src/integrity.mjs +0 -245
  272. package/src/intelligence.mjs +0 -447
  273. package/src/ledger.mjs +0 -196
  274. package/src/living-docs.mjs +0 -210
  275. package/src/memory-tiers.mjs +0 -193
  276. package/src/models.mjs +0 -363
  277. package/src/narrative.mjs +0 -169
  278. package/src/nextstep.mjs +0 -100
  279. package/src/observer.mjs +0 -241
  280. package/src/outcome.mjs +0 -400
  281. package/src/pipeline.mjs +0 -1711
  282. package/src/playbook.mjs +0 -257
  283. package/src/pr-agent.mjs +0 -214
  284. package/src/predictive.mjs +0 -250
  285. package/src/profile.mjs +0 -1411
  286. package/src/prompt-audit.mjs +0 -231
  287. package/src/prompt-intel.mjs +0 -325
  288. package/src/provider-context.mjs +0 -257
  289. package/src/receipt.mjs +0 -344
  290. package/src/recommendations.mjs +0 -296
  291. package/src/redact.mjs +0 -192
  292. package/src/replit.mjs +0 -1210
  293. package/src/repo.mjs +0 -445
  294. package/src/revert.mjs +0 -149
  295. package/src/routing-advisor.mjs +0 -204
  296. package/src/self-correct.mjs +0 -147
  297. package/src/session-lock.mjs +0 -160
  298. package/src/session.mjs +0 -1655
  299. package/src/settings-tui.mjs +0 -373
  300. package/src/setup-flow.mjs +0 -223
  301. package/src/signal.mjs +0 -115
  302. package/src/simmer.mjs +0 -241
  303. package/src/strategy.mjs +0 -235
  304. package/src/subscription.mjs +0 -212
  305. package/src/templates.mjs +0 -260
  306. package/src/think-engine.mjs +0 -428
  307. package/src/tui.mjs +0 -276
  308. package/src/update-check.mjs +0 -35
  309. 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
- };