@plexor-dev/claude-code-plugin-staging 0.1.0-beta.2 → 0.1.0-beta.21
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/README.md +4 -7
- package/commands/plexor-agent.js +84 -0
- package/commands/plexor-agent.md +36 -0
- package/commands/plexor-enabled.js +177 -18
- package/commands/plexor-enabled.md +31 -13
- package/commands/plexor-login.js +212 -43
- package/commands/plexor-login.md +4 -21
- package/commands/plexor-logout.js +72 -14
- package/commands/plexor-logout.md +2 -20
- package/commands/plexor-provider.js +62 -81
- package/commands/plexor-provider.md +23 -13
- package/commands/plexor-routing.js +77 -0
- package/commands/plexor-routing.md +37 -0
- package/commands/plexor-settings.js +161 -123
- package/commands/plexor-settings.md +38 -14
- package/commands/plexor-setup.js +253 -0
- package/commands/plexor-setup.md +16 -160
- package/commands/plexor-status.js +252 -24
- package/commands/plexor-status.md +1 -13
- package/commands/plexor-uninstall.js +319 -0
- package/commands/plexor-uninstall.md +12 -0
- package/hooks/intercept.js +211 -32
- package/hooks/track-response.js +302 -2
- package/lib/config-utils.js +314 -0
- package/lib/config.js +22 -3
- package/lib/constants.js +19 -1
- package/lib/logger.js +64 -5
- package/lib/settings-manager.js +233 -24
- package/lib/verify-route.js +84 -0
- package/package.json +6 -4
- package/scripts/postinstall.js +271 -44
- package/scripts/uninstall.js +194 -41
- package/commands/plexor-config.js +0 -170
- package/commands/plexor-config.md +0 -28
- package/commands/plexor-mode.js +0 -107
- package/commands/plexor-mode.md +0 -27
package/lib/constants.js
CHANGED
|
@@ -4,7 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Get the user's home directory with proper validation
|
|
9
|
+
* @returns {string} Home directory path
|
|
10
|
+
* @throws {Error} If HOME is not set or empty
|
|
11
|
+
*/
|
|
12
|
+
function getHomeDir() {
|
|
13
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
14
|
+
if (!home || home.trim() === '') {
|
|
15
|
+
console.error('Error: HOME environment variable is not set.');
|
|
16
|
+
console.error(' Please set HOME to your user directory before running Plexor commands.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
return home;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const HOME_DIR = getHomeDir();
|
|
23
|
+
const PLEXOR_DIR = path.join(HOME_DIR, '.plexor');
|
|
8
24
|
const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
|
|
9
25
|
const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
|
|
10
26
|
const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
|
|
@@ -16,6 +32,8 @@ const DEFAULT_API_URL = 'https://staging.api.plexor.dev';
|
|
|
16
32
|
const DEFAULT_TIMEOUT = 5000;
|
|
17
33
|
|
|
18
34
|
module.exports = {
|
|
35
|
+
getHomeDir,
|
|
36
|
+
HOME_DIR,
|
|
19
37
|
PLEXOR_DIR,
|
|
20
38
|
CONFIG_PATH,
|
|
21
39
|
SESSION_PATH,
|
package/lib/logger.js
CHANGED
|
@@ -2,35 +2,94 @@
|
|
|
2
2
|
* Plexor Logger
|
|
3
3
|
*
|
|
4
4
|
* Simple logger that outputs to stderr to avoid interfering with stdout JSON.
|
|
5
|
+
* Uses ANSI-colored badges for branded Plexor messages.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
// ANSI color codes for branded badge output
|
|
9
|
+
const RESET = '\x1b[0m';
|
|
10
|
+
const BOLD = '\x1b[1m';
|
|
11
|
+
const DIM = '\x1b[2m';
|
|
12
|
+
const WHITE = '\x1b[37m';
|
|
13
|
+
const YELLOW_FG = '\x1b[33m';
|
|
14
|
+
const RED_FG = '\x1b[31m';
|
|
15
|
+
const CYAN_FG = '\x1b[36m';
|
|
16
|
+
const BLUE_BG = '\x1b[44m';
|
|
17
|
+
const YELLOW_BG = '\x1b[43m';
|
|
18
|
+
const RED_BG = '\x1b[41m';
|
|
19
|
+
|
|
20
|
+
// Pre-built badge strings
|
|
21
|
+
const BADGE_INFO = `${BOLD}${WHITE}${BLUE_BG} PLEXOR ${RESET}`;
|
|
22
|
+
const BADGE_WARN = `${BOLD}${WHITE}${YELLOW_BG} PLEXOR ${RESET}`;
|
|
23
|
+
const BADGE_ERROR = `${BOLD}${WHITE}${RED_BG} PLEXOR ${RESET}`;
|
|
24
|
+
|
|
25
|
+
function parseBooleanEnv(name, defaultValue) {
|
|
26
|
+
const raw = process.env[name];
|
|
27
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
31
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return defaultValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatUxMessage(message) {
|
|
41
|
+
const text = String(message || '').trim();
|
|
42
|
+
if (!text) {
|
|
43
|
+
return '[PLEXOR: message]';
|
|
44
|
+
}
|
|
45
|
+
if (text.startsWith('[PLEXOR:')) {
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
if (text.startsWith('[PLEXOR') && text.endsWith(']')) {
|
|
49
|
+
return text.replace(/^\[PLEXOR[^\]]*\]\s*/i, '[PLEXOR: ');
|
|
50
|
+
}
|
|
51
|
+
return `[PLEXOR: ${text}]`;
|
|
52
|
+
}
|
|
53
|
+
|
|
7
54
|
class Logger {
|
|
8
55
|
constructor(component = 'plexor') {
|
|
9
56
|
this.component = component;
|
|
10
|
-
this.debug_enabled =
|
|
57
|
+
this.debug_enabled = parseBooleanEnv('PLEXOR_DEBUG', false);
|
|
58
|
+
this.ux_messages_enabled =
|
|
59
|
+
parseBooleanEnv('PLEXOR_UX_MESSAGES', true) &&
|
|
60
|
+
parseBooleanEnv('PLEXOR_UX_DEBUG_MESSAGES', true);
|
|
11
61
|
}
|
|
12
62
|
|
|
13
63
|
debug(msg, data = null) {
|
|
14
64
|
if (this.debug_enabled) {
|
|
15
|
-
const output = data ?
|
|
65
|
+
const output = data ? `${DIM}[${this.component}]${RESET} ${msg} ${JSON.stringify(data)}` : `${DIM}[${this.component}]${RESET} ${msg}`;
|
|
16
66
|
console.error(output);
|
|
17
67
|
}
|
|
18
68
|
}
|
|
19
69
|
|
|
20
70
|
info(msg, data = null) {
|
|
21
|
-
const output = data ? `${msg} ${JSON.stringify(data)}` : msg
|
|
71
|
+
const output = data ? `${BADGE_INFO} ${msg} ${JSON.stringify(data)}` : `${BADGE_INFO} ${msg}`;
|
|
22
72
|
console.error(output);
|
|
23
73
|
}
|
|
24
74
|
|
|
25
75
|
warn(msg, data = null) {
|
|
26
|
-
const output = data ?
|
|
76
|
+
const output = data ? `${BADGE_WARN} ${YELLOW_FG}${msg} ${JSON.stringify(data)}${RESET}` : `${BADGE_WARN} ${YELLOW_FG}${msg}${RESET}`;
|
|
27
77
|
console.error(output);
|
|
28
78
|
}
|
|
29
79
|
|
|
30
80
|
error(msg, data = null) {
|
|
31
|
-
const output = data ?
|
|
81
|
+
const output = data ? `${BADGE_ERROR} ${RED_FG}${msg} ${JSON.stringify(data)}${RESET}` : `${BADGE_ERROR} ${RED_FG}${msg}${RESET}`;
|
|
32
82
|
console.error(output);
|
|
33
83
|
}
|
|
84
|
+
|
|
85
|
+
ux(msg, data = null) {
|
|
86
|
+
if (!this.ux_messages_enabled) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const prefixed = formatUxMessage(msg);
|
|
90
|
+
const output = data ? `${prefixed} ${JSON.stringify(data)}` : prefixed;
|
|
91
|
+
console.error(`${CYAN_FG}${output}${RESET}`);
|
|
92
|
+
}
|
|
34
93
|
}
|
|
35
94
|
|
|
36
95
|
module.exports = Logger;
|
package/lib/settings-manager.js
CHANGED
|
@@ -12,13 +12,111 @@
|
|
|
12
12
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
15
17
|
|
|
16
18
|
const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
|
|
17
19
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
20
|
+
const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
|
|
18
21
|
|
|
19
22
|
// Plexor gateway endpoints
|
|
20
23
|
const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
21
24
|
const PLEXOR_PROD_URL = 'https://api.plexor.dev/gateway/anthropic';
|
|
25
|
+
const PREVIOUS_API_KEY_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_API_KEY';
|
|
26
|
+
const PREVIOUS_AUTH_TOKEN_ENV = 'PLEXOR_PREVIOUS_ANTHROPIC_AUTH_TOKEN';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a base URL is a Plexor-managed gateway URL.
|
|
30
|
+
* Detects all variants: production, staging, localhost, tunnels.
|
|
31
|
+
*/
|
|
32
|
+
function isManagedGatewayUrl(baseUrl) {
|
|
33
|
+
if (!baseUrl) return false;
|
|
34
|
+
return (
|
|
35
|
+
baseUrl.includes('plexor') ||
|
|
36
|
+
baseUrl.includes('staging.api') ||
|
|
37
|
+
baseUrl.includes('localhost') ||
|
|
38
|
+
baseUrl.includes('127.0.0.1') ||
|
|
39
|
+
baseUrl.includes('ngrok') ||
|
|
40
|
+
baseUrl.includes('localtunnel')
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isPlexorApiKey(value = '') {
|
|
45
|
+
return typeof value === 'string' && value.startsWith('plx_');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getPlexorAuthKey(env = {}) {
|
|
49
|
+
const apiKey = env.ANTHROPIC_API_KEY || '';
|
|
50
|
+
const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
|
|
51
|
+
|
|
52
|
+
if (isPlexorApiKey(apiKey)) return apiKey;
|
|
53
|
+
if (isPlexorApiKey(authToken)) return authToken;
|
|
54
|
+
return apiKey || authToken || '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasPlexorManagedAuth(env = {}) {
|
|
58
|
+
return (
|
|
59
|
+
isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '') ||
|
|
60
|
+
isPlexorApiKey(env.ANTHROPIC_API_KEY || '') ||
|
|
61
|
+
isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '')
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stashDirectAuthEnv(env, plexorApiKey) {
|
|
66
|
+
const alreadyManaged = hasPlexorManagedAuth(env);
|
|
67
|
+
const currentApiKey = env.ANTHROPIC_API_KEY || '';
|
|
68
|
+
const currentAuthToken = env.ANTHROPIC_AUTH_TOKEN || '';
|
|
69
|
+
|
|
70
|
+
if (currentApiKey && !isPlexorApiKey(currentApiKey) && currentApiKey !== plexorApiKey) {
|
|
71
|
+
env[PREVIOUS_API_KEY_ENV] = currentApiKey;
|
|
72
|
+
} else if (!alreadyManaged) {
|
|
73
|
+
delete env[PREVIOUS_API_KEY_ENV];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (currentAuthToken && !isPlexorApiKey(currentAuthToken) && currentAuthToken !== plexorApiKey) {
|
|
77
|
+
env[PREVIOUS_AUTH_TOKEN_ENV] = currentAuthToken;
|
|
78
|
+
} else if (!alreadyManaged) {
|
|
79
|
+
delete env[PREVIOUS_AUTH_TOKEN_ENV];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setPlexorAuthKey(env, apiKey) {
|
|
84
|
+
stashDirectAuthEnv(env, apiKey);
|
|
85
|
+
env.ANTHROPIC_API_KEY = apiKey;
|
|
86
|
+
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clearPlexorRoutingEnv(env = {}) {
|
|
90
|
+
const hasManagedBaseUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '');
|
|
91
|
+
const hasPlexorApiKey = isPlexorApiKey(env.ANTHROPIC_API_KEY || '');
|
|
92
|
+
const hasPlexorAuthToken = isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '');
|
|
93
|
+
|
|
94
|
+
if (!hasManagedBaseUrl && !hasPlexorApiKey && !hasPlexorAuthToken) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (hasManagedBaseUrl) {
|
|
99
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
100
|
+
}
|
|
101
|
+
if (hasPlexorAuthToken) {
|
|
102
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
103
|
+
}
|
|
104
|
+
if (hasPlexorApiKey) {
|
|
105
|
+
delete env.ANTHROPIC_API_KEY;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!env.ANTHROPIC_API_KEY && env[PREVIOUS_API_KEY_ENV]) {
|
|
109
|
+
env.ANTHROPIC_API_KEY = env[PREVIOUS_API_KEY_ENV];
|
|
110
|
+
}
|
|
111
|
+
if (!env.ANTHROPIC_AUTH_TOKEN && env[PREVIOUS_AUTH_TOKEN_ENV]) {
|
|
112
|
+
env.ANTHROPIC_AUTH_TOKEN = env[PREVIOUS_AUTH_TOKEN_ENV];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
delete env[PREVIOUS_API_KEY_ENV];
|
|
116
|
+
delete env[PREVIOUS_AUTH_TOKEN_ENV];
|
|
117
|
+
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
22
120
|
|
|
23
121
|
class ClaudeSettingsManager {
|
|
24
122
|
constructor() {
|
|
@@ -27,8 +125,8 @@ class ClaudeSettingsManager {
|
|
|
27
125
|
}
|
|
28
126
|
|
|
29
127
|
/**
|
|
30
|
-
* Load current Claude settings
|
|
31
|
-
* @returns {Object} settings object or empty object if not found
|
|
128
|
+
* Load current Claude settings with integrity checking
|
|
129
|
+
* @returns {Object} settings object or empty object if not found/corrupted
|
|
32
130
|
*/
|
|
33
131
|
load() {
|
|
34
132
|
try {
|
|
@@ -36,15 +134,84 @@ class ClaudeSettingsManager {
|
|
|
36
134
|
return {};
|
|
37
135
|
}
|
|
38
136
|
const data = fs.readFileSync(this.settingsPath, 'utf8');
|
|
39
|
-
|
|
137
|
+
|
|
138
|
+
// Check for empty file
|
|
139
|
+
if (!data || data.trim() === '') {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parsed = JSON.parse(data);
|
|
144
|
+
|
|
145
|
+
// Basic schema validation - must be an object
|
|
146
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
147
|
+
const backupPath = this._backupCorruptedFile();
|
|
148
|
+
console.warn('');
|
|
149
|
+
console.warn('WARNING: Claude settings file has invalid format!');
|
|
150
|
+
if (backupPath) {
|
|
151
|
+
console.warn(` Corrupted file backed up to: ${backupPath}`);
|
|
152
|
+
}
|
|
153
|
+
console.warn(' Using default settings. Your previous settings may need manual recovery.');
|
|
154
|
+
console.warn('');
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return parsed;
|
|
40
159
|
} catch (err) {
|
|
41
|
-
|
|
160
|
+
if (err.code === 'ENOENT') {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
// JSON parse error or corrupted file
|
|
164
|
+
if (err instanceof SyntaxError) {
|
|
165
|
+
const backupPath = this._backupCorruptedFile();
|
|
166
|
+
console.warn('');
|
|
167
|
+
console.warn('WARNING: Claude settings file is corrupted (invalid JSON)!');
|
|
168
|
+
if (backupPath) {
|
|
169
|
+
console.warn(` Corrupted file backed up to: ${backupPath}`);
|
|
170
|
+
}
|
|
171
|
+
console.warn(' Using default settings. Your previous settings may need manual recovery.');
|
|
172
|
+
console.warn('');
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
// Permission error
|
|
176
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
177
|
+
console.warn(`Warning: Cannot read ${this.settingsPath} (permission denied)`);
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
console.warn('Warning: Failed to load Claude settings:', err.message);
|
|
42
181
|
return {};
|
|
43
182
|
}
|
|
44
183
|
}
|
|
45
184
|
|
|
46
185
|
/**
|
|
47
|
-
*
|
|
186
|
+
* Backup a corrupted settings file with numbered suffix for debugging
|
|
187
|
+
* Creates settings.json.corrupted.1, .corrupted.2, etc. to preserve history
|
|
188
|
+
* @returns {string|null} path to backup file, or null if backup failed
|
|
189
|
+
*/
|
|
190
|
+
_backupCorruptedFile() {
|
|
191
|
+
try {
|
|
192
|
+
if (fs.existsSync(this.settingsPath)) {
|
|
193
|
+
// Find next available numbered backup
|
|
194
|
+
let backupNum = 1;
|
|
195
|
+
let backupPath;
|
|
196
|
+
while (true) {
|
|
197
|
+
backupPath = `${this.settingsPath}.corrupted.${backupNum}`;
|
|
198
|
+
if (!fs.existsSync(backupPath)) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
backupNum++;
|
|
202
|
+
}
|
|
203
|
+
fs.copyFileSync(this.settingsPath, backupPath);
|
|
204
|
+
return backupPath;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// Ignore backup errors silently
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Save Claude settings using atomic write pattern
|
|
214
|
+
* Prevents race conditions by writing to temp file then renaming
|
|
48
215
|
* @param {Object} settings - settings object to save
|
|
49
216
|
* @returns {boolean} success status
|
|
50
217
|
*/
|
|
@@ -55,10 +222,27 @@ class ClaudeSettingsManager {
|
|
|
55
222
|
fs.mkdirSync(this.claudeDir, { recursive: true });
|
|
56
223
|
}
|
|
57
224
|
|
|
58
|
-
|
|
225
|
+
// Atomic write: write to temp file, then rename
|
|
226
|
+
// This prevents race conditions where concurrent writes corrupt the file
|
|
227
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
228
|
+
const tempPath = path.join(this.claudeDir, `.settings.${tempId}.tmp`);
|
|
229
|
+
|
|
230
|
+
// Write to temp file
|
|
231
|
+
const content = JSON.stringify(settings, null, 2);
|
|
232
|
+
fs.writeFileSync(tempPath, content, { mode: 0o600 });
|
|
233
|
+
|
|
234
|
+
// Atomic rename (on POSIX systems, rename is atomic)
|
|
235
|
+
fs.renameSync(tempPath, this.settingsPath);
|
|
236
|
+
|
|
59
237
|
return true;
|
|
60
238
|
} catch (err) {
|
|
61
|
-
|
|
239
|
+
// Clean error message for permission errors
|
|
240
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
241
|
+
console.error(`Error: Cannot write to ${this.settingsPath}`);
|
|
242
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
243
|
+
} else {
|
|
244
|
+
console.error('Failed to save Claude settings:', err.message);
|
|
245
|
+
}
|
|
62
246
|
return false;
|
|
63
247
|
}
|
|
64
248
|
}
|
|
@@ -66,8 +250,9 @@ class ClaudeSettingsManager {
|
|
|
66
250
|
/**
|
|
67
251
|
* Enable Plexor routing by setting env vars in settings.json
|
|
68
252
|
*
|
|
69
|
-
* This is the KEY mechanism: setting ANTHROPIC_BASE_URL
|
|
70
|
-
*
|
|
253
|
+
* This is the KEY mechanism: setting ANTHROPIC_BASE_URL plus both Claude auth
|
|
254
|
+
* env vars in settings.json redirects ALL Claude Code sessions to Plexor
|
|
255
|
+
* automatically, even when the user previously configured direct API auth.
|
|
71
256
|
*
|
|
72
257
|
* @param {string} apiKey - Plexor API key (plx_*)
|
|
73
258
|
* @param {Object} options - { useStaging: boolean }
|
|
@@ -86,10 +271,10 @@ class ClaudeSettingsManager {
|
|
|
86
271
|
settings.env = {};
|
|
87
272
|
}
|
|
88
273
|
|
|
89
|
-
//
|
|
90
|
-
//
|
|
274
|
+
// Mirror the Plexor key into both Claude auth env vars so every Claude
|
|
275
|
+
// runtime path uses the gateway instead of any previously saved direct key.
|
|
91
276
|
settings.env.ANTHROPIC_BASE_URL = apiUrl;
|
|
92
|
-
settings.env
|
|
277
|
+
setPlexorAuthKey(settings.env, apiKey);
|
|
93
278
|
|
|
94
279
|
const success = this.save(settings);
|
|
95
280
|
|
|
@@ -124,9 +309,10 @@ class ClaudeSettingsManager {
|
|
|
124
309
|
return true;
|
|
125
310
|
}
|
|
126
311
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
312
|
+
if (!clearPlexorRoutingEnv(settings.env)) {
|
|
313
|
+
console.log('Plexor routing is not currently enabled.');
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
130
316
|
|
|
131
317
|
// Clean up empty env block
|
|
132
318
|
if (Object.keys(settings.env).length === 0) {
|
|
@@ -157,26 +343,48 @@ class ClaudeSettingsManager {
|
|
|
157
343
|
const settings = this.load();
|
|
158
344
|
|
|
159
345
|
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || null;
|
|
160
|
-
const
|
|
346
|
+
const authKey = getPlexorAuthKey(settings.env);
|
|
347
|
+
const hasToken = !!authKey;
|
|
161
348
|
|
|
162
|
-
// Check if routing to Plexor
|
|
163
|
-
const isPlexorRouting = baseUrl
|
|
164
|
-
baseUrl.includes('plexor') ||
|
|
165
|
-
baseUrl.includes('staging.api')
|
|
166
|
-
);
|
|
349
|
+
// Check if routing to Plexor (any variant: prod, staging, localhost, tunnel)
|
|
350
|
+
const isPlexorRouting = isManagedGatewayUrl(baseUrl);
|
|
167
351
|
|
|
168
352
|
return {
|
|
169
353
|
enabled: isPlexorRouting,
|
|
170
354
|
baseUrl,
|
|
171
355
|
hasToken,
|
|
172
356
|
isStaging: baseUrl?.includes('staging') || false,
|
|
173
|
-
tokenPreview: hasToken ?
|
|
357
|
+
tokenPreview: hasToken ? authKey.substring(0, 12) + '...' : null
|
|
174
358
|
};
|
|
175
359
|
} catch {
|
|
176
360
|
return { enabled: false, baseUrl: null, hasToken: false };
|
|
177
361
|
}
|
|
178
362
|
}
|
|
179
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Detect partial routing state where URL points to Plexor but auth is missing/invalid
|
|
366
|
+
* This can cause confusing auth errors for users
|
|
367
|
+
* @returns {Object} { partial: boolean, issue: string|null }
|
|
368
|
+
*/
|
|
369
|
+
detectPartialState() {
|
|
370
|
+
try {
|
|
371
|
+
const settings = this.load();
|
|
372
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
|
|
373
|
+
const authKey = getPlexorAuthKey(settings.env);
|
|
374
|
+
const isPlexorUrl = isManagedGatewayUrl(baseUrl);
|
|
375
|
+
|
|
376
|
+
if (isPlexorUrl && !authKey) {
|
|
377
|
+
return { partial: true, issue: 'Plexor URL set but no Plexor auth key' };
|
|
378
|
+
}
|
|
379
|
+
if (isPlexorUrl && !isPlexorApiKey(authKey)) {
|
|
380
|
+
return { partial: true, issue: 'Plexor URL set but auth key is not a Plexor key' };
|
|
381
|
+
}
|
|
382
|
+
return { partial: false, issue: null };
|
|
383
|
+
} catch {
|
|
384
|
+
return { partial: false, issue: null };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
180
388
|
/**
|
|
181
389
|
* Update just the API key without changing other settings
|
|
182
390
|
* @param {string} apiKey - new Plexor API key
|
|
@@ -190,7 +398,7 @@ class ClaudeSettingsManager {
|
|
|
190
398
|
settings.env = {};
|
|
191
399
|
}
|
|
192
400
|
|
|
193
|
-
settings.env
|
|
401
|
+
setPlexorAuthKey(settings.env, apiKey);
|
|
194
402
|
return this.save(settings);
|
|
195
403
|
} catch (err) {
|
|
196
404
|
console.error('Failed to update API key:', err.message);
|
|
@@ -208,7 +416,7 @@ class ClaudeSettingsManager {
|
|
|
208
416
|
const settings = this.load();
|
|
209
417
|
|
|
210
418
|
if (!settings.env?.ANTHROPIC_BASE_URL) {
|
|
211
|
-
console.log('Plexor routing is not enabled. Run /plexor-
|
|
419
|
+
console.log('Plexor routing is not enabled. Run /plexor-setup first.');
|
|
212
420
|
return false;
|
|
213
421
|
}
|
|
214
422
|
|
|
@@ -232,6 +440,7 @@ const settingsManager = new ClaudeSettingsManager();
|
|
|
232
440
|
module.exports = {
|
|
233
441
|
ClaudeSettingsManager,
|
|
234
442
|
settingsManager,
|
|
443
|
+
isManagedGatewayUrl,
|
|
235
444
|
CLAUDE_DIR,
|
|
236
445
|
SETTINGS_PATH,
|
|
237
446
|
PLEXOR_STAGING_URL,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const { spawnSync } = require('child_process');
|
|
2
|
+
const { HOME_DIR } = require('./constants');
|
|
3
|
+
|
|
4
|
+
const VERIFY_PROMPT = 'Reply with exactly PONG. Do not use tools.';
|
|
5
|
+
const EXPECTED_VERIFY_RESPONSE = 'PONG';
|
|
6
|
+
|
|
7
|
+
function normalizeVerifyOutput(output = '') {
|
|
8
|
+
return String(output).trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createVerifyEnv(sourceEnv = process.env) {
|
|
12
|
+
const env = { ...sourceEnv };
|
|
13
|
+
delete env.CLAUDECODE;
|
|
14
|
+
return env;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function interpretVerifyResult(result = {}) {
|
|
18
|
+
const stdout = normalizeVerifyOutput(result.stdout);
|
|
19
|
+
const stderr = normalizeVerifyOutput(result.stderr);
|
|
20
|
+
|
|
21
|
+
if (result.error) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
stdout,
|
|
25
|
+
stderr,
|
|
26
|
+
code: null,
|
|
27
|
+
reason: result.error.message || 'Claude verification failed to start'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (result.status !== 0) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
stdout,
|
|
35
|
+
stderr,
|
|
36
|
+
code: result.status,
|
|
37
|
+
reason: stderr || stdout || `Claude exited with status ${result.status}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (stdout !== EXPECTED_VERIFY_RESPONSE) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
stdout,
|
|
45
|
+
stderr,
|
|
46
|
+
code: result.status,
|
|
47
|
+
reason: `Expected "${EXPECTED_VERIFY_RESPONSE}" but received "${stdout || '(empty)'}"`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
stdout,
|
|
54
|
+
stderr,
|
|
55
|
+
code: result.status,
|
|
56
|
+
reason: ''
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runClaudeRouteVerification(options = {}, runCommand = spawnSync) {
|
|
61
|
+
const result = runCommand(options.claudeCommand || 'claude', [
|
|
62
|
+
'-p',
|
|
63
|
+
'--tools', '',
|
|
64
|
+
'--output-format', 'text',
|
|
65
|
+
VERIFY_PROMPT
|
|
66
|
+
], {
|
|
67
|
+
cwd: options.cwd || HOME_DIR,
|
|
68
|
+
env: createVerifyEnv(options.env || process.env),
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
timeout: options.timeoutMs || 30000,
|
|
71
|
+
maxBuffer: 1024 * 1024
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return interpretVerifyResult(result);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
EXPECTED_VERIFY_RESPONSE,
|
|
79
|
+
VERIFY_PROMPT,
|
|
80
|
+
createVerifyEnv,
|
|
81
|
+
interpretVerifyResult,
|
|
82
|
+
normalizeVerifyOutput,
|
|
83
|
+
runClaudeRouteVerification
|
|
84
|
+
};
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plexor-dev/claude-code-plugin-staging",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.21",
|
|
4
4
|
"description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
|
|
5
5
|
"main": "lib/constants.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"plexor-status": "./commands/plexor-status.js",
|
|
8
|
-
"plexor-
|
|
8
|
+
"plexor-setup": "./commands/plexor-setup.js",
|
|
9
9
|
"plexor-enabled": "./commands/plexor-enabled.js",
|
|
10
|
-
"plexor-provider": "./commands/plexor-provider.js",
|
|
11
10
|
"plexor-login": "./commands/plexor-login.js",
|
|
12
11
|
"plexor-logout": "./commands/plexor-logout.js",
|
|
12
|
+
"plexor-uninstall": "./commands/plexor-uninstall.js",
|
|
13
13
|
"plexor-settings": "./commands/plexor-settings.js",
|
|
14
|
-
"plexor-
|
|
14
|
+
"plexor-routing": "./commands/plexor-routing.js",
|
|
15
|
+
"plexor-agent": "./commands/plexor-agent.js",
|
|
16
|
+
"plexor-provider": "./commands/plexor-provider.js"
|
|
15
17
|
},
|
|
16
18
|
"scripts": {
|
|
17
19
|
"postinstall": "node scripts/postinstall.js",
|