@plexor-dev/claude-code-plugin-staging 0.1.0-beta.2 → 0.1.0-beta.21

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.
@@ -9,10 +9,29 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const https = require('https');
11
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
12
+ // Import centralized constants with HOME directory validation
13
+ const { HOME_DIR, CONFIG_PATH, SESSION_PATH, SESSION_TIMEOUT_MS } = require('../lib/constants');
14
+ const CLAUDE_SETTINGS_PATH = path.join(HOME_DIR, '.claude', 'settings.json');
15
+
16
+ function isManagedGatewayUrl(baseUrl = '') {
17
+ return (
18
+ baseUrl.includes('plexor') ||
19
+ baseUrl.includes('staging.api') ||
20
+ baseUrl.includes('ngrok') ||
21
+ baseUrl.includes('localtunnel') ||
22
+ baseUrl.includes('localhost') ||
23
+ baseUrl.includes('127.0.0.1')
24
+ );
25
+ }
26
+
27
+ function getPlexorAuthKey(env = {}) {
28
+ const apiKey = env.ANTHROPIC_API_KEY || '';
29
+ const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
30
+
31
+ if (apiKey.startsWith('plx_')) return apiKey;
32
+ if (authToken.startsWith('plx_')) return authToken;
33
+ return apiKey || authToken || '';
34
+ }
16
35
 
