@plexor-dev/claude-code-plugin-staging 0.1.0-beta.1 → 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.
@@ -12,9 +12,12 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
+ const os = require('os');
16
+ const crypto = require('crypto');
15
17
 
16
18
  const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
17
19
  const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
20
+ const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
18
21
 
19
22
  // Plexor gateway endpoints
20
23
  const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
@@ -27,8 +30,8 @@ class ClaudeSettingsManager {
27
30
  }
28
31
 
29
32
  /**
30
- * Load current Claude settings
31
- * @returns {Object} settings object or empty object if not found
33
+ * Load current Claude settings with integrity checking
34
+ * @returns {Object} settings object or empty object if not found/corrupted
32
35
  */
33
36
  load() {
34
37
  try {
@@ -36,15 +39,84 @@ class ClaudeSettingsManager {
36
39
  return {};
37
40
  }
38
41
  const data = fs.readFileSync(this.settingsPath, 'utf8');
39
- return JSON.parse(data);
42
+
43
+ // Check for empty file
44
+ if (!data || data.trim() === '') {
45
+ return {};
46
+ }
47
+
48
+ const parsed = JSON.parse(data);
49
+
50
+ // Basic schema validation - must be an object
51
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
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('');
60
+ return {};
61
+ }
62
+
63
+ return parsed;
40
64
  } catch (err) {
41
- // Return empty object if file doesn't exist or parse error
65
+ if (err.code === 'ENOENT') {
66
+ return {};
67
+ }
68
+ // JSON parse error or corrupted file
69
+ if (err instanceof SyntaxError) {
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('');
78
+ return {};
79
+ }
80
+ // Permission error
81
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
82
+ console.warn(`Warning: Cannot read ${this.settingsPath} (permission denied)`);
83
+ return {};
84
+ }
85
+ console.warn('Warning: Failed to load Claude settings:', err.message);
42
86
  return {};
43
87
  }
44
88
  }
45
89
 
46
90
  /**
47
- * Save Claude settings
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
94
+ */
95
+ _backupCorruptedFile() {
96
+ try {
97
+ if (fs.existsSync(this.settingsPath)) {
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
+ }
108
+ fs.copyFileSync(this.settingsPath, backupPath);
109
+ return backupPath;
110
+ }
111
+ } catch {
112
+ // Ignore backup errors silently
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Save Claude settings using atomic write pattern
119
+ * Prevents race conditions by writing to temp file then renaming
48
120
  * @param {Object} settings - settings object to save
49
121
  * @returns {boolean} success status
50
122
  */
@@ -55,10 +127,27 @@ class ClaudeSettingsManager {
55
127
  fs.mkdirSync(this.claudeDir, { recursive: true });
56
128
  }
57
129
 
58
- fs.writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2));
130
+ // Atomic write: write to temp file, then rename
131
+ // This prevents race conditions where concurrent writes corrupt the file
132
+ const tempId = crypto.randomBytes(8).toString('hex');
133
+ const tempPath = path.join(this.claudeDir, `.settings.${tempId}.tmp`);
134
+
135
+ // Write to temp file
136
+ const content = JSON.stringify(settings, null, 2);
137
+ fs.writeFileSync(tempPath, content, { mode: 0o600 });
138
+
139
+ // Atomic rename (on POSIX systems, rename is atomic)
140
+ fs.renameSync(tempPath, this.settingsPath);
141
+
59
142
  return true;
60
143
  } catch (err) {
61
- console.error('Failed to save Claude settings:', err.message);
144
+ // Clean error message for permission errors
145
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
146
+ console.error(`Error: Cannot write to ${this.settingsPath}`);
147
+ console.error(' Check file permissions or run with appropriate access.');
148
+ } else {
149
+ console.error('Failed to save Claude settings:', err.message);
150
+ }
62
151
  return false;
63
152
  }
64
153
  }
@@ -177,6 +266,30 @@ class ClaudeSettingsManager {
177
266
  }
178
267
  }
