@plexor-dev/claude-code-plugin-staging 0.1.0-beta.25 → 0.1.0-beta.26
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-logout.js +3 -3
- package/commands/plexor-setup.js +14 -2
- package/commands/plexor-setup.md +4 -4
- package/commands/plexor-status.js +5 -1
- package/commands/plexor-uninstall.js +12 -6
- package/lib/config-utils.js +58 -13
- package/lib/settings-manager.js +48 -47
- package/package.json +1 -1
- package/scripts/plexor-cli.sh +6 -1
- package/scripts/postinstall.js +79 -61
- package/scripts/uninstall.js +10 -2
|
@@ -115,9 +115,9 @@ function main() {
|
|
|
115
115
|
process.exit(1);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
//
|
|
119
|
-
// This removes
|
|
120
|
-
// no longer
|
|
118
|
+
// Disable Claude Code routing in ~/.claude/settings.json.
|
|
119
|
+
// This removes the Plexor gateway URL and managed Plexor headers so Claude
|
|
120
|
+
// no longer routes through Plexor with removed credentials.
|
|
121
121
|
const routingDisabled = settingsManager.disablePlexorRouting();
|
|
122
122
|
|
|
123
123
|
// Clear session
|
package/commands/plexor-setup.js
CHANGED
|
@@ -93,7 +93,7 @@ function getGatewayLabel(apiUrl) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function isRunningInsideClaudeSession(env = process.env) {
|
|
96
|
-
return Boolean(env.CLAUDECODE);
|
|
96
|
+
return Boolean(env.CLAUDECODE || env.PLEXOR_SETUP_IN_CLAUDE);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function createSkipVerifyResult() {
|
|
@@ -143,6 +143,14 @@ function printUsage() {
|
|
|
143
143
|
console.log(' PLEXOR_API_KEY=plx_... /plexor-setup');
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
function printRestartBanner() {
|
|
147
|
+
const BRIGHT_YELLOW = '\x1b[93m';
|
|
148
|
+
const BOLD = '\x1b[1m';
|
|
149
|
+
const RESET = '\x1b[0m';
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(`${BOLD}${BRIGHT_YELLOW}RESTART CLAUDE NOW.${RESET}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
146
154
|
function printReceipt({ user, gateway, previousAuthPreserved, verifyResult }) {
|
|
147
155
|
const line = (content) => `│ ${String(content).slice(0, 43).padEnd(43)}│`;
|
|
148
156
|
|
|
@@ -169,7 +177,11 @@ function printReceipt({ user, gateway, previousAuthPreserved, verifyResult }) {
|
|
|
169
177
|
}
|
|
170
178
|
console.log('└─────────────────────────────────────────────┘');
|
|
171
179
|
|
|
172
|
-
if (
|
|
180
|
+
if (verifyResult.pendingRestart) {
|
|
181
|
+
printRestartBanner();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!verifyResult.ok && !verifyResult.pendingRestart) {
|
|
173
185
|
console.log('');
|
|
174
186
|
console.log(`Verification failed: ${verifyResult.reason}`);
|
|
175
187
|
}
|
package/commands/plexor-setup.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
description: Guided first-run setup for Plexor with Claude Code (user)
|
|
2
2
|
---
|
|
3
3
|
|
|
4
|
-
**RULE: Execute this workflow EXACTLY ONCE. After the Bash tool returns output, your ONLY action is to present that output to the user. DO NOT restart the workflow.**
|
|
4
|
+
**RULE: Execute this workflow EXACTLY ONCE. After the Bash tool returns output, your ONLY action is to present that output to the user. If the output says restart is required, add one standalone line exactly `RESTART CLAUDE NOW.` after the tool output. DO NOT restart the workflow.**
|
|
5
5
|
|
|
6
6
|
Plexor setup is the primary human setup flow.
|
|
7
7
|
|
|
8
8
|
If `$ARGUMENTS` already contains a Plexor API key (`plx_...`), run:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
node ~/.claude/plugins/plexor/commands/plexor-setup.js $ARGUMENTS
|
|
11
|
+
PLEXOR_SETUP_IN_CLAUDE=1 node ~/.claude/plugins/plexor/commands/plexor-setup.js $ARGUMENTS
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
If the user did not provide a key yet, ask them:
|
|
@@ -18,11 +18,11 @@ If the user did not provide a key yet, ask them:
|
|
|
18
18
|
After the user replies with the key, run:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
node ~/.claude/plugins/plexor/commands/plexor-setup.js <user_key>
|
|
21
|
+
PLEXOR_SETUP_IN_CLAUDE=1 node ~/.claude/plugins/plexor/commands/plexor-setup.js <user_key>
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
This command:
|
|
25
25
|
- saves the Plexor key
|
|
26
26
|
- routes Claude through the Plexor staging gateway
|
|
27
|
-
-
|
|
27
|
+
- keeps Claude's existing direct auth intact and adds Plexor auth via managed headers
|
|
28
28
|
- runs a deterministic Claude verification step
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const https = require('https');
|
|
11
|
+
const { parseCustomHeaders } = require('../lib/config-utils');
|
|
11
12
|
|
|
12
13
|
// Import centralized constants with HOME directory validation
|
|
13
14
|
const { HOME_DIR, CONFIG_PATH, SESSION_PATH, SESSION_TIMEOUT_MS } = require('../lib/constants');
|
|
@@ -26,12 +27,15 @@ function isManagedGatewayUrl(baseUrl = '') {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function getPlexorAuthKey(env = {}) {
|
|
30
|
+
const headers = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
|
|
31
|
+
const headerApiKey = headers['x-plexor-key'] || '';
|
|
29
32
|
const apiKey = env.ANTHROPIC_API_KEY || '';
|
|
30
33
|
const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
|
|
31
34
|
|
|
35
|
+
if (headerApiKey.startsWith('plx_')) return headerApiKey;
|
|
32
36
|
if (apiKey.startsWith('plx_')) return apiKey;
|
|
33
37
|
if (authToken.startsWith('plx_')) return authToken;
|
|
34
|
-
return
|
|
38
|
+
return '';
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
function getDirectClaudeAuthState() {
|
|
@@ -18,18 +18,24 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const os = require('os');
|
|
21
|
+
const { execSync } = require('child_process');
|
|
21
22
|
const { removeManagedStatusLine } = require('../lib/statusline-manager');
|
|
22
23
|
const { removeManagedHooks, cleanupLegacyManagedHooksFile } = require('../lib/hooks-manager');
|
|
23
24
|
const { removeManagedClaudeCustomHeadersFromEnv } = require('../lib/config-utils');
|
|
24
25
|
|
|
25
26
|
// Get home directory, handling sudo case
|
|
26
27
|
function getHomeDir() {
|
|
27
|
-
if (process.env.SUDO_USER) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
if (os.platform() !== 'win32' && process.env.SUDO_USER) {
|
|
29
|
+
try {
|
|
30
|
+
if (typeof process.getuid === 'function' && process.getuid() === 0) {
|
|
31
|
+
const entry = execSync(`getent passwd ${process.env.SUDO_USER}`, { encoding: 'utf8' }).trim();
|
|
32
|
+
const fields = entry.split(':');
|
|
33
|
+
if (fields.length >= 6 && fields[5]) {
|
|
34
|
+
return fields[5];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Fall through to HOME/os.homedir below.
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
package/lib/config-utils.js
CHANGED
|
@@ -12,6 +12,7 @@ const DISABLED_HINT_VALUES = new Set(['', 'auto', 'none', 'off']);
|
|
|
12
12
|
const VALID_ORCHESTRATION_MODES = new Set(['supervised', 'autonomous', 'danger-full-auto']);
|
|
13
13
|
const VALID_ROUTING_MODES = new Set(['eco', 'balanced', 'quality', 'passthrough', 'cost']);
|
|
14
14
|
const MANAGED_HEADER_KEYS = new Set([
|
|
15
|
+
'x-plexor-key',
|
|
15
16
|
'x-force-provider',
|
|
16
17
|
'x-force-model',
|
|
17
18
|
'x-allow-providers',
|
|
@@ -23,6 +24,10 @@ const MANAGED_HEADER_KEYS = new Set([
|
|
|
23
24
|
]);
|
|
24
25
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
25
26
|
|
|
27
|
+
function isPlexorApiKey(value = '') {
|
|
28
|
+
return typeof value === 'string' && value.startsWith('plx_');
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
function normalizeForcedProvider(value) {
|
|
27
32
|
if (typeof value !== 'string') {
|
|
28
33
|
return null;
|
|
@@ -137,9 +142,39 @@ function removeManagedClaudeCustomHeadersFromEnv(env = {}) {
|
|
|
137
142
|
return true;
|
|
138
143
|
}
|
|
139
144
|
|
|
145
|
+
function getManagedPlexorAuthHeader(env = {}) {
|
|
146
|
+
const existing = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
|
|
147
|
+
const managedAuthKey = existing['x-plexor-key'] || '';
|
|
148
|
+
return isPlexorApiKey(managedAuthKey) ? managedAuthKey : '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function upsertManagedPlexorAuthHeader(env = {}, apiKey = '') {
|
|
152
|
+
const existing = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
|
|
153
|
+
const previousHeaders = env.ANTHROPIC_CUSTOM_HEADERS || '';
|
|
154
|
+
|
|
155
|
+
if (isPlexorApiKey(apiKey)) {
|
|
156
|
+
existing['x-plexor-key'] = apiKey;
|
|
157
|
+
} else {
|
|
158
|
+
delete existing['x-plexor-key'];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Object.keys(existing).length) {
|
|
162
|
+
env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(existing);
|
|
163
|
+
} else {
|
|
164
|
+
delete env.ANTHROPIC_CUSTOM_HEADERS;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (env.ANTHROPIC_CUSTOM_HEADERS || '') !== previousHeaders;
|
|
168
|
+
}
|
|
169
|
+
|
|
140
170
|
function buildManagedAnthropicHeaders(config) {
|
|
141
171
|
const settings = config?.settings || {};
|
|
142
172
|
const headers = {};
|
|
173
|
+
const apiKey = config?.auth?.api_key || config?.auth?.apiKey || config?.apiKey || '';
|
|
174
|
+
|
|
175
|
+
if (isPlexorApiKey(apiKey)) {
|
|
176
|
+
headers['x-plexor-key'] = apiKey;
|
|
177
|
+
}
|
|
143
178
|
|
|
144
179
|
const modeRaw = String(settings.mode || '')
|
|
145
180
|
.trim()
|
|
@@ -194,6 +229,25 @@ function buildManagedAnthropicHeaders(config) {
|
|
|
194
229
|
return headers;
|
|
195
230
|
}
|
|
196
231
|
|
|
232
|
+
function applyManagedClaudeCustomHeadersToEnv(env = {}, config = {}) {
|
|
233
|
+
const previousHeaders = env.ANTHROPIC_CUSTOM_HEADERS || '';
|
|
234
|
+
const existing = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
|
|
235
|
+
for (const key of MANAGED_HEADER_KEYS) {
|
|
236
|
+
delete existing[key];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const managed = buildManagedAnthropicHeaders(config);
|
|
240
|
+
const merged = { ...existing, ...managed };
|
|
241
|
+
|
|
242
|
+
if (Object.keys(merged).length) {
|
|
243
|
+
env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(merged);
|
|
244
|
+
} else {
|
|
245
|
+
delete env.ANTHROPIC_CUSTOM_HEADERS;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return (env.ANTHROPIC_CUSTOM_HEADERS || '') !== previousHeaders;
|
|
249
|
+
}
|
|
250
|
+
|
|
197
251
|
function syncClaudeCustomHeaders(config) {
|
|
198
252
|
try {
|
|
199
253
|
let settings = {};
|
|
@@ -207,19 +261,7 @@ function syncClaudeCustomHeaders(config) {
|
|
|
207
261
|
}
|
|
208
262
|
settings.env = settings.env && typeof settings.env === 'object' ? settings.env : {};
|
|
209
263
|
|
|
210
|
-
|
|
211
|
-
for (const key of MANAGED_HEADER_KEYS) {
|
|
212
|
-
delete existing[key];
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const managed = buildManagedAnthropicHeaders(config);
|
|
216
|
-
const merged = { ...existing, ...managed };
|
|
217
|
-
|
|
218
|
-
if (Object.keys(merged).length) {
|
|
219
|
-
settings.env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(merged);
|
|
220
|
-
} else {
|
|
221
|
-
delete settings.env.ANTHROPIC_CUSTOM_HEADERS;
|
|
222
|
-
}
|
|
264
|
+
applyManagedClaudeCustomHeadersToEnv(settings.env, config);
|
|
223
265
|
|
|
224
266
|
const claudeDir = path.dirname(CLAUDE_SETTINGS_PATH);
|
|
225
267
|
if (!fs.existsSync(claudeDir)) {
|
|
@@ -332,6 +374,9 @@ module.exports = {
|
|
|
332
374
|
readSetting,
|
|
333
375
|
hasForcedHintConflict,
|
|
334
376
|
validateForcedHintConfig,
|
|
377
|
+
applyManagedClaudeCustomHeadersToEnv,
|
|
378
|
+
getManagedPlexorAuthHeader,
|
|
379
|
+
upsertManagedPlexorAuthHeader,
|
|
335
380
|
removeManagedClaudeCustomHeadersFromEnv,
|
|
336
381
|
parseCustomHeaders,
|
|
337
382
|
serializeCustomHeaders,
|
package/lib/settings-manager.js
CHANGED
|
@@ -14,12 +14,15 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
getManagedPlexorAuthHeader,
|
|
19
|
+
removeManagedClaudeCustomHeadersFromEnv,
|
|
20
|
+
upsertManagedPlexorAuthHeader
|
|
21
|
+
} = require('./config-utils');
|
|
18
22
|
|
|
19
23
|
const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
|
|
20
24
|
const CLAUDE_STATE_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude.json');
|
|
21
25
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
22
|
-
const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
|
|
23
26
|
|
|
24
27
|
// Plexor gateway endpoints
|
|
25
28
|
const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
@@ -49,44 +52,47 @@ function isPlexorApiKey(value = '') {
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
function getPlexorAuthKey(env = {}) {
|
|
55
|
+
const headerKey = getManagedPlexorAuthHeader(env);
|
|
52
56
|
const apiKey = env.ANTHROPIC_API_KEY || '';
|
|
53
57
|
const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
|
|
54
58
|
|
|
59
|
+
if (isPlexorApiKey(headerKey)) return headerKey;
|
|
55
60
|
if (isPlexorApiKey(apiKey)) return apiKey;
|
|
56
61
|
if (isPlexorApiKey(authToken)) return authToken;
|
|
57
|
-
return
|
|
62
|
+
return '';
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '')
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function stashDirectAuthEnv(env, plexorApiKey) {
|
|
69
|
-
const alreadyManaged = hasPlexorManagedAuth(env);
|
|
70
|
-
const currentApiKey = env.ANTHROPIC_API_KEY || '';
|
|
71
|
-
const currentAuthToken = env.ANTHROPIC_AUTH_TOKEN || '';
|
|
65
|
+
function clearLegacyPlexorAuthValue(env = {}, field, backupField) {
|
|
66
|
+
let changed = false;
|
|
67
|
+
const currentValue = env[field] || '';
|
|
68
|
+
const backupValue = env[backupField] || '';
|
|
72
69
|
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
if (isPlexorApiKey(currentValue)) {
|
|
71
|
+
if (backupValue && !isPlexorApiKey(backupValue)) {
|
|
72
|
+
env[field] = backupValue;
|
|
73
|
+
} else {
|
|
74
|
+
delete env[field];
|
|
75
|
+
}
|
|
76
|
+
changed = true;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
if (
|
|
80
|
-
env[
|
|
81
|
-
|
|
82
|
-
delete env[PREVIOUS_AUTH_TOKEN_ENV];
|
|
79
|
+
if (backupValue) {
|
|
80
|
+
delete env[backupField];
|
|
81
|
+
changed = true;
|
|
83
82
|
}
|
|
83
|
+
|
|
84
|
+
return changed;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
function migrateLegacyPlexorAuthEnv(env = {}, claudeState = {}) {
|
|
88
|
+
let changed = false;
|
|
89
|
+
|
|
90
|
+
changed = clearLegacyPlexorAuthValue(env, 'ANTHROPIC_API_KEY', PREVIOUS_API_KEY_ENV) || changed;
|
|
91
|
+
changed = clearLegacyPlexorAuthValue(env, 'ANTHROPIC_AUTH_TOKEN', PREVIOUS_AUTH_TOKEN_ENV) || changed;
|
|
92
|
+
|
|
93
|
+
changed = restoreDirectPrimaryApiKey(env, claudeState) || changed;
|
|
94
|
+
|
|
95
|
+
return changed;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
function clearPlexorRoutingEnv(env = {}) {
|
|
@@ -122,17 +128,6 @@ function clearPlexorRoutingEnv(env = {}) {
|
|
|
122
128
|
return true;
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
function stashDirectPrimaryApiKey(env = {}, claudeState = {}, plexorApiKey = '') {
|
|
126
|
-
const currentPrimaryApiKey = claudeState.primaryApiKey || '';
|
|
127
|
-
if (!currentPrimaryApiKey || isPlexorApiKey(currentPrimaryApiKey) || currentPrimaryApiKey === plexorApiKey) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
env[PREVIOUS_PRIMARY_API_KEY_ENV] = currentPrimaryApiKey;
|
|
132
|
-
delete claudeState.primaryApiKey;
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
131
|
function restoreDirectPrimaryApiKey(env = {}, claudeState = {}, options = {}) {
|
|
137
132
|
const { consumeBackup = true } = options;
|
|
138
133
|
const previousPrimaryApiKey = env[PREVIOUS_PRIMARY_API_KEY_ENV] || '';
|
|
@@ -356,11 +351,9 @@ class ClaudeSettingsManager {
|
|
|
356
351
|
settings.env = {};
|
|
357
352
|
}
|
|
358
353
|
|
|
359
|
-
// Mirror the Plexor key into both Claude auth env vars so every Claude
|
|
360
|
-
// runtime path uses the gateway instead of any previously saved direct key.
|
|
361
354
|
settings.env.ANTHROPIC_BASE_URL = apiUrl;
|
|
362
|
-
|
|
363
|
-
const claudeStateChanged =
|
|
355
|
+
upsertManagedPlexorAuthHeader(settings.env, apiKey);
|
|
356
|
+
const claudeStateChanged = migrateLegacyPlexorAuthEnv(settings.env, claudeState);
|
|
364
357
|
|
|
365
358
|
const success = this.save(settings);
|
|
366
359
|
if (!success) {
|
|
@@ -493,9 +486,6 @@ class ClaudeSettingsManager {
|
|
|
493
486
|
if (isPlexorUrl && !isPlexorApiKey(authKey)) {
|
|
494
487
|
return { partial: true, issue: 'Plexor URL set but auth key is not a Plexor key' };
|
|
495
488
|
}
|
|
496
|
-
if (isPlexorUrl && claudeState.primaryApiKey && !isPlexorApiKey(claudeState.primaryApiKey)) {
|
|
497
|
-
return { partial: true, issue: 'Plexor URL set but Claude managed API key is still active' };
|
|
498
|
-
}
|
|
499
489
|
return { partial: false, issue: null };
|
|
500
490
|
} catch {
|
|
501
491
|
return { partial: false, issue: null };
|
|
@@ -515,8 +505,19 @@ class ClaudeSettingsManager {
|
|
|
515
505
|
settings.env = {};
|
|
516
506
|
}
|
|
517
507
|
|
|
518
|
-
|
|
519
|
-
|
|
508
|
+
const previousSettings = JSON.parse(JSON.stringify(settings));
|
|
509
|
+
const claudeState = this.loadClaudeState();
|
|
510
|
+
upsertManagedPlexorAuthHeader(settings.env, apiKey);
|
|
511
|
+
const claudeStateChanged = migrateLegacyPlexorAuthEnv(settings.env, claudeState);
|
|
512
|
+
const success = this.save(settings);
|
|
513
|
+
if (!success) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
if (claudeStateChanged && !this.saveClaudeState(claudeState)) {
|
|
517
|
+
this.save(previousSettings);
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
return true;
|
|
520
521
|
} catch (err) {
|
|
521
522
|
console.error('Failed to update API key:', err.message);
|
|
522
523
|
return false;
|
package/package.json
CHANGED
package/scripts/plexor-cli.sh
CHANGED
|
@@ -21,7 +21,12 @@ if [ -f "$CONFIG_FILE" ]; then
|
|
|
21
21
|
if [ "$ENABLED" = "True" ] && [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
|
|
22
22
|
# Set ANTHROPIC_BASE_URL to Plexor gateway (hypervisor mode)
|
|
23
23
|
export ANTHROPIC_BASE_URL="${API_URL}/gateway/anthropic/v1"
|
|
24
|
-
|
|
24
|
+
if [ -n "$ANTHROPIC_CUSTOM_HEADERS" ]; then
|
|
25
|
+
export ANTHROPIC_CUSTOM_HEADERS="x-plexor-key: ${API_KEY}
|
|
26
|
+
${ANTHROPIC_CUSTOM_HEADERS}"
|
|
27
|
+
else
|
|
28
|
+
export ANTHROPIC_CUSTOM_HEADERS="x-plexor-key: ${API_KEY}"
|
|
29
|
+
fi
|
|
25
30
|
|
|
26
31
|
# Show Plexor branding
|
|
27
32
|
echo -e "${CYAN}"
|
package/scripts/postinstall.js
CHANGED
|
@@ -11,6 +11,10 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const os = require('os');
|
|
13
13
|
const { execSync } = require('child_process');
|
|
14
|
+
const {
|
|
15
|
+
applyManagedClaudeCustomHeadersToEnv,
|
|
16
|
+
getManagedPlexorAuthHeader
|
|
17
|
+
} = require('../lib/config-utils');
|
|
14
18
|
const { upsertManagedStatusLine } = require('../lib/statusline-manager');
|
|
15
19
|
const { upsertManagedHooks, cleanupLegacyManagedHooksFile } = require('../lib/hooks-manager');
|
|
16
20
|
|
|
@@ -178,31 +182,42 @@ const PREVIOUS_PRIMARY_API_KEY_ENV = 'PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY';
|
|
|
178
182
|
/**
|
|
179
183
|
* Check for orphaned Plexor routing in settings.json without valid config.
|
|
180
184
|
* Also detects variant mismatch (e.g., localhost plugin was installed, now
|
|
181
|
-
* installing staging plugin) and migrates
|
|
182
|
-
* Claude
|
|
185
|
+
* installing staging plugin) and migrates legacy env-based Plexor auth into
|
|
186
|
+
* managed Claude custom headers.
|
|
183
187
|
*/
|
|
184
|
-
function selectManagedAuthKey(env = {}) {
|
|
188
|
+
function selectManagedAuthKey(env = {}, config = {}) {
|
|
189
|
+
const configApiKey = config.auth?.api_key || config.auth?.apiKey || config.apiKey || '';
|
|
185
190
|
const apiKey = env.ANTHROPIC_API_KEY || '';
|
|
186
191
|
const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
|
|
192
|
+
const headerApiKey = getManagedPlexorAuthHeader(env);
|
|
187
193
|
|
|
194
|
+
if (configApiKey.startsWith('plx_')) return configApiKey;
|
|
195
|
+
if (headerApiKey.startsWith('plx_')) return headerApiKey;
|
|
188
196
|
if (apiKey.startsWith('plx_')) return apiKey;
|
|
189
197
|
if (authToken.startsWith('plx_')) return authToken;
|
|
190
|
-
return
|
|
198
|
+
return '';
|
|
191
199
|
}
|
|
192
200
|
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
const
|
|
201
|
+
function restoreLegacyManagedAuthEnvValue(env, field, backupField) {
|
|
202
|
+
let changed = false;
|
|
203
|
+
const currentValue = env[field] || '';
|
|
204
|
+
const backupValue = env[backupField] || '';
|
|
196
205
|
|
|
197
|
-
if (
|
|
198
|
-
|
|
206
|
+
if (currentValue.startsWith('plx_')) {
|
|
207
|
+
if (backupValue && !backupValue.startsWith('plx_')) {
|
|
208
|
+
env[field] = backupValue;
|
|
209
|
+
} else {
|
|
210
|
+
delete env[field];
|
|
211
|
+
}
|
|
212
|
+
changed = true;
|
|
199
213
|
}
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
|
|
215
|
+
if (backupValue) {
|
|
216
|
+
delete env[backupField];
|
|
217
|
+
changed = true;
|
|
202
218
|
}
|
|
203
219
|
|
|
204
|
-
|
|
205
|
-
env.ANTHROPIC_AUTH_TOKEN = managedAuthKey;
|
|
220
|
+
return changed;
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
function writeJsonAtomically(filePath, value) {
|
|
@@ -211,36 +226,36 @@ function writeJsonAtomically(filePath, value) {
|
|
|
211
226
|
fs.renameSync(tempPath, filePath);
|
|
212
227
|
}
|
|
213
228
|
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
229
|
+
function migrateManagedAuthState(env, claudeState, config = {}) {
|
|
230
|
+
let settingsChanged = false;
|
|
231
|
+
let claudeStateChanged = false;
|
|
232
|
+
const managedAuthKey = selectManagedAuthKey(env, config);
|
|
233
|
+
|
|
234
|
+
if (managedAuthKey) {
|
|
235
|
+
const managedConfig = {
|
|
236
|
+
...config,
|
|
237
|
+
auth: {
|
|
238
|
+
...(config.auth || {}),
|
|
239
|
+
api_key: managedAuthKey
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
settingsChanged = applyManagedClaudeCustomHeadersToEnv(env, managedConfig) || settingsChanged;
|
|
243
|
+
}
|
|
220
244
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return { changed: false };
|
|
224
|
-
}
|
|
245
|
+
settingsChanged = restoreLegacyManagedAuthEnvValue(env, 'ANTHROPIC_API_KEY', PREVIOUS_API_KEY_ENV) || settingsChanged;
|
|
246
|
+
settingsChanged = restoreLegacyManagedAuthEnvValue(env, 'ANTHROPIC_AUTH_TOKEN', PREVIOUS_AUTH_TOKEN_ENV) || settingsChanged;
|
|
225
247
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (!
|
|
229
|
-
|
|
248
|
+
const previousPrimaryApiKey = env[PREVIOUS_PRIMARY_API_KEY_ENV] || '';
|
|
249
|
+
if (previousPrimaryApiKey) {
|
|
250
|
+
if (!claudeState.primaryApiKey) {
|
|
251
|
+
claudeState.primaryApiKey = previousPrimaryApiKey;
|
|
252
|
+
claudeStateChanged = true;
|
|
230
253
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const nextClaudeState = { ...claudeState };
|
|
234
|
-
delete nextClaudeState.primaryApiKey;
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
changed: true,
|
|
238
|
-
statePath,
|
|
239
|
-
claudeState: nextClaudeState
|
|
240
|
-
};
|
|
241
|
-
} catch {
|
|
242
|
-
return { changed: false };
|
|
254
|
+
delete env[PREVIOUS_PRIMARY_API_KEY_ENV];
|
|
255
|
+
settingsChanged = true;
|
|
243
256
|
}
|
|
257
|
+
|
|
258
|
+
return { settingsChanged, claudeStateChanged };
|
|
244
259
|
}
|
|
245
260
|
|
|
246
261
|
function checkOrphanedRouting() {
|
|
@@ -254,32 +269,19 @@ function checkOrphanedRouting() {
|
|
|
254
269
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
255
270
|
const env = settings.env || {};
|
|
256
271
|
let settingsChanged = false;
|
|
257
|
-
let
|
|
272
|
+
let claudeStateChanged = false;
|
|
273
|
+
let claudeState = {};
|
|
274
|
+
const statePath = path.join(HOME_DIR, '.claude.json');
|
|
258
275
|
|
|
259
276
|
const hasPlexorUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL);
|
|
260
277
|
|
|
261
278
|
if (hasPlexorUrl) {
|
|
262
|
-
// Keep both Claude auth env vars aligned to the Plexor key so Claude API
|
|
263
|
-
// auth cannot override the gateway after plugin setup.
|
|
264
|
-
const managedAuthKey = selectManagedAuthKey(env);
|
|
265
|
-
if (managedAuthKey &&
|
|
266
|
-
(env.ANTHROPIC_API_KEY !== managedAuthKey || env.ANTHROPIC_AUTH_TOKEN !== managedAuthKey)) {
|
|
267
|
-
syncManagedAuthEnv(env, managedAuthKey);
|
|
268
|
-
settings.env = env;
|
|
269
|
-
settingsChanged = true;
|
|
270
|
-
console.log('\n Synced Plexor auth into ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN');
|
|
271
|
-
}
|
|
272
|
-
const primaryApiKeySync = managedAuthKey ? syncManagedPrimaryApiKey(env, managedAuthKey) : { changed: false };
|
|
273
|
-
if (primaryApiKeySync.changed) {
|
|
274
|
-
settings.env = env;
|
|
275
|
-
settingsChanged = true;
|
|
276
|
-
managedPrimaryApiKeySync = primaryApiKeySync;
|
|
277
|
-
}
|
|
278
279
|
// Check if there's a valid Plexor config
|
|
279
280
|
let hasValidConfig = false;
|
|
281
|
+
let config = null;
|
|
280
282
|
try {
|
|
281
283
|
if (fs.existsSync(configPath)) {
|
|
282
|
-
|
|
284
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
283
285
|
hasValidConfig = (config.auth?.api_key || config.apiKey || '').startsWith('plx_');
|
|
284
286
|
}
|
|
285
287
|
} catch (e) {}
|
|
@@ -290,6 +292,22 @@ function checkOrphanedRouting() {
|
|
|
290
292
|
console.log(' Run /plexor-login to reconfigure, or');
|
|
291
293
|
console.log(' Run /plexor-uninstall to clean up\n');
|
|
292
294
|
} else {
|
|
295
|
+
try {
|
|
296
|
+
if (fs.existsSync(statePath)) {
|
|
297
|
+
const data = fs.readFileSync(statePath, 'utf8');
|
|
298
|
+
if (data && data.trim()) {
|
|
299
|
+
claudeState = JSON.parse(data);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
claudeState = {};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const migration = migrateManagedAuthState(env, claudeState, config);
|
|
307
|
+
settings.env = env;
|
|
308
|
+
settingsChanged = migration.settingsChanged || settingsChanged;
|
|
309
|
+
claudeStateChanged = migration.claudeStateChanged || claudeStateChanged;
|
|
310
|
+
|
|
293
311
|
// Fix #2176: Detect variant mismatch and migrate URL
|
|
294
312
|
const currentUrl = env.ANTHROPIC_BASE_URL;
|
|
295
313
|
if (currentUrl !== THIS_VARIANT_URL) {
|
|
@@ -316,12 +334,12 @@ function checkOrphanedRouting() {
|
|
|
316
334
|
fs.renameSync(tempPath, settingsPath);
|
|
317
335
|
}
|
|
318
336
|
|
|
319
|
-
if (
|
|
337
|
+
if (claudeStateChanged) {
|
|
320
338
|
try {
|
|
321
|
-
writeJsonAtomically(
|
|
322
|
-
console.log('\n
|
|
339
|
+
writeJsonAtomically(statePath, claudeState);
|
|
340
|
+
console.log('\n Restored saved Claude auth alongside Plexor header routing');
|
|
323
341
|
} catch (e) {
|
|
324
|
-
console.log('\n Warning:
|
|
342
|
+
console.log('\n Warning: Migrated Plexor auth headers but could not restore saved Claude auth');
|
|
325
343
|
console.log(` ${e.message}`);
|
|
326
344
|
}
|
|
327
345
|
}
|
package/scripts/uninstall.js
CHANGED
|
@@ -59,7 +59,8 @@ const results = {
|
|
|
59
59
|
hooks: false,
|
|
60
60
|
commands: [],
|
|
61
61
|
restored: [],
|
|
62
|
-
pluginDir: false
|
|
62
|
+
pluginDir: false,
|
|
63
|
+
configDir: false
|
|
63
64
|
};
|
|
64
65
|
const PREVIOUS_PRIMARY_API_KEY_ENV = 'PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY';
|
|
65
66
|
const CLAUDE_SETTINGS_PATH = path.join(home, '.claude', 'settings.json');
|
|
@@ -254,7 +255,14 @@ try {
|
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
// Output results
|
|
257
|
-
if (
|
|
258
|
+
if (
|
|
259
|
+
results.routing ||
|
|
260
|
+
results.statusLine ||
|
|
261
|
+
results.hooks ||
|
|
262
|
+
results.commands.length > 0 ||
|
|
263
|
+
results.pluginDir ||
|
|
264
|
+
results.configDir
|
|
265
|
+
) {
|
|
258
266
|
console.log(' Plexor plugin uninstalled');
|
|
259
267
|
console.log('');
|
|
260
268
|
|