@plexor-dev/claude-code-plugin-staging 0.1.0-beta.2 → 0.1.0-beta.20
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/README.md +4 -7
- package/commands/plexor-agent.js +84 -0
- package/commands/plexor-agent.md +36 -0
- package/commands/plexor-enabled.js +177 -18
- package/commands/plexor-enabled.md +31 -13
- package/commands/plexor-login.js +211 -42
- package/commands/plexor-login.md +4 -21
- package/commands/plexor-logout.js +72 -14
- package/commands/plexor-logout.md +2 -20
- package/commands/plexor-provider.js +62 -81
- package/commands/plexor-provider.md +23 -13
- package/commands/plexor-routing.js +77 -0
- package/commands/plexor-routing.md +37 -0
- package/commands/plexor-settings.js +161 -123
- package/commands/plexor-settings.md +38 -14
- package/commands/plexor-setup.js +253 -0
- package/commands/plexor-setup.md +16 -160
- package/commands/plexor-status.js +244 -18
- package/commands/plexor-status.md +1 -13
- package/commands/plexor-uninstall.js +319 -0
- package/commands/plexor-uninstall.md +12 -0
- package/hooks/intercept.js +211 -32
- package/hooks/track-response.js +302 -2
- package/lib/config-utils.js +314 -0
- package/lib/config.js +22 -3
- package/lib/constants.js +19 -1
- package/lib/logger.js +64 -5
- package/lib/settings-manager.js +233 -24
- package/lib/verify-route.js +77 -0
- package/package.json +6 -4
- package/scripts/postinstall.js +271 -44
- package/scripts/uninstall.js +194 -41
- package/commands/plexor-config.js +0 -170
- package/commands/plexor-config.md +0 -28
- package/commands/plexor-mode.js +0 -107
- package/commands/plexor-mode.md +0 -27
|
@@ -9,10 +9,29 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const https = require('https');
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const CLAUDE_SETTINGS_PATH = path.join(
|
|
15
|
-
|
|
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');
|
|
15
|
+
|
|
16
|
+
function isManagedGatewayUrl(baseUrl = '') {
|
|
17
|
+
return (
|
|
18
|
+
baseUrl.includes('plexor') ||
|
|
19
|
+
baseUrl.includes('staging.api') ||
|
|
20
|
+
baseUrl.includes('ngrok') ||
|
|
21
|
+
baseUrl.includes('localtunnel') ||
|
|
22
|
+
baseUrl.includes('localhost') ||
|
|
23
|
+
baseUrl.includes('127.0.0.1')
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getPlexorAuthKey(env = {}) {
|
|
28
|
+
const apiKey = env.ANTHROPIC_API_KEY || '';
|
|
29
|
+
const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
|
|
30
|
+
|
|
31
|
+
if (apiKey.startsWith('plx_')) return apiKey;
|
|
32
|
+
if (authToken.startsWith('plx_')) return authToken;
|
|
33
|
+
return apiKey || authToken || '';
|
|
34
|
+
}
|
|
16
35
|
|
|
17
36
|
/**
|
|
18
37
|
* Check if Claude Code is actually routing through Plexor
|
|
@@ -23,10 +42,10 @@ function getRoutingStatus() {
|
|
|
23
42
|
const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
|
|
24
43
|
const settings = JSON.parse(data);
|
|
25
44
|
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
|
|
26
|
-
const
|
|
27
|
-
const isPlexorRouting =
|
|
45
|
+
const authKey = getPlexorAuthKey(settings.env);
|
|
46
|
+
const isPlexorRouting = isManagedGatewayUrl(baseUrl);
|
|
28
47
|
return {
|
|
29
|
-
active: isPlexorRouting &&
|
|
48
|
+
active: isPlexorRouting && !!authKey,
|
|
30
49
|
baseUrl,
|
|
31
50
|
isStaging: baseUrl.includes('staging')
|
|
32
51
|
};
|
|
@@ -35,6 +54,31 @@ function getRoutingStatus() {
|
|
|
35
54
|
}
|
|
36
55
|
}
|
|
37
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Detect partial routing state where URL points to Plexor but auth is missing/invalid
|
|
59
|
+
* This can cause confusing auth errors for users
|
|
60
|
+
* @returns {Object} { partial: boolean, issue: string|null }
|
|
61
|
+
*/
|
|
62
|
+
function detectPartialState() {
|
|
63
|
+
try {
|
|
64
|
+
const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
|
|
65
|
+
const settings = JSON.parse(data);
|
|
66
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
|
|
67
|
+
const authKey = getPlexorAuthKey(settings.env);
|
|
68
|
+
const isPlexorUrl = isManagedGatewayUrl(baseUrl);
|
|
69
|
+
|
|
70
|
+
if (isPlexorUrl && !authKey) {
|
|
71
|
+
return { partial: true, issue: 'Plexor URL set but no Plexor auth key' };
|
|
72
|
+
}
|
|
73
|
+
if (isPlexorUrl && !authKey.startsWith('plx_')) {
|
|
74
|
+
return { partial: true, issue: 'Plexor URL set but auth key is not a Plexor key' };
|
|
75
|
+
}
|
|
76
|
+
return { partial: false, issue: null };
|
|
77
|
+
} catch {
|
|
78
|
+
return { partial: false, issue: null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
38
82
|
function loadSessionStats() {
|
|
39
83
|
try {
|
|
40
84
|
const data = fs.readFileSync(SESSION_PATH, 'utf8');
|
|
@@ -49,14 +93,107 @@ function loadSessionStats() {
|
|
|
49
93
|
}
|
|
50
94
|
}
|
|
51
95
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Validate API key format
|
|
98
|
+
* @param {string} key - API key to validate
|
|
99
|
+
* @returns {boolean} true if valid format
|
|
100
|
+
*/
|
|
101
|
+
function isValidApiKeyFormat(key) {
|
|
102
|
+
return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Load config file with integrity checking
|
|
107
|
+
* @returns {Object|null} config object or null if invalid
|
|
108
|
+
*/
|
|
109
|
+
function loadConfig() {
|
|
55
110
|
try {
|
|
111
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
56
114
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
57
|
-
|
|
115
|
+
if (!data || data.trim() === '') {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const config = JSON.parse(data);
|
|
119
|
+
if (typeof config !== 'object' || config === null) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return config;
|
|
58
123
|
} catch (err) {
|
|
59
|
-
|
|
124
|
+
if (err instanceof SyntaxError) {
|
|
125
|
+
console.log('Config file is corrupted. Run /plexor-setup to reconfigure.');
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check for environment mismatch between config and routing
|
|
133
|
+
*/
|
|
134
|
+
function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
|
|
135
|
+
if (!configApiUrl || !routingBaseUrl) return null;
|
|
136
|
+
|
|
137
|
+
const configIsStaging = configApiUrl.includes('staging');
|
|
138
|
+
const routingIsStaging = routingBaseUrl.includes('staging');
|
|
139
|
+
|
|
140
|
+
if (configIsStaging !== routingIsStaging) {
|
|
141
|
+
return {
|
|
142
|
+
config: configIsStaging ? 'staging' : 'production',
|
|
143
|
+
routing: routingIsStaging ? 'staging' : 'production'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check for state mismatch between config.json enabled flag and settings.json routing
|
|
151
|
+
* @param {boolean} configEnabled - enabled flag from config.json
|
|
152
|
+
* @param {boolean} routingActive - whether settings.json has Plexor routing configured
|
|
153
|
+
* @returns {Object|null} mismatch details or null if states are consistent
|
|
154
|
+
*/
|
|
155
|
+
function checkStateMismatch(configEnabled, routingActive) {
|
|
156
|
+
if (configEnabled && !routingActive) {
|
|
157
|
+
return {
|
|
158
|
+
type: 'config-enabled-routing-inactive',
|
|
159
|
+
message: 'Config shows enabled but Claude routing is not configured',
|
|
160
|
+
suggestion: 'Run /plexor-enabled true to sync and configure routing'
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (!configEnabled && routingActive) {
|
|
164
|
+
return {
|
|
165
|
+
type: 'config-disabled-routing-active',
|
|
166
|
+
message: 'Config shows disabled but Claude routing is active',
|
|
167
|
+
suggestion: 'Run /plexor-enabled false to sync and disable routing'
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function printHealthSummary(summary) {
|
|
174
|
+
const line = (content) => ` │ ${String(content).slice(0, 43).padEnd(43)}│`;
|
|
175
|
+
console.log(` ┌─────────────────────────────────────────────┐`);
|
|
176
|
+
console.log(line('Plexor health'));
|
|
177
|
+
console.log(` ├─────────────────────────────────────────────┤`);
|
|
178
|
+
console.log(line(`Installed: ${summary.installed}`));
|
|
179
|
+
console.log(line(`Connected: ${summary.connected}`));
|
|
180
|
+
console.log(line(`Routing Active: ${summary.routing}`));
|
|
181
|
+
console.log(line(`Verified: ${summary.verified}`));
|
|
182
|
+
console.log(line(`Next action: ${summary.nextAction}`));
|
|
183
|
+
console.log(` └─────────────────────────────────────────────┘`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function main() {
|
|
187
|
+
// Read config with integrity checking
|
|
188
|
+
const config = loadConfig();
|
|
189
|
+
if (!config) {
|
|
190
|
+
printHealthSummary({
|
|
191
|
+
installed: 'Missing',
|
|
192
|
+
connected: 'Not connected',
|
|
193
|
+
routing: 'Inactive',
|
|
194
|
+
verified: 'Not run',
|
|
195
|
+
nextAction: 'Run /plexor-setup'
|
|
196
|
+
});
|
|
60
197
|
process.exit(1);
|
|
61
198
|
}
|
|
62
199
|
|
|
@@ -66,21 +203,42 @@ async function main() {
|
|
|
66
203
|
const provider = config.settings?.preferred_provider || 'auto';
|
|
67
204
|
const localCache = config.settings?.localCacheEnabled ?? false;
|
|
68
205
|
const apiUrl = config.settings?.apiUrl || 'https://api.plexor.dev';
|
|
206
|
+
const verification = config.health || {};
|
|
69
207
|
|
|
70
208
|
if (!apiKey) {
|
|
71
|
-
|
|
209
|
+
printHealthSummary({
|
|
210
|
+
installed: 'OK',
|
|
211
|
+
connected: 'Missing key',
|
|
212
|
+
routing: 'Inactive',
|
|
213
|
+
verified: 'Not run',
|
|
214
|
+
nextAction: 'Run /plexor-setup'
|
|
215
|
+
});
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Validate API key format
|
|
220
|
+
if (!isValidApiKeyFormat(apiKey)) {
|
|
221
|
+
printHealthSummary({
|
|
222
|
+
installed: 'OK',
|
|
223
|
+
connected: 'Invalid key',
|
|
224
|
+
routing: 'Needs repair',
|
|
225
|
+
verified: 'Failed',
|
|
226
|
+
nextAction: 'Run /plexor-setup'
|
|
227
|
+
});
|
|
72
228
|
process.exit(1);
|
|
73
229
|
}
|
|
74
230
|
|
|
75
231
|
// Fetch user info and stats
|
|
76
232
|
let user = { email: 'Unknown', tier: { name: 'Free', limits: {} } };
|
|
77
233
|
let stats = { period: {}, summary: {} };
|
|
234
|
+
let userFetchWorked = false;
|
|
78
235
|
|
|
79
236
|
try {
|
|
80
237
|
[user, stats] = await Promise.all([
|
|
81
238
|
fetchJson(apiUrl, '/v1/user', apiKey),
|
|
82
239
|
fetchJson(apiUrl, '/v1/stats', apiKey)
|
|
83
240
|
]);
|
|
241
|
+
userFetchWorked = true;
|
|
84
242
|
} catch (err) {
|
|
85
243
|
// Continue with defaults if API fails
|
|
86
244
|
}
|
|
@@ -164,6 +322,38 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
|
|
|
164
322
|
const routing = getRoutingStatus();
|
|
165
323
|
const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
|
|
166
324
|
const envLabel = routing.isStaging ? '(staging)' : '(production)';
|
|
325
|
+
const partialState = detectPartialState();
|
|
326
|
+
const stateMismatch = checkStateMismatch(enabled, routing.active);
|
|
327
|
+
const connectedState = userFetchWorked ? 'OK' : 'Unknown';
|
|
328
|
+
const verifiedState = verification.verified
|
|
329
|
+
? 'OK'
|
|
330
|
+
: (verification.verify_error ? 'Failed' : 'Not yet');
|
|
331
|
+
const nextAction = !routing.active || partialState.partial || stateMismatch || !verification.verified
|
|
332
|
+
? '/plexor-setup'
|
|
333
|
+
: '/plexor-status';
|
|
334
|
+
|
|
335
|
+
// Note: Environment mismatch warning removed - it caused false positives during
|
|
336
|
+
// concurrent operations and transient states. The partial state and config/routing
|
|
337
|
+
// mismatch warnings below provide more actionable feedback.
|
|
338
|
+
|
|
339
|
+
printHealthSummary({
|
|
340
|
+
installed: 'OK',
|
|
341
|
+
connected: connectedState,
|
|
342
|
+
routing: routing.active ? `OK ${envLabel}` : 'Needs repair',
|
|
343
|
+
verified: verifiedState,
|
|
344
|
+
nextAction
|
|
345
|
+
});
|
|
346
|
+
console.log('');
|
|
347
|
+
if (partialState.partial) {
|
|
348
|
+
console.log(` ⚠ PARTIAL STATE DETECTED: ${partialState.issue}`);
|
|
349
|
+
console.log(` Run /plexor-setup to repair, or /plexor-logout to disable routing\n`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check for state mismatch between config enabled flag and routing status
|
|
353
|
+
if (stateMismatch) {
|
|
354
|
+
console.log(` ⚠ State mismatch: ${stateMismatch.message}`);
|
|
355
|
+
console.log(` └─ ${stateMismatch.suggestion}\n`);
|
|
356
|
+
}
|
|
167
357
|
|
|
168
358
|
console.log(` ┌─────────────────────────────────────────────┐
|
|
169
359
|
${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
|
|
@@ -202,7 +392,13 @@ ${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https:/
|
|
|
202
392
|
|
|
203
393
|
function fetchJson(apiUrl, endpoint, apiKey) {
|
|
204
394
|
return new Promise((resolve, reject) => {
|
|
205
|
-
|
|
395
|
+
let url;
|
|
396
|
+
try {
|
|
397
|
+
url = new URL(`${apiUrl}${endpoint}`);
|
|
398
|
+
} catch {
|
|
399
|
+
reject(new Error('Invalid API URL'));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
206
402
|
|
|
207
403
|
const options = {
|
|
208
404
|
hostname: url.hostname,
|
|
@@ -218,18 +414,48 @@ function fetchJson(apiUrl, endpoint, apiKey) {
|
|
|
218
414
|
let data = '';
|
|
219
415
|
res.on('data', chunk => data += chunk);
|
|
220
416
|
res.on('end', () => {
|
|
417
|
+
// Check HTTP status code first
|
|
418
|
+
if (res.statusCode === 401) {
|
|
419
|
+
reject(new Error('Invalid API key'));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (res.statusCode === 403) {
|
|
423
|
+
reject(new Error('Access denied'));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (res.statusCode >= 500) {
|
|
427
|
+
reject(new Error('Server error'));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (res.statusCode !== 200) {
|
|
431
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check for empty response
|
|
436
|
+
if (!data || data.trim() === '') {
|
|
437
|
+
reject(new Error('Empty response'));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Parse JSON
|
|
221
442
|
try {
|
|
222
|
-
|
|
443
|
+
const parsed = JSON.parse(data);
|
|
444
|
+
if (parsed === null) {
|
|
445
|
+
reject(new Error('Null response'));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
resolve(parsed);
|
|
223
449
|
} catch {
|
|
224
|
-
reject(new Error('Invalid response'));
|
|
450
|
+
reject(new Error('Invalid JSON response'));
|
|
225
451
|
}
|
|
226
452
|
});
|
|
227
453
|
});
|
|
228
454
|
|
|
229
|
-
req.on('error', reject);
|
|
455
|
+
req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
|
|
230
456
|
req.setTimeout(5000, () => {
|
|
231
457
|
req.destroy();
|
|
232
|
-
reject(new Error('
|
|
458
|
+
reject(new Error('Request timeout'));
|
|
233
459
|
});
|
|
234
460
|
req.end();
|
|
235
461
|
});
|
|
@@ -2,20 +2,8 @@
|
|
|
2
2
|
description: Show Plexor optimization statistics and savings (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Run this command to display Plexor statistics:
|
|
5
|
+
**RULE: Execute the bash command below EXACTLY ONCE. After the Bash tool returns output, your ONLY action is to present that output to the user. DO NOT re-execute the command. DO NOT call any other tools.**
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
8
|
node ~/.claude/plugins/plexor/commands/plexor-status.js
|
|
11
9
|
```
|
|
12
|
-
|
|
13
|
-
Use the Bash tool to execute this single command.
|
|
14
|
-
|
|
15
|
-
**IMPORTANT**: After running this command and displaying the output, STOP. Do not:
|
|
16
|
-
- Read any files
|
|
17
|
-
- Explore the codebase
|
|
18
|
-
- Run additional commands
|
|
19
|
-
- Ask follow-up questions
|
|
20
|
-
|
|
21
|
-
The command output is the complete response. Simply show the output and wait for the user's next input.
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plexor Uninstall Command (STAGING)
|
|
5
|
+
*
|
|
6
|
+
* Comprehensive cleanup before npm uninstall.
|
|
7
|
+
*
|
|
8
|
+
* CRITICAL: npm's preuninstall hook does NOT run for global package uninstalls.
|
|
9
|
+
* Users MUST run this command BEFORE running npm uninstall -g.
|
|
10
|
+
*
|
|
11
|
+
* This command:
|
|
12
|
+
* 1. Removes Plexor routing from ~/.claude/settings.json
|
|
13
|
+
* 2. Removes slash command .md files from ~/.claude/commands/
|
|
14
|
+
* 3. Removes plugin directory ~/.claude/plugins/plexor/
|
|
15
|
+
* 4. Optionally removes ~/.plexor/ config directory
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
// Get home directory, handling sudo case
|
|
23
|
+
function getHomeDir() {
|
|
24
|
+
if (process.env.SUDO_USER) {
|
|
25
|
+
const platform = os.platform();
|
|
26
|
+
if (platform === 'darwin') {
|
|
27
|
+
return path.join('/Users', process.env.SUDO_USER);
|
|
28
|
+
} else if (platform === 'linux') {
|
|
29
|
+
return path.join('/home', process.env.SUDO_USER);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const HOME_DIR = getHomeDir();
|
|
36
|
+
const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
|
|
37
|
+
const CLAUDE_COMMANDS_DIR = path.join(CLAUDE_DIR, 'commands');
|
|
38
|
+
const CLAUDE_PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins', 'plexor');
|
|
39
|
+
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
40
|
+
|
|
41
|
+
// All Plexor slash command files
|
|
42
|
+
const PLEXOR_COMMANDS = [
|
|
43
|
+
'plexor-enabled.md',
|
|
44
|
+
'plexor-login.md',
|
|
45
|
+
'plexor-logout.md',
|
|
46
|
+
'plexor-setup.md',
|
|
47
|
+
'plexor-status.md',
|
|
48
|
+
'plexor-uninstall.md',
|
|
49
|
+
'plexor-mode.md',
|
|
50
|
+
'plexor-provider.md',
|
|
51
|
+
'plexor-settings.md',
|
|
52
|
+
'plexor-config.md'
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load settings manager if available
|
|
57
|
+
*/
|
|
58
|
+
function loadSettingsManager() {
|
|
59
|
+
// Try plugin dir first (installed location)
|
|
60
|
+
try {
|
|
61
|
+
const pluginLib = path.join(CLAUDE_PLUGINS_DIR, 'lib', 'settings-manager.js');
|
|
62
|
+
if (fs.existsSync(pluginLib)) {
|
|
63
|
+
const lib = require(pluginLib);
|
|
64
|
+
return lib.settingsManager;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Continue to fallback
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Try package lib (during npm lifecycle)
|
|
71
|
+
try {
|
|
72
|
+
const lib = require('../lib/settings-manager');
|
|
73
|
+
return lib.settingsManager;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Disable Plexor routing in Claude settings manually
|
|
81
|
+
* Fallback when settings-manager is not available
|
|
82
|
+
*/
|
|
83
|
+
function disableRoutingManually() {
|
|
84
|
+
const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.existsSync(settingsPath)) {
|
|
88
|
+
return { success: true, message: 'Settings file does not exist' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = fs.readFileSync(settingsPath, 'utf8');
|
|
92
|
+
if (!data || data.trim() === '') {
|
|
93
|
+
return { success: true, message: 'Settings file is empty' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const settings = JSON.parse(data);
|
|
97
|
+
|
|
98
|
+
if (!settings.env) {
|
|
99
|
+
return { success: true, message: 'No env block in settings' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isManagedGatewayUrl = (baseUrl = '') =>
|
|
103
|
+
baseUrl.includes('plexor') ||
|
|
104
|
+
baseUrl.includes('staging.api') ||
|
|
105
|
+
baseUrl.includes('localhost') ||
|
|
106
|
+
baseUrl.includes('127.0.0.1') ||
|
|
107
|
+
baseUrl.includes('ngrok') ||
|
|
108
|
+
baseUrl.includes('localtunnel');
|
|
109
|
+
const isPlexorApiKey = (value = '') =>
|
|
110
|
+
typeof value === 'string' && value.startsWith('plx_');
|
|
111
|
+
|
|
112
|
+
const hasManagedBaseUrl = isManagedGatewayUrl(settings.env.ANTHROPIC_BASE_URL || '');
|
|
113
|
+
const hasPlexorAuthToken = isPlexorApiKey(settings.env.ANTHROPIC_AUTH_TOKEN || '');
|
|
114
|
+
const hasPlexorApiKey = isPlexorApiKey(settings.env.ANTHROPIC_API_KEY || '');
|
|
115
|
+
|
|
116
|
+
if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey) {
|
|
117
|
+
return { success: true, message: 'Plexor routing not active' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (hasManagedBaseUrl) {
|
|
121
|
+
delete settings.env.ANTHROPIC_BASE_URL;
|
|
122
|
+
}
|
|
123
|
+
if (hasPlexorAuthToken) {
|
|
124
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
125
|
+
}
|
|
126
|
+
if (hasPlexorApiKey) {
|
|
127
|
+
delete settings.env.ANTHROPIC_API_KEY;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!settings.env.ANTHROPIC_API_KEY && settings.env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY) {
|
|
131
|
+
settings.env.ANTHROPIC_API_KEY = settings.env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY;
|
|
132
|
+
}
|
|
133
|
+
if (!settings.env.ANTHROPIC_AUTH_TOKEN && settings.env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN) {
|
|
134
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = settings.env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
delete settings.env.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY;
|
|
138
|
+
delete settings.env.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN;
|
|
139
|
+
|
|
140
|
+
// Clean up empty env block
|
|
141
|
+
if (Object.keys(settings.env).length === 0) {
|
|
142
|
+
delete settings.env;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Atomic write
|
|
146
|
+
const crypto = require('crypto');
|
|
147
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
148
|
+
const tempPath = path.join(CLAUDE_DIR, `.settings.${tempId}.tmp`);
|
|
149
|
+
|
|
150
|
+
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
151
|
+
fs.renameSync(tempPath, settingsPath);
|
|
152
|
+
|
|
153
|
+
return { success: true, message: 'Routing disabled' };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return { success: false, message: err.message };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Remove slash command files
|
|
161
|
+
*/
|
|
162
|
+
function removeSlashCommands() {
|
|
163
|
+
let removed = 0;
|
|
164
|
+
let restored = 0;
|
|
165
|
+
|
|
166
|
+
for (const cmd of PLEXOR_COMMANDS) {
|
|
167
|
+
const cmdPath = path.join(CLAUDE_COMMANDS_DIR, cmd);
|
|
168
|
+
const backupPath = cmdPath + '.backup';
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
if (fs.existsSync(cmdPath)) {
|
|
172
|
+
fs.unlinkSync(cmdPath);
|
|
173
|
+
removed++;
|
|
174
|
+
|
|
175
|
+
// Restore backup if exists
|
|
176
|
+
if (fs.existsSync(backupPath)) {
|
|
177
|
+
fs.renameSync(backupPath, cmdPath);
|
|
178
|
+
restored++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Continue with other files
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { removed, restored };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Remove plugin directory
|
|
191
|
+
*/
|
|
192
|
+
function removePluginDirectory() {
|
|
193
|
+
try {
|
|
194
|
+
if (fs.existsSync(CLAUDE_PLUGINS_DIR)) {
|
|
195
|
+
fs.rmSync(CLAUDE_PLUGINS_DIR, { recursive: true, force: true });
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Remove config directory
|
|
206
|
+
*/
|
|
207
|
+
function removeConfigDirectory() {
|
|
208
|
+
try {
|
|
209
|
+
if (fs.existsSync(PLEXOR_CONFIG_DIR)) {
|
|
210
|
+
fs.rmSync(PLEXOR_CONFIG_DIR, { recursive: true, force: true });
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function main() {
|
|
220
|
+
const args = process.argv.slice(2);
|
|
221
|
+
|
|
222
|
+
// Handle help flag
|
|
223
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
224
|
+
console.log('');
|
|
225
|
+
console.log(' Usage: plexor-uninstall [options]');
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log(' Cleans up Plexor integration before npm uninstall.');
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log(' Options:');
|
|
230
|
+
console.log(' --remove-config, -c Also remove ~/.plexor/ config directory');
|
|
231
|
+
console.log(' --quiet, -q Suppress output messages');
|
|
232
|
+
console.log(' --help, -h Show this help message');
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log(' After running this command:');
|
|
235
|
+
console.log(' npm uninstall -g @plexor-dev/claude-code-plugin-staging');
|
|
236
|
+
console.log('');
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const removeConfig = args.includes('--remove-config') || args.includes('-c');
|
|
241
|
+
const quiet = args.includes('--quiet') || args.includes('-q');
|
|
242
|
+
|
|
243
|
+
if (!quiet) {
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log(' Plexor Uninstall (STAGING) - Cleaning up...');
|
|
246
|
+
console.log('');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 1. Remove routing from ~/.claude/settings.json
|
|
250
|
+
let routingResult;
|
|
251
|
+
const settingsManager = loadSettingsManager();
|
|
252
|
+
|
|
253
|
+
if (settingsManager) {
|
|
254
|
+
try {
|
|
255
|
+
const success = settingsManager.disablePlexorRouting();
|
|
256
|
+
routingResult = { success, message: success ? 'Routing disabled via manager' : 'Already clean' };
|
|
257
|
+
} catch (err) {
|
|
258
|
+
routingResult = disableRoutingManually();
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
routingResult = disableRoutingManually();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!quiet) {
|
|
265
|
+
console.log(routingResult.success
|
|
266
|
+
? ' ✓ Removed Plexor routing from Claude settings'
|
|
267
|
+
: ` ✗ Failed to remove routing: ${routingResult.message}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 2. Remove slash command .md files
|
|
271
|
+
const cmdResult = removeSlashCommands();
|
|
272
|
+
if (!quiet) {
|
|
273
|
+
console.log(` ✓ Removed ${cmdResult.removed} slash command files`);
|
|
274
|
+
if (cmdResult.restored > 0) {
|
|
275
|
+
console.log(` ✓ Restored ${cmdResult.restored} backed-up files`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 3. Remove plugin directory
|
|
280
|
+
const pluginRemoved = removePluginDirectory();
|
|
281
|
+
if (!quiet) {
|
|
282
|
+
console.log(pluginRemoved
|
|
283
|
+
? ' ✓ Removed plugin directory'
|
|
284
|
+
: ' ○ Plugin directory not found (already clean)');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 4. Optionally remove config directory
|
|
288
|
+
if (removeConfig) {
|
|
289
|
+
const configRemoved = removeConfigDirectory();
|
|
290
|
+
if (!quiet) {
|
|
291
|
+
console.log(configRemoved
|
|
292
|
+
? ' ✓ Removed ~/.plexor config directory'
|
|
293
|
+
: ' ○ Config directory not found');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Show next steps
|
|
298
|
+
if (!quiet) {
|
|
299
|
+
console.log('');
|
|
300
|
+
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
|
301
|
+
console.log(' │ Cleanup complete! Now run: │');
|
|
302
|
+
console.log(' │ │');
|
|
303
|
+
console.log(' │ npm uninstall -g @plexor-dev/claude-code-plugin-staging │');
|
|
304
|
+
console.log(' │ │');
|
|
305
|
+
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
306
|
+
console.log('');
|
|
307
|
+
console.log(' Your Claude Code is ready to work normally again.');
|
|
308
|
+
console.log('');
|
|
309
|
+
|
|
310
|
+
if (!removeConfig) {
|
|
311
|
+
console.log(' Note: ~/.plexor/ config directory was preserved.');
|
|
312
|
+
console.log(' To also remove it: plexor-uninstall --remove-config');
|
|
313
|
+
console.log(' Or manually: rm -rf ~/.plexor');
|
|
314
|
+
console.log('');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
main();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plexor-uninstall
|
|
3
|
+
description: Clean up Plexor integration before uninstalling
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
**RULE: Execute the bash command below EXACTLY ONCE. After the Bash tool returns output, your ONLY action is to present that output to the user. DO NOT re-execute the command. DO NOT call any other tools.**
|
|
7
|
+
|
|
8
|
+
**IMPORTANT**: Run this BEFORE `npm uninstall -g @plexor-dev/claude-code-plugin-staging`. Pass `--remove-config` to also remove ~/.plexor/ config directory.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node ~/.claude/plugins/plexor/commands/plexor-uninstall.js $ARGUMENTS 2>&1 || plexor-uninstall $ARGUMENTS 2>&1
|
|
12
|
+
```
|