@plexor-dev/claude-code-plugin 0.1.0-beta.32 → 0.1.0-beta.34
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 +1 -1
- package/commands/plexor-enabled.js +60 -0
- package/commands/plexor-enabled.md +2 -21
- package/commands/plexor-login.js +67 -5
- package/commands/plexor-login.md +3 -19
- package/commands/plexor-logout.md +2 -20
- package/commands/plexor-setup.md +5 -2
- package/commands/plexor-status.js +64 -7
- package/commands/plexor-status.md +1 -13
- package/commands/plexor-uninstall.js +293 -0
- package/commands/plexor-uninstall.md +12 -0
- package/hooks/intercept.js +17 -21
- package/hooks/track-response.js +2 -2
- package/lib/config.js +1 -1
- package/lib/settings-manager.js +56 -8
- package/package.json +3 -2
- package/scripts/postinstall.js +48 -0
- package/scripts/uninstall.js +123 -66
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ rm -rf ~/.plexor
|
|
|
106
106
|
## Support
|
|
107
107
|
|
|
108
108
|
- **Documentation**: https://plexor.dev/docs
|
|
109
|
-
- **Issues**: https://github.com/
|
|
109
|
+
- **Issues**: https://github.com/micoverde/plex-vc-fund-platform/issues
|
|
110
110
|
- **Email**: hello@plexor.dev
|
|
111
111
|
|
|
112
112
|
## License
|
|
@@ -94,6 +94,30 @@ function isValidApiKeyFormat(key) {
|
|
|
94
94
|
return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Check for state mismatch between config.json enabled flag and settings.json routing
|
|
99
|
+
* @param {boolean} configEnabled - enabled flag from config.json
|
|
100
|
+
* @param {boolean} routingActive - whether settings.json has Plexor routing configured
|
|
101
|
+
* @returns {Object|null} mismatch details or null if states are consistent
|
|
102
|
+
*/
|
|
103
|
+
function checkStateMismatch(configEnabled, routingActive) {
|
|
104
|
+
if (configEnabled && !routingActive) {
|
|
105
|
+
return {
|
|
106
|
+
type: 'config-enabled-routing-inactive',
|
|
107
|
+
message: 'Config shows enabled but Claude routing is not configured',
|
|
108
|
+
suggestion: 'Run /plexor-enabled true to sync and configure routing'
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (!configEnabled && routingActive) {
|
|
112
|
+
return {
|
|
113
|
+
type: 'config-disabled-routing-active',
|
|
114
|
+
message: 'Config shows disabled but Claude routing is active',
|
|
115
|
+
suggestion: 'Run /plexor-enabled false to sync and disable routing'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
function main() {
|
|
98
122
|
const args = process.argv.slice(2);
|
|
99
123
|
const config = loadConfig();
|
|
@@ -107,6 +131,10 @@ function main() {
|
|
|
107
131
|
if (args.length === 0) {
|
|
108
132
|
const status = currentEnabled ? '● Enabled' : '○ Disabled';
|
|
109
133
|
const routingStr = routingStatus.enabled ? '● Active' : '○ Inactive';
|
|
134
|
+
|
|
135
|
+
// Check for state mismatch between config enabled flag and routing status
|
|
136
|
+
const stateMismatch = checkStateMismatch(currentEnabled, routingStatus.enabled);
|
|
137
|
+
|
|
110
138
|
console.log(`┌─────────────────────────────────────────────┐`);
|
|
111
139
|
console.log(`│ Plexor Proxy Status │`);
|
|
112
140
|
console.log(`├─────────────────────────────────────────────┤`);
|
|
@@ -115,6 +143,12 @@ function main() {
|
|
|
115
143
|
if (routingStatus.enabled) {
|
|
116
144
|
console.log(`│ Endpoint: ${(routingStatus.isStaging ? 'Staging' : 'Production').padEnd(32)}│`);
|
|
117
145
|
}
|
|
146
|
+
if (stateMismatch) {
|
|
147
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
148
|
+
console.log(`│ ⚠ State mismatch detected: │`);
|
|
149
|
+
console.log(`│ ${stateMismatch.message.padEnd(42)}│`);
|
|
150
|
+
console.log(`│ ${stateMismatch.suggestion.padEnd(42)}│`);
|
|
151
|
+
}
|
|
118
152
|
console.log(`├─────────────────────────────────────────────┤`);
|
|
119
153
|
console.log(`│ Usage: │`);
|
|
120
154
|
console.log(`│ /plexor-enabled true - Enable proxy │`);
|
|
@@ -161,6 +195,19 @@ function main() {
|
|
|
161
195
|
const apiUrl = config.settings?.apiUrl || 'https://api.plexor.dev';
|
|
162
196
|
const useStaging = apiUrl.includes('staging');
|
|
163
197
|
routingUpdated = settingsManager.enablePlexorRouting(apiKey, { useStaging });
|
|
198
|
+
|
|
199
|
+
// Check if settings.json update failed
|
|
200
|
+
if (!routingUpdated) {
|
|
201
|
+
console.log(`┌─────────────────────────────────────────────┐`);
|
|
202
|
+
console.log(`│ ✗ Failed to Enable Plexor Routing │`);
|
|
203
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
204
|
+
console.log(`│ Could not update ~/.claude/settings.json │`);
|
|
205
|
+
console.log(`│ Config.json was updated but routing failed.│`);
|
|
206
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
207
|
+
console.log(`│ Check file permissions and try again. │`);
|
|
208
|
+
console.log(`└─────────────────────────────────────────────┘`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
164
211
|
}
|
|
165
212
|
} else {
|
|
166
213
|
// Update Plexor plugin config
|
|
@@ -172,6 +219,19 @@ function main() {
|
|
|
172
219
|
|
|
173
220
|
// Disable routing - remove env vars from settings.json
|
|
174
221
|
routingUpdated = settingsManager.disablePlexorRouting();
|
|
222
|
+
|
|
223
|
+
// Check if settings.json update failed
|
|
224
|
+
if (!routingUpdated) {
|
|
225
|
+
console.log(`┌─────────────────────────────────────────────┐`);
|
|
226
|
+
console.log(`│ ✗ Failed to Disable Plexor Routing │`);
|
|
227
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
228
|
+
console.log(`│ Could not update ~/.claude/settings.json │`);
|
|
229
|
+
console.log(`│ Config.json was updated but routing failed.│`);
|
|
230
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
231
|
+
console.log(`│ Check file permissions and try again. │`);
|
|
232
|
+
console.log(`└─────────────────────────────────────────────┘`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
175
235
|
}
|
|
176
236
|
|
|
177
237
|
// Show error if no API key when enabling
|
|
@@ -2,27 +2,8 @@
|
|
|
2
2
|
description: Enable or disable Plexor proxy (routes all traffic through Plexor API) (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Run this command to view or toggle the Plexor proxy:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
node ~/.claude/plugins/plexor/commands/plexor-enabled.js
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
To enable or disable, pass an argument:
|
|
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.**
|
|
14
6
|
|
|
15
7
|
```bash
|
|
16
|
-
node ~/.claude/plugins/plexor/commands/plexor-enabled.js
|
|
17
|
-
node ~/.claude/plugins/plexor/commands/plexor-enabled.js false
|
|
8
|
+
node ~/.claude/plugins/plexor/commands/plexor-enabled.js $ARGUMENTS
|
|
18
9
|
```
|
|
19
|
-
|
|
20
|
-
Use the Bash tool to execute this command.
|
|
21
|
-
|
|
22
|
-
**IMPORTANT**: After running this command and displaying the output, STOP. Do not:
|
|
23
|
-
- Read any files
|
|
24
|
-
- Explore the codebase
|
|
25
|
-
- Run additional commands
|
|
26
|
-
- Ask follow-up questions
|
|
27
|
-
|
|
28
|
-
The command output is the complete response. Simply show the output and wait for the user's next input.
|
package/commands/plexor-login.js
CHANGED
|
@@ -76,6 +76,18 @@ function saveConfig(config) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function isPlexorApiKey(token) {
|
|
80
|
+
return typeof token === 'string' && token.startsWith('plx_') && token.length >= 20;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isJwtToken(token) {
|
|
84
|
+
if (typeof token !== 'string' || !token.startsWith('eyJ')) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const parts = token.split('.');
|
|
88
|
+
return parts.length === 3 && parts.every(part => part.length > 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
function validateApiKey(apiUrl, apiKey) {
|
|
80
92
|
return new Promise((resolve, reject) => {
|
|
81
93
|
const url = new URL(`${apiUrl}/v1/user`);
|
|
@@ -119,6 +131,56 @@ function validateApiKey(apiUrl, apiKey) {
|
|
|
119
131
|
});
|
|
120
132
|
}
|
|
121
133
|
|
|
134
|
+
function validateJwtToken(apiUrl, jwtToken) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const url = new URL(`${apiUrl}/v1/auth/me`);
|
|
137
|
+
const isHttps = url.protocol === 'https:';
|
|
138
|
+
const lib = isHttps ? https : http;
|
|
139
|
+
|
|
140
|
+
const options = {
|
|
141
|
+
hostname: url.hostname,
|
|
142
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
143
|
+
path: url.pathname,
|
|
144
|
+
method: 'GET',
|
|
145
|
+
headers: {
|
|
146
|
+
'Authorization': `Bearer ${jwtToken}`
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const req = lib.request(options, (res) => {
|
|
151
|
+
let data = '';
|
|
152
|
+
res.on('data', chunk => data += chunk);
|
|
153
|
+
res.on('end', () => {
|
|
154
|
+
if (res.statusCode === 200) {
|
|
155
|
+
try {
|
|
156
|
+
resolve(JSON.parse(data));
|
|
157
|
+
} catch {
|
|
158
|
+
reject(new Error('Invalid response from server'));
|
|
159
|
+
}
|
|
160
|
+
} else if (res.statusCode === 401) {
|
|
161
|
+
reject(new Error('Invalid login token'));
|
|
162
|
+
} else {
|
|
163
|
+
reject(new Error(`Server error: ${res.statusCode}`));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
|
|
169
|
+
req.setTimeout(10000, () => {
|
|
170
|
+
req.destroy();
|
|
171
|
+
reject(new Error('Connection timeout'));
|
|
172
|
+
});
|
|
173
|
+
req.end();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validateCredential(apiUrl, credential) {
|
|
178
|
+
if (isJwtToken(credential)) {
|
|
179
|
+
return validateJwtToken(apiUrl, credential);
|
|
180
|
+
}
|
|
181
|
+
return validateApiKey(apiUrl, credential);
|
|
182
|
+
}
|
|
183
|
+
|
|
122
184
|
/**
|
|
123
185
|
* Read API key from stdin (for piped input)
|
|
124
186
|
* @returns {Promise<string>} The API key read from stdin
|
|
@@ -220,10 +282,10 @@ async function main() {
|
|
|
220
282
|
console.log('');
|
|
221
283
|
}
|
|
222
284
|
|
|
223
|
-
// Validate key
|
|
224
|
-
if (!apiKey
|
|
225
|
-
console.error(`Error: Invalid
|
|
226
|
-
console.error(`
|
|
285
|
+
// Validate credential format (Plexor API key OR login JWT)
|
|
286
|
+
if (!isPlexorApiKey(apiKey) && !isJwtToken(apiKey)) {
|
|
287
|
+
console.error(`Error: Invalid credential format`);
|
|
288
|
+
console.error(`Expected Plexor API key ("plx_...") or login token ("eyJ...")`);
|
|
227
289
|
process.exit(1);
|
|
228
290
|
}
|
|
229
291
|
|
|
@@ -232,7 +294,7 @@ async function main() {
|
|
|
232
294
|
console.log('Validating API key...');
|
|
233
295
|
|
|
234
296
|
try {
|
|
235
|
-
const user = await
|
|
297
|
+
const user = await validateCredential(apiUrl, apiKey);
|
|
236
298
|
|
|
237
299
|
config.auth = config.auth || {};
|
|
238
300
|
config.auth.api_key = apiKey;
|
package/commands/plexor-login.md
CHANGED
|
@@ -2,26 +2,10 @@
|
|
|
2
2
|
description: Authenticate with Plexor to enable optimization (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
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.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Pass the API key as argument (format: `plx_your_api_key_here`).
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
node ~/.claude/plugins/plexor/commands/plexor-login.js
|
|
10
|
+
node ~/.claude/plugins/plexor/commands/plexor-login.js $ARGUMENTS
|
|
11
11
|
```
|
|
12
|
-
|
|
13
|
-
To login with an API key:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
node ~/.claude/plugins/plexor/commands/plexor-login.js plx_your_api_key_here
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Use the Bash tool to execute this command.
|
|
20
|
-
|
|
21
|
-
**IMPORTANT**: After running this command and displaying the output, STOP. Do not:
|
|
22
|
-
- Read any files
|
|
23
|
-
- Explore the codebase
|
|
24
|
-
- Run additional commands
|
|
25
|
-
- Ask follow-up questions
|
|
26
|
-
|
|
27
|
-
The command output is the complete response. Simply show the output and wait for the user's next input.
|
|
@@ -2,26 +2,8 @@
|
|
|
2
2
|
description: Log out from Plexor and clear credentials (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Run this command to log out and clear credentials:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
node ~/.claude/plugins/plexor/commands/plexor-logout.js
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
To also clear the cache:
|
|
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.**
|
|
14
6
|
|
|
15
7
|
```bash
|
|
16
|
-
node ~/.claude/plugins/plexor/commands/plexor-logout.js
|
|
8
|
+
node ~/.claude/plugins/plexor/commands/plexor-logout.js $ARGUMENTS
|
|
17
9
|
```
|
|
18
|
-
|
|
19
|
-
Use the Bash tool to execute this command.
|
|
20
|
-
|
|
21
|
-
**IMPORTANT**: After running this command and displaying the output, STOP. Do not:
|
|
22
|
-
- Read any files
|
|
23
|
-
- Explore the codebase
|
|
24
|
-
- Run additional commands
|
|
25
|
-
- Ask follow-up questions
|
|
26
|
-
|
|
27
|
-
The command output is the complete response. Simply show the output and wait for the user's next input.
|
package/commands/plexor-setup.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
description: First-time setup wizard for Plexor with Claude Code (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
**RULE: Execute this workflow EXACTLY ONCE. After completing all steps, STOP. DO NOT restart the workflow. DO NOT re-execute any commands. DO NOT call any other tools.**
|
|
6
|
+
|
|
5
7
|
# Plexor Setup Wizard
|
|
6
8
|
|
|
7
9
|
Guide users through first-time Plexor setup. **No manual environment variable configuration required!**
|
|
@@ -165,8 +167,9 @@ Commands:
|
|
|
165
167
|
Changes take effect immediately in all Claude Code sessions!
|
|
166
168
|
```
|
|
167
169
|
|
|
168
|
-
**
|
|
170
|
+
**NOTES**:
|
|
169
171
|
- The `~/.claude/settings.json` env block is the KEY mechanism that routes Claude Code through Plexor
|
|
170
172
|
- ANTHROPIC_AUTH_TOKEN takes precedence over ANTHROPIC_API_KEY (use AUTH_TOKEN for the Plexor key)
|
|
171
173
|
- Changes take effect immediately - no shell restart needed
|
|
172
|
-
|
|
174
|
+
|
|
175
|
+
After completing all steps, STOP. DO NOT restart the workflow. DO NOT re-execute any commands.
|
|
@@ -34,6 +34,31 @@ function getRoutingStatus() {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Detect partial routing state where URL points to Plexor but auth is missing/invalid
|
|
39
|
+
* This can cause confusing auth errors for users
|
|
40
|
+
* @returns {Object} { partial: boolean, issue: string|null }
|
|
41
|
+
*/
|
|
42
|
+
function detectPartialState() {
|
|
43
|
+
try {
|
|
44
|
+
const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
|
|
45
|
+
const settings = JSON.parse(data);
|
|
46
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
|
|
47
|
+
const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
|
|
48
|
+
const isPlexorUrl = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
|
|
49
|
+
|
|
50
|
+
if (isPlexorUrl && !authToken) {
|
|
51
|
+
return { partial: true, issue: 'Plexor URL set but no auth token' };
|
|
52
|
+
}
|
|
53
|
+
if (isPlexorUrl && !authToken.startsWith('plx_')) {
|
|
54
|
+
return { partial: true, issue: 'Plexor URL set but auth token is not a Plexor key' };
|
|
55
|
+
}
|
|
56
|
+
return { partial: false, issue: null };
|
|
57
|
+
} catch {
|
|
58
|
+
return { partial: false, issue: null };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
function loadSessionStats() {
|
|
38
63
|
try {
|
|
39
64
|
const data = fs.readFileSync(SESSION_PATH, 'utf8');
|
|
@@ -101,6 +126,30 @@ function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
|
|
|
101
126
|
return null;
|
|
102
127
|
}
|
|
103
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Check for state mismatch between config.json enabled flag and settings.json routing
|
|
131
|
+
* @param {boolean} configEnabled - enabled flag from config.json
|
|
132
|
+
* @param {boolean} routingActive - whether settings.json has Plexor routing configured
|
|
133
|
+
* @returns {Object|null} mismatch details or null if states are consistent
|
|
134
|
+
*/
|
|
135
|
+
function checkStateMismatch(configEnabled, routingActive) {
|
|
136
|
+
if (configEnabled && !routingActive) {
|
|
137
|
+
return {
|
|
138
|
+
type: 'config-enabled-routing-inactive',
|
|
139
|
+
message: 'Config shows enabled but Claude routing is not configured',
|
|
140
|
+
suggestion: 'Run /plexor-enabled true to sync and configure routing'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!configEnabled && routingActive) {
|
|
144
|
+
return {
|
|
145
|
+
type: 'config-disabled-routing-active',
|
|
146
|
+
message: 'Config shows disabled but Claude routing is active',
|
|
147
|
+
suggestion: 'Run /plexor-enabled false to sync and disable routing'
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
104
153
|
async function main() {
|
|
105
154
|
// Read config with integrity checking
|
|
106
155
|
const config = loadConfig();
|
|
@@ -221,14 +270,22 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
|
|
|
221
270
|
const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
|
|
222
271
|
const envLabel = routing.isStaging ? '(staging)' : '(production)';
|
|
223
272
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
273
|
+
// Note: Environment mismatch warning removed - it caused false positives during
|
|
274
|
+
// concurrent operations and transient states. The partial state and config/routing
|
|
275
|
+
// mismatch warnings below provide more actionable feedback.
|
|
276
|
+
|
|
277
|
+
// Check for partial routing state (Plexor URL without valid auth)
|
|
278
|
+
const partialState = detectPartialState();
|
|
279
|
+
if (partialState.partial) {
|
|
280
|
+
console.log(` ⚠ PARTIAL STATE DETECTED: ${partialState.issue}`);
|
|
281
|
+
console.log(` Run /plexor-login to fix, or /plexor-logout to disable routing\n`);
|
|
282
|
+
}
|
|
229
283
|
|
|
230
|
-
|
|
231
|
-
|
|
284
|
+
// Check for state mismatch between config enabled flag and routing status
|
|
285
|
+
const stateMismatch = checkStateMismatch(enabled, routing.active);
|
|
286
|
+
if (stateMismatch) {
|
|
287
|
+
console.log(` ⚠ State mismatch: ${stateMismatch.message}`);
|
|
288
|
+
console.log(` └─ ${stateMismatch.suggestion}\n`);
|
|
232
289
|
}
|
|
233
290
|
|
|
234
291
|
console.log(` ┌─────────────────────────────────────────────┐
|
|
@@ -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,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plexor Uninstall Command
|
|
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
|
+
// Check if Plexor routing is active
|
|
103
|
+
const baseUrl = settings.env.ANTHROPIC_BASE_URL || '';
|
|
104
|
+
const isPlexorRouting = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
|
|
105
|
+
|
|
106
|
+
if (!isPlexorRouting) {
|
|
107
|
+
return { success: true, message: 'Plexor routing not active' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Remove Plexor env vars
|
|
111
|
+
delete settings.env.ANTHROPIC_BASE_URL;
|
|
112
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
113
|
+
|
|
114
|
+
// Clean up empty env block
|
|
115
|
+
if (Object.keys(settings.env).length === 0) {
|
|
116
|
+
delete settings.env;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Atomic write
|
|
120
|
+
const crypto = require('crypto');
|
|
121
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
122
|
+
const tempPath = path.join(CLAUDE_DIR, `.settings.${tempId}.tmp`);
|
|
123
|
+
|
|
124
|
+
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
125
|
+
fs.renameSync(tempPath, settingsPath);
|
|
126
|
+
|
|
127
|
+
return { success: true, message: 'Routing disabled' };
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return { success: false, message: err.message };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove slash command files
|
|
135
|
+
*/
|
|
136
|
+
function removeSlashCommands() {
|
|
137
|
+
let removed = 0;
|
|
138
|
+
let restored = 0;
|
|
139
|
+
|
|
140
|
+
for (const cmd of PLEXOR_COMMANDS) {
|
|
141
|
+
const cmdPath = path.join(CLAUDE_COMMANDS_DIR, cmd);
|
|
142
|
+
const backupPath = cmdPath + '.backup';
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
if (fs.existsSync(cmdPath)) {
|
|
146
|
+
fs.unlinkSync(cmdPath);
|
|
147
|
+
removed++;
|
|
148
|
+
|
|
149
|
+
// Restore backup if exists
|
|
150
|
+
if (fs.existsSync(backupPath)) {
|
|
151
|
+
fs.renameSync(backupPath, cmdPath);
|
|
152
|
+
restored++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// Continue with other files
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { removed, restored };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Remove plugin directory
|
|
165
|
+
*/
|
|
166
|
+
function removePluginDirectory() {
|
|
167
|
+
try {
|
|
168
|
+
if (fs.existsSync(CLAUDE_PLUGINS_DIR)) {
|
|
169
|
+
fs.rmSync(CLAUDE_PLUGINS_DIR, { recursive: true, force: true });
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Remove config directory
|
|
180
|
+
*/
|
|
181
|
+
function removeConfigDirectory() {
|
|
182
|
+
try {
|
|
183
|
+
if (fs.existsSync(PLEXOR_CONFIG_DIR)) {
|
|
184
|
+
fs.rmSync(PLEXOR_CONFIG_DIR, { recursive: true, force: true });
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function main() {
|
|
194
|
+
const args = process.argv.slice(2);
|
|
195
|
+
|
|
196
|
+
// Handle help flag
|
|
197
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
198
|
+
console.log('');
|
|
199
|
+
console.log(' Usage: plexor-uninstall [options]');
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log(' Cleans up Plexor integration before npm uninstall.');
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log(' Options:');
|
|
204
|
+
console.log(' --remove-config, -c Also remove ~/.plexor/ config directory');
|
|
205
|
+
console.log(' --quiet, -q Suppress output messages');
|
|
206
|
+
console.log(' --help, -h Show this help message');
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(' After running this command:');
|
|
209
|
+
console.log(' npm uninstall -g @plexor-dev/claude-code-plugin');
|
|
210
|
+
console.log('');
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const removeConfig = args.includes('--remove-config') || args.includes('-c');
|
|
215
|
+
const quiet = args.includes('--quiet') || args.includes('-q');
|
|
216
|
+
|
|
217
|
+
if (!quiet) {
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(' Plexor Uninstall - Cleaning up...');
|
|
220
|
+
console.log('');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 1. Remove routing from ~/.claude/settings.json
|
|
224
|
+
let routingResult;
|
|
225
|
+
const settingsManager = loadSettingsManager();
|
|
226
|
+
|
|
227
|
+
if (settingsManager) {
|
|
228
|
+
try {
|
|
229
|
+
const success = settingsManager.disablePlexorRouting();
|
|
230
|
+
routingResult = { success, message: success ? 'Routing disabled via manager' : 'Already clean' };
|
|
231
|
+
} catch (err) {
|
|
232
|
+
routingResult = disableRoutingManually();
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
routingResult = disableRoutingManually();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!quiet) {
|
|
239
|
+
console.log(routingResult.success
|
|
240
|
+
? ' ✓ Removed Plexor routing from Claude settings'
|
|
241
|
+
: ` ✗ Failed to remove routing: ${routingResult.message}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2. Remove slash command .md files
|
|
245
|
+
const cmdResult = removeSlashCommands();
|
|
246
|
+
if (!quiet) {
|
|
247
|
+
console.log(` ✓ Removed ${cmdResult.removed} slash command files`);
|
|
248
|
+
if (cmdResult.restored > 0) {
|
|
249
|
+
console.log(` ✓ Restored ${cmdResult.restored} backed-up files`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 3. Remove plugin directory
|
|
254
|
+
const pluginRemoved = removePluginDirectory();
|
|
255
|
+
if (!quiet) {
|
|
256
|
+
console.log(pluginRemoved
|
|
257
|
+
? ' ✓ Removed plugin directory'
|
|
258
|
+
: ' ○ Plugin directory not found (already clean)');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 4. Optionally remove config directory
|
|
262
|
+
if (removeConfig) {
|
|
263
|
+
const configRemoved = removeConfigDirectory();
|
|
264
|
+
if (!quiet) {
|
|
265
|
+
console.log(configRemoved
|
|
266
|
+
? ' ✓ Removed ~/.plexor config directory'
|
|
267
|
+
: ' ○ Config directory not found');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Show next steps
|
|
272
|
+
if (!quiet) {
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
|
275
|
+
console.log(' │ Cleanup complete! Now run: │');
|
|
276
|
+
console.log(' │ │');
|
|
277
|
+
console.log(' │ npm uninstall -g @plexor-dev/claude-code-plugin │');
|
|
278
|
+
console.log(' │ │');
|
|
279
|
+
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
280
|
+
console.log('');
|
|
281
|
+
console.log(' Your Claude Code is ready to work normally again.');
|
|
282
|
+
console.log('');
|
|
283
|
+
|
|
284
|
+
if (!removeConfig) {
|
|
285
|
+
console.log(' Note: ~/.plexor/ config directory was preserved.');
|
|
286
|
+
console.log(' To also remove it: plexor-uninstall --remove-config');
|
|
287
|
+
console.log(' Or manually: rm -rf ~/.plexor');
|
|
288
|
+
console.log('');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
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`. 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
|
+
```
|
package/hooks/intercept.js
CHANGED
|
@@ -106,9 +106,9 @@ try {
|
|
|
106
106
|
const saveSession = (s) => {
|
|
107
107
|
try {
|
|
108
108
|
const dir = path.dirname(SESSION_PATH);
|
|
109
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
109
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
110
110
|
s.last_activity = Date.now();
|
|
111
|
-
fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2));
|
|
111
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2), { mode: 0o600 });
|
|
112
112
|
} catch {}
|
|
113
113
|
};
|
|
114
114
|
|
|
@@ -179,6 +179,17 @@ async function main() {
|
|
|
179
179
|
input = await readStdin();
|
|
180
180
|
request = JSON.parse(input);
|
|
181
181
|
|
|
182
|
+
// Issue #2042: Check for slash commands FIRST, before ANY other processing
|
|
183
|
+
// Slash commands must pass through completely clean — no metadata injection
|
|
184
|
+
// Adding _plexor_client or _plexor to slash command requests adds context noise
|
|
185
|
+
// that causes the model to re-execute commands in a loop
|
|
186
|
+
// Note: session.recordPassthrough() intentionally omitted — slash commands are
|
|
187
|
+
// not API requests and should not pollute session analytics
|
|
188
|
+
if (isSlashCommand(request)) {
|
|
189
|
+
logger.debug('Slash command detected, clean passthrough (no metadata)');
|
|
190
|
+
return output(request); // Completely clean — no metadata added
|
|
191
|
+
}
|
|
192
|
+
|
|
182
193
|
// Phase 3 Hypervisor Mode Detection
|
|
183
194
|
// When ANTHROPIC_BASE_URL points to Plexor, all intelligence is server-side
|
|
184
195
|
// The plugin just passes through - server handles optimization, routing, quality
|
|
@@ -202,25 +213,6 @@ async function main() {
|
|
|
202
213
|
});
|
|
203
214
|
}
|
|
204
215
|
|
|
205
|
-
// CRITICAL: Check for slash commands FIRST (before agentic check)
|
|
206
|
-
// Slash commands like /plexor-status should pass through unchanged
|
|
207
|
-
// Must check before isAgenticRequest since all Claude Code requests have tools
|
|
208
|
-
if (isSlashCommand(request)) {
|
|
209
|
-
logger.debug('Slash command detected, passing through unchanged');
|
|
210
|
-
session.recordPassthrough();
|
|
211
|
-
return output({
|
|
212
|
-
...request,
|
|
213
|
-
plexor_cwd: process.cwd(),
|
|
214
|
-
_plexor: {
|
|
215
|
-
request_id: generateRequestId('slash'), // Issue #701: Add request_id for tracking
|
|
216
|
-
source: 'passthrough_slash_command',
|
|
217
|
-
reason: 'slash_command_detected',
|
|
218
|
-
cwd: process.cwd(),
|
|
219
|
-
latency_ms: Date.now() - startTime
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
216
|
// CRITICAL: Skip optimization for CLI commands requiring tool execution
|
|
225
217
|
// Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
|
|
226
218
|
if (requiresToolExecution(request)) {
|
|
@@ -578,12 +570,16 @@ function isSlashCommand(request) {
|
|
|
578
570
|
}
|
|
579
571
|
|
|
580
572
|
// Check for system messages with skill instructions
|
|
573
|
+
// Issue #2042: Updated to match new RULE-based .md format (old H1 headers removed)
|
|
581
574
|
for (const msg of messages) {
|
|
582
575
|
if (msg.role === 'system') {
|
|
583
576
|
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
584
577
|
if (/# Plexor (?:Status|Login|Logout|Mode|Provider|Enabled|Settings)/i.test(content)) {
|
|
585
578
|
return true;
|
|
586
579
|
}
|
|
580
|
+
if (/plexor\/commands\/plexor-/i.test(content)) {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
587
583
|
}
|
|
588
584
|
}
|
|
589
585
|
|
package/hooks/track-response.js
CHANGED
|
@@ -85,10 +85,10 @@ try {
|
|
|
85
85
|
save(session) {
|
|
86
86
|
try {
|
|
87
87
|
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
88
|
-
fs.mkdirSync(PLEXOR_DIR, { recursive: true });
|
|
88
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
89
89
|
}
|
|
90
90
|
session.last_activity = Date.now();
|
|
91
|
-
fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2));
|
|
91
|
+
fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
92
92
|
} catch {}
|
|
93
93
|
}
|
|
94
94
|
|
package/lib/config.js
CHANGED
package/lib/settings-manager.js
CHANGED
|
@@ -49,8 +49,14 @@ class ClaudeSettingsManager {
|
|
|
49
49
|
|
|
50
50
|
// Basic schema validation - must be an object
|
|
51
51
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const backupPath = this._backupCorruptedFile();
|
|
53
|
+
console.warn('');
|
|
54
|
+
console.warn('WARNING: Claude settings file has invalid format!');
|
|
55
|
+
if (backupPath) {
|
|
56
|
+
console.warn(` Corrupted file backed up to: ${backupPath}`);
|
|
57
|
+
}
|
|
58
|
+
console.warn(' Using default settings. Your previous settings may need manual recovery.');
|
|
59
|
+
console.warn('');
|
|
54
60
|
return {};
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -61,9 +67,14 @@ class ClaudeSettingsManager {
|
|
|
61
67
|
}
|
|
62
68
|
// JSON parse error or corrupted file
|
|
63
69
|
if (err instanceof SyntaxError) {
|
|
64
|
-
|
|
65
|
-
console.warn('
|
|
66
|
-
|
|
70
|
+
const backupPath = this._backupCorruptedFile();
|
|
71
|
+
console.warn('');
|
|
72
|
+
console.warn('WARNING: Claude settings file is corrupted (invalid JSON)!');
|
|
73
|
+
if (backupPath) {
|
|
74
|
+
console.warn(` Corrupted file backed up to: ${backupPath}`);
|
|
75
|
+
}
|
|
76
|
+
console.warn(' Using default settings. Your previous settings may need manual recovery.');
|
|
77
|
+
console.warn('');
|
|
67
78
|
return {};
|
|
68
79
|
}
|
|
69
80
|
// Permission error
|
|
@@ -77,17 +88,30 @@ class ClaudeSettingsManager {
|
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
/**
|
|
80
|
-
* Backup a corrupted settings file for debugging
|
|
91
|
+
* Backup a corrupted settings file with numbered suffix for debugging
|
|
92
|
+
* Creates settings.json.corrupted.1, .corrupted.2, etc. to preserve history
|
|
93
|
+
* @returns {string|null} path to backup file, or null if backup failed
|
|
81
94
|
*/
|
|
82
95
|
_backupCorruptedFile() {
|
|
83
96
|
try {
|
|
84
97
|
if (fs.existsSync(this.settingsPath)) {
|
|
85
|
-
|
|
98
|
+
// Find next available numbered backup
|
|
99
|
+
let backupNum = 1;
|
|
100
|
+
let backupPath;
|
|
101
|
+
while (true) {
|
|
102
|
+
backupPath = `${this.settingsPath}.corrupted.${backupNum}`;
|
|
103
|
+
if (!fs.existsSync(backupPath)) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
backupNum++;
|
|
107
|
+
}
|
|
86
108
|
fs.copyFileSync(this.settingsPath, backupPath);
|
|
109
|
+
return backupPath;
|
|
87
110
|
}
|
|
88
111
|
} catch {
|
|
89
|
-
// Ignore backup errors
|
|
112
|
+
// Ignore backup errors silently
|
|
90
113
|
}
|
|
114
|
+
return null;
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
/**
|
|
@@ -242,6 +266,30 @@ class ClaudeSettingsManager {
|
|
|
242
266
|
}
|
|
243
267
|
}
|
|
244
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Detect partial routing state where URL points to Plexor but auth is missing/invalid
|
|
271
|
+
* This can cause confusing auth errors for users
|
|
272
|
+
* @returns {Object} { partial: boolean, issue: string|null }
|
|
273
|
+
*/
|
|
274
|
+
detectPartialState() {
|
|
275
|
+
try {
|
|
276
|
+
const settings = this.load();
|
|
277
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
|
|
278
|
+
const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
|
|
279
|
+
const isPlexorUrl = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
|
|
280
|
+
|
|
281
|
+
if (isPlexorUrl && !authToken) {
|
|
282
|
+
return { partial: true, issue: 'Plexor URL set but no auth token' };
|
|
283
|
+
}
|
|
284
|
+
if (isPlexorUrl && !authToken.startsWith('plx_')) {
|
|
285
|
+
return { partial: true, issue: 'Plexor URL set but auth token is not a Plexor key' };
|
|
286
|
+
}
|
|
287
|
+
return { partial: false, issue: null };
|
|
288
|
+
} catch {
|
|
289
|
+
return { partial: false, issue: null };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
245
293
|
/**
|
|
246
294
|
* Update just the API key without changing other settings
|
|
247
295
|
* @param {string} apiKey - new Plexor API key
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plexor-dev/claude-code-plugin",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.34",
|
|
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": {
|
|
7
7
|
"plexor-status": "./commands/plexor-status.js",
|
|
8
8
|
"plexor-enabled": "./commands/plexor-enabled.js",
|
|
9
9
|
"plexor-login": "./commands/plexor-login.js",
|
|
10
|
-
"plexor-logout": "./commands/plexor-logout.js"
|
|
10
|
+
"plexor-logout": "./commands/plexor-logout.js",
|
|
11
|
+
"plexor-uninstall": "./commands/plexor-uninstall.js"
|
|
11
12
|
},
|
|
12
13
|
"scripts": {
|
|
13
14
|
"postinstall": "node scripts/postinstall.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -77,6 +77,51 @@ const PLEXOR_LIB_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'lib'
|
|
|
77
77
|
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
78
78
|
const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Check for orphaned Plexor routing in settings.json without valid config.
|
|
82
|
+
* This can happen if a previous uninstall was incomplete.
|
|
83
|
+
*/
|
|
84
|
+
function checkOrphanedRouting() {
|
|
85
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
86
|
+
if (!home) return;
|
|
87
|
+
|
|
88
|
+
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
89
|
+
const configPath = path.join(home, '.plexor', 'config.json');
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
93
|
+
|
|
94
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
95
|
+
const env = settings.env || {};
|
|
96
|
+
|
|
97
|
+
const hasPlexorUrl = env.ANTHROPIC_BASE_URL &&
|
|
98
|
+
env.ANTHROPIC_BASE_URL.includes('plexor');
|
|
99
|
+
|
|
100
|
+
if (hasPlexorUrl) {
|
|
101
|
+
// Check if there's a valid Plexor config
|
|
102
|
+
let hasValidConfig = false;
|
|
103
|
+
try {
|
|
104
|
+
if (fs.existsSync(configPath)) {
|
|
105
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
106
|
+
hasValidConfig = config.apiKey && config.apiKey.startsWith('plx_');
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {}
|
|
109
|
+
|
|
110
|
+
if (!hasValidConfig) {
|
|
111
|
+
console.log('\n Warning: Detected orphaned Plexor routing in Claude settings');
|
|
112
|
+
console.log(' This may be from a previous installation.\n');
|
|
113
|
+
console.log(' Run /plexor-login to reconfigure, or');
|
|
114
|
+
console.log(' Run /plexor-uninstall to clean up\n');
|
|
115
|
+
} else {
|
|
116
|
+
console.log('\n Existing Plexor configuration detected');
|
|
117
|
+
console.log(' Your previous settings have been preserved.\n');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Ignore errors in detection - don't break install
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
80
125
|
// Default configuration for new installs
|
|
81
126
|
// PRODUCTION PACKAGE - uses production API
|
|
82
127
|
const DEFAULT_CONFIG = {
|
|
@@ -94,6 +139,9 @@ const DEFAULT_CONFIG = {
|
|
|
94
139
|
};
|
|
95
140
|
|
|
96
141
|
function main() {
|
|
142
|
+
// Check for orphaned routing at start of postinstall
|
|
143
|
+
checkOrphanedRouting();
|
|
144
|
+
|
|
97
145
|
try {
|
|
98
146
|
// Get target user info for chown (if running with sudo)
|
|
99
147
|
const targetUser = getTargetUserIds();
|
package/scripts/uninstall.js
CHANGED
|
@@ -1,97 +1,154 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Plexor Claude Code Plugin - Uninstall Script
|
|
4
|
+
* Plexor Claude Code Plugin - Comprehensive Uninstall Script
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Runs on npm uninstall (when npm actually calls it).
|
|
7
|
+
* Also callable directly: node scripts/uninstall.js
|
|
8
|
+
*
|
|
9
|
+
* Performs complete cleanup:
|
|
10
|
+
* 1. Removes Plexor routing from ~/.claude/settings.json
|
|
11
|
+
* 2. Removes slash command files from ~/.claude/commands/
|
|
12
|
+
* 3. Removes plugin directory from ~/.claude/plugins/plexor/
|
|
13
|
+
* 4. Restores any backups if they exist
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
const fs = require('fs');
|
|
11
17
|
const path = require('path');
|
|
12
|
-
const os = require('os');
|
|
13
18
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} catch (err) {
|
|
20
|
-
// If settings manager can't be loaded during uninstall, continue anyway
|
|
21
|
-
settingsManager = null;
|
|
19
|
+
// Get home directory - support both Unix and Windows
|
|
20
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
21
|
+
if (!home) {
|
|
22
|
+
console.log('Warning: HOME not set, skipping cleanup');
|
|
23
|
+
process.exit(0);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log(' Plexor plugin cleanup...');
|
|
28
|
+
console.log('');
|
|
29
|
+
|
|
30
|
+
const results = {
|
|
31
|
+
routing: false,
|
|
32
|
+
commands: [],
|
|
33
|
+
restored: [],
|
|
34
|
+
pluginDir: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 1. Remove routing from settings.json
|
|
38
|
+
// This is CRITICAL - do NOT depend on settings-manager module since it may not load during uninstall
|
|
39
|
+
try {
|
|
40
|
+
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
41
|
+
if (fs.existsSync(settingsPath)) {
|
|
42
|
+
const data = fs.readFileSync(settingsPath, 'utf8');
|
|
43
|
+
if (data && data.trim()) {
|
|
44
|
+
const settings = JSON.parse(data);
|
|
45
|
+
if (settings.env) {
|
|
46
|
+
const hadBaseUrl = !!settings.env.ANTHROPIC_BASE_URL;
|
|
47
|
+
const hadAuthToken = !!settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
48
|
+
|
|
49
|
+
delete settings.env.ANTHROPIC_BASE_URL;
|
|
50
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
51
|
+
|
|
52
|
+
// Clean up empty env block
|
|
53
|
+
if (Object.keys(settings.env).length === 0) {
|
|
54
|
+
delete settings.env;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (hadBaseUrl || hadAuthToken) {
|
|
58
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
59
|
+
results.routing = true;
|
|
60
|
+
}
|
|
37
61
|
}
|
|
38
62
|
}
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.log(` Warning: Could not clean settings.json: ${e.message}`);
|
|
66
|
+
}
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
// 2. Remove slash command files
|
|
69
|
+
// These are the Plexor-specific command files that get installed to ~/.claude/commands/
|
|
70
|
+
const plexorCommands = [
|
|
71
|
+
'plexor-config.md',
|
|
72
|
+
'plexor-enabled.md',
|
|
73
|
+
'plexor-login.md',
|
|
74
|
+
'plexor-logout.md',
|
|
75
|
+
'plexor-mode.md',
|
|
76
|
+
'plexor-provider.md',
|
|
77
|
+
'plexor-settings.md',
|
|
78
|
+
'plexor-setup.md',
|
|
79
|
+
'plexor-status.md',
|
|
80
|
+
'plexor-uninstall.md'
|
|
81
|
+
];
|
|
46
82
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
try {
|
|
84
|
+
const commandsDir = path.join(home, '.claude', 'commands');
|
|
85
|
+
if (fs.existsSync(commandsDir)) {
|
|
86
|
+
for (const cmd of plexorCommands) {
|
|
87
|
+
const cmdPath = path.join(commandsDir, cmd);
|
|
88
|
+
const backupPath = cmdPath + '.backup';
|
|
50
89
|
|
|
51
|
-
if (fs.existsSync(
|
|
52
|
-
fs.unlinkSync(
|
|
53
|
-
|
|
90
|
+
if (fs.existsSync(cmdPath)) {
|
|
91
|
+
fs.unlinkSync(cmdPath);
|
|
92
|
+
results.commands.push(cmd.replace('.md', ''));
|
|
54
93
|
|
|
55
94
|
// Restore backup if it exists
|
|
56
95
|
if (fs.existsSync(backupPath)) {
|
|
57
|
-
fs.renameSync(backupPath,
|
|
58
|
-
restored.push(
|
|
96
|
+
fs.renameSync(backupPath, cmdPath);
|
|
97
|
+
results.restored.push(cmd);
|
|
59
98
|
}
|
|
60
99
|
}
|
|
61
100
|
}
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.log(` Warning: Could not clean commands: ${e.message}`);
|
|
104
|
+
}
|
|
62
105
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
// 3. Remove plugin directory
|
|
107
|
+
try {
|
|
108
|
+
const pluginDir = path.join(home, '.claude', 'plugins', 'plexor');
|
|
109
|
+
if (fs.existsSync(pluginDir)) {
|
|
110
|
+
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
111
|
+
results.pluginDir = true;
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.log(` Warning: Could not remove plugin directory: ${e.message}`);
|
|
115
|
+
}
|
|
67
116
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
117
|
+
// Output results
|
|
118
|
+
if (results.routing || results.commands.length > 0 || results.pluginDir) {
|
|
119
|
+
console.log(' Plexor plugin uninstalled');
|
|
120
|
+
console.log('');
|
|
73
121
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
if (results.routing) {
|
|
123
|
+
console.log(' Removed Plexor routing from Claude settings');
|
|
124
|
+
console.log(' (Claude Code now connects directly to Anthropic)');
|
|
125
|
+
console.log('');
|
|
126
|
+
}
|
|
78
127
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
if (results.commands.length > 0) {
|
|
129
|
+
console.log(' Removed commands:');
|
|
130
|
+
results.commands.forEach(cmd => console.log(` /${cmd}`));
|
|
131
|
+
console.log('');
|
|
132
|
+
}
|
|
84
133
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
134
|
+
if (results.restored.length > 0) {
|
|
135
|
+
console.log(' Restored from backup:');
|
|
136
|
+
results.restored.forEach(f => console.log(` ${f}`));
|
|
137
|
+
console.log('');
|
|
138
|
+
}
|
|
90
139
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.
|
|
140
|
+
if (results.pluginDir) {
|
|
141
|
+
console.log(' Removed plugin directory');
|
|
142
|
+
console.log('');
|
|
94
143
|
}
|
|
144
|
+
|
|
145
|
+
console.log(' Note: ~/.plexor/ config directory was preserved.');
|
|
146
|
+
console.log(' To remove it: rm -rf ~/.plexor');
|
|
147
|
+
console.log('');
|
|
148
|
+
} else {
|
|
149
|
+
console.log(' No Plexor components found to clean up.');
|
|
150
|
+
console.log('');
|
|
95
151
|
}
|
|
96
152
|
|
|
97
|
-
|
|
153
|
+
console.log(' Cleanup complete');
|
|
154
|
+
console.log('');
|