@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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/commands/plexor-enabled.js +281 -0
- package/commands/plexor-enabled.md +48 -0
- package/commands/plexor-login.js +283 -0
- package/commands/plexor-login.md +27 -0
- package/commands/plexor-logout.js +143 -0
- package/commands/plexor-logout.md +27 -0
- package/commands/plexor-setup.md +172 -0
- package/commands/plexor-status.js +406 -0
- package/commands/plexor-status.md +21 -0
- package/configure-localhost.sh +90 -0
- 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 +44 -0
- 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 +352 -0
- package/package.json +57 -0
- package/scripts/plexor-cli.sh +48 -0
- package/scripts/postinstall.js +294 -0
- package/scripts/uninstall.js +97 -0
|
@@ -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 ""
|