@plexor-dev/claude-code-plugin-staging 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +117 -0
- package/commands/plexor-config.js +170 -0
- package/commands/plexor-config.md +28 -0
- package/commands/plexor-enabled.js +122 -0
- package/commands/plexor-enabled.md +28 -0
- package/commands/plexor-login.js +189 -0
- package/commands/plexor-login.md +27 -0
- package/commands/plexor-logout.js +92 -0
- package/commands/plexor-logout.md +27 -0
- package/commands/plexor-mode.js +107 -0
- package/commands/plexor-mode.md +27 -0
- package/commands/plexor-provider.js +110 -0
- package/commands/plexor-provider.md +27 -0
- package/commands/plexor-settings.js +155 -0
- package/commands/plexor-settings.md +28 -0
- package/commands/plexor-setup.md +172 -0
- package/commands/plexor-status.js +240 -0
- package/commands/plexor-status.md +21 -0
- package/hooks/intercept.js +634 -0
- package/hooks/track-response.js +376 -0
- package/lib/cache.js +107 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +26 -0
- package/lib/index.js +19 -0
- package/lib/logger.js +36 -0
- package/lib/plexor-client.js +122 -0
- package/lib/server-sync.js +237 -0
- package/lib/session.js +156 -0
- package/lib/settings-manager.js +239 -0
- package/package.json +60 -0
- package/scripts/plexor-cli.sh +48 -0
- package/scripts/postinstall.js +251 -0
- package/scripts/uninstall.js +67 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Settings Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages ~/.claude/settings.json to enable automatic Plexor routing.
|
|
5
|
+
*
|
|
6
|
+
* KEY DISCOVERY: Claude Code reads settings.json at runtime and the `env` block
|
|
7
|
+
* overrides environment variables. This allows the plugin to redirect ALL Claude
|
|
8
|
+
* Code sessions to Plexor without users manually setting environment variables.
|
|
9
|
+
*
|
|
10
|
+
* Reference: Claude Code v2.0.1+ behavior - settings.json env takes precedence
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
|
|
17
|
+
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
18
|
+
|
|
19
|
+
// Plexor gateway endpoints
|
|
20
|
+
const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
21
|
+
const PLEXOR_PROD_URL = 'https://api.plexor.dev/gateway/anthropic';
|
|
22
|
+
|
|
23
|
+
class ClaudeSettingsManager {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.settingsPath = SETTINGS_PATH;
|
|
26
|
+
this.claudeDir = CLAUDE_DIR;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load current Claude settings
|
|
31
|
+
* @returns {Object} settings object or empty object if not found
|
|
32
|
+
*/
|
|
33
|
+
load() {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(this.settingsPath)) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
const data = fs.readFileSync(this.settingsPath, 'utf8');
|
|
39
|
+
return JSON.parse(data);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// Return empty object if file doesn't exist or parse error
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Save Claude settings
|
|
48
|
+
* @param {Object} settings - settings object to save
|
|
49
|
+
* @returns {boolean} success status
|
|
50
|
+
*/
|
|
51
|
+
save(settings) {
|
|
52
|
+
try {
|
|
53
|
+
// Ensure .claude directory exists
|
|
54
|
+
if (!fs.existsSync(this.claudeDir)) {
|
|
55
|
+
fs.mkdirSync(this.claudeDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2));
|
|
59
|
+
return true;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('Failed to save Claude settings:', err.message);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Enable Plexor routing by setting env vars in settings.json
|
|
68
|
+
*
|
|
69
|
+
* This is the KEY mechanism: setting ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
|
|
70
|
+
* in the env block redirects ALL Claude Code sessions to Plexor automatically.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} apiKey - Plexor API key (plx_*)
|
|
73
|
+
* @param {Object} options - { useStaging: boolean }
|
|
74
|
+
* @returns {boolean} success status
|
|
75
|
+
*/
|
|
76
|
+
enablePlexorRouting(apiKey, options = {}) {
|
|
77
|
+
// STAGING PACKAGE - defaults to staging API
|
|
78
|
+
const { useStaging = true } = options;
|
|
79
|
+
const apiUrl = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const settings = this.load();
|
|
83
|
+
|
|
84
|
+
// Initialize env block if doesn't exist
|
|
85
|
+
if (!settings.env) {
|
|
86
|
+
settings.env = {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set the magic environment variables
|
|
90
|
+
// ANTHROPIC_AUTH_TOKEN has higher precedence than ANTHROPIC_API_KEY
|
|
91
|
+
settings.env.ANTHROPIC_BASE_URL = apiUrl;
|
|
92
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
93
|
+
|
|
94
|
+
const success = this.save(settings);
|
|
95
|
+
|
|
96
|
+
if (success) {
|
|
97
|
+
console.log(`✓ Plexor routing enabled`);
|
|
98
|
+
console.log(` Base URL: ${apiUrl}`);
|
|
99
|
+
console.log(` API Key: ${apiKey.substring(0, 12)}...`);
|
|
100
|
+
console.log(`\n All Claude Code sessions will now route through Plexor.`);
|
|
101
|
+
console.log(` Changes take effect immediately (no restart needed).`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return success;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error('Failed to enable Plexor routing:', err.message);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Disable Plexor routing by removing env vars from settings.json
|
|
113
|
+
*
|
|
114
|
+
* This restores direct connection to Anthropic API.
|
|
115
|
+
*
|
|
116
|
+
* @returns {boolean} success status
|
|
117
|
+
*/
|
|
118
|
+
disablePlexorRouting() {
|
|
119
|
+
try {
|
|
120
|
+
const settings = this.load();
|
|
121
|
+
|
|
122
|
+
if (!settings.env) {
|
|
123
|
+
console.log('Plexor routing is not currently enabled.');
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Remove Plexor-specific env vars
|
|
128
|
+
delete settings.env.ANTHROPIC_BASE_URL;
|
|
129
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
130
|
+
|
|
131
|
+
// Clean up empty env block
|
|
132
|
+
if (Object.keys(settings.env).length === 0) {
|
|
133
|
+
delete settings.env;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const success = this.save(settings);
|
|
137
|
+
|
|
138
|
+
if (success) {
|
|
139
|
+
console.log('✓ Plexor routing disabled');
|
|
140
|
+
console.log(' Claude Code will now connect directly to Anthropic.');
|
|
141
|
+
console.log(' Changes take effect immediately (no restart needed).');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return success;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('Failed to disable Plexor routing:', err.message);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get current routing status
|
|
153
|
+
* @returns {Object} { enabled: boolean, baseUrl: string|null, hasToken: boolean }
|
|
154
|
+
*/
|
|
155
|
+
getRoutingStatus() {
|
|
156
|
+
try {
|
|
157
|
+
const settings = this.load();
|
|
158
|
+
|
|
159
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || null;
|
|
160
|
+
const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
161
|
+
|
|
162
|
+
// Check if routing to Plexor
|
|
163
|
+
const isPlexorRouting = baseUrl && (
|
|
164
|
+
baseUrl.includes('plexor') ||
|
|
165
|
+
baseUrl.includes('staging.api')
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
enabled: isPlexorRouting,
|
|
170
|
+
baseUrl,
|
|
171
|
+
hasToken,
|
|
172
|
+
isStaging: baseUrl?.includes('staging') || false,
|
|
173
|
+
tokenPreview: hasToken ? settings.env.ANTHROPIC_AUTH_TOKEN.substring(0, 12) + '...' : null
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
return { enabled: false, baseUrl: null, hasToken: false };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Update just the API key without changing other settings
|
|
182
|
+
* @param {string} apiKey - new Plexor API key
|
|
183
|
+
* @returns {boolean} success status
|
|
184
|
+
*/
|
|
185
|
+
updateApiKey(apiKey) {
|
|
186
|
+
try {
|
|
187
|
+
const settings = this.load();
|
|
188
|
+
|
|
189
|
+
if (!settings.env) {
|
|
190
|
+
settings.env = {};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
194
|
+
return this.save(settings);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error('Failed to update API key:', err.message);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Switch between staging and production
|
|
203
|
+
* @param {boolean} useStaging - true for staging, false for production
|
|
204
|
+
* @returns {boolean} success status
|
|
205
|
+
*/
|
|
206
|
+
setEnvironment(useStaging) {
|
|
207
|
+
try {
|
|
208
|
+
const settings = this.load();
|
|
209
|
+
|
|
210
|
+
if (!settings.env?.ANTHROPIC_BASE_URL) {
|
|
211
|
+
console.log('Plexor routing is not enabled. Run /plexor-login first.');
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
settings.env.ANTHROPIC_BASE_URL = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
|
|
216
|
+
|
|
217
|
+
const success = this.save(settings);
|
|
218
|
+
if (success) {
|
|
219
|
+
console.log(`✓ Switched to ${useStaging ? 'staging' : 'production'} environment`);
|
|
220
|
+
}
|
|
221
|
+
return success;
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error('Failed to switch environment:', err.message);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Export singleton instance and class
|
|
230
|
+
const settingsManager = new ClaudeSettingsManager();
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
ClaudeSettingsManager,
|
|
234
|
+
settingsManager,
|
|
235
|
+
CLAUDE_DIR,
|
|
236
|
+
SETTINGS_PATH,
|
|
237
|
+
PLEXOR_STAGING_URL,
|
|
238
|
+
PLEXOR_PROD_URL
|
|
239
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@plexor-dev/claude-code-plugin-staging",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
|
|
5
|
+
"main": "lib/constants.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"plexor-status": "./commands/plexor-status.js",
|
|
8
|
+
"plexor-mode": "./commands/plexor-mode.js",
|
|
9
|
+
"plexor-enabled": "./commands/plexor-enabled.js",
|
|
10
|
+
"plexor-provider": "./commands/plexor-provider.js",
|
|
11
|
+
"plexor-login": "./commands/plexor-login.js",
|
|
12
|
+
"plexor-logout": "./commands/plexor-logout.js",
|
|
13
|
+
"plexor-settings": "./commands/plexor-settings.js",
|
|
14
|
+
"plexor-config": "./commands/plexor-config.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node scripts/postinstall.js",
|
|
18
|
+
"preuninstall": "node scripts/uninstall.js",
|
|
19
|
+
"test": "node --test"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"commands/",
|
|
23
|
+
"hooks/",
|
|
24
|
+
"scripts/",
|
|
25
|
+
"lib/",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"claude",
|
|
34
|
+
"claude-code",
|
|
35
|
+
"llm",
|
|
36
|
+
"optimization",
|
|
37
|
+
"plexor",
|
|
38
|
+
"cost-savings",
|
|
39
|
+
"ai",
|
|
40
|
+
"anthropic"
|
|
41
|
+
],
|
|
42
|
+
"author": "Plexor <hello@plexor.dev>",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/plexor-dev/claude-code-plugin"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/plexor-dev/claude-code-plugin/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://plexor.dev",
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=16"
|
|
54
|
+
},
|
|
55
|
+
"os": [
|
|
56
|
+
"darwin",
|
|
57
|
+
"linux",
|
|
58
|
+
"win32"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Plexor CLI - Claude Code with intelligent optimization
|
|
3
|
+
# This wrapper auto-enables hypervisor mode for direct gateway routing
|
|
4
|
+
|
|
5
|
+
# Colors
|
|
6
|
+
CYAN='\033[0;36m'
|
|
7
|
+
GREEN='\033[0;32m'
|
|
8
|
+
YELLOW='\033[0;33m'
|
|
9
|
+
DIM='\033[2m'
|
|
10
|
+
NC='\033[0m' # No Color
|
|
11
|
+
|
|
12
|
+
CONFIG_FILE="$HOME/.plexor/config.json"
|
|
13
|
+
|
|
14
|
+
# Auto-configure ANTHROPIC_BASE_URL if Plexor is enabled
|
|
15
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
16
|
+
ENABLED=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('settings',{}).get('enabled', False))" 2>/dev/null)
|
|
17
|
+
API_URL=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('settings',{}).get('apiUrl', ''))" 2>/dev/null)
|
|
18
|
+
API_KEY=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('auth',{}).get('api_key', ''))" 2>/dev/null)
|
|
19
|
+
MODE=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('settings',{}).get('mode', 'balanced'))" 2>/dev/null)
|
|
20
|
+
|
|
21
|
+
if [ "$ENABLED" = "True" ] && [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
|
|
22
|
+
# Set ANTHROPIC_BASE_URL to Plexor gateway (hypervisor mode)
|
|
23
|
+
export ANTHROPIC_BASE_URL="${API_URL}/gateway/anthropic/v1"
|
|
24
|
+
export ANTHROPIC_API_KEY="$API_KEY"
|
|
25
|
+
|
|
26
|
+
# Show Plexor branding
|
|
27
|
+
echo -e "${CYAN}"
|
|
28
|
+
cat << 'EOF'
|
|
29
|
+
____ __
|
|
30
|
+
/ __ \/ /__ _ ______ _____
|
|
31
|
+
/ /_/ / / _ \| |/_/ __ \/ ___/
|
|
32
|
+
/ ____/ / __/> </ /_/ / /
|
|
33
|
+
/_/ /_/\___/_/|_|\____/_/
|
|
34
|
+
|
|
35
|
+
EOF
|
|
36
|
+
echo -e "${NC}"
|
|
37
|
+
echo -e "${GREEN}●${NC} Hypervisor Mode: ${GREEN}Active${NC}"
|
|
38
|
+
echo -e " Gateway: ${DIM}${ANTHROPIC_BASE_URL}${NC}"
|
|
39
|
+
echo -e " Mode: ${MODE}"
|
|
40
|
+
echo ""
|
|
41
|
+
else
|
|
42
|
+
echo -e "${YELLOW}○${NC} Plexor: ${DIM}Disabled${NC} (run /plexor-login to enable)"
|
|
43
|
+
echo ""
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Launch claude with all arguments passed through
|
|
48
|
+
exec claude "$@"
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plexor Claude Code Plugin - Postinstall Script
|
|
5
|
+
*
|
|
6
|
+
* Copies slash commands to ~/.claude/commands/ and creates
|
|
7
|
+
* the ~/.plexor/ configuration directory.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the correct home directory, accounting for sudo.
|
|
17
|
+
* When running with sudo, os.homedir() returns /root, but we want
|
|
18
|
+
* the actual user's home directory.
|
|
19
|
+
*/
|
|
20
|
+
function getHomeDir() {
|
|
21
|
+
// Check if running with sudo - SUDO_USER contains the original username
|
|
22
|
+
if (process.env.SUDO_USER) {
|
|
23
|
+
// On Linux/Mac, home directories are typically /home/<user> or /Users/<user>
|
|
24
|
+
const platform = os.platform();
|
|
25
|
+
if (platform === 'darwin') {
|
|
26
|
+
return path.join('/Users', process.env.SUDO_USER);
|
|
27
|
+
} else if (platform === 'linux') {
|
|
28
|
+
return path.join('/home', process.env.SUDO_USER);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return os.homedir();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get uid/gid for the target user (handles sudo case).
|
|
36
|
+
* Returns null if not running with sudo or on Windows.
|
|
37
|
+
*/
|
|
38
|
+
function getTargetUserIds() {
|
|
39
|
+
const sudoUser = process.env.SUDO_USER;
|
|
40
|
+
if (!sudoUser || os.platform() === 'win32') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Get uid and gid for the sudo user
|
|
46
|
+
const uid = parseInt(execSync(`id -u ${sudoUser}`, { encoding: 'utf8' }).trim(), 10);
|
|
47
|
+
const gid = parseInt(execSync(`id -g ${sudoUser}`, { encoding: 'utf8' }).trim(), 10);
|
|
48
|
+
return { uid, gid, user: sudoUser };
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Recursively chown a directory and all its contents.
|
|
56
|
+
*/
|
|
57
|
+
function chownRecursive(dirPath, uid, gid) {
|
|
58
|
+
if (!fs.existsSync(dirPath)) return;
|
|
59
|
+
|
|
60
|
+
const stat = fs.statSync(dirPath);
|
|
61
|
+
fs.chownSync(dirPath, uid, gid);
|
|
62
|
+
|
|
63
|
+
if (stat.isDirectory()) {
|
|
64
|
+
const entries = fs.readdirSync(dirPath);
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
chownRecursive(path.join(dirPath, entry), uid, gid);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const HOME_DIR = getHomeDir();
|
|
72
|
+
const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
|
|
73
|
+
const CLAUDE_COMMANDS_DIR = path.join(HOME_DIR, '.claude', 'commands');
|
|
74
|
+
const PLEXOR_PLUGINS_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'commands');
|
|
75
|
+
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
76
|
+
const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
|
|
77
|
+
|
|
78
|
+
// Default configuration for new installs
|
|
79
|
+
const DEFAULT_CONFIG = {
|
|
80
|
+
version: 1,
|
|
81
|
+
auth: {
|
|
82
|
+
mode: "pending",
|
|
83
|
+
authenticated_at: null
|
|
84
|
+
},
|
|
85
|
+
settings: {
|
|
86
|
+
enabled: true,
|
|
87
|
+
apiUrl: "https://api.plexor.dev",
|
|
88
|
+
mode: "balanced",
|
|
89
|
+
localCacheEnabled: true
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function main() {
|
|
94
|
+
try {
|
|
95
|
+
// Get target user info for chown (if running with sudo)
|
|
96
|
+
const targetUser = getTargetUserIds();
|
|
97
|
+
|
|
98
|
+
// Create ~/.claude/commands/ if not exists
|
|
99
|
+
fs.mkdirSync(CLAUDE_COMMANDS_DIR, { recursive: true });
|
|
100
|
+
|
|
101
|
+
// Create ~/.claude/plugins/plexor/commands/ for JS executors
|
|
102
|
+
fs.mkdirSync(PLEXOR_PLUGINS_DIR, { recursive: true });
|
|
103
|
+
|
|
104
|
+
// Create ~/.plexor/ with secure permissions (owner only)
|
|
105
|
+
fs.mkdirSync(PLEXOR_CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
106
|
+
|
|
107
|
+
// Create default config.json if it doesn't exist
|
|
108
|
+
let configCreated = false;
|
|
109
|
+
if (!fs.existsSync(PLEXOR_CONFIG_FILE)) {
|
|
110
|
+
fs.writeFileSync(
|
|
111
|
+
PLEXOR_CONFIG_FILE,
|
|
112
|
+
JSON.stringify(DEFAULT_CONFIG, null, 2),
|
|
113
|
+
{ mode: 0o600 }
|
|
114
|
+
);
|
|
115
|
+
configCreated = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get list of command files (.md for Claude, .js for executors)
|
|
119
|
+
const mdFiles = fs.readdirSync(COMMANDS_SOURCE)
|
|
120
|
+
.filter(f => f.endsWith('.md'));
|
|
121
|
+
const jsFiles = fs.readdirSync(COMMANDS_SOURCE)
|
|
122
|
+
.filter(f => f.endsWith('.js'));
|
|
123
|
+
|
|
124
|
+
if (mdFiles.length === 0) {
|
|
125
|
+
console.error('No command files found in package. Installation may be corrupt.');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const installed = [];
|
|
130
|
+
const backed_up = [];
|
|
131
|
+
|
|
132
|
+
// Copy .md command files to ~/.claude/commands/
|
|
133
|
+
for (const file of mdFiles) {
|
|
134
|
+
const src = path.join(COMMANDS_SOURCE, file);
|
|
135
|
+
const dest = path.join(CLAUDE_COMMANDS_DIR, file);
|
|
136
|
+
|
|
137
|
+
// Backup existing file if present and different
|
|
138
|
+
if (fs.existsSync(dest)) {
|
|
139
|
+
const existingContent = fs.readFileSync(dest, 'utf8');
|
|
140
|
+
const newContent = fs.readFileSync(src, 'utf8');
|
|
141
|
+
|
|
142
|
+
if (existingContent !== newContent) {
|
|
143
|
+
const backupPath = dest + '.backup';
|
|
144
|
+
fs.copyFileSync(dest, backupPath);
|
|
145
|
+
backed_up.push(file);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fs.copyFileSync(src, dest);
|
|
150
|
+
installed.push(file.replace('.md', ''));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Copy .js executor files to ~/.claude/plugins/plexor/commands/
|
|
154
|
+
const jsInstalled = [];
|
|
155
|
+
for (const file of jsFiles) {
|
|
156
|
+
const src = path.join(COMMANDS_SOURCE, file);
|
|
157
|
+
const dest = path.join(PLEXOR_PLUGINS_DIR, file);
|
|
158
|
+
fs.copyFileSync(src, dest);
|
|
159
|
+
// Make executable
|
|
160
|
+
fs.chmodSync(dest, 0o755);
|
|
161
|
+
jsInstalled.push(file);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Fix file ownership when running with sudo
|
|
165
|
+
// Files are created as root but should be owned by the original user
|
|
166
|
+
if (targetUser) {
|
|
167
|
+
const { uid, gid } = targetUser;
|
|
168
|
+
// Chown the .claude directory and all contents we created
|
|
169
|
+
chownRecursive(path.join(HOME_DIR, '.claude'), uid, gid);
|
|
170
|
+
// Chown the .plexor directory and all contents
|
|
171
|
+
chownRecursive(PLEXOR_CONFIG_DIR, uid, gid);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Detect shell type
|
|
175
|
+
const shell = process.env.SHELL || '';
|
|
176
|
+
const isZsh = shell.includes('zsh');
|
|
177
|
+
const shellRc = isZsh ? '~/.zshrc' : '~/.bashrc';
|
|
178
|
+
|
|
179
|
+
// Print success message with clear onboarding steps
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(' ╔═══════════════════════════════════════════════════════════════════╗');
|
|
182
|
+
console.log(' ║ ║');
|
|
183
|
+
console.log(' ║ Plexor Claude Code Plugin installed successfully! ║');
|
|
184
|
+
console.log(' ║ ║');
|
|
185
|
+
console.log(' ╚═══════════════════════════════════════════════════════════════════╝');
|
|
186
|
+
console.log('');
|
|
187
|
+
|
|
188
|
+
if (configCreated) {
|
|
189
|
+
console.log(' ✓ Created ~/.plexor/config.json');
|
|
190
|
+
}
|
|
191
|
+
console.log(` ✓ Installed ${installed.length} slash commands to ~/.claude/commands/`);
|
|
192
|
+
if (jsInstalled.length > 0) {
|
|
193
|
+
console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/`);
|
|
194
|
+
}
|
|
195
|
+
if (targetUser) {
|
|
196
|
+
console.log(` ✓ Set file ownership to ${targetUser.user}`);
|
|
197
|
+
}
|
|
198
|
+
console.log('');
|
|
199
|
+
|
|
200
|
+
// CRITICAL: Make the required step VERY obvious
|
|
201
|
+
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
|
202
|
+
console.log(' │ REQUIRED: Run this command to enable Plexor routing: │');
|
|
203
|
+
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(' For Claude MAX users (OAuth):');
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
208
|
+
console.log(` source ${shellRc}`);
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(' For API key users (get key at https://plexor.dev/dashboard):');
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
213
|
+
console.log(` echo 'export ANTHROPIC_API_KEY="plx_your_key_here"' >> ${shellRc}`);
|
|
214
|
+
console.log(` source ${shellRc}`);
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
|
217
|
+
console.log(' │ Then start Claude Code and run: /plexor-status │');
|
|
218
|
+
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(' Available commands:');
|
|
221
|
+
console.log(' /plexor-status - Check connection and see savings');
|
|
222
|
+
console.log(' /plexor-mode - Switch modes (eco/balanced/quality)');
|
|
223
|
+
console.log(' /plexor-login - Authenticate with API key');
|
|
224
|
+
console.log(' /plexor-settings - View/modify settings');
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log(' Documentation: https://plexor.dev/docs');
|
|
227
|
+
console.log('');
|
|
228
|
+
|
|
229
|
+
if (backed_up.length > 0) {
|
|
230
|
+
console.log(' Note: Existing files backed up (.backup):');
|
|
231
|
+
backed_up.forEach(f => console.log(` ${f}`));
|
|
232
|
+
console.log('');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('');
|
|
237
|
+
console.error(' Plexor plugin installation failed');
|
|
238
|
+
console.error('');
|
|
239
|
+
console.error(` Error: ${error.message}`);
|
|
240
|
+
console.error('');
|
|
241
|
+
console.error(' Troubleshooting:');
|
|
242
|
+
console.error(' - Ensure you have write access to ~/.claude/commands/');
|
|
243
|
+
console.error(' - Try running with sudo if permission denied');
|
|
244
|
+
console.error('');
|
|
245
|
+
console.error(' Report issues: https://github.com/plexor-ai/claude-code-plugin/issues');
|
|
246
|
+
console.error('');
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
main();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plexor Claude Code Plugin - Uninstall Script
|
|
5
|
+
*
|
|
6
|
+
* Removes slash commands from ~/.claude/commands/
|
|
7
|
+
* Optionally restores backups if they exist.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
|
|
15
|
+
const CLAUDE_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
|
|
16
|
+
|
|
17
|
+
function main() {
|
|
18
|
+
try {
|
|
19
|
+
// Get list of our command files
|
|
20
|
+
const files = fs.readdirSync(COMMANDS_SOURCE)
|
|
21
|
+
.filter(f => f.endsWith('.md'));
|
|
22
|
+
|
|
23
|
+
const removed = [];
|
|
24
|
+
const restored = [];
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const dest = path.join(CLAUDE_COMMANDS_DIR, file);
|
|
28
|
+
const backupPath = dest + '.backup';
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(dest)) {
|
|
31
|
+
fs.unlinkSync(dest);
|
|
32
|
+
removed.push(file.replace('.md', ''));
|
|
33
|
+
|
|
34
|
+
// Restore backup if it exists
|
|
35
|
+
if (fs.existsSync(backupPath)) {
|
|
36
|
+
fs.renameSync(backupPath, dest);
|
|
37
|
+
restored.push(file);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (removed.length > 0) {
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(' Plexor plugin uninstalled');
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(' Removed commands:');
|
|
47
|
+
removed.forEach(cmd => console.log(` /${cmd}`));
|
|
48
|
+
|
|
49
|
+
if (restored.length > 0) {
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(' Restored from backup:');
|
|
52
|
+
restored.forEach(f => console.log(` ${f}`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(' Note: ~/.plexor/ config directory was preserved.');
|
|
57
|
+
console.log(' To remove it: rm -rf ~/.plexor');
|
|
58
|
+
console.log('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Don't fail uninstall on errors - just warn
|
|
63
|
+
console.warn(` Warning: Could not fully uninstall: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main();
|