@plexor-dev/claude-code-plugin 0.1.0-beta.30 → 0.1.0-beta.32

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.
@@ -14,26 +14,84 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
 
17
- // Import settings manager for automatic Claude Code configuration
18
- const { settingsManager, PLEXOR_STAGING_URL, PLEXOR_PROD_URL } = require('../lib/settings-manager');
19
-
20
- const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
21
- const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
17
+ // Import centralized constants with HOME directory validation
18
+ const { PLEXOR_DIR, CONFIG_PATH } = require('../lib/constants');
19
+
20
+ // Import settings manager with error handling for missing lib
21
+ let settingsManager, PLEXOR_STAGING_URL, PLEXOR_PROD_URL;
22
+ try {
23
+ const lib = require('../lib/settings-manager');
24
+ settingsManager = lib.settingsManager;
25
+ PLEXOR_STAGING_URL = lib.PLEXOR_STAGING_URL;
26
+ PLEXOR_PROD_URL = lib.PLEXOR_PROD_URL;
27
+ } catch (err) {
28
+ if (err.code === 'MODULE_NOT_FOUND') {
29
+ console.error('Error: Plexor plugin files are missing or corrupted.');
30
+ console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin');
31
+ process.exit(1);
32
+ }
33
+ throw err;
34
+ }
22
35
 
23
36
  function loadConfig() {
24
37
  try {
38
+ if (!fs.existsSync(CONFIG_PATH)) {
39
+ return { version: 1, auth: {}, settings: {} };
40
+ }
25
41
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
26
- return JSON.parse(data);
27
- } catch {
42
+ if (!data || data.trim() === '') {
43
+ return { version: 1, auth: {}, settings: {} };
44
+ }
45
+ const config = JSON.parse(data);
46
+ if (typeof config !== 'object' || config === null) {
47
+ console.warn('Warning: Config file has invalid format, using defaults');
48
+ return { version: 1, auth: {}, settings: {} };
49
+ }
50
+ return config;
51
+ } catch (err) {
52
+ if (err instanceof SyntaxError) {
53
+ console.warn('Warning: Config file is corrupted, using defaults');
54
+ // Backup corrupted file
55
+ try {
56
+ fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.corrupted');
57
+ } catch { /* ignore */ }
58
+ }
28
59
  return { version: 1, auth: {}, settings: {} };
29
60
  }
30
61
  }
31
62
 
32
63
  function saveConfig(config) {
33
- if (!fs.existsSync(PLEXOR_DIR)) {
34
- fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
64
+ try {
65
+ if (!fs.existsSync(PLEXOR_DIR)) {
66
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
67
+ }
68
+
69
+ // Atomic write: write to temp file, then rename
70
+ const crypto = require('crypto');
71
+ const tempId = crypto.randomBytes(8).toString('hex');
72
+ const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
73
+
74
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
75
+ fs.renameSync(tempPath, CONFIG_PATH);
76
+ return true;
77
+ } catch (err) {
78
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
79
+ console.error(`Error: Cannot write to ~/.plexor/config.json`);
80
+ console.error(' Check file permissions or run with appropriate access.');
81
+ } else {
82
+ console.error('Failed to save config:', err.message);
83
+ }
84
+ return false;
35
85
  }
36
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
86
+ }
87
+
88
+ /**
89
+ * Validate API key format
90
+ * @param {string} key - API key to validate
91
+ * @returns {boolean} true if valid format
92
+ */
93
+ function isValidApiKeyFormat(key) {
94
+ return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
37
95
  }
38
96
 
