@plexor-dev/claude-code-plugin-staging 0.1.0-beta.2 → 0.1.0-beta.20

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.
@@ -13,42 +13,109 @@ const os = require('os');
13
13
  const { execSync } = require('child_process');
14
14
 
15
15
  /**
16
- * Get the correct home directory, accounting for sudo.
17
- * When running with sudo, os.homedir() returns /root, but we want
18
- * the actual user's home directory.
16
+ * Resolve the home directory for a given username by querying /etc/passwd.
17
+ * This is the authoritative source and handles non-standard home paths
18
+ * (e.g., /root, /opt/users/foo, NIS/LDAP users, etc.).
19
+ * Returns null if lookup fails (Windows, missing getent, etc.).
20
+ */
21
+ function getHomeDirFromPasswd(username) {
22
+ try {
23
+ const entry = execSync(`getent passwd ${username}`, { encoding: 'utf8' }).trim();
24
+ // Format: username:x:uid:gid:gecos:homedir:shell
25
+ const fields = entry.split(':');
26
+ if (fields.length >= 6 && fields[5]) {
27
+ return fields[5];
28
+ }
29
+ } catch {
30
+ // getent not available or user not found
31
+ }
32
+ return null;
33
+ }
34
+
35
+ /**
36
+ * Get the correct home directory for the process's effective user.
37
+ *
38
+ * Handles three scenarios:
39
+ * 1. Normal execution: HOME is correct, os.homedir() is correct.
40
+ * 2. `sudo npm install`: SUDO_USER is set, os.homedir() returns /root,
41
+ * but we want the SUDO_USER's home.
42
+ * 3. `sudo -u target npm install`:
43
+ * HOME may still be the *caller's* home (e.g.,
44
+ * /home/azureuser), SUDO_USER is the *caller*
45
+ * (not the target), but process.getuid() returns
46
+ * the *target* UID. We must resolve home from
47
+ * /etc/passwd by UID.
48
+ *
49
+ * Resolution order (most authoritative first):
50
+ * a) Look up the effective UID in /etc/passwd via getent (handles sudo -u)
51
+ * b) Fall back to os.homedir() (works for normal execution)
52
+ * c) Fall back to HOME / USERPROFILE env vars (last resort)
19
53
  */
