@plexor-dev/claude-code-plugin-staging 0.1.0-beta.7 → 0.1.0-beta.9

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');
17
+ // Import settings manager with error handling for missing lib
18
+ let settingsManager, PLEXOR_STAGING_URL, PLEXOR_PROD_URL;
19
+ try {
20
+ const lib = require('../lib/settings-manager');
21
+ settingsManager = lib.settingsManager;
22
+ PLEXOR_STAGING_URL = lib.PLEXOR_STAGING_URL;
23
+ PLEXOR_PROD_URL = lib.PLEXOR_PROD_URL;
24
+ } catch (err) {
25
+ if (err.code === 'MODULE_NOT_FOUND') {
26
+ console.error('Error: Plexor plugin files are missing or corrupted.');
27
+ console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin-staging');
28
+ process.exit(1);
29
+ }
30
+ throw err;
31
+ }
19
32
 
20
33
  const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
21
34
  const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
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,28 +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
+
93
160
  // STAGING PACKAGE - uses staging API
94
161
  const apiUrl = config.settings?.apiUrl || 'https://staging.api.plexor.dev';
95
162
  const useStaging = apiUrl.includes('staging');
96
163
  routingUpdated = settingsManager.enablePlexorRouting(apiKey, { useStaging });
97
- } else {
98
- console.log('⚠ No API key found. Run /plexor-login first.');
99
164
  }
100
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
+
101
173
  // Disable routing - remove env vars from settings.json
102
174
  routingUpdated = settingsManager.disablePlexorRouting();
103
175
  }
104
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
+
105
204
  const newStatus = newEnabled ? '● Enabled' : '○ Disabled';
106
205
  const prevStatus = currentEnabled ? 'Enabled' : 'Disabled';
107
206
  const routingMsg = routingUpdated
@@ -13,8 +13,18 @@ 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 settings manager with error handling for missing lib
17
+ let settingsManager;
18
+ try {
19
+ settingsManager = require('../lib/settings-manager').settingsManager;
20
+ } catch (err) {
21
+ if (err.code === 'MODULE_NOT_FOUND') {
22
+ console.error('Error: Plexor plugin files are missing or corrupted.');
23
+ console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin-staging');
24
+ process.exit(1);
25
+ }
26
+ throw err;
27
+ }
18
28
 
19
29
  const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
20
30
  const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
@@ -23,18 +33,49 @@ const DEFAULT_API_URL = 'https://staging.api.plexor.dev';
23
33
 
24
34
  function loadConfig() {
25
35
  try {
36
+ if (!fs.existsSync(CONFIG_PATH)) {
37
+ return { version: 1, auth: {}, settings: {} };
38
+ }
26
39
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
27
- return JSON.parse(data);
28
- } catch {
40
+ if (!data || data.trim() === '') {
41
+ return { version: 1, auth: {}, settings: {} };
42
+ }
43
+ const config = JSON.parse(data);
44
+ if (typeof config !== 'object' || config === null) {
45
+ return { version: 1, auth: {}, settings: {} };
46
+ }
47
+ return config;
48
+ } catch (err) {
49
+ if (err instanceof SyntaxError) {
50
+ console.warn('Warning: Config file is corrupted, will be overwritten');
51
+ }
29
52
  return { version: 1, auth: {}, settings: {} };
30
53
  }
31
54
  }
32
55
 
33
56
  function saveConfig(config) {
34
- if (!fs.existsSync(PLEXOR_DIR)) {
35
- fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
57
+ try {
58
+ if (!fs.existsSync(PLEXOR_DIR)) {
59
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
60
+ }
61
+
62
+ // Atomic write: write to temp file, then rename
63
+ const crypto = require('crypto');
64
+ const tempId = crypto.randomBytes(8).toString('hex');
65
+ const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
66
+
67
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
68
+ fs.renameSync(tempPath, CONFIG_PATH);
69
+ return true;
70
+ } catch (err) {
71
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
72
+ console.error('Error: Cannot write to ~/.plexor/config.json');
73
+ console.error(' Check file permissions or run with appropriate access.');
74
+ } else {
75
+ console.error('Failed to save config:', err.message);
76
+ }
77
+ return false;
36
78
  }
37
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
38
79
  }
39
80
 
