dual-brain 4.6.0 → 4.7.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/CLAUDE.md +35 -130
- package/README.md +34 -179
- package/hooks/control-panel.mjs +379 -8
- package/hooks/cost-logger.mjs +11 -53
- package/hooks/cost-report.mjs +126 -65
- package/hooks/decision-ledger.mjs +3 -53
- package/hooks/dual-brain-review.mjs +25 -261
- package/hooks/dual-brain-think.mjs +37 -300
- package/hooks/enforce-tier.mjs +93 -265
- package/hooks/failure-detector.mjs +1 -3
- package/hooks/gpt-work-dispatcher.mjs +153 -12
- package/hooks/health-check.mjs +25 -17
- package/hooks/quality-gate.mjs +11 -6
- package/hooks/risk-classifier.mjs +2 -135
- package/hooks/session-report.mjs +71 -41
- package/hooks/summary-checkpoint.mjs +8 -35
- package/hooks/test-orchestrator.mjs +31 -2080
- package/install.mjs +628 -1557
- package/orchestrator.json +96 -73
- package/package.json +2 -7
- package/hooks/agent-chains.mjs +0 -369
- package/hooks/agent-templates.mjs +0 -441
- package/hooks/atomic-write.mjs +0 -109
- package/hooks/config-validator.mjs +0 -156
- package/hooks/confirmation-policy.mjs +0 -167
- package/hooks/error-channel.mjs +0 -68
- package/hooks/ship-captain.mjs +0 -1176
- package/hooks/ship-gate.mjs +0 -971
package/install.mjs
CHANGED
|
@@ -4,18 +4,22 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* npx -y dual-brain # auto-detect, configure, done
|
|
7
|
+
* npx -y dual-brain update # refresh hooks to latest package version
|
|
7
8
|
* npx dual-brain --force # overwrite existing config
|
|
8
9
|
* npx dual-brain --dry-run # detect only, don't install
|
|
9
10
|
* npx dual-brain --help
|
|
10
11
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
12
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
13
|
+
import { createInterface } from 'readline';
|
|
13
14
|
import { dirname, join, resolve } from 'path';
|
|
14
15
|
import { fileURLToPath } from 'url';
|
|
15
16
|
import { spawnSync } from 'child_process';
|
|
16
17
|
|
|
17
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
|
|
20
|
+
const VERSION_STAMP_FILE = '.claude/dual-brain.version.json';
|
|
21
|
+
const UPDATE_CACHE_FILE = '.claude/dual-brain.update-check.json';
|
|
22
|
+
const UPDATE_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
19
23
|
|
|
20
24
|
// ─── Replit Detection ──────────────────────────────────────────────────────
|
|
21
25
|
|
|
@@ -30,14 +34,9 @@ const flag = (f) => argv.includes(f);
|
|
|
30
34
|
const force = flag('--force');
|
|
31
35
|
const dryRun = flag('--dry-run');
|
|
32
36
|
const jsonOut = flag('--json');
|
|
33
|
-
const yesFlag = flag('--yes') || flag('-y');
|
|
34
|
-
const noLaunch = flag('--no-launch');
|
|
35
37
|
const positional = argv.filter(a => !a.startsWith('-'));
|
|
36
38
|
const subcommand = positional[0] || null;
|
|
37
39
|
|
|
38
|
-
const BRAND = `Data Tools — Dual Brain v${VERSION}`;
|
|
39
|
-
const BRAND_SUBTITLE = 'Built on replit-tools by Steve Moraco';
|
|
40
|
-
|
|
41
40
|
if (flag('--version') || flag('-v')) {
|
|
42
41
|
console.log(`dual-brain v${VERSION}`);
|
|
43
42
|
process.exit(0);
|
|
@@ -45,107 +44,47 @@ if (flag('--version') || flag('-v')) {
|
|
|
45
44
|
|
|
46
45
|
if (flag('--help') || flag('-h')) {
|
|
47
46
|
console.log(`
|
|
48
|
-
${
|
|
49
|
-
${BRAND_SUBTITLE}
|
|
47
|
+
🧠 dual-brain v${VERSION} — Dual-provider orchestrator for Claude Code
|
|
50
48
|
|
|
51
49
|
Usage: npx -y dual-brain [command] [options]
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
(none)
|
|
51
|
+
⌨️ Commands:
|
|
52
|
+
(none) 🧠 Auto-detect and install/update orchestrator
|
|
53
|
+
status 🟢 Open live control panel
|
|
54
|
+
auth 🔑 Show, login, or refresh provider auth
|
|
55
|
+
mode 🎛️ Show or switch profile
|
|
56
|
+
budget 💵 Set session/daily spend limits
|
|
57
|
+
explain 🧭 Explain last routing decision
|
|
58
|
+
update 🔄 Force re-install of latest hooks
|
|
55
59
|
init Alias for default install
|
|
56
|
-
start Start persistent chat
|
|
57
|
-
chat Alias for start
|
|
58
|
-
auth Check and repair provider authentication
|
|
59
|
-
setup Configure providers, hooks, and preferences
|
|
60
|
-
doctor Check system health and report issues
|
|
61
|
-
reset Clear all state files (keeps config/hooks)
|
|
62
|
-
repair Fix corrupt files, stale locks, re-register hooks
|
|
63
|
-
--uninstall Remove dual-brain hooks and state files
|
|
64
|
-
|
|
65
|
-
Status:
|
|
66
|
-
status Open live control panel
|
|
67
|
-
health Verify system health
|
|
68
|
-
budget Show or set session/daily spend limits
|
|
69
|
-
cost Activity and cost estimates
|
|
70
|
-
report Generate session report
|
|
71
|
-
|
|
72
|
-
Routing:
|
|
73
|
-
mode Show or switch profile
|
|
74
|
-
explain Explain last routing decision
|
|
75
|
-
gate Run quality gate
|
|
76
|
-
ledger Routing outcome insights
|
|
77
|
-
|
|
78
|
-
GPT:
|
|
79
|
-
think Dual-brain think (--question "..." [--round 2])
|
|
80
|
-
review Dual-brain code review
|
|
81
|
-
dispatch Dispatch work to GPT via Codex CLI
|
|
82
|
-
|
|
83
|
-
Vibe:
|
|
84
|
-
vibe Decompose casual requests into structured work
|
|
85
|
-
plan Generate execution plans (--utterance "...")
|
|
86
|
-
memory Persistent preferences and work threads
|
|
87
|
-
|
|
88
|
-
Agents:
|
|
89
|
-
agent Run a specialist agent template (explorer, security-review, test-writer, bug-hunter)
|
|
90
|
-
agents List all available agent templates
|
|
91
|
-
chain Run a built-in agent chain (explore-then-fix, review-and-test, audit-and-plan)
|
|
92
|
-
chains List all available agent chains
|
|
93
|
-
|
|
94
|
-
Ship Captain:
|
|
95
|
-
do "<goal>" Run full pipeline: agents → tests → gate → PR
|
|
96
|
-
--yolo Skip all confirmations
|
|
97
|
-
--careful Confirm every step
|
|
98
|
-
--plan-only Show plan without executing
|
|
99
|
-
--no-pr Skip PR creation
|
|
100
|
-
ship Create branch, run tests, open PR
|
|
101
|
-
test-run Discover and run project tests
|
|
102
|
-
diff Show current changes summary
|
|
103
|
-
runs List recent Ship Captain runs
|
|
104
|
-
resume Resume last incomplete run
|
|
105
|
-
|
|
106
|
-
Dev:
|
|
107
|
-
test Run self-tests
|
|
108
60
|
|
|
109
61
|
Options:
|
|
110
62
|
--force Overwrite all existing config
|
|
111
63
|
--dry-run Detect environment only
|
|
112
64
|
--json Output detection as JSON
|
|
113
|
-
--no-launch Install/setup only, do not open Claude
|
|
114
65
|
--help Show this help
|
|
115
66
|
|
|
116
|
-
Routing modes:
|
|
117
|
-
Auto (default) Adapts routing based on risk, health, outcomes
|
|
118
|
-
Balanced Auto-routes, uses both providers evenly
|
|
119
|
-
Conservative Fewer GPT dispatches, sticks to Claude
|
|
120
|
-
Aggressive Maximizes both subscriptions, dual-brain for medium+
|
|
67
|
+
🎛️ Routing modes:
|
|
68
|
+
🤖 Auto (default) Adapts routing based on risk, health, outcomes
|
|
69
|
+
⚖️ Balanced Auto-routes, uses both providers evenly
|
|
70
|
+
🛡️ Conservative Fewer GPT dispatches, sticks to Claude
|
|
71
|
+
🚀 Aggressive Maximizes both subscriptions, dual-brain for medium+
|
|
121
72
|
|
|
122
|
-
Examples:
|
|
123
|
-
${cmd('npx dual-brain')} #
|
|
124
|
-
${cmd('npx dual-brain setup')} # configure providers and hooks
|
|
73
|
+
🚀 Examples:
|
|
74
|
+
${cmd('npx dual-brain')} # install or update
|
|
125
75
|
${cmd('npx dual-brain status')} # open control panel
|
|
76
|
+
${cmd('npx dual-brain auth')} # show Claude/Codex auth
|
|
77
|
+
${cmd('npx dual-brain auth login')} # sign in missing providers
|
|
78
|
+
${cmd('npx dual-brain auth refresh')} # refresh provider auth
|
|
126
79
|
${cmd('npx dual-brain mode cost-saver')} # switch profile
|
|
127
80
|
${cmd('npx dual-brain budget 8 25')} # \$8 session / \$25 daily
|
|
128
|
-
${cmd('npx dual-brain
|
|
129
|
-
${cmd('npx dual-brain
|
|
130
|
-
${cmd('npx dual-brain agents')} # list agent templates
|
|
131
|
-
${cmd('npx dual-brain agent explorer --question "where is auth handled?"')}
|
|
132
|
-
${cmd('npx dual-brain agent security-review --scope src/auth --severity high')}
|
|
133
|
-
${cmd('npx dual-brain chains')} # list available chains
|
|
134
|
-
${cmd('npx dual-brain chain explore-then-fix --question "auth bug" --scope "src/auth"')}
|
|
81
|
+
${cmd('npx dual-brain explain')} # last routing decision
|
|
82
|
+
${cmd('npx dual-brain update')} # refresh installed hooks
|
|
135
83
|
`);
|
|
136
84
|
process.exit(0);
|
|
137
85
|
}
|
|
138
86
|
|
|
139
|
-
const SUBCOMMANDS = [
|
|
140
|
-
'init', 'setup', 'status', 'mode', 'budget', 'explain',
|
|
141
|
-
'review', 'think', 'health', 'report', 'gate',
|
|
142
|
-
'vibe', 'plan', 'cost', 'dispatch', 'memory',
|
|
143
|
-
'test', 'ledger', 'doctor', 'reset', 'repair',
|
|
144
|
-
'auth', 'start', 'chat',
|
|
145
|
-
'chain', 'chains',
|
|
146
|
-
'agent', 'agents',
|
|
147
|
-
'do', 'ship', 'test-run', 'diff', 'runs', 'resume',
|
|
148
|
-
];
|
|
87
|
+
const SUBCOMMANDS = ['init', 'status', 'auth', 'mode', 'budget', 'explain', 'update'];
|
|
149
88
|
if (subcommand && !SUBCOMMANDS.includes(subcommand)) {
|
|
150
89
|
console.error(` Unknown command: ${subcommand}`);
|
|
151
90
|
console.error(` Run: ${cmd('npx dual-brain --help')}`);
|
|
@@ -174,54 +113,18 @@ function run(cmd, args, opts = {}) {
|
|
|
174
113
|
});
|
|
175
114
|
}
|
|
176
115
|
|
|
177
|
-
function
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const SESSION_PREFS_REL = '.claude/dual-brain.session.json';
|
|
186
|
-
const DEFAULT_SESSION_PREFS = {
|
|
187
|
-
head_model: 'sonnet',
|
|
188
|
-
effort: 'medium',
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
function writeAtomicJson(targetPath, data) {
|
|
192
|
-
const tmpPath = `${targetPath}.tmp.${process.pid}`;
|
|
193
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
|
|
194
|
-
renameSync(tmpPath, targetPath);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function loadSessionPrefs(workspace) {
|
|
198
|
-
const prefsPath = join(workspace, SESSION_PREFS_REL);
|
|
199
|
-
try {
|
|
200
|
-
const raw = JSON.parse(readFileSync(prefsPath, 'utf8'));
|
|
201
|
-
return { ...DEFAULT_SESSION_PREFS, ...raw };
|
|
202
|
-
} catch {
|
|
203
|
-
return { ...DEFAULT_SESSION_PREFS };
|
|
116
|
+
function compareVersions(a, b) {
|
|
117
|
+
const aParts = String(a || '').replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
118
|
+
const bParts = String(b || '').replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
119
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
120
|
+
for (let i = 0; i < len; i++) {
|
|
121
|
+
const diff = (aParts[i] || 0) - (bParts[i] || 0);
|
|
122
|
+
if (diff !== 0) return diff;
|
|
204
123
|
}
|
|
124
|
+
return 0;
|
|
205
125
|
}
|
|
206
126
|
|
|
207
|
-
function
|
|
208
|
-
const claudeDir = join(workspace, '.claude');
|
|
209
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
210
|
-
const prefsPath = join(workspace, SESSION_PREFS_REL);
|
|
211
|
-
writeAtomicJson(prefsPath, { ...DEFAULT_SESSION_PREFS, ...prefs });
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function getClaudeCredentialPaths(workspace = process.cwd()) {
|
|
215
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
216
|
-
const home = process.env.HOME || '';
|
|
217
|
-
return [
|
|
218
|
-
configDir ? join(configDir, '.credentials.json') : null,
|
|
219
|
-
join(home, '.claude', '.credentials.json'),
|
|
220
|
-
resolve(workspace, '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
221
|
-
].filter(Boolean);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function safeParseJson(path) {
|
|
127
|
+
function readJsonFile(path) {
|
|
225
128
|
try {
|
|
226
129
|
return JSON.parse(readFileSync(path, 'utf8'));
|
|
227
130
|
} catch {
|
|
@@ -229,189 +132,106 @@ function safeParseJson(path) {
|
|
|
229
132
|
}
|
|
230
133
|
}
|
|
231
134
|
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
const parts = token.split('.');
|
|
235
|
-
if (parts.length < 2) return null;
|
|
236
|
-
try {
|
|
237
|
-
return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
238
|
-
} catch {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function computeExpiresInHours(expiresAtMs) {
|
|
244
|
-
if (!Number.isFinite(expiresAtMs)) return null;
|
|
245
|
-
return Math.round(((expiresAtMs - Date.now()) / 3_600_000) * 10) / 10;
|
|
135
|
+
function writeJsonFile(path, value) {
|
|
136
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
|
|
246
137
|
}
|
|
247
138
|
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
if (expiresInHours <= 0) return 'expired';
|
|
251
|
-
return `${Math.round(expiresInHours)}h remaining`;
|
|
139
|
+
function getVersionStamp(workspace) {
|
|
140
|
+
return readJsonFile(join(workspace, VERSION_STAMP_FILE));
|
|
252
141
|
}
|
|
253
142
|
|
|
254
|
-
function
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
143
|
+
function writeVersionStamp(workspace) {
|
|
144
|
+
const target = join(workspace, VERSION_STAMP_FILE);
|
|
145
|
+
const now = new Date().toISOString();
|
|
146
|
+
const existing = readJsonFile(target);
|
|
147
|
+
const stamp = {
|
|
148
|
+
version: VERSION,
|
|
149
|
+
installed_at: existing?.installed_at || now,
|
|
150
|
+
updated_at: now,
|
|
261
151
|
};
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const cred = safeParseJson(credPath);
|
|
265
|
-
const oauth = cred?.claudeAiOauth;
|
|
266
|
-
if (!oauth) continue;
|
|
267
|
-
|
|
268
|
-
const expiresAtMs = typeof oauth.expiresAt === 'number'
|
|
269
|
-
? oauth.expiresAt
|
|
270
|
-
: oauth.expiresAt
|
|
271
|
-
? Date.parse(oauth.expiresAt)
|
|
272
|
-
: NaN;
|
|
273
|
-
const expiresInHours = computeExpiresInHours(expiresAtMs);
|
|
274
|
-
const valid = Number.isFinite(expiresAtMs) ? expiresAtMs > Date.now() : false;
|
|
275
|
-
|
|
276
|
-
let subscription = null;
|
|
277
|
-
try {
|
|
278
|
-
const authOut = run('claude', ['auth', 'status', '--json']);
|
|
279
|
-
if (authOut.status === 0) {
|
|
280
|
-
const info = JSON.parse(authOut.stdout.trim());
|
|
281
|
-
subscription = info.subscriptionType || null;
|
|
282
|
-
}
|
|
283
|
-
} catch {}
|
|
284
|
-
if (!subscription) subscription = oauth.subscriptionType || null;
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
valid,
|
|
288
|
-
expiresInHours,
|
|
289
|
-
hasRefreshToken: !!oauth.refreshToken,
|
|
290
|
-
email: oauth.email || oauth.account?.email || cred.email || null,
|
|
291
|
-
subscription,
|
|
292
|
-
credPath,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return defaults;
|
|
152
|
+
writeJsonFile(target, stamp);
|
|
153
|
+
return stamp;
|
|
297
154
|
}
|
|
298
155
|
|
|
299
|
-
function
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const payload = decodeJwtPayload(accessToken);
|
|
306
|
-
const expiresAtMs = payload?.exp ? payload.exp * 1000 : NaN;
|
|
307
|
-
const expiresInHours = computeExpiresInHours(expiresAtMs);
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
valid: Number.isFinite(expiresAtMs) ? expiresAtMs > Date.now() : false,
|
|
311
|
-
expiresInHours,
|
|
312
|
-
hasRefreshToken: !!refreshToken,
|
|
313
|
-
email: payload?.email || auth?.email || auth?.user?.email || null,
|
|
314
|
-
};
|
|
156
|
+
function getUpdateCache(workspace) {
|
|
157
|
+
const cache = readJsonFile(join(workspace, UPDATE_CACHE_FILE));
|
|
158
|
+
if (!cache?.checked_at) return null;
|
|
159
|
+
const age = Date.now() - Date.parse(cache.checked_at);
|
|
160
|
+
if (!Number.isFinite(age) || age < 0 || age > UPDATE_CACHE_TTL_MS) return null;
|
|
161
|
+
return cache;
|
|
315
162
|
}
|
|
316
163
|
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
method: 'POST',
|
|
322
|
-
headers: {
|
|
323
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
324
|
-
'Content-Length': payload.length,
|
|
325
|
-
},
|
|
326
|
-
timeout: 8000,
|
|
327
|
-
}, (res) => {
|
|
328
|
-
const chunks = [];
|
|
329
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
330
|
-
res.on('end', () => {
|
|
331
|
-
const raw = Buffer.concat(chunks).toString('utf8');
|
|
332
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
333
|
-
try {
|
|
334
|
-
resolve(JSON.parse(raw));
|
|
335
|
-
} catch (err) {
|
|
336
|
-
reject(err);
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
reject(new Error(`HTTP ${res.statusCode || 0}`));
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
req.on('timeout', () => req.destroy(new Error('Request timeout')));
|
|
344
|
-
req.on('error', reject);
|
|
345
|
-
req.write(payload);
|
|
346
|
-
req.end();
|
|
164
|
+
function writeUpdateCache(workspace, result) {
|
|
165
|
+
writeJsonFile(join(workspace, UPDATE_CACHE_FILE), {
|
|
166
|
+
checked_at: new Date().toISOString(),
|
|
167
|
+
...result,
|
|
347
168
|
});
|
|
348
169
|
}
|
|
349
170
|
|
|
350
|
-
|
|
171
|
+
function checkForUpdate(workspace, { force = false } = {}) {
|
|
172
|
+
const installedStamp = getVersionStamp(workspace);
|
|
173
|
+
const installed = installedStamp?.version || VERSION;
|
|
174
|
+
|
|
175
|
+
if (!force) {
|
|
176
|
+
const cached = getUpdateCache(workspace);
|
|
177
|
+
if (cached && cached.installed === installed) {
|
|
178
|
+
return {
|
|
179
|
+
updateAvailable: !!cached.updateAvailable,
|
|
180
|
+
installed: cached.installed,
|
|
181
|
+
latest: cached.latest || installed,
|
|
182
|
+
checkedAt: cached.checked_at,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
351
187
|
try {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const refreshed = await postForm('https://console.anthropic.com/v1/oauth/token', {
|
|
357
|
-
grant_type: 'refresh_token',
|
|
358
|
-
refresh_token: oauth.refreshToken,
|
|
359
|
-
client_id: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
188
|
+
const result = spawnSync('npm', ['view', 'dual-brain', 'version', '--json'], {
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
191
|
+
timeout: 5000,
|
|
360
192
|
});
|
|
193
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
361
196
|
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
refreshToken: refreshed.refresh_token || oauth.refreshToken,
|
|
366
|
-
tokenType: refreshed.token_type || oauth.tokenType,
|
|
367
|
-
scopes: refreshed.scope || oauth.scopes,
|
|
368
|
-
expiresAt: Date.now() + ((refreshed.expires_in || 0) * 1000),
|
|
369
|
-
};
|
|
370
|
-
const updated = { ...cred, claudeAiOauth: nextOauth };
|
|
371
|
-
writeAtomicJson(credPath, updated);
|
|
197
|
+
const latestRaw = JSON.parse(result.stdout);
|
|
198
|
+
const latest = Array.isArray(latestRaw) ? latestRaw[latestRaw.length - 1] : latestRaw;
|
|
199
|
+
if (!latest) return null;
|
|
372
200
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
201
|
+
const updateAvailable = compareVersions(latest, installed) > 0;
|
|
202
|
+
const payload = { updateAvailable, installed, latest };
|
|
203
|
+
writeUpdateCache(workspace, payload);
|
|
204
|
+
return payload;
|
|
377
205
|
} catch {
|
|
378
|
-
return
|
|
206
|
+
return null;
|
|
379
207
|
}
|
|
380
208
|
}
|
|
381
209
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
refresh_token: refreshToken,
|
|
393
|
-
client_id: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
210
|
+
function performUpdate(workspace, env, mode) {
|
|
211
|
+
return install(workspace, env, mode);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function promptForUpdate(updateInfo) {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
217
|
+
rl.question(`Update available: v${updateInfo.installed} → v${updateInfo.latest}. Press [u] to update or Enter to continue. `, (answer) => {
|
|
218
|
+
rl.close();
|
|
219
|
+
resolve(answer.trim().toLowerCase() === 'u');
|
|
394
220
|
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
395
223
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
scope: refreshed.scope || tokens.scope,
|
|
403
|
-
};
|
|
404
|
-
const updated = auth?.tokens ? { ...auth, tokens: updatedTokens } : updatedTokens;
|
|
405
|
-
writeAtomicJson(authPath, updated);
|
|
224
|
+
function isLoggedInStatus(result) {
|
|
225
|
+
const out = ((result?.stdout || '') + (result?.stderr || '')).toLowerCase();
|
|
226
|
+
if (/\b(not\s+logged\s+in|unauthenticated|logged\s+out|no\s+auth)\b/.test(out)) return false;
|
|
227
|
+
return result?.status === 0 ||
|
|
228
|
+
/\b(logged\s+in|authenticated|signed\s+in|valid\s+session)\b/.test(out);
|
|
229
|
+
}
|
|
406
230
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
};
|
|
412
|
-
} catch {
|
|
413
|
-
return { success: false, expiresInHours: null };
|
|
414
|
-
}
|
|
231
|
+
function detectReplit() {
|
|
232
|
+
const isReplit = !!(process.env.REPL_ID || process.env.REPL_SLUG);
|
|
233
|
+
const hasReplitTools = existsSync(resolve(process.cwd(), '.replit-tools'));
|
|
234
|
+
return { isReplit, hasReplitTools };
|
|
415
235
|
}
|
|
416
236
|
|
|
417
237
|
function detectClaude() {
|
|
@@ -455,7 +275,7 @@ function detectClaude() {
|
|
|
455
275
|
}
|
|
456
276
|
|
|
457
277
|
function detectCodex() {
|
|
458
|
-
const result = { installed: false, version: null, authed: false, path: null };
|
|
278
|
+
const result = { installed: false, version: null, authed: false, path: null, authMethod: null };
|
|
459
279
|
|
|
460
280
|
const which = run('which', ['codex']);
|
|
461
281
|
if (which.status === 0 && which.stdout.trim()) {
|
|
@@ -480,12 +300,17 @@ function detectCodex() {
|
|
|
480
300
|
if (ver.status === 0) result.version = ver.stdout.trim().split('\n')[0];
|
|
481
301
|
|
|
482
302
|
const login = run(result.path, ['login', 'status']);
|
|
483
|
-
|
|
484
|
-
if (login.status === 0 || out.includes('logged in') || out.includes('authenticated')) {
|
|
303
|
+
if (isLoggedInStatus(login)) {
|
|
485
304
|
result.authed = true;
|
|
305
|
+
result.authMethod = 'oauth';
|
|
486
306
|
}
|
|
487
307
|
}
|
|
488
308
|
|
|
309
|
+
if (!result.authed && process.env.OPENAI_API_KEY) {
|
|
310
|
+
result.authed = true;
|
|
311
|
+
result.authMethod = 'api_key_env';
|
|
312
|
+
}
|
|
313
|
+
|
|
489
314
|
return result;
|
|
490
315
|
}
|
|
491
316
|
|
|
@@ -509,49 +334,324 @@ function detectEnvironment() {
|
|
|
509
334
|
};
|
|
510
335
|
}
|
|
511
336
|
|
|
512
|
-
|
|
513
|
-
return existsSync(resolve(workspace, '.replit-tools'));
|
|
514
|
-
}
|
|
337
|
+
// ─── NPM Token Persistence ────────────────────────────────────────────────
|
|
515
338
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (!env.claude.installed) issues.push('Claude CLI not found.');
|
|
519
|
-
else if (!env.claude.authed) issues.push('Claude CLI not authenticated.');
|
|
339
|
+
const NPM_PERSIST = resolve(process.cwd(), '.replit-tools', '.npm-persistent', '.npmrc');
|
|
340
|
+
const NPMRC_HOME = join(process.env.HOME || '', '.npmrc');
|
|
520
341
|
|
|
521
|
-
|
|
522
|
-
|
|
342
|
+
function restoreNpmToken() {
|
|
343
|
+
if (existsSync(NPMRC_HOME)) return;
|
|
344
|
+
if (!existsSync(NPM_PERSIST)) return;
|
|
345
|
+
try {
|
|
346
|
+
const content = readFileSync(NPM_PERSIST, 'utf8');
|
|
347
|
+
if (content.includes('_authToken')) {
|
|
348
|
+
writeFileSync(NPMRC_HOME, content);
|
|
349
|
+
}
|
|
350
|
+
} catch {}
|
|
351
|
+
}
|
|
523
352
|
|
|
524
|
-
|
|
353
|
+
// ─── Auth Self-Healing ─────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
function healClaudeAuth(env) {
|
|
356
|
+
if (env.claude.authed) return env;
|
|
357
|
+
const refreshScript = resolve(process.cwd(), '.replit-tools', 'scripts', 'claude-auth-refresh.sh');
|
|
358
|
+
if (existsSync(refreshScript)) {
|
|
359
|
+
const result = run('bash', [refreshScript, '--auto']);
|
|
360
|
+
const out = ((result.stdout || '') + (result.stderr || '')).toLowerCase();
|
|
361
|
+
if (out.includes('refreshed') || out.includes('success')) {
|
|
362
|
+
env.claude.authed = true;
|
|
363
|
+
console.log(' 🔄 Claude token refreshed via data-tools');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return env;
|
|
525
367
|
}
|
|
526
368
|
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
369
|
+
function healCodexAuth(env) {
|
|
370
|
+
if (env.codex.authed) return env;
|
|
371
|
+
if (!env.codex.installed || !env.codex.path) return env;
|
|
372
|
+
|
|
373
|
+
// Try restoring persisted credentials from data-tools
|
|
374
|
+
if (restoreCodexCredentials()) {
|
|
375
|
+
const login = run(env.codex.path, ['login', 'status']);
|
|
376
|
+
if (isLoggedInStatus(login)) {
|
|
377
|
+
env.codex.authed = true;
|
|
378
|
+
env.codex.authMethod = 'restored_persistent';
|
|
379
|
+
console.log(' 🔄 Codex credentials restored from data-tools');
|
|
380
|
+
return env;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (process.env.OPENAI_API_KEY) {
|
|
385
|
+
const pipe = spawnSync(env.codex.path, ['login', '--with-api-key'], {
|
|
386
|
+
input: process.env.OPENAI_API_KEY,
|
|
387
|
+
encoding: 'utf8',
|
|
388
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
389
|
+
timeout: 10000,
|
|
390
|
+
});
|
|
391
|
+
if (pipe.status === 0) {
|
|
392
|
+
env.codex.authed = true;
|
|
393
|
+
env.codex.authMethod = 'api_key_env';
|
|
394
|
+
saveCodexCredentials();
|
|
395
|
+
console.log(' 🔄 Codex authenticated via OPENAI_API_KEY');
|
|
396
|
+
return env;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return env;
|
|
536
401
|
}
|
|
537
402
|
|
|
538
|
-
function
|
|
539
|
-
const
|
|
540
|
-
return
|
|
403
|
+
function prompt(question) {
|
|
404
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
405
|
+
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
541
406
|
}
|
|
542
407
|
|
|
543
|
-
function
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
408
|
+
async function authGuidance(env) {
|
|
409
|
+
if (env.claude.authed && env.codex.authed) return env;
|
|
410
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return env;
|
|
411
|
+
|
|
412
|
+
console.log('');
|
|
413
|
+
console.log(' ┌────────────────────────────────────────────┐');
|
|
414
|
+
console.log(' │ 🔑 Auth Setup │');
|
|
415
|
+
console.log(' └────────────────────────────────────────────┘');
|
|
416
|
+
|
|
417
|
+
if (!env.claude.authed) {
|
|
551
418
|
console.log('');
|
|
552
|
-
console.log(
|
|
419
|
+
console.log(' 🟠 Claude — not authenticated');
|
|
420
|
+
if (env.isReplit) {
|
|
421
|
+
console.log(' Run in your terminal: ! claude login');
|
|
422
|
+
console.log(' It will give you a URL + code to paste in your browser.');
|
|
423
|
+
} else {
|
|
424
|
+
console.log(' Run: claude login');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!env.codex.authed) {
|
|
553
429
|
console.log('');
|
|
430
|
+
console.log(' 🟢 Codex — not authenticated');
|
|
431
|
+
if (!env.codex.installed) {
|
|
432
|
+
console.log(' Install first: npm i -g @openai/codex');
|
|
433
|
+
console.log('');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (env.codex.installed && env.codex.path) {
|
|
437
|
+
console.log('');
|
|
438
|
+
console.log(' Sign in with your ChatGPT subscription (no API key needed):');
|
|
439
|
+
console.log('');
|
|
440
|
+
|
|
441
|
+
const answer = await prompt(' Press Enter to start device auth (or "skip" to skip): ');
|
|
442
|
+
if (answer.toLowerCase() === 'skip') {
|
|
443
|
+
console.log(' ⏭️ Skipped — GPT features disabled until Codex is authed');
|
|
444
|
+
} else {
|
|
445
|
+
console.log('');
|
|
446
|
+
if (runCodexDeviceAuth(env.codex.path)) {
|
|
447
|
+
env.codex.authed = true;
|
|
448
|
+
env.codex.authMethod = 'device_auth';
|
|
449
|
+
console.log('');
|
|
450
|
+
console.log(' ✅ Codex authenticated! (credentials saved for next session)');
|
|
451
|
+
} else {
|
|
452
|
+
console.log('');
|
|
453
|
+
console.log(' ❌ Auth failed — try again with: codex login --device-auth');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
console.log('');
|
|
460
|
+
return env;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ─── Codex Credential Persistence ──────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
const CODEX_HOME = join(process.env.HOME || '', '.codex');
|
|
466
|
+
const CODEX_PERSIST = resolve(process.cwd(), '.replit-tools', '.codex-persistent');
|
|
467
|
+
|
|
468
|
+
function saveCodexCredentials() {
|
|
469
|
+
const authFile = join(CODEX_HOME, 'auth.json');
|
|
470
|
+
if (!existsSync(authFile)) return false;
|
|
471
|
+
try {
|
|
472
|
+
const auth = readFileSync(authFile, 'utf8');
|
|
473
|
+
if (!auth.trim() || auth.trim() === '{}') return false;
|
|
474
|
+
mkdirSync(CODEX_PERSIST, { recursive: true });
|
|
475
|
+
const persisted = join(CODEX_PERSIST, 'auth.json');
|
|
476
|
+
writeFileSync(persisted, auth, { mode: 0o600 });
|
|
477
|
+
try { chmodSync(persisted, 0o600); } catch {}
|
|
478
|
+
return true;
|
|
479
|
+
} catch { return false; }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function restoreCodexCredentials() {
|
|
483
|
+
const persistedAuth = join(CODEX_PERSIST, 'auth.json');
|
|
484
|
+
const targetAuth = join(CODEX_HOME, 'auth.json');
|
|
485
|
+
if (existsSync(targetAuth)) return false;
|
|
486
|
+
if (!existsSync(persistedAuth)) return false;
|
|
487
|
+
try {
|
|
488
|
+
const auth = readFileSync(persistedAuth, 'utf8');
|
|
489
|
+
if (!auth.trim() || auth.trim() === '{}') return false;
|
|
490
|
+
mkdirSync(CODEX_HOME, { recursive: true });
|
|
491
|
+
writeFileSync(targetAuth, auth, { mode: 0o600 });
|
|
492
|
+
try { chmodSync(targetAuth, 0o600); } catch {}
|
|
493
|
+
return true;
|
|
494
|
+
} catch { return false; }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function parseDateValue(value) {
|
|
498
|
+
if (!value) return null;
|
|
499
|
+
if (typeof value === 'number') {
|
|
500
|
+
const ms = value > 1e12 ? value : value * 1000;
|
|
501
|
+
const d = new Date(ms);
|
|
502
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
503
|
+
}
|
|
504
|
+
const d = new Date(value);
|
|
505
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function formatExpiry(value) {
|
|
509
|
+
const d = parseDateValue(value);
|
|
510
|
+
if (!d) return 'unknown';
|
|
511
|
+
const iso = d.toISOString();
|
|
512
|
+
return d.getTime() <= Date.now() ? `${iso} (expired)` : iso;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function formatTimestamp(value) {
|
|
516
|
+
const d = parseDateValue(value);
|
|
517
|
+
return d ? d.toISOString() : 'unknown';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function relPath(p) {
|
|
521
|
+
if (!p) return 'unknown';
|
|
522
|
+
const cwd = resolve(process.cwd());
|
|
523
|
+
const full = resolve(p);
|
|
524
|
+
if (full.startsWith(cwd + '/')) return full.slice(cwd.length + 1);
|
|
525
|
+
const home = process.env.HOME || '';
|
|
526
|
+
if (home && full.startsWith(home + '/')) return `~/${full.slice(home.length + 1)}`;
|
|
527
|
+
return full;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function readJsonIfExists(file) {
|
|
531
|
+
if (!file || !existsSync(file)) return null;
|
|
532
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function claudeCredentialCandidates() {
|
|
536
|
+
return [
|
|
537
|
+
join(process.env.HOME || '', '.claude', '.credentials.json'),
|
|
538
|
+
join(process.env.HOME || '', '.claude', 'credentials.json'),
|
|
539
|
+
resolve(process.cwd(), '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
540
|
+
];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function getClaudeAuthDetails() {
|
|
544
|
+
const detected = detectClaude();
|
|
545
|
+
const status = detected.installed ? run('claude', ['auth', 'status']) : null;
|
|
546
|
+
let method = 'none';
|
|
547
|
+
let storage = null;
|
|
548
|
+
let expiry = null;
|
|
549
|
+
|
|
550
|
+
for (const file of claudeCredentialCandidates()) {
|
|
551
|
+
const cred = readJsonIfExists(file);
|
|
552
|
+
if (!cred) continue;
|
|
553
|
+
if (cred.claudeAiOauth) {
|
|
554
|
+
method = 'oauth';
|
|
555
|
+
storage = file;
|
|
556
|
+
expiry = cred.claudeAiOauth.expiresAt || null;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
if (cred.apiKey || cred.oauth_token) {
|
|
560
|
+
method = cred.apiKey ? 'api_key' : 'oauth_token';
|
|
561
|
+
storage = file;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const statusText = ((status?.stdout || '') + (status?.stderr || '')).trim();
|
|
567
|
+
return {
|
|
568
|
+
provider: 'claude',
|
|
569
|
+
installed: detected.installed,
|
|
570
|
+
version: detected.version,
|
|
571
|
+
authed: detected.authed,
|
|
572
|
+
method,
|
|
573
|
+
expiry,
|
|
574
|
+
expiryText: formatExpiry(expiry),
|
|
575
|
+
storage,
|
|
576
|
+
storageText: relPath(storage),
|
|
577
|
+
statusText,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function getCodexAuthDetails() {
|
|
582
|
+
const detected = detectCodex();
|
|
583
|
+
const status = (detected.installed && detected.path) ? run(detected.path, ['login', 'status']) : null;
|
|
584
|
+
const authFile = join(CODEX_HOME, 'auth.json');
|
|
585
|
+
const persisted = join(CODEX_PERSIST, 'auth.json');
|
|
586
|
+
const activeFile = existsSync(authFile) ? authFile : existsSync(persisted) ? persisted : null;
|
|
587
|
+
const auth = readJsonIfExists(activeFile);
|
|
588
|
+
|
|
589
|
+
let method = detected.authMethod || 'none';
|
|
590
|
+
if (auth?.auth_mode === 'chatgpt') method = 'chatgpt_device_auth';
|
|
591
|
+
else if (auth?.auth_mode === 'api_key') method = 'api_key';
|
|
592
|
+
else if (!detected.authed && process.env.OPENAI_API_KEY) method = 'api_key_env';
|
|
593
|
+
|
|
594
|
+
const lastRefresh = auth?.last_refresh || null;
|
|
595
|
+
const statusText = ((status?.stdout || '') + (status?.stderr || '')).trim();
|
|
596
|
+
return {
|
|
597
|
+
provider: 'codex',
|
|
598
|
+
installed: detected.installed,
|
|
599
|
+
version: detected.version,
|
|
600
|
+
authed: detected.authed,
|
|
601
|
+
method,
|
|
602
|
+
expiry: null,
|
|
603
|
+
expiryText: 'n/a',
|
|
604
|
+
lastRefresh,
|
|
605
|
+
lastRefreshText: formatTimestamp(lastRefresh),
|
|
606
|
+
storage: activeFile,
|
|
607
|
+
storageText: relPath(activeFile),
|
|
608
|
+
statusText,
|
|
609
|
+
path: detected.path,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function getAuthState() {
|
|
614
|
+
return {
|
|
615
|
+
claude: getClaudeAuthDetails(),
|
|
616
|
+
codex: getCodexAuthDetails(),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function printAuthStatusBox(state) {
|
|
621
|
+
const c = state.claude;
|
|
622
|
+
const x = state.codex;
|
|
623
|
+
const cIcon = c.authed ? '✅' : c.installed ? '⚠️' : '❌';
|
|
624
|
+
const xIcon = x.authed ? '✅' : x.installed ? '⚠️' : '❌';
|
|
625
|
+
|
|
626
|
+
console.log('');
|
|
627
|
+
console.log(` ${br('╔', '╗')}`);
|
|
628
|
+
console.log(` ${ln(`🔑 Auth Status`)}`);
|
|
629
|
+
console.log(` ${sep()}`);
|
|
630
|
+
console.log(` ${ln(`🟠 Claude ${cIcon} ${c.authed ? 'authenticated' : c.installed ? 'not authenticated' : 'not installed'}`)}`);
|
|
631
|
+
console.log(` ${ln(` Method: ${c.method}`)}`);
|
|
632
|
+
console.log(` ${ln(` Expiry: ${c.expiryText}`)}`);
|
|
633
|
+
console.log(` ${ln(` Storage: ${c.storageText}`)}`);
|
|
634
|
+
console.log(` ${sep()}`);
|
|
635
|
+
console.log(` ${ln(`🟢 Codex ${xIcon} ${x.authed ? 'authenticated' : x.installed ? 'not authenticated' : 'not installed'}`)}`);
|
|
636
|
+
console.log(` ${ln(` Method: ${x.method}`)}`);
|
|
637
|
+
console.log(` ${ln(` Expiry: ${x.expiryText}`)}`);
|
|
638
|
+
console.log(` ${ln(` Storage: ${x.storageText}`)}`);
|
|
639
|
+
if (x.lastRefresh) console.log(` ${ln(` Refreshed:${x.lastRefreshText}`)}`);
|
|
640
|
+
console.log(` ${br('╚', '╝')}`);
|
|
641
|
+
console.log('');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function runCodexDeviceAuth(codexPath) {
|
|
645
|
+
if (!codexPath) return false;
|
|
646
|
+
const result = spawnSync(codexPath, ['login', '--device-auth'], {
|
|
647
|
+
stdio: 'inherit',
|
|
648
|
+
timeout: 900000,
|
|
649
|
+
});
|
|
650
|
+
if (result.status === 0) {
|
|
651
|
+
saveCodexCredentials();
|
|
652
|
+
return true;
|
|
554
653
|
}
|
|
654
|
+
return false;
|
|
555
655
|
}
|
|
556
656
|
|
|
557
657
|
// ─── Mode Resolution ────────────────────────────────────────────────────────
|
|
@@ -618,7 +718,7 @@ function generateSettings(workspace) {
|
|
|
618
718
|
],
|
|
619
719
|
PostToolUse: [
|
|
620
720
|
{
|
|
621
|
-
matcher: '
|
|
721
|
+
matcher: '',
|
|
622
722
|
hooks: [{ type: 'command', command: 'node .claude/hooks/cost-logger.mjs' }],
|
|
623
723
|
},
|
|
624
724
|
],
|
|
@@ -667,6 +767,8 @@ function generateGitignoreEntries(workspace) {
|
|
|
667
767
|
'.claude/hooks/decision-ledger.jsonl',
|
|
668
768
|
'.claude/.launched',
|
|
669
769
|
'.claude/dual-brain.memory.json',
|
|
770
|
+
'.claude/dual-brain.version.json',
|
|
771
|
+
'.claude/dual-brain.update-check.json',
|
|
670
772
|
];
|
|
671
773
|
let existing = '';
|
|
672
774
|
try { existing = readFileSync(join(workspace, '.gitignore'), 'utf8'); } catch {}
|
|
@@ -691,8 +793,6 @@ function install(workspace, env, mode) {
|
|
|
691
793
|
'summary-checkpoint.mjs', 'decision-ledger.mjs', 'control-panel.mjs',
|
|
692
794
|
'risk-classifier.mjs', 'failure-detector.mjs',
|
|
693
795
|
'vibe-router.mjs', 'plan-generator.mjs', 'vibe-memory.mjs',
|
|
694
|
-
'agent-templates.mjs', 'agent-chains.mjs',
|
|
695
|
-
'ship-captain.mjs', 'ship-gate.mjs', 'confirmation-policy.mjs',
|
|
696
796
|
];
|
|
697
797
|
for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
|
|
698
798
|
actions.push(`✓ ${HOOKS.length} hook scripts`);
|
|
@@ -734,21 +834,10 @@ function install(workspace, env, mode) {
|
|
|
734
834
|
actions.push('✓ .gitignore updated');
|
|
735
835
|
}
|
|
736
836
|
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// ─── Quick Start Block ───────────────────────────────────────────────────────
|
|
837
|
+
const stamp = writeVersionStamp(workspace);
|
|
838
|
+
actions.push(`✓ version stamp (v${stamp.version})`);
|
|
741
839
|
|
|
742
|
-
|
|
743
|
-
console.log('');
|
|
744
|
-
console.log(` ${BRAND}`);
|
|
745
|
-
console.log('');
|
|
746
|
-
console.log(' Quick start:');
|
|
747
|
-
console.log(' npx dual-brain do "fix the login bug" ← one command to PR');
|
|
748
|
-
console.log(' npx dual-brain setup ← configure providers');
|
|
749
|
-
console.log(' npx dual-brain doctor ← check system health');
|
|
750
|
-
console.log(' npx dual-brain help ← full command list');
|
|
751
|
-
console.log('');
|
|
840
|
+
return actions;
|
|
752
841
|
}
|
|
753
842
|
|
|
754
843
|
// ─── Status Report ──────────────────────────────────────────────────────────
|
|
@@ -757,14 +846,22 @@ function printReport(env, mode, actions, isDryRun) {
|
|
|
757
846
|
const lines = [];
|
|
758
847
|
|
|
759
848
|
lines.push(br('╔', '╗'));
|
|
760
|
-
lines.push(ln(
|
|
761
|
-
lines.push(ln(BRAND_SUBTITLE));
|
|
849
|
+
lines.push(ln(`🧠 Dual-Brain v${VERSION}`));
|
|
762
850
|
lines.push(sep());
|
|
763
|
-
|
|
851
|
+
|
|
852
|
+
const cAuth = env.claude.authed ? '✅' : env.claude.installed ? '⚠️' : '❌';
|
|
853
|
+
const xAuth = env.codex.authed ? '✅' : env.codex.installed ? '⚠️' : '❌';
|
|
854
|
+
lines.push(ln(` 🟠 Claude ${cAuth} 🟢 Codex ${xAuth}`));
|
|
855
|
+
|
|
856
|
+
if (env.isReplit) {
|
|
857
|
+
lines.push(ln(` 🌀 Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
|
|
858
|
+
}
|
|
764
859
|
|
|
765
860
|
if (actions) {
|
|
766
861
|
lines.push(sep());
|
|
767
862
|
for (const a of actions) lines.push(ln(` ${a}`));
|
|
863
|
+
lines.push(sep());
|
|
864
|
+
lines.push(ln('✅ Installed — launching session manager...'));
|
|
768
865
|
} else if (isDryRun) {
|
|
769
866
|
lines.push(sep());
|
|
770
867
|
lines.push(ln('Dry run — no files written'));
|
|
@@ -851,6 +948,132 @@ function launchPanel() {
|
|
|
851
948
|
}
|
|
852
949
|
}
|
|
853
950
|
|
|
951
|
+
// ─── Subcommand: auth ──────────────────────────────────────────────────────
|
|
952
|
+
|
|
953
|
+
function cmdAuthStatus() {
|
|
954
|
+
const state = getAuthState();
|
|
955
|
+
if (jsonOut) {
|
|
956
|
+
console.log(JSON.stringify({ version: VERSION, auth: state }, null, 2));
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
printAuthStatusBox(state);
|
|
960
|
+
console.log(` Login: ${cmd('npx dual-brain auth login')}`);
|
|
961
|
+
console.log(` Refresh: ${cmd('npx dual-brain auth refresh')}`);
|
|
962
|
+
console.log('');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function cmdAuthLogin() {
|
|
966
|
+
const state = getAuthState();
|
|
967
|
+
|
|
968
|
+
console.log('');
|
|
969
|
+
if (!state.claude.authed) {
|
|
970
|
+
if (!state.claude.installed) {
|
|
971
|
+
console.log(' 🟠 Claude is not installed.');
|
|
972
|
+
} else {
|
|
973
|
+
console.log(' 🟠 Claude needs login.');
|
|
974
|
+
console.log(` Run: ${cmd('claude login')}`);
|
|
975
|
+
console.log(' Claude uses its own browser/device flow from the CLI.');
|
|
976
|
+
}
|
|
977
|
+
console.log('');
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (!state.codex.authed) {
|
|
981
|
+
if (!state.codex.installed || !state.codex.path) {
|
|
982
|
+
console.log(' 🟢 Codex is not installed.');
|
|
983
|
+
console.log(' Install first: npm i -g @openai/codex');
|
|
984
|
+
console.log('');
|
|
985
|
+
} else if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
986
|
+
console.log(' 🟢 Codex login requires an interactive terminal.');
|
|
987
|
+
console.log(` Run: ${cmd('codex login --device-auth')}`);
|
|
988
|
+
console.log('');
|
|
989
|
+
} else {
|
|
990
|
+
console.log(' 🟢 Starting Codex device auth...');
|
|
991
|
+
console.log('');
|
|
992
|
+
if (runCodexDeviceAuth(state.codex.path)) {
|
|
993
|
+
console.log('');
|
|
994
|
+
console.log(' ✅ Codex authenticated and credentials saved.');
|
|
995
|
+
} else {
|
|
996
|
+
console.log('');
|
|
997
|
+
console.log(` ❌ Codex auth failed. Retry with: ${cmd('codex login --device-auth')}`);
|
|
998
|
+
}
|
|
999
|
+
console.log('');
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (state.claude.authed && state.codex.authed) {
|
|
1004
|
+
console.log(' ✅ Claude and Codex are already authenticated.');
|
|
1005
|
+
console.log('');
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const finalState = getAuthState();
|
|
1010
|
+
if (finalState.claude.authed && finalState.codex.authed) {
|
|
1011
|
+
printAuthStatusBox(finalState);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
printAuthStatusBox(finalState);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function cmdAuthRefresh() {
|
|
1019
|
+
let env = detectEnvironment();
|
|
1020
|
+
let codexRefreshed = false;
|
|
1021
|
+
|
|
1022
|
+
console.log('');
|
|
1023
|
+
console.log(' 🔄 Refreshing provider auth...');
|
|
1024
|
+
console.log('');
|
|
1025
|
+
|
|
1026
|
+
env = { ...env, claude: { ...env.claude, authed: false } };
|
|
1027
|
+
env = healClaudeAuth(env);
|
|
1028
|
+
|
|
1029
|
+
if (env.codex.installed && env.codex.path && process.stdin.isTTY && process.stdout.isTTY) {
|
|
1030
|
+
console.log(' 🟢 Codex device auth refresh');
|
|
1031
|
+
console.log('');
|
|
1032
|
+
codexRefreshed = runCodexDeviceAuth(env.codex.path);
|
|
1033
|
+
if (codexRefreshed) {
|
|
1034
|
+
env.codex.authed = true;
|
|
1035
|
+
env.codex.authMethod = 'device_auth';
|
|
1036
|
+
console.log('');
|
|
1037
|
+
console.log(' ✅ Codex auth refreshed and credentials saved.');
|
|
1038
|
+
console.log('');
|
|
1039
|
+
} else {
|
|
1040
|
+
console.log('');
|
|
1041
|
+
console.log(` ❌ Codex refresh failed. Retry with: ${cmd('codex login --device-auth')}`);
|
|
1042
|
+
console.log('');
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
env = { ...env, codex: { ...env.codex, authed: false } };
|
|
1046
|
+
env = healCodexAuth(env);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (env.claude.installed && !env.claude.authed) {
|
|
1050
|
+
console.log(` 🟠 Claude refresh unavailable here. Run: ${cmd('claude login')}`);
|
|
1051
|
+
console.log('');
|
|
1052
|
+
}
|
|
1053
|
+
if (env.codex.installed && !env.codex.authed && !codexRefreshed) {
|
|
1054
|
+
console.log(` 🟢 Codex refresh incomplete. Run: ${cmd('codex login --device-auth')}`);
|
|
1055
|
+
console.log('');
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const finalState = getAuthState();
|
|
1059
|
+
if (jsonOut) {
|
|
1060
|
+
console.log(JSON.stringify({ version: VERSION, auth: finalState }, null, 2));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
printAuthStatusBox(finalState);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function cmdAuth() {
|
|
1067
|
+
const action = positional[1] || 'status';
|
|
1068
|
+
if (action === 'status') { cmdAuthStatus(); return; }
|
|
1069
|
+
if (action === 'login') { cmdAuthLogin(); return; }
|
|
1070
|
+
if (action === 'refresh') { cmdAuthRefresh(); return; }
|
|
1071
|
+
|
|
1072
|
+
console.error(` Unknown auth command: ${action}`);
|
|
1073
|
+
console.error(` Run: ${cmd('npx dual-brain auth [status|login|refresh]')}`);
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
854
1077
|
// ─── Subcommand: mode ──────────────────────────────────────────────────────
|
|
855
1078
|
|
|
856
1079
|
function cmdMode() {
|
|
@@ -1056,1223 +1279,58 @@ function cmdExplain() {
|
|
|
1056
1279
|
console.log('');
|
|
1057
1280
|
}
|
|
1058
1281
|
|
|
1059
|
-
// ───
|
|
1060
|
-
|
|
1061
|
-
function cmdDoctor() {
|
|
1062
|
-
const workspace = resolve(process.cwd());
|
|
1063
|
-
const claudeDir = join(workspace, '.claude');
|
|
1064
|
-
const hooksDir = join(claudeDir, 'hooks');
|
|
1065
|
-
const results = [];
|
|
1282
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
1066
1283
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
'node .claude/hooks/cost-logger.mjs',
|
|
1072
|
-
];
|
|
1073
|
-
if (existsSync(settingsPath)) {
|
|
1074
|
-
try {
|
|
1075
|
-
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
1076
|
-
const registeredCmds = [];
|
|
1077
|
-
if (settings.hooks) {
|
|
1078
|
-
for (const entries of Object.values(settings.hooks)) {
|
|
1079
|
-
for (const entry of entries) {
|
|
1080
|
-
for (const h of (entry.hooks || [])) {
|
|
1081
|
-
if (DUAL_BRAIN_CMDS.includes(h.command)) registeredCmds.push(h.command);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
const missing = DUAL_BRAIN_CMDS.filter(c => !registeredCmds.includes(c));
|
|
1087
|
-
if (missing.length === 0) {
|
|
1088
|
-
results.push({ name: 'Hook registration', status: 'pass', detail: `${DUAL_BRAIN_CMDS.length}/${DUAL_BRAIN_CMDS.length} hooks registered` });
|
|
1089
|
-
} else {
|
|
1090
|
-
results.push({ name: 'Hook registration', status: 'fail', detail: `Missing: ${missing.map(c => c.split('/').pop()).join(', ')}`, fix: 'Run: npx dual-brain repair' });
|
|
1091
|
-
}
|
|
1092
|
-
} catch (err) {
|
|
1093
|
-
results.push({ name: 'Hook registration', status: 'fail', detail: `settings.json parse error: ${err.message}`, fix: 'Run: npx dual-brain repair' });
|
|
1094
|
-
}
|
|
1095
|
-
} else {
|
|
1096
|
-
results.push({ name: 'Hook registration', status: 'fail', detail: 'settings.json not found', fix: 'Run: npx dual-brain' });
|
|
1284
|
+
async function main() {
|
|
1285
|
+
if (subcommand === 'status') {
|
|
1286
|
+
launchPanel();
|
|
1287
|
+
return;
|
|
1097
1288
|
}
|
|
1289
|
+
if (subcommand === 'auth') { cmdAuth(); return; }
|
|
1290
|
+
if (subcommand === 'mode') { cmdMode(); return; }
|
|
1291
|
+
if (subcommand === 'budget') { cmdBudget(); return; }
|
|
1292
|
+
if (subcommand === 'explain') { cmdExplain(); return; }
|
|
1098
1293
|
|
|
1099
|
-
//
|
|
1100
|
-
|
|
1101
|
-
if (existsSync(orchPath)) {
|
|
1102
|
-
try {
|
|
1103
|
-
const config = JSON.parse(readFileSync(orchPath, 'utf8'));
|
|
1104
|
-
const requiredKeys = ['subscriptions', 'tiers', 'providers', 'budgets'];
|
|
1105
|
-
const missing = requiredKeys.filter(k => !config[k]);
|
|
1106
|
-
if (missing.length === 0) {
|
|
1107
|
-
results.push({ name: 'Config validity', status: 'pass', detail: 'orchestrator.json valid, all required keys present' });
|
|
1108
|
-
} else {
|
|
1109
|
-
results.push({ name: 'Config validity', status: 'warn', detail: `Missing keys: ${missing.join(', ')}`, fix: 'Run: npx dual-brain --force' });
|
|
1110
|
-
}
|
|
1111
|
-
} catch (err) {
|
|
1112
|
-
results.push({ name: 'Config validity', status: 'fail', detail: `orchestrator.json corrupt: ${err.message}`, fix: 'Run: npx dual-brain repair' });
|
|
1113
|
-
}
|
|
1114
|
-
} else {
|
|
1115
|
-
results.push({ name: 'Config validity', status: 'fail', detail: 'orchestrator.json not found', fix: 'Run: npx dual-brain' });
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// 3. Auth status
|
|
1119
|
-
const claude = detectClaude();
|
|
1120
|
-
if (claude.authed) {
|
|
1121
|
-
results.push({ name: 'Claude CLI', status: 'pass', detail: 'installed and authenticated' });
|
|
1122
|
-
} else if (claude.installed) {
|
|
1123
|
-
results.push({ name: 'Claude CLI', status: 'warn', detail: 'installed but not authenticated' });
|
|
1124
|
-
} else {
|
|
1125
|
-
results.push({ name: 'Claude CLI', status: 'warn', detail: 'not found' });
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
const codexResult = detectCodex();
|
|
1129
|
-
if (codexResult.authed) {
|
|
1130
|
-
results.push({ name: 'Codex CLI', status: 'pass', detail: 'installed and authenticated' });
|
|
1131
|
-
} else if (codexResult.installed) {
|
|
1132
|
-
results.push({ name: 'Codex CLI', status: 'warn', detail: 'installed but not authenticated' });
|
|
1133
|
-
} else {
|
|
1134
|
-
results.push({ name: 'Codex CLI', status: 'warn', detail: 'not found (GPT features disabled)' });
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// 4. State file health
|
|
1138
|
-
const stateCheckFiles = [
|
|
1139
|
-
{ path: join(hooksDir, '.burst-state'), label: '.burst-state', format: 'json' },
|
|
1140
|
-
{ path: join(hooksDir, 'burst-state.json'), label: 'burst-state.json', format: 'json' },
|
|
1141
|
-
{ path: join(hooksDir, '.drift-warned'), label: '.drift-warned', format: 'any' },
|
|
1142
|
-
{ path: join(claudeDir, 'dual-brain.profile.json'), label: 'dual-brain.profile.json', format: 'json' },
|
|
1143
|
-
{ path: join(hooksDir, 'decision-ledger.jsonl'), label: 'decision-ledger.jsonl', format: 'jsonl' },
|
|
1144
|
-
];
|
|
1145
|
-
|
|
1146
|
-
// Add any usage-*.jsonl files
|
|
1147
|
-
try {
|
|
1148
|
-
for (const f of readdirSync(hooksDir)) {
|
|
1149
|
-
if (f.startsWith('usage-') && f.endsWith('.jsonl')) {
|
|
1150
|
-
stateCheckFiles.push({ path: join(hooksDir, f), label: f, format: 'jsonl' });
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
} catch {}
|
|
1154
|
-
|
|
1155
|
-
let stateHealthy = 0, stateWarns = 0, stateFails = 0;
|
|
1156
|
-
const stateIssues = [];
|
|
1157
|
-
|
|
1158
|
-
for (const sf of stateCheckFiles) {
|
|
1159
|
-
if (!existsSync(sf.path)) continue;
|
|
1160
|
-
try {
|
|
1161
|
-
const raw = readFileSync(sf.path, 'utf8');
|
|
1162
|
-
if (sf.format === 'json') {
|
|
1163
|
-
JSON.parse(raw);
|
|
1164
|
-
stateHealthy++;
|
|
1165
|
-
} else if (sf.format === 'jsonl') {
|
|
1166
|
-
const lines = raw.split('\n').filter(Boolean);
|
|
1167
|
-
let badLines = 0;
|
|
1168
|
-
for (const line of lines) {
|
|
1169
|
-
try { JSON.parse(line); } catch { badLines++; }
|
|
1170
|
-
}
|
|
1171
|
-
if (badLines > 0) {
|
|
1172
|
-
stateWarns++;
|
|
1173
|
-
stateIssues.push(`${sf.label}: ${badLines}/${lines.length} unparseable lines`);
|
|
1174
|
-
} else {
|
|
1175
|
-
stateHealthy++;
|
|
1176
|
-
}
|
|
1177
|
-
} else {
|
|
1178
|
-
stateHealthy++;
|
|
1179
|
-
}
|
|
1180
|
-
} catch (err) {
|
|
1181
|
-
stateFails++;
|
|
1182
|
-
stateIssues.push(`${sf.label}: corrupt (${err.message})`);
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// Check for stale locks
|
|
1187
|
-
let staleLocks = 0;
|
|
1188
|
-
for (const dir of [hooksDir, claudeDir]) {
|
|
1189
|
-
try {
|
|
1190
|
-
for (const f of readdirSync(dir)) {
|
|
1191
|
-
if (f.endsWith('.lock')) {
|
|
1192
|
-
try {
|
|
1193
|
-
const st = statSync(join(dir, f));
|
|
1194
|
-
if (Date.now() - st.mtimeMs > 30_000) staleLocks++;
|
|
1195
|
-
} catch {}
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
} catch {}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
if (staleLocks > 0) {
|
|
1202
|
-
stateIssues.push(`${staleLocks} stale lock file(s)`);
|
|
1203
|
-
stateWarns++;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
if (stateFails > 0) {
|
|
1207
|
-
results.push({ name: 'State files', status: 'fail', detail: stateIssues.join('; '), fix: 'Run: npx dual-brain repair' });
|
|
1208
|
-
} else if (stateWarns > 0) {
|
|
1209
|
-
results.push({ name: 'State files', status: 'warn', detail: stateIssues.join('; '), fix: 'Run: npx dual-brain repair' });
|
|
1210
|
-
} else {
|
|
1211
|
-
results.push({ name: 'State files', status: 'pass', detail: `${stateHealthy} file(s) healthy` });
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// 5. Error channel
|
|
1215
|
-
const errorsFile = join(hooksDir, 'errors.jsonl');
|
|
1216
|
-
if (existsSync(errorsFile)) {
|
|
1217
|
-
try {
|
|
1218
|
-
const raw = readFileSync(errorsFile, 'utf8');
|
|
1219
|
-
const lines = raw.split('\n').filter(Boolean);
|
|
1220
|
-
const entries = [];
|
|
1221
|
-
for (const line of lines) {
|
|
1222
|
-
try { entries.push(JSON.parse(line)); } catch {}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
|
|
1226
|
-
const recent = entries.filter(e => e.timestamp && Date.parse(e.timestamp) >= cutoff24h);
|
|
1227
|
-
|
|
1228
|
-
if (recent.length === 0) {
|
|
1229
|
-
results.push({ name: 'Error channel', status: 'pass', detail: 'no errors in last 24h' });
|
|
1230
|
-
} else {
|
|
1231
|
-
const last3 = recent.slice(-3);
|
|
1232
|
-
const detail = `${recent.length} error(s) in last 24h`;
|
|
1233
|
-
results.push({ name: 'Error channel', status: 'warn', detail, errors: last3 });
|
|
1234
|
-
}
|
|
1235
|
-
} catch {
|
|
1236
|
-
results.push({ name: 'Error channel', status: 'warn', detail: 'errors.jsonl unreadable' });
|
|
1237
|
-
}
|
|
1238
|
-
} else {
|
|
1239
|
-
results.push({ name: 'Error channel', status: 'pass', detail: 'no errors.jsonl (clean)' });
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// Output
|
|
1243
|
-
const passCount = results.filter(r => r.status === 'pass').length;
|
|
1244
|
-
const warnCount = results.filter(r => r.status === 'warn').length;
|
|
1245
|
-
const failCount = results.filter(r => r.status === 'fail').length;
|
|
1246
|
-
|
|
1247
|
-
console.log('');
|
|
1248
|
-
console.log(` 🩺 dual-brain v${VERSION} — doctor`);
|
|
1249
|
-
console.log(' ' + '─'.repeat(50));
|
|
1250
|
-
|
|
1251
|
-
for (const r of results) {
|
|
1252
|
-
const icon = r.status === 'pass' ? '✓' : r.status === 'warn' ? '⚠' : '✗';
|
|
1253
|
-
console.log(` ${icon} ${r.name}: ${r.detail}`);
|
|
1254
|
-
if (r.fix) console.log(` → ${r.fix}`);
|
|
1255
|
-
if (r.errors) {
|
|
1256
|
-
for (const e of r.errors) {
|
|
1257
|
-
const ts = e.timestamp ? e.timestamp.slice(11, 19) : '??:??:??';
|
|
1258
|
-
console.log(` ${ts} [${e.hook || '?'}] ${e.error || '?'}`);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
console.log(' ' + '─'.repeat(50));
|
|
1264
|
-
if (failCount > 0) {
|
|
1265
|
-
console.log(` ✗ Needs repair: ${failCount} issue(s) require attention`);
|
|
1266
|
-
} else if (warnCount > 0) {
|
|
1267
|
-
console.log(` ⚠ Warnings: ${warnCount} — system functional but has issues`);
|
|
1268
|
-
} else {
|
|
1269
|
-
console.log(` ✓ Healthy: all ${passCount} checks passed`);
|
|
1270
|
-
}
|
|
1271
|
-
console.log('');
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// ─── Subcommand: reset ────────────────────────────────────────────────────
|
|
1275
|
-
|
|
1276
|
-
function cmdReset() {
|
|
1277
|
-
const workspace = resolve(process.cwd());
|
|
1278
|
-
const claudeDir = join(workspace, '.claude');
|
|
1279
|
-
const hooksDir = join(claudeDir, 'hooks');
|
|
1280
|
-
|
|
1281
|
-
// Collect state files (NOT config, hooks, or profile)
|
|
1282
|
-
const STATE_FIXED = [
|
|
1283
|
-
join(hooksDir, 'usage.jsonl'),
|
|
1284
|
-
join(hooksDir, 'decision-ledger.jsonl'),
|
|
1285
|
-
join(hooksDir, 'failure-ledger.json'),
|
|
1286
|
-
join(hooksDir, '.burst-state'),
|
|
1287
|
-
join(hooksDir, 'burst-state.json'),
|
|
1288
|
-
join(hooksDir, '.drift-warned'),
|
|
1289
|
-
join(hooksDir, '.budget-alerted'),
|
|
1290
|
-
join(hooksDir, 'errors.jsonl'),
|
|
1291
|
-
join(hooksDir, 'summary-checkpoint.json'),
|
|
1292
|
-
join(claudeDir, '.launched'),
|
|
1293
|
-
];
|
|
1294
|
-
|
|
1295
|
-
const toDelete = [];
|
|
1296
|
-
for (const f of STATE_FIXED) {
|
|
1297
|
-
if (existsSync(f)) toDelete.push(f);
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
// Scan for date-stamped files and lock files
|
|
1301
|
-
try {
|
|
1302
|
-
for (const f of readdirSync(hooksDir)) {
|
|
1303
|
-
if (f.startsWith('usage-') && f.endsWith('.jsonl')) toDelete.push(join(hooksDir, f));
|
|
1304
|
-
if (f.startsWith('usage-summary-') && f.endsWith('.json')) toDelete.push(join(hooksDir, f));
|
|
1305
|
-
if (f.endsWith('.lock')) toDelete.push(join(hooksDir, f));
|
|
1306
|
-
}
|
|
1307
|
-
} catch {}
|
|
1308
|
-
try {
|
|
1309
|
-
for (const f of readdirSync(claudeDir)) {
|
|
1310
|
-
if (f.endsWith('.lock')) toDelete.push(join(claudeDir, f));
|
|
1311
|
-
}
|
|
1312
|
-
} catch {}
|
|
1313
|
-
|
|
1314
|
-
const unique = [...new Set(toDelete)].filter(f => existsSync(f));
|
|
1315
|
-
|
|
1316
|
-
if (unique.length === 0) {
|
|
1317
|
-
console.log('');
|
|
1318
|
-
console.log(' 🔄 Nothing to reset — no state files found.');
|
|
1319
|
-
console.log('');
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
// Require --force or confirmation
|
|
1324
|
-
if (!force) {
|
|
1325
|
-
console.log('');
|
|
1326
|
-
console.log(` 🔄 dual-brain reset — will delete ${unique.length} state file(s):`);
|
|
1327
|
-
console.log('');
|
|
1328
|
-
for (const f of unique) {
|
|
1329
|
-
const rel = f.startsWith(workspace) ? f.slice(workspace.length + 1) : f;
|
|
1330
|
-
console.log(` ${rel}`);
|
|
1331
|
-
}
|
|
1332
|
-
console.log('');
|
|
1333
|
-
console.log(' Preserved: orchestrator.json, settings.json, hooks, profile, CLAUDE.md');
|
|
1334
|
-
console.log('');
|
|
1335
|
-
console.log(` To confirm: ${cmd('npx dual-brain reset --force')}`);
|
|
1336
|
-
console.log('');
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
let removed = 0;
|
|
1341
|
-
const errors = [];
|
|
1342
|
-
for (const f of unique) {
|
|
1343
|
-
try {
|
|
1344
|
-
unlinkSync(f);
|
|
1345
|
-
removed++;
|
|
1346
|
-
} catch (err) {
|
|
1347
|
-
errors.push(` ⚠ Could not remove ${f.split('/').pop()}: ${err.message}`);
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
console.log('');
|
|
1352
|
-
console.log(` 🔄 dual-brain v${VERSION} — reset`);
|
|
1353
|
-
console.log(' ' + '─'.repeat(40));
|
|
1354
|
-
console.log(` ✓ Removed ${removed} state file(s)`);
|
|
1355
|
-
for (const e of errors) console.log(e);
|
|
1356
|
-
console.log('');
|
|
1357
|
-
console.log(' Preserved: orchestrator.json, settings.json, hooks, profile, CLAUDE.md');
|
|
1358
|
-
console.log(' State will rebuild automatically on next session.');
|
|
1359
|
-
console.log('');
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// ─── Subcommand: repair ───────────────────────────────────────────────────
|
|
1363
|
-
|
|
1364
|
-
function cmdRepair() {
|
|
1365
|
-
const workspace = resolve(process.cwd());
|
|
1366
|
-
const claudeDir = join(workspace, '.claude');
|
|
1367
|
-
const hooksDir = join(claudeDir, 'hooks');
|
|
1368
|
-
const actions = [];
|
|
1369
|
-
|
|
1370
|
-
// 1. Remove stale lock files (older than 30 seconds)
|
|
1371
|
-
let staleLocks = 0;
|
|
1372
|
-
for (const dir of [hooksDir, claudeDir]) {
|
|
1373
|
-
try {
|
|
1374
|
-
for (const f of readdirSync(dir)) {
|
|
1375
|
-
if (!f.endsWith('.lock')) continue;
|
|
1376
|
-
const lockPath = join(dir, f);
|
|
1377
|
-
try {
|
|
1378
|
-
const st = statSync(lockPath);
|
|
1379
|
-
if (Date.now() - st.mtimeMs > 30_000) {
|
|
1380
|
-
unlinkSync(lockPath);
|
|
1381
|
-
staleLocks++;
|
|
1382
|
-
}
|
|
1383
|
-
} catch {}
|
|
1384
|
-
}
|
|
1385
|
-
} catch {}
|
|
1386
|
-
}
|
|
1387
|
-
if (staleLocks > 0) {
|
|
1388
|
-
actions.push(`✓ Removed ${staleLocks} stale lock file(s)`);
|
|
1389
|
-
} else {
|
|
1390
|
-
actions.push('⊘ No stale locks found');
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
// 2. Repair corrupt JSONL files
|
|
1394
|
-
const jsonlFiles = [
|
|
1395
|
-
join(hooksDir, 'decision-ledger.jsonl'),
|
|
1396
|
-
join(hooksDir, 'errors.jsonl'),
|
|
1397
|
-
join(hooksDir, 'usage.jsonl'),
|
|
1398
|
-
];
|
|
1399
|
-
try {
|
|
1400
|
-
for (const f of readdirSync(hooksDir)) {
|
|
1401
|
-
if (f.startsWith('usage-') && f.endsWith('.jsonl')) {
|
|
1402
|
-
jsonlFiles.push(join(hooksDir, f));
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
} catch {}
|
|
1406
|
-
|
|
1407
|
-
let repairedJsonl = 0;
|
|
1408
|
-
for (const filePath of [...new Set(jsonlFiles)]) {
|
|
1409
|
-
if (!existsSync(filePath)) continue;
|
|
1410
|
-
try {
|
|
1411
|
-
const raw = readFileSync(filePath, 'utf8');
|
|
1412
|
-
const lines = raw.split('\n').filter(Boolean);
|
|
1413
|
-
let badCount = 0;
|
|
1414
|
-
const goodLines = [];
|
|
1415
|
-
for (const line of lines) {
|
|
1416
|
-
try {
|
|
1417
|
-
JSON.parse(line);
|
|
1418
|
-
goodLines.push(line);
|
|
1419
|
-
} catch {
|
|
1420
|
-
badCount++;
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
if (badCount > 0) {
|
|
1424
|
-
const tmp = filePath + '.tmp.' + process.pid;
|
|
1425
|
-
writeFileSync(tmp, goodLines.length > 0 ? goodLines.join('\n') + '\n' : '');
|
|
1426
|
-
renameSync(tmp, filePath);
|
|
1427
|
-
repairedJsonl++;
|
|
1428
|
-
actions.push(`✓ ${filePath.split('/').pop()}: removed ${badCount} corrupt line(s), kept ${goodLines.length}`);
|
|
1429
|
-
}
|
|
1430
|
-
} catch (err) {
|
|
1431
|
-
actions.push(`⚠ ${filePath.split('/').pop()}: could not repair (${err.message})`);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
if (repairedJsonl === 0) {
|
|
1435
|
-
actions.push('⊘ No corrupt JSONL files found');
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// 3. Re-validate and fix orchestrator.json formatting
|
|
1439
|
-
const orchPath = join(claudeDir, 'orchestrator.json');
|
|
1440
|
-
if (existsSync(orchPath)) {
|
|
1441
|
-
try {
|
|
1442
|
-
const raw = readFileSync(orchPath, 'utf8');
|
|
1443
|
-
const config = JSON.parse(raw);
|
|
1444
|
-
const pretty = JSON.stringify(config, null, 2) + '\n';
|
|
1445
|
-
if (raw !== pretty) {
|
|
1446
|
-
const tmp = orchPath + '.tmp.' + process.pid;
|
|
1447
|
-
writeFileSync(tmp, pretty);
|
|
1448
|
-
renameSync(tmp, orchPath);
|
|
1449
|
-
actions.push('✓ orchestrator.json: re-formatted');
|
|
1450
|
-
} else {
|
|
1451
|
-
actions.push('⊘ orchestrator.json: already well-formatted');
|
|
1452
|
-
}
|
|
1453
|
-
} catch (err) {
|
|
1454
|
-
actions.push(`✗ orchestrator.json: invalid JSON — ${err.message}`);
|
|
1455
|
-
actions.push(' → Run: npx dual-brain --force (to regenerate from template)');
|
|
1456
|
-
}
|
|
1457
|
-
} else {
|
|
1458
|
-
actions.push('✗ orchestrator.json: not found — run: npx dual-brain');
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
// 4. Re-run hook registration (ensure hooks are in settings.json)
|
|
1462
|
-
if (existsSync(join(claudeDir, 'settings.json')) || existsSync(orchPath)) {
|
|
1463
|
-
try {
|
|
1464
|
-
const settings = generateSettings(workspace);
|
|
1465
|
-
const tmp = join(claudeDir, 'settings.json.tmp.' + process.pid);
|
|
1466
|
-
writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n');
|
|
1467
|
-
renameSync(tmp, join(claudeDir, 'settings.json'));
|
|
1468
|
-
actions.push('✓ settings.json: hooks re-registered');
|
|
1469
|
-
} catch (err) {
|
|
1470
|
-
actions.push(`⚠ settings.json: could not re-register hooks (${err.message})`);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
// Print report
|
|
1475
|
-
console.log('');
|
|
1476
|
-
console.log(` 🔧 dual-brain v${VERSION} — repair`);
|
|
1477
|
-
console.log(' ' + '─'.repeat(40));
|
|
1478
|
-
for (const a of actions) {
|
|
1479
|
-
console.log(` ${a}`);
|
|
1480
|
-
}
|
|
1481
|
-
console.log('');
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
// ─── Uninstall ─────────────────────────────────────────────────────────────
|
|
1485
|
-
|
|
1486
|
-
function cmdUninstall() {
|
|
1487
|
-
const workspace = resolve(process.cwd());
|
|
1488
|
-
const claudeDir = join(workspace, '.claude');
|
|
1489
|
-
const hooksDir = join(claudeDir, 'hooks');
|
|
1490
|
-
const actions = [];
|
|
1491
|
-
|
|
1492
|
-
// 1. Remove dual-brain hooks from settings.json
|
|
1493
|
-
const settingsPath = join(claudeDir, 'settings.json');
|
|
1494
|
-
if (existsSync(settingsPath)) {
|
|
1495
|
-
try {
|
|
1496
|
-
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
1497
|
-
const DUAL_BRAIN_CMDS = [
|
|
1498
|
-
'node .claude/hooks/enforce-tier.mjs',
|
|
1499
|
-
'node .claude/hooks/cost-logger.mjs',
|
|
1500
|
-
];
|
|
1501
|
-
|
|
1502
|
-
if (settings.hooks) {
|
|
1503
|
-
let removedCount = 0;
|
|
1504
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
1505
|
-
const before = settings.hooks[event].length;
|
|
1506
|
-
settings.hooks[event] = settings.hooks[event].filter(entry =>
|
|
1507
|
-
!entry.hooks?.some(h => DUAL_BRAIN_CMDS.includes(h.command))
|
|
1508
|
-
);
|
|
1509
|
-
removedCount += before - settings.hooks[event].length;
|
|
1510
|
-
|
|
1511
|
-
// Clean up empty arrays
|
|
1512
|
-
if (settings.hooks[event].length === 0) {
|
|
1513
|
-
delete settings.hooks[event];
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// Clean up empty hooks object
|
|
1518
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
1519
|
-
delete settings.hooks;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1523
|
-
if (removedCount > 0) {
|
|
1524
|
-
actions.push(`✓ Removed ${removedCount} hook(s) from settings.json`);
|
|
1525
|
-
} else {
|
|
1526
|
-
actions.push('⊘ No dual-brain hooks found in settings.json');
|
|
1527
|
-
}
|
|
1528
|
-
} else {
|
|
1529
|
-
actions.push('⊘ No hooks section in settings.json');
|
|
1530
|
-
}
|
|
1531
|
-
} catch (err) {
|
|
1532
|
-
actions.push(`⚠ Could not parse settings.json: ${err.message}`);
|
|
1533
|
-
}
|
|
1534
|
-
} else {
|
|
1535
|
-
actions.push('⊘ No settings.json found');
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
// 2. Remove state files
|
|
1539
|
-
const stateFiles = [
|
|
1540
|
-
join(claudeDir, 'dual-brain.profile.json'),
|
|
1541
|
-
join(claudeDir, 'dual-brain.memory.json'),
|
|
1542
|
-
join(claudeDir, '.launched'),
|
|
1543
|
-
];
|
|
1544
|
-
|
|
1545
|
-
// Add date-stamped usage files and summary files
|
|
1546
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
1547
|
-
stateFiles.push(join(hooksDir, 'usage.jsonl'));
|
|
1548
|
-
stateFiles.push(join(hooksDir, `usage-${today}.jsonl`));
|
|
1549
|
-
stateFiles.push(join(hooksDir, 'decision-ledger.jsonl'));
|
|
1550
|
-
stateFiles.push(join(hooksDir, '.drift-warned'));
|
|
1551
|
-
stateFiles.push(join(hooksDir, '.budget-alerted'));
|
|
1552
|
-
|
|
1553
|
-
// Scan for any usage-*.jsonl and usage-summary-*.json files
|
|
1554
|
-
try {
|
|
1555
|
-
const files = readdirSync(hooksDir);
|
|
1556
|
-
for (const f of files) {
|
|
1557
|
-
if (f.startsWith('usage-') && f.endsWith('.jsonl')) {
|
|
1558
|
-
stateFiles.push(join(hooksDir, f));
|
|
1559
|
-
}
|
|
1560
|
-
if (f.startsWith('usage-summary-') && f.endsWith('.json')) {
|
|
1561
|
-
stateFiles.push(join(hooksDir, f));
|
|
1562
|
-
}
|
|
1563
|
-
if (f === 'burst-state.json' || f === 'failure-ledger.json') {
|
|
1564
|
-
stateFiles.push(join(hooksDir, f));
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
} catch {}
|
|
1568
|
-
|
|
1569
|
-
// Deduplicate
|
|
1570
|
-
const uniqueFiles = [...new Set(stateFiles)];
|
|
1571
|
-
|
|
1572
|
-
let removedFiles = 0;
|
|
1573
|
-
for (const f of uniqueFiles) {
|
|
1574
|
-
try {
|
|
1575
|
-
if (existsSync(f)) {
|
|
1576
|
-
unlinkSync(f);
|
|
1577
|
-
removedFiles++;
|
|
1578
|
-
}
|
|
1579
|
-
} catch {}
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
if (removedFiles > 0) {
|
|
1583
|
-
actions.push(`✓ Removed ${removedFiles} state file(s)`);
|
|
1584
|
-
} else {
|
|
1585
|
-
actions.push('⊘ No state files to remove');
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
// 3. Print summary
|
|
1589
|
-
console.log('');
|
|
1590
|
-
console.log(` 🧠 dual-brain v${VERSION} — uninstall`);
|
|
1591
|
-
console.log(' ' + '─'.repeat(40));
|
|
1592
|
-
for (const a of actions) {
|
|
1593
|
-
console.log(` ${a}`);
|
|
1594
|
-
}
|
|
1595
|
-
console.log('');
|
|
1596
|
-
console.log(' Hook scripts in .claude/hooks/ were left in place');
|
|
1597
|
-
console.log(' (they are part of the npm package, not your repo).');
|
|
1598
|
-
console.log('');
|
|
1599
|
-
console.log(' To reinstall: npx -y dual-brain');
|
|
1600
|
-
console.log('');
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
// ─── Hook Delegation ───────────────────────────────────────────────────────
|
|
1604
|
-
|
|
1605
|
-
const HOOK_COMMANDS = {
|
|
1606
|
-
review: 'dual-brain-review.mjs',
|
|
1607
|
-
think: 'dual-brain-think.mjs',
|
|
1608
|
-
health: 'health-check.mjs',
|
|
1609
|
-
report: 'session-report.mjs',
|
|
1610
|
-
gate: 'quality-gate.mjs',
|
|
1611
|
-
vibe: 'vibe-router.mjs',
|
|
1612
|
-
plan: 'plan-generator.mjs',
|
|
1613
|
-
cost: 'cost-report.mjs',
|
|
1614
|
-
dispatch: 'gpt-work-dispatcher.mjs',
|
|
1615
|
-
memory: 'vibe-memory.mjs',
|
|
1616
|
-
test: 'test-orchestrator.mjs',
|
|
1617
|
-
ledger: 'decision-ledger.mjs',
|
|
1618
|
-
chains: 'agent-chains.mjs',
|
|
1619
|
-
};
|
|
1620
|
-
|
|
1621
|
-
function resolveHookScript(hookFile) {
|
|
1622
|
-
const workspace = resolve(process.cwd());
|
|
1623
|
-
const installed = join(workspace, '.claude', 'hooks', hookFile);
|
|
1624
|
-
const bundled = join(__dirname, 'hooks', hookFile);
|
|
1625
|
-
return existsSync(installed) ? installed : existsSync(bundled) ? bundled : null;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
function delegateToHook(hookFile) {
|
|
1629
|
-
const script = resolveHookScript(hookFile);
|
|
1630
|
-
|
|
1631
|
-
if (!script) {
|
|
1632
|
-
console.error(` Hook not found: ${hookFile}`);
|
|
1633
|
-
console.error(` Run: ${cmd('npx dual-brain')} to install hooks first.`);
|
|
1634
|
-
process.exit(1);
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
// Pass through all args after the subcommand
|
|
1638
|
-
const extraArgs = process.argv.slice(3);
|
|
1639
|
-
const { status } = spawnSync(process.execPath, [script, ...extraArgs], {
|
|
1640
|
-
stdio: 'inherit',
|
|
1641
|
-
cwd: resolve(process.cwd()),
|
|
1642
|
-
});
|
|
1643
|
-
process.exit(status || 0);
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
function delegateToHookWithArgs(hookFile, args) {
|
|
1647
|
-
const script = resolveHookScript(hookFile);
|
|
1648
|
-
|
|
1649
|
-
if (!script) {
|
|
1650
|
-
console.error(` Hook not found: ${hookFile}`);
|
|
1651
|
-
console.error(` Run: ${cmd('npx dual-brain')} to install hooks first.`);
|
|
1652
|
-
process.exit(1);
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
const { status } = spawnSync(process.execPath, [script, ...args], {
|
|
1656
|
-
stdio: 'inherit',
|
|
1657
|
-
cwd: resolve(process.cwd()),
|
|
1658
|
-
});
|
|
1659
|
-
process.exit(status || 0);
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
1663
|
-
|
|
1664
|
-
// ─── Replit-Tools Check ────────────────────────────────────────────────────
|
|
1665
|
-
|
|
1666
|
-
async function checkReplitTools() {
|
|
1667
|
-
// Only run this check when actually on Replit
|
|
1668
|
-
if (!IS_REPLIT) return;
|
|
1669
|
-
|
|
1670
|
-
// Check if replit-tools is installed
|
|
1671
|
-
const hasReplitTools = existsSync(resolve(process.cwd(), '.replit-tools'));
|
|
1672
|
-
if (hasReplitTools) return;
|
|
1673
|
-
|
|
1674
|
-
const yesFlag = flag('--yes') || flag('-y');
|
|
1675
|
-
|
|
1676
|
-
console.log('');
|
|
1677
|
-
console.log(' ━━━ replit-tools Required ━━━');
|
|
1678
|
-
console.log('');
|
|
1679
|
-
console.log(' dual-brain is built on data-tools/replit-tools by Steve Moraco.');
|
|
1680
|
-
console.log(' replit-tools provides persistent auth, session management, and');
|
|
1681
|
-
console.log(' container survival that dual-brain depends on.');
|
|
1682
|
-
console.log('');
|
|
1683
|
-
|
|
1684
|
-
let install = yesFlag;
|
|
1685
|
-
|
|
1686
|
-
if (!install) {
|
|
1687
|
-
process.stdout.write(' Install now? [Y/n] ');
|
|
1688
|
-
|
|
1689
|
-
if (process.stdin.isTTY) {
|
|
1690
|
-
install = await new Promise((resolve) => {
|
|
1691
|
-
process.stdin.setEncoding('utf8');
|
|
1692
|
-
process.stdin.once('data', (chunk) => {
|
|
1693
|
-
const answer = chunk.trim().toLowerCase();
|
|
1694
|
-
// Default yes: empty input (just Enter) or explicit y/yes
|
|
1695
|
-
resolve(answer === '' || answer === 'y' || answer === 'yes');
|
|
1696
|
-
});
|
|
1697
|
-
process.stdin.resume();
|
|
1698
|
-
});
|
|
1699
|
-
process.stdin.pause();
|
|
1700
|
-
} else {
|
|
1701
|
-
// Non-TTY: default to yes
|
|
1702
|
-
console.log('');
|
|
1703
|
-
install = true;
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1294
|
+
// Restore npm token if missing (for publish access)
|
|
1295
|
+
restoreNpmToken();
|
|
1706
1296
|
|
|
1707
|
-
|
|
1297
|
+
let env = detectEnvironment();
|
|
1298
|
+
const startupUpdateInfo = (subcommand === 'update' || dryRun || jsonOut)
|
|
1299
|
+
? null
|
|
1300
|
+
: checkForUpdate(env.workspace);
|
|
1708
1301
|
|
|
1709
|
-
if (
|
|
1710
|
-
|
|
1302
|
+
if (
|
|
1303
|
+
startupUpdateInfo?.updateAvailable &&
|
|
1304
|
+
process.stdin.isTTY &&
|
|
1305
|
+
process.stdout.isTTY &&
|
|
1306
|
+
!process.env.CI
|
|
1307
|
+
) {
|
|
1711
1308
|
console.log('');
|
|
1712
|
-
const
|
|
1309
|
+
const shouldUpdate = await promptForUpdate(startupUpdateInfo);
|
|
1713
1310
|
console.log('');
|
|
1714
|
-
if (
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
if (
|
|
1721
|
-
|
|
1722
|
-
} else {
|
|
1723
|
-
console.log(' replit-tools may need a shell reload. Continuing with dual-brain install.');
|
|
1311
|
+
if (shouldUpdate) {
|
|
1312
|
+
env = healClaudeAuth(env);
|
|
1313
|
+
env = healCodexAuth(env);
|
|
1314
|
+
const updateMode = resolveMode(env);
|
|
1315
|
+
const updateActions = performUpdate(env.workspace, env, updateMode);
|
|
1316
|
+
printReport(env, updateMode, updateActions);
|
|
1317
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
1318
|
+
launchPanel();
|
|
1724
1319
|
}
|
|
1725
|
-
console.log('');
|
|
1726
|
-
}
|
|
1727
|
-
} else {
|
|
1728
|
-
console.log(' dual-brain will work with reduced functionality. Some features');
|
|
1729
|
-
console.log(' (persistent state, session resume, auth survival) require replit-tools.');
|
|
1730
|
-
console.log('');
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
// ─── Auth Command ──────────────────────────────────────────────────────────
|
|
1735
|
-
|
|
1736
|
-
async function cmdAuth() {
|
|
1737
|
-
console.log('');
|
|
1738
|
-
console.log(` ${BRAND} — Auth Status`);
|
|
1739
|
-
console.log(' ──────────────────────────────────────────────────');
|
|
1740
|
-
|
|
1741
|
-
const claude = getClaudeAuthStatus();
|
|
1742
|
-
if (claude.valid) {
|
|
1743
|
-
const email = claude.email ? ` — ${claude.email}` : '';
|
|
1744
|
-
const sub = claude.subscription ? ` (${claude.subscription})` : '';
|
|
1745
|
-
console.log(` ✓ Claude: authenticated (${Math.round(claude.expiresInHours)}h remaining)${email}${sub}`);
|
|
1746
|
-
} else if (claude.hasRefreshToken) {
|
|
1747
|
-
console.log(' ⟳ Claude: expired — attempting refresh...');
|
|
1748
|
-
const r = await refreshClaudeToken(claude.credPath);
|
|
1749
|
-
console.log(r.success ? ` ✓ Claude: refreshed (${Math.round(r.expiresInHours)}h remaining)` : ' ✗ Claude: refresh failed — run: claude login');
|
|
1750
|
-
} else {
|
|
1751
|
-
console.log(` ✗ Claude: ${claude.credPath ? 'expired' : 'not configured'} — run: claude login`);
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
const codex = getCodexAuthStatus();
|
|
1755
|
-
if (codex.valid) {
|
|
1756
|
-
const email = codex.email ? ` — ${codex.email}` : '';
|
|
1757
|
-
console.log(` ✓ Codex: authenticated (${Math.round(codex.expiresInHours)}h remaining)${email}`);
|
|
1758
|
-
} else if (codex.hasRefreshToken) {
|
|
1759
|
-
console.log(' ⟳ Codex: expired — attempting refresh...');
|
|
1760
|
-
const r = await refreshCodexToken();
|
|
1761
|
-
console.log(r.success ? ` ✓ Codex: refreshed (${Math.round(r.expiresInHours)}h remaining)` : ' ✗ Codex: refresh failed — run: codex login');
|
|
1762
|
-
} else {
|
|
1763
|
-
console.log(' ✗ Codex: not configured — run: codex login');
|
|
1764
|
-
console.log(' (GPT lane will be disabled)');
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
if (IS_REPLIT) {
|
|
1768
|
-
console.log('');
|
|
1769
|
-
console.log(' Replit tip: If login shows localhost errors, use browser-based auth');
|
|
1770
|
-
console.log(' or run the login command from a local machine and copy credentials.');
|
|
1771
|
-
}
|
|
1772
|
-
console.log('');
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
// ─── Preflight Auth ────────────────────────────────────────────────────────
|
|
1776
|
-
|
|
1777
|
-
async function preflightAuth() {
|
|
1778
|
-
const claude = getClaudeAuthStatus();
|
|
1779
|
-
const codex = getCodexAuthStatus();
|
|
1780
|
-
let claudeOk = claude.valid;
|
|
1781
|
-
let codexOk = codex.valid;
|
|
1782
|
-
|
|
1783
|
-
if (!claudeOk && claude.hasRefreshToken) {
|
|
1784
|
-
const r = await refreshClaudeToken(claude.credPath);
|
|
1785
|
-
claudeOk = r.success;
|
|
1786
|
-
if (r.success) console.log(` ⟳ Claude auth refreshed (${Math.round(r.expiresInHours)}h)`);
|
|
1787
|
-
}
|
|
1788
|
-
if (!codexOk && codex.hasRefreshToken) {
|
|
1789
|
-
const r = await refreshCodexToken();
|
|
1790
|
-
codexOk = r.success;
|
|
1791
|
-
if (r.success) console.log(` ⟳ Codex auth refreshed (${Math.round(r.expiresInHours)}h)`);
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
if (!claudeOk) {
|
|
1795
|
-
console.log('');
|
|
1796
|
-
console.log(' ✗ Claude auth required. Run: claude login');
|
|
1797
|
-
if (IS_REPLIT) console.log(' (Use browser flow — localhost callbacks may not work)');
|
|
1798
|
-
console.log('');
|
|
1799
|
-
return false;
|
|
1800
|
-
}
|
|
1801
|
-
if (!codexOk) console.log(' ⚠ Codex not authenticated — GPT lane disabled (Claude-only mode)');
|
|
1802
|
-
return true;
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
// ─── Session Wizard ────────────────────────────────────────────────────────
|
|
1806
|
-
|
|
1807
|
-
function recommendModel() {
|
|
1808
|
-
const claude = getClaudeAuthStatus();
|
|
1809
|
-
const codex = getCodexAuthStatus();
|
|
1810
|
-
if (claude.subscription === 'max' && codex.valid) return { model: 'sonnet', reason: 'Both $100 subs — sonnet for chat, opus auto-escalates for thinking' };
|
|
1811
|
-
if (claude.subscription === 'max') return { model: 'sonnet', reason: '$100 Claude Max — sonnet balances speed + quality' };
|
|
1812
|
-
if (claude.subscription === 'pro') return { model: 'haiku', reason: '$20 Claude Pro — haiku conserves limited credits' };
|
|
1813
|
-
return { model: 'sonnet', reason: 'Default — good balance of speed and capability' };
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
async function askLine(prompt) {
|
|
1817
|
-
if (!process.stdin.isTTY || yesFlag || process.env.CI) return '';
|
|
1818
|
-
process.stdout.write(prompt);
|
|
1819
|
-
const answer = await new Promise(resolve => {
|
|
1820
|
-
process.stdin.setEncoding('utf8');
|
|
1821
|
-
process.stdin.once('data', chunk => resolve(chunk.trim().toLowerCase()));
|
|
1822
|
-
process.stdin.resume();
|
|
1823
|
-
});
|
|
1824
|
-
process.stdin.pause();
|
|
1825
|
-
return answer;
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
async function runSessionWizard(workspace) {
|
|
1829
|
-
const rec = recommendModel();
|
|
1830
|
-
const claude = getClaudeAuthStatus();
|
|
1831
|
-
const codex = getCodexAuthStatus();
|
|
1832
|
-
|
|
1833
|
-
console.log('');
|
|
1834
|
-
console.log(` ${BRAND}`);
|
|
1835
|
-
console.log(` ${BRAND_SUBTITLE}`);
|
|
1836
|
-
console.log('');
|
|
1837
|
-
|
|
1838
|
-
const parts = [];
|
|
1839
|
-
if (claude.subscription) parts.push(`Claude ${claude.subscription}`);
|
|
1840
|
-
if (codex.valid) parts.push('Codex Pro');
|
|
1841
|
-
if (parts.length) {
|
|
1842
|
-
console.log(` Detected: ${parts.join(' + ')}`);
|
|
1843
|
-
console.log(` Recommendation: ${rec.reason}`);
|
|
1844
|
-
console.log('');
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
console.log(' Head session model:');
|
|
1848
|
-
console.log(' [s] Sonnet — fast, cost-effective, great for most work');
|
|
1849
|
-
console.log(' [o] Opus — slower, smarter, best for complex architecture');
|
|
1850
|
-
console.log(' [h] Haiku — fastest, cheapest, good for simple tasks');
|
|
1851
|
-
const modelKey = await askLine(` Choice [${rec.model === 'haiku' ? 'h' : 's'}]: `);
|
|
1852
|
-
let model = rec.model === 'haiku' ? 'haiku' : 'sonnet';
|
|
1853
|
-
if (modelKey === 'o' || modelKey === 'opus') model = 'opus';
|
|
1854
|
-
else if (modelKey === 'h' || modelKey === 'haiku') model = 'haiku';
|
|
1855
|
-
else if (modelKey === 's' || modelKey === 'sonnet') model = 'sonnet';
|
|
1856
|
-
|
|
1857
|
-
if (model === 'opus' && process.stdin.isTTY) {
|
|
1858
|
-
console.log(' ⚠ Opus uses ~5x more credits than Sonnet.');
|
|
1859
|
-
const confirm = await askLine(' Continue with Opus? [y/N] ');
|
|
1860
|
-
if (confirm !== 'y' && confirm !== 'yes') { model = 'sonnet'; console.log(' Switched to Sonnet.'); }
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
console.log('');
|
|
1864
|
-
console.log(' Effort level:');
|
|
1865
|
-
console.log(' [l] Low — faster responses, less thorough');
|
|
1866
|
-
console.log(' [m] Medium — balanced (default)');
|
|
1867
|
-
console.log(' [h] High — thorough, slower, more expensive');
|
|
1868
|
-
const effortKey = await askLine(' Choice [m]: ');
|
|
1869
|
-
let effort = 'medium';
|
|
1870
|
-
if (effortKey === 'l' || effortKey === 'low') effort = 'low';
|
|
1871
|
-
else if (effortKey === 'h' || effortKey === 'high') effort = 'high';
|
|
1872
|
-
|
|
1873
|
-
const prefs = { head_model: model, effort, created_at: new Date().toISOString(), updated_at: new Date().toISOString() };
|
|
1874
|
-
saveSessionPrefs(workspace, prefs);
|
|
1875
|
-
console.log('');
|
|
1876
|
-
console.log(` Saved: ${model} / ${effort} effort`);
|
|
1877
|
-
console.log(' Change anytime: npx dual-brain start --model opus --effort high');
|
|
1878
|
-
return prefs;
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
// ─── Session Launcher ──────────────────────────────────────────────────────
|
|
1882
|
-
|
|
1883
|
-
function launchClaudeSession(workspace, prefs) {
|
|
1884
|
-
const model = prefs.head_model || 'sonnet';
|
|
1885
|
-
const args = [];
|
|
1886
|
-
const helpOut = run('claude', ['--help']);
|
|
1887
|
-
const helpText = (helpOut.stdout || '') + (helpOut.stderr || '');
|
|
1888
|
-
if (helpText.includes('--model')) args.push('--model', model);
|
|
1889
|
-
if (helpText.includes('--resume') || helpText.includes('-c')) args.push('-c');
|
|
1890
|
-
|
|
1891
|
-
console.log('');
|
|
1892
|
-
console.log(` Launching ${model} session...`);
|
|
1893
|
-
console.log(' (Work agents auto-route: haiku/sonnet/opus by task tier)');
|
|
1894
|
-
console.log('');
|
|
1895
|
-
|
|
1896
|
-
const result = spawnSync('claude', args, {
|
|
1897
|
-
stdio: 'inherit',
|
|
1898
|
-
cwd: workspace,
|
|
1899
|
-
env: { ...process.env, DUAL_BRAIN_HEAD_MODEL: model, DUAL_BRAIN_HEAD_EFFORT: prefs.effort || 'medium', DUAL_BRAIN_WORKSPACE: workspace },
|
|
1900
|
-
});
|
|
1901
|
-
process.exit(result.status ?? 0);
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
// ─── Ensure Setup + Launch ─────────────────────────────────────────────────
|
|
1905
|
-
|
|
1906
|
-
async function ensureAndLaunch() {
|
|
1907
|
-
const workspace = resolve(process.cwd());
|
|
1908
|
-
if (!dryRun && !jsonOut) await checkReplitTools();
|
|
1909
|
-
|
|
1910
|
-
const alreadySetUp = existsSync(join(workspace, '.claude', 'hooks'))
|
|
1911
|
-
|| existsSync(join(workspace, '.claude', 'settings.json'));
|
|
1912
|
-
if (!alreadySetUp || force) {
|
|
1913
|
-
const env = detectEnvironment();
|
|
1914
|
-
const mode = resolveMode(env);
|
|
1915
|
-
install(env.workspace, env, mode);
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
const authOk = await preflightAuth();
|
|
1919
|
-
if (!authOk) { console.log(' Fix auth first, then run: npx dual-brain'); process.exit(1); }
|
|
1920
|
-
|
|
1921
|
-
const cliModel = argv.find((a, i) => argv[i - 1] === '--model');
|
|
1922
|
-
const cliEffort = argv.find((a, i) => argv[i - 1] === '--effort');
|
|
1923
|
-
let prefs = loadSessionPrefs(workspace);
|
|
1924
|
-
|
|
1925
|
-
if (!prefs.created_at && !noLaunch) prefs = await runSessionWizard(workspace);
|
|
1926
|
-
if (cliModel) prefs.head_model = cliModel;
|
|
1927
|
-
if (cliEffort) prefs.effort = cliEffort;
|
|
1928
|
-
|
|
1929
|
-
if (noLaunch) { printQuickStart(); return; }
|
|
1930
|
-
launchClaudeSession(workspace, prefs);
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
// ─── Doctor with Auth ──────────────────────────────────────────────────────
|
|
1934
|
-
|
|
1935
|
-
async function cmdDoctorWithAuth() {
|
|
1936
|
-
cmdDoctor();
|
|
1937
|
-
console.log(' Auth Status:');
|
|
1938
|
-
const claude = getClaudeAuthStatus();
|
|
1939
|
-
if (claude.valid) {
|
|
1940
|
-
const email = claude.email ? ` — ${claude.email}` : '';
|
|
1941
|
-
const sub = claude.subscription ? ` (${claude.subscription})` : '';
|
|
1942
|
-
console.log(` ✓ Claude: ${Math.round(claude.expiresInHours)}h remaining${email}${sub}`);
|
|
1943
|
-
} else {
|
|
1944
|
-
console.log(' ✗ Claude: not authenticated — run: npx dual-brain auth');
|
|
1945
|
-
}
|
|
1946
|
-
const codex = getCodexAuthStatus();
|
|
1947
|
-
if (codex.valid) {
|
|
1948
|
-
const email = codex.email ? ` — ${codex.email}` : '';
|
|
1949
|
-
console.log(` ✓ Codex: ${Math.round(codex.expiresInHours)}h remaining${email}`);
|
|
1950
|
-
} else {
|
|
1951
|
-
console.log(' ✗ Codex: not authenticated — run: npx dual-brain auth');
|
|
1952
|
-
}
|
|
1953
|
-
console.log('');
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
1957
|
-
|
|
1958
|
-
async function main() {
|
|
1959
|
-
if (flag('--uninstall')) { cmdUninstall(); return; }
|
|
1960
|
-
|
|
1961
|
-
if (subcommand === 'status') {
|
|
1962
|
-
launchPanel();
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
if (subcommand === 'mode') { cmdMode(); return; }
|
|
1966
|
-
if (subcommand === 'budget') { cmdBudget(); return; }
|
|
1967
|
-
if (subcommand === 'explain') { cmdExplain(); return; }
|
|
1968
|
-
if (subcommand === 'doctor') { await cmdDoctorWithAuth(); return; }
|
|
1969
|
-
if (subcommand === 'reset') { cmdReset(); return; }
|
|
1970
|
-
if (subcommand === 'repair') { cmdRepair(); return; }
|
|
1971
|
-
if (subcommand === 'auth') { await cmdAuth(); return; }
|
|
1972
|
-
if (subcommand === 'start' || subcommand === 'chat') {
|
|
1973
|
-
await ensureAndLaunch(); return;
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
// agent <template> [flags] → agent-templates.mjs --run <template> [flags]
|
|
1977
|
-
if (subcommand === 'agent') {
|
|
1978
|
-
const templateName = positional[1];
|
|
1979
|
-
if (!templateName) {
|
|
1980
|
-
// No template name — fall through to list
|
|
1981
|
-
delegateToHookWithArgs('agent-templates.mjs', ['--list']);
|
|
1982
1320
|
return;
|
|
1983
1321
|
}
|
|
1984
|
-
const extraFlags = process.argv.slice(4); // skip node, install.mjs, 'agent', <template>
|
|
1985
|
-
delegateToHookWithArgs('agent-templates.mjs', ['--run', templateName, ...extraFlags]);
|
|
1986
|
-
return;
|
|
1987
1322
|
}
|
|
1988
1323
|
|
|
1989
|
-
//
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1324
|
+
// Auth self-healing: try to fix expired/missing auth silently
|
|
1325
|
+
env = healClaudeAuth(env);
|
|
1326
|
+
env = healCodexAuth(env);
|
|
1994
1327
|
|
|
1995
|
-
//
|
|
1996
|
-
if (
|
|
1997
|
-
|
|
1998
|
-
if (!chainName) {
|
|
1999
|
-
// No chain name given — fall through to list
|
|
2000
|
-
delegateToHookWithArgs('agent-chains.mjs', ['--list']);
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
const extraFlags = process.argv.slice(4); // skip node, install.mjs, 'chain', <name>
|
|
2004
|
-
delegateToHookWithArgs('agent-chains.mjs', ['--run', chainName, ...extraFlags]);
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
// chains → agent-chains.mjs --list
|
|
2009
|
-
if (subcommand === 'chains') {
|
|
2010
|
-
delegateToHookWithArgs('agent-chains.mjs', ['--list']);
|
|
2011
|
-
return;
|
|
1328
|
+
// Interactive auth guidance if still not authed
|
|
1329
|
+
if (!env.claude.authed || !env.codex.authed) {
|
|
1330
|
+
env = await authGuidance(env);
|
|
2012
1331
|
}
|
|
2013
1332
|
|
|
2014
|
-
//
|
|
2015
|
-
|
|
2016
|
-
// do "<goal>" → ship-captain.mjs --goal "<goal>" [remaining flags]
|
|
2017
|
-
if (subcommand === 'do') {
|
|
2018
|
-
const goal = positional[1] || '';
|
|
2019
|
-
if (!goal) {
|
|
2020
|
-
console.error(' Usage: npx dual-brain do "<goal>"');
|
|
2021
|
-
process.exit(1);
|
|
2022
|
-
}
|
|
2023
|
-
// Collect remaining flags (everything after the goal string)
|
|
2024
|
-
const extraFlags = process.argv.slice(4).filter(a => a.startsWith('-'));
|
|
2025
|
-
delegateToHookWithArgs('ship-captain.mjs', ['--goal', goal, ...extraFlags]);
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
// ship → ship-gate.mjs --ship [remaining flags]
|
|
2030
|
-
if (subcommand === 'ship') {
|
|
2031
|
-
const extraFlags = process.argv.slice(3).filter(a => a.startsWith('-'));
|
|
2032
|
-
delegateToHookWithArgs('ship-gate.mjs', ['--ship', ...extraFlags]);
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// test-run → ship-gate.mjs --test-only
|
|
2037
|
-
if (subcommand === 'test-run') {
|
|
2038
|
-
const extraFlags = process.argv.slice(3).filter(a => a.startsWith('-'));
|
|
2039
|
-
delegateToHookWithArgs('ship-gate.mjs', ['--test-only', ...extraFlags]);
|
|
2040
|
-
return;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
// diff → ship-gate.mjs --diff-only
|
|
2044
|
-
if (subcommand === 'diff') {
|
|
2045
|
-
delegateToHookWithArgs('ship-gate.mjs', ['--diff-only']);
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
// runs → list recent Ship Captain runs from .claude/runs/
|
|
2050
|
-
if (subcommand === 'runs') {
|
|
2051
|
-
const workspace = resolve(process.cwd());
|
|
2052
|
-
const runsDir = join(workspace, '.claude', 'runs');
|
|
2053
|
-
if (!existsSync(runsDir)) {
|
|
2054
|
-
console.log('');
|
|
2055
|
-
console.log(' No Ship Captain runs found.');
|
|
2056
|
-
console.log(` Run: ${cmd('npx dual-brain do "<goal>"')} to start your first run.`);
|
|
2057
|
-
console.log('');
|
|
2058
|
-
return;
|
|
2059
|
-
}
|
|
2060
|
-
let files;
|
|
2061
|
-
try {
|
|
2062
|
-
files = readdirSync(runsDir).filter(f => f.endsWith('.json')).sort().reverse();
|
|
2063
|
-
} catch {
|
|
2064
|
-
files = [];
|
|
2065
|
-
}
|
|
2066
|
-
if (files.length === 0) {
|
|
2067
|
-
console.log('');
|
|
2068
|
-
console.log(' No run records found in .claude/runs/');
|
|
2069
|
-
console.log('');
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
const rows = [];
|
|
2073
|
-
for (const f of files) {
|
|
2074
|
-
try {
|
|
2075
|
-
const rec = JSON.parse(readFileSync(join(runsDir, f), 'utf8'));
|
|
2076
|
-
const id = rec.id || f.replace('.json', '');
|
|
2077
|
-
const goal = (rec.goal || '').slice(0, 35);
|
|
2078
|
-
const status = rec.status || '?';
|
|
2079
|
-
const steps = Array.isArray(rec.steps) ? rec.steps.length : (rec.step_count || '?');
|
|
2080
|
-
const dur = rec.duration_ms != null ? `${(rec.duration_ms / 1000).toFixed(1)}s` : rec.duration || '?';
|
|
2081
|
-
const date = rec.started_at ? rec.started_at.slice(0, 16).replace('T', ' ') : (rec.date || '?');
|
|
2082
|
-
rows.push({ id, goal, status, steps, dur, date });
|
|
2083
|
-
} catch {}
|
|
2084
|
-
}
|
|
2085
|
-
if (rows.length === 0) {
|
|
2086
|
-
console.log(' No readable run records.');
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
const colW = { id: 14, goal: 37, status: 10, steps: 6, dur: 8, date: 16 };
|
|
2090
|
-
const header = [
|
|
2091
|
-
'ID'.padEnd(colW.id), 'GOAL'.padEnd(colW.goal), 'STATUS'.padEnd(colW.status),
|
|
2092
|
-
'STEPS'.padStart(colW.steps), 'DUR'.padStart(colW.dur), 'DATE'.padEnd(colW.date),
|
|
2093
|
-
].join(' ');
|
|
2094
|
-
console.log('');
|
|
2095
|
-
console.log(` Ship Captain Runs (${rows.length})`);
|
|
2096
|
-
console.log(' ' + '─'.repeat(header.length));
|
|
2097
|
-
console.log(' ' + header);
|
|
2098
|
-
console.log(' ' + '─'.repeat(header.length));
|
|
2099
|
-
for (const r of rows) {
|
|
2100
|
-
const line = [
|
|
2101
|
-
String(r.id).padEnd(colW.id), String(r.goal).padEnd(colW.goal),
|
|
2102
|
-
String(r.status).padEnd(colW.status), String(r.steps).padStart(colW.steps),
|
|
2103
|
-
String(r.dur).padStart(colW.dur), String(r.date).padEnd(colW.date),
|
|
2104
|
-
].join(' ');
|
|
2105
|
-
console.log(' ' + line);
|
|
2106
|
-
}
|
|
2107
|
-
console.log('');
|
|
2108
|
-
return;
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
// resume → find most recent incomplete/failed run, offer to re-execute from the failed step
|
|
2112
|
-
if (subcommand === 'resume') {
|
|
2113
|
-
const workspace = resolve(process.cwd());
|
|
2114
|
-
const runsDir = join(workspace, '.claude', 'runs');
|
|
2115
|
-
if (!existsSync(runsDir)) {
|
|
2116
|
-
console.log('');
|
|
2117
|
-
console.log(" No runs found. Start with: npx dual-brain do '<goal>'");
|
|
2118
|
-
console.log('');
|
|
2119
|
-
return;
|
|
2120
|
-
}
|
|
2121
|
-
let files;
|
|
2122
|
-
try {
|
|
2123
|
-
files = readdirSync(runsDir).filter(f => f.endsWith('.json')).sort().reverse();
|
|
2124
|
-
} catch {
|
|
2125
|
-
files = [];
|
|
2126
|
-
}
|
|
2127
|
-
if (files.length === 0) {
|
|
2128
|
-
console.log('');
|
|
2129
|
-
console.log(" No runs found. Start with: npx dual-brain do '<goal>'");
|
|
2130
|
-
console.log('');
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
// Load the most recent run record
|
|
2135
|
-
let rec = null;
|
|
2136
|
-
let recFile = null;
|
|
2137
|
-
try {
|
|
2138
|
-
recFile = files[0];
|
|
2139
|
-
rec = JSON.parse(readFileSync(join(runsDir, recFile), 'utf8'));
|
|
2140
|
-
} catch {}
|
|
2141
|
-
|
|
2142
|
-
if (!rec) {
|
|
2143
|
-
console.log('');
|
|
2144
|
-
console.log(" No runs found. Start with: npx dual-brain do '<goal>'");
|
|
2145
|
-
console.log('');
|
|
2146
|
-
return;
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
const INCOMPLETE_STATUSES = ['failed', 'error', 'partial', 'running', 'pending', 'incomplete', 'aborted'];
|
|
2150
|
-
const steps = Array.isArray(rec.steps) ? rec.steps : [];
|
|
2151
|
-
const isCompleted = (rec.status || '').toLowerCase() === 'completed';
|
|
2152
|
-
const allDone = steps.length > 0 && steps.every(s =>
|
|
2153
|
-
s.status === 'done' || s.status === 'complete' || s.status === 'skipped'
|
|
2154
|
-
);
|
|
2155
|
-
|
|
2156
|
-
if (isCompleted && (allDone || steps.length === 0)) {
|
|
2157
|
-
const goal = rec.goal || '(unknown)';
|
|
2158
|
-
console.log('');
|
|
2159
|
-
console.log(' Last run completed successfully.');
|
|
2160
|
-
console.log(` Start a new one with: npx dual-brain do '${goal}'`);
|
|
2161
|
-
console.log('');
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
// Run has failed/aborted/incomplete — print summary
|
|
2166
|
-
const done = steps.filter(s => s.status === 'done' || s.status === 'complete').length;
|
|
2167
|
-
const failedSteps = steps.filter(s => s.status === 'failed' || s.status === 'error');
|
|
2168
|
-
const incompleteIdx = steps.findIndex(s =>
|
|
2169
|
-
s.status !== 'done' && s.status !== 'complete' && s.status !== 'skipped'
|
|
2170
|
-
);
|
|
2171
|
-
const resumeFromIdx = incompleteIdx >= 0 ? incompleteIdx : steps.length;
|
|
2172
|
-
|
|
2173
|
-
console.log('');
|
|
2174
|
-
console.log(' Last Run State');
|
|
2175
|
-
console.log(' ' + '─'.repeat(50));
|
|
2176
|
-
console.log(` ID: ${rec.id || recFile.replace('.json', '')}`);
|
|
2177
|
-
console.log(` Goal: ${rec.goal || '(unknown)'}`);
|
|
2178
|
-
console.log(` Status: ${rec.status || '(unknown)'}`);
|
|
2179
|
-
if (steps.length > 0) {
|
|
2180
|
-
console.log(` Steps: ${done}/${steps.length} done, ${failedSteps.length} failed`);
|
|
2181
|
-
const failedStep = failedSteps[0];
|
|
2182
|
-
if (failedStep) {
|
|
2183
|
-
const fi = steps.indexOf(failedStep);
|
|
2184
|
-
const fname = failedStep.task || failedStep.name || failedStep.label || `step ${fi}`;
|
|
2185
|
-
console.log(` Failed: ${fname}`);
|
|
2186
|
-
if (failedStep.error) console.log(` Error: ${failedStep.error}`);
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
if (rec.started_at) console.log(` Started: ${rec.started_at.slice(0, 16).replace('T', ' ')}`);
|
|
2190
|
-
if (rec.error) console.log(` Error: ${rec.error}`);
|
|
2191
|
-
console.log('');
|
|
2192
|
-
|
|
2193
|
-
const isResumable = INCOMPLETE_STATUSES.includes((rec.status || '').toLowerCase()) && rec.goal;
|
|
2194
|
-
|
|
2195
|
-
if (!isResumable) {
|
|
2196
|
-
console.log(" Use npx dual-brain do '<goal>' to start fresh");
|
|
2197
|
-
console.log('');
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
const stepNum = resumeFromIdx + 1;
|
|
2202
|
-
const totalSteps = steps.length || '?';
|
|
2203
|
-
console.log(` Resume from step ${stepNum}/${totalSteps}? [Y/n]`);
|
|
2204
|
-
|
|
2205
|
-
const yesFlag = flag('--yes') || flag('-y');
|
|
2206
|
-
|
|
2207
|
-
if (!yesFlag && process.stdin.isTTY) {
|
|
2208
|
-
const { createInterface } = await import('readline');
|
|
2209
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2210
|
-
const answer = await new Promise((res) => {
|
|
2211
|
-
rl.question(' > ', (a) => { rl.close(); res(a.trim()); });
|
|
2212
|
-
});
|
|
2213
|
-
if (answer !== '' && !/^y(es)?$/i.test(answer)) {
|
|
2214
|
-
console.log('');
|
|
2215
|
-
console.log(` Use npx dual-brain do '${rec.goal}' to start fresh`);
|
|
2216
|
-
console.log('');
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
// Reconstruct flags from stored options in the run record
|
|
2222
|
-
const storedOpts = rec.options || {};
|
|
2223
|
-
|
|
2224
|
-
// Dynamically import and invoke ship-captain's executeShipCaptain
|
|
2225
|
-
const captainPath = resolveHookScript('ship-captain.mjs');
|
|
2226
|
-
if (!captainPath) {
|
|
2227
|
-
console.error(' ship-captain.mjs not found. Run: npx dual-brain to reinstall hooks.');
|
|
2228
|
-
process.exit(1);
|
|
2229
|
-
}
|
|
2230
|
-
const { executeShipCaptain } = await import(captainPath);
|
|
2231
|
-
await executeShipCaptain(rec.goal, {
|
|
2232
|
-
yes: yesFlag || storedOpts.yes || false,
|
|
2233
|
-
yolo: storedOpts.yolo || false,
|
|
2234
|
-
careful: storedOpts.careful || false,
|
|
2235
|
-
noPr: storedOpts.noPr || false,
|
|
2236
|
-
mode: storedOpts.mode || null,
|
|
2237
|
-
resumeFrom: resumeFromIdx,
|
|
2238
|
-
resumedFromId: rec.id,
|
|
2239
|
-
});
|
|
2240
|
-
return;
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
// Delegate hook-backed commands
|
|
2244
|
-
if (subcommand && HOOK_COMMANDS[subcommand]) {
|
|
2245
|
-
delegateToHook(HOOK_COMMANDS[subcommand]);
|
|
2246
|
-
return;
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
// ── Bare invocation (no subcommand): setup if needed, then launch ──
|
|
2250
|
-
if (!subcommand) {
|
|
2251
|
-
await ensureAndLaunch();
|
|
2252
|
-
return;
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
if (subcommand === 'setup') {
|
|
2256
|
-
if (!dryRun && !jsonOut) await checkReplitTools();
|
|
2257
|
-
const env = detectEnvironment();
|
|
2258
|
-
const mode = resolveMode(env);
|
|
2259
|
-
const needsGuidance = printGuidedAuth(env);
|
|
2260
|
-
if (needsGuidance && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
2261
|
-
await waitForAuth(env);
|
|
2262
|
-
Object.assign(mode, resolveMode(env));
|
|
2263
|
-
}
|
|
2264
|
-
const actions = install(env.workspace, env, mode);
|
|
2265
|
-
printReport(env, mode, actions);
|
|
2266
|
-
printQuickStart();
|
|
2267
|
-
return;
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
// ── replit-tools check — first thing before auth detection or install ──
|
|
2271
|
-
if (!dryRun && !jsonOut) {
|
|
2272
|
-
await checkReplitTools();
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
const env = detectEnvironment();
|
|
1333
|
+
// Re-resolve mode after healing
|
|
2276
1334
|
const mode = resolveMode(env);
|
|
2277
1335
|
|
|
2278
1336
|
if (dryRun || jsonOut) {
|
|
@@ -2284,17 +1342,30 @@ async function main() {
|
|
|
2284
1342
|
process.exit(0);
|
|
2285
1343
|
}
|
|
2286
1344
|
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
1345
|
+
if (subcommand === 'update') {
|
|
1346
|
+
const actions = performUpdate(env.workspace, env, mode);
|
|
1347
|
+
printReport(env, mode, actions);
|
|
1348
|
+
process.exit(0);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Check for replit-tools on Replit
|
|
1352
|
+
if (env.isReplit && !env.hasReplitTools) {
|
|
1353
|
+
console.log('');
|
|
1354
|
+
console.log(' ⚠️ replit-tools not found — recommended for Replit environments.');
|
|
1355
|
+
console.log(' Dual-brain works best alongside replit-tools for persistent auth,');
|
|
1356
|
+
console.log(' session management, and shell integration.');
|
|
1357
|
+
console.log('');
|
|
1358
|
+
console.log(` Install: ${cmd('npx -y data-tools')}`);
|
|
1359
|
+
console.log('');
|
|
2293
1360
|
}
|
|
2294
1361
|
|
|
2295
1362
|
const actions = install(env.workspace, env, mode);
|
|
2296
1363
|
printReport(env, mode, actions);
|
|
2297
|
-
|
|
1364
|
+
|
|
1365
|
+
// After install, launch the session manager (interactive TTY only)
|
|
1366
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
1367
|
+
launchPanel();
|
|
1368
|
+
}
|
|
2298
1369
|
}
|
|
2299
1370
|
|
|
2300
1371
|
main();
|