@plexor-dev/claude-code-plugin-staging 0.1.0-beta.1 → 0.1.0-beta.11
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-enabled.js +177 -18
- package/commands/plexor-enabled.md +32 -12
- package/commands/plexor-login.js +119 -25
- package/commands/plexor-logout.js +62 -11
- package/commands/plexor-setup.md +4 -4
- package/commands/plexor-status.js +180 -14
- package/commands/plexor-uninstall.js +274 -0
- package/commands/plexor-uninstall.md +30 -0
- package/hooks/intercept.js +2 -2
- package/hooks/track-response.js +2 -2
- package/lib/config.js +1 -1
- package/lib/constants.js +19 -1
- package/lib/settings-manager.js +120 -7
- package/package.json +2 -5
- package/scripts/postinstall.js +96 -5
- package/scripts/uninstall.js +129 -42
- 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
- package/commands/plexor-provider.js +0 -110
- package/commands/plexor-provider.md +0 -27
- package/commands/plexor-settings.js +0 -155
- package/commands/plexor-settings.md +0 -28
package/commands/plexor-setup.md
CHANGED
|
@@ -61,7 +61,7 @@ If user selected "Yes, I have Claude MAX":
|
|
|
61
61
|
},
|
|
62
62
|
"settings": {
|
|
63
63
|
"enabled": true,
|
|
64
|
-
"apiUrl": "https://api.plexor.dev",
|
|
64
|
+
"apiUrl": "https://staging.api.plexor.dev",
|
|
65
65
|
"mode": "balanced",
|
|
66
66
|
"localCacheEnabled": true
|
|
67
67
|
}
|
|
@@ -72,7 +72,7 @@ If user selected "Yes, I have Claude MAX":
|
|
|
72
72
|
```json
|
|
73
73
|
{
|
|
74
74
|
"env": {
|
|
75
|
-
"ANTHROPIC_BASE_URL": "https://api.plexor.dev/gateway/anthropic",
|
|
75
|
+
"ANTHROPIC_BASE_URL": "https://staging.api.plexor.dev/gateway/anthropic",
|
|
76
76
|
"ANTHROPIC_AUTH_TOKEN": "[user's Plexor key]"
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -122,7 +122,7 @@ If user selected "No, I'll use a Plexor API key":
|
|
|
122
122
|
},
|
|
123
123
|
"settings": {
|
|
124
124
|
"enabled": true,
|
|
125
|
-
"apiUrl": "https://api.plexor.dev",
|
|
125
|
+
"apiUrl": "https://staging.api.plexor.dev",
|
|
126
126
|
"preferred_provider": "auto",
|
|
127
127
|
"mode": "balanced",
|
|
128
128
|
"localCacheEnabled": true
|
|
@@ -134,7 +134,7 @@ If user selected "No, I'll use a Plexor API key":
|
|
|
134
134
|
```json
|
|
135
135
|
{
|
|
136
136
|
"env": {
|
|
137
|
-
"ANTHROPIC_BASE_URL": "https://api.plexor.dev/gateway/anthropic",
|
|
137
|
+
"ANTHROPIC_BASE_URL": "https://staging.api.plexor.dev/gateway/anthropic",
|
|
138
138
|
"ANTHROPIC_AUTH_TOKEN": "[user's Plexor key]"
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -9,10 +9,9 @@ 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
|
-
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
12
|
+
// Import centralized constants with HOME directory validation
|
|
13
|
+
const { HOME_DIR, CONFIG_PATH, SESSION_PATH, SESSION_TIMEOUT_MS } = require('../lib/constants');
|
|
14
|
+
const CLAUDE_SETTINGS_PATH = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Check if Claude Code is actually routing through Plexor
|
|
@@ -35,6 +34,31 @@ function getRoutingStatus() {
|
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
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
|
+
|
|
38
62
|
function loadSessionStats() {
|
|
39
63
|
try {
|
|
40
64
|
const data = fs.readFileSync(SESSION_PATH, 'utf8');
|
|
@@ -49,13 +73,87 @@ function loadSessionStats() {
|
|
|
49
73
|
}
|
|
50
74
|
}
|
|
51
75
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Validate API key format
|
|
78
|
+
* @param {string} key - API key to validate
|
|
79
|
+
* @returns {boolean} true if valid format
|
|
80
|
+
*/
|
|
81
|
+
function isValidApiKeyFormat(key) {
|
|
82
|
+
return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Load config file with integrity checking
|
|
87
|
+
* @returns {Object|null} config object or null if invalid
|
|
88
|
+
*/
|
|
89
|
+
function loadConfig() {
|
|
55
90
|
try {
|
|
91
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
56
94
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
57
|
-
|
|
95
|
+
if (!data || data.trim() === '') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const config = JSON.parse(data);
|
|
99
|
+
if (typeof config !== 'object' || config === null) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return config;
|
|
58
103
|
} catch (err) {
|
|
104
|
+
if (err instanceof SyntaxError) {
|
|
105
|
+
console.log('Config file is corrupted. Run /plexor-login to reconfigure.');
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check for environment mismatch between config and routing
|
|
113
|
+
*/
|
|
114
|
+
function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
|
|
115
|
+
if (!configApiUrl || !routingBaseUrl) return null;
|
|
116
|
+
|
|
117
|
+
const configIsStaging = configApiUrl.includes('staging');
|
|
118
|
+
const routingIsStaging = routingBaseUrl.includes('staging');
|
|
119
|
+
|
|
120
|
+
if (configIsStaging !== routingIsStaging) {
|
|
121
|
+
return {
|
|
122
|
+
config: configIsStaging ? 'staging' : 'production',
|
|
123
|
+
routing: routingIsStaging ? 'staging' : 'production'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
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
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
// Read config with integrity checking
|
|
155
|
+
const config = loadConfig();
|
|
156
|
+
if (!config) {
|
|
59
157
|
console.log('Not configured. Run /plexor-login first.');
|
|
60
158
|
process.exit(1);
|
|
61
159
|
}
|
|
@@ -72,6 +170,13 @@ async function main() {
|
|
|
72
170
|
process.exit(1);
|
|
73
171
|
}
|
|
74
172
|
|
|
173
|
+
// Validate API key format
|
|
174
|
+
if (!isValidApiKeyFormat(apiKey)) {
|
|
175
|
+
console.log('Invalid API key format. Keys must start with "plx_" and be at least 20 characters.');
|
|
176
|
+
console.log('Run /plexor-login with a valid API key.');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
75
180
|
// Fetch user info and stats
|
|
76
181
|
let user = { email: 'Unknown', tier: { name: 'Free', limits: {} } };
|
|
77
182
|
let stats = { period: {}, summary: {} };
|
|
@@ -165,6 +270,30 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
|
|
|
165
270
|
const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
|
|
166
271
|
const envLabel = routing.isStaging ? '(staging)' : '(production)';
|
|
167
272
|
|
|
273
|
+
// Check for environment mismatch
|
|
274
|
+
const envMismatch = checkEnvironmentMismatch(apiUrl, routing.baseUrl);
|
|
275
|
+
const mismatchWarning = envMismatch
|
|
276
|
+
? ` ⚠ Warning: Config uses ${envMismatch.config} but routing is ${envMismatch.routing}\n`
|
|
277
|
+
: '';
|
|
278
|
+
|
|
279
|
+
if (mismatchWarning) {
|
|
280
|
+
console.log(mismatchWarning);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check for partial routing state (Plexor URL without valid auth)
|
|
284
|
+
const partialState = detectPartialState();
|
|
285
|
+
if (partialState.partial) {
|
|
286
|
+
console.log(` ⚠ PARTIAL STATE DETECTED: ${partialState.issue}`);
|
|
287
|
+
console.log(` Run /plexor-login to fix, or /plexor-logout to disable routing\n`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check for state mismatch between config enabled flag and routing status
|
|
291
|
+
const stateMismatch = checkStateMismatch(enabled, routing.active);
|
|
292
|
+
if (stateMismatch) {
|
|
293
|
+
console.log(` ⚠ State mismatch: ${stateMismatch.message}`);
|
|
294
|
+
console.log(` └─ ${stateMismatch.suggestion}\n`);
|
|
295
|
+
}
|
|
296
|
+
|
|
168
297
|
console.log(` ┌─────────────────────────────────────────────┐
|
|
169
298
|
${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
|
|
170
299
|
├─────────────────────────────────────────────┤
|
|
@@ -192,7 +321,8 @@ ${line('Settings')}
|
|
|
192
321
|
${line(`├── Optimization: ${optEnabled}`)}
|
|
193
322
|
${line(`├── Local cache: ${cacheEnabled}`)}
|
|
194
323
|
${line(`├── Mode: ${mode}`)}
|
|
195
|
-
${line(
|
|
324
|
+
${line(`├── Provider routing: ${provider}`)}
|
|
325
|
+
${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https://', '').substring(0, 30) : 'not configured'}`)}
|
|
196
326
|
└─────────────────────────────────────────────┘
|
|
197
327
|
|
|
198
328
|
Dashboard: ${dashboardUrl}
|
|
@@ -201,7 +331,13 @@ ${line(`└── Provider routing: ${provider}`)}
|
|
|
201
331
|
|
|
202
332
|
function fetchJson(apiUrl, endpoint, apiKey) {
|
|
203
333
|
return new Promise((resolve, reject) => {
|
|
204
|
-
|
|
334
|
+
let url;
|
|
335
|
+
try {
|
|
336
|
+
url = new URL(`${apiUrl}${endpoint}`);
|
|
337
|
+
} catch {
|
|
338
|
+
reject(new Error('Invalid API URL'));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
205
341
|
|
|
206
342
|
const options = {
|
|
207
343
|
hostname: url.hostname,
|
|
@@ -217,18 +353,48 @@ function fetchJson(apiUrl, endpoint, apiKey) {
|
|
|
217
353
|
let data = '';
|
|
218
354
|
res.on('data', chunk => data += chunk);
|
|
219
355
|
res.on('end', () => {
|
|
356
|
+
// Check HTTP status code first
|
|
357
|
+
if (res.statusCode === 401) {
|
|
358
|
+
reject(new Error('Invalid API key'));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (res.statusCode === 403) {
|
|
362
|
+
reject(new Error('Access denied'));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (res.statusCode >= 500) {
|
|
366
|
+
reject(new Error('Server error'));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (res.statusCode !== 200) {
|
|
370
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Check for empty response
|
|
375
|
+
if (!data || data.trim() === '') {
|
|
376
|
+
reject(new Error('Empty response'));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Parse JSON
|
|
220
381
|
try {
|
|
221
|
-
|
|
382
|
+
const parsed = JSON.parse(data);
|
|
383
|
+
if (parsed === null) {
|
|
384
|
+
reject(new Error('Null response'));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
resolve(parsed);
|
|
222
388
|
} catch {
|
|
223
|
-
reject(new Error('Invalid response'));
|
|
389
|
+
reject(new Error('Invalid JSON response'));
|
|
224
390
|
}
|
|
225
391
|
});
|
|
226
392
|
});
|
|
227
393
|
|
|
228
|
-
req.on('error', reject);
|
|
394
|
+
req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
|
|
229
395
|
req.setTimeout(5000, () => {
|
|
230
396
|
req.destroy();
|
|
231
|
-
reject(new Error('
|
|
397
|
+
reject(new Error('Request timeout'));
|
|
232
398
|
});
|
|
233
399
|
req.end();
|
|
234
400
|
});
|
|
@@ -0,0 +1,274 @@
|
|
|
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
|
+
// 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
|
+
const removeConfig = args.includes('--remove-config') || args.includes('-c');
|
|
196
|
+
const quiet = args.includes('--quiet') || args.includes('-q');
|
|
197
|
+
|
|
198
|
+
if (!quiet) {
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(' Plexor Uninstall (STAGING) - Cleaning up...');
|
|
201
|
+
console.log('');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 1. Remove routing from ~/.claude/settings.json
|
|
205
|
+
let routingResult;
|
|
206
|
+
const settingsManager = loadSettingsManager();
|
|
207
|
+
|
|
208
|
+
if (settingsManager) {
|
|
209
|
+
try {
|
|
210
|
+
const success = settingsManager.disablePlexorRouting();
|
|
211
|
+
routingResult = { success, message: success ? 'Routing disabled via manager' : 'Already clean' };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
routingResult = disableRoutingManually();
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
routingResult = disableRoutingManually();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!quiet) {
|
|
220
|
+
console.log(routingResult.success
|
|
221
|
+
? ' ✓ Removed Plexor routing from Claude settings'
|
|
222
|
+
: ` ✗ Failed to remove routing: ${routingResult.message}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 2. Remove slash command .md files
|
|
226
|
+
const cmdResult = removeSlashCommands();
|
|
227
|
+
if (!quiet) {
|
|
228
|
+
console.log(` ✓ Removed ${cmdResult.removed} slash command files`);
|
|
229
|
+
if (cmdResult.restored > 0) {
|
|
230
|
+
console.log(` ✓ Restored ${cmdResult.restored} backed-up files`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 3. Remove plugin directory
|
|
235
|
+
const pluginRemoved = removePluginDirectory();
|
|
236
|
+
if (!quiet) {
|
|
237
|
+
console.log(pluginRemoved
|
|
238
|
+
? ' ✓ Removed plugin directory'
|
|
239
|
+
: ' ○ Plugin directory not found (already clean)');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 4. Optionally remove config directory
|
|
243
|
+
if (removeConfig) {
|
|
244
|
+
const configRemoved = removeConfigDirectory();
|
|
245
|
+
if (!quiet) {
|
|
246
|
+
console.log(configRemoved
|
|
247
|
+
? ' ✓ Removed ~/.plexor config directory'
|
|
248
|
+
: ' ○ Config directory not found');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Show next steps
|
|
253
|
+
if (!quiet) {
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
|
256
|
+
console.log(' │ Cleanup complete! Now run: │');
|
|
257
|
+
console.log(' │ │');
|
|
258
|
+
console.log(' │ npm uninstall -g @plexor-dev/claude-code-plugin-staging │');
|
|
259
|
+
console.log(' │ │');
|
|
260
|
+
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(' Your Claude Code is ready to work normally again.');
|
|
263
|
+
console.log('');
|
|
264
|
+
|
|
265
|
+
if (!removeConfig) {
|
|
266
|
+
console.log(' Note: ~/.plexor/ config directory was preserved.');
|
|
267
|
+
console.log(' To also remove it: plexor-uninstall --remove-config');
|
|
268
|
+
console.log(' Or manually: rm -rf ~/.plexor');
|
|
269
|
+
console.log('');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
main();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plexor-uninstall
|
|
3
|
+
description: Clean up Plexor integration before uninstalling
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Runs comprehensive cleanup to prepare for npm uninstall.
|
|
7
|
+
|
|
8
|
+
**IMPORTANT**: Run this command BEFORE `npm uninstall -g @plexor-dev/claude-code-plugin-staging`
|
|
9
|
+
|
|
10
|
+
npm's preuninstall hook does NOT run for global package uninstalls, so this command
|
|
11
|
+
must be run manually to ensure proper cleanup.
|
|
12
|
+
|
|
13
|
+
This removes:
|
|
14
|
+
- Plexor routing from Claude settings (~/.claude/settings.json)
|
|
15
|
+
- Slash command files from ~/.claude/commands/
|
|
16
|
+
- Plugin directory ~/.claude/plugins/plexor/
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
- `--remove-config` or `-c`: Also remove ~/.plexor/ config directory
|
|
20
|
+
|
|
21
|
+
After running this command, run:
|
|
22
|
+
```bash
|
|
23
|
+
npm uninstall -g @plexor-dev/claude-code-plugin-staging
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
$ARGUMENTS: $ARGUMENTS
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node ~/.claude/plugins/plexor/commands/plexor-uninstall.js $ARGUMENTS 2>&1 || plexor-uninstall $ARGUMENTS 2>&1
|
|
30
|
+
```
|
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
|
|
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/constants.js
CHANGED
|
@@ -4,7 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Get the user's home directory with proper validation
|
|
9
|
+
* @returns {string} Home directory path
|
|
10
|
+
* @throws {Error} If HOME is not set or empty
|
|
11
|
+
*/
|
|
12
|
+
function getHomeDir() {
|
|
13
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
14
|
+
if (!home || home.trim() === '') {
|
|
15
|
+
console.error('Error: HOME environment variable is not set.');
|
|
16
|
+
console.error(' Please set HOME to your user directory before running Plexor commands.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
return home;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const HOME_DIR = getHomeDir();
|
|
23
|
+
const PLEXOR_DIR = path.join(HOME_DIR, '.plexor');
|
|
8
24
|
const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
|
|
9
25
|
const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
|
|
10
26
|
const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
|
|
@@ -16,6 +32,8 @@ const DEFAULT_API_URL = 'https://staging.api.plexor.dev';
|
|
|
16
32
|
const DEFAULT_TIMEOUT = 5000;
|
|
17
33
|
|
|
18
34
|
module.exports = {
|
|
35
|
+
getHomeDir,
|
|
36
|
+
HOME_DIR,
|
|
19
37
|
PLEXOR_DIR,
|
|
20
38
|
CONFIG_PATH,
|
|
21
39
|
SESSION_PATH,
|