@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,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Plexor Logout Command
5
+ * Clear Plexor credentials and disable proxy
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
12
+ const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
13
+ const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
14
+ const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
15
+
16
+ function loadConfig() {
17
+ try {
18
+ if (!fs.existsSync(CONFIG_PATH)) {
19
+ return null;
20
+ }
21
+ const data = fs.readFileSync(CONFIG_PATH, 'utf8');
22
+ if (!data || data.trim() === '') {
23
+ return null;
24
+ }
25
+ const config = JSON.parse(data);
26
+ if (typeof config !== 'object' || config === null) {
27
+ return null;
28
+ }
29
+ return config;
30
+ } catch (err) {
31
+ if (err instanceof SyntaxError) {
32
+ console.warn('Warning: Config file is corrupted');
33
+ }
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function saveConfig(config) {
39
+ try {
40
+ if (!fs.existsSync(PLEXOR_DIR)) {
41
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
42
+ }
43
+
44
+ // Atomic write: write to temp file, then rename
45
+ const crypto = require('crypto');
46
+ const tempId = crypto.randomBytes(8).toString('hex');
47
+ const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
48
+
49
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
50
+ fs.renameSync(tempPath, CONFIG_PATH);
51
+ return true;
52
+ } catch (err) {
53
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
54
+ console.error('Error: Cannot write to ~/.plexor/config.json');
55
+ console.error(' Check file permissions or run with appropriate access.');
56
+ } else {
57
+ console.error('Failed to save config:', err.message);
58
+ }
59
+ return false;
60
+ }
61
+ }
62
+
63
+ function deleteFile(filePath) {
64
+ try {
65
+ if (fs.existsSync(filePath)) {
66
+ fs.unlinkSync(filePath);
67
+ return true;
68
+ }
69
+ } catch {
70
+ // Ignore errors
71
+ }
72
+ return false;
73
+ }
74
+
75
+ function main() {
76
+ const args = process.argv.slice(2);
77
+ const config = loadConfig();
78
+
79
+ if (!config || !config.auth?.api_key) {
80
+ console.log(`┌─────────────────────────────────────────────┐`);
81
+ console.log(`│ Not Logged In │`);
82
+ console.log(`├─────────────────────────────────────────────┤`);
83
+ console.log(`│ No active Plexor session found. │`);
84
+ console.log(`│ Run /plexor-login to authenticate. │`);
85
+ console.log(`└─────────────────────────────────────────────┘`);
86
+ return;
87
+ }
88
+
89
+ const clearCache = args.includes('--clear-cache') || args.includes('-c');
90
+
91
+ // Clear credentials
92
+ delete config.auth.api_key;
93
+ config.settings = config.settings || {};
94
+ config.settings.enabled = false;
95
+ if (!saveConfig(config)) {
96
+ process.exit(1);
97
+ }
98
+
99
+ // Clear session
100
+ deleteFile(SESSION_PATH);
101
+
102
+ // Optionally clear cache
103
+ let cacheCleared = false;
104
+ if (clearCache) {
105
+ cacheCleared = deleteFile(CACHE_PATH);
106
+ }
107
+
108
+ console.log(`┌─────────────────────────────────────────────┐`);
109
+ console.log(`│ ✓ Logged Out │`);
110
+ console.log(`├─────────────────────────────────────────────┤`);
111
+ console.log(`│ ✓ API key removed │`);
112
+ console.log(`│ ✓ Plexor proxy disabled │`);
113
+ console.log(`│ ✓ Session cleared │`);
114
+ if (clearCache) {
115
+ console.log(`│ ${cacheCleared ? '✓' : '○'} Cache cleared │`);
116
+ }
117
+ console.log(`├─────────────────────────────────────────────┤`);
118
+ console.log(`│ Run /plexor-login to re-authenticate. │`);
119
+ if (!clearCache) {
120
+ console.log(`│ Use --clear-cache to also clear cache. │`);
121
+ }
122
+ console.log(`└─────────────────────────────────────────────┘`);
123
+ }
124
+
125
+ main();
@@ -4,39 +4,24 @@ description: Log out from Plexor and clear credentials (user)
4
4
 
5
5
  # Plexor Logout
6
6
 
7
- Log out from your Plexor account and clear stored credentials.
7
+ Run this command to log out and clear credentials:
8
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 or has no `apiKey`, show:
16
- ```
17
- Plexor Logout
18
- =============
19
- You are not currently logged in.
20
- Run /plexor-login to authenticate.
9
+ ```bash
10
+ node ~/.claude/plugins/plexor/commands/plexor-logout.js
21
11
  ```
22
12
 
23
- **Step 2: Clear the API key**
24
-
25
- If authenticated, use the Write tool to update `~/.plexor/config.json`:
26
- - Remove or clear the `apiKey` field (set to empty string "")
27
- - Keep all other settings (mode, preferredProvider, etc.)
28
-
29
- **Step 3: Show confirmation**
13
+ To also clear the cache:
30
14
 
15
+ ```bash
16
+ node ~/.claude/plugins/plexor/commands/plexor-logout.js --clear-cache
31
17
  ```
32
- Plexor Logout
33
- =============
34
- Successfully logged out!
35
18
 
36
- - API key cleared from ~/.plexor/config.json
37
- - Settings preserved (mode, provider preferences)
19
+ Use the Bash tool to execute this command.
38
20
 
39
- To use Plexor again, run /plexor-login
21
+ **IMPORTANT**: After running this command and displaying the output, STOP. Do not:
22
+ - Read any files
23
+ - Explore the codebase
24
+ - Run additional commands
25
+ - Ask follow-up questions
40
26
 
41
- Dashboard: https://plexor.dev/dashboard
42
- ```
27
+ The command output is the complete response. Simply show the output and wait for the user's next input.
@@ -0,0 +1,172 @@
1
+ ---
2
+ description: First-time setup wizard for Plexor with Claude Code (user)
3
+ ---
4
+
5
+ # Plexor Setup Wizard
6
+
7
+ Guide users through first-time Plexor setup. **No manual environment variable configuration required!**
8
+
9
+ The plugin automatically configures `~/.claude/settings.json` to route all Claude Code sessions through Plexor.
10
+
11
+ ## Steps
12
+
13
+ **Step 1: Check if already configured**
14
+
15
+ Use the Read tool to check if `~/.plexor/config.json` exists and has valid configuration.
16
+ Also check `~/.claude/settings.json` for routing status.
17
+
18
+ If configured, show:
19
+ ```
20
+ Plexor Setup
21
+ ============
22
+ Already configured!
23
+
24
+ API URL: [apiUrl from config]
25
+ Mode: [mode from config]
26
+ Status: [Enabled/Disabled]
27
+ Claude Routing: [Active/Inactive]
28
+
29
+ Run /plexor-status to see your usage.
30
+ Run /plexor-settings to modify configuration.
31
+ Run /plexor-enabled off to disable routing.
32
+ ```
33
+
34
+ **Step 2: Ask about Claude subscription**
35
+
36
+ Use the AskUserQuestion tool:
37
+
38
+ Question: "How do you pay for Claude usage?"
39
+ Header: "Billing"
40
+ Options:
41
+ 1. **Claude MAX subscription (Pro/Team/Enterprise)** - I have a subscription and want Plexor for optimization & tracking (you'll still need a Plexor API key)
42
+ 2. **Pay-per-use via Plexor** - I want Plexor to handle billing and route to the cheapest provider
43
+
44
+ **Step 3A: Claude MAX User Setup**
45
+
46
+ If user selected "Yes, I have Claude MAX":
47
+
48
+ 1. Ask for their Plexor API key:
49
+ "Please provide your Plexor API key (starts with 'plx_')."
50
+ "Get one at: https://plexor.dev/dashboard"
51
+ "Your MAX subscription will be used for Claude - the Plexor key is for tracking/optimization."
52
+
53
+ 2. Use the Write tool to create `~/.plexor/config.json`:
54
+ ```json
55
+ {
56
+ "version": 1,
57
+ "auth": {
58
+ "api_key": "[user's Plexor key]",
59
+ "mode": "oauth_passthrough",
60
+ "authenticated_at": "[current ISO timestamp]"
61
+ },
62
+ "settings": {
63
+ "enabled": true,
64
+ "apiUrl": "https://api.plexor.dev",
65
+ "mode": "balanced",
66
+ "localCacheEnabled": true
67
+ }
68
+ }
69
+ ```
70
+
71
+ 3. Use the Write tool to update `~/.claude/settings.json` env block:
72
+ ```json
73
+ {
74
+ "env": {
75
+ "ANTHROPIC_BASE_URL": "https://api.plexor.dev/gateway/anthropic",
76
+ "ANTHROPIC_AUTH_TOKEN": "[user's Plexor key]"
77
+ }
78
+ }
79
+ ```
80
+ Note: Preserve any existing settings, just add/update the env block.
81
+
82
+ 4. Show the user:
83
+ ```
84
+ Plexor Setup - Claude MAX User
85
+ ==============================
86
+ Setup Complete! No manual configuration needed.
87
+
88
+ What was configured:
89
+ - ~/.plexor/config.json (Plexor plugin settings)
90
+ - ~/.claude/settings.json (automatic Claude Code routing)
91
+
92
+ How it works:
93
+ - All Claude Code sessions now route through Plexor
94
+ - Your MAX subscription OAuth token is passed through
95
+ - You keep your MAX benefits ($0 cost, 20x rate limits)
96
+ - Plexor optimizes prompts and tracks usage
97
+
98
+ Commands:
99
+ - /plexor-status - See your usage stats
100
+ - /plexor-enabled off - Temporarily disable Plexor
101
+ - /plexor-enabled on - Re-enable Plexor
102
+
103
+ Changes take effect immediately in all Claude Code sessions!
104
+ ```
105
+
106
+ **Step 3B: API Key User Setup**
107
+
108
+ If user selected "No, I'll use a Plexor API key":
109
+
110
+ 1. Ask for their Plexor API key:
111
+ "Please provide your Plexor API key (starts with 'plx_')."
112
+ "Get one at: https://plexor.dev/dashboard"
113
+
114
+ 2. Once they provide the key, use the Write tool to create `~/.plexor/config.json`:
115
+ ```json
116
+ {
117
+ "version": 1,
118
+ "auth": {
119
+ "api_key": "[user's API key]",
120
+ "mode": "api_key",
121
+ "authenticated_at": "[current ISO timestamp]"
122
+ },
123
+ "settings": {
124
+ "enabled": true,
125
+ "apiUrl": "https://api.plexor.dev",
126
+ "preferred_provider": "auto",
127
+ "mode": "balanced",
128
+ "localCacheEnabled": true
129
+ }
130
+ }
131
+ ```
132
+
133
+ 3. Use the Write tool to update `~/.claude/settings.json` env block:
134
+ ```json
135
+ {
136
+ "env": {
137
+ "ANTHROPIC_BASE_URL": "https://api.plexor.dev/gateway/anthropic",
138
+ "ANTHROPIC_AUTH_TOKEN": "[user's Plexor key]"
139
+ }
140
+ }
141
+ ```
142
+ Note: Preserve any existing settings, just add/update the env block.
143
+
144
+ 4. Show the user:
145
+ ```
146
+ Plexor Setup - API Key User
147
+ ===========================
148
+ Setup Complete! No manual configuration needed.
149
+
150
+ What was configured:
151
+ - ~/.plexor/config.json (Plexor plugin settings)
152
+ - ~/.claude/settings.json (automatic Claude Code routing)
153
+
154
+ How it works:
155
+ - All Claude Code sessions now route through Plexor
156
+ - Plexor picks the best provider (can save up to 90%)
157
+ - Your usage is tracked and optimized
158
+
159
+ Commands:
160
+ - /plexor-status - See your usage and savings
161
+ - /plexor-mode eco - Maximize savings
162
+ - /plexor-mode quality - Maximize quality
163
+ - /plexor-enabled off - Temporarily disable Plexor
164
+
165
+ Changes take effect immediately in all Claude Code sessions!
166
+ ```
167
+
168
+ **IMPORTANT NOTES**:
169
+ - The `~/.claude/settings.json` env block is the KEY mechanism that routes Claude Code through Plexor
170
+ - ANTHROPIC_AUTH_TOKEN takes precedence over ANTHROPIC_API_KEY (use AUTH_TOKEN for the Plexor key)
171
+ - Changes take effect immediately - no shell restart needed
172
+ - After completing setup, STOP. Do not run additional commands.
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Plexor Status Command
5
+ * Displays formatted status with usage statistics
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const https = require('https');
11
+
12
+ const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
13
+ const SESSION_PATH = path.join(process.env.HOME, '.plexor', 'session.json');
14
+ const CLAUDE_SETTINGS_PATH = path.join(process.env.HOME, '.claude', 'settings.json');
15
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
16
+
17
+ /**
18
+ * Check if Claude Code is actually routing through Plexor
19
+ * by reading ~/.claude/settings.json
20
+ */
21
+ function getRoutingStatus() {
22
+ try {
23
+ const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
24
+ const settings = JSON.parse(data);
25
+ const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
26
+ const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
27
+ const isPlexorRouting = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
28
+ return {
29
+ active: isPlexorRouting && hasToken,
30
+ baseUrl,
31
+ isStaging: baseUrl.includes('staging')
32
+ };
33
+ } catch {
34
+ return { active: false, baseUrl: null, isStaging: false };
35
+ }
36
+ }
37
+
38
+ function loadSessionStats() {
39
+ try {
40
+ const data = fs.readFileSync(SESSION_PATH, 'utf8');
41
+ const session = JSON.parse(data);
42
+ // Check if session has expired
43
+ if (Date.now() - session.last_activity > SESSION_TIMEOUT_MS) {
44
+ return null;
45
+ }
46
+ return session;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Validate API key format
54
+ * @param {string} key - API key to validate
55
+ * @returns {boolean} true if valid format
56
+ */
57
+ function isValidApiKeyFormat(key) {
58
+ return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
59
+ }
60
+
61
+ /**
62
+ * Load config file with integrity checking
63
+ * @returns {Object|null} config object or null if invalid
64
+ */
65
+ function loadConfig() {
66
+ try {
67
+ if (!fs.existsSync(CONFIG_PATH)) {
68
+ return null;
69
+ }
70
+ const data = fs.readFileSync(CONFIG_PATH, 'utf8');
71
+ if (!data || data.trim() === '') {
72
+ return null;
73
+ }
74
+ const config = JSON.parse(data);
75
+ if (typeof config !== 'object' || config === null) {
76
+ return null;
77
+ }
78
+ return config;
79
+ } catch (err) {
80
+ if (err instanceof SyntaxError) {
81
+ console.log('Config file is corrupted. Run /plexor-login to reconfigure.');
82
+ }
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Check for environment mismatch between config and routing
89
+ */
90
+ function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
91
+ if (!configApiUrl || !routingBaseUrl) return null;
92
+
93
+ const configIsStaging = configApiUrl.includes('staging');
94
+ const routingIsStaging = routingBaseUrl.includes('staging');
95
+
96
+ if (configIsStaging !== routingIsStaging) {
97
+ return {
98
+ config: configIsStaging ? 'staging' : 'production',
99
+ routing: routingIsStaging ? 'staging' : 'production'
100
+ };
101
+ }
102
+ return null;
103
+ }
104
+
105
+ async function main() {
106
+ // Read config with integrity checking
107
+ const config = loadConfig();
108
+ if (!config) {
109
+ console.log('Not configured. Run /plexor-login first.');
110
+ process.exit(1);
111
+ }
112
+
113
+ const apiKey = config.auth?.api_key;
114
+ const enabled = config.settings?.enabled ?? false;
115
+ const mode = config.settings?.mode || 'balanced';
116
+ const provider = config.settings?.preferred_provider || 'auto';
117
+ const localCache = config.settings?.localCacheEnabled ?? false;
118
+ const apiUrl = config.settings?.apiUrl || 'https://api.plexor.dev';
119
+
120
+ if (!apiKey) {
121
+ console.log('Not authenticated. Run /plexor-login first.');
122
+ process.exit(1);
123
+ }
124
+
125
+ // Validate API key format
126
+ if (!isValidApiKeyFormat(apiKey)) {
127
+ console.log('Invalid API key format. Keys must start with "plx_" and be at least 20 characters.');
128
+ console.log('Run /plexor-login with a valid API key.');
129
+ process.exit(1);
130
+ }
131
+
132
+ // Fetch user info and stats
133
+ let user = { email: 'Unknown', tier: { name: 'Free', limits: {} } };
134
+ let stats = { period: {}, summary: {} };
135
+
136
+ try {
137
+ [user, stats] = await Promise.all([
138
+ fetchJson(apiUrl, '/v1/user', apiKey),
139
+ fetchJson(apiUrl, '/v1/stats', apiKey)
140
+ ]);
141
+ } catch (err) {
142
+ // Continue with defaults if API fails
143
+ }
144
+
145
+ // Load session stats
146
+ const session = loadSessionStats();
147
+
148
+ // Extract data
149
+ const email = user.email || 'Unknown';
150
+ const tierName = user.tier?.name || 'Free';
151
+ const monthlyOpts = user.tier?.limits?.monthly_optimizations || '∞';
152
+ const monthlyComps = user.tier?.limits?.monthly_completions || '∞';
153
+
154
+ const period = stats.period || {};
155
+ const summary = stats.summary || {};
156
+
157
+ const formatDate = (iso) => {
158
+ if (!iso) return '?';
159
+ const d = new Date(iso);
160
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
161
+ };
162
+ const weekRange = `${formatDate(period.start)} - ${formatDate(period.end)}`;
163
+
164
+ // Format numbers
165
+ const formatNum = (n) => (n || 0).toLocaleString();
166
+ const formatPct = (n) => (n || 0).toFixed(1);
167
+ const formatCost = (n) => (n || 0).toFixed(2);
168
+
169
+ const status = enabled ? '● Active' : '○ Inactive';
170
+ const optEnabled = enabled ? 'Enabled' : 'Disabled';
171
+ const cacheEnabled = localCache ? 'Enabled' : 'Disabled';
172
+ const cacheRate = formatPct((summary.cache_hit_rate || 0) * 100);
173
+
174
+ // Build dashboard URL from API URL
175
+ // API: https://api.plexor.dev or https://staging.api.plexor.dev
176
+ // Dashboard: https://plexor.dev/dashboard or https://staging.plexor.dev/dashboard
177
+ let dashboardUrl = 'https://plexor.dev/dashboard';
178
+ try {
179
+ const url = new URL(apiUrl);
180
+ // Remove 'api.' prefix from hostname if present
181
+ const host = url.hostname.replace(/^api\./, '').replace(/\.api\./, '.');
182
+ dashboardUrl = `${url.protocol}//${host}/dashboard`;
183
+ } catch {
184
+ // If URL parsing fails, use default
185
+ }
186
+
187
+ // Output formatted status - each line is exactly 43 chars inner width
188
+ const line = (content) => ` │ ${content.padEnd(43)}│`;
189
+
190
+ // Session stats formatting
191
+ const formatDuration = (startedAt) => {
192
+ if (!startedAt) return '0m';
193
+ const elapsed = Date.now() - new Date(startedAt).getTime();
194
+ const minutes = Math.floor(elapsed / 60000);
195
+ if (minutes < 60) return `${minutes}m`;
196
+ const hours = Math.floor(minutes / 60);
197
+ return `${hours}h ${minutes % 60}m`;
198
+ };
199
+
200
+ const sessionDuration = session ? formatDuration(session.started_at) : '0m';
201
+ const sessionRequests = session ? formatNum(session.requests) : '0';
202
+ const sessionOptimizations = session ? formatNum(session.optimizations) : '0';
203
+ const sessionCacheHits = session ? formatNum(session.cache_hits) : '0';
204
+ const sessionTokensSaved = session ? formatNum(session.tokens_saved) : '0';
205
+ const sessionTokensSavedPct = session && session.original_tokens > 0
206
+ ? formatPct((session.tokens_saved / session.original_tokens) * 100)
207
+ : '0.0';
208
+ const sessionCostSaved = session ? formatCost(session.cost_saved) : '0.00';
209
+
210
+ // Build session section (only show if session exists)
211
+ const sessionSection = session ? ` ├─────────────────────────────────────────────┤
212
+ ${line(`This Session (${sessionDuration})`)}
213
+ ${line(`├── Requests: ${sessionRequests}`)}
214
+ ${line(`├── Optimizations: ${sessionOptimizations}`)}
215
+ ${line(`├── Cache hits: ${sessionCacheHits}`)}
216
+ ${line(`├── Tokens saved: ${sessionTokensSaved} (${sessionTokensSavedPct}%)`)}
217
+ ${line(`└── Cost saved: $${sessionCostSaved}`)}
218
+ ` : '';
219
+
220
+ // Get routing status from Claude settings.json
221
+ const routing = getRoutingStatus();
222
+ const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
223
+ const envLabel = routing.isStaging ? '(staging)' : '(production)';
224
+
225
+ // Check for environment mismatch
226
+ const envMismatch = checkEnvironmentMismatch(apiUrl, routing.baseUrl);
227
+ const mismatchWarning = envMismatch
228
+ ? ` ⚠ Warning: Config uses ${envMismatch.config} but routing is ${envMismatch.routing}\n`
229
+ : '';
230
+
231
+ if (mismatchWarning) {
232
+ console.log(mismatchWarning);
233
+ }
234
+
235
+ console.log(` ┌─────────────────────────────────────────────┐
236
+ ${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
237
+ ├─────────────────────────────────────────────┤
238
+ ${line(`Account: ${tierName}`)}
239
+ ${line(`Email: ${email}`)}
240
+ ${line(`Status: ${status}`)}
241
+ ${sessionSection} ├─────────────────────────────────────────────┤
242
+ ${line(`This Week (${weekRange})`)}
243
+ ${line(`├── Requests: ${formatNum(summary.total_requests)}`)}
244
+ ${line(`├── Original tokens: ${formatNum(summary.original_tokens)}`)}
245
+ ${line(`├── Optimized tokens: ${formatNum(summary.optimized_tokens)}`)}
246
+ ${line(`├── Tokens saved: ${formatNum(summary.tokens_saved)} (${formatPct(summary.tokens_saved_percent)}%)`)}
247
+ ${line(`├── Baseline cost: $${formatCost(summary.baseline_cost)}`)}
248
+ ${line(`├── Actual cost: $${formatCost(summary.total_cost)}`)}
249
+ ${line(`└── Cost saved: $${formatCost(summary.cost_saved)} (${formatPct(summary.cost_saved_percent)}%)`)}
250
+ ├─────────────────────────────────────────────┤
251
+ ${line('Performance')}
252
+ ${line(`└── Cache hit rate: ${cacheRate}%`)}
253
+ ├─────────────────────────────────────────────┤
254
+ ${line('Limits')}
255
+ ${line(`├── Monthly optimizations: ${formatNum(monthlyOpts)}`)}
256
+ ${line(`└── Monthly completions: ${formatNum(monthlyComps)}`)}
257
+ ├─────────────────────────────────────────────┤
258
+ ${line('Settings')}
259
+ ${line(`├── Optimization: ${optEnabled}`)}
260
+ ${line(`├── Local cache: ${cacheEnabled}`)}
261
+ ${line(`├── Mode: ${mode}`)}
262
+ ${line(`├── Provider routing: ${provider}`)}
263
+ ${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https://', '').substring(0, 30) : 'not configured'}`)}
264
+ └─────────────────────────────────────────────┘
265
+
266
+ Dashboard: ${dashboardUrl}
267
+ `);
268
+ }
269
+
270
+ function fetchJson(apiUrl, endpoint, apiKey) {
271
+ return new Promise((resolve, reject) => {
272
+ let url;
273
+ try {
274
+ url = new URL(`${apiUrl}${endpoint}`);
275
+ } catch {
276
+ reject(new Error('Invalid API URL'));
277
+ return;
278
+ }
279
+
280
+ const options = {
281
+ hostname: url.hostname,
282
+ port: 443,
283
+ path: url.pathname,
284
+ method: 'GET',
285
+ headers: {
286
+ 'X-Plexor-Key': apiKey
287
+ }
288
+ };
289
+
290
+ const req = https.request(options, (res) => {
291
+ let data = '';
292
+ res.on('data', chunk => data += chunk);
293
+ res.on('end', () => {
294
+ // Check HTTP status code first
295
+ if (res.statusCode === 401) {
296
+ reject(new Error('Invalid API key'));
297
+ return;
298
+ }
299
+ if (res.statusCode === 403) {
300
+ reject(new Error('Access denied'));
301
+ return;
302
+ }
303
+ if (res.statusCode >= 500) {
304
+ reject(new Error('Server error'));
305
+ return;
306
+ }
307
+ if (res.statusCode !== 200) {
308
+ reject(new Error(`HTTP ${res.statusCode}`));
309
+ return;
310
+ }
311
+
312
+ // Check for empty response
313
+ if (!data || data.trim() === '') {
314
+ reject(new Error('Empty response'));
315
+ return;
316
+ }
317
+
318
+ // Parse JSON
319
+ try {
320
+ const parsed = JSON.parse(data);
321
+ if (parsed === null) {
322
+ reject(new Error('Null response'));
323
+ return;
324
+ }
325
+ resolve(parsed);
326
+ } catch {
327
+ reject(new Error('Invalid JSON response'));
328
+ }
329
+ });
330
+ });
331
+
332
+ req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
333
+ req.setTimeout(5000, () => {
334
+ req.destroy();
335
+ reject(new Error('Request timeout'));
336
+ });
337
+ req.end();
338
+ });
339
+ }
340
+
341
+ main().catch(err => {
342
+ console.error('Error:', err.message);
343
+ process.exit(1);
344
+ });