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

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