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

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.
@@ -13,28 +13,79 @@ const path = require('path');
13
13
  const https = require('https');
14
14
  const http = require('http');
15
15
 
16
- // Import settings manager for automatic Claude Code configuration
17
- const { settingsManager } = require('../lib/settings-manager');
16
+ // Import centralized constants with HOME directory validation
17
+ const { PLEXOR_DIR, CONFIG_PATH, DEFAULT_API_URL } = require('../lib/constants');
18
18
 
19
- const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
20
- const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
21
- // STAGING PACKAGE - uses staging API
22
- const DEFAULT_API_URL = 'https://staging.api.plexor.dev';
19
+ // Import settings manager with error handling for missing lib
20
+ let settingsManager;
21
+ try {
22
+ settingsManager = require('../lib/settings-manager').settingsManager;
23
+ } catch (err) {
24
+ if (err.code === 'MODULE_NOT_FOUND') {
25
+ console.error('Error: Plexor plugin files are missing or corrupted.');
26
+ console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin-staging');
27
+ process.exit(1);
28
+ }
29
+ throw err;
30
+ }
23
31
 
24
32
  function loadConfig() {
25
33
  try {
34
+ if (!fs.existsSync(CONFIG_PATH)) {
35
+ return { version: 1, auth: {}, settings: {} };
36
+ }
26
37
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
27
- return JSON.parse(data);
28
- } catch {
38
+ if (!data || data.trim() === '') {
39
+ return { version: 1, auth: {}, settings: {} };
40
+ }
41
+ const config = JSON.parse(data);
42
+ if (typeof config !== 'object' || config === null) {
43
+ return { version: 1, auth: {}, settings: {} };
44
+ }
45
+ return config;
46
+ } catch (err) {
47
+ if (err instanceof SyntaxError) {
48
+ console.warn('Warning: Config file is corrupted, will be overwritten');
49
+ }
29
50
  return { version: 1, auth: {}, settings: {} };
30
51
  }
31
52
  }
32
53
 
