@plexor-dev/claude-code-plugin-localhost 0.1.0-beta.11

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,406 @@
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
+ // 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
+ /**
17
+ * Check if Claude Code is actually routing through Plexor
18
+ * by reading ~/.claude/settings.json
19
+ */
20
+ function getRoutingStatus() {
21
+ try {
22
+ const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
23
+ const settings = JSON.parse(data);
24
+ const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
25
+ const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
26
+ const isPlexorRouting = baseUrl.includes('plexor') || baseUrl.includes('staging.api') || baseUrl.includes('ngrok') || baseUrl.includes('localhost');
27
+ return {
28
+ active: isPlexorRouting && hasToken,
29
+ baseUrl,
30
+ isStaging: baseUrl.includes('staging')
31
+ };
32
+ } catch {
33
+ return { active: false, baseUrl: null, isStaging: false };
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Detect partial routing state where URL points to Plexor but auth is missing/invalid
39
+ * This can cause confusing auth errors for users
40
+ * @returns {Object} { partial: boolean, issue: string|null }
41
+ */
42
+ function detectPartialState() {
43
+ try {
44
+ const data = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
45
+ const settings = JSON.parse(data);
46
+ const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
47
+ const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
48
+ const isPlexorUrl = baseUrl.includes('plexor') || baseUrl.includes('staging.api') || baseUrl.includes('ngrok') || baseUrl.includes('localhost');
49
+
50
+ if (isPlexorUrl && !authToken) {
51
+ return { partial: true, issue: 'Plexor URL set but no auth token' };
52
+ }
53
+ if (isPlexorUrl && !authToken.startsWith('plx_')) {
54
+ return { partial: true, issue: 'Plexor URL set but auth token is not a Plexor key' };
55
+ }
56
+ return { partial: false, issue: null };
57
+ } catch {
58
+ return { partial: false, issue: null };
59
+ }
60
+ }
61
+
62
+ function loadSessionStats() {
63
+ try {
64
+ const data = fs.readFileSync(SESSION_PATH, 'utf8');
65
+ const session = JSON.parse(data);
66
+ // Check if session has expired
67
+ if (Date.now() - session.last_activity > SESSION_TIMEOUT_MS) {
68
+ return null;
69
+ }
70
+ return session;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Validate API key format
78
+ * @param {string} key - API key to validate
79
+ * @returns {boolean} true if valid format
80
+ */
81
+ function isValidApiKeyFormat(key) {
82
+ return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
83
+ }
84
+
85
+ /**
86
+ * Load config file with integrity checking
87
+ * @returns {Object|null} config object or null if invalid
88
+ */
89
+ function loadConfig() {
90
+ try {
91
+ if (!fs.existsSync(CONFIG_PATH)) {
92
+ return null;
93
+ }
94
+ const data = fs.readFileSync(CONFIG_PATH, 'utf8');
95
+ if (!data || data.trim() === '') {
96
+ return null;
97
+ }
98
+ const config = JSON.parse(data);
99
+ if (typeof config !== 'object' || config === null) {
100
+ return null;
101
+ }
102
+ return config;
103
+ } catch (err) {
104
+ if (err instanceof SyntaxError) {
105
+ console.log('Config file is corrupted. Run /plexor-login to reconfigure.');
106
+ }
107
+ return null;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Check for environment mismatch between config and routing
113
+ */
114
+ function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
115
+ if (!configApiUrl || !routingBaseUrl) return null;
116
+
117
+ const configIsStaging = configApiUrl.includes('staging');
118
+ const routingIsStaging = routingBaseUrl.includes('staging');
119
+
120
+ if (configIsStaging !== routingIsStaging) {
121
+ return {
122
+ config: configIsStaging ? 'staging' : 'production',
123
+ routing: routingIsStaging ? 'staging' : 'production'
124
+ };
125
+ }
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Check for state mismatch between config.json enabled flag and settings.json routing
131
+ * @param {boolean} configEnabled - enabled flag from config.json
132
+ * @param {boolean} routingActive - whether settings.json has Plexor routing configured
133
+ * @returns {Object|null} mismatch details or null if states are consistent
134
+ */
135
+ function checkStateMismatch(configEnabled, routingActive) {
136
+ if (configEnabled && !routingActive) {
137
+ return {
138
+ type: 'config-enabled-routing-inactive',
139
+ message: 'Config shows enabled but Claude routing is not configured',
140
+ suggestion: 'Run /plexor-enabled true to sync and configure routing'
141
+ };
142
+ }
143
+ if (!configEnabled && routingActive) {
144
+ return {
145
+ type: 'config-disabled-routing-active',
146
+ message: 'Config shows disabled but Claude routing is active',
147
+ suggestion: 'Run /plexor-enabled false to sync and disable routing'
148
+ };
149
+ }
150
+ return null;
151
+ }
152
+
153
+ async function main() {
154
+ // Read config with integrity checking
155
+ const config = loadConfig();
156
+ if (!config) {
157
+ console.log('Not configured. Run /plexor-login first.');
158
+ process.exit(1);
159
+ }
160
+
161
+ const apiKey = config.auth?.api_key;
162
+ const enabled = config.settings?.enabled ?? false;
163
+ const mode = config.settings?.mode || 'balanced';
164
+ const provider = config.settings?.preferred_provider || 'auto';
165
+ const localCache = config.settings?.localCacheEnabled ?? false;
166
+ const apiUrl = config.settings?.apiUrl || 'LOCALHOST_TUNNEL_URL';
167
+
168
+ if (!apiKey) {
169
+ console.log('Not authenticated. Run /plexor-login first.');
170
+ process.exit(1);
171
+ }
172
+
173
+ // Validate API key format
174
+ if (!isValidApiKeyFormat(apiKey)) {
175
+ console.log('Invalid API key format. Keys must start with "plx_" and be at least 20 characters.');
176
+ console.log('Run /plexor-login with a valid API key.');
177
+ process.exit(1);
178
+ }
179
+
180
+ // Fetch user info and stats
181
+ let user = { email: 'Unknown', tier: { name: 'Free', limits: {} } };
182
+ let stats = { period: {}, summary: {} };
183
+
184
+ try {
185
+ [user, stats] = await Promise.all([
186
+ fetchJson(apiUrl, '/v1/user', apiKey),
187
+ fetchJson(apiUrl, '/v1/stats', apiKey)
188
+ ]);
189
+ } catch (err) {
190
+ // Continue with defaults if API fails
191
+ }
192
+
193
+ // Load session stats
194
+ const session = loadSessionStats();
195
+
196
+ // Extract data
197
+ const email = user.email || 'Unknown';
198
+ const tierName = user.tier?.name || 'Free';
199
+ const monthlyOpts = user.tier?.limits?.monthly_optimizations || '∞';
200
+ const monthlyComps = user.tier?.limits?.monthly_completions || '∞';
201
+
202
+ const period = stats.period || {};
203
+ const summary = stats.summary || {};
204
+
205
+ const formatDate = (iso) => {
206
+ if (!iso) return '?';
207
+ const d = new Date(iso);
208
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
209
+ };
210
+ const weekRange = `${formatDate(period.start)} - ${formatDate(period.end)}`;
211
+
212
+ // Format numbers
213
+ const formatNum = (n) => (n || 0).toLocaleString();
214
+ const formatPct = (n) => (n || 0).toFixed(1);
215
+ const formatCost = (n) => (n || 0).toFixed(2);
216
+
217
+ const status = enabled ? '● Active' : '○ Inactive';
218
+ const optEnabled = enabled ? 'Enabled' : 'Disabled';
219
+ const cacheEnabled = localCache ? 'Enabled' : 'Disabled';
220
+ const cacheRate = formatPct((summary.cache_hit_rate || 0) * 100);
221
+
222
+ // Build dashboard URL from API URL
223
+ // API: https://api.plexor.dev or https://staging.api.plexor.dev
224
+ // Dashboard: https://plexor.dev/dashboard or https://staging.plexor.dev/dashboard
225
+ let dashboardUrl = 'https://plexor.dev/dashboard';
226
+ try {
227
+ const url = new URL(apiUrl);
228
+ // Remove 'api.' prefix from hostname if present
229
+ const host = url.hostname.replace(/^api\./, '').replace(/\.api\./, '.');
230
+ dashboardUrl = `${url.protocol}//${host}/dashboard`;
231
+ } catch {
232
+ // If URL parsing fails, use default
233
+ }
234
+
235
+ // Output formatted status - each line is exactly 43 chars inner width
236
+ const line = (content) => ` │ ${content.padEnd(43)}│`;
237
+
238
+ // Session stats formatting
239
+ const formatDuration = (startedAt) => {
240
+ if (!startedAt) return '0m';
241
+ const elapsed = Date.now() - new Date(startedAt).getTime();
242
+ const minutes = Math.floor(elapsed / 60000);
243
+ if (minutes < 60) return `${minutes}m`;
244
+ const hours = Math.floor(minutes / 60);
245
+ return `${hours}h ${minutes % 60}m`;
246
+ };
247
+
248
+ const sessionDuration = session ? formatDuration(session.started_at) : '0m';
249
+ const sessionRequests = session ? formatNum(session.requests) : '0';
250
+ const sessionOptimizations = session ? formatNum(session.optimizations) : '0';
251
+ const sessionCacheHits = session ? formatNum(session.cache_hits) : '0';
252
+ const sessionTokensSaved = session ? formatNum(session.tokens_saved) : '0';
253
+ const sessionTokensSavedPct = session && session.original_tokens > 0
254
+ ? formatPct((session.tokens_saved / session.original_tokens) * 100)
255
+ : '0.0';
256
+ const sessionCostSaved = session ? formatCost(session.cost_saved) : '0.00';
257
+
258
+ // Build session section (only show if session exists)
259
+ const sessionSection = session ? ` ├─────────────────────────────────────────────┤
260
+ ${line(`This Session (${sessionDuration})`)}
261
+ ${line(`├── Requests: ${sessionRequests}`)}
262
+ ${line(`├── Optimizations: ${sessionOptimizations}`)}
263
+ ${line(`├── Cache hits: ${sessionCacheHits}`)}
264
+ ${line(`├── Tokens saved: ${sessionTokensSaved} (${sessionTokensSavedPct}%)`)}
265
+ ${line(`└── Cost saved: $${sessionCostSaved}`)}
266
+ ` : '';
267
+
268
+ // Get routing status from Claude settings.json
269
+ const routing = getRoutingStatus();
270
+ const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
271
+ const envLabel = routing.isStaging ? '(staging)' : '(production)';
272
+
273
+ // Check for environment mismatch
274
+ const envMismatch = checkEnvironmentMismatch(apiUrl, routing.baseUrl);
275
+ const mismatchWarning = envMismatch
276
+ ? ` ⚠ Warning: Config uses ${envMismatch.config} but routing is ${envMismatch.routing}\n`
277
+ : '';
278
+
279
+ if (mismatchWarning) {
280
+ console.log(mismatchWarning);
281
+ }
282
+
283
+ // Check for partial routing state (Plexor URL without valid auth)
284
+ const partialState = detectPartialState();
285
+ if (partialState.partial) {
286
+ console.log(` ⚠ PARTIAL STATE DETECTED: ${partialState.issue}`);
287
+ console.log(` Run /plexor-login to fix, or /plexor-logout to disable routing\n`);
288
+ }
289
+
290
+ // Check for state mismatch between config enabled flag and routing status
291
+ const stateMismatch = checkStateMismatch(enabled, routing.active);
292
+ if (stateMismatch) {
293
+ console.log(` ⚠ State mismatch: ${stateMismatch.message}`);
294
+ console.log(` └─ ${stateMismatch.suggestion}\n`);
295
+ }
296
+
297
+ console.log(` ┌─────────────────────────────────────────────┐
298
+ ${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
299
+ ├─────────────────────────────────────────────┤
300
+ ${line(`Account: ${tierName}`)}
301
+ ${line(`Email: ${email}`)}
302
+ ${line(`Status: ${status}`)}
303
+ ${sessionSection} ├─────────────────────────────────────────────┤
304
+ ${line(`This Week (${weekRange})`)}
305
+ ${line(`├── Requests: ${formatNum(summary.total_requests)}`)}
306
+ ${line(`├── Original tokens: ${formatNum(summary.original_tokens)}`)}
307
+ ${line(`├── Optimized tokens: ${formatNum(summary.optimized_tokens)}`)}
308
+ ${line(`├── Tokens saved: ${formatNum(summary.tokens_saved)} (${formatPct(summary.tokens_saved_percent)}%)`)}
309
+ ${line(`├── Baseline cost: $${formatCost(summary.baseline_cost)}`)}
310
+ ${line(`├── Actual cost: $${formatCost(summary.total_cost)}`)}
311
+ ${line(`└── Cost saved: $${formatCost(summary.cost_saved)} (${formatPct(summary.cost_saved_percent)}%)`)}
312
+ ├─────────────────────────────────────────────┤
313
+ ${line('Performance')}
314
+ ${line(`└── Cache hit rate: ${cacheRate}%`)}
315
+ ├─────────────────────────────────────────────┤
316
+ ${line('Limits')}
317
+ ${line(`├── Monthly optimizations: ${formatNum(monthlyOpts)}`)}
318
+ ${line(`└── Monthly completions: ${formatNum(monthlyComps)}`)}
319
+ ├─────────────────────────────────────────────┤
320
+ ${line('Settings')}
321
+ ${line(`├── Optimization: ${optEnabled}`)}
322
+ ${line(`├── Local cache: ${cacheEnabled}`)}
323
+ ${line(`├── Mode: ${mode}`)}
324
+ ${line(`├── Provider routing: ${provider}`)}
325
+ ${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https://', '').substring(0, 30) : 'not configured'}`)}
326
+ └─────────────────────────────────────────────┘
327
+
328
+ Dashboard: ${dashboardUrl}
329
+ `);
330
+ }
331
+
332
+ function fetchJson(apiUrl, endpoint, apiKey) {
333
+ return new Promise((resolve, reject) => {
334
+ let url;
335
+ try {
336
+ url = new URL(`${apiUrl}${endpoint}`);
337
+ } catch {
338
+ reject(new Error('Invalid API URL'));
339
+ return;
340
+ }
341
+
342
+ const options = {
343
+ hostname: url.hostname,
344
+ port: 443,
345
+ path: url.pathname,
346
+ method: 'GET',
347
+ headers: {
348
+ 'X-Plexor-Key': apiKey
349
+ }
350
+ };
351
+
352
+ const req = https.request(options, (res) => {
353
+ let data = '';
354
+ res.on('data', chunk => data += chunk);
355
+ res.on('end', () => {
356
+ // Check HTTP status code first
357
+ if (res.statusCode === 401) {
358
+ reject(new Error('Invalid API key'));
359
+ return;
360
+ }
361
+ if (res.statusCode === 403) {
362
+ reject(new Error('Access denied'));
363
+ return;
364
+ }
365
+ if (res.statusCode >= 500) {
366
+ reject(new Error('Server error'));
367
+ return;
368
+ }
369
+ if (res.statusCode !== 200) {
370
+ reject(new Error(`HTTP ${res.statusCode}`));
371
+ return;
372
+ }
373
+
374
+ // Check for empty response
375
+ if (!data || data.trim() === '') {
376
+ reject(new Error('Empty response'));
377
+ return;
378
+ }
379
+
380
+ // Parse JSON
381
+ try {
382
+ const parsed = JSON.parse(data);
383
+ if (parsed === null) {
384
+ reject(new Error('Null response'));
385
+ return;
386
+ }
387
+ resolve(parsed);
388
+ } catch {
389
+ reject(new Error('Invalid JSON response'));
390
+ }
391
+ });
392
+ });
393
+
394
+ req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
395
+ req.setTimeout(5000, () => {
396
+ req.destroy();
397
+ reject(new Error('Request timeout'));
398
+ });
399
+ req.end();
400
+ });
401
+ }
402
+
403
+ main().catch(err => {
404
+ console.error('Error:', err.message);
405
+ process.exit(1);
406
+ });
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: Show Plexor optimization statistics and savings (user)
3
+ ---
4
+
5
+ # Plexor Status
6
+
7
+ Run this command to display Plexor statistics:
8
+
9
+ ```bash
10
+ node ~/.claude/plugins/plexor/commands/plexor-status.js
11
+ ```
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.
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+ #
3
+ # Configure Plexor Localhost Plugin with Tunnel URL
4
+ #
5
+ # Usage:
6
+ # ./configure-localhost.sh https://your-ngrok-url.ngrok-free.app
7
+ #
8
+ # This script replaces the LOCALHOST_TUNNEL_URL placeholder in all plugin files
9
+ # with your actual ngrok/tunnel URL.
10
+ #
11
+
12
+ set -e
13
+
14
+ # Colors for output
15
+ RED='\033[0;31m'
16
+ GREEN='\033[0;32m'
17
+ YELLOW='\033[1;33m'
18
+ NC='\033[0m' # No Color
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+
22
+ if [ -z "$1" ]; then
23
+ echo -e "${RED}Error: No tunnel URL provided${NC}"
24
+ echo ""
25
+ echo "Usage: $0 <tunnel-url>"
26
+ echo ""
27
+ echo "Example:"
28
+ echo " $0 https://abc123.ngrok-free.app"
29
+ echo " $0 http://localhost:8000"
30
+ echo ""
31
+ exit 1
32
+ fi
33
+
34
+ TUNNEL_URL="$1"
35
+
36
+ # Remove trailing slash if present
37
+ TUNNEL_URL="${TUNNEL_URL%/}"
38
+
39
+ echo -e "${YELLOW}Configuring Plexor localhost plugin...${NC}"
40
+ echo "Tunnel URL: $TUNNEL_URL"
41
+ echo ""
42
+
43
+ # Files to update
44
+ FILES=(
45
+ "lib/constants.js"
46
+ "lib/settings-manager.js"
47
+ "lib/plexor-client.js"
48
+ "hooks/intercept.js"
49
+ "hooks/track-response.js"
50
+ "scripts/postinstall.js"
51
+ "commands/plexor-enabled.js"
52
+ "commands/plexor-status.js"
53
+ "commands/plexor-login.js"
54
+ "commands/plexor-setup.md"
55
+ "README.md"
56
+ )
57
+
58
+ UPDATED=0
59
+ FAILED=0
60
+
61
+ for file in "${FILES[@]}"; do
62
+ filepath="$SCRIPT_DIR/$file"
63
+ if [ -f "$filepath" ]; then
64
+ if grep -q "LOCALHOST_TUNNEL_URL" "$filepath" 2>/dev/null; then
65
+ sed -i "s|LOCALHOST_TUNNEL_URL|${TUNNEL_URL}|g" "$filepath"
66
+ echo -e " ${GREEN}[OK]${NC} $file"
67
+ ((UPDATED++))
68
+ else
69
+ echo -e " ${YELLOW}[SKIP]${NC} $file (no placeholder found)"
70
+ fi
71
+ else
72
+ echo -e " ${RED}[MISSING]${NC} $file"
73
+ ((FAILED++))
74
+ fi
75
+ done
76
+
77
+ echo ""
78
+ echo -e "${GREEN}Configuration complete!${NC}"
79
+ echo " Files updated: $UPDATED"
80
+ if [ $FAILED -gt 0 ]; then
81
+ echo -e " ${RED}Files missing: $FAILED${NC}"
82
+ fi
83
+
84
+ echo ""
85
+ echo "Next steps:"
86
+ echo " 1. Pack the plugin: cd $SCRIPT_DIR && npm pack"
87
+ echo " 2. Copy to VM: scp plexor-dev-claude-code-plugin-localhost-*.tgz user@vm:~/"
88
+ echo " 3. Install on VM: npm install -g ./plexor-dev-claude-code-plugin-localhost-*.tgz"
89
+ echo " 4. Login: /plexor-login <your-test-api-key>"
90
+ echo ""