20
54
  function getHomeDir() {
21
- // Check if running with sudo - SUDO_USER contains the original username
22
- if (process.env.SUDO_USER) {
23
- // On Linux/Mac, home directories are typically /home/<user> or /Users/<user>
24
- const platform = os.platform();
25
- if (platform === 'darwin') {
26
- return path.join('/Users', process.env.SUDO_USER);
27
- } else if (platform === 'linux') {
28
- return path.join('/home', process.env.SUDO_USER);
55
+ // On non-Windows, resolve via the effective UID's passwd entry.
56
+ // This is the most reliable method and correctly handles both
57
+ // `sudo` and `sudo -u <target>` scenarios.
58
+ if (os.platform() !== 'win32') {
59
+ try {
60
+ const uid = process.getuid();
61
+ const entry = execSync(`getent passwd ${uid}`, { encoding: 'utf8' }).trim();
62
+ const fields = entry.split(':');
63
+ if (fields.length >= 6 && fields[5]) {
64
+ return fields[5];
65
+ }
66
+ } catch {
67
+ // Fall through to other methods
29
68
  }
30
69
  }
31
- return os.homedir();
70
+
71
+ // Fallback: os.homedir() (reads HOME env var, then passwd on Unix)
72
+ const home = os.homedir();
73
+ if (home) return home;
74
+
75
+ // Last resort: environment variables
76
+ return process.env.HOME || process.env.USERPROFILE || '/tmp';
32
77
  }
33
78
 
34
79
  /**
35
- * Get uid/gid for the target user (handles sudo case).
36
- * Returns null if not running with sudo or on Windows.
80
+ * Get uid/gid for the effective user running this process.
81
+ * Under `sudo`, the effective user is root but we want to chown to the
82
+ * original (SUDO_USER) or target (`sudo -u target`) user.
83
+ * Under `sudo -u target`, process.getuid() IS the target, so we use that.
84
+ * Returns null on Windows or if no privilege elevation detected.
37
85
  */
38
86
  function getTargetUserIds() {
39
- const sudoUser = process.env.SUDO_USER;
40
- if (!sudoUser || os.platform() === 'win32') {
87
+ if (os.platform() === 'win32') {
41
88
  return null;
42
89
  }
43
90
 
44
91
  try {
45
- // Get uid and gid for the sudo user
46
- const uid = parseInt(execSync(`id -u ${sudoUser}`, { encoding: 'utf8' }).trim(), 10);
47
- const gid = parseInt(execSync(`id -g ${sudoUser}`, { encoding: 'utf8' }).trim(), 10);
48
- return { uid, gid, user: sudoUser };
92
+ const effectiveUid = process.getuid();
93
+
94
+ // If we're running as root (uid 0), we were likely invoked via `sudo`.
95
+ // Chown files to SUDO_USER (the human who ran sudo).
96
+ if (effectiveUid === 0 && process.env.SUDO_USER) {
97
+ const uid = parseInt(execSync(`id -u ${process.env.SUDO_USER}`, { encoding: 'utf8' }).trim(), 10);
98
+ const gid = parseInt(execSync(`id -g ${process.env.SUDO_USER}`, { encoding: 'utf8' }).trim(), 10);
99
+ return { uid, gid, user: process.env.SUDO_USER };
100
+ }
101
+
102
+ // If we're NOT root but SUDO_USER is set, we were invoked via `sudo -u target`.
103
+ // The effective UID is already the target user. Chown to that user.
104
+ if (effectiveUid !== 0 && process.env.SUDO_USER) {
105
+ const entry = execSync(`getent passwd ${effectiveUid}`, { encoding: 'utf8' }).trim();
106
+ const fields = entry.split(':');
107
+ if (fields.length >= 4) {
108
+ const username = fields[0];
109
+ const uid = parseInt(fields[2], 10);
110
+ const gid = parseInt(fields[3], 10);
111
+ return { uid, gid, user: username };
112
+ }
113
+ }
49
114
  } catch {
50
- return null;
115
+ // Fall through
51
116
  }
117
+
118
+ return null;
52
119
  }
53
120
 
54
121
  /**
@@ -70,12 +137,139 @@ function chownRecursive(dirPath, uid, gid) {
70
137
 
71
138
  const HOME_DIR = getHomeDir();
72
139
  const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
140
+ const LIB_SOURCE = path.join(__dirname, '..', 'lib');
73
141
  const CLAUDE_COMMANDS_DIR = path.join(HOME_DIR, '.claude', 'commands');
74
142
  const PLEXOR_PLUGINS_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'commands');
143
+ const PLEXOR_LIB_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'lib');
75
144
  const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
76
145
  const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
77
146
 
147
+ /**
148
+ * Check if a base URL is a Plexor-managed gateway URL.
149
+ * Detects all variants: production, staging, localhost, tunnels.
150
+ */
151
+ function isManagedGatewayUrl(baseUrl) {
152
+ if (!baseUrl) return false;
153
+ return (
154
+ baseUrl.includes('plexor') ||
155
+ baseUrl.includes('staging.api') ||
156
+ baseUrl.includes('localhost') ||
157
+ baseUrl.includes('127.0.0.1') ||
158
+ baseUrl.includes('ngrok') ||
159
+ baseUrl.includes('localtunnel')
160
+ );
161
+ }
162
+
163
+ /**
164
+ * The expected base URL for THIS plugin variant.
165
+ * Used to detect when a different variant was previously installed.
166
+ */
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';
170
+
171
+ /**
172
+ * Check for orphaned Plexor routing in settings.json without valid config.
173
+ * Also detects variant mismatch (e.g., localhost plugin was installed, now
174
+ * installing staging plugin) and migrates ANTHROPIC_BASE_URL + syncs
175
+ * Claude auth env vars for Plexor-managed gateways.
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
+
201
+ function checkOrphanedRouting() {
202
+ // Use the resolved HOME_DIR (not process.env.HOME which may be wrong under sudo -u)
203
+ const settingsPath = path.join(HOME_DIR, '.claude', 'settings.json');
204
+ const configPath = path.join(HOME_DIR, '.plexor', 'config.json');
205
+
206
+ try {
207
+ if (!fs.existsSync(settingsPath)) return;
208
+
209
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
210
+ const env = settings.env || {};
211
+ let settingsChanged = false;
212
+
213
+ const hasPlexorUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL);
214
+
215
+ if (hasPlexorUrl) {
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);
222
+ settings.env = env;
223
+ settingsChanged = true;
224
+ console.log('\n Synced Plexor auth into ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN');
225
+ }
226
+ // Check if there's a valid Plexor config
227
+ let hasValidConfig = false;
228
+ try {
229
+ if (fs.existsSync(configPath)) {
230
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
231
+ hasValidConfig = (config.auth?.api_key || config.apiKey || '').startsWith('plx_');
232
+ }
233
+ } catch (e) {}
234
+
235
+ if (!hasValidConfig) {
236
+ console.log('\n Warning: Detected orphaned Plexor routing in Claude settings');
237
+ console.log(' This may be from a previous installation.\n');
238
+ console.log(' Run /plexor-login to reconfigure, or');
239
+ console.log(' Run /plexor-uninstall to clean up\n');
240
+ } else {
241
+ // Fix #2176: Detect variant mismatch and migrate URL
242
+ const currentUrl = env.ANTHROPIC_BASE_URL;
243
+ if (currentUrl !== THIS_VARIANT_URL) {
244
+ env.ANTHROPIC_BASE_URL = THIS_VARIANT_URL;
245
+ settings.env = env;
246
+ settingsChanged = true;
247
+ console.log(`\n Migrated ANTHROPIC_BASE_URL to this variant's gateway:`);
248
+ console.log(` Old: ${currentUrl}`);
249
+ console.log(` New: ${THIS_VARIANT_URL}\n`);
250
+ } else {
251
+ console.log('\n Existing Plexor configuration detected');
252
+ console.log(' Your previous settings have been preserved.\n');
253
+ }
254
+ }
255
+ }
256
+
257
+ // Write back settings if any migration was applied
258
+ if (settingsChanged) {
259
+ const crypto = require('crypto');
260
+ const claudeDir = path.join(HOME_DIR, '.claude');
261
+ const tempId = crypto.randomBytes(8).toString('hex');
262
+ const tempPath = path.join(claudeDir, `.settings.${tempId}.tmp`);
263
+ fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
264
+ fs.renameSync(tempPath, settingsPath);
265
+ }
266
+ } catch (e) {
267
+ // Ignore errors in detection - don't break install
268
+ }
269
+ }
270
+
78
271
  // Default configuration for new installs
272
+ // STAGING PACKAGE - uses staging API
79
273
  const DEFAULT_CONFIG = {
80
274
  version: 1,
81
275
  auth: {
@@ -84,13 +278,16 @@ const DEFAULT_CONFIG = {
84
278
  },
85
279
  settings: {
86
280
  enabled: true,
87
- apiUrl: "https://api.plexor.dev",
281
+ apiUrl: "https://staging.api.plexor.dev",
88
282
  mode: "balanced",
89
283
  localCacheEnabled: true
90
284
  }
91
285
  };
92
286
 
93
287
  function main() {
288
+ // Check for orphaned routing at start of postinstall
289
+ checkOrphanedRouting();
290
+
94
291
  try {
95
292
  // Get target user info for chown (if running with sudo)
96
293
  const targetUser = getTargetUserIds();
@@ -101,6 +298,9 @@ function main() {
101
298
  // Create ~/.claude/plugins/plexor/commands/ for JS executors
102
299
  fs.mkdirSync(PLEXOR_PLUGINS_DIR, { recursive: true });
103
300
 
301
+ // Create ~/.claude/plugins/plexor/lib/ for shared modules
302
+ fs.mkdirSync(PLEXOR_LIB_DIR, { recursive: true });
303
+
104
304
  // Create ~/.plexor/ with secure permissions (owner only)
105
305
  fs.mkdirSync(PLEXOR_CONFIG_DIR, { recursive: true, mode: 0o700 });
106
306
 
@@ -161,6 +361,40 @@ function main() {
161
361
  jsInstalled.push(file);
162
362
  }
163
363
 
364
+ // Copy lib files to ~/.claude/plugins/plexor/lib/
365
+ // CRITICAL: These are required for commands to work
366
+ const libInstalled = [];
367
+ if (fs.existsSync(LIB_SOURCE)) {
368
+ const libFiles = fs.readdirSync(LIB_SOURCE).filter(f => f.endsWith('.js'));
369
+ if (libFiles.length === 0) {
370
+ console.warn(' ⚠ Warning: No lib files found in package. Commands may not work.');
371
+ }
372
+ for (const file of libFiles) {
373
+ try {
374
+ const src = path.join(LIB_SOURCE, file);
375
+ const dest = path.join(PLEXOR_LIB_DIR, file);
376
+ fs.copyFileSync(src, dest);
377
+ libInstalled.push(file);
378
+ } catch (err) {
379
+ console.error(` ✗ Failed to copy lib/${file}: ${err.message}`);
380
+ }
381
+ }
382
+ } else {
383
+ console.error(' ✗ CRITICAL: lib/ directory not found in package.');
384
+ console.error(' Commands will fail. Please reinstall the package.');
385
+ console.error(` Expected location: ${LIB_SOURCE}`);
386
+ }
387
+
388
+ // Verify critical lib file exists
389
+ const criticalLibFile = path.join(PLEXOR_LIB_DIR, 'settings-manager.js');
390
+ if (!fs.existsSync(criticalLibFile)) {
391
+ console.error('');
392
+ console.error(' ✗ CRITICAL: settings-manager.js was not installed.');
393
+ console.error(' This file is required for commands to work.');
394
+ console.error(' Try reinstalling: npm install @plexor-dev/claude-code-plugin-staging');
395
+ console.error('');
396
+ }
397
+
164
398
  // Fix file ownership when running with sudo
165
399
  // Files are created as root but should be owned by the original user
166
400
  if (targetUser) {
@@ -171,11 +405,6 @@ function main() {
171
405
  chownRecursive(PLEXOR_CONFIG_DIR, uid, gid);
172
406
  }
173
407
 
174
- // Detect shell type
175
- const shell = process.env.SHELL || '';
176
- const isZsh = shell.includes('zsh');
177
- const shellRc = isZsh ? '~/.zshrc' : '~/.bashrc';
178
-
179
408
  // Print success message with clear onboarding steps
180
409
  console.log('');
181
410
  console.log(' ╔═══════════════════════════════════════════════════════════════════╗');
@@ -190,38 +419,36 @@ function main() {
190
419
  }
191
420
  console.log(` ✓ Installed ${installed.length} slash commands to ~/.claude/commands/`);
192
421
  if (jsInstalled.length > 0) {
193
- console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/`);
422
+ console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/commands/`);
423
+ }
424
+ if (libInstalled.length > 0) {
425
+ console.log(` ✓ Installed ${libInstalled.length} lib modules to ~/.claude/plugins/plexor/lib/`);
194
426
  }
195
427
  if (targetUser) {
196
428
  console.log(` ✓ Set file ownership to ${targetUser.user}`);
197
429
  }
198
430
  console.log('');
199
431
 
200
- // CRITICAL: Make the required step VERY obvious
201
432
  console.log(' ┌─────────────────────────────────────────────────────────────────┐');
202
- console.log(' │ REQUIRED: Run this command to enable Plexor routing: │');
433
+ console.log(' │ NEXT: Start Claude Code and run /plexor-setup │');
203
434
  console.log(' └─────────────────────────────────────────────────────────────────┘');
204
435
  console.log('');
205
- console.log(' For Claude MAX users (OAuth):');
206
- console.log('');
207
- console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
208
- console.log(` source ${shellRc}`);
209
- console.log('');
210
- console.log(' For API key users (get key at https://plexor.dev/dashboard):');
211
- console.log('');
212
- console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
213
- console.log(` echo 'export ANTHROPIC_API_KEY="plx_your_key_here"' >> ${shellRc}`);
214
- 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. Preserve prior Claude auth for restore on logout');
441
+ console.log(' 5. Verify Claude routing with a deterministic check');
215
442
  console.log('');
216
443
  console.log(' ┌─────────────────────────────────────────────────────────────────┐');
217
- console.log(' │ Then start Claude Code and run: /plexor-status │');
444
+ console.log(' │ No shell edits or Claude restart required after setup │');
218
445
  console.log(' └─────────────────────────────────────────────────────────────────┘');
219
446
  console.log('');
220
447
  console.log(' Available commands:');
448
+ console.log(' /plexor-setup - First-time setup wizard');
449
+ console.log(' /plexor-login - Advanced/manual auth path');
221
450
  console.log(' /plexor-status - Check connection and see savings');
222
- console.log(' /plexor-mode - Switch modes (eco/balanced/quality)');
223
- console.log(' /plexor-login - Authenticate with API key');
224
- console.log(' /plexor-settings - View/modify settings');
451
+ console.log(' /plexor-enabled - Enable/disable Plexor routing');
225
452
  console.log('');
226
453
  console.log(' Documentation: https://plexor.dev/docs');
227
454
  console.log('');
@@ -1,67 +1,220 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Plexor Claude Code Plugin - Uninstall Script
4
+ * Plexor Claude Code Plugin (Staging) - Comprehensive Uninstall Script
5
5
  *
6
- * Removes slash commands from ~/.claude/commands/
7
- * Optionally restores backups if they exist.
6
+ * Runs on npm uninstall (when npm actually calls it).
7
+ * Also callable directly: node scripts/uninstall.js
8
+ *
9
+ * Performs complete cleanup:
10
+ * 1. Removes Plexor routing from ~/.claude/settings.json
11
+ * 2. Removes slash command files from ~/.claude/commands/
12
+ * 3. Removes plugin directory from ~/.claude/plugins/plexor/
13
+ * 4. Restores any backups if they exist
8
14
  */
9
15
 
10
16
  const fs = require('fs');
11
17
  const path = require('path');
12
18
  const os = require('os');
19
+ const { execSync } = require('child_process');
13
20
 
14
- const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
15
- const CLAUDE_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
21
+ /**
22
+ * Get the correct home directory for the process's effective user.
23
+ * Resolves via /etc/passwd to handle sudo and sudo -u correctly.
24
+ */
25
+ function getHomeDir() {
26
+ if (os.platform() !== 'win32') {
27
+ try {
28
+ const uid = process.getuid();
29
+ const entry = execSync(`getent passwd ${uid}`, { encoding: 'utf8' }).trim();
30
+ const fields = entry.split(':');
31
+ if (fields.length >= 6 && fields[5]) {
32
+ return fields[5];
33
+ }
34
+ } catch {
35
+ // Fall through
36
+ }
37
+ }
38
+ const h = os.homedir();
39
+ if (h) return h;
40
+ return process.env.HOME || process.env.USERPROFILE || null;
41
+ }
16
42
 
17
- function main() {
18
- try {
19
- // Get list of our command files
20
- const files = fs.readdirSync(COMMANDS_SOURCE)
21
- .filter(f => f.endsWith('.md'));
43
+ const home = getHomeDir();
44
+ if (!home) {
45
+ console.log('Warning: Could not determine home directory, skipping cleanup');
46
+ process.exit(0);
47
+ }
48
+
49
+ console.log('');
50
+ console.log(' Plexor plugin cleanup...');
51
+ console.log('');
52
+
53
+ const results = {
54
+ routing: false,
55
+ commands: [],
56
+ restored: [],
57
+ pluginDir: false
58
+ };
22
59
 
23
- const removed = [];
24
- const restored = [];
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
+ }
25
74
 
26
- for (const file of files) {
27
- const dest = path.join(CLAUDE_COMMANDS_DIR, file);
28
- const backupPath = dest + '.backup';
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
+ }
29
106
 
30
- if (fs.existsSync(dest)) {
31
- fs.unlinkSync(dest);
32
- removed.push(file.replace('.md', ''));
107
+ // 1. Remove routing from settings.json
108
+ // This is CRITICAL - do NOT depend on settings-manager module since it may not load during uninstall
109
+ try {
110
+ const settingsPath = path.join(home, '.claude', 'settings.json');
111
+ if (fs.existsSync(settingsPath)) {
112
+ const data = fs.readFileSync(settingsPath, 'utf8');
113
+ if (data && data.trim()) {
114
+ const settings = JSON.parse(data);
115
+ if (settings.env) {
116
+ const routingChanged = clearPlexorRoutingEnv(settings.env);
117
+
118
+ // Clean up empty env block
119
+ if (Object.keys(settings.env).length === 0) {
120
+ delete settings.env;
121
+ }
122
+
123
+ if (routingChanged) {
124
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
125
+ results.routing = true;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ } catch (e) {
131
+ console.log(` Warning: Could not clean settings.json: ${e.message}`);
132
+ }
133
+
134
+ // 2. Remove slash command files
135
+ // These are the Plexor-specific command files that get installed to ~/.claude/commands/
136
+ const plexorCommands = [
137
+ 'plexor-config.md',
138
+ 'plexor-enabled.md',
139
+ 'plexor-login.md',
140
+ 'plexor-logout.md',
141
+ 'plexor-mode.md',
142
+ 'plexor-provider.md',
143
+ 'plexor-settings.md',
144
+ 'plexor-setup.md',
145
+ 'plexor-status.md',
146
+ 'plexor-uninstall.md'
147
+ ];
148
+
149
+ try {
150
+ const commandsDir = path.join(home, '.claude', 'commands');
151
+ if (fs.existsSync(commandsDir)) {
152
+ for (const cmd of plexorCommands) {
153
+ const cmdPath = path.join(commandsDir, cmd);
154
+ const backupPath = cmdPath + '.backup';
155
+
156
+ if (fs.existsSync(cmdPath)) {
157
+ fs.unlinkSync(cmdPath);
158
+ results.commands.push(cmd.replace('.md', ''));
33
159
 
34
160
  // Restore backup if it exists
35
161
  if (fs.existsSync(backupPath)) {
36
- fs.renameSync(backupPath, dest);
37
- restored.push(file);
162
+ fs.renameSync(backupPath, cmdPath);
163
+ results.restored.push(cmd);
38
164
  }
39
165
  }
40
166
  }
167
+ }
168
+ } catch (e) {
169
+ console.log(` Warning: Could not clean commands: ${e.message}`);
170
+ }
41
171
 
42
- if (removed.length > 0) {
43
- console.log('');
44
- console.log(' Plexor plugin uninstalled');
45
- console.log('');
46
- console.log(' Removed commands:');
47
- removed.forEach(cmd => console.log(` /${cmd}`));
48
-
49
- if (restored.length > 0) {
50
- console.log('');
51
- console.log(' Restored from backup:');
52
- restored.forEach(f => console.log(` ${f}`));
53
- }
172
+ // 3. Remove plugin directory
173
+ try {
174
+ const pluginDir = path.join(home, '.claude', 'plugins', 'plexor');
175
+ if (fs.existsSync(pluginDir)) {
176
+ fs.rmSync(pluginDir, { recursive: true, force: true });
177
+ results.pluginDir = true;
178
+ }
179
+ } catch (e) {
180
+ console.log(` Warning: Could not remove plugin directory: ${e.message}`);
181
+ }
54
182
 
55
- console.log('');
56
- console.log(' Note: ~/.plexor/ config directory was preserved.');
57
- console.log(' To remove it: rm -rf ~/.plexor');
58
- console.log('');
59
- }
183
+ // Output results
184
+ if (results.routing || results.commands.length > 0 || results.pluginDir) {
185
+ console.log(' Plexor plugin uninstalled');
186
+ console.log('');
187
+
188
+ if (results.routing) {
189
+ console.log(' Removed Plexor routing from Claude settings');
190
+ console.log(' (Claude Code now connects directly to Anthropic)');
191
+ console.log('');
192
+ }
193
+
194
+ if (results.commands.length > 0) {
195
+ console.log(' Removed commands:');
196
+ results.commands.forEach(cmd => console.log(` /${cmd}`));
197
+ console.log('');
198
+ }
60
199
 
61
- } catch (error) {
62
- // Don't fail uninstall on errors - just warn
63
- console.warn(` Warning: Could not fully uninstall: ${error.message}`);
200
+ if (results.restored.length > 0) {
201
+ console.log(' Restored from backup:');
202
+ results.restored.forEach(f => console.log(` ${f}`));
203
+ console.log('');
64
204
  }
205
+
206
+ if (results.pluginDir) {
207
+ console.log(' Removed plugin directory');
208
+ console.log('');
209
+ }
210
+
211
+ console.log(' Note: ~/.plexor/ config directory was preserved.');
212
+ console.log(' To remove it: rm -rf ~/.plexor');
213
+ console.log('');
214
+ } else {
215
+ console.log(' No Plexor components found to clean up.');
216
+ console.log('');
65
217
  }
66
218
 
67
- main();
219
+ console.log(' Cleanup complete');
220
+ console.log('');