179
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
+
180
293
  /**
181
294
  * Update just the API key without changing other settings
182
295
  * @param {string} apiKey - new Plexor API key
package/package.json CHANGED
@@ -1,17 +1,14 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin-staging",
3
- "version": "0.1.0-beta.1",
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
- "plexor-mode": "./commands/plexor-mode.js",
9
8
  "plexor-enabled": "./commands/plexor-enabled.js",
10
- "plexor-provider": "./commands/plexor-provider.js",
11
9
  "plexor-login": "./commands/plexor-login.js",
12
10
  "plexor-logout": "./commands/plexor-logout.js",
13
- "plexor-settings": "./commands/plexor-settings.js",
14
- "plexor-config": "./commands/plexor-config.js"
11
+ "plexor-uninstall": "./commands/plexor-uninstall.js"
15
12
  },
16
13
  "scripts": {
17
14
  "postinstall": "node scripts/postinstall.js",
@@ -70,12 +70,60 @@ function chownRecursive(dirPath, uid, gid) {
70
70
 
71
71
  const HOME_DIR = getHomeDir();
72
72
  const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
73
+ const LIB_SOURCE = path.join(__dirname, '..', 'lib');
73
74
  const CLAUDE_COMMANDS_DIR = path.join(HOME_DIR, '.claude', 'commands');
74
75
  const PLEXOR_PLUGINS_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'commands');
76
+ const PLEXOR_LIB_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'lib');
75
77
  const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
76
78
  const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
77
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
+
78
125
  // Default configuration for new installs
126
+ // STAGING PACKAGE - uses staging API
79
127
  const DEFAULT_CONFIG = {
80
128
  version: 1,
81
129
  auth: {
@@ -84,13 +132,16 @@ const DEFAULT_CONFIG = {
84
132
  },
85
133
  settings: {
86
134
  enabled: true,
87
- apiUrl: "https://api.plexor.dev",
135
+ apiUrl: "https://staging.api.plexor.dev",
88
136
  mode: "balanced",
89
137
  localCacheEnabled: true
90
138
  }
91
139
  };
92
140
 
93
141
  function main() {
142
+ // Check for orphaned routing at start of postinstall
143
+ checkOrphanedRouting();
144
+
94
145
  try {
95
146
  // Get target user info for chown (if running with sudo)
96
147
  const targetUser = getTargetUserIds();
@@ -101,6 +152,9 @@ function main() {
101
152
  // Create ~/.claude/plugins/plexor/commands/ for JS executors
102
153
  fs.mkdirSync(PLEXOR_PLUGINS_DIR, { recursive: true });
103
154
 
155
+ // Create ~/.claude/plugins/plexor/lib/ for shared modules
156
+ fs.mkdirSync(PLEXOR_LIB_DIR, { recursive: true });
157
+
104
158
  // Create ~/.plexor/ with secure permissions (owner only)
105
159
  fs.mkdirSync(PLEXOR_CONFIG_DIR, { recursive: true, mode: 0o700 });
106
160
 
@@ -161,6 +215,40 @@ function main() {
161
215
  jsInstalled.push(file);
162
216
  }
163
217
 
218
+ // Copy lib files to ~/.claude/plugins/plexor/lib/
219
+ // CRITICAL: These are required for commands to work
220
+ const libInstalled = [];
221
+ if (fs.existsSync(LIB_SOURCE)) {
222
+ const libFiles = fs.readdirSync(LIB_SOURCE).filter(f => f.endsWith('.js'));
223
+ if (libFiles.length === 0) {
224
+ console.warn(' ⚠ Warning: No lib files found in package. Commands may not work.');
225
+ }
226
+ for (const file of libFiles) {
227
+ try {
228
+ const src = path.join(LIB_SOURCE, file);
229
+ const dest = path.join(PLEXOR_LIB_DIR, file);
230
+ fs.copyFileSync(src, dest);
231
+ libInstalled.push(file);
232
+ } catch (err) {
233
+ console.error(` ✗ Failed to copy lib/${file}: ${err.message}`);
234
+ }
235
+ }
236
+ } else {
237
+ console.error(' ✗ CRITICAL: lib/ directory not found in package.');
238
+ console.error(' Commands will fail. Please reinstall the package.');
239
+ console.error(` Expected location: ${LIB_SOURCE}`);
240
+ }
241
+
242
+ // Verify critical lib file exists
243
+ const criticalLibFile = path.join(PLEXOR_LIB_DIR, 'settings-manager.js');
244
+ if (!fs.existsSync(criticalLibFile)) {
245
+ console.error('');
246
+ console.error(' ✗ CRITICAL: settings-manager.js was not installed.');
247
+ console.error(' This file is required for commands to work.');
248
+ console.error(' Try reinstalling: npm install @plexor-dev/claude-code-plugin-staging');
249
+ console.error('');
250
+ }
251
+
164
252
  // Fix file ownership when running with sudo
165
253
  // Files are created as root but should be owned by the original user
166
254
  if (targetUser) {
@@ -190,7 +278,10 @@ function main() {
190
278
  }
191
279
  console.log(` ✓ Installed ${installed.length} slash commands to ~/.claude/commands/`);
192
280
  if (jsInstalled.length > 0) {
193
- console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/`);
281
+ console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/commands/`);
282
+ }
283
+ if (libInstalled.length > 0) {
284
+ console.log(` ✓ Installed ${libInstalled.length} lib modules to ~/.claude/plugins/plexor/lib/`);
194
285
  }
195
286
  if (targetUser) {
196
287
  console.log(` ✓ Set file ownership to ${targetUser.user}`);
@@ -218,10 +309,10 @@ function main() {
218
309
  console.log(' └─────────────────────────────────────────────────────────────────┘');
219
310
  console.log('');
220
311
  console.log(' Available commands:');
221
- console.log(' /plexor-status - Check connection and see savings');
222
- console.log(' /plexor-mode - Switch modes (eco/balanced/quality)');
312
+ console.log(' /plexor-setup - First-time setup wizard');
223
313
  console.log(' /plexor-login - Authenticate with API key');
224
- console.log(' /plexor-settings - View/modify settings');
314
+ console.log(' /plexor-status - Check connection and see savings');
315
+ console.log(' /plexor-enabled - Enable/disable Plexor routing');
225
316
  console.log('');
226
317
  console.log(' Documentation: https://plexor.dev/docs');
227
318
  console.log('');
@@ -1,67 +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
- const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
15
- const CLAUDE_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
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);
24
+ }
25
+
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
+ };
16
36
 
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'));
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
+ }
61
+ }
62
+ }
63
+ }
64
+ } catch (e) {
65
+ console.log(` Warning: Could not clean settings.json: ${e.message}`);
66
+ }
22
67
 
23
- const removed = [];
24
- 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
+ ];
25
82
 
26
- for (const file of files) {
27
- const dest = path.join(CLAUDE_COMMANDS_DIR, file);
28
- 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';
29
89
 
30
- if (fs.existsSync(dest)) {
31
- fs.unlinkSync(dest);
32
- removed.push(file.replace('.md', ''));
90
+ if (fs.existsSync(cmdPath)) {
91
+ fs.unlinkSync(cmdPath);
92
+ results.commands.push(cmd.replace('.md', ''));
33
93
 
34
94
  // Restore backup if it exists
35
95
  if (fs.existsSync(backupPath)) {
36
- fs.renameSync(backupPath, dest);
37
- restored.push(file);
96
+ fs.renameSync(backupPath, cmdPath);
97
+ results.restored.push(cmd);
38
98
  }
39
99
  }
40
100
  }
101
+ }
102
+ } catch (e) {
103
+ console.log(` Warning: Could not clean commands: ${e.message}`);
104
+ }
41
105
 
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
- }
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
+ }
54
116
 
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
- }
117
+ // Output results
118
+ if (results.routing || results.commands.length > 0 || results.pluginDir) {
119
+ console.log(' Plexor plugin uninstalled');
120
+ console.log('');
60
121
 
61
- } catch (error) {
62
- // Don't fail uninstall on errors - just warn
63
- console.warn(` Warning: Could not fully uninstall: ${error.message}`);
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('');
64
126
  }
127
+
128
+ if (results.commands.length > 0) {
129
+ console.log(' Removed commands:');
130
+ results.commands.forEach(cmd => console.log(` /${cmd}`));
131
+ console.log('');
132
+ }
133
+
134
+ if (results.restored.length > 0) {
135
+ console.log(' Restored from backup:');
136
+ results.restored.forEach(f => console.log(` ${f}`));
137
+ console.log('');
138
+ }
139
+
140
+ if (results.pluginDir) {
141
+ console.log(' Removed plugin directory');
142
+ console.log('');
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('');
65
151
  }
66
152
 
67
- main();
153
+ console.log(' Cleanup complete');
154
+ console.log('');