@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.
@@ -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",
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 "$@"
@@ -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 CLAUDE_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
16
- const PLEXOR_CONFIG_DIR = path.join(os.homedir(), '.plexor');
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
- // Get list of command files
27
- const files = fs.readdirSync(COMMANDS_SOURCE)
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 (files.length === 0) {
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
- for (const file of files) {
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
- // Print success message
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 (backed_up.length > 0) {
72
- console.log('');
73
- console.log(' Existing files backed up (.backup):');
74
- backed_up.forEach(f => console.log(` ${f}`));
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(' Next steps:');
79
- console.log(' 1. Open Claude Code');
80
- console.log(' 2. Run /plexor-login to authenticate');
81
- console.log(' 3. Start saving on LLM costs!');
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');