17
36
  /**
18
37
  * Check if Claude Code is actually routing through Plexor
@@ -23,10 +42,10 @@ function getRoutingStatus() {
23
42
  const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
24
43
  const settings = JSON.parse(data);
25
44
  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');
45
+ const authKey = getPlexorAuthKey(settings.env);
46
+ const isPlexorRouting = isManagedGatewayUrl(baseUrl);
28
47
  return {
29
- active: isPlexorRouting && hasToken,
48
+ active: isPlexorRouting && !!authKey,
30
49
  baseUrl,
31
50
  isStaging: baseUrl.includes('staging')
32
51
  };
@@ -35,6 +54,31 @@ function getRoutingStatus() {
35
54
  }
36
55
  }
37
56
 
57
+ /**
58
+ * Detect partial routing state where URL points to Plexor but auth is missing/invalid
59
+ * This can cause confusing auth errors for users
60
+ * @returns {Object} { partial: boolean, issue: string|null }
61
+ */
62
+ function detectPartialState() {
63
+ try {
64
+ const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
65
+ const settings = JSON.parse(data);
66
+ const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
67
+ const authKey = getPlexorAuthKey(settings.env);
68
+ const isPlexorUrl = isManagedGatewayUrl(baseUrl);
69
+
70
+ if (isPlexorUrl && !authKey) {
71
+ return { partial: true, issue: 'Plexor URL set but no Plexor auth key' };
72
+ }
73
+ if (isPlexorUrl && !authKey.startsWith('plx_')) {
74
+ return { partial: true, issue: 'Plexor URL set but auth key is not a Plexor key' };
75
+ }
76
+ return { partial: false, issue: null };
77
+ } catch {
78
+ return { partial: false, issue: null };
79
+ }
80
+ }
81
+
38
82
  function loadSessionStats() {
39
83
  try {
40
84
  const data = fs.readFileSync(SESSION_PATH, 'utf8');
@@ -49,14 +93,107 @@ function loadSessionStats() {
49
93
  }
50
94
  }
51
95
 
52
- async function main() {
53
- // Read config
54
- let config;
96
+ /**
97
+ * Validate API key format
98
+ * @param {string} key - API key to validate
99
+ * @returns {boolean} true if valid format
100
+ */
101
+ function isValidApiKeyFormat(key) {
102
+ return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
103
+ }
104
+
105
+ /**
106
+ * Load config file with integrity checking
107
+ * @returns {Object|null} config object or null if invalid
108
+ */
109
+ function loadConfig() {
55
110
  try {
111
+ if (!fs.existsSync(CONFIG_PATH)) {
112
+ return null;
113
+ }
56
114
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
57
- config = JSON.parse(data);
115
+ if (!data || data.trim() === '') {
116
+ return null;
117
+ }
118
+ const config = JSON.parse(data);
119
+ if (typeof config !== 'object' || config === null) {
120
+ return null;
121
+ }
122
+ return config;
58
123
  } catch (err) {
59
- console.log('Not configured. Run /plexor-login first.');
124
+ if (err instanceof SyntaxError) {
125
+ console.log('Config file is corrupted. Run /plexor-setup to reconfigure.');
126
+ }
127
+ return null;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Check for environment mismatch between config and routing
133
+ */
134
+ function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
135
+ if (!configApiUrl || !routingBaseUrl) return null;
136
+
137
+ const configIsStaging = configApiUrl.includes('staging');
138
+ const routingIsStaging = routingBaseUrl.includes('staging');
139
+
140
+ if (configIsStaging !== routingIsStaging) {
141
+ return {
142
+ config: configIsStaging ? 'staging' : 'production',
143
+ routing: routingIsStaging ? 'staging' : 'production'
144
+ };
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Check for state mismatch between config.json enabled flag and settings.json routing
151
+ * @param {boolean} configEnabled - enabled flag from config.json
152
+ * @param {boolean} routingActive - whether settings.json has Plexor routing configured
153
+ * @returns {Object|null} mismatch details or null if states are consistent
154
+ */
155
+ function checkStateMismatch(configEnabled, routingActive) {
156
+ if (configEnabled && !routingActive) {
157
+ return {
158
+ type: 'config-enabled-routing-inactive',
159
+ message: 'Config shows enabled but Claude routing is not configured',
160
+ suggestion: 'Run /plexor-enabled true to sync and configure routing'
161
+ };
162
+ }
163
+ if (!configEnabled && routingActive) {
164
+ return {
165
+ type: 'config-disabled-routing-active',
166
+ message: 'Config shows disabled but Claude routing is active',
167
+ suggestion: 'Run /plexor-enabled false to sync and disable routing'
168
+ };
169
+ }
170
+ return null;
171
+ }
172
+
173
+ function printHealthSummary(summary) {
174
+ const line = (content) => ` │ ${String(content).slice(0, 43).padEnd(43)}│`;
175
+ console.log(` ┌─────────────────────────────────────────────┐`);
176
+ console.log(line('Plexor health'));
177
+ console.log(` ├─────────────────────────────────────────────┤`);
178
+ console.log(line(`Installed: ${summary.installed}`));
179
+ console.log(line(`Connected: ${summary.connected}`));
180
+ console.log(line(`Routing Active: ${summary.routing}`));
181
+ console.log(line(`Verified: ${summary.verified}`));
182
+ console.log(line(`Next action: ${summary.nextAction}`));
183
+ console.log(` └─────────────────────────────────────────────┘`);
184
+ }
185
+
186
+ async function main() {
187
+ // Read config with integrity checking
188
+ const config = loadConfig();
189
+ if (!config) {
190
+ printHealthSummary({
191
+ installed: 'Missing',
192
+ connected: 'Not connected',
193
+ routing: 'Inactive',
194
+ verified: 'Not run',
195
+ nextAction: 'Run /plexor-setup'
196
+ });
60
197
  process.exit(1);
61
198
  }
62
199
 
@@ -66,21 +203,42 @@ async function main() {
66
203
  const provider = config.settings?.preferred_provider || 'auto';
67
204
  const localCache = config.settings?.localCacheEnabled ?? false;
68
205
  const apiUrl = config.settings?.apiUrl || 'https://api.plexor.dev';
206
+ const verification = config.health || {};
69
207
 
70
208
  if (!apiKey) {
71
- console.log('Not authenticated. Run /plexor-login first.');
209
+ printHealthSummary({
210
+ installed: 'OK',
211
+ connected: 'Missing key',
212
+ routing: 'Inactive',
213
+ verified: 'Not run',
214
+ nextAction: 'Run /plexor-setup'
215
+ });
216
+ process.exit(1);
217
+ }
218
+
219
+ // Validate API key format
220
+ if (!isValidApiKeyFormat(apiKey)) {
221
+ printHealthSummary({
222
+ installed: 'OK',
223
+ connected: 'Invalid key',
224
+ routing: 'Needs repair',
225
+ verified: 'Failed',
226
+ nextAction: 'Run /plexor-setup'
227
+ });
72
228
  process.exit(1);
73
229
  }
74
230
 
75
231
  // Fetch user info and stats
76
232
  let user = { email: 'Unknown', tier: { name: 'Free', limits: {} } };
77
233
  let stats = { period: {}, summary: {} };
234
+ let userFetchWorked = false;
78
235
 
79
236
  try {
80
237
  [user, stats] = await Promise.all([
81
238
  fetchJson(apiUrl, '/v1/user', apiKey),
82
239
  fetchJson(apiUrl, '/v1/stats', apiKey)
83
240
  ]);
241
+ userFetchWorked = true;
84
242
  } catch (err) {
85
243
  // Continue with defaults if API fails
86
244
  }
@@ -114,15 +272,17 @@ async function main() {
114
272
  const cacheEnabled = localCache ? 'Enabled' : 'Disabled';
115
273
  const cacheRate = formatPct((summary.cache_hit_rate || 0) * 100);
116
274
 
117
- // Build dashboard URL from API URL
118
- // API: https://api.plexor.dev or https://staging.api.plexor.dev
119
- // Dashboard: https://plexor.dev/dashboard or https://staging.plexor.dev/dashboard
275
+ // Build dashboard URL from API URL.
276
+ // Staging uses a separate dashboard host.
120
277
  let dashboardUrl = 'https://plexor.dev/dashboard';
121
278
  try {
122
279
  const url = new URL(apiUrl);
123
- // Remove 'api.' prefix from hostname if present
124
- const host = url.hostname.replace(/^api\./, '').replace(/\.api\./, '.');
125
- dashboardUrl = `${url.protocol}//${host}/dashboard`;
280
+ if (url.hostname.includes('staging.api.plexor.dev')) {
281
+ dashboardUrl = 'https://staging.plexorlabs.com/dashboard';
282
+ } else {
283
+ const host = url.hostname.replace(/^api\./, '').replace(/\.api\./, '.');
284
+ dashboardUrl = `${url.protocol}//${host}/dashboard`;
285
+ }
126
286
  } catch {
127
287
  // If URL parsing fails, use default
128
288
  }
@@ -164,6 +324,38 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
164
324
  const routing = getRoutingStatus();
165
325
  const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
166
326
  const envLabel = routing.isStaging ? '(staging)' : '(production)';
327
+ const partialState = detectPartialState();
328
+ const stateMismatch = checkStateMismatch(enabled, routing.active);
329
+ const connectedState = userFetchWorked ? 'OK' : 'Unknown';
330
+ const verifiedState = verification.verified
331
+ ? 'OK'
332
+ : (verification.verify_error ? 'Failed' : 'Not yet');
333
+ const nextAction = !routing.active || partialState.partial || stateMismatch || !verification.verified
334
+ ? '/plexor-setup'
335
+ : '/plexor-status';
336
+
337
+ // Note: Environment mismatch warning removed - it caused false positives during
338
+ // concurrent operations and transient states. The partial state and config/routing
339
+ // mismatch warnings below provide more actionable feedback.
340
+
341
+ printHealthSummary({
342
+ installed: 'OK',
343
+ connected: connectedState,
344
+ routing: routing.active ? `OK ${envLabel}` : 'Needs repair',
345
+ verified: verifiedState,
346
+ nextAction
347
+ });
348
+ console.log('');
349
+ if (partialState.partial) {
350
+ console.log(` ⚠ PARTIAL STATE DETECTED: ${partialState.issue}`);
351
+ console.log(` Run /plexor-setup to repair, or /plexor-logout to disable routing\n`);
352
+ }
353
+
354
+ // Check for state mismatch between config enabled flag and routing status
355
+ if (stateMismatch) {
356
+ console.log(` ⚠ State mismatch: ${stateMismatch.message}`);
357
+ console.log(` └─ ${stateMismatch.suggestion}\n`);
358
+ }
167
359
 
168
360
  console.log(` ┌─────────────────────────────────────────────┐
169
361
  ${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
@@ -202,7 +394,13 @@ ${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https:/
202
394
 
203
395
  function fetchJson(apiUrl, endpoint, apiKey) {
204
396
  return new Promise((resolve, reject) => {
205
- const url = new URL(`${apiUrl}${endpoint}`);
397
+ let url;
398
+ try {
399
+ url = new URL(`${apiUrl}${endpoint}`);
400
+ } catch {
401
+ reject(new Error('Invalid API URL'));
402
+ return;
403
+ }
206
404
 
207
405
  const options = {
208
406
  hostname: url.hostname,
@@ -218,18 +416,48 @@ function fetchJson(apiUrl, endpoint, apiKey) {
218
416
  let data = '';
219
417
  res.on('data', chunk => data += chunk);
220
418
  res.on('end', () => {
419
+ // Check HTTP status code first
420
+ if (res.statusCode === 401) {
421
+ reject(new Error('Invalid API key'));
422
+ return;
423
+ }
424
+ if (res.statusCode === 403) {
425
+ reject(new Error('Access denied'));
426
+ return;
427
+ }
428
+ if (res.statusCode >= 500) {
429
+ reject(new Error('Server error'));
430
+ return;
431
+ }
432
+ if (res.statusCode !== 200) {
433
+ reject(new Error(`HTTP ${res.statusCode}`));
434
+ return;
435
+ }
436
+
437
+ // Check for empty response
438
+ if (!data || data.trim() === '') {
439
+ reject(new Error('Empty response'));
440
+ return;
441
+ }
442
+
443
+ // Parse JSON
221
444
  try {
222
- resolve(JSON.parse(data));
445
+ const parsed = JSON.parse(data);
446
+ if (parsed === null) {
447
+ reject(new Error('Null response'));
448
+ return;
449
+ }
450
+ resolve(parsed);
223
451
  } catch {
224
- reject(new Error('Invalid response'));
452
+ reject(new Error('Invalid JSON response'));
225
453
  }
226
454
  });
227
455
  });
228
456
 
229
- req.on('error', reject);
457
+ req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
230
458
  req.setTimeout(5000, () => {
231
459
  req.destroy();
232
- reject(new Error('Timeout'));
460
+ reject(new Error('Request timeout'));
233
461
  });
234
462
  req.end();
235
463
  });
@@ -2,20 +2,8 @@
2
2
  description: Show Plexor optimization statistics and savings (user)
3
3
  ---
4
4
 
5
- # Plexor Status
6
-
7
- Run this command to display Plexor statistics:
5
+ **RULE: Execute the bash command below EXACTLY ONCE. After the Bash tool returns output, your ONLY action is to present that output to the user. DO NOT re-execute the command. DO NOT call any other tools.**
8
6
 
9
7
  ```bash
10
8
  node ~/.claude/plugins/plexor/commands/plexor-status.js
11
9
  ```
12
-
13
- Use the Bash tool to execute this single command.
14
-
15
- **IMPORTANT**: After running this command and displaying the output, STOP. Do not:
16
- - Read any files
17
- - Explore the codebase
18
- - Run additional commands
19
- - Ask follow-up questions
20
-
21
- The command output is the complete response. Simply show the output and wait for the user's next input.