39
97
  function main() {
@@ -80,27 +138,69 @@ function main() {
80
138
  process.exit(1);
81
139
  }
82
140
 
83
- // Update Plexor plugin config
84
- config.settings = config.settings || {};
85
- config.settings.enabled = newEnabled;
86
- saveConfig(config);
87
-
88
141
  // THE KEY FEATURE: Update Claude Code settings.json routing
89
142
  let routingUpdated = false;
143
+ let missingApiKey = false;
144
+ let invalidApiKey = false;
145
+
90
146
  if (newEnabled) {
91
- // Enable routing - need API key from config
92
- if (apiKey) {
147
+ // Enable routing - need valid API key from config
148
+ if (!apiKey) {
149
+ missingApiKey = true;
150
+ } else if (!isValidApiKeyFormat(apiKey)) {
151
+ invalidApiKey = true;
152
+ } else {
153
+ // Update Plexor plugin config first
154
+ config.settings = config.settings || {};
155
+ config.settings.enabled = newEnabled;
156
+ if (!saveConfig(config)) {
157
+ process.exit(1);
158
+ }
159
+
160
+ // PRODUCTION PACKAGE - uses production API
93
161
  const apiUrl = config.settings?.apiUrl || 'https://api.plexor.dev';
94
162
  const useStaging = apiUrl.includes('staging');
95
163
  routingUpdated = settingsManager.enablePlexorRouting(apiKey, { useStaging });
96
- } else {
97
- console.log('⚠ No API key found. Run /plexor-login first.');
98
164
  }
99
165
  } else {
166
+ // Update Plexor plugin config
167
+ config.settings = config.settings || {};
168
+ config.settings.enabled = newEnabled;
169
+ if (!saveConfig(config)) {
170
+ process.exit(1);
171
+ }
172
+
100
173
  // Disable routing - remove env vars from settings.json
101
174
  routingUpdated = settingsManager.disablePlexorRouting();
102
175
  }
103
176
 
177
+ // Show error if no API key when enabling
178
+ if (missingApiKey) {
179
+ console.log(`┌─────────────────────────────────────────────┐`);
180
+ console.log(`│ ✗ Cannot Enable Plexor │`);
181
+ console.log(`├─────────────────────────────────────────────┤`);
182
+ console.log(`│ No API key configured. │`);
183
+ console.log(`│ Run /plexor-login <api-key> first. │`);
184
+ console.log(`├─────────────────────────────────────────────┤`);
185
+ console.log(`│ Get your API key at: │`);
186
+ console.log(`│ https://plexor.dev/dashboard/api-keys │`);
187
+ console.log(`└─────────────────────────────────────────────┘`);
188
+ process.exit(1);
189
+ }
190
+
191
+ // Show error if API key format is invalid
192
+ if (invalidApiKey) {
193
+ console.log(`┌─────────────────────────────────────────────┐`);
194
+ console.log(`│ ✗ Cannot Enable Plexor │`);
195
+ console.log(`├─────────────────────────────────────────────┤`);
196
+ console.log(`│ Invalid API key format in config. │`);
197
+ console.log(`│ Keys must start with "plx_" (20+ chars). │`);
198
+ console.log(`├─────────────────────────────────────────────┤`);
199
+ console.log(`│ Run /plexor-login <api-key> to fix. │`);
200
+ console.log(`└─────────────────────────────────────────────┘`);
201
+ process.exit(1);
202
+ }
203
+
104
204
  const newStatus = newEnabled ? '● Enabled' : '○ Disabled';
105
205
  const prevStatus = currentEnabled ? 'Enabled' : 'Disabled';
106
206
  const routingMsg = routingUpdated
@@ -13,27 +13,67 @@ const path = require('path');
13
13
  const https = require('https');
14
14
  const http = require('http');
15
15
 
16
- // Import settings manager for automatic Claude Code configuration
17
- const { settingsManager } = require('../lib/settings-manager');
16
+ // Import centralized constants with HOME directory validation
17
+ const { PLEXOR_DIR, CONFIG_PATH, DEFAULT_API_URL } = require('../lib/constants');
18
18
 
19
- const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
20
- const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
21
- const DEFAULT_API_URL = 'https://api.plexor.dev';
19
+ // Import settings manager with error handling for missing lib
20
+ let settingsManager;
21
+ try {
22
+ settingsManager = require('../lib/settings-manager').settingsManager;
23
+ } catch (err) {
24
+ if (err.code === 'MODULE_NOT_FOUND') {
25
+ console.error('Error: Plexor plugin files are missing or corrupted.');
26
+ console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin');
27
+ process.exit(1);
28
+ }
29
+ throw err;
30
+ }
22
31
 
23
32
  function loadConfig() {
24
33
  try {
34
+ if (!fs.existsSync(CONFIG_PATH)) {
35
+ return { version: 1, auth: {}, settings: {} };
36
+ }
25
37
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
26
- return JSON.parse(data);
27
- } catch {
38
+ if (!data || data.trim() === '') {
39
+ return { version: 1, auth: {}, settings: {} };
40
+ }
41
+ const config = JSON.parse(data);
42
+ if (typeof config !== 'object' || config === null) {
43
+ return { version: 1, auth: {}, settings: {} };
44
+ }
45
+ return config;
46
+ } catch (err) {
47
+ if (err instanceof SyntaxError) {
48
+ console.warn('Warning: Config file is corrupted, will be overwritten');
49
+ }
28
50
  return { version: 1, auth: {}, settings: {} };
29
51
  }
30
52
  }
31
53
 
32
54
  function saveConfig(config) {
33
- if (!fs.existsSync(PLEXOR_DIR)) {
34
- fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
55
+ try {
56
+ if (!fs.existsSync(PLEXOR_DIR)) {
57
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
58
+ }
59
+
60
+ // Atomic write: write to temp file, then rename
61
+ const crypto = require('crypto');
62
+ const tempId = crypto.randomBytes(8).toString('hex');
63
+ const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
64
+
65
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
66
+ fs.renameSync(tempPath, CONFIG_PATH);
67
+ return true;
68
+ } catch (err) {
69
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
70
+ console.error('Error: Cannot write to ~/.plexor/config.json');
71
+ console.error(' Check file permissions or run with appropriate access.');
72
+ } else {
73
+ console.error('Failed to save config:', err.message);
74
+ }
75
+ return false;
35
76
  }
36
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
37
77
  }
38
78
 
39
79
  function validateApiKey(apiUrl, apiKey) {
@@ -79,9 +119,62 @@ function validateApiKey(apiUrl, apiKey) {
79
119
  });
80
120
  }
81
121
 
122
+ /**
123
+ * Read API key from stdin (for piped input)
124
+ * @returns {Promise<string>} The API key read from stdin
125
+ */
126
+ function readFromStdin() {
127
+ return new Promise((resolve, reject) => {
128
+ let data = '';
129
+ const timeout = setTimeout(() => {
130
+ reject(new Error('Timeout reading from stdin'));
131
+ }, 5000);
132
+
133
+ process.stdin.setEncoding('utf8');
134
+ process.stdin.on('data', (chunk) => {
135
+ data += chunk;
136
+ });
137
+ process.stdin.on('end', () => {
138
+ clearTimeout(timeout);
139
+ resolve(data.trim());
140
+ });
141
+ process.stdin.on('error', (err) => {
142
+ clearTimeout(timeout);
143
+ reject(err);
144
+ });
145
+ process.stdin.resume();
146
+ });
147
+ }
148
+
82
149
  async function main() {
83
150
  const args = process.argv.slice(2);
84
- let apiKey = args[0];
151
+ let apiKey = null;
152
+ let keySource = null;
153
+
154
+ // SECURITY FIX: Check for API key in order of preference
155
+ // 1. Environment variable (most secure for scripts/CI)
156
+ // 2. Stdin pipe (secure for interactive use)
157
+ // 3. Command line argument (warns about security risk)
158
+
159
+ // Check environment variable first (most secure)
160
+ if (process.env.PLEXOR_API_KEY) {
161
+ apiKey = process.env.PLEXOR_API_KEY;
162
+ keySource = 'environment';
163
+ }
164
+ // Check if stdin has data (piped input)
165
+ else if (!process.stdin.isTTY && args.length === 0) {
166
+ try {
167
+ apiKey = await readFromStdin();
168
+ keySource = 'stdin';
169
+ } catch {
170
+ // Stdin read failed, continue to check args
171
+ }
172
+ }
173
+ // Check command line argument (least secure - shows in ps)
174
+ else if (args[0]) {
175
+ apiKey = args[0];
176
+ keySource = 'argument';
177
+ }
85
178
 
86
179
  // Check for existing login
87
180
  const config = loadConfig();
@@ -93,14 +186,14 @@ async function main() {
93
186
  console.log(`├─────────────────────────────────────────────┤`);
94
187
  console.log(`│ API Key: ${(existingKey.substring(0, 8) + '...').padEnd(33)}│`);
95
188
  console.log(`│ To re-login, provide a new key: │`);
96
- console.log(`│ /plexor-login <api-key> │`);
189
+ console.log(`│ echo $PLEXOR_API_KEY | /plexor-login │`);
97
190
  console.log(`│ To logout: │`);
98
191
  console.log(`│ /plexor-logout │`);
99
192
  console.log(`└─────────────────────────────────────────────┘`);
100
193
  return;
101
194
  }
102
195
 
103
- // If no key provided, prompt for it
196
+ // If no key provided, show secure usage options
104
197
  if (!apiKey) {
105
198
  console.log(`┌─────────────────────────────────────────────┐`);
106
199
  console.log(`│ Plexor Login │`);
@@ -108,11 +201,25 @@ async function main() {
108
201
  console.log(`│ Get your API key at: │`);
109
202
  console.log(`│ https://plexor.dev/dashboard/api-keys │`);
110
203
  console.log(`├─────────────────────────────────────────────┤`);
111
- console.log(`│ Usage: /plexor-login <api-key> │`);
204
+ console.log(`│ Secure usage (recommended): │`);
205
+ console.log(`│ echo "plx_..." | /plexor-login │`);
206
+ console.log(`│ PLEXOR_API_KEY=plx_... /plexor-login │`);
207
+ console.log(`├─────────────────────────────────────────────┤`);
208
+ console.log(`│ Direct usage (visible in ps): │`);
209
+ console.log(`│ /plexor-login <api-key> │`);
112
210
  console.log(`└─────────────────────────────────────────────┘`);
113
211
  return;
114
212
  }
115
213
 
214
+ // Warn if API key was passed as command line argument
215
+ if (keySource === 'argument') {
216
+ console.log(`⚠ Security note: API key passed as argument is visible in`);
217
+ console.log(` process list (ps aux). For better security, use:`);
218
+ console.log(` echo "plx_..." | /plexor-login`);
219
+ console.log(` PLEXOR_API_KEY=plx_... /plexor-login`);
220
+ console.log('');
221
+ }
222
+
116
223
  // Validate key format
117
224
  if (!apiKey.startsWith('plx_') || apiKey.length < 20) {
118
225
  console.error(`Error: Invalid API key format`);
@@ -131,7 +238,9 @@ async function main() {
131
238
  config.auth.api_key = apiKey;
132
239
  config.settings = config.settings || {};
133
240
  config.settings.enabled = true;
134
- saveConfig(config);
241
+ if (!saveConfig(config)) {
242
+ process.exit(1);
243
+ }
135
244
 
136
245
  // AUTO-CONFIGURE CLAUDE CODE ROUTING
137
246
  // This is the key feature: automatically set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
@@ -8,25 +8,68 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
 
11
- const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
12
- const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
13
- const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
14
- const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
11
+ // Import centralized constants with HOME directory validation
12
+ const { PLEXOR_DIR, CONFIG_PATH, SESSION_PATH, CACHE_PATH } = require('../lib/constants');
13
+
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 (err.code === 'MODULE_NOT_FOUND') {
21
+ console.error('Error: Plexor plugin files are missing or corrupted.');
22
+ console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin');
23
+ process.exit(1);
24
+ }
25
+ throw err;
26
+ }
15
27
 
16
28
  function loadConfig() {
17
29
  try {
30
+ if (!fs.existsSync(CONFIG_PATH)) {
31
+ return null;
32
+ }
18
33
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
19
- return JSON.parse(data);
20
- } catch {
34
+ if (!data || data.trim() === '') {
35
+ return null;
36
+ }
37
+ const config = JSON.parse(data);
38
+ if (typeof config !== 'object' || config === null) {
39
+ return null;
40
+ }
41
+ return config;
42
+ } catch (err) {
43
+ if (err instanceof SyntaxError) {
44
+ console.warn('Warning: Config file is corrupted');
45
+ }
21
46
  return null;
22
47
  }
23
48
  }
24
49
 
25
50
  function saveConfig(config) {
26
- if (!fs.existsSync(PLEXOR_DIR)) {
27
- fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
51
+ try {
52
+ if (!fs.existsSync(PLEXOR_DIR)) {
53
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
54
+ }
55
+
56
+ // Atomic write: write to temp file, then rename
57
+ const crypto = require('crypto');
58
+ const tempId = crypto.randomBytes(8).toString('hex');
59
+ const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
60
+
61
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
62
+ fs.renameSync(tempPath, CONFIG_PATH);
63
+ return true;
64
+ } catch (err) {
65
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
66
+ console.error('Error: Cannot write to ~/.plexor/config.json');
67
+ console.error(' Check file permissions or run with appropriate access.');
68
+ } else {
69
+ console.error('Failed to save config:', err.message);
70
+ }
71
+ return false;
28
72
  }
29
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
30
73
  }
31
74
 
32
75
  function deleteFile(filePath) {
@@ -61,7 +104,14 @@ function main() {
61
104
  delete config.auth.api_key;
62
105
  config.settings = config.settings || {};
63
106
  config.settings.enabled = false;
64
- saveConfig(config);
107
+ if (!saveConfig(config)) {
108
+ process.exit(1);
109
+ }
110
+
111
+ // CRITICAL FIX: Disable Claude Code routing in ~/.claude/settings.json
112
+ // This removes ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN so Claude Code
113
+ // no longer tries to route through Plexor with removed credentials
114
+ const routingDisabled = settingsManager.disablePlexorRouting();
65
115
 
66
116
  // Clear session
67
117
  deleteFile(SESSION_PATH);
@@ -76,12 +126,13 @@ function main() {
76
126
  console.log(`│ ✓ Logged Out │`);
77
127
  console.log(`├─────────────────────────────────────────────┤`);
78
128
  console.log(`│ ✓ API key removed │`);
79
- console.log(`│ ✓ Plexor proxy disabled │`);
129
+ console.log(`│ ${routingDisabled ? '' : '○'} Claude Code routing disabled │`);
80
130
  console.log(`│ ✓ Session cleared │`);
81
131
  if (clearCache) {
82
132
  console.log(`│ ${cacheCleared ? '✓' : '○'} Cache cleared │`);
83
133
  }
84
134
  console.log(`├─────────────────────────────────────────────┤`);
135
+ console.log(`│ Claude Code now connects directly. │`);
85
136
  console.log(`│ Run /plexor-login to re-authenticate. │`);
86
137
  if (!clearCache) {
87
138
  console.log(`│ Use --clear-cache to also clear cache. │`);
@@ -9,10 +9,9 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const https = require('https');
11
11
 
12
- const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
13
- const SESSION_PATH = path.join(process.env.HOME, '.plexor', 'session.json');
14
- const CLAUDE_SETTINGS_PATH = path.join(process.env.HOME, '.claude', 'settings.json');
15
- const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
12
+ // Import centralized constants with HOME directory validation
13
+ const { HOME_DIR, CONFIG_PATH, SESSION_PATH, SESSION_TIMEOUT_MS } = require('../lib/constants');
14
+ const CLAUDE_SETTINGS_PATH = path.join(HOME_DIR, '.claude', 'settings.json');
16
15
 
17
16
  /**
18
17
  * Check if Claude Code is actually routing through Plexor
@@ -49,13 +48,63 @@ function loadSessionStats() {
49
48
  }
50
49
  }
51
50
 
52
- async function main() {
53
- // Read config
54
- let config;
51
+ /**
52
+ * Validate API key format
53
+ * @param {string} key - API key to validate
54
+ * @returns {boolean} true if valid format
55
+ */
56
+ function isValidApiKeyFormat(key) {
57
+ return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
58
+ }
59
+
60
+ /**
61
+ * Load config file with integrity checking
62
+ * @returns {Object|null} config object or null if invalid
63
+ */
64
+ function loadConfig() {
55
65
  try {
66
+ if (!fs.existsSync(CONFIG_PATH)) {
67
+ return null;
68
+ }
56
69
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
57
- config = JSON.parse(data);
70
+ if (!data || data.trim() === '') {
71
+ return null;
72
+ }
73
+ const config = JSON.parse(data);
74
+ if (typeof config !== 'object' || config === null) {
75
+ return null;
76
+ }
77
+ return config;
58
78
  } catch (err) {
79
+ if (err instanceof SyntaxError) {
80
+ console.log('Config file is corrupted. Run /plexor-login to reconfigure.');
81
+ }
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check for environment mismatch between config and routing
88
+ */
89
+ function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
90
+ if (!configApiUrl || !routingBaseUrl) return null;
91
+
92
+ const configIsStaging = configApiUrl.includes('staging');
93
+ const routingIsStaging = routingBaseUrl.includes('staging');
94
+
95
+ if (configIsStaging !== routingIsStaging) {
96
+ return {
97
+ config: configIsStaging ? 'staging' : 'production',
98
+ routing: routingIsStaging ? 'staging' : 'production'
99
+ };
100
+ }
101
+ return null;
102
+ }
103
+
104
+ async function main() {
105
+ // Read config with integrity checking
106
+ const config = loadConfig();
107
+ if (!config) {
59
108
  console.log('Not configured. Run /plexor-login first.');
60
109
  process.exit(1);
61
110
  }
@@ -72,6 +121,13 @@ async function main() {
72
121
  process.exit(1);
73
122
  }
74
123
 
124
+ // Validate API key format
125
+ if (!isValidApiKeyFormat(apiKey)) {
126
+ console.log('Invalid API key format. Keys must start with "plx_" and be at least 20 characters.');
127
+ console.log('Run /plexor-login with a valid API key.');
128
+ process.exit(1);
129
+ }
130
+
75
131
  // Fetch user info and stats
76
132
  let user = { email: 'Unknown', tier: { name: 'Free', limits: {} } };
77
133
  let stats = { period: {}, summary: {} };
@@ -165,6 +221,16 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
165
221
  const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
166
222
  const envLabel = routing.isStaging ? '(staging)' : '(production)';
167
223
 
224
+ // Check for environment mismatch
225
+ const envMismatch = checkEnvironmentMismatch(apiUrl, routing.baseUrl);
226
+ const mismatchWarning = envMismatch
227
+ ? ` ⚠ Warning: Config uses ${envMismatch.config} but routing is ${envMismatch.routing}\n`
228
+ : '';
229
+
230
+ if (mismatchWarning) {
231
+ console.log(mismatchWarning);
232
+ }
233
+
168
234
  console.log(` ┌─────────────────────────────────────────────┐
169
235
  ${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
170
236
  ├─────────────────────────────────────────────┤
@@ -192,7 +258,8 @@ ${line('Settings')}
192
258
  ${line(`├── Optimization: ${optEnabled}`)}
193
259
  ${line(`├── Local cache: ${cacheEnabled}`)}
194
260
  ${line(`├── Mode: ${mode}`)}
195
- ${line(`└── Provider routing: ${provider}`)}
261
+ ${line(`├── Provider routing: ${provider}`)}
262
+ ${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https://', '').substring(0, 30) : 'not configured'}`)}
196
263
  └─────────────────────────────────────────────┘
197
264
 
198
265
  Dashboard: ${dashboardUrl}
@@ -201,7 +268,13 @@ ${line(`└── Provider routing: ${provider}`)}
201
268
 
202
269
  function fetchJson(apiUrl, endpoint, apiKey) {
203
270
  return new Promise((resolve, reject) => {
204
- const url = new URL(`${apiUrl}${endpoint}`);
271
+ let url;
272
+ try {
273
+ url = new URL(`${apiUrl}${endpoint}`);
274
+ } catch {
275
+ reject(new Error('Invalid API URL'));
276
+ return;
277
+ }
205
278
 
206
279
  const options = {
207
280
  hostname: url.hostname,
@@ -217,18 +290,48 @@ function fetchJson(apiUrl, endpoint, apiKey) {
217
290
  let data = '';
218
291
  res.on('data', chunk => data += chunk);
219
292
  res.on('end', () => {
293
+ // Check HTTP status code first
294
+ if (res.statusCode === 401) {
295
+ reject(new Error('Invalid API key'));
296
+ return;
297
+ }
298
+ if (res.statusCode === 403) {
299
+ reject(new Error('Access denied'));
300
+ return;
301
+ }
302
+ if (res.statusCode >= 500) {
303
+ reject(new Error('Server error'));
304
+ return;
305
+ }
306
+ if (res.statusCode !== 200) {
307
+ reject(new Error(`HTTP ${res.statusCode}`));
308
+ return;
309
+ }
310
+
311
+ // Check for empty response
312
+ if (!data || data.trim() === '') {
313
+ reject(new Error('Empty response'));
314
+ return;
315
+ }
316
+
317
+ // Parse JSON
220
318
  try {
221
- resolve(JSON.parse(data));
319
+ const parsed = JSON.parse(data);
320
+ if (parsed === null) {
321
+ reject(new Error('Null response'));
322
+ return;
323
+ }
324
+ resolve(parsed);
222
325
  } catch {
223
- reject(new Error('Invalid response'));
326
+ reject(new Error('Invalid JSON response'));
224
327
  }
225
328
  });
226
329
  });
227
330
 
228
- req.on('error', reject);
331
+ req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
229
332
  req.setTimeout(5000, () => {
230
333
  req.destroy();
231
- reject(new Error('Timeout'));
334
+ reject(new Error('Request timeout'));
232
335
  });
233
336
  req.end();
234
337
  });
package/lib/constants.js CHANGED
@@ -4,7 +4,23 @@
4
4
 
5
5
  const path = require('path');
6
6
 
7
- const PLEXOR_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.plexor');
7
+ /**
8
+ * Get the user's home directory with proper validation
9
+ * @returns {string} Home directory path
10
+ * @throws {Error} If HOME is not set or empty
11
+ */
12
+ function getHomeDir() {
13
+ const home = process.env.HOME || process.env.USERPROFILE;
14
+ if (!home || home.trim() === '') {
15
+ console.error('Error: HOME environment variable is not set.');
16
+ console.error(' Please set HOME to your user directory before running Plexor commands.');
17
+ process.exit(1);
18
+ }
19
+ return home;
20
+ }
21
+
22
+ const HOME_DIR = getHomeDir();
23
+ const PLEXOR_DIR = path.join(HOME_DIR, '.plexor');
8
24
  const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
9
25
  const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
10
26
  const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
@@ -15,6 +31,8 @@ const DEFAULT_API_URL = 'https://api.plexor.dev';
15
31
  const DEFAULT_TIMEOUT = 5000;
16
32
 
17
33
  module.exports = {
34
+ getHomeDir,
35
+ HOME_DIR,
18
36
  PLEXOR_DIR,
19
37
  CONFIG_PATH,
20
38
  SESSION_PATH,
@@ -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,18 +39,60 @@ 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
+ console.warn('Warning: Claude settings file has invalid format, using defaults');
53
+ this._backupCorruptedFile();
54
+ return {};
55
+ }
56
+
57
+ return parsed;
40
58
  } catch (err) {
41
- // Log unexpected errors (not ENOENT which is handled above)
42
- if (err.code !== 'ENOENT') {
43
- console.warn('Warning: Failed to load Claude settings:', err.message);
59
+ if (err.code === 'ENOENT') {
60
+ return {};
61
+ }
62
+ // JSON parse error or corrupted file
63
+ 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();
67
+ return {};
68
+ }
69
+ // Permission error
70
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
71
+ console.warn(`Warning: Cannot read ${this.settingsPath} (permission denied)`);
72
+ return {};
44
73
  }
74
+ console.warn('Warning: Failed to load Claude settings:', err.message);
45
75
  return {};
46
76
  }
47
77
  }
48
78
 
49
79
  /**
50
- * Save Claude settings
80
+ * Backup a corrupted settings file for debugging
81
+ */
82
+ _backupCorruptedFile() {
83
+ try {
84
+ if (fs.existsSync(this.settingsPath)) {
85
+ const backupPath = this.settingsPath + '.corrupted';
86
+ fs.copyFileSync(this.settingsPath, backupPath);
87
+ }
88
+ } catch {
89
+ // Ignore backup errors
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Save Claude settings using atomic write pattern
95
+ * Prevents race conditions by writing to temp file then renaming
51
96
  * @param {Object} settings - settings object to save
52
97
  * @returns {boolean} success status
53
98
  */
@@ -58,10 +103,27 @@ class ClaudeSettingsManager {
58
103
  fs.mkdirSync(this.claudeDir, { recursive: true });
59
104
  }
60
105
 
61
- fs.writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
106
+ // Atomic write: write to temp file, then rename
107
+ // This prevents race conditions where concurrent writes corrupt the file
108
+ const tempId = crypto.randomBytes(8).toString('hex');
109
+ const tempPath = path.join(this.claudeDir, `.settings.${tempId}.tmp`);
110
+
111
+ // Write to temp file
112
+ const content = JSON.stringify(settings, null, 2);
113
+ fs.writeFileSync(tempPath, content, { mode: 0o600 });
114
+
115
+ // Atomic rename (on POSIX systems, rename is atomic)
116
+ fs.renameSync(tempPath, this.settingsPath);
117
+
62
118
  return true;
63
119
  } catch (err) {
64
- console.error('Failed to save Claude settings:', err.message);
120
+ // Clean error message for permission errors
121
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
122
+ console.error(`Error: Cannot write to ${this.settingsPath}`);
123
+ console.error(' Check file permissions or run with appropriate access.');
124
+ } else {
125
+ console.error('Failed to save Claude settings:', err.message);
126
+ }
65
127
  return false;
66
128
  }
67
129
  }
@@ -77,7 +139,7 @@ class ClaudeSettingsManager {
77
139
  * @returns {boolean} success status
78
140
  */
79
141
  enablePlexorRouting(apiKey, options = {}) {
80
- // Default to production. Use useStaging: true for staging keys.
142
+ // PRODUCTION PACKAGE - defaults to production API
81
143
  const { useStaging = false } = options;
82
144
  const apiUrl = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
83
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin",
3
- "version": "0.1.0-beta.30",
3
+ "version": "0.1.0-beta.32",
4
4
  "description": "LLM cost optimization plugin for Claude Code - Save up to 90% on AI costs",
5
5
  "main": "lib/constants.js",
6
6
  "bin": {
@@ -70,12 +70,15 @@ 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
 
78
80
  // Default configuration for new installs
81
+ // PRODUCTION PACKAGE - uses production API
79
82
  const DEFAULT_CONFIG = {
80
83
  version: 1,
81
84
  auth: {
@@ -101,6 +104,9 @@ function main() {
101
104
  // Create ~/.claude/plugins/plexor/commands/ for JS executors
102
105
  fs.mkdirSync(PLEXOR_PLUGINS_DIR, { recursive: true });
103
106
 
107
+ // Create ~/.claude/plugins/plexor/lib/ for shared modules
108
+ fs.mkdirSync(PLEXOR_LIB_DIR, { recursive: true });
109
+
104
110
  // Create ~/.plexor/ with secure permissions (owner only)
105
111
  fs.mkdirSync(PLEXOR_CONFIG_DIR, { recursive: true, mode: 0o700 });
106
112
 
@@ -161,6 +167,40 @@ function main() {
161
167
  jsInstalled.push(file);
162
168
  }
163
169
 
170
+ // Copy lib files to ~/.claude/plugins/plexor/lib/
171
+ // CRITICAL: These are required for commands to work
172
+ const libInstalled = [];
173
+ if (fs.existsSync(LIB_SOURCE)) {
174
+ const libFiles = fs.readdirSync(LIB_SOURCE).filter(f => f.endsWith('.js'));
175
+ if (libFiles.length === 0) {
176
+ console.warn(' ⚠ Warning: No lib files found in package. Commands may not work.');
177
+ }
178
+ for (const file of libFiles) {
179
+ try {
180
+ const src = path.join(LIB_SOURCE, file);
181
+ const dest = path.join(PLEXOR_LIB_DIR, file);
182
+ fs.copyFileSync(src, dest);
183
+ libInstalled.push(file);
184
+ } catch (err) {
185
+ console.error(` ✗ Failed to copy lib/${file}: ${err.message}`);
186
+ }
187
+ }
188
+ } else {
189
+ console.error(' ✗ CRITICAL: lib/ directory not found in package.');
190
+ console.error(' Commands will fail. Please reinstall the package.');
191
+ console.error(` Expected location: ${LIB_SOURCE}`);
192
+ }
193
+
194
+ // Verify critical lib file exists
195
+ const criticalLibFile = path.join(PLEXOR_LIB_DIR, 'settings-manager.js');
196
+ if (!fs.existsSync(criticalLibFile)) {
197
+ console.error('');
198
+ console.error(' ✗ CRITICAL: settings-manager.js was not installed.');
199
+ console.error(' This file is required for commands to work.');
200
+ console.error(' Try reinstalling: npm install @plexor-dev/claude-code-plugin');
201
+ console.error('');
202
+ }
203
+
164
204
  // Fix file ownership when running with sudo
165
205
  // Files are created as root but should be owned by the original user
166
206
  if (targetUser) {
@@ -190,7 +230,10 @@ function main() {
190
230
  }
191
231
  console.log(` ✓ Installed ${installed.length} slash commands to ~/.claude/commands/`);
192
232
  if (jsInstalled.length > 0) {
193
- console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/`);
233
+ console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/commands/`);
234
+ }
235
+ if (libInstalled.length > 0) {
236
+ console.log(` ✓ Installed ${libInstalled.length} lib modules to ~/.claude/plugins/plexor/lib/`);
194
237
  }
195
238
  if (targetUser) {
196
239
  console.log(` ✓ Set file ownership to ${targetUser.user}`);
@@ -11,11 +11,32 @@ const fs = require('fs');
11
11
  const path = require('path');
12
12
  const os = require('os');
13
13
 
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;
22
+ }
23
+
14
24
  const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
15
25
  const CLAUDE_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
16
26
 
17
27
  function main() {
18
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
37
+ }
38
+ }
39
+
19
40
  // Get list of our command files
20
41
  const files = fs.readdirSync(COMMANDS_SOURCE)
21
42
  .filter(f => f.endsWith('.md'));
@@ -39,12 +60,21 @@ function main() {
39
60
  }
40
61
  }
41
62
 
42
- if (removed.length > 0) {
63
+ if (removed.length > 0 || routingDisabled) {
43
64
  console.log('');
44
65
  console.log(' Plexor plugin uninstalled');
45
66
  console.log('');
46
- console.log(' Removed commands:');
47
- removed.forEach(cmd => console.log(` /${cmd}`));
67
+
68
+ if (routingDisabled) {
69
+ console.log(' ✓ Claude Code routing disabled');
70
+ console.log(' (Claude Code now connects directly to Anthropic)');
71
+ console.log('');
72
+ }
73
+
74
+ if (removed.length > 0) {
75
+ console.log(' Removed commands:');
76
+ removed.forEach(cmd => console.log(` /${cmd}`));
77
+ }
48
78
 
49
79
  if (restored.length > 0) {
50
80
  console.log('');