dual-brain 2.0.0 → 3.0.1
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/README.md +58 -43
- package/install.mjs +419 -128
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,79 +1,94 @@
|
|
|
1
1
|
# Dual-Brain Orchestrator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One command. Both brains. Auto-detected. Auto-configured.
|
|
4
|
+
|
|
5
|
+
Dual-provider orchestration for Claude Code across Claude and OpenAI subscriptions. Routes search to cheap models, execution to mid-tier, thinking to the most capable. Dispatches work to GPT via Codex CLI. Dual-brain analysis for high-risk decisions.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npx dual-brain
|
|
10
|
+
npx -y dual-brain
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
That's it. The installer auto-detects your environment:
|
|
14
|
+
- Finds Claude CLI and checks auth status
|
|
15
|
+
- Finds Codex CLI and checks auth status
|
|
16
|
+
- Detects Replit and replit-tools if present
|
|
17
|
+
- Configures dual-provider, Claude-only, or OpenAI-only mode automatically
|
|
18
|
+
- Registers hooks in `.claude/settings.json`
|
|
19
|
+
- No wizard. No restart. No manual steps.
|
|
20
|
+
|
|
21
|
+
Run it again anytime — it's idempotent. Re-detects providers, updates hooks, preserves your config.
|
|
22
|
+
|
|
23
|
+
### Unlock full features
|
|
12
24
|
|
|
13
25
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
# Claude (you probably have this already)
|
|
27
|
+
claude login
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
# OpenAI (optional — enables GPT lane + dual-brain)
|
|
30
|
+
npm i -g @openai/codex
|
|
31
|
+
codex login
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
- Registers `enforce-tier.mjs` (PreToolUse) and `cost-logger.mjs` (PostToolUse) in `.claude/settings.json`
|
|
23
|
-
- Creates a `review-rules.md` template for your project-specific GPT review rules
|
|
24
|
-
- Updates `.gitignore` to exclude usage logs and review artifacts
|
|
33
|
+
# Re-run to detect new providers
|
|
34
|
+
npx -y dual-brain
|
|
35
|
+
```
|
|
25
36
|
|
|
26
37
|
## How it works
|
|
27
38
|
|
|
28
|
-
Two hooks
|
|
39
|
+
**Two hooks fire automatically** (registered in `.claude/settings.json`):
|
|
40
|
+
|
|
41
|
+
- **enforce-tier.mjs** (PreToolUse on Agent): Classifies tasks, advises the correct model, detects duplicates, suggests cross-provider routing
|
|
42
|
+
- **cost-logger.mjs** (PostToolUse on all tools): Logs usage to daily rotated files for cost tracking
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
- **cost-logger.mjs** (PostToolUse on all tools): Logs usage data to daily rotated files for cost tracking
|
|
44
|
+
**Three tiers route work by complexity:**
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
| Tier | Claude | OpenAI | Use for |
|
|
47
|
+
|------|--------|--------|---------|
|
|
48
|
+
| Search | Haiku | GPT-4.1-mini | grep, explore, file reads |
|
|
49
|
+
| Execute | Sonnet | GPT-5.4 | edits, tests, git ops |
|
|
50
|
+
| Think | Opus | GPT-5.5 | architecture, review, planning |
|
|
34
51
|
|
|
35
|
-
-
|
|
36
|
-
- **Gate**: Catches code changes that weren't reviewed before the session ends
|
|
37
|
-
- **Cost**: Checks that dispatched subagents use the correct model tier
|
|
52
|
+
**Dual-brain** kicks in automatically for high-risk decisions — both providers think on the same problem independently.
|
|
38
53
|
|
|
39
54
|
## Scripts
|
|
40
55
|
|
|
41
56
|
| Script | Purpose |
|
|
42
57
|
|--------|---------|
|
|
43
|
-
| `hooks/setup-wizard.mjs` | Interactive setup — configure your subscription and preferences |
|
|
44
58
|
| `hooks/cost-report.mjs` | Activity & cost estimates by model tier |
|
|
45
|
-
| `hooks/dual-brain-review.mjs` | Send
|
|
59
|
+
| `hooks/dual-brain-review.mjs` | Send git diff to GPT for independent review |
|
|
46
60
|
| `hooks/dual-brain-think.mjs` | Dual-perspective analysis on architecture decisions |
|
|
47
|
-
| `hooks/quality-gate.mjs` |
|
|
48
|
-
| `hooks/budget-balancer.mjs` |
|
|
61
|
+
| `hooks/quality-gate.mjs` | Sensitivity-scored quality gate with review artifacts |
|
|
62
|
+
| `hooks/budget-balancer.mjs` | Provider balance and routing recommendations |
|
|
49
63
|
| `hooks/gpt-work-dispatcher.mjs` | Dispatch execution tasks to GPT via Codex CLI |
|
|
50
|
-
| `hooks/session-report.mjs` | Session-end summary: activity,
|
|
51
|
-
| `hooks/health-check.mjs` | Verify all hooks and dependencies are
|
|
52
|
-
| `hooks/test-orchestrator.mjs` | Self-test harness
|
|
53
|
-
| `hooks/
|
|
54
|
-
| `hooks/
|
|
55
|
-
| `hooks/cost-logger.mjs` | PostToolUse hook — logs usage data (automatic) |
|
|
64
|
+
| `hooks/session-report.mjs` | Session-end summary: activity, compliance, quality |
|
|
65
|
+
| `hooks/health-check.mjs` | Verify all hooks and dependencies are working |
|
|
66
|
+
| `hooks/test-orchestrator.mjs` | Self-test harness (14 tests) |
|
|
67
|
+
| `hooks/setup-wizard.mjs` | Interactive config (optional — for custom plans) |
|
|
68
|
+
| `hooks/install-git-hooks.mjs` | Git pre-commit hook for quality gate |
|
|
56
69
|
|
|
57
|
-
##
|
|
70
|
+
## CLI options
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
```bash
|
|
73
|
+
npx -y dual-brain # detect, configure, install
|
|
74
|
+
npx dual-brain --force # overwrite all config
|
|
75
|
+
npx dual-brain --dry-run # detect only, don't write
|
|
76
|
+
npx dual-brain --json # output detection as JSON
|
|
77
|
+
npx dual-brain --help # show help
|
|
78
|
+
```
|
|
60
79
|
|
|
61
80
|
## Customize
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
82
|
+
After install, edit these files:
|
|
83
|
+
|
|
84
|
+
- `orchestrator.json` — subscriptions, tiers, quality gate, budgets, routing
|
|
85
|
+
- `review-rules.md` — project-specific rules for GPT code review
|
|
86
|
+
- `settings.json` — hook registrations (auto-generated, safe to extend)
|
|
66
87
|
|
|
67
88
|
## Requirements
|
|
68
89
|
|
|
69
90
|
- Node 20+
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
## Works with any subscription
|
|
73
|
-
|
|
74
|
-
The setup wizard supports any combination:
|
|
75
|
-
- Claude only ($20 Pro / $100 Max / $200 Max / API)
|
|
76
|
-
- OpenAI only ($20 Plus / $100 Pro)
|
|
77
|
-
- Both providers (recommended for dual-brain features)
|
|
91
|
+
- Claude Code (any subscription tier)
|
|
92
|
+
- Codex CLI (optional) — `npm i -g @openai/codex && codex login`
|
|
78
93
|
|
|
79
|
-
|
|
94
|
+
Works with any subscription combination. Without OpenAI, GPT features gracefully degrade — all work routes through Claude.
|
package/install.mjs
CHANGED
|
@@ -1,153 +1,444 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* dual-brain —
|
|
3
|
+
* dual-brain — Dual-provider orchestrator for Claude Code.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* npx dual-brain
|
|
6
|
+
* npx -y dual-brain # auto-detect, configure, done
|
|
7
|
+
* npx dual-brain --force # overwrite existing config
|
|
8
|
+
* npx dual-brain --dry-run # detect only, don't install
|
|
7
9
|
* npx dual-brain --help
|
|
8
10
|
*/
|
|
9
11
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
10
12
|
import { dirname, join, resolve } from 'path';
|
|
11
13
|
import { fileURLToPath } from 'url';
|
|
14
|
+
import { spawnSync } from 'child_process';
|
|
12
15
|
|
|
13
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
const
|
|
15
|
-
const command = args.find(a => !a.startsWith('-'));
|
|
16
|
-
const force = args.includes('--force');
|
|
17
|
-
|
|
18
|
-
const W = 50;
|
|
19
|
-
const border = (l, r) => l + '═'.repeat(W) + r;
|
|
20
|
-
const line = (text) => {
|
|
21
|
-
const padded = String(text).padEnd(W - 2);
|
|
22
|
-
return `║ ${padded.slice(0, W - 2)} ║`;
|
|
23
|
-
};
|
|
17
|
+
const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
console.log(
|
|
35
|
-
|
|
19
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
const flag = (f) => argv.includes(f);
|
|
23
|
+
const force = flag('--force');
|
|
24
|
+
const dryRun = flag('--dry-run');
|
|
25
|
+
const jsonOut = flag('--json');
|
|
26
|
+
|
|
27
|
+
if (flag('--help') || flag('-h')) {
|
|
28
|
+
console.log(`
|
|
29
|
+
dual-brain v${VERSION} — Dual-provider orchestrator for Claude Code
|
|
30
|
+
|
|
31
|
+
Usage: npx -y dual-brain [options]
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--force Overwrite all existing config (keeps review-rules.md)
|
|
35
|
+
--dry-run Detect environment only, don't install
|
|
36
|
+
--json Output detection as JSON (implies --dry-run)
|
|
37
|
+
--help Show this help
|
|
38
|
+
`);
|
|
36
39
|
process.exit(0);
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
// Silently accept 'init' for backward compat
|
|
43
|
+
const positional = argv.filter(a => !a.startsWith('-'));
|
|
44
|
+
if (positional.length > 0 && positional[0] !== 'init') {
|
|
45
|
+
console.error(` Unknown command: ${positional[0]}`);
|
|
41
46
|
console.error(' Run: npx dual-brain --help');
|
|
42
47
|
process.exit(1);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
// ─── Box Drawing ────────────────────────────────────────────────────────────
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const W = 54;
|
|
53
|
+
const pad = (s, len = W - 2) => {
|
|
54
|
+
s = String(s);
|
|
55
|
+
return s.length >= len ? s.slice(0, len) : s + ' '.repeat(len - s.length);
|
|
56
|
+
};
|
|
57
|
+
const ln = (s) => `║ ${pad(s)} ║`;
|
|
58
|
+
const br = (l, r) => l + '═'.repeat(W) + r;
|
|
59
|
+
const sep = () => '╠' + '═'.repeat(W) + '╣';
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
// ─── Detection ──────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function run(cmd, args, opts = {}) {
|
|
64
|
+
return spawnSync(cmd, args, {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
67
|
+
timeout: 8000,
|
|
68
|
+
...opts,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectReplit() {
|
|
73
|
+
const isReplit = !!(process.env.REPL_ID || process.env.REPL_SLUG);
|
|
74
|
+
const hasReplitTools = existsSync(resolve(process.cwd(), '.replit-tools'));
|
|
75
|
+
return { isReplit, hasReplitTools };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectClaude() {
|
|
79
|
+
const result = { installed: false, version: null, authed: false };
|
|
80
|
+
|
|
81
|
+
const ver = run('claude', ['--version']);
|
|
82
|
+
if (ver.status === 0 && ver.stdout.trim()) {
|
|
83
|
+
result.installed = true;
|
|
84
|
+
result.version = ver.stdout.trim().split('\n')[0];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!result.installed) {
|
|
88
|
+
const which = run('which', ['claude']);
|
|
89
|
+
if (which.status === 0 && which.stdout.trim()) result.installed = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const credPaths = [
|
|
93
|
+
join(process.env.HOME || '', '.claude', '.credentials.json'),
|
|
94
|
+
join(process.env.HOME || '', '.claude', 'credentials.json'),
|
|
95
|
+
resolve(process.cwd(), '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
96
|
+
];
|
|
97
|
+
for (const p of credPaths) {
|
|
98
|
+
try {
|
|
99
|
+
const cred = JSON.parse(readFileSync(p, 'utf8'));
|
|
100
|
+
if (cred.claudeAiOauth || cred.apiKey || cred.oauth_token) {
|
|
101
|
+
result.authed = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!result.authed && result.installed) {
|
|
108
|
+
const auth = run('claude', ['auth', 'status']);
|
|
109
|
+
const out = ((auth.stdout || '') + (auth.stderr || '')).toLowerCase();
|
|
110
|
+
if (out.includes('logged in') || out.includes('authenticated') || out.includes('valid')) {
|
|
111
|
+
result.authed = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
59
116
|
}
|
|
60
117
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
function detectCodex() {
|
|
119
|
+
const result = { installed: false, version: null, authed: false, path: null };
|
|
120
|
+
|
|
121
|
+
const which = run('which', ['codex']);
|
|
122
|
+
if (which.status === 0 && which.stdout.trim()) {
|
|
123
|
+
result.path = which.stdout.trim();
|
|
124
|
+
result.installed = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!result.installed) {
|
|
128
|
+
const home = process.env.HOME || '';
|
|
129
|
+
const fallbacks = [
|
|
130
|
+
join(home, '.local', 'bin', 'codex'),
|
|
131
|
+
join(home, 'bin', 'codex'),
|
|
132
|
+
'/usr/local/bin/codex',
|
|
133
|
+
];
|
|
134
|
+
for (const p of fallbacks) {
|
|
135
|
+
if (existsSync(p)) { result.path = p; result.installed = true; break; }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (result.installed && result.path) {
|
|
140
|
+
const ver = run(result.path, ['--version']);
|
|
141
|
+
if (ver.status === 0) result.version = ver.stdout.trim().split('\n')[0];
|
|
142
|
+
|
|
143
|
+
const login = run(result.path, ['login', 'status']);
|
|
144
|
+
const out = ((login.stdout || '') + (login.stderr || '')).toLowerCase();
|
|
145
|
+
if (login.status === 0 || out.includes('logged in') || out.includes('authenticated')) {
|
|
146
|
+
result.authed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function detectExisting(workspace) {
|
|
154
|
+
const claude = resolve(workspace, '.claude');
|
|
155
|
+
return {
|
|
156
|
+
hasClaudeDir: existsSync(claude),
|
|
157
|
+
hasOrchestrator: existsSync(join(claude, 'orchestrator.json')),
|
|
158
|
+
hasSettings: existsSync(join(claude, 'settings.json')),
|
|
159
|
+
hasHooks: existsSync(join(claude, 'hooks', 'enforce-tier.mjs')),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function detectEnvironment() {
|
|
164
|
+
return {
|
|
165
|
+
...detectReplit(),
|
|
166
|
+
claude: detectClaude(),
|
|
167
|
+
codex: detectCodex(),
|
|
168
|
+
existing: detectExisting(process.cwd()),
|
|
169
|
+
workspace: resolve(process.cwd()),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Mode Resolution ────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function resolveMode(env) {
|
|
176
|
+
const c = env.claude.authed || env.claude.installed;
|
|
177
|
+
const o = env.codex.authed;
|
|
178
|
+
if (c && o) return { mode: 'dual', claudeEnabled: true, openaiEnabled: true };
|
|
179
|
+
if (c) return { mode: 'claude-only', claudeEnabled: true, openaiEnabled: false };
|
|
180
|
+
if (o) return { mode: 'openai-only', claudeEnabled: false, openaiEnabled: true };
|
|
181
|
+
return { mode: 'detect-only', claudeEnabled: true, openaiEnabled: false };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const MODE_LABELS = {
|
|
185
|
+
'dual': 'dual-provider (full features)',
|
|
186
|
+
'claude-only': 'Claude only (GPT features available when Codex authed)',
|
|
187
|
+
'openai-only': 'OpenAI + Claude (auth Claude for full features)',
|
|
188
|
+
'detect-only': 'hooks installed (auth providers to activate)',
|
|
114
189
|
};
|
|
115
190
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
// ─── Config Generation ──────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
function generateOrchestrator(mode, workspace) {
|
|
194
|
+
const template = JSON.parse(readFileSync(join(__dirname, 'orchestrator.json'), 'utf8'));
|
|
195
|
+
const existing = {};
|
|
196
|
+
const existingPath = join(workspace, '.claude', 'orchestrator.json');
|
|
197
|
+
try { Object.assign(existing, JSON.parse(readFileSync(existingPath, 'utf8'))); } catch {}
|
|
198
|
+
|
|
199
|
+
const config = force ? { ...template } : { ...template, ...existing };
|
|
200
|
+
|
|
201
|
+
config.providers = config.providers || template.providers;
|
|
202
|
+
config.providers.claude = { ...(template.providers?.claude || {}), ...(config.providers?.claude || {}) };
|
|
203
|
+
config.providers.openai = { ...(template.providers?.openai || {}), ...(config.providers?.openai || {}) };
|
|
204
|
+
config.providers.claude.enabled = mode.claudeEnabled;
|
|
205
|
+
config.providers.openai.enabled = mode.openaiEnabled;
|
|
206
|
+
|
|
207
|
+
config.dual_thinking = config.dual_thinking || template.dual_thinking;
|
|
208
|
+
config.dual_thinking.enabled = mode.mode === 'dual';
|
|
209
|
+
|
|
210
|
+
config.subscriptions = config.subscriptions || template.subscriptions;
|
|
211
|
+
config.model_intelligence = config.model_intelligence || template.model_intelligence;
|
|
212
|
+
config.tiers = config.tiers || template.tiers;
|
|
213
|
+
config.quality_gate = force ? template.quality_gate : (config.quality_gate || template.quality_gate);
|
|
214
|
+
config.routing_rules = force ? template.routing_rules : (config.routing_rules || template.routing_rules);
|
|
215
|
+
config.budgets = force ? template.budgets : (config.budgets || template.budgets);
|
|
216
|
+
config.routing = force ? template.routing : (config.routing || template.routing);
|
|
217
|
+
config.codex_skills = template.codex_skills;
|
|
218
|
+
config.pricing_verified = new Date().toISOString().slice(0, 10);
|
|
219
|
+
|
|
220
|
+
return config;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function generateSettings(workspace) {
|
|
224
|
+
const settingsPath = join(workspace, '.claude', 'settings.json');
|
|
225
|
+
let existing = {};
|
|
226
|
+
try { existing = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch {}
|
|
227
|
+
|
|
228
|
+
const hooks = {
|
|
229
|
+
PreToolUse: [
|
|
230
|
+
{
|
|
231
|
+
matcher: 'Agent',
|
|
232
|
+
hooks: [{ type: 'command', command: 'node .claude/hooks/enforce-tier.mjs' }],
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
PostToolUse: [
|
|
236
|
+
{
|
|
237
|
+
matcher: '',
|
|
238
|
+
hooks: [{ type: 'command', command: 'node .claude/hooks/cost-logger.mjs' }],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return { ...existing, hooks };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function generateClaudeMd(mode) {
|
|
247
|
+
let md = readFileSync(join(__dirname, 'CLAUDE.md'), 'utf8');
|
|
248
|
+
|
|
249
|
+
if (mode.mode === 'claude-only') {
|
|
250
|
+
md = md.replace(
|
|
251
|
+
/## GPT Lane[\s\S]*?(?=## )/,
|
|
252
|
+
'## GPT Lane\n\nGPT features activate automatically when Codex CLI is authenticated (`npm i -g @openai/codex && codex login`).\n\n'
|
|
253
|
+
);
|
|
254
|
+
} else if (mode.mode === 'detect-only') {
|
|
255
|
+
md = '# Dual-Brain Orchestrator\n\nHooks installed but no providers authenticated yet.\nRun `npx dual-brain` again after authenticating Claude or Codex.\n\n' + md.split('\n').slice(3).join('\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return md;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function generateGitignoreEntries(workspace) {
|
|
262
|
+
const entries = [
|
|
263
|
+
'.claude/hooks/usage-*.jsonl',
|
|
264
|
+
'.claude/hooks/usage.jsonl',
|
|
265
|
+
'.claude/reviews/',
|
|
266
|
+
'.claude/hooks/.drift-warned',
|
|
267
|
+
'.claude/hooks/.budget-alerted',
|
|
268
|
+
];
|
|
269
|
+
let existing = '';
|
|
270
|
+
try { existing = readFileSync(join(workspace, '.gitignore'), 'utf8'); } catch {}
|
|
271
|
+
const needed = entries.filter(e => !existing.includes(e));
|
|
272
|
+
return { existing, needed };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─── Installation ───────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
function install(workspace, env, mode) {
|
|
278
|
+
const target = join(workspace, '.claude');
|
|
279
|
+
const actions = [];
|
|
280
|
+
|
|
281
|
+
mkdirSync(join(target, 'hooks'), { recursive: true });
|
|
282
|
+
|
|
283
|
+
const HOOKS = [
|
|
284
|
+
'enforce-tier.mjs', 'cost-logger.mjs', 'cost-report.mjs',
|
|
285
|
+
'dual-brain-review.mjs', 'dual-brain-think.mjs', 'quality-gate.mjs',
|
|
286
|
+
'test-orchestrator.mjs', 'setup-wizard.mjs', 'health-check.mjs',
|
|
287
|
+
'install-git-hooks.mjs', 'session-report.mjs', 'budget-balancer.mjs',
|
|
288
|
+
'gpt-work-dispatcher.mjs',
|
|
289
|
+
];
|
|
290
|
+
for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
|
|
291
|
+
actions.push(`✓ ${HOOKS.length} hook scripts`);
|
|
292
|
+
|
|
293
|
+
const RULES = [
|
|
294
|
+
'hookify.orchestrator-route.local.md',
|
|
295
|
+
'hookify.orchestrator-gate.local.md',
|
|
296
|
+
'hookify.orchestrator-cost.local.md',
|
|
297
|
+
];
|
|
298
|
+
for (const r of RULES) cpSync(join(__dirname, r), join(target, r));
|
|
299
|
+
actions.push(`✓ ${RULES.length} hookify rules`);
|
|
300
|
+
|
|
301
|
+
const orch = generateOrchestrator(mode, workspace);
|
|
302
|
+
writeFileSync(join(target, 'orchestrator.json'), JSON.stringify(orch, null, 2) + '\n');
|
|
303
|
+
actions.push(`✓ orchestrator.json (${mode.mode})`);
|
|
304
|
+
|
|
305
|
+
const settings = generateSettings(workspace);
|
|
306
|
+
writeFileSync(join(target, 'settings.json'), JSON.stringify(settings, null, 2) + '\n');
|
|
307
|
+
actions.push('✓ settings.json (hooks registered)');
|
|
308
|
+
|
|
309
|
+
const claudeMd = generateClaudeMd(mode);
|
|
310
|
+
writeFileSync(join(target, 'CLAUDE.md'), claudeMd);
|
|
311
|
+
actions.push('✓ CLAUDE.md (session instructions)');
|
|
312
|
+
|
|
313
|
+
const rulesTarget = join(target, 'review-rules.md');
|
|
314
|
+
if (!existsSync(rulesTarget) || force) {
|
|
315
|
+
cpSync(join(__dirname, 'review-rules.md'), rulesTarget);
|
|
316
|
+
actions.push('✓ review-rules.md template');
|
|
317
|
+
} else {
|
|
318
|
+
actions.push('⊘ review-rules.md (kept yours)');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const { existing: gi, needed } = generateGitignoreEntries(workspace);
|
|
322
|
+
if (needed.length > 0) {
|
|
323
|
+
writeFileSync(
|
|
324
|
+
join(workspace, '.gitignore'),
|
|
325
|
+
gi + '\n# Dual-Brain Orchestrator\n' + needed.join('\n') + '\n'
|
|
326
|
+
);
|
|
327
|
+
actions.push('✓ .gitignore updated');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return actions;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ─── Status Report ──────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
function statusIcon(val) { return val ? '✓' : '✗'; }
|
|
336
|
+
|
|
337
|
+
function printReport(env, mode, actions) {
|
|
338
|
+
const lines = [];
|
|
339
|
+
|
|
340
|
+
lines.push(br('╔', '╗'));
|
|
341
|
+
lines.push(ln(`Dual-Brain Orchestrator v${VERSION}`));
|
|
342
|
+
lines.push(sep());
|
|
343
|
+
|
|
344
|
+
lines.push(ln('Environment'));
|
|
345
|
+
if (env.isReplit) {
|
|
346
|
+
lines.push(ln(` Platform: Replit${env.hasReplitTools ? ' (replit-tools detected)' : ''}`));
|
|
347
|
+
} else {
|
|
348
|
+
lines.push(ln(' Platform: standalone'));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const cVer = env.claude.version ? ` ${env.claude.version}` : '';
|
|
352
|
+
const cAuth = env.claude.authed ? 'authenticated' : env.claude.installed ? 'not authenticated' : 'not found';
|
|
353
|
+
lines.push(ln(` Claude CLI: ${statusIcon(env.claude.authed)} ${cAuth}${cVer}`));
|
|
354
|
+
|
|
355
|
+
const xVer = env.codex.version ? ` ${env.codex.version}` : '';
|
|
356
|
+
const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? 'not authenticated' : 'not found';
|
|
357
|
+
lines.push(ln(` Codex CLI: ${statusIcon(env.codex.authed)} ${xAuth}${xVer}`));
|
|
358
|
+
|
|
359
|
+
lines.push(sep());
|
|
360
|
+
lines.push(ln(`Mode: ${MODE_LABELS[mode.mode]}`));
|
|
361
|
+
|
|
362
|
+
if (actions) {
|
|
363
|
+
lines.push(sep());
|
|
364
|
+
lines.push(ln('Installed'));
|
|
365
|
+
for (const a of actions) lines.push(ln(` ${a}`));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const needsAction = !env.claude.authed || !env.codex.authed;
|
|
369
|
+
if (needsAction && mode.mode !== 'dual') {
|
|
370
|
+
lines.push(sep());
|
|
371
|
+
lines.push(ln('To unlock full features:'));
|
|
372
|
+
if (!env.claude.installed) {
|
|
373
|
+
lines.push(ln(' curl -fsSL https://claude.ai/install.sh | sh'));
|
|
374
|
+
}
|
|
375
|
+
if (!env.claude.authed) {
|
|
376
|
+
lines.push(ln(' claude login'));
|
|
377
|
+
}
|
|
378
|
+
if (!env.codex.installed) {
|
|
379
|
+
lines.push(ln(' npm i -g @openai/codex'));
|
|
380
|
+
}
|
|
381
|
+
if (!env.codex.authed && env.codex.installed) {
|
|
382
|
+
lines.push(ln(' codex login'));
|
|
383
|
+
}
|
|
384
|
+
lines.push(ln(' Then run: npx dual-brain'));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
lines.push(sep());
|
|
388
|
+
if (actions) {
|
|
389
|
+
lines.push(ln(mode.mode === 'dual'
|
|
390
|
+
? 'Ready — both providers active, no restart needed'
|
|
391
|
+
: 'Ready — hooks active, run commands above for full power'));
|
|
392
|
+
} else {
|
|
393
|
+
lines.push(ln('Dry run — no files written'));
|
|
394
|
+
}
|
|
395
|
+
lines.push(br('╚', '╝'));
|
|
396
|
+
|
|
397
|
+
console.log('');
|
|
398
|
+
for (const l of lines) console.log(` ${l}`);
|
|
399
|
+
console.log('');
|
|
400
|
+
|
|
401
|
+
if (actions) {
|
|
402
|
+
console.log(' What just happened:');
|
|
403
|
+
console.log(' Every Claude Code session in this project now auto-routes');
|
|
404
|
+
console.log(' agent work by complexity — cheap models for search, mid-tier');
|
|
405
|
+
console.log(' for execution, best models for thinking. Cost is tracked.');
|
|
406
|
+
if (mode.mode === 'dual') {
|
|
407
|
+
console.log(' Both Claude and GPT are available as work providers.');
|
|
408
|
+
}
|
|
409
|
+
console.log('');
|
|
410
|
+
console.log(' Try these in your next Claude Code session:');
|
|
411
|
+
console.log(' node .claude/hooks/health-check.mjs # verify setup');
|
|
412
|
+
console.log(' node .claude/hooks/cost-report.mjs # see activity');
|
|
413
|
+
console.log(' node .claude/hooks/budget-balancer.mjs # provider balance');
|
|
414
|
+
if (mode.openaiEnabled) {
|
|
415
|
+
console.log(' node .claude/hooks/dual-brain-review.mjs # GPT code review');
|
|
416
|
+
}
|
|
417
|
+
console.log('');
|
|
418
|
+
console.log(' Customize:');
|
|
419
|
+
console.log(' .claude/review-rules.md # your project\'s review rules');
|
|
420
|
+
console.log(' .claude/orchestrator.json # routing, budgets, tiers');
|
|
421
|
+
console.log('');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
function main() {
|
|
428
|
+
const env = detectEnvironment();
|
|
429
|
+
const mode = resolveMode(env);
|
|
430
|
+
|
|
431
|
+
if (dryRun || jsonOut) {
|
|
432
|
+
if (jsonOut) {
|
|
433
|
+
console.log(JSON.stringify({ version: VERSION, env, mode }, null, 2));
|
|
434
|
+
} else {
|
|
435
|
+
printReport(env, mode, null);
|
|
436
|
+
}
|
|
437
|
+
process.exit(0);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const actions = install(env.workspace, env, mode);
|
|
441
|
+
printReport(env, mode, actions);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
main();
|
package/package.json
CHANGED