40
81
  function validateApiKey(apiUrl, apiKey) {
@@ -132,7 +173,9 @@ async function main() {
132
173
  config.auth.api_key = apiKey;
133
174
  config.settings = config.settings || {};
134
175
  config.settings.enabled = true;
135
- saveConfig(config);
176
+ if (!saveConfig(config)) {
177
+ process.exit(1);
178
+ }
136
179
 
137
180
  // AUTO-CONFIGURE CLAUDE CODE ROUTING
138
181
  // This is the key feature: automatically set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
@@ -15,18 +15,49 @@ const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
15
15
 
16
16
  function loadConfig() {
17
17
  try {
18
+ if (!fs.existsSync(CONFIG_PATH)) {
19
+ return null;
20
+ }
18
21
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
19
- return JSON.parse(data);
20
- } catch {
22
+ if (!data || data.trim() === '') {
23
+ return null;
24
+ }
25
+ const config = JSON.parse(data);
26
+ if (typeof config !== 'object' || config === null) {
27
+ return null;
28
+ }
29
+ return config;
30
+ } catch (err) {
31
+ if (err instanceof SyntaxError) {
32
+ console.warn('Warning: Config file is corrupted');
33
+ }
21
34
  return null;
22
35
  }
23
36
  }
24
37
 
25
38
  function saveConfig(config) {
26
- if (!fs.existsSync(PLEXOR_DIR)) {
27
- fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
39
+ try {
40
+ if (!fs.existsSync(PLEXOR_DIR)) {
41
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
42
+ }
43
+
44
+ // Atomic write: write to temp file, then rename
45
+ const crypto = require('crypto');
46
+ const tempId = crypto.randomBytes(8).toString('hex');
47
+ const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
48
+
49
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
50
+ fs.renameSync(tempPath, CONFIG_PATH);
51
+ return true;
52
+ } catch (err) {
53
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
54
+ console.error('Error: Cannot write to ~/.plexor/config.json');
55
+ console.error(' Check file permissions or run with appropriate access.');
56
+ } else {
57
+ console.error('Failed to save config:', err.message);
58
+ }
59
+ return false;
28
60
  }
29
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
30
61
  }
31
62
 
32
63
  function deleteFile(filePath) {
@@ -61,7 +92,9 @@ function main() {
61
92
  delete config.auth.api_key;
62
93
  config.settings = config.settings || {};
63
94
  config.settings.enabled = false;
64
- saveConfig(config);
95
+ if (!saveConfig(config)) {
96
+ process.exit(1);
97
+ }
65
98
 
66
99
  // Clear session
67
100
  deleteFile(SESSION_PATH);
@@ -49,13 +49,63 @@ function loadSessionStats() {
49
49
  }
50
50
  }
51
51
 
52
- async function main() {
53
- // Read config
54
- let config;
52
+ /**
53
+ * Validate API key format
54
+ * @param {string} key - API key to validate
55
+ * @returns {boolean} true if valid format
56
+ */
57
+ function isValidApiKeyFormat(key) {
58
+ return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
59
+ }
60
+
61
+ /**
62
+ * Load config file with integrity checking
63
+ * @returns {Object|null} config object or null if invalid
64
+ */
65
+ function loadConfig() {
55
66
  try {
67
+ if (!fs.existsSync(CONFIG_PATH)) {
68
+ return null;
69
+ }
56
70
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
57
- config = JSON.parse(data);
71
+ if (!data || data.trim() === '') {
72
+ return null;
73
+ }
74
+ const config = JSON.parse(data);
75
+ if (typeof config !== 'object' || config === null) {
76
+ return null;
77
+ }
78
+ return config;
58
79
  } catch (err) {
80
+ if (err instanceof SyntaxError) {
81
+ console.log('Config file is corrupted. Run /plexor-login to reconfigure.');
82
+ }
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Check for environment mismatch between config and routing
89
+ */
90
+ function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
91
+ if (!configApiUrl || !routingBaseUrl) return null;
92
+
93
+ const configIsStaging = configApiUrl.includes('staging');
94
+ const routingIsStaging = routingBaseUrl.includes('staging');
95
+
96
+ if (configIsStaging !== routingIsStaging) {
97
+ return {
98
+ config: configIsStaging ? 'staging' : 'production',
99
+ routing: routingIsStaging ? 'staging' : 'production'
100
+ };
101
+ }
102
+ return null;
103
+ }
104
+
105
+ async function main() {
106
+ // Read config with integrity checking
107
+ const config = loadConfig();
108
+ if (!config) {
59
109
  console.log('Not configured. Run /plexor-login first.');
60
110
  process.exit(1);
61
111
  }
@@ -72,6 +122,13 @@ async function main() {
72
122
  process.exit(1);
73
123
  }
74
124
 
125
+ // Validate API key format
126
+ if (!isValidApiKeyFormat(apiKey)) {
127
+ console.log('Invalid API key format. Keys must start with "plx_" and be at least 20 characters.');
128
+ console.log('Run /plexor-login with a valid API key.');
129
+ process.exit(1);
130
+ }
131
+
75
132
  // Fetch user info and stats
76
133
  let user = { email: 'Unknown', tier: { name: 'Free', limits: {} } };
77
134
  let stats = { period: {}, summary: {} };
@@ -165,6 +222,16 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
165
222
  const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
166
223
  const envLabel = routing.isStaging ? '(staging)' : '(production)';
167
224
 
225
+ // Check for environment mismatch
226
+ const envMismatch = checkEnvironmentMismatch(apiUrl, routing.baseUrl);
227
+ const mismatchWarning = envMismatch
228
+ ? ` ⚠ Warning: Config uses ${envMismatch.config} but routing is ${envMismatch.routing}\n`
229
+ : '';
230
+
231
+ if (mismatchWarning) {
232
+ console.log(mismatchWarning);
233
+ }
234
+
168
235
  console.log(` ┌─────────────────────────────────────────────┐
169
236
  ${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
170
237
  ├─────────────────────────────────────────────┤
@@ -202,7 +269,13 @@ ${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https:/
202
269
 
203
270
  function fetchJson(apiUrl, endpoint, apiKey) {
204
271
  return new Promise((resolve, reject) => {
205
- const url = new URL(`${apiUrl}${endpoint}`);
272
+ let url;
273
+ try {
274
+ url = new URL(`${apiUrl}${endpoint}`);
275
+ } catch {
276
+ reject(new Error('Invalid API URL'));
277
+ return;
278
+ }
206
279
 
207
280
  const options = {
208
281
  hostname: url.hostname,
@@ -218,18 +291,48 @@ function fetchJson(apiUrl, endpoint, apiKey) {
218
291
  let data = '';
219
292
  res.on('data', chunk => data += chunk);
220
293
  res.on('end', () => {
294
+ // Check HTTP status code first
295
+ if (res.statusCode === 401) {
296
+ reject(new Error('Invalid API key'));
297
+ return;
298
+ }
299
+ if (res.statusCode === 403) {
300
+ reject(new Error('Access denied'));
301
+ return;
302
+ }
303
+ if (res.statusCode >= 500) {
304
+ reject(new Error('Server error'));
305
+ return;
306
+ }
307
+ if (res.statusCode !== 200) {
308
+ reject(new Error(`HTTP ${res.statusCode}`));
309
+ return;
310
+ }
311
+
312
+ // Check for empty response
313
+ if (!data || data.trim() === '') {
314
+ reject(new Error('Empty response'));
315
+ return;
316
+ }
317
+
318
+ // Parse JSON
221
319
  try {
222
- resolve(JSON.parse(data));
320
+ const parsed = JSON.parse(data);
321
+ if (parsed === null) {
322
+ reject(new Error('Null response'));
323
+ return;
324
+ }
325
+ resolve(parsed);
223
326
  } catch {
224
- reject(new Error('Invalid response'));
327
+ reject(new Error('Invalid JSON response'));
225
328
  }
226
329
  });
227
330
  });
228
331
 
229
- req.on('error', reject);
332
+ req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
230
333
  req.setTimeout(5000, () => {
231
334
  req.destroy();
232
- reject(new Error('Timeout'));
335
+ reject(new Error('Request timeout'));
233
336
  });
234
337
  req.end();
235
338
  });
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin-staging",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
5
5
  "main": "lib/constants.js",
6
6
  "bin": {
@@ -168,15 +168,37 @@ function main() {
168
168
  }
169
169
 
170
170
  // Copy lib files to ~/.claude/plugins/plexor/lib/
171
+ // CRITICAL: These are required for commands to work
171
172
  const libInstalled = [];
172
173
  if (fs.existsSync(LIB_SOURCE)) {
173
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
+ }
174
178
  for (const file of libFiles) {
175
- const src = path.join(LIB_SOURCE, file);
176
- const dest = path.join(PLEXOR_LIB_DIR, file);
177
- fs.copyFileSync(src, dest);
178
- libInstalled.push(file);
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
+ }
179
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-staging');
201
+ console.error('');
180
202
  }
181
203
 
182
204
  // Fix file ownership when running with sudo