myshell-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/data/orchestrator.json +113 -0
- package/package.json +49 -0
- package/src/auth/recovery.mjs +328 -0
- package/src/auth/refresh.mjs +373 -0
- package/src/chef.mjs +348 -0
- package/src/cli/doctor.mjs +568 -0
- package/src/cli/reset.mjs +447 -0
- package/src/cli/status.mjs +379 -0
- package/src/cli.mjs +429 -0
- package/src/commands/doctor.mjs +375 -0
- package/src/commands/help.mjs +324 -0
- package/src/commands/status.mjs +331 -0
- package/src/monitor/health.mjs +486 -0
- package/src/monitor/performance.mjs +442 -0
- package/src/monitor/report.mjs +535 -0
- package/src/orchestrator/classify.mjs +391 -0
- package/src/orchestrator/confidence.mjs +151 -0
- package/src/orchestrator/handoffs.mjs +231 -0
- package/src/orchestrator/review.mjs +222 -0
- package/src/providers/balance.mjs +201 -0
- package/src/providers/claude.mjs +236 -0
- package/src/providers/codex.mjs +255 -0
- package/src/providers/detect.mjs +185 -0
- package/src/providers/errors.mjs +373 -0
- package/src/providers/select.mjs +162 -0
- package/src/repl-enhanced.mjs +417 -0
- package/src/repl.mjs +321 -0
- package/src/state/archive.mjs +366 -0
- package/src/state/atomic.mjs +116 -0
- package/src/state/cleanup.mjs +440 -0
- package/src/state/recovery.mjs +461 -0
- package/src/state/session.mjs +147 -0
- package/src/ui/errors.mjs +456 -0
- package/src/ui/formatter.mjs +327 -0
- package/src/ui/icons.mjs +318 -0
- package/src/ui/progress.mjs +468 -0
- package/templates/prompts/confidence-format.txt +14 -0
- package/templates/prompts/ic-with-feedback.txt +41 -0
- package/templates/prompts/ic.txt +13 -0
- package/templates/prompts/manager-review.txt +40 -0
- package/templates/prompts/manager.txt +14 -0
- package/templates/prompts/worker.txt +12 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recovery.mjs — Authentication failure recovery with helpful guidance
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import { backgroundRefresh, displayRefreshStatus } from './refresh.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a provider CLI is installed
|
|
10
|
+
*/
|
|
11
|
+
function isCliInstalled(command) {
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync('which', [command], {
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
stdio: 'pipe'
|
|
16
|
+
});
|
|
17
|
+
return result.status === 0;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect OS for platform-specific installation guidance
|
|
25
|
+
*/
|
|
26
|
+
function detectOS() {
|
|
27
|
+
const platform = process.platform;
|
|
28
|
+
const arch = process.arch;
|
|
29
|
+
|
|
30
|
+
if (platform === 'darwin') return 'macOS';
|
|
31
|
+
if (platform === 'win32') return 'Windows';
|
|
32
|
+
if (platform === 'linux') {
|
|
33
|
+
// Try to detect specific Linux distribution
|
|
34
|
+
try {
|
|
35
|
+
const result = spawnSync('cat', ['/etc/os-release'], {
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
stdio: 'pipe'
|
|
38
|
+
});
|
|
39
|
+
if (result.status === 0) {
|
|
40
|
+
if (result.stdout.includes('Ubuntu')) return 'Ubuntu';
|
|
41
|
+
if (result.stdout.includes('Debian')) return 'Debian';
|
|
42
|
+
if (result.stdout.includes('CentOS') || result.stdout.includes('Red Hat')) return 'RHEL/CentOS';
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
return 'Linux';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return platform;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Provide installation guidance for missing CLIs
|
|
53
|
+
*/
|
|
54
|
+
export function provideCLIInstallationGuidance(provider) {
|
|
55
|
+
const os = detectOS();
|
|
56
|
+
|
|
57
|
+
console.log(`\n❌ ${provider.toUpperCase()} CLI not found`);
|
|
58
|
+
console.log(`📦 Installation Instructions for ${os}:`);
|
|
59
|
+
|
|
60
|
+
if (provider === 'claude') {
|
|
61
|
+
switch (os) {
|
|
62
|
+
case 'macOS':
|
|
63
|
+
console.log(' # Option 1: Using pip');
|
|
64
|
+
console.log(' pip install anthropic-cli');
|
|
65
|
+
console.log('\n # Option 2: Using Homebrew (if available)');
|
|
66
|
+
console.log(' brew install anthropic-cli');
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 'Ubuntu':
|
|
70
|
+
case 'Debian':
|
|
71
|
+
console.log(' # Install pip if not available');
|
|
72
|
+
console.log(' sudo apt update && sudo apt install python3-pip');
|
|
73
|
+
console.log('\n # Install Claude CLI');
|
|
74
|
+
console.log(' pip3 install anthropic-cli');
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'RHEL/CentOS':
|
|
78
|
+
console.log(' # Install pip if not available');
|
|
79
|
+
console.log(' sudo yum install python3-pip');
|
|
80
|
+
console.log('\n # Install Claude CLI');
|
|
81
|
+
console.log(' pip3 install anthropic-cli');
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'Windows':
|
|
85
|
+
console.log(' # Install via pip (requires Python)');
|
|
86
|
+
console.log(' pip install anthropic-cli');
|
|
87
|
+
console.log('\n # Or download from: https://claude.ai/cli');
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
default:
|
|
91
|
+
console.log(' pip install anthropic-cli');
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('\n🔐 After installation, authenticate:');
|
|
96
|
+
console.log(' claude auth login');
|
|
97
|
+
|
|
98
|
+
} else if (provider === 'codex') {
|
|
99
|
+
switch (os) {
|
|
100
|
+
case 'macOS':
|
|
101
|
+
console.log(' # Using npm (recommended)');
|
|
102
|
+
console.log(' npm install -g @openai/codex');
|
|
103
|
+
console.log('\n # Or using Homebrew');
|
|
104
|
+
console.log(' brew install openai-codex');
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'Ubuntu':
|
|
108
|
+
case 'Debian':
|
|
109
|
+
console.log(' # Install Node.js if not available');
|
|
110
|
+
console.log(' curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -');
|
|
111
|
+
console.log(' sudo apt-get install -y nodejs');
|
|
112
|
+
console.log('\n # Install Codex CLI');
|
|
113
|
+
console.log(' sudo npm install -g @openai/codex');
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case 'RHEL/CentOS':
|
|
117
|
+
console.log(' # Install Node.js if not available');
|
|
118
|
+
console.log(' curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash -');
|
|
119
|
+
console.log(' sudo yum install nodejs npm');
|
|
120
|
+
console.log('\n # Install Codex CLI');
|
|
121
|
+
console.log(' sudo npm install -g @openai/codex');
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'Windows':
|
|
125
|
+
console.log(' # Install via npm (requires Node.js)');
|
|
126
|
+
console.log(' npm install -g @openai/codex');
|
|
127
|
+
console.log('\n # Or download from: https://platform.openai.com/docs/tools/codex-cli');
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
console.log(' npm install -g @openai/codex');
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log('\n🔐 After installation, authenticate:');
|
|
136
|
+
console.log(' codex login');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('\n💡 You need at least one authenticated CLI to use Cortex.');
|
|
140
|
+
console.log(' Both CLIs enable different model tiers and redundancy.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Provide authentication guidance for installed but unauth'd CLIs
|
|
145
|
+
*/
|
|
146
|
+
export function provideAuthGuidance(provider) {
|
|
147
|
+
console.log(`\n⚠️ ${provider.toUpperCase()} CLI found but not authenticated`);
|
|
148
|
+
|
|
149
|
+
if (provider === 'claude') {
|
|
150
|
+
console.log('🔐 Authenticate Claude CLI:');
|
|
151
|
+
console.log(' claude auth login');
|
|
152
|
+
console.log('\n💡 This will open a browser to sign in to your Claude account.');
|
|
153
|
+
console.log(' If running on a server, use: claude auth login --no-browser');
|
|
154
|
+
|
|
155
|
+
} else if (provider === 'codex') {
|
|
156
|
+
console.log('🔐 Authenticate Codex CLI:');
|
|
157
|
+
console.log(' codex login');
|
|
158
|
+
console.log('\n💡 This will prompt for your OpenAI API key.');
|
|
159
|
+
console.log(' Get your key from: https://platform.openai.com/api-keys');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log('\n🔄 After authentication, restart Cortex to refresh provider status.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Handle authentication failures during operation
|
|
167
|
+
*/
|
|
168
|
+
export async function handleAuthFailure(provider, error) {
|
|
169
|
+
console.log(`\n🔴 Authentication failed for ${provider.toUpperCase()}`);
|
|
170
|
+
console.log(` Error: ${error}`);
|
|
171
|
+
|
|
172
|
+
// Try to refresh tokens first
|
|
173
|
+
console.log('\n🔄 Attempting token refresh...');
|
|
174
|
+
const refreshResult = await backgroundRefresh();
|
|
175
|
+
|
|
176
|
+
if (refreshResult.success && !refreshResult.needReauth.includes(provider)) {
|
|
177
|
+
console.log('✅ Token refresh successful! Please retry your request.');
|
|
178
|
+
return { recovered: true, action: 'retry' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// If refresh failed or this provider needs reauth
|
|
182
|
+
console.log('❌ Token refresh failed or expired. Re-authentication required.');
|
|
183
|
+
|
|
184
|
+
if (provider === 'claude') {
|
|
185
|
+
console.log('\n🔐 Re-authenticate Claude CLI:');
|
|
186
|
+
console.log(' claude auth login');
|
|
187
|
+
|
|
188
|
+
// Check if it's a subscription issue
|
|
189
|
+
if (error.includes('subscription') || error.includes('billing')) {
|
|
190
|
+
console.log('\n💳 Possible subscription issue detected.');
|
|
191
|
+
console.log(' Check your Claude Pro subscription at: https://claude.ai/settings');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
} else if (provider === 'codex' || provider === 'openai') {
|
|
195
|
+
console.log('\n🔐 Re-authenticate Codex CLI:');
|
|
196
|
+
console.log(' codex login');
|
|
197
|
+
|
|
198
|
+
// Check if it's an API quota/billing issue
|
|
199
|
+
if (error.includes('quota') || error.includes('billing') || error.includes('insufficient')) {
|
|
200
|
+
console.log('\n💳 Possible API quota/billing issue detected.');
|
|
201
|
+
console.log(' Check your OpenAI account at: https://platform.openai.com/usage');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { recovered: false, action: 'reauth_required' };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Comprehensive provider health check
|
|
210
|
+
*/
|
|
211
|
+
export async function checkProviderHealth(environment) {
|
|
212
|
+
const health = {
|
|
213
|
+
claude: { healthy: false, issues: [] },
|
|
214
|
+
codex: { healthy: false, issues: [] },
|
|
215
|
+
overall: { healthy: false, availableProviders: 0 }
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Check Claude
|
|
219
|
+
if (!environment.claude.installed) {
|
|
220
|
+
health.claude.issues.push('CLI not installed');
|
|
221
|
+
} else if (!environment.claude.authed) {
|
|
222
|
+
health.claude.issues.push('Not authenticated');
|
|
223
|
+
} else {
|
|
224
|
+
health.claude.healthy = true;
|
|
225
|
+
health.overall.availableProviders++;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check Codex
|
|
229
|
+
if (!environment.codex.installed) {
|
|
230
|
+
health.codex.issues.push('CLI not installed');
|
|
231
|
+
} else if (!environment.codex.authed) {
|
|
232
|
+
health.codex.issues.push('Not authenticated');
|
|
233
|
+
} else {
|
|
234
|
+
health.codex.healthy = true;
|
|
235
|
+
health.overall.availableProviders++;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
health.overall.healthy = health.overall.availableProviders > 0;
|
|
239
|
+
|
|
240
|
+
return health;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Auto-recovery sequence with user guidance
|
|
245
|
+
*/
|
|
246
|
+
export async function autoRecovery(environment) {
|
|
247
|
+
console.log('🔧 Running auto-recovery sequence...\n');
|
|
248
|
+
|
|
249
|
+
const health = await checkProviderHealth(environment);
|
|
250
|
+
|
|
251
|
+
// Try token refresh first if we have any auth'd providers
|
|
252
|
+
if (health.overall.availableProviders > 0) {
|
|
253
|
+
console.log('🔄 Refreshing authentication tokens...');
|
|
254
|
+
const refreshResult = await backgroundRefresh();
|
|
255
|
+
displayRefreshStatus(refreshResult);
|
|
256
|
+
|
|
257
|
+
if (refreshResult.success && refreshResult.refreshed.length > 0) {
|
|
258
|
+
console.log('✅ Some tokens refreshed successfully.');
|
|
259
|
+
return { recovered: true, action: 'tokens_refreshed' };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Provide guidance for missing/unauth'd providers
|
|
264
|
+
if (!health.claude.healthy) {
|
|
265
|
+
if (health.claude.issues.includes('CLI not installed')) {
|
|
266
|
+
provideCLIInstallationGuidance('claude');
|
|
267
|
+
} else if (health.claude.issues.includes('Not authenticated')) {
|
|
268
|
+
provideAuthGuidance('claude');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!health.codex.healthy) {
|
|
273
|
+
if (health.codex.issues.includes('CLI not installed')) {
|
|
274
|
+
provideCLIInstallationGuidance('codex');
|
|
275
|
+
} else if (health.codex.issues.includes('Not authenticated')) {
|
|
276
|
+
provideAuthGuidance('codex');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Final status
|
|
281
|
+
if (health.overall.healthy) {
|
|
282
|
+
console.log('\n✅ At least one provider is healthy. Cortex can continue.');
|
|
283
|
+
return { recovered: true, action: 'partial_recovery' };
|
|
284
|
+
} else {
|
|
285
|
+
console.log('\n❌ No healthy providers available. Please follow the guidance above.');
|
|
286
|
+
return { recovered: false, action: 'manual_intervention_required' };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Recovery suggestions based on error type
|
|
292
|
+
*/
|
|
293
|
+
export function getRecoverySuggestions(error) {
|
|
294
|
+
const errorString = error.toString().toLowerCase();
|
|
295
|
+
const suggestions = [];
|
|
296
|
+
|
|
297
|
+
if (errorString.includes('enoent') || errorString.includes('command not found')) {
|
|
298
|
+
suggestions.push('CLI tool not installed or not in PATH');
|
|
299
|
+
suggestions.push('Check installation instructions above');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (errorString.includes('authentication') || errorString.includes('unauthorized') || errorString.includes('401')) {
|
|
303
|
+
suggestions.push('Authentication failed or expired');
|
|
304
|
+
suggestions.push('Try re-authenticating with the CLI');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (errorString.includes('timeout') || errorString.includes('network') || errorString.includes('enotfound')) {
|
|
308
|
+
suggestions.push('Network connectivity issue');
|
|
309
|
+
suggestions.push('Check internet connection and try again');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (errorString.includes('rate limit') || errorString.includes('429')) {
|
|
313
|
+
suggestions.push('API rate limit exceeded');
|
|
314
|
+
suggestions.push('Wait a few minutes before retrying');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (errorString.includes('quota') || errorString.includes('billing') || errorString.includes('subscription')) {
|
|
318
|
+
suggestions.push('Account/billing issue detected');
|
|
319
|
+
suggestions.push('Check your account status and subscription');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (suggestions.length === 0) {
|
|
323
|
+
suggestions.push('Unknown error occurred');
|
|
324
|
+
suggestions.push('Try running with --doctor for more details');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return suggestions;
|
|
328
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* refresh.mjs — OAuth token refresh system for Claude and Codex
|
|
3
|
+
* Adapted from archive/dual-brain/install.mjs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import https from 'https';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join, resolve, dirname } from 'path';
|
|
9
|
+
import { atomicWriteJSON } from '../state/atomic.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HTTP POST form helper
|
|
13
|
+
*/
|
|
14
|
+
function postForm(url, body) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const payload = Buffer.from(new URLSearchParams(body).toString(), 'utf8');
|
|
17
|
+
const req = https.request(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
21
|
+
'Content-Length': payload.length,
|
|
22
|
+
},
|
|
23
|
+
timeout: 8000,
|
|
24
|
+
}, (res) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
27
|
+
res.on('end', () => {
|
|
28
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
29
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
30
|
+
try {
|
|
31
|
+
resolve(JSON.parse(raw));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
reject(err);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
reject(new Error(`HTTP ${res.statusCode || 0}: ${raw}`));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
req.on('timeout', () => req.destroy(new Error('Request timeout')));
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
req.write(payload);
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Safe JSON parsing with fallback
|
|
49
|
+
*/
|
|
50
|
+
function safeParseJson(path) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decode JWT payload without verification
|
|
60
|
+
*/
|
|
61
|
+
function decodeJwtPayload(token) {
|
|
62
|
+
if (!token || typeof token !== 'string') return null;
|
|
63
|
+
const parts = token.split('.');
|
|
64
|
+
if (parts.length < 2) return null;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compute expiration time in hours
|
|
74
|
+
*/
|
|
75
|
+
function computeExpiresInHours(expiresAtMs) {
|
|
76
|
+
if (!Number.isFinite(expiresAtMs)) return null;
|
|
77
|
+
return Math.round(((expiresAtMs - Date.now()) / 3_600_000) * 10) / 10;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get potential Claude credential file paths
|
|
82
|
+
*/
|
|
83
|
+
function getClaudeCredentialPaths(workspace = process.cwd()) {
|
|
84
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
85
|
+
const home = process.env.HOME || '';
|
|
86
|
+
return [
|
|
87
|
+
configDir ? join(configDir, '.credentials.json') : null,
|
|
88
|
+
join(home, '.claude', '.credentials.json'),
|
|
89
|
+
join(home, '.claude', 'credentials.json'),
|
|
90
|
+
resolve(workspace, '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
91
|
+
].filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get Codex auth file path
|
|
96
|
+
*/
|
|
97
|
+
function getCodexAuthPath() {
|
|
98
|
+
const home = process.env.HOME || '';
|
|
99
|
+
return join(home, '.codex', 'auth.json');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Refresh Claude OAuth token
|
|
104
|
+
*/
|
|
105
|
+
async function refreshClaudeToken() {
|
|
106
|
+
const credPaths = getClaudeCredentialPaths();
|
|
107
|
+
|
|
108
|
+
for (const credPath of credPaths) {
|
|
109
|
+
if (!existsSync(credPath)) continue;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const cred = safeParseJson(credPath);
|
|
113
|
+
const oauth = cred?.claudeAiOauth;
|
|
114
|
+
if (!oauth?.refreshToken) continue;
|
|
115
|
+
|
|
116
|
+
const refreshed = await postForm('https://console.anthropic.com/v1/oauth/token', {
|
|
117
|
+
grant_type: 'refresh_token',
|
|
118
|
+
refresh_token: oauth.refreshToken,
|
|
119
|
+
client_id: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const nextOauth = {
|
|
123
|
+
...oauth,
|
|
124
|
+
accessToken: refreshed.access_token || oauth.accessToken,
|
|
125
|
+
refreshToken: refreshed.refresh_token || oauth.refreshToken,
|
|
126
|
+
tokenType: refreshed.token_type || oauth.tokenType,
|
|
127
|
+
scopes: refreshed.scope || oauth.scopes,
|
|
128
|
+
expiresAt: Date.now() + ((refreshed.expires_in || 0) * 1000),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const updated = { ...cred, claudeAiOauth: nextOauth };
|
|
132
|
+
atomicWriteJSON(credPath, updated);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
provider: 'claude',
|
|
137
|
+
expiresInHours: computeExpiresInHours(nextOauth.expiresAt),
|
|
138
|
+
refreshed: true
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
provider: 'claude',
|
|
144
|
+
action: 'reauth_required',
|
|
145
|
+
error: error.message
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
provider: 'claude',
|
|
153
|
+
action: 'no_credentials',
|
|
154
|
+
error: 'No Claude credentials found'
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Refresh OpenAI/Codex token
|
|
160
|
+
*/
|
|
161
|
+
async function refreshOpenAIToken() {
|
|
162
|
+
const authPath = getCodexAuthPath();
|
|
163
|
+
|
|
164
|
+
if (!existsSync(authPath)) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
provider: 'openai',
|
|
168
|
+
action: 'no_credentials',
|
|
169
|
+
error: 'No Codex auth file found'
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const auth = safeParseJson(authPath);
|
|
175
|
+
const tokens = auth?.tokens || auth;
|
|
176
|
+
const refreshToken = tokens?.refresh_token;
|
|
177
|
+
|
|
178
|
+
if (!refreshToken) {
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
provider: 'openai',
|
|
182
|
+
action: 'no_refresh_token',
|
|
183
|
+
error: 'No refresh token available'
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const refreshed = await postForm('https://auth.openai.com/oauth/token', {
|
|
188
|
+
grant_type: 'refresh_token',
|
|
189
|
+
refresh_token: refreshToken,
|
|
190
|
+
client_id: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const updatedTokens = {
|
|
194
|
+
...tokens,
|
|
195
|
+
access_token: refreshed.access_token || tokens.access_token,
|
|
196
|
+
refresh_token: refreshed.refresh_token || tokens.refresh_token,
|
|
197
|
+
id_token: refreshed.id_token || tokens.id_token,
|
|
198
|
+
token_type: refreshed.token_type || tokens.token_type,
|
|
199
|
+
scope: refreshed.scope || tokens.scope,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const updated = auth?.tokens ? { ...auth, tokens: updatedTokens } : updatedTokens;
|
|
203
|
+
atomicWriteJSON(authPath, updated);
|
|
204
|
+
|
|
205
|
+
const payload = decodeJwtPayload(updatedTokens.access_token);
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
provider: 'openai',
|
|
209
|
+
expiresInHours: payload?.exp ? computeExpiresInHours(payload.exp * 1000) : null,
|
|
210
|
+
refreshed: true
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
provider: 'openai',
|
|
216
|
+
action: 'reauth_required',
|
|
217
|
+
error: error.message
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Refresh all available tokens
|
|
224
|
+
*/
|
|
225
|
+
export async function refreshTokens() {
|
|
226
|
+
const results = [];
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const claudeResult = await refreshClaudeToken();
|
|
230
|
+
results.push(claudeResult);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
results.push({
|
|
233
|
+
success: false,
|
|
234
|
+
provider: 'claude',
|
|
235
|
+
error: error.message,
|
|
236
|
+
action: 'reauth_required'
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const openaiResult = await refreshOpenAIToken();
|
|
242
|
+
results.push(openaiResult);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
results.push({
|
|
245
|
+
success: false,
|
|
246
|
+
provider: 'openai',
|
|
247
|
+
error: error.message,
|
|
248
|
+
action: 'reauth_required'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const successful = results.filter(r => r.success);
|
|
253
|
+
const needReauth = results.filter(r => r.action === 'reauth_required');
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
success: successful.length > 0,
|
|
257
|
+
results,
|
|
258
|
+
refreshed: successful.filter(r => r.refreshed),
|
|
259
|
+
needReauth: needReauth,
|
|
260
|
+
hasValidAuth: successful.length > 0
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get refresh state file path
|
|
266
|
+
*/
|
|
267
|
+
function getRefreshStatePath() {
|
|
268
|
+
const home = process.env.HOME || '';
|
|
269
|
+
const dir = join(home, '.cortex', 'auth');
|
|
270
|
+
if (!existsSync(dir)) {
|
|
271
|
+
mkdirSync(dir, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
return join(dir, 'refresh-state.json');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Save refresh state for background processing
|
|
278
|
+
*/
|
|
279
|
+
export function saveRefreshState(results) {
|
|
280
|
+
const statePath = getRefreshStatePath();
|
|
281
|
+
const state = {
|
|
282
|
+
lastRefresh: new Date().toISOString(),
|
|
283
|
+
results,
|
|
284
|
+
nextRefreshDue: new Date(Date.now() + (23 * 60 * 60 * 1000)).toISOString() // 23 hours
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
atomicWriteJSON(statePath, state);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.warn('Failed to save refresh state:', error.message);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Load refresh state
|
|
296
|
+
*/
|
|
297
|
+
export function loadRefreshState() {
|
|
298
|
+
const statePath = getRefreshStatePath();
|
|
299
|
+
|
|
300
|
+
if (!existsSync(statePath)) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
return safeParseJson(statePath);
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if refresh is due (run every 24 hours)
|
|
313
|
+
*/
|
|
314
|
+
export function isRefreshDue() {
|
|
315
|
+
const state = loadRefreshState();
|
|
316
|
+
if (!state?.nextRefreshDue) return true;
|
|
317
|
+
|
|
318
|
+
return new Date() >= new Date(state.nextRefreshDue);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Background token refresh with error handling
|
|
323
|
+
*/
|
|
324
|
+
export async function backgroundRefresh() {
|
|
325
|
+
if (!isRefreshDue()) {
|
|
326
|
+
return { skipped: true, reason: 'Not due for refresh' };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const results = await refreshTokens();
|
|
331
|
+
saveRefreshState(results.results);
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
hasValidAuth: results.hasValidAuth,
|
|
336
|
+
refreshed: results.refreshed.map(r => r.provider),
|
|
337
|
+
needReauth: results.needReauth.map(r => r.provider)
|
|
338
|
+
};
|
|
339
|
+
} catch (error) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
error: error.message
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Display refresh status to user
|
|
349
|
+
*/
|
|
350
|
+
export function displayRefreshStatus(refreshResult) {
|
|
351
|
+
if (refreshResult.skipped) {
|
|
352
|
+
return; // Silent when not due
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (refreshResult.success) {
|
|
356
|
+
if (refreshResult.refreshed.length > 0) {
|
|
357
|
+
console.log(`🔄 Refreshed tokens: ${refreshResult.refreshed.join(', ')}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (refreshResult.needReauth.length > 0) {
|
|
361
|
+
console.log(`\n⚠️ Re-authentication required for: ${refreshResult.needReauth.join(', ')}`);
|
|
362
|
+
for (const provider of refreshResult.needReauth) {
|
|
363
|
+
if (provider === 'claude') {
|
|
364
|
+
console.log(' Run: claude auth login');
|
|
365
|
+
} else if (provider === 'openai') {
|
|
366
|
+
console.log(' Run: codex login');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} else if (refreshResult.error) {
|
|
371
|
+
console.log(`⚠️ Token refresh failed: ${refreshResult.error}`);
|
|
372
|
+
}
|
|
373
|
+
}
|