@plexor-dev/claude-code-plugin-localhost 0.1.0-localhost.1

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,354 @@
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
+ // LOCALHOST PACKAGE - both point to same localhost (no staging/prod distinction locally)
24
+ const PLEXOR_STAGING_URL = 'http://localhost:8000/gateway/anthropic';
25
+ const PLEXOR_PROD_URL = 'http://localhost:8000/gateway/anthropic';
26
+
27
+ class ClaudeSettingsManager {
28
+ constructor() {
29
+ this.settingsPath = SETTINGS_PATH;
30
+ this.claudeDir = CLAUDE_DIR;
31
+ }
32
+
33
+ /**
34
+ * Load current Claude settings with integrity checking
35
+ * @returns {Object} settings object or empty object if not found/corrupted
36
+ */
37
+ load() {
38
+ try {
39
+ if (!fs.existsSync(this.settingsPath)) {
40
+ return {};
41
+ }
42
+ const data = fs.readFileSync(this.settingsPath, 'utf8');
43
+
44
+ // Check for empty file
45
+ if (!data || data.trim() === '') {
46
+ return {};
47
+ }
48
+
49
+ const parsed = JSON.parse(data);
50
+
51
+ // Basic schema validation - must be an object
52
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
53
+ const backupPath = this._backupCorruptedFile();
54
+ console.warn('');
55
+ console.warn('WARNING: Claude settings file has invalid format!');
56
+ if (backupPath) {
57
+ console.warn(` Corrupted file backed up to: ${backupPath}`);
58
+ }
59
+ console.warn(' Using default settings. Your previous settings may need manual recovery.');
60
+ console.warn('');
61
+ return {};
62
+ }
63
+
64
+ return parsed;
65
+ } catch (err) {
66
+ if (err.code === 'ENOENT') {
67
+ return {};
68
+ }
69
+ // JSON parse error or corrupted file
70
+ if (err instanceof SyntaxError) {
71
+ const backupPath = this._backupCorruptedFile();
72
+ console.warn('');
73
+ console.warn('WARNING: Claude settings file is corrupted (invalid JSON)!');
74
+ if (backupPath) {
75
+ console.warn(` Corrupted file backed up to: ${backupPath}`);
76
+ }
77
+ console.warn(' Using default settings. Your previous settings may need manual recovery.');
78
+ console.warn('');
79
+ return {};
80
+ }
81
+ // Permission error
82
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
83
+ console.warn(`Warning: Cannot read ${this.settingsPath} (permission denied)`);
84
+ return {};
85
+ }
86
+ console.warn('Warning: Failed to load Claude settings:', err.message);
87
+ return {};
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Backup a corrupted settings file with numbered suffix for debugging
93
+ * Creates settings.json.corrupted.1, .corrupted.2, etc. to preserve history
94
+ * @returns {string|null} path to backup file, or null if backup failed
95
+ */
96
+ _backupCorruptedFile() {
97
+ try {
98
+ if (fs.existsSync(this.settingsPath)) {
99
+ // Find next available numbered backup
100
+ let backupNum = 1;
101
+ let backupPath;
102
+ while (true) {
103
+ backupPath = `${this.settingsPath}.corrupted.${backupNum}`;
104
+ if (!fs.existsSync(backupPath)) {
105
+ break;
106
+ }
107
+ backupNum++;
108
+ }
109
+ fs.copyFileSync(this.settingsPath, backupPath);
110
+ return backupPath;
111
+ }
112
+ } catch {
113
+ // Ignore backup errors silently
114
+ }
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Save Claude settings using atomic write pattern
120
+ * Prevents race conditions by writing to temp file then renaming
121
+ * @param {Object} settings - settings object to save
122
+ * @returns {boolean} success status
123
+ */
124
+ save(settings) {
125
+ try {
126
+ // Ensure .claude directory exists
127
+ if (!fs.existsSync(this.claudeDir)) {
128
+ fs.mkdirSync(this.claudeDir, { recursive: true });
129
+ }
130
+
131
+ // Atomic write: write to temp file, then rename
132
+ // This prevents race conditions where concurrent writes corrupt the file
133
+ const tempId = crypto.randomBytes(8).toString('hex');
134
+ const tempPath = path.join(this.claudeDir, `.settings.${tempId}.tmp`);
135
+
136
+ // Write to temp file
137
+ const content = JSON.stringify(settings, null, 2);
138
+ fs.writeFileSync(tempPath, content, { mode: 0o600 });
139
+
140
+ // Atomic rename (on POSIX systems, rename is atomic)
141
+ fs.renameSync(tempPath, this.settingsPath);
142
+
143
+ return true;
144
+ } catch (err) {
145
+ // Clean error message for permission errors
146
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
147
+ console.error(`Error: Cannot write to ${this.settingsPath}`);
148
+ console.error(' Check file permissions or run with appropriate access.');
149
+ } else {
150
+ console.error('Failed to save Claude settings:', err.message);
151
+ }
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Enable Plexor routing by setting env vars in settings.json
158
+ *
159
+ * This is the KEY mechanism: setting ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
160
+ * in the env block redirects ALL Claude Code sessions to Plexor automatically.
161
+ *
162
+ * @param {string} apiKey - Plexor API key (plx_*)
163
+ * @param {Object} options - { useStaging: boolean }
164
+ * @returns {boolean} success status
165
+ */
166
+ enablePlexorRouting(apiKey, options = {}) {
167
+ // LOCALHOST PACKAGE - always uses localhost
168
+ const { useStaging = true } = options;
169
+ const apiUrl = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
170
+
171
+ try {
172
+ const settings = this.load();
173
+
174
+ // Initialize env block if doesn't exist
175
+ if (!settings.env) {
176
+ settings.env = {};
177
+ }
178
+
179
+ // Set the magic environment variables
180
+ // ANTHROPIC_AUTH_TOKEN has higher precedence than ANTHROPIC_API_KEY
181
+ settings.env.ANTHROPIC_BASE_URL = apiUrl;
182
+ settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
183
+
184
+ const success = this.save(settings);
185
+
186
+ if (success) {
187
+ console.log(`✓ Plexor routing enabled`);
188
+ console.log(` Base URL: ${apiUrl}`);
189
+ console.log(` API Key: ${apiKey.substring(0, 12)}...`);
190
+ console.log(`\n All Claude Code sessions will now route through Plexor.`);
191
+ console.log(` Changes take effect immediately (no restart needed).`);
192
+ }
193
+
194
+ return success;
195
+ } catch (err) {
196
+ console.error('Failed to enable Plexor routing:', err.message);
197
+ return false;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Disable Plexor routing by removing env vars from settings.json
203
+ *
204
+ * This restores direct connection to Anthropic API.
205
+ *
206
+ * @returns {boolean} success status
207
+ */
208
+ disablePlexorRouting() {
209
+ try {
210
+ const settings = this.load();
211
+
212
+ if (!settings.env) {
213
+ console.log('Plexor routing is not currently enabled.');
214
+ return true;
215
+ }
216
+
217
+ // Remove Plexor-specific env vars
218
+ delete settings.env.ANTHROPIC_BASE_URL;
219
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
220
+
221
+ // Clean up empty env block
222
+ if (Object.keys(settings.env).length === 0) {
223
+ delete settings.env;
224
+ }
225
+
226
+ const success = this.save(settings);
227
+
228
+ if (success) {
229
+ console.log('✓ Plexor routing disabled');
230
+ console.log(' Claude Code will now connect directly to Anthropic.');
231
+ console.log(' Changes take effect immediately (no restart needed).');
232
+ }
233
+
234
+ return success;
235
+ } catch (err) {
236
+ console.error('Failed to disable Plexor routing:', err.message);
237
+ return false;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Get current routing status
243
+ * @returns {Object} { enabled: boolean, baseUrl: string|null, hasToken: boolean }
244
+ */
245
+ getRoutingStatus() {
246
+ try {
247
+ const settings = this.load();
248
+
249
+ const baseUrl = settings.env?.ANTHROPIC_BASE_URL || null;
250
+ const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
251
+
252
+ // Check if routing to Plexor
253
+ const isPlexorRouting = baseUrl && (
254
+ baseUrl.includes('plexor') ||
255
+ baseUrl.includes('staging.api') ||
256
+ baseUrl.includes('localhost')
257
+ );
258
+
259
+ return {
260
+ enabled: isPlexorRouting,
261
+ baseUrl,
262
+ hasToken,
263
+ isStaging: baseUrl?.includes('staging') || false,
264
+ tokenPreview: hasToken ? settings.env.ANTHROPIC_AUTH_TOKEN.substring(0, 12) + '...' : null
265
+ };
266
+ } catch {
267
+ return { enabled: false, baseUrl: null, hasToken: false };
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Detect partial routing state where URL points to Plexor but auth is missing/invalid
273
+ * This can cause confusing auth errors for users
274
+ * @returns {Object} { partial: boolean, issue: string|null }
275
+ */
276
+ detectPartialState() {
277
+ try {
278
+ const settings = this.load();
279
+ const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
280
+ const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
281
+ const isPlexorUrl = baseUrl.includes('plexor') || baseUrl.includes('staging.api') || baseUrl.includes('localhost');
282
+
283
+ if (isPlexorUrl && !authToken) {
284
+ return { partial: true, issue: 'Plexor URL set but no auth token' };
285
+ }
286
+ if (isPlexorUrl && !authToken.startsWith('plx_')) {
287
+ return { partial: true, issue: 'Plexor URL set but auth token is not a Plexor key' };
288
+ }
289
+ return { partial: false, issue: null };
290
+ } catch {
291
+ return { partial: false, issue: null };
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Update just the API key without changing other settings
297
+ * @param {string} apiKey - new Plexor API key
298
+ * @returns {boolean} success status
299
+ */
300
+ updateApiKey(apiKey) {
301
+ try {
302
+ const settings = this.load();
303
+
304
+ if (!settings.env) {
305
+ settings.env = {};
306
+ }
307
+
308
+ settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
309
+ return this.save(settings);
310
+ } catch (err) {
311
+ console.error('Failed to update API key:', err.message);
312
+ return false;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Switch between staging and production
318
+ * @param {boolean} useStaging - true for staging, false for production
319
+ * @returns {boolean} success status
320
+ */
321
+ setEnvironment(useStaging) {
322
+ try {
323
+ const settings = this.load();
324
+
325
+ if (!settings.env?.ANTHROPIC_BASE_URL) {
326
+ console.log('Plexor routing is not enabled. Run /plexor-login first.');
327
+ return false;
328
+ }
329
+
330
+ settings.env.ANTHROPIC_BASE_URL = useStaging ? PLEXOR_STAGING_URL : PLEXOR_PROD_URL;
331
+
332
+ const success = this.save(settings);
333
+ if (success) {
334
+ console.log(`✓ Switched to ${useStaging ? 'staging' : 'production'} environment`);
335
+ }
336
+ return success;
337
+ } catch (err) {
338
+ console.error('Failed to switch environment:', err.message);
339
+ return false;
340
+ }
341
+ }
342
+ }
343
+
344
+ // Export singleton instance and class
345
+ const settingsManager = new ClaudeSettingsManager();
346
+
347
+ module.exports = {
348
+ ClaudeSettingsManager,
349
+ settingsManager,
350
+ CLAUDE_DIR,
351
+ SETTINGS_PATH,
352
+ PLEXOR_STAGING_URL,
353
+ PLEXOR_PROD_URL
354
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@plexor-dev/claude-code-plugin-localhost",
3
+ "version": "0.1.0-localhost.1",
4
+ "description": "LOCALHOST - LLM cost optimization plugin for Claude Code (local VM testing on port 8000)",
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
+ "plexor-uninstall": "./commands/plexor-uninstall.js"
12
+ },
13
+ "scripts": {
14
+ "postinstall": "node scripts/postinstall.js",
15
+ "preuninstall": "node scripts/uninstall.js",
16
+ "test": "node --test"
17
+ },
18
+ "files": [
19
+ "commands/",
20
+ "hooks/",
21
+ "scripts/",
22
+ "lib/",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "keywords": [
30
+ "claude",
31
+ "claude-code",
32
+ "llm",
33
+ "optimization",
34
+ "plexor",
35
+ "cost-savings",
36
+ "ai",
37
+ "anthropic"
38
+ ],
39
+ "author": "Plexor <hello@plexor.dev>",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/plexor-dev/claude-code-plugin"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/plexor-dev/claude-code-plugin/issues"
47
+ },
48
+ "homepage": "https://plexor.dev",
49
+ "engines": {
50
+ "node": ">=16"
51
+ },
52
+ "os": [
53
+ "darwin",
54
+ "linux",
55
+ "win32"
56
+ ]
57
+ }
@@ -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 "$@"