@plexor-dev/claude-code-plugin 0.1.0-beta.30 → 0.1.0-beta.31
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.
- package/commands/plexor-enabled.js +116 -16
- package/commands/plexor-login.js +52 -8
- package/commands/plexor-logout.js +39 -6
- package/commands/plexor-status.js +114 -10
- package/lib/settings-manager.js +72 -10
- package/package.json +1 -1
- package/scripts/postinstall.js +44 -1
|
@@ -14,26 +14,84 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
|
|
17
|
-
// Import settings manager
|
|
18
|
-
|
|
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');
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
34
|
-
fs.
|
|
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
|
-
|
|
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
|
package/commands/plexor-login.js
CHANGED
|
@@ -13,27 +13,69 @@ const path = require('path');
|
|
|
13
13
|
const https = require('https');
|
|
14
14
|
const http = require('http');
|
|
15
15
|
|
|
16
|
-
// Import settings manager
|
|
17
|
-
|
|
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');
|
|
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');
|
|
31
|
+
// PRODUCTION PACKAGE - uses production API
|
|
21
32
|
const DEFAULT_API_URL = 'https://api.plexor.dev';
|
|
22
33
|
|
|
23
34
|
function loadConfig() {
|
|
24
35
|
try {
|
|
36
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
37
|
+
return { version: 1, auth: {}, settings: {} };
|
|
38
|
+
}
|
|
25
39
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
}
|
|
28
52
|
return { version: 1, auth: {}, settings: {} };
|
|
29
53
|
}
|
|
30
54
|
}
|
|
31
55
|
|
|
32
56
|
function saveConfig(config) {
|
|
33
|
-
|
|
34
|
-
fs.
|
|
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;
|
|
35
78
|
}
|
|
36
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
37
79
|
}
|
|
38
80
|
|
|
39
81
|
function validateApiKey(apiUrl, apiKey) {
|
|
@@ -131,7 +173,9 @@ async function main() {
|
|
|
131
173
|
config.auth.api_key = apiKey;
|
|
132
174
|
config.settings = config.settings || {};
|
|
133
175
|
config.settings.enabled = true;
|
|
134
|
-
saveConfig(config)
|
|
176
|
+
if (!saveConfig(config)) {
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
135
179
|
|
|
136
180
|
// AUTO-CONFIGURE CLAUDE CODE ROUTING
|
|
137
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
fs.
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
├─────────────────────────────────────────────┤
|
|
@@ -192,7 +259,8 @@ ${line('Settings')}
|
|
|
192
259
|
${line(`├── Optimization: ${optEnabled}`)}
|
|
193
260
|
${line(`├── Local cache: ${cacheEnabled}`)}
|
|
194
261
|
${line(`├── Mode: ${mode}`)}
|
|
195
|
-
${line(
|
|
262
|
+
${line(`├── Provider routing: ${provider}`)}
|
|
263
|
+
${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https://', '').substring(0, 30) : 'not configured'}`)}
|
|
196
264
|
└─────────────────────────────────────────────┘
|
|
197
265
|
|
|
198
266
|
Dashboard: ${dashboardUrl}
|
|
@@ -201,7 +269,13 @@ ${line(`└── Provider routing: ${provider}`)}
|
|
|
201
269
|
|
|
202
270
|
function fetchJson(apiUrl, endpoint, apiKey) {
|
|
203
271
|
return new Promise((resolve, reject) => {
|
|
204
|
-
|
|
272
|
+
let url;
|
|
273
|
+
try {
|
|
274
|
+
url = new URL(`${apiUrl}${endpoint}`);
|
|
275
|
+
} catch {
|
|
276
|
+
reject(new Error('Invalid API URL'));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
205
279
|
|
|
206
280
|
const options = {
|
|
207
281
|
hostname: url.hostname,
|
|
@@ -217,18 +291,48 @@ function fetchJson(apiUrl, endpoint, apiKey) {
|
|
|
217
291
|
let data = '';
|
|
218
292
|
res.on('data', chunk => data += chunk);
|
|
219
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
|
|
220
319
|
try {
|
|
221
|
-
|
|
320
|
+
const parsed = JSON.parse(data);
|
|
321
|
+
if (parsed === null) {
|
|
322
|
+
reject(new Error('Null response'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
resolve(parsed);
|
|
222
326
|
} catch {
|
|
223
|
-
reject(new Error('Invalid response'));
|
|
327
|
+
reject(new Error('Invalid JSON response'));
|
|
224
328
|
}
|
|
225
329
|
});
|
|
226
330
|
});
|
|
227
331
|
|
|
228
|
-
req.on('error', reject);
|
|
332
|
+
req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
|
|
229
333
|
req.setTimeout(5000, () => {
|
|
230
334
|
req.destroy();
|
|
231
|
-
reject(new Error('
|
|
335
|
+
reject(new Error('Request timeout'));
|
|
232
336
|
});
|
|
233
337
|
req.end();
|
|
234
338
|
});
|
package/lib/settings-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
package/scripts/postinstall.js
CHANGED
|
@@ -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}`);
|