33
54
  function saveConfig(config) {
34
- if (!fs.existsSync(PLEXOR_DIR)) {
35
- fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
55
+ try {
56
+ if (!fs.existsSync(PLEXOR_DIR)) {
57
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
58
+ }
59
+
60
+ // Atomic write: write to temp file, then rename
61
+ const crypto = require('crypto');
62
+ const tempId = crypto.randomBytes(8).toString('hex');
63
+ const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
64
+
65
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
66
+ fs.renameSync(tempPath, CONFIG_PATH);
67
+ return true;
68
+ } catch (err) {
69
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
70
+ console.error('Error: Cannot write to ~/.plexor/config.json');
71
+ console.error(' Check file permissions or run with appropriate access.');
72
+ } else {
73
+ console.error('Failed to save config:', err.message);
74
+ }
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function isPlexorApiKey(token) {
80
+ return typeof token === 'string' && token.startsWith('plx_') && token.length >= 20;
81
+ }
82
+
83
+ function isJwtToken(token) {
84
+ if (typeof token !== 'string' || !token.startsWith('eyJ')) {
85
+ return false;
36
86
  }
37
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
87
+ const parts = token.split('.');
88
+ return parts.length === 3 && parts.every(part => part.length > 0);
38
89
  }
39
90
 
40
91
  function validateApiKey(apiUrl, apiKey) {
@@ -80,23 +131,112 @@ function validateApiKey(apiUrl, apiKey) {
80
131
  });
81
132
  }
82
133
 
83
- async function promptForApiKey() {
84
- const rl = readline.createInterface({
85
- input: process.stdin,
86
- output: process.stdout
134
+ function validateJwtToken(apiUrl, jwtToken) {
135
+ return new Promise((resolve, reject) => {
136
+ const url = new URL(`${apiUrl}/v1/auth/me`);
137
+ const isHttps = url.protocol === 'https:';
138
+ const lib = isHttps ? https : http;
139
+
140
+ const options = {
141
+ hostname: url.hostname,
142
+ port: url.port || (isHttps ? 443 : 80),
143
+ path: url.pathname,
144
+ method: 'GET',
145
+ headers: {
146
+ 'Authorization': `Bearer ${jwtToken}`
147
+ }
148
+ };
149
+
150
+ const req = lib.request(options, (res) => {
151
+ let data = '';
152
+ res.on('data', chunk => data += chunk);
153
+ res.on('end', () => {
154
+ if (res.statusCode === 200) {
155
+ try {
156
+ resolve(JSON.parse(data));
157
+ } catch {
158
+ reject(new Error('Invalid response from server'));
159
+ }
160
+ } else if (res.statusCode === 401) {
161
+ reject(new Error('Invalid login token'));
162
+ } else {
163
+ reject(new Error(`Server error: ${res.statusCode}`));
164
+ }
165
+ });
166
+ });
167
+
168
+ req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
169
+ req.setTimeout(10000, () => {
170
+ req.destroy();
171
+ reject(new Error('Connection timeout'));
172
+ });
173
+ req.end();
87
174
  });
175
+ }
176
+
177
+ function validateCredential(apiUrl, credential) {
178
+ if (isJwtToken(credential)) {
179
+ return validateJwtToken(apiUrl, credential);
180
+ }
181
+ return validateApiKey(apiUrl, credential);
182
+ }
88
183
 
89
- return new Promise((resolve) => {
90
- rl.question('Enter your Plexor API key: ', (answer) => {
91
- rl.close();
92
- resolve(answer.trim());
184
+ /**
185
+ * Read API key from stdin (for piped input)
186
+ * @returns {Promise<string>} The API key read from stdin
187
+ */
188
+ function readFromStdin() {
189
+ return new Promise((resolve, reject) => {
190
+ let data = '';
191
+ const timeout = setTimeout(() => {
192
+ reject(new Error('Timeout reading from stdin'));
193
+ }, 5000);
194
+
195
+ process.stdin.setEncoding('utf8');
196
+ process.stdin.on('data', (chunk) => {
197
+ data += chunk;
198
+ });
199
+ process.stdin.on('end', () => {
200
+ clearTimeout(timeout);
201
+ resolve(data.trim());
202
+ });
203
+ process.stdin.on('error', (err) => {
204
+ clearTimeout(timeout);
205
+ reject(err);
93
206
  });
207
+ process.stdin.resume();
94
208
  });
95
209
  }
96
210
 
97
211
  async function main() {
98
212
  const args = process.argv.slice(2);
99
- let apiKey = args[0];
213
+ let apiKey = null;
214
+ let keySource = null;
215
+
216
+ // SECURITY FIX: Check for API key in order of preference
217
+ // 1. Environment variable (most secure for scripts/CI)
218
+ // 2. Stdin pipe (secure for interactive use)
219
+ // 3. Command line argument (warns about security risk)
220
+
221
+ // Check environment variable first (most secure)
222
+ if (process.env.PLEXOR_API_KEY) {
223
+ apiKey = process.env.PLEXOR_API_KEY;
224
+ keySource = 'environment';
225
+ }
226
+ // Check if stdin has data (piped input)
227
+ else if (!process.stdin.isTTY && args.length === 0) {
228
+ try {
229
+ apiKey = await readFromStdin();
230
+ keySource = 'stdin';
231
+ } catch {
232
+ // Stdin read failed, continue to check args
233
+ }
234
+ }
235
+ // Check command line argument (least secure - shows in ps)
236
+ else if (args[0]) {
237
+ apiKey = args[0];
238
+ keySource = 'argument';
239
+ }
100
240
 
101
241
  // Check for existing login
102
242
  const config = loadConfig();
@@ -107,31 +247,46 @@ async function main() {
107
247
  console.log(`│ Already Logged In │`);
108
248
  console.log(`├─────────────────────────────────────────────┤`);
109
249
  console.log(`│ API Key: ${(existingKey.substring(0, 8) + '...').padEnd(33)}│`);
110
- console.log(`│ To re-login, provide a new key: │`);
111
- console.log(`│ /plexor-login <api-key> │`);
112
- console.log(`│ To logout: │`);
113
- console.log(`│ /plexor-logout │`);
250
+ console.log(`│ Human setup path: /plexor-setup │`);
251
+ console.log(`│ Automation path: /plexor-login <key> │`);
252
+ console.log(`│ To logout: /plexor-logout │`);
114
253
  console.log(`└─────────────────────────────────────────────┘`);
115
254
  return;
116
255
  }
117
256
 
118
- // If no key provided, prompt for it
257
+ // If no key provided, show secure usage options
119
258
  if (!apiKey) {
120
259
  console.log(`┌─────────────────────────────────────────────┐`);
121
- console.log(`│ Plexor Login │`);
260
+ console.log(`│ Plexor Login (Advanced) │`);
261
+ console.log(`├─────────────────────────────────────────────┤`);
262
+ console.log(`│ Human first-run: /plexor-setup │`);
122
263
  console.log(`├─────────────────────────────────────────────┤`);
123
264
  console.log(`│ Get your API key at: │`);
124
265
  console.log(`│ https://plexor.dev/dashboard/api-keys │`);
125
266
  console.log(`├─────────────────────────────────────────────┤`);
126
- console.log(`│ Usage: /plexor-login <api-key> │`);
267
+ console.log(`│ Secure usage (recommended): │`);
268
+ console.log(`│ echo "plx_..." | /plexor-login │`);
269
+ console.log(`│ PLEXOR_API_KEY=plx_... /plexor-login │`);
270
+ console.log(`├─────────────────────────────────────────────┤`);
271
+ console.log(`│ Direct usage (visible in ps): │`);
272
+ console.log(`│ /plexor-login <api-key> │`);
127
273
  console.log(`└─────────────────────────────────────────────┘`);
128
274
  return;
129
275
  }
130
276
 
131
- // Validate key format
132
- if (!apiKey.startsWith('plx_') || apiKey.length < 20) {
133
- console.error(`Error: Invalid API key format`);
134
- console.error(`API keys start with "plx_" and are at least 20 characters`);
277
+ // Warn if API key was passed as command line argument
278
+ if (keySource === 'argument') {
279
+ console.log(`⚠ Security note: API key passed as argument is visible in`);
280
+ console.log(` process list (ps aux). For better security, use:`);
281
+ console.log(` echo "plx_..." | /plexor-login`);
282
+ console.log(` PLEXOR_API_KEY=plx_... /plexor-login`);
283
+ console.log('');
284
+ }
285
+
286
+ // Validate credential format (Plexor API key OR login JWT)
287
+ if (!isPlexorApiKey(apiKey) && !isJwtToken(apiKey)) {
288
+ console.error(`Error: Invalid credential format`);
289
+ console.error(`Expected Plexor API key ("plx_...") or login token ("eyJ...")`);
135
290
  process.exit(1);
136
291
  }
137
292
 
@@ -140,38 +295,52 @@ async function main() {
140
295
  console.log('Validating API key...');
141
296
 
142
297
  try {
143
- const user = await validateApiKey(apiUrl, apiKey);
298
+ const user = await validateCredential(apiUrl, apiKey);
144
299
 
145
300
  config.auth = config.auth || {};
146
301
  config.auth.api_key = apiKey;
147
302
  config.settings = config.settings || {};
148
303
  config.settings.enabled = true;
149
- saveConfig(config);
304
+ if (!saveConfig(config)) {
305
+ process.exit(1);
306
+ }
150
307
 
151
- // AUTO-CONFIGURE CLAUDE CODE ROUTING
152
- // This is the key feature: automatically set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
153
- // in ~/.claude/settings.json so ALL Claude Code sessions route through Plexor
308
+ // Automation path: save credentials and activate routing.
309
+ // Guided human setup should go through /plexor-setup, which also verifies the route.
154
310
  const useStaging = apiUrl.includes('staging');
155
311
  const routingEnabled = settingsManager.enablePlexorRouting(apiKey, { useStaging });
156
312
 
313
+ config.health = {
314
+ installed: true,
315
+ connected: true,
316
+ routing_active: routingEnabled,
317
+ verified: false,
318
+ verified_at: null,
319
+ verify_error: 'Verification not run. Use /plexor-setup for guided verification.',
320
+ gateway: useStaging ? 'staging' : 'production',
321
+ previous_auth_preserved: false
322
+ };
323
+ saveConfig(config);
324
+
157
325
  const email = user.email || 'Unknown';
158
326
  const tier = user.tier?.name || 'Free';
159
327
 
160
328
  console.log(`┌─────────────────────────────────────────────┐`);
161
- console.log(`│ ✓ Login Successful │`);
329
+ console.log(`│ ✓ Plexor Credentials Saved │`);
162
330
  console.log(`├─────────────────────────────────────────────┤`);
163
331
  console.log(`│ Email: ${email.substring(0, 35).padEnd(35)}│`);
164
332
  console.log(`│ Tier: ${tier.padEnd(36)}│`);
165
333
  console.log(`├─────────────────────────────────────────────┤`);
166
334
  if (routingEnabled) {
167
- console.log(`│ ✓ Claude Code routing: CONFIGURED │`);
168
- console.log(`│ All sessions now route through Plexor │`);
335
+ console.log(`│ ✓ Claude routing: ACTIVE │`);
336
+ console.log(`│ Claude verify: NOT RUN │`);
169
337
  } else {
170
- console.log(`│ ⚠ Claude Code routing: MANUAL SETUP NEEDED │`);
171
- console.log(`│ Set ANTHROPIC_BASE_URL in environment │`);
338
+ console.log(`│ ⚠ Claude routing: MANUAL SETUP NEEDED │`);
339
+ console.log(`│ Run /plexor-setup after fixing permissions │`);
172
340
  }
173
341
  console.log(`├─────────────────────────────────────────────┤`);
174
- console.log(`│ Run /plexor-status to see your stats. │`);
342
+ console.log(`│ Human first-run: /plexor-setup │`);
343
+ console.log(`│ Automation complete. Status: /plexor-status│`);
175
344
  console.log(`└─────────────────────────────────────────────┘`);
176
345
  } catch (err) {
177
346
  console.error(`┌─────────────────────────────────────────────┐`);
@@ -1,27 +1,10 @@
1
+ description: Advanced/manual Plexor authentication for automation and repair (user)
1
2
  ---
2
- description: Authenticate with Plexor to enable optimization (user)
3
- ---
4
-
5
- # Plexor Login
6
3
 
7
- Run this command to authenticate with Plexor:
8
-
9
- ```bash
10
- node ~/.claude/plugins/plexor/commands/plexor-login.js
11
- ```
4
+ **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.**
12
5
 
13
- To login with an API key:
6
+ Pass the API key as argument (format: `plx_your_api_key_here`).
14
7
 
15
8
  ```bash
16
- node ~/.claude/plugins/plexor/commands/plexor-login.js plx_your_api_key_here
9
+ node ~/.claude/plugins/plexor/commands/plexor-login.js $ARGUMENTS
17
10
  ```
18
-
19
- Use the Bash tool to execute this command.
20
-
21
- **IMPORTANT**: After running this command and displaying the output, STOP. Do not:
22
- - Read any files
23
- - Explore the codebase
24
- - Run additional commands
25
- - Ask follow-up questions
26
-
27
- The command output is the complete response. Simply show the output and wait for the user's next input.
@@ -8,25 +8,68 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
 
11
- const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
12
- const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
13
- const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
14
- const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
11
+ // Import centralized constants with HOME directory validation
12
+ const { PLEXOR_DIR, CONFIG_PATH, SESSION_PATH, CACHE_PATH } = require('../lib/constants');
13
+
14
+ // Import settings manager for Claude Code routing cleanup
15
+ let settingsManager;
16
+ try {
17
+ const lib = require('../lib/settings-manager');
18
+ settingsManager = lib.settingsManager;
19
+ } catch (err) {
20
+ if (err.code === 'MODULE_NOT_FOUND') {
21
+ console.error('Error: Plexor plugin files are missing or corrupted.');
22
+ console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin-staging');
23
+ process.exit(1);
24
+ }
25
+ throw err;
26
+ }
15
27
 
16
28
  function loadConfig() {
17
29
  try {
30
+ if (!fs.existsSync(CONFIG_PATH)) {
31
+ return null;
32
+ }
18
33
  const data = fs.readFileSync(CONFIG_PATH, 'utf8');
19
- return JSON.parse(data);
20
- } catch {
34
+ if (!data || data.trim() === '') {
35
+ return null;
36
+ }
37
+ const config = JSON.parse(data);
38
+ if (typeof config !== 'object' || config === null) {
39
+ return null;
40
+ }
41
+ return config;
42
+ } catch (err) {
43
+ if (err instanceof SyntaxError) {
44
+ console.warn('Warning: Config file is corrupted');
45
+ }
21
46
  return null;
22
47
  }
23
48
  }
24
49
 
25
50
  function saveConfig(config) {
26
- if (!fs.existsSync(PLEXOR_DIR)) {
27
- fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
51
+ try {
52
+ if (!fs.existsSync(PLEXOR_DIR)) {
53
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
54
+ }
55
+
56
+ // Atomic write: write to temp file, then rename
57
+ const crypto = require('crypto');
58
+ const tempId = crypto.randomBytes(8).toString('hex');
59
+ const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
60
+
61
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
62
+ fs.renameSync(tempPath, CONFIG_PATH);
63
+ return true;
64
+ } catch (err) {
65
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
66
+ console.error('Error: Cannot write to ~/.plexor/config.json');
67
+ console.error(' Check file permissions or run with appropriate access.');
68
+ } else {
69
+ console.error('Failed to save config:', err.message);
70
+ }
71
+ return false;
28
72
  }
29
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
30
73
  }
31
74
 
32
75
  function deleteFile(filePath) {
@@ -47,21 +90,34 @@ function main() {
47
90
 
48
91
  if (!config || !config.auth?.api_key) {
49
92
  console.log(`┌─────────────────────────────────────────────┐`);
50
- console.log(`│ Not Logged In │`);
93
+ console.log(`│ Plexor Is Not Active │`);
51
94
  console.log(`├─────────────────────────────────────────────┤`);
52
95
  console.log(`│ No active Plexor session found. │`);
53
- console.log(`│ Run /plexor-login to authenticate. │`);
96
+ console.log(`│ Run /plexor-setup to connect Plexor. │`);
54
97
  console.log(`└─────────────────────────────────────────────┘`);
55
98
  return;
56
99
  }
57
100
 
58
101
  const clearCache = args.includes('--clear-cache') || args.includes('-c');
102
+ const currentSettings = settingsManager.load();
103
+ const hadPriorAuth = Boolean(
104
+ currentSettings.env?.PLEXOR_PREVIOUS_ANTHROPIC_API_KEY ||
105
+ currentSettings.env?.PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN
106
+ );
59
107
 
60
108
  // Clear credentials
61
109
  delete config.auth.api_key;
62
110
  config.settings = config.settings || {};
63
111
  config.settings.enabled = false;
64
- saveConfig(config);
112
+ delete config.health;
113
+ if (!saveConfig(config)) {
114
+ process.exit(1);
115
+ }
116
+
117
+ // CRITICAL FIX: Disable Claude Code routing in ~/.claude/settings.json
118
+ // This removes ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN so Claude Code
119
+ // no longer tries to route through Plexor with removed credentials
120
+ const routingDisabled = settingsManager.disablePlexorRouting();
65
121
 
66
122
  // Clear session
67
123
  deleteFile(SESSION_PATH);
@@ -76,13 +132,15 @@ function main() {
76
132
  console.log(`│ ✓ Logged Out │`);
77
133
  console.log(`├─────────────────────────────────────────────┤`);
78
134
  console.log(`│ ✓ API key removed │`);
79
- console.log(`│ ✓ Plexor proxy disabled │`);
135
+ console.log(`│ ${routingDisabled ? '' : '○'} Claude Code routing disabled │`);
136
+ console.log(`│ ${hadPriorAuth ? '✓' : '○'} Prior Claude auth restored │`);
80
137
  console.log(`│ ✓ Session cleared │`);
81
138
  if (clearCache) {
82
139
  console.log(`│ ${cacheCleared ? '✓' : '○'} Cache cleared │`);
83
140
  }
84
141
  console.log(`├─────────────────────────────────────────────┤`);
85
- console.log(`│ Run /plexor-login to re-authenticate. │`);
142
+ console.log(`│ Claude Code now connects directly. │`);
143
+ console.log(`│ Run /plexor-setup to reconnect Plexor. │`);
86
144
  if (!clearCache) {
87
145
  console.log(`│ Use --clear-cache to also clear cache. │`);
88
146
  }
@@ -2,26 +2,8 @@
2
2
  description: Log out from Plexor and clear credentials (user)
3
3
  ---
4
4
 
5
- # Plexor Logout
6
-
7
- Run this command to log out and clear credentials:
8
-
9
- ```bash
10
- node ~/.claude/plugins/plexor/commands/plexor-logout.js
11
- ```
12
-
13
- To also clear the cache:
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.**
14
6
 
15
7
  ```bash
16
- node ~/.claude/plugins/plexor/commands/plexor-logout.js --clear-cache
8
+ node ~/.claude/plugins/plexor/commands/plexor-logout.js $ARGUMENTS
17
9
  ```
18
-
19
- Use the Bash tool to execute this command.
20
-
21
- **IMPORTANT**: After running this command and displaying the output, STOP. Do not:
22
- - Read any files
23
- - Explore the codebase
24
- - Run additional commands
25
- - Ask follow-up questions
26
-
27
- The command output is the complete response. Simply show the output and wait for the user's next input.