@plexor-dev/claude-code-plugin 0.1.0-beta.3 → 0.1.0-beta.30
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 +121 -0
- package/commands/plexor-enabled.md +15 -41
- package/commands/plexor-login.js +174 -0
- package/commands/plexor-login.md +13 -62
- package/commands/plexor-logout.js +92 -0
- package/commands/plexor-logout.md +13 -28
- package/commands/plexor-setup.md +172 -0
- package/commands/plexor-status.js +240 -0
- package/commands/plexor-status.md +10 -50
- 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 +16 -31
- 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 +242 -0
- package/package.json +8 -1
- package/scripts/plexor-cli.sh +48 -0
- package/scripts/postinstall.js +173 -24
- package/commands/plexor-config.md +0 -42
- package/commands/plexor-mode.md +0 -47
- package/commands/plexor-provider.md +0 -47
- package/commands/plexor-settings.md +0 -58
|
@@ -0,0 +1,242 @@
|
|
|
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
|
+
// Log unexpected errors (not ENOENT which is handled above)
|
|
42
|
+
if (err.code !== 'ENOENT') {
|
|
43
|
+
console.warn('Warning: Failed to load Claude settings:', err.message);
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save Claude settings
|
|
51
|
+
* @param {Object} settings - settings object to save
|
|
52
|
+
* @returns {boolean} success status
|
|
53
|
+
*/
|
|
54
|
+
save(settings) {
|
|
55
|
+
try {
|
|
56
|
+
// Ensure .claude directory exists
|
|
57
|
+
if (!fs.existsSync(this.claudeDir)) {
|
|
58
|
+
fs.mkdirSync(this.claudeDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
62
|
+
return true;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error('Failed to save Claude settings:', err.message);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Enable Plexor routing by setting env vars in settings.json
|
|
71
|
+
*
|
|
72
|
+
* This is the KEY mechanism: setting ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
|
|
73
|
+
* in the env block redirects ALL Claude Code sessions to Plexor automatically.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} apiKey - Plexor API key (plx_*)
|
|
76
|
+
* @param {Object} options - { useStaging: boolean }
|
|
77
|
+
* @returns {boolean} success status
|
|
78
|
+
*/
|
|
79
|
+
enablePlexorRouting(apiKey, options = {}) {
|
|
80
|
+
// Default to production. Use useStaging: true for staging keys.
|
|
81
|
+
const { useStaging = false } = options;
|
|
82
|
+
const apiUrl = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const settings = this.load();
|
|
86
|
+
|
|
87
|
+
// Initialize env block if doesn't exist
|
|
88
|
+
if (!settings.env) {
|
|
89
|
+
settings.env = {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Set the magic environment variables
|
|
93
|
+
// ANTHROPIC_AUTH_TOKEN has higher precedence than ANTHROPIC_API_KEY
|
|
94
|
+
settings.env.ANTHROPIC_BASE_URL = apiUrl;
|
|
95
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
96
|
+
|
|
97
|
+
const success = this.save(settings);
|
|
98
|
+
|
|
99
|
+
if (success) {
|
|
100
|
+
console.log(`✓ Plexor routing enabled`);
|
|
101
|
+
console.log(` Base URL: ${apiUrl}`);
|
|
102
|
+
console.log(` API Key: ${apiKey.substring(0, 12)}...`);
|
|
103
|
+
console.log(`\n All Claude Code sessions will now route through Plexor.`);
|
|
104
|
+
console.log(` Changes take effect immediately (no restart needed).`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return success;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('Failed to enable Plexor routing:', err.message);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Disable Plexor routing by removing env vars from settings.json
|
|
116
|
+
*
|
|
117
|
+
* This restores direct connection to Anthropic API.
|
|
118
|
+
*
|
|
119
|
+
* @returns {boolean} success status
|
|
120
|
+
*/
|
|
121
|
+
disablePlexorRouting() {
|
|
122
|
+
try {
|
|
123
|
+
const settings = this.load();
|
|
124
|
+
|
|
125
|
+
if (!settings.env) {
|
|
126
|
+
console.log('Plexor routing is not currently enabled.');
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Remove Plexor-specific env vars
|
|
131
|
+
delete settings.env.ANTHROPIC_BASE_URL;
|
|
132
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
133
|
+
|
|
134
|
+
// Clean up empty env block
|
|
135
|
+
if (Object.keys(settings.env).length === 0) {
|
|
136
|
+
delete settings.env;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const success = this.save(settings);
|
|
140
|
+
|
|
141
|
+
if (success) {
|
|
142
|
+
console.log('✓ Plexor routing disabled');
|
|
143
|
+
console.log(' Claude Code will now connect directly to Anthropic.');
|
|
144
|
+
console.log(' Changes take effect immediately (no restart needed).');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return success;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('Failed to disable Plexor routing:', err.message);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get current routing status
|
|
156
|
+
* @returns {Object} { enabled: boolean, baseUrl: string|null, hasToken: boolean }
|
|
157
|
+
*/
|
|
158
|
+
getRoutingStatus() {
|
|
159
|
+
try {
|
|
160
|
+
const settings = this.load();
|
|
161
|
+
|
|
162
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || null;
|
|
163
|
+
const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
164
|
+
|
|
165
|
+
// Check if routing to Plexor
|
|
166
|
+
const isPlexorRouting = baseUrl && (
|
|
167
|
+
baseUrl.includes('plexor') ||
|
|
168
|
+
baseUrl.includes('staging.api')
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
enabled: isPlexorRouting,
|
|
173
|
+
baseUrl,
|
|
174
|
+
hasToken,
|
|
175
|
+
isStaging: baseUrl?.includes('staging') || false,
|
|
176
|
+
tokenPreview: hasToken ? settings.env.ANTHROPIC_AUTH_TOKEN.substring(0, 12) + '...' : null
|
|
177
|
+
};
|
|
178
|
+
} catch {
|
|
179
|
+
return { enabled: false, baseUrl: null, hasToken: false };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Update just the API key without changing other settings
|
|
185
|
+
* @param {string} apiKey - new Plexor API key
|
|
186
|
+
* @returns {boolean} success status
|
|
187
|
+
*/
|
|
188
|
+
updateApiKey(apiKey) {
|
|
189
|
+
try {
|
|
190
|
+
const settings = this.load();
|
|
191
|
+
|
|
192
|
+
if (!settings.env) {
|
|
193
|
+
settings.env = {};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
197
|
+
return this.save(settings);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error('Failed to update API key:', err.message);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Switch between staging and production
|
|
206
|
+
* @param {boolean} useStaging - true for staging, false for production
|
|
207
|
+
* @returns {boolean} success status
|
|
208
|
+
*/
|
|
209
|
+
setEnvironment(useStaging) {
|
|
210
|
+
try {
|
|
211
|
+
const settings = this.load();
|
|
212
|
+
|
|
213
|
+
if (!settings.env?.ANTHROPIC_BASE_URL) {
|
|
214
|
+
console.log('Plexor routing is not enabled. Run /plexor-login first.');
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
settings.env.ANTHROPIC_BASE_URL = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
|
|
219
|
+
|
|
220
|
+
const success = this.save(settings);
|
|
221
|
+
if (success) {
|
|
222
|
+
console.log(`✓ Switched to ${useStaging ? 'staging' : 'production'} environment`);
|
|
223
|
+
}
|
|
224
|
+
return success;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error('Failed to switch environment:', err.message);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Export singleton instance and class
|
|
233
|
+
const settingsManager = new ClaudeSettingsManager();
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
ClaudeSettingsManager,
|
|
237
|
+
settingsManager,
|
|
238
|
+
CLAUDE_DIR,
|
|
239
|
+
SETTINGS_PATH,
|
|
240
|
+
PLEXOR_STAGING_URL,
|
|
241
|
+
PLEXOR_PROD_URL
|
|
242
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plexor-dev/claude-code-plugin",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.30",
|
|
4
4
|
"description": "LLM cost optimization plugin for Claude Code - Save up to 90% on AI costs",
|
|
5
5
|
"main": "lib/constants.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"plexor-status": "./commands/plexor-status.js",
|
|
8
|
+
"plexor-enabled": "./commands/plexor-enabled.js",
|
|
9
|
+
"plexor-login": "./commands/plexor-login.js",
|
|
10
|
+
"plexor-logout": "./commands/plexor-logout.js"
|
|
11
|
+
},
|
|
6
12
|
"scripts": {
|
|
7
13
|
"postinstall": "node scripts/postinstall.js",
|
|
8
14
|
"preuninstall": "node scripts/uninstall.js",
|
|
@@ -10,6 +16,7 @@
|
|
|
10
16
|
},
|
|
11
17
|
"files": [
|
|
12
18
|
"commands/",
|
|
19
|
+
"hooks/",
|
|
13
20
|
"scripts/",
|
|
14
21
|
"lib/",
|
|
15
22
|
"README.md",
|
|
@@ -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 "$@"
|
package/scripts/postinstall.js
CHANGED
|
@@ -10,24 +10,118 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const os = require('os');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
13
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();
|
|
14
72
|
const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
|
|
15
|
-
const CLAUDE_COMMANDS_DIR = path.join(
|
|
16
|
-
const
|
|
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
|
+
};
|
|
17
92
|
|
|
18
93
|
function main() {
|
|
19
94
|
try {
|
|
95
|
+
// Get target user info for chown (if running with sudo)
|
|
96
|
+
const targetUser = getTargetUserIds();
|
|
97
|
+
|
|
20
98
|
// Create ~/.claude/commands/ if not exists
|
|
21
99
|
fs.mkdirSync(CLAUDE_COMMANDS_DIR, { recursive: true });
|
|
22
100
|
|
|
101
|
+
// Create ~/.claude/plugins/plexor/commands/ for JS executors
|
|
102
|
+
fs.mkdirSync(PLEXOR_PLUGINS_DIR, { recursive: true });
|
|
103
|
+
|
|
23
104
|
// Create ~/.plexor/ with secure permissions (owner only)
|
|
24
105
|
fs.mkdirSync(PLEXOR_CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
25
106
|
|
|
26
|
-
//
|
|
27
|
-
|
|
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)
|
|
28
120
|
.filter(f => f.endsWith('.md'));
|
|
121
|
+
const jsFiles = fs.readdirSync(COMMANDS_SOURCE)
|
|
122
|
+
.filter(f => f.endsWith('.js'));
|
|
29
123
|
|
|
30
|
-
if (
|
|
124
|
+
if (mdFiles.length === 0) {
|
|
31
125
|
console.error('No command files found in package. Installation may be corrupt.');
|
|
32
126
|
process.exit(1);
|
|
33
127
|
}
|
|
@@ -35,7 +129,8 @@ function main() {
|
|
|
35
129
|
const installed = [];
|
|
36
130
|
const backed_up = [];
|
|
37
131
|
|
|
38
|
-
|
|
132
|
+
// Copy .md command files to ~/.claude/commands/
|
|
133
|
+
for (const file of mdFiles) {
|
|
39
134
|
const src = path.join(COMMANDS_SOURCE, file);
|
|
40
135
|
const dest = path.join(CLAUDE_COMMANDS_DIR, file);
|
|
41
136
|
|
|
@@ -55,34 +150,88 @@ function main() {
|
|
|
55
150
|
installed.push(file.replace('.md', ''));
|
|
56
151
|
}
|
|
57
152
|
|
|
58
|
-
//
|
|
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
|
|
59
180
|
console.log('');
|
|
60
|
-
console.log('
|
|
61
|
-
console.log(' ║
|
|
62
|
-
console.log(' ║ Plexor Claude Code Plugin installed successfully!
|
|
63
|
-
console.log(' ║
|
|
64
|
-
console.log('
|
|
181
|
+
console.log(' ╔═══════════════════════════════════════════════════════════════════╗');
|
|
182
|
+
console.log(' ║ ║');
|
|
183
|
+
console.log(' ║ Plexor Claude Code Plugin installed successfully! ║');
|
|
184
|
+
console.log(' ║ ║');
|
|
185
|
+
console.log(' ╚═══════════════════════════════════════════════════════════════════╝');
|
|
65
186
|
console.log('');
|
|
66
|
-
console.log(' Commands installed to ~/.claude/commands/:');
|
|
67
|
-
installed.forEach(cmd => {
|
|
68
|
-
console.log(` /${cmd}`);
|
|
69
|
-
});
|
|
70
187
|
|
|
71
|
-
if (
|
|
72
|
-
console.log('');
|
|
73
|
-
|
|
74
|
-
|
|
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/`);
|
|
75
194
|
}
|
|
195
|
+
if (targetUser) {
|
|
196
|
+
console.log(` ✓ Set file ownership to ${targetUser.user}`);
|
|
197
|
+
}
|
|
198
|
+
console.log('');
|
|
76
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}`);
|
|
77
215
|
console.log('');
|
|
78
|
-
console.log('
|
|
79
|
-
console.log('
|
|
80
|
-
console.log('
|
|
81
|
-
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-setup - First-time setup wizard');
|
|
222
|
+
console.log(' /plexor-login - Authenticate with API key');
|
|
223
|
+
console.log(' /plexor-status - Check connection and see savings');
|
|
224
|
+
console.log(' /plexor-enabled - Enable/disable Plexor routing');
|
|
82
225
|
console.log('');
|
|
83
226
|
console.log(' Documentation: https://plexor.dev/docs');
|
|
84
227
|
console.log('');
|
|
85
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
|
+
|
|
86
235
|
} catch (error) {
|
|
87
236
|
console.error('');
|
|
88
237
|
console.error(' Plexor plugin installation failed');
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Configure Plexor settings (user)
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Plexor Config
|
|
6
|
-
|
|
7
|
-
View and modify Plexor configuration. This is an alias for /plexor-settings.
|
|
8
|
-
|
|
9
|
-
## Steps
|
|
10
|
-
|
|
11
|
-
**Step 1: Read current configuration**
|
|
12
|
-
|
|
13
|
-
Use the Read tool to read `~/.plexor/config.json`.
|
|
14
|
-
|
|
15
|
-
If the file doesn't exist, show:
|
|
16
|
-
```
|
|
17
|
-
Plexor Config
|
|
18
|
-
=============
|
|
19
|
-
No configuration found. Run /plexor-login to set up Plexor.
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
**Step 2: Display current configuration**
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
Plexor Config
|
|
26
|
-
=============
|
|
27
|
-
Enabled: [enabled]
|
|
28
|
-
API URL: [apiUrl]
|
|
29
|
-
API Key: [show "configured" if apiKey exists, otherwise "not configured"]
|
|
30
|
-
Mode: [mode]
|
|
31
|
-
Provider: [preferredProvider]
|
|
32
|
-
Local Cache: [localCacheEnabled]
|
|
33
|
-
Timeout: [timeout]ms
|
|
34
|
-
|
|
35
|
-
Configuration file: ~/.plexor/config.json
|
|
36
|
-
|
|
37
|
-
Other commands:
|
|
38
|
-
- /plexor-settings - View/edit all settings
|
|
39
|
-
- /plexor-mode - Change optimization mode
|
|
40
|
-
- /plexor-provider - Change provider
|
|
41
|
-
- /plexor-enabled - Enable/disable proxy
|
|
42
|
-
```
|
package/commands/plexor-mode.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Set Plexor optimization mode (eco/balanced/quality/passthrough) (user)
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Plexor Mode
|
|
6
|
-
|
|
7
|
-
Set the Plexor optimization mode to control cost vs quality trade-offs.
|
|
8
|
-
|
|
9
|
-
## Steps
|
|
10
|
-
|
|
11
|
-
**Step 1: Read current configuration**
|
|
12
|
-
|
|
13
|
-
Use the Read tool to read `~/.plexor/config.json` and check the current `mode` setting.
|
|
14
|
-
|
|
15
|
-
**Step 2: Ask user which mode they want**
|
|
16
|
-
|
|
17
|
-
Use the `AskUserQuestion` tool:
|
|
18
|
-
|
|
19
|
-
Question: "Which optimization mode would you like to use?"
|
|
20
|
-
Header: "Mode"
|
|
21
|
-
Options:
|
|
22
|
-
1. **eco** - Maximum savings (~$0.04/1M tokens) - Uses Ministral 3B
|
|
23
|
-
2. **balanced** - Good balance (~$0.15/1M) - DeepSeek, Gemini Flash, Mistral Small
|
|
24
|
-
3. **quality** - Premium models (~$5/1M) - Claude Opus, Gemini Pro, Mistral Large
|
|
25
|
-
4. **passthrough** - No optimization - Direct to requested model
|
|
26
|
-
|
|
27
|
-
**Step 3: Update the configuration**
|
|
28
|
-
|
|
29
|
-
Use the Read tool to get the current config, then use the Write tool to update `~/.plexor/config.json`:
|
|
30
|
-
- Set the `mode` field to the selected value: "eco", "balanced", "quality", or "passthrough"
|
|
31
|
-
|
|
32
|
-
Keep all other settings unchanged.
|
|
33
|
-
|
|
34
|
-
**Step 4: Show confirmation**
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
Plexor Mode: [SELECTED MODE]
|
|
38
|
-
|
|
39
|
-
Mode Details:
|
|
40
|
-
- eco: Maximum cost savings using Ministral 3B ($0.04/$0.04 per 1M tokens)
|
|
41
|
-
- balanced: Cost-first with quality fallbacks (DeepSeek → Gemini Flash → Mistral)
|
|
42
|
-
- quality: Premium models for complex tasks (Claude Opus, Gemini Pro)
|
|
43
|
-
- passthrough: No optimization, direct to Anthropic
|
|
44
|
-
|
|
45
|
-
Current mode: [mode]
|
|
46
|
-
Run /plexor-status to see your savings.
|
|
47
|
-
```
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Force a specific LLM provider (claude/openai/deepseek/mistral/gemini/auto) (user)
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Plexor Provider
|
|
6
|
-
|
|
7
|
-
Set your preferred LLM provider for Plexor routing.
|
|
8
|
-
|
|
9
|
-
## Steps
|
|
10
|
-
|
|
11
|
-
**Step 1: Read current configuration**
|
|
12
|
-
|
|
13
|
-
Use the Read tool to read `~/.plexor/config.json` and check the current `preferredProvider` setting.
|
|
14
|
-
|
|
15
|
-
**Step 2: Ask user which provider they prefer**
|
|
16
|
-
|
|
17
|
-
Use the `AskUserQuestion` tool:
|
|
18
|
-
|
|
19
|
-
Question: "Which LLM provider would you prefer?"
|
|
20
|
-
Header: "Provider"
|
|
21
|
-
Options:
|
|
22
|
-
1. **auto** - Let Plexor choose the best provider based on task (Recommended)
|
|
23
|
-
2. **deepseek** - Use DeepSeek models (cheapest, good for code)
|
|
24
|
-
3. **mistral** - Use Mistral models (fast, good balance)
|
|
25
|
-
4. **gemini** - Use Google Gemini models (good for analysis)
|
|
26
|
-
|
|
27
|
-
**Step 3: Update the configuration**
|
|
28
|
-
|
|
29
|
-
Use the Read tool to get the current config, then use the Write tool to update `~/.plexor/config.json`:
|
|
30
|
-
- Set `preferredProvider` to: "auto", "deepseek", "mistral", or "gemini"
|
|
31
|
-
|
|
32
|
-
Keep all other settings unchanged.
|
|
33
|
-
|
|
34
|
-
**Step 4: Show confirmation**
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
Plexor Provider: [SELECTED PROVIDER]
|
|
38
|
-
|
|
39
|
-
Provider options:
|
|
40
|
-
- auto: Plexor automatically selects the best provider for each task
|
|
41
|
-
- deepseek: DeepSeek models ($0.14/$0.28 per 1M tokens) - Great for code
|
|
42
|
-
- mistral: Mistral models ($0.10/$0.30 per 1M tokens) - Fast and versatile
|
|
43
|
-
- gemini: Google Gemini ($0.075/$0.30 per 1M tokens) - Good for analysis
|
|
44
|
-
|
|
45
|
-
Current provider: [preferredProvider]
|
|
46
|
-
Run /plexor-status to see your usage.
|
|
47
|
-
```
|