@plexor-dev/claude-code-plugin-staging 0.1.0-beta.21 → 0.1.0-beta.23
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/commands/plexor-enabled.js +2 -1
- package/commands/plexor-login.js +3 -2
- package/commands/plexor-logout.js +2 -1
- package/commands/plexor-setup.js +4 -3
- package/commands/plexor-setup.md +1 -1
- package/commands/plexor-status.js +61 -3
- package/commands/plexor-uninstall.js +30 -2
- package/lib/plexor-client.js +1 -1
- package/lib/settings-manager.js +123 -9
- package/package.json +1 -1
- package/scripts/postinstall.js +36 -0
- package/scripts/uninstall.js +33 -1
|
@@ -243,7 +243,8 @@ function main() {
|
|
|
243
243
|
console.log(`│ Run /plexor-setup first. │`);
|
|
244
244
|
console.log(`├─────────────────────────────────────────────┤`);
|
|
245
245
|
console.log(`│ Get your API key at: │`);
|
|
246
|
-
console.log(`│ https://staging.plexorlabs.com
|
|
246
|
+
console.log(`│ https://staging.plexorlabs.com/ │`);
|
|
247
|
+
console.log(`│ api-keys.html │`);
|
|
247
248
|
console.log(`└─────────────────────────────────────────────┘`);
|
|
248
249
|
process.exit(1);
|
|
249
250
|
}
|
package/commands/plexor-login.js
CHANGED
|
@@ -262,7 +262,8 @@ async function main() {
|
|
|
262
262
|
console.log(`│ Human first-run: /plexor-setup │`);
|
|
263
263
|
console.log(`├─────────────────────────────────────────────┤`);
|
|
264
264
|
console.log(`│ Get your API key at: │`);
|
|
265
|
-
console.log(`│ https://staging.plexorlabs.com
|
|
265
|
+
console.log(`│ https://staging.plexorlabs.com/ │`);
|
|
266
|
+
console.log(`│ api-keys.html │`);
|
|
266
267
|
console.log(`├─────────────────────────────────────────────┤`);
|
|
267
268
|
console.log(`│ Secure usage (recommended): │`);
|
|
268
269
|
console.log(`│ echo "plx_..." | /plexor-login │`);
|
|
@@ -340,7 +341,7 @@ async function main() {
|
|
|
340
341
|
}
|
|
341
342
|
console.log(`├─────────────────────────────────────────────┤`);
|
|
342
343
|
console.log(`│ Human first-run: /plexor-setup │`);
|
|
343
|
-
console.log(`│
|
|
344
|
+
console.log(`│ Restart Claude before testing prompts │`);
|
|
344
345
|
console.log(`└─────────────────────────────────────────────┘`);
|
|
345
346
|
} catch (err) {
|
|
346
347
|
console.error(`┌─────────────────────────────────────────────┐`);
|
|
@@ -102,7 +102,8 @@ function main() {
|
|
|
102
102
|
const currentSettings = settingsManager.load();
|
|
103
103
|
const hadPriorAuth = Boolean(
|
|
104
104
|
currentSettings.env?.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY ||
|
|
105
|
-
currentSettings.env?.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN
|
|
105
|
+
currentSettings.env?.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN ||
|
|
106
|
+
currentSettings.env?.PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY
|
|
106
107
|
);
|
|
107
108
|
|
|
108
109
|
// Clear credentials
|
package/commands/plexor-setup.js
CHANGED
|
@@ -128,9 +128,9 @@ function printReceipt({ user, gateway, previousAuthPreserved, verifyResult }) {
|
|
|
128
128
|
console.log(line(`Previous Claude auth: ${previousAuthPreserved ? 'Saved' : 'None found'}`));
|
|
129
129
|
console.log('├─────────────────────────────────────────────┤');
|
|
130
130
|
if (verifyResult.ok) {
|
|
131
|
-
console.log(line('
|
|
131
|
+
console.log(line('Plexor routing is configured'));
|
|
132
|
+
console.log(line('Restart Claude, then send a prompt'));
|
|
132
133
|
console.log(line('Logout/uninstall restores prior auth'));
|
|
133
|
-
console.log(line('Next: Run /plexor-status for health + stats'));
|
|
134
134
|
} else {
|
|
135
135
|
console.log(line('Claude routing was configured'));
|
|
136
136
|
console.log(line('Verification did not pass'));
|
|
@@ -217,7 +217,8 @@ async function main() {
|
|
|
217
217
|
const currentSettings = settingsManager.load();
|
|
218
218
|
const previousAuthPreserved = Boolean(
|
|
219
219
|
currentSettings.env?.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY ||
|
|
220
|
-
currentSettings.env?.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN
|
|
220
|
+
currentSettings.env?.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN ||
|
|
221
|
+
currentSettings.env?.PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY
|
|
221
222
|
);
|
|
222
223
|
|
|
223
224
|
const verifyResult = skipVerify
|
package/commands/plexor-setup.md
CHANGED
|
@@ -13,7 +13,7 @@ node ~/.claude/plugins/plexor/commands/plexor-setup.js $ARGUMENTS
|
|
|
13
13
|
|
|
14
14
|
If the user did not provide a key yet, ask them:
|
|
15
15
|
|
|
16
|
-
`Please paste your Plexor API key (starts with plx_). You can get one at https://staging.plexorlabs.com.`
|
|
16
|
+
`Please paste your Plexor API key (starts with plx_). You can get one at https://staging.plexorlabs.com/api-keys.html.`
|
|
17
17
|
|
|
18
18
|
After the user replies with the key, run:
|
|
19
19
|
|
|
@@ -12,6 +12,7 @@ const https = require('https');
|
|
|
12
12
|
// Import centralized constants with HOME directory validation
|
|
13
13
|
const { HOME_DIR, CONFIG_PATH, SESSION_PATH, SESSION_TIMEOUT_MS } = require('../lib/constants');
|
|
14
14
|
const CLAUDE_SETTINGS_PATH = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
15
|
+
const CLAUDE_STATE_PATH = path.join(HOME_DIR, '.claude.json');
|
|
15
16
|
|
|
16
17
|
function isManagedGatewayUrl(baseUrl = '') {
|
|
17
18
|
return (
|
|
@@ -33,6 +34,49 @@ function getPlexorAuthKey(env = {}) {
|
|
|
33
34
|
return apiKey || authToken || '';
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function getDirectClaudeAuthState() {
|
|
38
|
+
try {
|
|
39
|
+
const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
|
|
40
|
+
const settings = JSON.parse(data);
|
|
41
|
+
const env = settings.env || {};
|
|
42
|
+
const baseUrl = env.ANTHROPIC_BASE_URL || '';
|
|
43
|
+
const apiKey = env.ANTHROPIC_API_KEY || '';
|
|
44
|
+
const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
|
|
45
|
+
|
|
46
|
+
if (!isManagedGatewayUrl(baseUrl)) {
|
|
47
|
+
if (apiKey && !apiKey.startsWith('plx_')) {
|
|
48
|
+
return { present: true, source: 'Claude settings' };
|
|
49
|
+
}
|
|
50
|
+
if (authToken && !authToken.startsWith('plx_')) {
|
|
51
|
+
return { present: true, source: 'Claude settings' };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Ignore missing or unreadable Claude settings.
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const data = fs.readFileSync(CLAUDE_STATE_PATH, 'utf8');
|
|
60
|
+
const state = JSON.parse(data);
|
|
61
|
+
if (state.primaryApiKey) {
|
|
62
|
+
return { present: true, source: '/login managed key' };
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore missing or unreadable Claude state.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const envApiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
69
|
+
const envAuthToken = process.env.ANTHROPIC_AUTH_TOKEN || '';
|
|
70
|
+
if (envApiKey && !envApiKey.startsWith('plx_')) {
|
|
71
|
+
return { present: true, source: 'shell env' };
|
|
72
|
+
}
|
|
73
|
+
if (envAuthToken && !envAuthToken.startsWith('plx_')) {
|
|
74
|
+
return { present: true, source: 'shell env' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { present: false, source: null };
|
|
78
|
+
}
|
|
79
|
+
|
|
36
80
|
/**
|
|
37
81
|
* Check if Claude Code is actually routing through Plexor
|
|
38
82
|
* by reading ~/.claude/settings.json
|
|
@@ -204,8 +248,24 @@ async function main() {
|
|
|
204
248
|
const localCache = config.settings?.localCacheEnabled ?? false;
|
|
205
249
|
const apiUrl = config.settings?.apiUrl || 'https://api.plexor.dev';
|
|
206
250
|
const verification = config.health || {};
|
|
251
|
+
const routing = getRoutingStatus();
|
|
252
|
+
const partialState = detectPartialState();
|
|
253
|
+
const directClaudeAuth = getDirectClaudeAuthState();
|
|
207
254
|
|
|
208
255
|
if (!apiKey) {
|
|
256
|
+
if (!enabled && !routing.active && !partialState.partial && directClaudeAuth.present) {
|
|
257
|
+
printHealthSummary({
|
|
258
|
+
installed: 'OK',
|
|
259
|
+
connected: 'Direct Claude auth',
|
|
260
|
+
routing: 'Inactive',
|
|
261
|
+
verified: 'Restored',
|
|
262
|
+
nextAction: 'Run /plexor-setup'
|
|
263
|
+
});
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log(` Claude is using ${directClaudeAuth.source}. Plexor is inactive.`);
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
268
|
+
|
|
209
269
|
printHealthSummary({
|
|
210
270
|
installed: 'OK',
|
|
211
271
|
connected: 'Missing key',
|
|
@@ -277,7 +337,7 @@ async function main() {
|
|
|
277
337
|
let dashboardUrl = 'https://plexor.dev/dashboard';
|
|
278
338
|
try {
|
|
279
339
|
const url = new URL(apiUrl);
|
|
280
|
-
if (url.hostname
|
|
340
|
+
if (url.hostname === 'staging.api.plexor.dev') {
|
|
281
341
|
dashboardUrl = 'https://staging.plexorlabs.com/dashboard';
|
|
282
342
|
} else {
|
|
283
343
|
const host = url.hostname.replace(/^api\./, '').replace(/\.api\./, '.');
|
|
@@ -321,10 +381,8 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
|
|
|
321
381
|
` : '';
|
|
322
382
|
|
|
323
383
|
// Get routing status from Claude settings.json
|
|
324
|
-
const routing = getRoutingStatus();
|
|
325
384
|
const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
|
|
326
385
|
const envLabel = routing.isStaging ? '(staging)' : '(production)';
|
|
327
|
-
const partialState = detectPartialState();
|
|
328
386
|
const stateMismatch = checkStateMismatch(enabled, routing.active);
|
|
329
387
|
const connectedState = userFetchWorked ? 'OK' : 'Unknown';
|
|
330
388
|
const verifiedState = verification.verified
|
|
@@ -82,6 +82,9 @@ function loadSettingsManager() {
|
|
|
82
82
|
*/
|
|
83
83
|
function disableRoutingManually() {
|
|
84
84
|
const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
|
|
85
|
+
const statePath = path.join(HOME_DIR, '.claude.json');
|
|
86
|
+
const previousPrimaryApiKeyEnv = 'PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY';
|
|
87
|
+
const crypto = require('crypto');
|
|
85
88
|
|
|
86
89
|
try {
|
|
87
90
|
if (!fs.existsSync(settingsPath)) {
|
|
@@ -113,7 +116,9 @@ function disableRoutingManually() {
|
|
|
113
116
|
const hasPlexorAuthToken = isPlexorApiKey(settings.env.ANTHROPIC_AUTH_TOKEN || '');
|
|
114
117
|
const hasPlexorApiKey = isPlexorApiKey(settings.env.ANTHROPIC_API_KEY || '');
|
|
115
118
|
|
|
116
|
-
|
|
119
|
+
const hasPreviousPrimaryApiKey = Boolean(settings.env[previousPrimaryApiKeyEnv]);
|
|
120
|
+
|
|
121
|
+
if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey && !hasPreviousPrimaryApiKey) {
|
|
117
122
|
return { success: true, message: 'Plexor routing not active' };
|
|
118
123
|
}
|
|
119
124
|
|
|
@@ -136,6 +141,30 @@ function disableRoutingManually() {
|
|
|
136
141
|
|
|
137
142
|
delete settings.env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY;
|
|
138
143
|
delete settings.env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN;
|
|
144
|
+
const previousPrimaryApiKey = settings.env[previousPrimaryApiKeyEnv] || '';
|
|
145
|
+
if (previousPrimaryApiKey) {
|
|
146
|
+
let claudeState = {};
|
|
147
|
+
try {
|
|
148
|
+
if (fs.existsSync(statePath)) {
|
|
149
|
+
const stateData = fs.readFileSync(statePath, 'utf8');
|
|
150
|
+
if (stateData && stateData.trim()) {
|
|
151
|
+
claudeState = JSON.parse(stateData);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
claudeState = {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!claudeState.primaryApiKey) {
|
|
159
|
+
claudeState.primaryApiKey = previousPrimaryApiKey;
|
|
160
|
+
}
|
|
161
|
+
delete settings.env[previousPrimaryApiKeyEnv];
|
|
162
|
+
|
|
163
|
+
const stateTempId = crypto.randomBytes(8).toString('hex');
|
|
164
|
+
const stateTempPath = path.join(HOME_DIR, `.claude.${stateTempId}.tmp`);
|
|
165
|
+
fs.writeFileSync(stateTempPath, JSON.stringify(claudeState, null, 2), { mode: 0o600 });
|
|
166
|
+
fs.renameSync(stateTempPath, statePath);
|
|
167
|
+
}
|
|
139
168
|
|
|
140
169
|
// Clean up empty env block
|
|
141
170
|
if (Object.keys(settings.env).length === 0) {
|
|
@@ -143,7 +172,6 @@ function disableRoutingManually() {
|
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
// Atomic write
|
|
146
|
-
const crypto = require('crypto');
|
|
147
175
|
const tempId = crypto.randomBytes(8).toString('hex');
|
|
148
176
|
const tempPath = path.join(CLAUDE_DIR, `.settings.${tempId}.tmp`);
|
|
149
177
|
|
package/lib/plexor-client.js
CHANGED
|
@@ -31,7 +31,7 @@ class PlexorClient {
|
|
|
31
31
|
'Content-Type': 'application/json',
|
|
32
32
|
'X-API-Key': this.apiKey,
|
|
33
33
|
'X-Plexor-Key': this.apiKey,
|
|
34
|
-
'User-Agent': 'plexor-claude-code-plugin/0.1.0-beta.
|
|
34
|
+
'User-Agent': 'plexor-claude-code-plugin/0.1.0-beta.23'
|
|
35
35
|
},
|
|
36
36
|
timeout: this.timeout
|
|
37
37
|
};
|
package/lib/settings-manager.js
CHANGED
|
@@ -16,6 +16,7 @@ const os = require('os');
|
|
|
16
16
|
const crypto = require('crypto');
|
|
17
17
|
|
|
18
18
|
const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
|
|
19
|
+
const CLAUDE_STATE_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude.json');
|
|
19
20
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
20
21
|
const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
|
|
21
22
|
|
|
@@ -24,6 +25,7 @@ const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
|
24
25
|
const PLEXOR_PROD_URL = 'https://api.plexor.dev/gateway/anthropic';
|
|
25
26
|
const PREVIOUS_API_KEY_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_API_KEY';
|
|
26
27
|
const PREVIOUS_AUTH_TOKEN_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN';
|
|
28
|
+
const PREVIOUS_PRIMARY_API_KEY_ENV = 'PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY';
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Check if a base URL is a Plexor-managed gateway URL.
|
|
@@ -118,10 +120,39 @@ function clearPlexorRoutingEnv(env = {}) {
|
|
|
118
120
|
return true;
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
function stashDirectPrimaryApiKey(env = {}, claudeState = {}, plexorApiKey = '') {
|
|
124
|
+
const currentPrimaryApiKey = claudeState.primaryApiKey || '';
|
|
125
|
+
if (!currentPrimaryApiKey || isPlexorApiKey(currentPrimaryApiKey) || currentPrimaryApiKey === plexorApiKey) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
env[PREVIOUS_PRIMARY_API_KEY_ENV] = currentPrimaryApiKey;
|
|
130
|
+
delete claudeState.primaryApiKey;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function restoreDirectPrimaryApiKey(env = {}, claudeState = {}, options = {}) {
|
|
135
|
+
const { consumeBackup = true } = options;
|
|
136
|
+
const previousPrimaryApiKey = env[PREVIOUS_PRIMARY_API_KEY_ENV] || '';
|
|
137
|
+
if (!previousPrimaryApiKey) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!claudeState.primaryApiKey) {
|
|
142
|
+
claudeState.primaryApiKey = previousPrimaryApiKey;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (consumeBackup) {
|
|
146
|
+
delete env[PREVIOUS_PRIMARY_API_KEY_ENV];
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
class ClaudeSettingsManager {
|
|
122
152
|
constructor() {
|
|
123
153
|
this.settingsPath = SETTINGS_PATH;
|
|
124
154
|
this.claudeDir = CLAUDE_DIR;
|
|
155
|
+
this.claudeStatePath = CLAUDE_STATE_PATH;
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
/**
|
|
@@ -209,6 +240,56 @@ class ClaudeSettingsManager {
|
|
|
209
240
|
return null;
|
|
210
241
|
}
|
|
211
242
|
|
|
243
|
+
loadClaudeState() {
|
|
244
|
+
try {
|
|
245
|
+
if (!fs.existsSync(this.claudeStatePath)) {
|
|
246
|
+
return {};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const data = fs.readFileSync(this.claudeStatePath, 'utf8');
|
|
250
|
+
if (!data || data.trim() === '') {
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const parsed = JSON.parse(data);
|
|
255
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return parsed;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err.code === 'ENOENT' || err instanceof SyntaxError) {
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
console.warn('Warning: Failed to load Claude auth state:', err.message);
|
|
265
|
+
return {};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
saveClaudeState(state) {
|
|
270
|
+
try {
|
|
271
|
+
const stateDir = path.dirname(this.claudeStatePath);
|
|
272
|
+
if (!fs.existsSync(stateDir)) {
|
|
273
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
277
|
+
const tempPath = path.join(stateDir, `.claude.${tempId}.tmp`);
|
|
278
|
+
const content = JSON.stringify(state, null, 2);
|
|
279
|
+
fs.writeFileSync(tempPath, content, { mode: 0o600 });
|
|
280
|
+
fs.renameSync(tempPath, this.claudeStatePath);
|
|
281
|
+
return true;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
284
|
+
console.error(`Error: Cannot write to ${this.claudeStatePath}`);
|
|
285
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
286
|
+
} else {
|
|
287
|
+
console.error('Failed to save Claude auth state:', err.message);
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
212
293
|
/**
|
|
213
294
|
* Save Claude settings using atomic write pattern
|
|
214
295
|
* Prevents race conditions by writing to temp file then renaming
|
|
@@ -265,6 +346,7 @@ class ClaudeSettingsManager {
|
|
|
265
346
|
|
|
266
347
|
try {
|
|
267
348
|
const settings = this.load();
|
|
349
|
+
const claudeState = this.loadClaudeState();
|
|
268
350
|
|
|
269
351
|
// Initialize env block if doesn't exist
|
|
270
352
|
if (!settings.env) {
|
|
@@ -275,15 +357,23 @@ class ClaudeSettingsManager {
|
|
|
275
357
|
// runtime path uses the gateway instead of any previously saved direct key.
|
|
276
358
|
settings.env.ANTHROPIC_BASE_URL = apiUrl;
|
|
277
359
|
setPlexorAuthKey(settings.env, apiKey);
|
|
360
|
+
const claudeStateChanged = stashDirectPrimaryApiKey(settings.env, claudeState, apiKey);
|
|
278
361
|
|
|
279
362
|
const success = this.save(settings);
|
|
363
|
+
if (!success) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (claudeStateChanged && !this.saveClaudeState(claudeState)) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
280
370
|
|
|
281
371
|
if (success) {
|
|
282
372
|
console.log(`✓ Plexor routing enabled`);
|
|
283
373
|
console.log(` Base URL: ${apiUrl}`);
|
|
284
374
|
console.log(` API Key: ${apiKey.substring(0, 12)}...`);
|
|
285
|
-
console.log(`\n
|
|
286
|
-
console.log(`
|
|
375
|
+
console.log(`\n New Claude launches will route through Plexor.`);
|
|
376
|
+
console.log(` Restart Claude before testing routed prompts.`);
|
|
287
377
|
}
|
|
288
378
|
|
|
289
379
|
return success;
|
|
@@ -303,31 +393,50 @@ class ClaudeSettingsManager {
|
|
|
303
393
|
disablePlexorRouting() {
|
|
304
394
|
try {
|
|
305
395
|
const settings = this.load();
|
|
396
|
+
const claudeState = this.loadClaudeState();
|
|
397
|
+
const originalClaudeState = JSON.parse(JSON.stringify(claudeState));
|
|
306
398
|
|
|
307
399
|
if (!settings.env) {
|
|
308
400
|
console.log('Plexor routing is not currently enabled.');
|
|
309
401
|
return true;
|
|
310
402
|
}
|
|
311
403
|
|
|
312
|
-
|
|
404
|
+
const routingChanged = clearPlexorRoutingEnv(settings.env);
|
|
405
|
+
const claudeStateChanged = restoreDirectPrimaryApiKey(settings.env, claudeState, {
|
|
406
|
+
consumeBackup: false
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (!routingChanged && !claudeStateChanged) {
|
|
313
410
|
console.log('Plexor routing is not currently enabled.');
|
|
314
411
|
return true;
|
|
315
412
|
}
|
|
316
413
|
|
|
414
|
+
if (claudeStateChanged && !this.saveClaudeState(claudeState)) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (claudeStateChanged) {
|
|
419
|
+
delete settings.env[PREVIOUS_PRIMARY_API_KEY_ENV];
|
|
420
|
+
}
|
|
421
|
+
|
|
317
422
|
// Clean up empty env block
|
|
318
423
|
if (Object.keys(settings.env).length === 0) {
|
|
319
424
|
delete settings.env;
|
|
320
425
|
}
|
|
321
426
|
|
|
322
427
|
const success = this.save(settings);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
428
|
+
if (!success) {
|
|
429
|
+
if (claudeStateChanged) {
|
|
430
|
+
this.saveClaudeState(originalClaudeState);
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
328
433
|
}
|
|
329
434
|
|
|
330
|
-
|
|
435
|
+
console.log('✓ Plexor routing disabled');
|
|
436
|
+
console.log(' New Claude launches will connect directly to Anthropic.');
|
|
437
|
+
console.log(' Restart Claude if it is still open.');
|
|
438
|
+
|
|
439
|
+
return true;
|
|
331
440
|
} catch (err) {
|
|
332
441
|
console.error('Failed to disable Plexor routing:', err.message);
|
|
333
442
|
return false;
|
|
@@ -369,6 +478,7 @@ class ClaudeSettingsManager {
|
|
|
369
478
|
detectPartialState() {
|
|
370
479
|
try {
|
|
371
480
|
const settings = this.load();
|
|
481
|
+
const claudeState = this.loadClaudeState();
|
|
372
482
|
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
|
|
373
483
|
const authKey = getPlexorAuthKey(settings.env);
|
|
374
484
|
const isPlexorUrl = isManagedGatewayUrl(baseUrl);
|
|
@@ -379,6 +489,9 @@ class ClaudeSettingsManager {
|
|
|
379
489
|
if (isPlexorUrl && !isPlexorApiKey(authKey)) {
|
|
380
490
|
return { partial: true, issue: 'Plexor URL set but auth key is not a Plexor key' };
|
|
381
491
|
}
|
|
492
|
+
if (isPlexorUrl && claudeState.primaryApiKey && !isPlexorApiKey(claudeState.primaryApiKey)) {
|
|
493
|
+
return { partial: true, issue: 'Plexor URL set but Claude managed API key is still active' };
|
|
494
|
+
}
|
|
382
495
|
return { partial: false, issue: null };
|
|
383
496
|
} catch {
|
|
384
497
|
return { partial: false, issue: null };
|
|
@@ -442,6 +555,7 @@ module.exports = {
|
|
|
442
555
|
settingsManager,
|
|
443
556
|
isManagedGatewayUrl,
|
|
444
557
|
CLAUDE_DIR,
|
|
558
|
+
CLAUDE_STATE_PATH,
|
|
445
559
|
SETTINGS_PATH,
|
|
446
560
|
PLEXOR_STAGING_URL,
|
|
447
561
|
PLEXOR_PROD_URL
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -167,6 +167,7 @@ function isManagedGatewayUrl(baseUrl) {
|
|
|
167
167
|
const THIS_VARIANT_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
168
168
|
const PREVIOUS_API_KEY_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_API_KEY';
|
|
169
169
|
const PREVIOUS_AUTH_TOKEN_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN';
|
|
170
|
+
const PREVIOUS_PRIMARY_API_KEY_ENV = 'PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY';
|
|
170
171
|
|
|
171
172
|
/**
|
|
172
173
|
* Check for orphaned Plexor routing in settings.json without valid config.
|
|
@@ -198,6 +199,36 @@ function syncManagedAuthEnv(env, managedAuthKey) {
|
|
|
198
199
|
env.ANTHROPIC_AUTH_TOKEN = managedAuthKey;
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
function syncManagedPrimaryApiKey(env, managedAuthKey) {
|
|
203
|
+
const statePath = path.join(HOME_DIR, '.claude.json');
|
|
204
|
+
try {
|
|
205
|
+
if (!fs.existsSync(statePath)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const data = fs.readFileSync(statePath, 'utf8');
|
|
210
|
+
if (!data || !data.trim()) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const claudeState = JSON.parse(data);
|
|
215
|
+
const primaryApiKey = claudeState.primaryApiKey || '';
|
|
216
|
+
if (!primaryApiKey || primaryApiKey.startsWith('plx_') || primaryApiKey === managedAuthKey) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
env[PREVIOUS_PRIMARY_API_KEY_ENV] = primaryApiKey;
|
|
221
|
+
delete claudeState.primaryApiKey;
|
|
222
|
+
|
|
223
|
+
const tempPath = `${statePath}.tmp.${Date.now()}`;
|
|
224
|
+
fs.writeFileSync(tempPath, JSON.stringify(claudeState, null, 2), { mode: 0o600 });
|
|
225
|
+
fs.renameSync(tempPath, statePath);
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
201
232
|
function checkOrphanedRouting() {
|
|
202
233
|
// Use the resolved HOME_DIR (not process.env.HOME which may be wrong under sudo -u)
|
|
203
234
|
const settingsPath = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
@@ -223,6 +254,11 @@ function checkOrphanedRouting() {
|
|
|
223
254
|
settingsChanged = true;
|
|
224
255
|
console.log('\n Synced Plexor auth into ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN');
|
|
225
256
|
}
|
|
257
|
+
if (managedAuthKey && syncManagedPrimaryApiKey(env, managedAuthKey)) {
|
|
258
|
+
settings.env = env;
|
|
259
|
+
settingsChanged = true;
|
|
260
|
+
console.log('\n Suspended Claude managed API key while Plexor is active');
|
|
261
|
+
}
|
|
226
262
|
// Check if there's a valid Plexor config
|
|
227
263
|
let hasValidConfig = false;
|
|
228
264
|
try {
|
package/scripts/uninstall.js
CHANGED
|
@@ -56,6 +56,7 @@ const results = {
|
|
|
56
56
|
restored: [],
|
|
57
57
|
pluginDir: false
|
|
58
58
|
};
|
|
59
|
+
const PREVIOUS_PRIMARY_API_KEY_ENV = 'PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY';
|
|
59
60
|
|
|
60
61
|
function isManagedGatewayUrl(baseUrl = '') {
|
|
61
62
|
return (
|
|
@@ -104,6 +105,36 @@ function clearPlexorRoutingEnv(env = {}) {
|
|
|
104
105
|
return true;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function restoreClaudePrimaryApiKey(env = {}) {
|
|
109
|
+
const previousPrimaryApiKey = env[PREVIOUS_PRIMARY_API_KEY_ENV] || '';
|
|
110
|
+
if (!previousPrimaryApiKey) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const statePath = path.join(home, '.claude.json');
|
|
115
|
+
let claudeState = {};
|
|
116
|
+
try {
|
|
117
|
+
if (fs.existsSync(statePath)) {
|
|
118
|
+
const data = fs.readFileSync(statePath, 'utf8');
|
|
119
|
+
if (data && data.trim()) {
|
|
120
|
+
claudeState = JSON.parse(data);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
claudeState = {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!claudeState.primaryApiKey) {
|
|
128
|
+
claudeState.primaryApiKey = previousPrimaryApiKey;
|
|
129
|
+
}
|
|
130
|
+
delete env[PREVIOUS_PRIMARY_API_KEY_ENV];
|
|
131
|
+
|
|
132
|
+
const tempPath = `${statePath}.tmp.${Date.now()}`;
|
|
133
|
+
fs.writeFileSync(tempPath, JSON.stringify(claudeState, null, 2), { mode: 0o600 });
|
|
134
|
+
fs.renameSync(tempPath, statePath);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
// 1. Remove routing from settings.json
|
|
108
139
|
// This is CRITICAL - do NOT depend on settings-manager module since it may not load during uninstall
|
|
109
140
|
try {
|
|
@@ -114,13 +145,14 @@ try {
|
|
|
114
145
|
const settings = JSON.parse(data);
|
|
115
146
|
if (settings.env) {
|
|
116
147
|
const routingChanged = clearPlexorRoutingEnv(settings.env);
|
|
148
|
+
const primaryApiKeyRestored = restoreClaudePrimaryApiKey(settings.env);
|
|
117
149
|
|
|
118
150
|
// Clean up empty env block
|
|
119
151
|
if (Object.keys(settings.env).length === 0) {
|
|
120
152
|
delete settings.env;
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
if (routingChanged) {
|
|
155
|
+
if (routingChanged || primaryApiKeyRestored) {
|
|
124
156
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
125
157
|
results.routing = true;
|
|
126
158
|
}
|