@plexor-dev/claude-code-plugin 0.1.0-beta.3 → 0.1.0-beta.31
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 +221 -0
- package/commands/plexor-enabled.md +15 -41
- package/commands/plexor-login.js +218 -0
- package/commands/plexor-login.md +13 -62
- package/commands/plexor-logout.js +125 -0
- package/commands/plexor-logout.md +13 -28
- package/commands/plexor-setup.md +172 -0
- package/commands/plexor-status.js +344 -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 +304 -0
- package/package.json +8 -1
- package/scripts/plexor-cli.sh +48 -0
- package/scripts/postinstall.js +216 -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,304 @@
|
|
|
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
|
+
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
|
|
19
|
+
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
20
|
+
const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
|
|
21
|
+
|
|
22
|
+
// Plexor gateway endpoints
|
|
23
|
+
const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
24
|
+
const PLEXOR_PROD_URL = 'https://api.plexor.dev/gateway/anthropic';
|
|
25
|
+
|
|
26
|
+
class ClaudeSettingsManager {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.settingsPath = SETTINGS_PATH;
|
|
29
|
+
this.claudeDir = CLAUDE_DIR;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load current Claude settings with integrity checking
|
|
34
|
+
* @returns {Object} settings object or empty object if not found/corrupted
|
|
35
|
+
*/
|
|
36
|
+
load() {
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(this.settingsPath)) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
const data = fs.readFileSync(this.settingsPath, 'utf8');
|
|
42
|
+
|
|
43
|
+
// Check for empty file
|
|
44
|
+
if (!data || data.trim() === '') {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parsed = JSON.parse(data);
|
|
49
|
+
|
|
50
|
+
// Basic schema validation - must be an object
|
|
51
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
52
|
+
console.warn('Warning: Claude settings file has invalid format, using defaults');
|
|
53
|
+
this._backupCorruptedFile();
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parsed;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err.code === 'ENOENT') {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
// JSON parse error or corrupted file
|
|
63
|
+
if (err instanceof SyntaxError) {
|
|
64
|
+
console.warn('Warning: Claude settings file is corrupted, using defaults');
|
|
65
|
+
console.warn(' A backup has been saved to settings.json.corrupted');
|
|
66
|
+
this._backupCorruptedFile();
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
// Permission error
|
|
70
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
71
|
+
console.warn(`Warning: Cannot read ${this.settingsPath} (permission denied)`);
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
console.warn('Warning: Failed to load Claude settings:', err.message);
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Backup a corrupted settings file for debugging
|
|
81
|
+
*/
|
|
82
|
+
_backupCorruptedFile() {
|
|
83
|
+
try {
|
|
84
|
+
if (fs.existsSync(this.settingsPath)) {
|
|
85
|
+
const backupPath = this.settingsPath + '.corrupted';
|
|
86
|
+
fs.copyFileSync(this.settingsPath, backupPath);
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Ignore backup errors
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Save Claude settings using atomic write pattern
|
|
95
|
+
* Prevents race conditions by writing to temp file then renaming
|
|
96
|
+
* @param {Object} settings - settings object to save
|
|
97
|
+
* @returns {boolean} success status
|
|
98
|
+
*/
|
|
99
|
+
save(settings) {
|
|
100
|
+
try {
|
|
101
|
+
// Ensure .claude directory exists
|
|
102
|
+
if (!fs.existsSync(this.claudeDir)) {
|
|
103
|
+
fs.mkdirSync(this.claudeDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Atomic write: write to temp file, then rename
|
|
107
|
+
// This prevents race conditions where concurrent writes corrupt the file
|
|
108
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
109
|
+
const tempPath = path.join(this.claudeDir, `.settings.${tempId}.tmp`);
|
|
110
|
+
|
|
111
|
+
// Write to temp file
|
|
112
|
+
const content = JSON.stringify(settings, null, 2);
|
|
113
|
+
fs.writeFileSync(tempPath, content, { mode: 0o600 });
|
|
114
|
+
|
|
115
|
+
// Atomic rename (on POSIX systems, rename is atomic)
|
|
116
|
+
fs.renameSync(tempPath, this.settingsPath);
|
|
117
|
+
|
|
118
|
+
return true;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// Clean error message for permission errors
|
|
121
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
122
|
+
console.error(`Error: Cannot write to ${this.settingsPath}`);
|
|
123
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
124
|
+
} else {
|
|
125
|
+
console.error('Failed to save Claude settings:', err.message);
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Enable Plexor routing by setting env vars in settings.json
|
|
133
|
+
*
|
|
134
|
+
* This is the KEY mechanism: setting ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
|
|
135
|
+
* in the env block redirects ALL Claude Code sessions to Plexor automatically.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} apiKey - Plexor API key (plx_*)
|
|
138
|
+
* @param {Object} options - { useStaging: boolean }
|
|
139
|
+
* @returns {boolean} success status
|
|
140
|
+
*/
|
|
141
|
+
enablePlexorRouting(apiKey, options = {}) {
|
|
142
|
+
// PRODUCTION PACKAGE - defaults to production API
|
|
143
|
+
const { useStaging = false } = options;
|
|
144
|
+
const apiUrl = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const settings = this.load();
|
|
148
|
+
|
|
149
|
+
// Initialize env block if doesn't exist
|
|
150
|
+
if (!settings.env) {
|
|
151
|
+
settings.env = {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Set the magic environment variables
|
|
155
|
+
// ANTHROPIC_AUTH_TOKEN has higher precedence than ANTHROPIC_API_KEY
|
|
156
|
+
settings.env.ANTHROPIC_BASE_URL = apiUrl;
|
|
157
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
158
|
+
|
|
159
|
+
const success = this.save(settings);
|
|
160
|
+
|
|
161
|
+
if (success) {
|
|
162
|
+
console.log(`✓ Plexor routing enabled`);
|
|
163
|
+
console.log(` Base URL: ${apiUrl}`);
|
|
164
|
+
console.log(` API Key: ${apiKey.substring(0, 12)}...`);
|
|
165
|
+
console.log(`\n All Claude Code sessions will now route through Plexor.`);
|
|
166
|
+
console.log(` Changes take effect immediately (no restart needed).`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return success;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error('Failed to enable Plexor routing:', err.message);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Disable Plexor routing by removing env vars from settings.json
|
|
178
|
+
*
|
|
179
|
+
* This restores direct connection to Anthropic API.
|
|
180
|
+
*
|
|
181
|
+
* @returns {boolean} success status
|
|
182
|
+
*/
|
|
183
|
+
disablePlexorRouting() {
|
|
184
|
+
try {
|
|
185
|
+
const settings = this.load();
|
|
186
|
+
|
|
187
|
+
if (!settings.env) {
|
|
188
|
+
console.log('Plexor routing is not currently enabled.');
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Remove Plexor-specific env vars
|
|
193
|
+
delete settings.env.ANTHROPIC_BASE_URL;
|
|
194
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
195
|
+
|
|
196
|
+
// Clean up empty env block
|
|
197
|
+
if (Object.keys(settings.env).length === 0) {
|
|
198
|
+
delete settings.env;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const success = this.save(settings);
|
|
202
|
+
|
|
203
|
+
if (success) {
|
|
204
|
+
console.log('✓ Plexor routing disabled');
|
|
205
|
+
console.log(' Claude Code will now connect directly to Anthropic.');
|
|
206
|
+
console.log(' Changes take effect immediately (no restart needed).');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return success;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error('Failed to disable Plexor routing:', err.message);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get current routing status
|
|
218
|
+
* @returns {Object} { enabled: boolean, baseUrl: string|null, hasToken: boolean }
|
|
219
|
+
*/
|
|
220
|
+
getRoutingStatus() {
|
|
221
|
+
try {
|
|
222
|
+
const settings = this.load();
|
|
223
|
+
|
|
224
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || null;
|
|
225
|
+
const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
226
|
+
|
|
227
|
+
// Check if routing to Plexor
|
|
228
|
+
const isPlexorRouting = baseUrl && (
|
|
229
|
+
baseUrl.includes('plexor') ||
|
|
230
|
+
baseUrl.includes('staging.api')
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
enabled: isPlexorRouting,
|
|
235
|
+
baseUrl,
|
|
236
|
+
hasToken,
|
|
237
|
+
isStaging: baseUrl?.includes('staging') || false,
|
|
238
|
+
tokenPreview: hasToken ? settings.env.ANTHROPIC_AUTH_TOKEN.substring(0, 12) + '...' : null
|
|
239
|
+
};
|
|
240
|
+
} catch {
|
|
241
|
+
return { enabled: false, baseUrl: null, hasToken: false };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Update just the API key without changing other settings
|
|
247
|
+
* @param {string} apiKey - new Plexor API key
|
|
248
|
+
* @returns {boolean} success status
|
|
249
|
+
*/
|
|
250
|
+
updateApiKey(apiKey) {
|
|
251
|
+
try {
|
|
252
|
+
const settings = this.load();
|
|
253
|
+
|
|
254
|
+
if (!settings.env) {
|
|
255
|
+
settings.env = {};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
259
|
+
return this.save(settings);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error('Failed to update API key:', err.message);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Switch between staging and production
|
|
268
|
+
* @param {boolean} useStaging - true for staging, false for production
|
|
269
|
+
* @returns {boolean} success status
|
|
270
|
+
*/
|
|
271
|
+
setEnvironment(useStaging) {
|
|
272
|
+
try {
|
|
273
|
+
const settings = this.load();
|
|
274
|
+
|
|
275
|
+
if (!settings.env?.ANTHROPIC_BASE_URL) {
|
|
276
|
+
console.log('Plexor routing is not enabled. Run /plexor-login first.');
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
settings.env.ANTHROPIC_BASE_URL = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
|
|
281
|
+
|
|
282
|
+
const success = this.save(settings);
|
|
283
|
+
if (success) {
|
|
284
|
+
console.log(`✓ Switched to ${useStaging ? 'staging' : 'production'} environment`);
|
|
285
|
+
}
|
|
286
|
+
return success;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error('Failed to switch environment:', err.message);
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Export singleton instance and class
|
|
295
|
+
const settingsManager = new ClaudeSettingsManager();
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
ClaudeSettingsManager,
|
|
299
|
+
settingsManager,
|
|
300
|
+
CLAUDE_DIR,
|
|
301
|
+
SETTINGS_PATH,
|
|
302
|
+
PLEXOR_STAGING_URL,
|
|
303
|
+
PLEXOR_PROD_URL
|
|
304
|
+
};
|
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.31",
|
|
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,124 @@
|
|
|
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
|
|
16
|
-
const
|
|
73
|
+
const LIB_SOURCE = path.join(__dirname, '..', 'lib');
|
|
74
|
+
const CLAUDE_COMMANDS_DIR = path.join(HOME_DIR, '.claude', 'commands');
|
|
75
|
+
const PLEXOR_PLUGINS_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'commands');
|
|
76
|
+
const PLEXOR_LIB_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'lib');
|
|
77
|
+
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
78
|
+
const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
|
|
79
|
+
|
|
80
|
+
// Default configuration for new installs
|
|
81
|
+
// PRODUCTION PACKAGE - uses production API
|
|
82
|
+
const DEFAULT_CONFIG = {
|
|
83
|
+
version: 1,
|
|
84
|
+
auth: {
|
|
85
|
+
mode: "pending",
|
|
86
|
+
authenticated_at: null
|
|
87
|
+
},
|
|
88
|
+
settings: {
|
|
89
|
+
enabled: true,
|
|
90
|
+
apiUrl: "https://api.plexor.dev",
|
|
91
|
+
mode: "balanced",
|
|
92
|
+
localCacheEnabled: true
|
|
93
|
+
}
|
|
94
|
+
};
|
|
17
95
|
|
|
18
96
|
function main() {
|
|
19
97
|
try {
|
|
98
|
+
// Get target user info for chown (if running with sudo)
|
|
99
|
+
const targetUser = getTargetUserIds();
|
|
100
|
+
|
|
20
101
|
// Create ~/.claude/commands/ if not exists
|
|
21
102
|
fs.mkdirSync(CLAUDE_COMMANDS_DIR, { recursive: true });
|
|
22
103
|
|
|
104
|
+
// Create ~/.claude/plugins/plexor/commands/ for JS executors
|
|
105
|
+
fs.mkdirSync(PLEXOR_PLUGINS_DIR, { recursive: true });
|
|
106
|
+
|
|
107
|
+
// Create ~/.claude/plugins/plexor/lib/ for shared modules
|
|
108
|
+
fs.mkdirSync(PLEXOR_LIB_DIR, { recursive: true });
|
|
109
|
+
|
|
23
110
|
// Create ~/.plexor/ with secure permissions (owner only)
|
|
24
111
|
fs.mkdirSync(PLEXOR_CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
25
112
|
|
|
26
|
-
//
|
|
27
|
-
|
|
113
|
+
// Create default config.json if it doesn't exist
|
|
114
|
+
let configCreated = false;
|
|
115
|
+
if (!fs.existsSync(PLEXOR_CONFIG_FILE)) {
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
PLEXOR_CONFIG_FILE,
|
|
118
|
+
JSON.stringify(DEFAULT_CONFIG, null, 2),
|
|
119
|
+
{ mode: 0o600 }
|
|
120
|
+
);
|
|
121
|
+
configCreated = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get list of command files (.md for Claude, .js for executors)
|
|
125
|
+
const mdFiles = fs.readdirSync(COMMANDS_SOURCE)
|
|
28
126
|
.filter(f => f.endsWith('.md'));
|
|
127
|
+
const jsFiles = fs.readdirSync(COMMANDS_SOURCE)
|
|
128
|
+
.filter(f => f.endsWith('.js'));
|
|
29
129
|
|
|
30
|
-
if (
|
|
130
|
+
if (mdFiles.length === 0) {
|
|
31
131
|
console.error('No command files found in package. Installation may be corrupt.');
|
|
32
132
|
process.exit(1);
|
|
33
133
|
}
|
|
@@ -35,7 +135,8 @@ function main() {
|
|
|
35
135
|
const installed = [];
|
|
36
136
|
const backed_up = [];
|
|
37
137
|
|
|
38
|
-
|
|
138
|
+
// Copy .md command files to ~/.claude/commands/
|
|
139
|
+
for (const file of mdFiles) {
|
|
39
140
|
const src = path.join(COMMANDS_SOURCE, file);
|
|
40
141
|
const dest = path.join(CLAUDE_COMMANDS_DIR, file);
|
|
41
142
|
|
|
@@ -55,34 +156,125 @@ function main() {
|
|
|
55
156
|
installed.push(file.replace('.md', ''));
|
|
56
157
|
}
|
|
57
158
|
|
|
58
|
-
//
|
|
159
|
+
// Copy .js executor files to ~/.claude/plugins/plexor/commands/
|
|
160
|
+
const jsInstalled = [];
|
|
161
|
+
for (const file of jsFiles) {
|
|
162
|
+
const src = path.join(COMMANDS_SOURCE, file);
|
|
163
|
+
const dest = path.join(PLEXOR_PLUGINS_DIR, file);
|
|
164
|
+
fs.copyFileSync(src, dest);
|
|
165
|
+
// Make executable
|
|
166
|
+
fs.chmodSync(dest, 0o755);
|
|
167
|
+
jsInstalled.push(file);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Copy lib files to ~/.claude/plugins/plexor/lib/
|
|
171
|
+
// CRITICAL: These are required for commands to work
|
|
172
|
+
const libInstalled = [];
|
|
173
|
+
if (fs.existsSync(LIB_SOURCE)) {
|
|
174
|
+
const libFiles = fs.readdirSync(LIB_SOURCE).filter(f => f.endsWith('.js'));
|
|
175
|
+
if (libFiles.length === 0) {
|
|
176
|
+
console.warn(' ⚠ Warning: No lib files found in package. Commands may not work.');
|
|
177
|
+
}
|
|
178
|
+
for (const file of libFiles) {
|
|
179
|
+
try {
|
|
180
|
+
const src = path.join(LIB_SOURCE, file);
|
|
181
|
+
const dest = path.join(PLEXOR_LIB_DIR, file);
|
|
182
|
+
fs.copyFileSync(src, dest);
|
|
183
|
+
libInstalled.push(file);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(` ✗ Failed to copy lib/${file}: ${err.message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
console.error(' ✗ CRITICAL: lib/ directory not found in package.');
|
|
190
|
+
console.error(' Commands will fail. Please reinstall the package.');
|
|
191
|
+
console.error(` Expected location: ${LIB_SOURCE}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Verify critical lib file exists
|
|
195
|
+
const criticalLibFile = path.join(PLEXOR_LIB_DIR, 'settings-manager.js');
|
|
196
|
+
if (!fs.existsSync(criticalLibFile)) {
|
|
197
|
+
console.error('');
|
|
198
|
+
console.error(' ✗ CRITICAL: settings-manager.js was not installed.');
|
|
199
|
+
console.error(' This file is required for commands to work.');
|
|
200
|
+
console.error(' Try reinstalling: npm install @plexor-dev/claude-code-plugin');
|
|
201
|
+
console.error('');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fix file ownership when running with sudo
|
|
205
|
+
// Files are created as root but should be owned by the original user
|
|
206
|
+
if (targetUser) {
|
|
207
|
+
const { uid, gid } = targetUser;
|
|
208
|
+
// Chown the .claude directory and all contents we created
|
|
209
|
+
chownRecursive(path.join(HOME_DIR, '.claude'), uid, gid);
|
|
210
|
+
// Chown the .plexor directory and all contents
|
|
211
|
+
chownRecursive(PLEXOR_CONFIG_DIR, uid, gid);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Detect shell type
|
|
215
|
+
const shell = process.env.SHELL || '';
|
|
216
|
+
const isZsh = shell.includes('zsh');
|
|
217
|
+
const shellRc = isZsh ? '~/.zshrc' : '~/.bashrc';
|
|
218
|
+
|
|
219
|
+
// Print success message with clear onboarding steps
|
|
59
220
|
console.log('');
|
|
60
|
-
console.log('
|
|
61
|
-
console.log(' ║
|
|
62
|
-
console.log(' ║ Plexor Claude Code Plugin installed successfully!
|
|
63
|
-
console.log(' ║
|
|
64
|
-
console.log('
|
|
221
|
+
console.log(' ╔═══════════════════════════════════════════════════════════════════╗');
|
|
222
|
+
console.log(' ║ ║');
|
|
223
|
+
console.log(' ║ Plexor Claude Code Plugin installed successfully! ║');
|
|
224
|
+
console.log(' ║ ║');
|
|
225
|
+
console.log(' ╚═══════════════════════════════════════════════════════════════════╝');
|
|
65
226
|
console.log('');
|
|
66
|
-
console.log(' Commands installed to ~/.claude/commands/:');
|
|
67
|
-
installed.forEach(cmd => {
|
|
68
|
-
console.log(` /${cmd}`);
|
|
69
|
-
});
|
|
70
227
|
|
|
71
|
-
if (
|
|
72
|
-
console.log('');
|
|
73
|
-
|
|
74
|
-
|
|
228
|
+
if (configCreated) {
|
|
229
|
+
console.log(' ✓ Created ~/.plexor/config.json');
|
|
230
|
+
}
|
|
231
|
+
console.log(` ✓ Installed ${installed.length} slash commands to ~/.claude/commands/`);
|
|
232
|
+
if (jsInstalled.length > 0) {
|
|
233
|
+
console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/commands/`);
|
|
234
|
+
}
|
|
235
|
+
if (libInstalled.length > 0) {
|
|
236
|
+
console.log(` ✓ Installed ${libInstalled.length} lib modules to ~/.claude/plugins/plexor/lib/`);
|
|
237
|
+
}
|
|
238
|
+
if (targetUser) {
|
|
239
|
+
console.log(` ✓ Set file ownership to ${targetUser.user}`);
|
|
75
240
|
}
|
|
241
|
+
console.log('');
|
|
76
242
|
|
|
243
|
+
// CRITICAL: Make the required step VERY obvious
|
|
244
|
+
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
|
245
|
+
console.log(' │ REQUIRED: Run this command to enable Plexor routing: │');
|
|
246
|
+
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(' For Claude MAX users (OAuth):');
|
|
249
|
+
console.log('');
|
|
250
|
+
console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
251
|
+
console.log(` source ${shellRc}`);
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(' For API key users (get key at https://plexor.dev/dashboard):');
|
|
77
254
|
console.log('');
|
|
78
|
-
console.log('
|
|
79
|
-
console.log(
|
|
80
|
-
console.log(
|
|
81
|
-
console.log('
|
|
255
|
+
console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
256
|
+
console.log(` echo 'export ANTHROPIC_API_KEY="plx_your_key_here"' >> ${shellRc}`);
|
|
257
|
+
console.log(` source ${shellRc}`);
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
|
260
|
+
console.log(' │ Then start Claude Code and run: /plexor-status │');
|
|
261
|
+
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log(' Available commands:');
|
|
264
|
+
console.log(' /plexor-setup - First-time setup wizard');
|
|
265
|
+
console.log(' /plexor-login - Authenticate with API key');
|
|
266
|
+
console.log(' /plexor-status - Check connection and see savings');
|
|
267
|
+
console.log(' /plexor-enabled - Enable/disable Plexor routing');
|
|
82
268
|
console.log('');
|
|
83
269
|
console.log(' Documentation: https://plexor.dev/docs');
|
|
84
270
|
console.log('');
|
|
85
271
|
|
|
272
|
+
if (backed_up.length > 0) {
|
|
273
|
+
console.log(' Note: Existing files backed up (.backup):');
|
|
274
|
+
backed_up.forEach(f => console.log(` ${f}`));
|
|
275
|
+
console.log('');
|
|
276
|
+
}
|
|
277
|
+
|
|
86
278
|
} catch (error) {
|
|
87
279
|
console.error('');
|
|
88
280
|
console.error(' Plexor plugin installation failed');
|