@plexor-dev/claude-code-plugin-staging 0.1.0-beta.10 → 0.1.0-beta.11

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.
@@ -94,6 +94,30 @@ function isValidApiKeyFormat(key) {
94
94
  return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
95
95
  }
96
96
 
97
+ /**
98
+ * Check for state mismatch between config.json enabled flag and settings.json routing
99
+ * @param {boolean} configEnabled - enabled flag from config.json
100
+ * @param {boolean} routingActive - whether settings.json has Plexor routing configured
101
+ * @returns {Object|null} mismatch details or null if states are consistent
102
+ */
103
+ function checkStateMismatch(configEnabled, routingActive) {
104
+ if (configEnabled && !routingActive) {
105
+ return {
106
+ type: 'config-enabled-routing-inactive',
107
+ message: 'Config shows enabled but Claude routing is not configured',
108
+ suggestion: 'Run /plexor-enabled true to sync and configure routing'
109
+ };
110
+ }
111
+ if (!configEnabled && routingActive) {
112
+ return {
113
+ type: 'config-disabled-routing-active',
114
+ message: 'Config shows disabled but Claude routing is active',
115
+ suggestion: 'Run /plexor-enabled false to sync and disable routing'
116
+ };
117
+ }
118
+ return null;
119
+ }
120
+
97
121
  function main() {
98
122
  const args = process.argv.slice(2);
99
123
  const config = loadConfig();
@@ -107,6 +131,10 @@ function main() {
107
131
  if (args.length === 0) {
108
132
  const status = currentEnabled ? '● Enabled' : '○ Disabled';
109
133
  const routingStr = routingStatus.enabled ? '● Active' : '○ Inactive';
134
+
135
+ // Check for state mismatch between config enabled flag and routing status
136
+ const stateMismatch = checkStateMismatch(currentEnabled, routingStatus.enabled);
137
+
110
138
  console.log(`┌─────────────────────────────────────────────┐`);
111
139
  console.log(`│ Plexor Proxy Status │`);
112
140
  console.log(`├─────────────────────────────────────────────┤`);
@@ -115,6 +143,12 @@ function main() {
115
143
  if (routingStatus.enabled) {
116
144
  console.log(`│ Endpoint: ${(routingStatus.isStaging ? 'Staging' : 'Production').padEnd(32)}│`);
117
145
  }
146
+ if (stateMismatch) {
147
+ console.log(`├─────────────────────────────────────────────┤`);
148
+ console.log(`│ ⚠ State mismatch detected: │`);
149
+ console.log(`│ ${stateMismatch.message.padEnd(42)}│`);
150
+ console.log(`│ ${stateMismatch.suggestion.padEnd(42)}│`);
151
+ }
118
152
  console.log(`├─────────────────────────────────────────────┤`);
119
153
  console.log(`│ Usage: │`);
120
154
  console.log(`│ /plexor-enabled true - Enable proxy │`);
@@ -161,6 +195,19 @@ function main() {
161
195
  const apiUrl = config.settings?.apiUrl || 'https://staging.api.plexor.dev';
162
196
  const useStaging = apiUrl.includes('staging');
163
197
  routingUpdated = settingsManager.enablePlexorRouting(apiKey, { useStaging });
198
+
199
+ // Check if settings.json update failed
200
+ if (!routingUpdated) {
201
+ console.log(`┌─────────────────────────────────────────────┐`);
202
+ console.log(`│ ✗ Failed to Enable Plexor Routing │`);
203
+ console.log(`├─────────────────────────────────────────────┤`);
204
+ console.log(`│ Could not update ~/.claude/settings.json │`);
205
+ console.log(`│ Config.json was updated but routing failed.│`);
206
+ console.log(`├─────────────────────────────────────────────┤`);
207
+ console.log(`│ Check file permissions and try again. │`);
208
+ console.log(`└─────────────────────────────────────────────┘`);
209
+ process.exit(1);
210
+ }
164
211
  }
165
212
  } else {
166
213
  // Update Plexor plugin config
@@ -172,6 +219,19 @@ function main() {
172
219
 
173
220
  // Disable routing - remove env vars from settings.json
174
221
  routingUpdated = settingsManager.disablePlexorRouting();
222
+
223
+ // Check if settings.json update failed
224
+ if (!routingUpdated) {
225
+ console.log(`┌─────────────────────────────────────────────┐`);
226
+ console.log(`│ ✗ Failed to Disable Plexor Routing │`);
227
+ console.log(`├─────────────────────────────────────────────┤`);
228
+ console.log(`│ Could not update ~/.claude/settings.json │`);
229
+ console.log(`│ Config.json was updated but routing failed.│`);
230
+ console.log(`├─────────────────────────────────────────────┤`);
231
+ console.log(`│ Check file permissions and try again. │`);
232
+ console.log(`└─────────────────────────────────────────────┘`);
233
+ process.exit(1);
234
+ }
175
235
  }
176
236
 
177
237
  // Show error if no API key when enabling
@@ -34,6 +34,31 @@ function getRoutingStatus() {
34
34
  }
35
35
  }
36
36
 
37
+ /**
38
+ * Detect partial routing state where URL points to Plexor but auth is missing/invalid
39
+ * This can cause confusing auth errors for users
40
+ * @returns {Object} { partial: boolean, issue: string|null }
41
+ */
42
+ function detectPartialState() {
43
+ try {
44
+ const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
45
+ const settings = JSON.parse(data);
46
+ 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');
49
+
50
+ if (isPlexorUrl && !authToken) {
51
+ return { partial: true, issue: 'Plexor URL set but no auth token' };
52
+ }
53
+ if (isPlexorUrl && !authToken.startsWith('plx_')) {
54
+ return { partial: true, issue: 'Plexor URL set but auth token is not a Plexor key' };
55
+ }
56
+ return { partial: false, issue: null };
57
+ } catch {
58
+ return { partial: false, issue: null };
59
+ }
60
+ }
61
+
37
62
  function loadSessionStats() {
38
63
  try {
39
64
  const data = fs.readFileSync(SESSION_PATH, 'utf8');
@@ -101,6 +126,30 @@ function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
101
126
  return null;
102
127
  }
103
128
 
129
+ /**
130
+ * Check for state mismatch between config.json enabled flag and settings.json routing
131
+ * @param {boolean} configEnabled - enabled flag from config.json
132
+ * @param {boolean} routingActive - whether settings.json has Plexor routing configured
133
+ * @returns {Object|null} mismatch details or null if states are consistent
134
+ */
135
+ function checkStateMismatch(configEnabled, routingActive) {
136
+ if (configEnabled && !routingActive) {
137
+ return {
138
+ type: 'config-enabled-routing-inactive',
139
+ message: 'Config shows enabled but Claude routing is not configured',
140
+ suggestion: 'Run /plexor-enabled true to sync and configure routing'
141
+ };
142
+ }
143
+ if (!configEnabled && routingActive) {
144
+ return {
145
+ type: 'config-disabled-routing-active',
146
+ message: 'Config shows disabled but Claude routing is active',
147
+ suggestion: 'Run /plexor-enabled false to sync and disable routing'
148
+ };
149
+ }
150
+ return null;
151
+ }
152
+
104
153
  async function main() {
105
154
  // Read config with integrity checking
106
155
  const config = loadConfig();
@@ -231,6 +280,20 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
231
280
  console.log(mismatchWarning);
232
281
  }
233
282
 
283
+ // Check for partial routing state (Plexor URL without valid auth)
284
+ const partialState = detectPartialState();
285
+ if (partialState.partial) {
286
+ console.log(` ⚠ PARTIAL STATE DETECTED: ${partialState.issue}`);
287
+ console.log(` Run /plexor-login to fix, or /plexor-logout to disable routing\n`);
288
+ }
289
+
290
+ // Check for state mismatch between config enabled flag and routing status
291
+ const stateMismatch = checkStateMismatch(enabled, routing.active);
292
+ if (stateMismatch) {
293
+ console.log(` ⚠ State mismatch: ${stateMismatch.message}`);
294
+ console.log(` └─ ${stateMismatch.suggestion}\n`);
295
+ }
296
+
234
297
  console.log(` ┌─────────────────────────────────────────────┐
235
298
  ${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
236
299
  ├─────────────────────────────────────────────┤
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Plexor Uninstall Command (STAGING)
5
+ *
6
+ * Comprehensive cleanup before npm uninstall.
7
+ *
8
+ * CRITICAL: npm's preuninstall hook does NOT run for global package uninstalls.
9
+ * Users MUST run this command BEFORE running npm uninstall -g.
10
+ *
11
+ * This command:
12
+ * 1. Removes Plexor routing from ~/.claude/settings.json
13
+ * 2. Removes slash command .md files from ~/.claude/commands/
14
+ * 3. Removes plugin directory ~/.claude/plugins/plexor/
15
+ * 4. Optionally removes ~/.plexor/ config directory
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+
22
+ // Get home directory, handling sudo case
23
+ function getHomeDir() {
24
+ if (process.env.SUDO_USER) {
25
+ const platform = os.platform();
26
+ if (platform === 'darwin') {
27
+ return path.join('/Users', process.env.SUDO_USER);
28
+ } else if (platform === 'linux') {
29
+ return path.join('/home', process.env.SUDO_USER);
30
+ }
31
+ }
32
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
33
+ }
34
+
35
+ const HOME_DIR = getHomeDir();
36
+ const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
37
+ const CLAUDE_COMMANDS_DIR = path.join(CLAUDE_DIR, 'commands');
38
+ const CLAUDE_PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins', 'plexor');
39
+ const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
40
+
41
+ // All Plexor slash command files
42
+ const PLEXOR_COMMANDS = [
43
+ 'plexor-enabled.md',
44
+ 'plexor-login.md',
45
+ 'plexor-logout.md',
46
+ 'plexor-setup.md',
47
+ 'plexor-status.md',
48
+ 'plexor-uninstall.md',
49
+ 'plexor-mode.md',
50
+ 'plexor-provider.md',
51
+ 'plexor-settings.md',
52
+ 'plexor-config.md'
53
+ ];
54
+
55
+ /**
56
+ * Load settings manager if available
57
+ */
58
+ function loadSettingsManager() {
59
+ // Try plugin dir first (installed location)
60
+ try {
61
+ const pluginLib = path.join(CLAUDE_PLUGINS_DIR, 'lib', 'settings-manager.js');
62
+ if (fs.existsSync(pluginLib)) {
63
+ const lib = require(pluginLib);
64
+ return lib.settingsManager;
65
+ }
66
+ } catch {
67
+ // Continue to fallback
68
+ }
69
+
70
+ // Try package lib (during npm lifecycle)
71
+ try {
72
+ const lib = require('../lib/settings-manager');
73
+ return lib.settingsManager;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Disable Plexor routing in Claude settings manually
81
+ * Fallback when settings-manager is not available
82
+ */
83
+ function disableRoutingManually() {
84
+ const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
85
+
86
+ try {
87
+ if (!fs.existsSync(settingsPath)) {
88
+ return { success: true, message: 'Settings file does not exist' };
89
+ }
90
+
91
+ const data = fs.readFileSync(settingsPath, 'utf8');
92
+ if (!data || data.trim() === '') {
93
+ return { success: true, message: 'Settings file is empty' };
94
+ }
95
+
96
+ const settings = JSON.parse(data);
97
+
98
+ if (!settings.env) {
99
+ return { success: true, message: 'No env block in settings' };
100
+ }
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) {
107
+ return { success: true, message: 'Plexor routing not active' };
108
+ }
109
+
110
+ // Remove Plexor env vars
111
+ delete settings.env.ANTHROPIC_BASE_URL;
112
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
113
+
114
+ // Clean up empty env block
115
+ if (Object.keys(settings.env).length === 0) {
116
+ delete settings.env;
117
+ }
118
+
119
+ // Atomic write
120
+ const crypto = require('crypto');
121
+ const tempId = crypto.randomBytes(8).toString('hex');
122
+ const tempPath = path.join(CLAUDE_DIR, `.settings.${tempId}.tmp`);
123
+
124
+ fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
125
+ fs.renameSync(tempPath, settingsPath);
126
+
127
+ return { success: true, message: 'Routing disabled' };
128
+ } catch (err) {
129
+ return { success: false, message: err.message };
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Remove slash command files
135
+ */
136
+ function removeSlashCommands() {
137
+ let removed = 0;
138
+ let restored = 0;
139
+
140
+ for (const cmd of PLEXOR_COMMANDS) {
141
+ const cmdPath = path.join(CLAUDE_COMMANDS_DIR, cmd);
142
+ const backupPath = cmdPath + '.backup';
143
+
144
+ try {
145
+ if (fs.existsSync(cmdPath)) {
146
+ fs.unlinkSync(cmdPath);
147
+ removed++;
148
+
149
+ // Restore backup if exists
150
+ if (fs.existsSync(backupPath)) {
151
+ fs.renameSync(backupPath, cmdPath);
152
+ restored++;
153
+ }
154
+ }
155
+ } catch {
156
+ // Continue with other files
157
+ }
158
+ }
159
+
160
+ return { removed, restored };
161
+ }
162
+
163
+ /**
164
+ * Remove plugin directory
165
+ */
166
+ function removePluginDirectory() {
167
+ try {
168
+ if (fs.existsSync(CLAUDE_PLUGINS_DIR)) {
169
+ fs.rmSync(CLAUDE_PLUGINS_DIR, { recursive: true, force: true });
170
+ return true;
171
+ }
172
+ } catch {
173
+ return false;
174
+ }
175
+ return false;
176
+ }
177
+
178
+ /**
179
+ * Remove config directory
180
+ */
181
+ function removeConfigDirectory() {
182
+ try {
183
+ if (fs.existsSync(PLEXOR_CONFIG_DIR)) {
184
+ fs.rmSync(PLEXOR_CONFIG_DIR, { recursive: true, force: true });
185
+ return true;
186
+ }
187
+ } catch {
188
+ return false;
189
+ }
190
+ return false;
191
+ }
192
+
193
+ function main() {
194
+ const args = process.argv.slice(2);
195
+ const removeConfig = args.includes('--remove-config') || args.includes('-c');
196
+ const quiet = args.includes('--quiet') || args.includes('-q');
197
+
198
+ if (!quiet) {
199
+ console.log('');
200
+ console.log(' Plexor Uninstall (STAGING) - Cleaning up...');
201
+ console.log('');
202
+ }
203
+
204
+ // 1. Remove routing from ~/.claude/settings.json
205
+ let routingResult;
206
+ const settingsManager = loadSettingsManager();
207
+
208
+ if (settingsManager) {
209
+ try {
210
+ const success = settingsManager.disablePlexorRouting();
211
+ routingResult = { success, message: success ? 'Routing disabled via manager' : 'Already clean' };
212
+ } catch (err) {
213
+ routingResult = disableRoutingManually();
214
+ }
215
+ } else {
216
+ routingResult = disableRoutingManually();
217
+ }
218
+
219
+ if (!quiet) {
220
+ console.log(routingResult.success
221
+ ? ' ✓ Removed Plexor routing from Claude settings'
222
+ : ` ✗ Failed to remove routing: ${routingResult.message}`);
223
+ }
224
+
225
+ // 2. Remove slash command .md files
226
+ const cmdResult = removeSlashCommands();
227
+ if (!quiet) {
228
+ console.log(` ✓ Removed ${cmdResult.removed} slash command files`);
229
+ if (cmdResult.restored > 0) {
230
+ console.log(` ✓ Restored ${cmdResult.restored} backed-up files`);
231
+ }
232
+ }
233
+
234
+ // 3. Remove plugin directory
235
+ const pluginRemoved = removePluginDirectory();
236
+ if (!quiet) {
237
+ console.log(pluginRemoved
238
+ ? ' ✓ Removed plugin directory'
239
+ : ' ○ Plugin directory not found (already clean)');
240
+ }
241
+
242
+ // 4. Optionally remove config directory
243
+ if (removeConfig) {
244
+ const configRemoved = removeConfigDirectory();
245
+ if (!quiet) {
246
+ console.log(configRemoved
247
+ ? ' ✓ Removed ~/.plexor config directory'
248
+ : ' ○ Config directory not found');
249
+ }
250
+ }
251
+
252
+ // Show next steps
253
+ if (!quiet) {
254
+ console.log('');
255
+ console.log(' ┌─────────────────────────────────────────────────────────────────┐');
256
+ console.log(' │ Cleanup complete! Now run: │');
257
+ console.log(' │ │');
258
+ console.log(' │ npm uninstall -g @plexor-dev/claude-code-plugin-staging │');
259
+ console.log(' │ │');
260
+ console.log(' └─────────────────────────────────────────────────────────────────┘');
261
+ console.log('');
262
+ console.log(' Your Claude Code is ready to work normally again.');
263
+ console.log('');
264
+
265
+ if (!removeConfig) {
266
+ console.log(' Note: ~/.plexor/ config directory was preserved.');
267
+ console.log(' To also remove it: plexor-uninstall --remove-config');
268
+ console.log(' Or manually: rm -rf ~/.plexor');
269
+ console.log('');
270
+ }
271
+ }
272
+ }
273
+
274
+ main();
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: plexor-uninstall
3
+ description: Clean up Plexor integration before uninstalling
4
+ ---
5
+
6
+ Runs comprehensive cleanup to prepare for npm uninstall.
7
+
8
+ **IMPORTANT**: Run this command BEFORE `npm uninstall -g @plexor-dev/claude-code-plugin-staging`
9
+
10
+ npm's preuninstall hook does NOT run for global package uninstalls, so this command
11
+ must be run manually to ensure proper cleanup.
12
+
13
+ This removes:
14
+ - Plexor routing from Claude settings (~/.claude/settings.json)
15
+ - Slash command files from ~/.claude/commands/
16
+ - Plugin directory ~/.claude/plugins/plexor/
17
+
18
+ Options:
19
+ - `--remove-config` or `-c`: Also remove ~/.plexor/ config directory
20
+
21
+ After running this command, run:
22
+ ```bash
23
+ npm uninstall -g @plexor-dev/claude-code-plugin-staging
24
+ ```
25
+
26
+ $ARGUMENTS: $ARGUMENTS
27
+
28
+ ```bash
29
+ node ~/.claude/plugins/plexor/commands/plexor-uninstall.js $ARGUMENTS 2>&1 || plexor-uninstall $ARGUMENTS 2>&1
30
+ ```
@@ -106,9 +106,9 @@ try {
106
106
  const saveSession = (s) => {
107
107
  try {
108
108
  const dir = path.dirname(SESSION_PATH);
109
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
109
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
110
110
  s.last_activity = Date.now();
111
- fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2));
111
+ fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2), { mode: 0o600 });
112
112
  } catch {}
113
113
  };
114
114
 
@@ -85,10 +85,10 @@ try {
85
85
  save(session) {
86
86
  try {
87
87
  if (!fs.existsSync(PLEXOR_DIR)) {
88
- fs.mkdirSync(PLEXOR_DIR, { recursive: true });
88
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
89
89
  }
90
90
  session.last_activity = Date.now();
91
- fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2));
91
+ fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2), { mode: 0o600 });
92
92
  } catch {}
93
93
  }
94
94
 
package/lib/config.js CHANGED
@@ -56,7 +56,7 @@ class ConfigManager {
56
56
  }
57
57
  };
58
58
 
59
- fs.writeFileSync(this.configPath, JSON.stringify(updated, null, 2));
59
+ fs.writeFileSync(this.configPath, JSON.stringify(updated, null, 2), { mode: 0o600 });
60
60
  return true;
61
61
  } catch {
62
62
  return false;
@@ -49,8 +49,14 @@ class ClaudeSettingsManager {
49
49
 
50
50
  // Basic schema validation - must be an object
51
51
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
52
- console.warn('Warning: Claude settings file has invalid format, using defaults');
53
- this._backupCorruptedFile();
52
+ const backupPath = this._backupCorruptedFile();
53
+ console.warn('');
54
+ console.warn('WARNING: Claude settings file has invalid format!');
55
+ if (backupPath) {
56
+ console.warn(` Corrupted file backed up to: ${backupPath}`);
57
+ }
58
+ console.warn(' Using default settings. Your previous settings may need manual recovery.');
59
+ console.warn('');
54
60
  return {};
55
61
  }
56
62
 
@@ -61,9 +67,14 @@ class ClaudeSettingsManager {
61
67
  }
62
68
  // JSON parse error or corrupted file
63
69
  if (err instanceof SyntaxError) {
64
- console.warn('Warning: Claude settings file is corrupted, using defaults');
65
- console.warn(' A backup has been saved to settings.json.corrupted');
66
- this._backupCorruptedFile();
70
+ const backupPath = this._backupCorruptedFile();
71
+ console.warn('');
72
+ console.warn('WARNING: Claude settings file is corrupted (invalid JSON)!');
73
+ if (backupPath) {
74
+ console.warn(` Corrupted file backed up to: ${backupPath}`);
75
+ }
76
+ console.warn(' Using default settings. Your previous settings may need manual recovery.');
77
+ console.warn('');
67
78
  return {};
68
79
  }
69
80
  // Permission error
@@ -77,17 +88,30 @@ class ClaudeSettingsManager {
77
88
  }
78
89
 
79
90
  /**
80
- * Backup a corrupted settings file for debugging
91
+ * Backup a corrupted settings file with numbered suffix for debugging
92
+ * Creates settings.json.corrupted.1, .corrupted.2, etc. to preserve history
93
+ * @returns {string|null} path to backup file, or null if backup failed
81
94
  */
82
95
  _backupCorruptedFile() {
83
96
  try {
84
97
  if (fs.existsSync(this.settingsPath)) {
85
- const backupPath = this.settingsPath + '.corrupted';
98
+ // Find next available numbered backup
99
+ let backupNum = 1;
100
+ let backupPath;
101
+ while (true) {
102
+ backupPath = `${this.settingsPath}.corrupted.${backupNum}`;
103
+ if (!fs.existsSync(backupPath)) {
104
+ break;
105
+ }
106
+ backupNum++;
107
+ }
86
108
  fs.copyFileSync(this.settingsPath, backupPath);
109
+ return backupPath;
87
110
  }
88
111
  } catch {
89
- // Ignore backup errors
112
+ // Ignore backup errors silently
90
113
  }
114
+ return null;
91
115
  }
92
116
 
93
117
  /**
@@ -242,6 +266,30 @@ class ClaudeSettingsManager {
242
266
  }
243
267
  }
244
268
 
269
+ /**
270
+ * Detect partial routing state where URL points to Plexor but auth is missing/invalid
271
+ * This can cause confusing auth errors for users
272
+ * @returns {Object} { partial: boolean, issue: string|null }
273
+ */
274
+ detectPartialState() {
275
+ try {
276
+ const settings = this.load();
277
+ const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
278
+ const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
279
+ const isPlexorUrl = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
280
+
281
+ if (isPlexorUrl && !authToken) {
282
+ return { partial: true, issue: 'Plexor URL set but no auth token' };
283
+ }
284
+ if (isPlexorUrl && !authToken.startsWith('plx_')) {
285
+ return { partial: true, issue: 'Plexor URL set but auth token is not a Plexor key' };
286
+ }
287
+ return { partial: false, issue: null };
288
+ } catch {
289
+ return { partial: false, issue: null };
290
+ }
291
+ }
292
+
245
293
  /**
246
294
  * Update just the API key without changing other settings
247
295
  * @param {string} apiKey - new Plexor API key
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin-staging",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.11",
4
4
  "description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
5
5
  "main": "lib/constants.js",
6
6
  "bin": {
7
7
  "plexor-status": "./commands/plexor-status.js",
8
8
  "plexor-enabled": "./commands/plexor-enabled.js",
9
9
  "plexor-login": "./commands/plexor-login.js",
10
- "plexor-logout": "./commands/plexor-logout.js"
10
+ "plexor-logout": "./commands/plexor-logout.js",
11
+ "plexor-uninstall": "./commands/plexor-uninstall.js"
11
12
  },
12
13
  "scripts": {
13
14
  "postinstall": "node scripts/postinstall.js",
@@ -77,6 +77,51 @@ const PLEXOR_LIB_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'lib'
77
77
  const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
78
78
  const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
79
79
 
80
+ /**
81
+ * Check for orphaned Plexor routing in settings.json without valid config.
82
+ * This can happen if a previous uninstall was incomplete.
83
+ */
84
+ function checkOrphanedRouting() {
85
+ const home = process.env.HOME || process.env.USERPROFILE;
86
+ if (!home) return;
87
+
88
+ const settingsPath = path.join(home, '.claude', 'settings.json');
89
+ const configPath = path.join(home, '.plexor', 'config.json');
90
+
91
+ try {
92
+ if (!fs.existsSync(settingsPath)) return;
93
+
94
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
95
+ const env = settings.env || {};
96
+
97
+ const hasPlexorUrl = env.ANTHROPIC_BASE_URL &&
98
+ env.ANTHROPIC_BASE_URL.includes('plexor');
99
+
100
+ if (hasPlexorUrl) {
101
+ // Check if there's a valid Plexor config
102
+ let hasValidConfig = false;
103
+ try {
104
+ if (fs.existsSync(configPath)) {
105
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
106
+ hasValidConfig = config.apiKey && config.apiKey.startsWith('plx_');
107
+ }
108
+ } catch (e) {}
109
+
110
+ if (!hasValidConfig) {
111
+ console.log('\n Warning: Detected orphaned Plexor routing in Claude settings');
112
+ console.log(' This may be from a previous installation.\n');
113
+ console.log(' Run /plexor-login to reconfigure, or');
114
+ console.log(' Run /plexor-uninstall to clean up\n');
115
+ } else {
116
+ console.log('\n Existing Plexor configuration detected');
117
+ console.log(' Your previous settings have been preserved.\n');
118
+ }
119
+ }
120
+ } catch (e) {
121
+ // Ignore errors in detection - don't break install
122
+ }
123
+ }
124
+
80
125
  // Default configuration for new installs
81
126
  // STAGING PACKAGE - uses staging API
82
127
  const DEFAULT_CONFIG = {
@@ -94,6 +139,9 @@ const DEFAULT_CONFIG = {
94
139
  };
95
140
 
96
141
  function main() {
142
+ // Check for orphaned routing at start of postinstall
143
+ checkOrphanedRouting();
144
+
97
145
  try {
98
146
  // Get target user info for chown (if running with sudo)
99
147
  const targetUser = getTargetUserIds();
@@ -1,97 +1,154 @@
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
- const os = require('os');
13
18
 
14
- // Import settings manager for Claude Code routing cleanup
15
- let settingsManager;
16
- try {
17
- const lib = require('../lib/settings-manager');
18
- settingsManager = lib.settingsManager;
19
- } catch (err) {
20
- // If settings manager can't be loaded during uninstall, continue anyway
21
- settingsManager = null;
19
+ // Get home directory - support both Unix and Windows
20
+ const home = process.env.HOME || process.env.USERPROFILE;
21
+ if (!home) {
22
+ console.log('Warning: HOME not set, skipping cleanup');
23
+ process.exit(0);
22
24
  }
23
25
 
24
- const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
25
- const CLAUDE_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
26
-
27
- function main() {
28
- try {
29
- // CRITICAL: Disable Claude Code routing before removing commands
30
- // This ensures users don't get stuck with Plexor routing after uninstall
31
- let routingDisabled = false;
32
- if (settingsManager) {
33
- try {
34
- routingDisabled = settingsManager.disablePlexorRouting();
35
- } catch (e) {
36
- // Continue with uninstall even if routing cleanup fails
26
+ console.log('');
27
+ console.log(' Plexor plugin cleanup...');
28
+ console.log('');
29
+
30
+ const results = {
31
+ routing: false,
32
+ commands: [],
33
+ restored: [],
34
+ pluginDir: false
35
+ };
36
+
37
+ // 1. Remove routing from settings.json
38
+ // This is CRITICAL - do NOT depend on settings-manager module since it may not load during uninstall
39
+ try {
40
+ const settingsPath = path.join(home, '.claude', 'settings.json');
41
+ if (fs.existsSync(settingsPath)) {
42
+ const data = fs.readFileSync(settingsPath, 'utf8');
43
+ if (data && data.trim()) {
44
+ const settings = JSON.parse(data);
45
+ if (settings.env) {
46
+ const hadBaseUrl = !!settings.env.ANTHROPIC_BASE_URL;
47
+ const hadAuthToken = !!settings.env.ANTHROPIC_AUTH_TOKEN;
48
+
49
+ delete settings.env.ANTHROPIC_BASE_URL;
50
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
51
+
52
+ // Clean up empty env block
53
+ if (Object.keys(settings.env).length === 0) {
54
+ delete settings.env;
55
+ }
56
+
57
+ if (hadBaseUrl || hadAuthToken) {
58
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
59
+ results.routing = true;
60
+ }
37
61
  }
38
62
  }
63
+ }
64
+ } catch (e) {
65
+ console.log(` Warning: Could not clean settings.json: ${e.message}`);
66
+ }
39
67
 
40
- // Get list of our command files
41
- const files = fs.readdirSync(COMMANDS_SOURCE)
42
- .filter(f => f.endsWith('.md'));
43
-
44
- const removed = [];
45
- const restored = [];
68
+ // 2. Remove slash command files
69
+ // These are the Plexor-specific command files that get installed to ~/.claude/commands/
70
+ const plexorCommands = [
71
+ 'plexor-config.md',
72
+ 'plexor-enabled.md',
73
+ 'plexor-login.md',
74
+ 'plexor-logout.md',
75
+ 'plexor-mode.md',
76
+ 'plexor-provider.md',
77
+ 'plexor-settings.md',
78
+ 'plexor-setup.md',
79
+ 'plexor-status.md',
80
+ 'plexor-uninstall.md'
81
+ ];
46
82
 
47
- for (const file of files) {
48
- const dest = path.join(CLAUDE_COMMANDS_DIR, file);
49
- const backupPath = dest + '.backup';
83
+ try {
84
+ const commandsDir = path.join(home, '.claude', 'commands');
85
+ if (fs.existsSync(commandsDir)) {
86
+ for (const cmd of plexorCommands) {
87
+ const cmdPath = path.join(commandsDir, cmd);
88
+ const backupPath = cmdPath + '.backup';
50
89
 
51
- if (fs.existsSync(dest)) {
52
- fs.unlinkSync(dest);
53
- removed.push(file.replace('.md', ''));
90
+ if (fs.existsSync(cmdPath)) {
91
+ fs.unlinkSync(cmdPath);
92
+ results.commands.push(cmd.replace('.md', ''));
54
93
 
55
94
  // Restore backup if it exists
56
95
  if (fs.existsSync(backupPath)) {
57
- fs.renameSync(backupPath, dest);
58
- restored.push(file);
96
+ fs.renameSync(backupPath, cmdPath);
97
+ results.restored.push(cmd);
59
98
  }
60
99
  }
61
100
  }
101
+ }
102
+ } catch (e) {
103
+ console.log(` Warning: Could not clean commands: ${e.message}`);
104
+ }
62
105
 
63
- if (removed.length > 0 || routingDisabled) {
64
- console.log('');
65
- console.log(' Plexor plugin uninstalled');
66
- console.log('');
106
+ // 3. Remove plugin directory
107
+ try {
108
+ const pluginDir = path.join(home, '.claude', 'plugins', 'plexor');
109
+ if (fs.existsSync(pluginDir)) {
110
+ fs.rmSync(pluginDir, { recursive: true, force: true });
111
+ results.pluginDir = true;
112
+ }
113
+ } catch (e) {
114
+ console.log(` Warning: Could not remove plugin directory: ${e.message}`);
115
+ }
67
116
 
68
- if (routingDisabled) {
69
- console.log(' ✓ Claude Code routing disabled');
70
- console.log(' (Claude Code now connects directly to Anthropic)');
71
- console.log('');
72
- }
117
+ // Output results
118
+ if (results.routing || results.commands.length > 0 || results.pluginDir) {
119
+ console.log(' Plexor plugin uninstalled');
120
+ console.log('');
73
121
 
74
- if (removed.length > 0) {
75
- console.log(' Removed commands:');
76
- removed.forEach(cmd => console.log(` /${cmd}`));
77
- }
122
+ if (results.routing) {
123
+ console.log(' Removed Plexor routing from Claude settings');
124
+ console.log(' (Claude Code now connects directly to Anthropic)');
125
+ console.log('');
126
+ }
78
127
 
79
- if (restored.length > 0) {
80
- console.log('');
81
- console.log(' Restored from backup:');
82
- restored.forEach(f => console.log(` ${f}`));
83
- }
128
+ if (results.commands.length > 0) {
129
+ console.log(' Removed commands:');
130
+ results.commands.forEach(cmd => console.log(` /${cmd}`));
131
+ console.log('');
132
+ }
84
133
 
85
- console.log('');
86
- console.log(' Note: ~/.plexor/ config directory was preserved.');
87
- console.log(' To remove it: rm -rf ~/.plexor');
88
- console.log('');
89
- }
134
+ if (results.restored.length > 0) {
135
+ console.log(' Restored from backup:');
136
+ results.restored.forEach(f => console.log(` ${f}`));
137
+ console.log('');
138
+ }
90
139
 
91
- } catch (error) {
92
- // Don't fail uninstall on errors - just warn
93
- console.warn(` Warning: Could not fully uninstall: ${error.message}`);
140
+ if (results.pluginDir) {
141
+ console.log(' Removed plugin directory');
142
+ console.log('');
94
143
  }
144
+
145
+ console.log(' Note: ~/.plexor/ config directory was preserved.');
146
+ console.log(' To remove it: rm -rf ~/.plexor');
147
+ console.log('');
148
+ } else {
149
+ console.log(' No Plexor components found to clean up.');
150
+ console.log('');
95
151
  }
96
152
 
97
- main();
153
+ console.log(' Cleanup complete');
154
+ console.log('');