@plexor-dev/claude-code-plugin-staging 0.1.0-beta.18 → 0.1.0-beta.19

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.
@@ -240,7 +240,7 @@ function main() {
240
240
  console.log(`│ ✗ Cannot Enable Plexor │`);
241
241
  console.log(`├─────────────────────────────────────────────┤`);
242
242
  console.log(`│ No API key configured. │`);
243
- console.log(`│ Run /plexor-login <api-key> first. │`);
243
+ console.log(`│ Run /plexor-setup first. │`);
244
244
  console.log(`├─────────────────────────────────────────────┤`);
245
245
  console.log(`│ Get your API key at: │`);
246
246
  console.log(`│ https://plexor.dev/dashboard/api-keys │`);
@@ -256,7 +256,7 @@ function main() {
256
256
  console.log(`│ Invalid API key format in config. │`);
257
257
  console.log(`│ Keys must start with "plx_" (20+ chars). │`);
258
258
  console.log(`├─────────────────────────────────────────────┤`);
259
- console.log(`│ Run /plexor-login <api-key> to fix. │`);
259
+ console.log(`│ Run /plexor-setup to fix it. │`);
260
260
  console.log(`└─────────────────────────────────────────────┘`);
261
261
  process.exit(1);
262
262
  }
@@ -45,7 +45,7 @@ Options:
45
45
 
46
46
  **Step 3A: Claude MAX User Setup**
47
47
 
48
- If user selected "Yes, I have Claude MAX":
48
+ If user selected the "Claude MAX subscription (Pro/Team/Enterprise)" option:
49
49
 
50
50
  1. Ask for their Plexor API key:
51
51
  "Please provide your Plexor API key (starts with 'plx_')."
@@ -75,11 +75,12 @@ If user selected "Yes, I have Claude MAX":
75
75
  {
76
76
  "env": {
77
77
  "ANTHROPIC_BASE_URL": "https://staging.api.plexor.dev/gateway/anthropic",
78
- "ANTHROPIC_AUTH_TOKEN": "[user's Plexor key]"
78
+ "ANTHROPIC_AUTH_TOKEN": "[user's Plexor key]",
79
+ "ANTHROPIC_API_KEY": "[user's Plexor key]"
79
80
  }
80
81
  }
81
82
  ```
82
- Note: Preserve any existing settings, just add/update the env block.
83
+ Note: Preserve unrelated settings, but replace `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, and `ANTHROPIC_API_KEY` in the env block. This is required when the user previously configured Claude with direct API auth.
83
84
 
84
85
  4. Show the user:
85
86
  ```
@@ -107,7 +108,7 @@ Changes take effect immediately in all Claude Code sessions!
107
108
 
108
109
  **Step 3B: API Key User Setup**
109
110
 
110
- If user selected "No, I'll use a Plexor API key":
111
+ If user selected the "Pay-per-use via Plexor" option:
111
112
 
112
113
  1. Ask for their Plexor API key:
113
114
  "Please provide your Plexor API key (starts with 'plx_')."
@@ -137,11 +138,12 @@ If user selected "No, I'll use a Plexor API key":
137
138
  {
138
139
  "env": {
139
140
  "ANTHROPIC_BASE_URL": "https://staging.api.plexor.dev/gateway/anthropic",
140
- "ANTHROPIC_AUTH_TOKEN": "[user's Plexor key]"
141
+ "ANTHROPIC_AUTH_TOKEN": "[user's Plexor key]",
142
+ "ANTHROPIC_API_KEY": "[user's Plexor key]"
141
143
  }
142
144
  }
143
145
  ```
144
- Note: Preserve any existing settings, just add/update the env block.
146
+ Note: Preserve unrelated settings, but replace `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, and `ANTHROPIC_API_KEY` in the env block. This is required when the user previously configured Claude with direct API auth.
145
147
 
146
148
  4. Show the user:
147
149
  ```
@@ -169,7 +171,7 @@ Changes take effect immediately in all Claude Code sessions!
169
171
 
170
172
  **NOTES**:
171
173
  - The `~/.claude/settings.json` env block is the KEY mechanism that routes Claude Code through Plexor
172
- - ANTHROPIC_AUTH_TOKEN takes precedence over ANTHROPIC_API_KEY (use AUTH_TOKEN for the Plexor key)
174
+ - Set both `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_API_KEY` to the Plexor key so Claude cannot bypass the Plexor gateway with a previously saved direct API key
173
175
  - Changes take effect immediately - no shell restart needed
174
176
 
175
177
  After completing all steps, STOP. DO NOT restart the workflow. DO NOT re-execute any commands.
@@ -13,6 +13,26 @@ const https = require('https');
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
15
 
16
+ function isManagedGatewayUrl(baseUrl = '') {
17
+ return (
18
+ baseUrl.includes('plexor') ||
19
+ baseUrl.includes('staging.api') ||
20
+ baseUrl.includes('ngrok') ||
21
+ baseUrl.includes('localtunnel') ||
22
+ baseUrl.includes('localhost') ||
23
+ baseUrl.includes('127.0.0.1')
24
+ );
25
+ }
26
+
27
+ function getPlexorAuthKey(env = {}) {
28
+ const apiKey = env.ANTHROPIC_API_KEY || '';
29
+ const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
30
+
31
+ if (apiKey.startsWith('plx_')) return apiKey;
32
+ if (authToken.startsWith('plx_')) return authToken;
33
+ return apiKey || authToken || '';
34
+ }
35
+
16
36
  /**
17
37
  * Check if Claude Code is actually routing through Plexor
18
38
  * by reading ~/.claude/settings.json
@@ -22,10 +42,10 @@ function getRoutingStatus() {
22
42
  const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
23
43
  const settings = JSON.parse(data);
24
44
  const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
25
- const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
26
- const isPlexorRouting = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
45
+ const authKey = getPlexorAuthKey(settings.env);
46
+ const isPlexorRouting = isManagedGatewayUrl(baseUrl);
27
47
  return {
28
- active: isPlexorRouting && hasToken,
48
+ active: isPlexorRouting && !!authKey,
29
49
  baseUrl,
30
50
  isStaging: baseUrl.includes('staging')
31
51
  };
@@ -44,14 +64,14 @@ function detectPartialState() {
44
64
  const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
45
65
  const settings = JSON.parse(data);
46
66
  const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
47
- const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
48
- const isPlexorUrl = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
67
+ const authKey = getPlexorAuthKey(settings.env);
68
+ const isPlexorUrl = isManagedGatewayUrl(baseUrl);
49
69
 
50
- if (isPlexorUrl && !authToken) {
51
- return { partial: true, issue: 'Plexor URL set but no auth token' };
70
+ if (isPlexorUrl && !authKey) {
71
+ return { partial: true, issue: 'Plexor URL set but no Plexor auth key' };
52
72
  }
53
- if (isPlexorUrl && !authToken.startsWith('plx_')) {
54
- return { partial: true, issue: 'Plexor URL set but auth token is not a Plexor key' };
73
+ if (isPlexorUrl && !authKey.startsWith('plx_')) {
74
+ return { partial: true, issue: 'Plexor URL set but auth key is not a Plexor key' };
55
75
  }
56
76
  return { partial: false, issue: null };
57
77
  } catch {
@@ -102,7 +122,7 @@ function loadConfig() {
102
122
  return config;
103
123
  } catch (err) {
104
124
  if (err instanceof SyntaxError) {
105
- console.log('Config file is corrupted. Run /plexor-login to reconfigure.');
125
+ console.log('Config file is corrupted. Run /plexor-setup to reconfigure.');
106
126
  }
107
127
  return null;
108
128
  }
@@ -154,7 +174,7 @@ async function main() {
154
174
  // Read config with integrity checking
155
175
  const config = loadConfig();
156
176
  if (!config) {
157
- console.log('Not configured. Run /plexor-login first.');
177
+ console.log('Not configured. Run /plexor-setup.');
158
178
  process.exit(1);
159
179
  }
160
180
 
@@ -166,14 +186,14 @@ async function main() {
166
186
  const apiUrl = config.settings?.apiUrl || 'https://api.plexor.dev';
167
187
 
168
188
  if (!apiKey) {
169
- console.log('Not authenticated. Run /plexor-login first.');
189
+ console.log('Not authenticated. Run /plexor-setup.');
170
190
  process.exit(1);
171
191
  }
172
192
 
173
193
  // Validate API key format
174
194
  if (!isValidApiKeyFormat(apiKey)) {
175
195
  console.log('Invalid API key format. Keys must start with "plx_" and be at least 20 characters.');
176
- console.log('Run /plexor-login with a valid API key.');
196
+ console.log('Run /plexor-setup with a valid Plexor API key.');
177
197
  process.exit(1);
178
198
  }
179
199
 
@@ -278,7 +298,7 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
278
298
  const partialState = detectPartialState();
279
299
  if (partialState.partial) {
280
300
  console.log(` ⚠ PARTIAL STATE DETECTED: ${partialState.issue}`);
281
- console.log(` Run /plexor-login to fix, or /plexor-logout to disable routing\n`);
301
+ console.log(` Run /plexor-setup to repair, or /plexor-logout to disable routing\n`);
282
302
  }
283
303
 
284
304
  // Check for state mismatch between config enabled flag and routing status
@@ -99,17 +99,43 @@ function disableRoutingManually() {
99
99
  return { success: true, message: 'No env block in settings' };
100
100
  }
101
101
 
102
- // Check if Plexor routing is active
103
- const baseUrl = settings.env.ANTHROPIC_BASE_URL || '';
104
- const isPlexorRouting = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
105
-
106
- if (!isPlexorRouting) {
102
+ const isManagedGatewayUrl = (baseUrl = '') =>
103
+ baseUrl.includes('plexor') ||
104
+ baseUrl.includes('staging.api') ||
105
+ baseUrl.includes('localhost') ||
106
+ baseUrl.includes('127.0.0.1') ||
107
+ baseUrl.includes('ngrok') ||
108
+ baseUrl.includes('localtunnel');
109
+ const isPlexorApiKey = (value = '') =>
110
+ typeof value === 'string' && value.startsWith('plx_');
111
+
112
+ const hasManagedBaseUrl = isManagedGatewayUrl(settings.env.ANTHROPIC_BASE_URL || '');
113
+ const hasPlexorAuthToken = isPlexorApiKey(settings.env.ANTHROPIC_AUTH_TOKEN || '');
114
+ const hasPlexorApiKey = isPlexorApiKey(settings.env.ANTHROPIC_API_KEY || '');
115
+
116
+ if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey) {
107
117
  return { success: true, message: 'Plexor routing not active' };
108
118
  }
109
119
 
110
- // Remove Plexor env vars
111
- delete settings.env.ANTHROPIC_BASE_URL;
112
- delete settings.env.ANTHROPIC_AUTH_TOKEN;
120
+ if (hasManagedBaseUrl) {
121
+ delete settings.env.ANTHROPIC_BASE_URL;
122
+ }
123
+ if (hasPlexorAuthToken) {
124
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
125
+ }
126
+ if (hasPlexorApiKey) {
127
+ delete settings.env.ANTHROPIC_API_KEY;
128
+ }
129
+
130
+ if (!settings.env.ANTHROPIC_API_KEY && settings.env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY) {
131
+ settings.env.ANTHROPIC_API_KEY = settings.env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY;
132
+ }
133
+ if (!settings.env.ANTHROPIC_AUTH_TOKEN && settings.env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN) {
134
+ settings.env.ANTHROPIC_AUTH_TOKEN = settings.env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN;
135
+ }
136
+
137
+ delete settings.env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY;
138
+ delete settings.env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN;
113
139
 
114
140
  // Clean up empty env block
115
141
  if (Object.keys(settings.env).length === 0) {
@@ -5,9 +5,23 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const crypto = require('crypto');
8
+ const os = require('os');
8
9
  const { PLEXOR_DIR, CONFIG_PATH } = require('./constants');
9
10
 
10
11
  const DISABLED_HINT_VALUES = new Set(['', 'auto', 'none', 'off']);
12
+ const VALID_ORCHESTRATION_MODES = new Set(['supervised', 'autonomous', 'danger-full-auto']);
13
+ const VALID_ROUTING_MODES = new Set(['eco', 'balanced', 'quality', 'passthrough', 'cost']);
14
+ const MANAGED_HEADER_KEYS = new Set([
15
+ 'x-force-provider',
16
+ 'x-force-model',
17
+ 'x-allow-providers',
18
+ 'x-deny-providers',
19
+ 'x-allow-models',
20
+ 'x-deny-models',
21
+ 'x-plexor-mode',
22
+ 'x-plexor-orchestration-mode'
23
+ ]);
24
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
11
25
 
12
26
  function normalizeForcedProvider(value) {
13
27
  if (typeof value !== 'string') {
@@ -31,6 +45,171 @@ function normalizeForcedModel(value) {
31
45
  return normalized;
32
46
  }
33
47
 
48
+ function normalizeCsv(value) {
49
+ if (typeof value !== 'string') {
50
+ return null;
51
+ }
52
+ const tokens = value
53
+ .split(',')
54
+ .map((token) => token.trim())
55
+ .filter(Boolean);
56
+ if (!tokens.length) {
57
+ return null;
58
+ }
59
+ return tokens.join(',');
60
+ }
61
+
62
+ function parseCustomHeaders(raw) {
63
+ if (typeof raw !== 'string' || !raw.trim()) {
64
+ return {};
65
+ }
66
+ const trimmedRaw = raw.trim();
67
+
68
+ // Backward compatibility: older plugin versions persisted ANTHROPIC_CUSTOM_HEADERS
69
+ // as a JSON object string. Parse that first so managed key replacement works.
70
+ if (trimmedRaw.startsWith('{')) {
71
+ try {
72
+ const legacy = JSON.parse(trimmedRaw);
73
+ if (legacy && typeof legacy === 'object' && !Array.isArray(legacy)) {
74
+ const out = {};
75
+ for (const [key, value] of Object.entries(legacy)) {
76
+ const normalizedKey = String(key || '').trim().toLowerCase();
77
+ const normalizedValue = String(value ?? '').trim();
78
+ if (!normalizedKey || !normalizedValue) {
79
+ continue;
80
+ }
81
+ out[normalizedKey] = normalizedValue;
82
+ }
83
+ return out;
84
+ }
85
+ } catch {
86
+ // Fall through to line-based parser.
87
+ }
88
+ }
89
+
90
+ const parsed = {};
91
+ for (const line of raw.split('\n')) {
92
+ const trimmed = line.trim();
93
+ if (!trimmed) {
94
+ continue;
95
+ }
96
+ const idx = trimmed.indexOf(':');
97
+ if (idx <= 0) {
98
+ continue;
99
+ }
100
+ const key = trimmed.slice(0, idx).trim().toLowerCase();
101
+ const value = trimmed.slice(idx + 1).trim();
102
+ if (!key || !value) {
103
+ continue;
104
+ }
105
+ parsed[key] = value;
106
+ }
107
+ return parsed;
108
+ }
109
+
110
+ function serializeCustomHeaders(headers) {
111
+ return Object.entries(headers)
112
+ .map(([key, value]) => `${key}: ${value}`)
113
+ .join('\n');
114
+ }
115
+
116
+ function buildManagedAnthropicHeaders(config) {
117
+ const settings = config?.settings || {};
118
+ const headers = {};
119
+
120
+ const modeRaw = String(settings.mode || '')
121
+ .trim()
122
+ .toLowerCase();
123
+ if (VALID_ROUTING_MODES.has(modeRaw)) {
124
+ headers['x-plexor-mode'] = modeRaw === 'cost' ? 'eco' : modeRaw;
125
+ }
126
+
127
+ const orchestrationRaw = String(
128
+ settings.orchestrationMode || settings.orchestration_mode || ''
129
+ )
130
+ .trim()
131
+ .toLowerCase();
132
+ if (VALID_ORCHESTRATION_MODES.has(orchestrationRaw)) {
133
+ headers['x-plexor-orchestration-mode'] = orchestrationRaw;
134
+ }
135
+
136
+ const forceProvider = normalizeForcedProvider(
137
+ settings.preferred_provider ?? settings.preferredProvider ?? 'auto'
138
+ );
139
+ if (forceProvider && forceProvider !== 'auto') {
140
+ headers['x-force-provider'] = forceProvider;
141
+ }
142
+
143
+ const forceModel = normalizeForcedModel(settings.preferred_model ?? settings.preferredModel);
144
+ if (forceModel) {
145
+ headers['x-force-model'] = forceModel;
146
+ }
147
+
148
+ const allowProviders = normalizeCsv(
149
+ settings.provider_allowlist ?? settings.providerAllowlist ?? ''
150
+ );
151
+ if (allowProviders) {
152
+ headers['x-allow-providers'] = allowProviders;
153
+ }
154
+
155
+ const denyProviders = normalizeCsv(settings.provider_denylist ?? settings.providerDenylist ?? '');
156
+ if (denyProviders) {
157
+ headers['x-deny-providers'] = denyProviders;
158
+ }
159
+
160
+ const allowModels = normalizeCsv(settings.model_allowlist ?? settings.modelAllowlist ?? '');
161
+ if (allowModels) {
162
+ headers['x-allow-models'] = allowModels;
163
+ }
164
+
165
+ const denyModels = normalizeCsv(settings.model_denylist ?? settings.modelDenylist ?? '');
166
+ if (denyModels) {
167
+ headers['x-deny-models'] = denyModels;
168
+ }
169
+
170
+ return headers;
171
+ }
172
+
173
+ function syncClaudeCustomHeaders(config) {
174
+ try {
175
+ let settings = {};
176
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
177
+ const raw = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
178
+ settings = raw.trim() ? JSON.parse(raw) : {};
179
+ }
180
+
181
+ if (typeof settings !== 'object' || settings === null || Array.isArray(settings)) {
182
+ settings = {};
183
+ }
184
+ settings.env = settings.env && typeof settings.env === 'object' ? settings.env : {};
185
+
186
+ const existing = parseCustomHeaders(settings.env.ANTHROPIC_CUSTOM_HEADERS);
187
+ for (const key of MANAGED_HEADER_KEYS) {
188
+ delete existing[key];
189
+ }
190
+
191
+ const managed = buildManagedAnthropicHeaders(config);
192
+ const merged = { ...existing, ...managed };
193
+
194
+ if (Object.keys(merged).length) {
195
+ settings.env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(merged);
196
+ } else {
197
+ delete settings.env.ANTHROPIC_CUSTOM_HEADERS;
198
+ }
199
+
200
+ const claudeDir = path.dirname(CLAUDE_SETTINGS_PATH);
201
+ if (!fs.existsSync(claudeDir)) {
202
+ fs.mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
203
+ }
204
+ const tempPath = path.join(claudeDir, `.settings.${crypto.randomBytes(8).toString('hex')}.tmp`);
205
+ fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
206
+ fs.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
34
213
  function hasForcedHintConflict(config) {
35
214
  const settings = config?.settings || {};
36
215
  const provider = normalizeForcedProvider(
@@ -88,6 +267,8 @@ function saveConfig(config) {
88
267
  const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
89
268
  fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
90
269
  fs.renameSync(tempPath, CONFIG_PATH);
270
+ // Best-effort sync to Claude client headers so force hints are respected.
271
+ syncClaudeCustomHeaders(config);
91
272
  return true;
92
273
  } catch (err) {
93
274
  if (err.code === 'EACCES' || err.code === 'EPERM') {
@@ -121,4 +302,13 @@ function readSetting(config, configKey, configKeyAlt, envVar, validValues, defau
121
302
  return { value: defaultValue, source: 'default' };
122
303
  }
123
304
 
124
- module.exports = { loadConfig, saveConfig, readSetting, hasForcedHintConflict, validateForcedHintConfig };
305
+ module.exports = {
306
+ loadConfig,
307
+ saveConfig,
308
+ readSetting,
309
+ hasForcedHintConflict,
310
+ validateForcedHintConfig,
311
+ parseCustomHeaders,
312
+ serializeCustomHeaders,
313
+ buildManagedAnthropicHeaders
314
+ };
@@ -22,6 +22,8 @@ const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
22
22
  // Plexor gateway endpoints
23
23
  const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
24
24
  const PLEXOR_PROD_URL = 'https://api.plexor.dev/gateway/anthropic';
25
+ const PREVIOUS_API_KEY_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_API_KEY';
26
+ const PREVIOUS_AUTH_TOKEN_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN';
25
27
 
26
28
  /**
27
29
  * Check if a base URL is a Plexor-managed gateway URL.
@@ -39,6 +41,83 @@ function isManagedGatewayUrl(baseUrl) {
39
41
  );
40
42
  }
41
43
 
44
+ function isPlexorApiKey(value = '') {
45
+ return typeof value === 'string' && value.startsWith('plx_');
46
+ }
47
+
48
+ function getPlexorAuthKey(env = {}) {
49
+ const apiKey = env.ANTHROPIC_API_KEY || '';
50
+ const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
51
+
52
+ if (isPlexorApiKey(apiKey)) return apiKey;
53
+ if (isPlexorApiKey(authToken)) return authToken;
54
+ return apiKey || authToken || '';
55
+ }
56
+
57
+ function hasPlexorManagedAuth(env = {}) {
58
+ return (
59
+ isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '') ||
60
+ isPlexorApiKey(env.ANTHROPIC_API_KEY || '') ||
61
+ isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '')
62
+ );
63
+ }
64
+
65
+ function stashDirectAuthEnv(env, plexorApiKey) {
66
+ const alreadyManaged = hasPlexorManagedAuth(env);
67
+ const currentApiKey = env.ANTHROPIC_API_KEY || '';
68
+ const currentAuthToken = env.ANTHROPIC_AUTH_TOKEN || '';
69
+
70
+ if (currentApiKey && !isPlexorApiKey(currentApiKey) && currentApiKey !== plexorApiKey) {
71
+ env[PREVIOUS_API_KEY_ENV] = currentApiKey;
72
+ } else if (!alreadyManaged) {
73
+ delete env[PREVIOUS_API_KEY_ENV];
74
+ }
75
+
76
+ if (currentAuthToken && !isPlexorApiKey(currentAuthToken) && currentAuthToken !== plexorApiKey) {
77
+ env[PREVIOUS_AUTH_TOKEN_ENV] = currentAuthToken;
78
+ } else if (!alreadyManaged) {
79
+ delete env[PREVIOUS_AUTH_TOKEN_ENV];
80
+ }
81
+ }
82
+
83
+ function setPlexorAuthKey(env, apiKey) {
84
+ stashDirectAuthEnv(env, apiKey);
85
+ env.ANTHROPIC_API_KEY = apiKey;
86
+ env.ANTHROPIC_AUTH_TOKEN = apiKey;
87
+ }
88
+
89
+ function clearPlexorRoutingEnv(env = {}) {
90
+ const hasManagedBaseUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '');
91
+ const hasPlexorApiKey = isPlexorApiKey(env.ANTHROPIC_API_KEY || '');
92
+ const hasPlexorAuthToken = isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '');
93
+
94
+ if (!hasManagedBaseUrl && !hasPlexorApiKey && !hasPlexorAuthToken) {
95
+ return false;
96
+ }
97
+
98
+ if (hasManagedBaseUrl) {
99
+ delete env.ANTHROPIC_BASE_URL;
100
+ }
101
+ if (hasPlexorAuthToken) {
102
+ delete env.ANTHROPIC_AUTH_TOKEN;
103
+ }
104
+ if (hasPlexorApiKey) {
105
+ delete env.ANTHROPIC_API_KEY;
106
+ }
107
+
108
+ if (!env.ANTHROPIC_API_KEY && env[PREVIOUS_API_KEY_ENV]) {
109
+ env.ANTHROPIC_API_KEY = env[PREVIOUS_API_KEY_ENV];
110
+ }
111
+ if (!env.ANTHROPIC_AUTH_TOKEN && env[PREVIOUS_AUTH_TOKEN_ENV]) {
112
+ env.ANTHROPIC_AUTH_TOKEN = env[PREVIOUS_AUTH_TOKEN_ENV];
113
+ }
114
+
115
+ delete env[PREVIOUS_API_KEY_ENV];
116
+ delete env[PREVIOUS_AUTH_TOKEN_ENV];
117
+
118
+ return true;
119
+ }
120
+
42
121
  class ClaudeSettingsManager {
43
122
  constructor() {
44
123
  this.settingsPath = SETTINGS_PATH;
@@ -171,8 +250,9 @@ class ClaudeSettingsManager {
171
250
  /**
172
251
  * Enable Plexor routing by setting env vars in settings.json
173
252
  *
174
- * This is the KEY mechanism: setting ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
175
- * in the env block redirects ALL Claude Code sessions to Plexor automatically.
253
+ * This is the KEY mechanism: setting ANTHROPIC_BASE_URL plus both Claude auth
254
+ * env vars in settings.json redirects ALL Claude Code sessions to Plexor
255
+ * automatically, even when the user previously configured direct API auth.
176
256
  *
177
257
  * @param {string} apiKey - Plexor API key (plx_*)
178
258
  * @param {Object} options - { useStaging: boolean }
@@ -191,10 +271,10 @@ class ClaudeSettingsManager {
191
271
  settings.env = {};
192
272
  }
193
273
 
194
- // Set the magic environment variables
195
- // ANTHROPIC_AUTH_TOKEN has higher precedence than ANTHROPIC_API_KEY
274
+ // Mirror the Plexor key into both Claude auth env vars so every Claude
275
+ // runtime path uses the gateway instead of any previously saved direct key.
196
276
  settings.env.ANTHROPIC_BASE_URL = apiUrl;
197
- settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
277
+ setPlexorAuthKey(settings.env, apiKey);
198
278
 
199
279
  const success = this.save(settings);
200
280
 
@@ -229,9 +309,10 @@ class ClaudeSettingsManager {
229
309
  return true;
230
310
  }
231
311
 
232
- // Remove Plexor-specific env vars
233
- delete settings.env.ANTHROPIC_BASE_URL;
234
- delete settings.env.ANTHROPIC_AUTH_TOKEN;
312
+ if (!clearPlexorRoutingEnv(settings.env)) {
313
+ console.log('Plexor routing is not currently enabled.');
314
+ return true;
315
+ }
235
316
 
236
317
  // Clean up empty env block
237
318
  if (Object.keys(settings.env).length === 0) {
@@ -262,7 +343,8 @@ class ClaudeSettingsManager {
262
343
  const settings = this.load();
263
344
 
264
345
  const baseUrl = settings.env?.ANTHROPIC_BASE_URL || null;
265
- const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
346
+ const authKey = getPlexorAuthKey(settings.env);
347
+ const hasToken = !!authKey;
266
348
 
267
349
  // Check if routing to Plexor (any variant: prod, staging, localhost, tunnel)
268
350
  const isPlexorRouting = isManagedGatewayUrl(baseUrl);
@@ -272,7 +354,7 @@ class ClaudeSettingsManager {
272
354
  baseUrl,
273
355
  hasToken,
274
356
  isStaging: baseUrl?.includes('staging') || false,
275
- tokenPreview: hasToken ? settings.env.ANTHROPIC_AUTH_TOKEN.substring(0, 12) + '...' : null
357
+ tokenPreview: hasToken ? authKey.substring(0, 12) + '...' : null
276
358
  };
277
359
  } catch {
278
360
  return { enabled: false, baseUrl: null, hasToken: false };
@@ -288,14 +370,14 @@ class ClaudeSettingsManager {
288
370
  try {
289
371
  const settings = this.load();
290
372
  const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
291
- const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
373
+ const authKey = getPlexorAuthKey(settings.env);
292
374
  const isPlexorUrl = isManagedGatewayUrl(baseUrl);
293
375
 
294
- if (isPlexorUrl && !authToken) {
295
- return { partial: true, issue: 'Plexor URL set but no auth token' };
376
+ if (isPlexorUrl && !authKey) {
377
+ return { partial: true, issue: 'Plexor URL set but no Plexor auth key' };
296
378
  }
297
- if (isPlexorUrl && !authToken.startsWith('plx_')) {
298
- return { partial: true, issue: 'Plexor URL set but auth token is not a Plexor key' };
379
+ if (isPlexorUrl && !isPlexorApiKey(authKey)) {
380
+ return { partial: true, issue: 'Plexor URL set but auth key is not a Plexor key' };
299
381
  }
300
382
  return { partial: false, issue: null };
301
383
  } catch {
@@ -316,7 +398,7 @@ class ClaudeSettingsManager {
316
398
  settings.env = {};
317
399
  }
318
400
 
319
- settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
401
+ setPlexorAuthKey(settings.env, apiKey);
320
402
  return this.save(settings);
321
403
  } catch (err) {
322
404
  console.error('Failed to update API key:', err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin-staging",
3
- "version": "0.1.0-beta.18",
3
+ "version": "0.1.0-beta.19",
4
4
  "description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
5
5
  "main": "lib/constants.js",
6
6
  "bin": {
@@ -165,13 +165,39 @@ function isManagedGatewayUrl(baseUrl) {
165
165
  * Used to detect when a different variant was previously installed.
166
166
  */
167
167
  const THIS_VARIANT_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
168
+ const PREVIOUS_API_KEY_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_API_KEY';
169
+ const PREVIOUS_AUTH_TOKEN_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN';
168
170
 
169
171
  /**
170
172
  * Check for orphaned Plexor routing in settings.json without valid config.
171
173
  * Also detects variant mismatch (e.g., localhost plugin was installed, now
172
- * installing staging plugin) and migrates ANTHROPIC_BASE_URL + fixes
173
- * ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN (#2174).
174
+ * installing staging plugin) and migrates ANTHROPIC_BASE_URL + syncs
175
+ * Claude auth env vars for Plexor-managed gateways.
174
176
  */
177
+ function selectManagedAuthKey(env = {}) {
178
+ const apiKey = env.ANTHROPIC_API_KEY || '';
179
+ const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
180
+
181
+ if (apiKey.startsWith('plx_')) return apiKey;
182
+ if (authToken.startsWith('plx_')) return authToken;
183
+ return apiKey || authToken || '';
184
+ }
185
+
186
+ function syncManagedAuthEnv(env, managedAuthKey) {
187
+ const currentApiKey = env.ANTHROPIC_API_KEY || '';
188
+ const currentAuthToken = env.ANTHROPIC_AUTH_TOKEN || '';
189
+
190
+ if (currentApiKey && !currentApiKey.startsWith('plx_') && currentApiKey !== managedAuthKey) {
191
+ env[PREVIOUS_API_KEY_ENV] = currentApiKey;
192
+ }
193
+ if (currentAuthToken && !currentAuthToken.startsWith('plx_') && currentAuthToken !== managedAuthKey) {
194
+ env[PREVIOUS_AUTH_TOKEN_ENV] = currentAuthToken;
195
+ }
196
+
197
+ env.ANTHROPIC_API_KEY = managedAuthKey;
198
+ env.ANTHROPIC_AUTH_TOKEN = managedAuthKey;
199
+ }
200
+
175
201
  function checkOrphanedRouting() {
176
202
  // Use the resolved HOME_DIR (not process.env.HOME which may be wrong under sudo -u)
177
203
  const settingsPath = path.join(HOME_DIR, '.claude', 'settings.json');
@@ -187,21 +213,15 @@ function checkOrphanedRouting() {
187
213
  const hasPlexorUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL);
188
214
 
189
215
  if (hasPlexorUrl) {
190
- // Fix #2174: Only migrate ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN
191
- // when routing through a Plexor-managed URL. Non-Plexor setups
192
- // (direct Anthropic, etc.) should not have their auth mutated.
193
- if (env.ANTHROPIC_API_KEY && !env.ANTHROPIC_AUTH_TOKEN) {
194
- env.ANTHROPIC_AUTH_TOKEN = env.ANTHROPIC_API_KEY;
195
- delete env.ANTHROPIC_API_KEY;
196
- settings.env = env;
197
- settingsChanged = true;
198
- console.log('\n Migrated ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN (fix #2174)');
199
- } else if (env.ANTHROPIC_API_KEY && env.ANTHROPIC_AUTH_TOKEN) {
200
- // Both exist — remove the lower-precedence one to avoid confusion
201
- delete env.ANTHROPIC_API_KEY;
216
+ // Keep both Claude auth env vars aligned to the Plexor key so Claude API
217
+ // auth cannot override the gateway after plugin setup.
218
+ const managedAuthKey = selectManagedAuthKey(env);
219
+ if (managedAuthKey &&
220
+ (env.ANTHROPIC_API_KEY !== managedAuthKey || env.ANTHROPIC_AUTH_TOKEN !== managedAuthKey)) {
221
+ syncManagedAuthEnv(env, managedAuthKey);
202
222
  settings.env = env;
203
223
  settingsChanged = true;
204
- console.log('\n Removed redundant ANTHROPIC_API_KEY (ANTHROPIC_AUTH_TOKEN takes precedence)');
224
+ console.log('\n Synced Plexor auth into ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN');
205
225
  }
206
226
  // Check if there's a valid Plexor config
207
227
  let hasValidConfig = false;
@@ -385,11 +405,6 @@ function main() {
385
405
  chownRecursive(PLEXOR_CONFIG_DIR, uid, gid);
386
406
  }
387
407
 
388
- // Detect shell type
389
- const shell = process.env.SHELL || '';
390
- const isZsh = shell.includes('zsh');
391
- const shellRc = isZsh ? '~/.zshrc' : '~/.bashrc';
392
-
393
408
  // Print success message with clear onboarding steps
394
409
  console.log('');
395
410
  console.log(' ╔═══════════════════════════════════════════════════════════════════╗');
@@ -414,24 +429,18 @@ function main() {
414
429
  }
415
430
  console.log('');
416
431
 
417
- // CRITICAL: Make the required step VERY obvious
418
432
  console.log(' ┌─────────────────────────────────────────────────────────────────┐');
419
- console.log(' │ REQUIRED: Run this command to enable Plexor routing: │');
433
+ console.log(' │ NEXT: Start Claude Code and run /plexor-setup │');
420
434
  console.log(' └─────────────────────────────────────────────────────────────────┘');
421
435
  console.log('');
422
- console.log(' For Claude MAX users (OAuth):');
423
- console.log('');
424
- console.log(` echo 'export ANTHROPIC_BASE_URL="https://staging.api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
425
- console.log(` source ${shellRc}`);
426
- console.log('');
427
- console.log(' For API key users (get key at https://plexor.dev/dashboard):');
428
- console.log('');
429
- console.log(` echo 'export ANTHROPIC_BASE_URL="https://staging.api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
430
- console.log(` echo 'export ANTHROPIC_AUTH_TOKEN="plx_your_key_here"' >> ${shellRc}`);
431
- console.log(` source ${shellRc}`);
436
+ console.log(' /plexor-setup will:');
437
+ console.log(' 1. Ask for your Plexor API key');
438
+ console.log(' 2. Write ~/.plexor/config.json');
439
+ console.log(' 3. Point Claude at the Plexor staging gateway');
440
+ console.log(' 4. Replace any existing Claude API env auth while Plexor is active');
432
441
  console.log('');
433
442
  console.log(' ┌─────────────────────────────────────────────────────────────────┐');
434
- console.log(' │ Then start Claude Code and run: /plexor-status │');
443
+ console.log(' │ No shell edits or Claude restart required after setup │');
435
444
  console.log(' └─────────────────────────────────────────────────────────────────┘');
436
445
  console.log('');
437
446
  console.log(' Available commands:');
@@ -57,6 +57,53 @@ const results = {
57
57
  pluginDir: false
58
58
  };
59
59
 
60
+ function isManagedGatewayUrl(baseUrl = '') {
61
+ return (
62
+ baseUrl.includes('plexor') ||
63
+ baseUrl.includes('staging.api') ||
64
+ baseUrl.includes('localhost') ||
65
+ baseUrl.includes('127.0.0.1') ||
66
+ baseUrl.includes('ngrok') ||
67
+ baseUrl.includes('localtunnel')
68
+ );
69
+ }
70
+
71
+ function isPlexorApiKey(value = '') {
72
+ return typeof value === 'string' && value.startsWith('plx_');
73
+ }
74
+
75
+ function clearPlexorRoutingEnv(env = {}) {
76
+ const hasManagedBaseUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '');
77
+ const hasPlexorAuthToken = isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '');
78
+ const hasPlexorApiKey = isPlexorApiKey(env.ANTHROPIC_API_KEY || '');
79
+
80
+ if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey) {
81
+ return false;
82
+ }
83
+
84
+ if (hasManagedBaseUrl) {
85
+ delete env.ANTHROPIC_BASE_URL;
86
+ }
87
+ if (hasPlexorAuthToken) {
88
+ delete env.ANTHROPIC_AUTH_TOKEN;
89
+ }
90
+ if (hasPlexorApiKey) {
91
+ delete env.ANTHROPIC_API_KEY;
92
+ }
93
+
94
+ if (!env.ANTHROPIC_API_KEY && env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY) {
95
+ env.ANTHROPIC_API_KEY = env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY;
96
+ }
97
+ if (!env.ANTHROPIC_AUTH_TOKEN && env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN) {
98
+ env.ANTHROPIC_AUTH_TOKEN = env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN;
99
+ }
100
+
101
+ delete env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY;
102
+ delete env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN;
103
+
104
+ return true;
105
+ }
106
+
60
107
  // 1. Remove routing from settings.json
61
108
  // This is CRITICAL - do NOT depend on settings-manager module since it may not load during uninstall
62
109
  try {
@@ -66,18 +113,14 @@ try {
66
113
  if (data && data.trim()) {
67
114
  const settings = JSON.parse(data);
68
115
  if (settings.env) {
69
- const hadBaseUrl = !!settings.env.ANTHROPIC_BASE_URL;
70
- const hadAuthToken = !!settings.env.ANTHROPIC_AUTH_TOKEN;
71
-
72
- delete settings.env.ANTHROPIC_BASE_URL;
73
- delete settings.env.ANTHROPIC_AUTH_TOKEN;
116
+ const routingChanged = clearPlexorRoutingEnv(settings.env);
74
117
 
75
118
  // Clean up empty env block
76
119
  if (Object.keys(settings.env).length === 0) {
77
120
  delete settings.env;
78
121
  }
79
122
 
80
- if (hadBaseUrl || hadAuthToken) {
123
+ if (routingChanged) {
81
124
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
82
125
  results.routing = true;
83
126
  }