@plexor-dev/claude-code-plugin-localhost 0.1.0-beta.12 → 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.
- package/package.json +1 -1
- package/lib/cache.js +0 -107
- package/lib/config.js +0 -67
- package/lib/constants.js +0 -44
- package/lib/index.js +0 -19
- package/lib/logger.js +0 -36
- package/lib/plexor-client.js +0 -122
- package/lib/server-sync.js +0 -237
- package/lib/session.js +0 -156
- package/lib/settings-manager.js +0 -352
package/package.json
CHANGED
package/lib/cache.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor Local Cache
|
|
3
|
-
*
|
|
4
|
-
* Stores request/response metadata for cache hit detection.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const { CACHE_PATH, PLEXOR_DIR } = require('./constants');
|
|
9
|
-
|
|
10
|
-
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
|
-
|
|
12
|
-
class LocalCache {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.cachePath = CACHE_PATH;
|
|
15
|
-
this.cache = this.load();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
load() {
|
|
19
|
-
try {
|
|
20
|
-
const data = fs.readFileSync(this.cachePath, 'utf8');
|
|
21
|
-
return JSON.parse(data);
|
|
22
|
-
} catch {
|
|
23
|
-
return { entries: {}, metadata: {} };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
save() {
|
|
28
|
-
try {
|
|
29
|
-
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
30
|
-
fs.mkdirSync(PLEXOR_DIR, { recursive: true });
|
|
31
|
-
}
|
|
32
|
-
fs.writeFileSync(this.cachePath, JSON.stringify(this.cache, null, 2));
|
|
33
|
-
return true;
|
|
34
|
-
} catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
generateKey(messages) {
|
|
40
|
-
const str = JSON.stringify(messages);
|
|
41
|
-
let hash = 0;
|
|
42
|
-
for (let i = 0; i < str.length; i++) {
|
|
43
|
-
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
44
|
-
hash |= 0;
|
|
45
|
-
}
|
|
46
|
-
return `cache_${Math.abs(hash)}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async get(key) {
|
|
50
|
-
const entry = this.cache.entries[key];
|
|
51
|
-
if (!entry) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Check if expired
|
|
56
|
-
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
|
|
57
|
-
delete this.cache.entries[key];
|
|
58
|
-
this.save();
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return entry.value;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async set(key, value) {
|
|
66
|
-
this.cache.entries[key] = {
|
|
67
|
-
value,
|
|
68
|
-
timestamp: Date.now()
|
|
69
|
-
};
|
|
70
|
-
this.cleanup();
|
|
71
|
-
this.save();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async getMetadata(requestId) {
|
|
75
|
-
return this.cache.metadata[requestId] || null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async setMetadata(requestId, metadata) {
|
|
79
|
-
this.cache.metadata[requestId] = {
|
|
80
|
-
...metadata,
|
|
81
|
-
timestamp: Date.now()
|
|
82
|
-
};
|
|
83
|
-
this.cleanupMetadata();
|
|
84
|
-
this.save();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
cleanup() {
|
|
88
|
-
const now = Date.now();
|
|
89
|
-
for (const key of Object.keys(this.cache.entries)) {
|
|
90
|
-
if (now - this.cache.entries[key].timestamp > CACHE_TTL_MS) {
|
|
91
|
-
delete this.cache.entries[key];
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
cleanupMetadata() {
|
|
97
|
-
const now = Date.now();
|
|
98
|
-
const METADATA_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
99
|
-
for (const key of Object.keys(this.cache.metadata)) {
|
|
100
|
-
if (now - this.cache.metadata[key].timestamp > METADATA_TTL_MS) {
|
|
101
|
-
delete this.cache.metadata[key];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
module.exports = LocalCache;
|
package/lib/config.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor Configuration Manager
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const { CONFIG_PATH, PLEXOR_DIR, DEFAULT_API_URL, DEFAULT_TIMEOUT } = require('./constants');
|
|
8
|
-
|
|
9
|
-
class ConfigManager {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.configPath = CONFIG_PATH;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async load() {
|
|
15
|
-
try {
|
|
16
|
-
const data = fs.readFileSync(this.configPath, 'utf8');
|
|
17
|
-
const cfg = JSON.parse(data);
|
|
18
|
-
return {
|
|
19
|
-
enabled: cfg.settings?.enabled ?? false,
|
|
20
|
-
apiKey: cfg.auth?.api_key || '',
|
|
21
|
-
apiUrl: cfg.settings?.apiUrl || DEFAULT_API_URL,
|
|
22
|
-
timeout: cfg.settings?.timeout || DEFAULT_TIMEOUT,
|
|
23
|
-
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
24
|
-
mode: cfg.settings?.mode || 'balanced',
|
|
25
|
-
preferredProvider: cfg.settings?.preferred_provider || 'auto'
|
|
26
|
-
};
|
|
27
|
-
} catch {
|
|
28
|
-
return { enabled: false, apiKey: '', apiUrl: DEFAULT_API_URL };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async save(config) {
|
|
33
|
-
try {
|
|
34
|
-
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
35
|
-
fs.mkdirSync(PLEXOR_DIR, { recursive: true });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let existing = {};
|
|
39
|
-
try {
|
|
40
|
-
const data = fs.readFileSync(this.configPath, 'utf8');
|
|
41
|
-
existing = JSON.parse(data);
|
|
42
|
-
} catch {
|
|
43
|
-
existing = { version: 1, auth: {}, settings: {} };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const updated = {
|
|
47
|
-
...existing,
|
|
48
|
-
settings: {
|
|
49
|
-
...existing.settings,
|
|
50
|
-
enabled: config.enabled ?? existing.settings?.enabled,
|
|
51
|
-
apiUrl: config.apiUrl ?? existing.settings?.apiUrl,
|
|
52
|
-
timeout: config.timeout ?? existing.settings?.timeout,
|
|
53
|
-
localCacheEnabled: config.localCacheEnabled ?? existing.settings?.localCacheEnabled,
|
|
54
|
-
mode: config.mode ?? existing.settings?.mode,
|
|
55
|
-
preferred_provider: config.preferredProvider ?? existing.settings?.preferred_provider
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
fs.writeFileSync(this.configPath, JSON.stringify(updated, null, 2), { mode: 0o600 });
|
|
60
|
-
return true;
|
|
61
|
-
} catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
module.exports = ConfigManager;
|
package/lib/constants.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor Plugin Constants
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
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');
|
|
24
|
-
const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
|
|
25
|
-
const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
|
|
26
|
-
const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
|
|
27
|
-
|
|
28
|
-
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
29
|
-
|
|
30
|
-
// STAGING PACKAGE - uses staging API
|
|
31
|
-
const DEFAULT_API_URL = 'LOCALHOST_TUNNEL_URL';
|
|
32
|
-
const DEFAULT_TIMEOUT = 5000;
|
|
33
|
-
|
|
34
|
-
module.exports = {
|
|
35
|
-
getHomeDir,
|
|
36
|
-
HOME_DIR,
|
|
37
|
-
PLEXOR_DIR,
|
|
38
|
-
CONFIG_PATH,
|
|
39
|
-
SESSION_PATH,
|
|
40
|
-
CACHE_PATH,
|
|
41
|
-
SESSION_TIMEOUT_MS,
|
|
42
|
-
DEFAULT_API_URL,
|
|
43
|
-
DEFAULT_TIMEOUT
|
|
44
|
-
};
|
package/lib/index.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor Plugin Library
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const ConfigManager = require('./config');
|
|
6
|
-
const SessionManager = require('./session');
|
|
7
|
-
const LocalCache = require('./cache');
|
|
8
|
-
const Logger = require('./logger');
|
|
9
|
-
const PlexorClient = require('./plexor-client');
|
|
10
|
-
const constants = require('./constants');
|
|
11
|
-
|
|
12
|
-
module.exports = {
|
|
13
|
-
ConfigManager,
|
|
14
|
-
SessionManager,
|
|
15
|
-
LocalCache,
|
|
16
|
-
Logger,
|
|
17
|
-
PlexorClient,
|
|
18
|
-
...constants
|
|
19
|
-
};
|
package/lib/logger.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor Logger
|
|
3
|
-
*
|
|
4
|
-
* Simple logger that outputs to stderr to avoid interfering with stdout JSON.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
class Logger {
|
|
8
|
-
constructor(component = 'plexor') {
|
|
9
|
-
this.component = component;
|
|
10
|
-
this.debug_enabled = process.env.PLEXOR_DEBUG === '1' || process.env.PLEXOR_DEBUG === 'true';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
debug(msg, data = null) {
|
|
14
|
-
if (this.debug_enabled) {
|
|
15
|
-
const output = data ? `[DEBUG][${this.component}] ${msg} ${JSON.stringify(data)}` : `[DEBUG][${this.component}] ${msg}`;
|
|
16
|
-
console.error(output);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
info(msg, data = null) {
|
|
21
|
-
const output = data ? `${msg} ${JSON.stringify(data)}` : msg;
|
|
22
|
-
console.error(output);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
warn(msg, data = null) {
|
|
26
|
-
const output = data ? `[WARN][${this.component}] ${msg} ${JSON.stringify(data)}` : `[WARN][${this.component}] ${msg}`;
|
|
27
|
-
console.error(output);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
error(msg, data = null) {
|
|
31
|
-
const output = data ? `[ERROR][${this.component}] ${msg} ${JSON.stringify(data)}` : `[ERROR][${this.component}] ${msg}`;
|
|
32
|
-
console.error(output);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
module.exports = Logger;
|
package/lib/plexor-client.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor API Client
|
|
3
|
-
*
|
|
4
|
-
* Handles communication with the Plexor optimization API.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const https = require('https');
|
|
8
|
-
const http = require('http');
|
|
9
|
-
const { URL } = require('url');
|
|
10
|
-
const { DEFAULT_API_URL, DEFAULT_TIMEOUT } = require('./constants');
|
|
11
|
-
|
|
12
|
-
class PlexorClient {
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
this.apiKey = options.apiKey || '';
|
|
15
|
-
this.baseUrl = options.baseUrl || DEFAULT_API_URL;
|
|
16
|
-
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async request(method, path, body = null) {
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
const url = new URL(path, this.baseUrl);
|
|
22
|
-
const isHttps = url.protocol === 'https:';
|
|
23
|
-
const lib = isHttps ? https : http;
|
|
24
|
-
|
|
25
|
-
const options = {
|
|
26
|
-
hostname: url.hostname,
|
|
27
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
28
|
-
path: url.pathname + url.search,
|
|
29
|
-
method: method,
|
|
30
|
-
headers: {
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
'X-API-Key': this.apiKey,
|
|
33
|
-
'X-Plexor-Key': this.apiKey,
|
|
34
|
-
'User-Agent': 'plexor-claude-code-plugin/0.1.0-beta.22'
|
|
35
|
-
},
|
|
36
|
-
timeout: this.timeout
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const req = lib.request(options, (res) => {
|
|
40
|
-
let data = '';
|
|
41
|
-
res.on('data', (chunk) => data += chunk);
|
|
42
|
-
res.on('end', () => {
|
|
43
|
-
try {
|
|
44
|
-
const json = JSON.parse(data);
|
|
45
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
46
|
-
resolve(json);
|
|
47
|
-
} else {
|
|
48
|
-
reject(new Error(json.message || `HTTP ${res.statusCode}`));
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
reject(new Error(`Invalid JSON response: ${data.substring(0, 100)}`));
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
req.on('error', reject);
|
|
57
|
-
req.on('timeout', () => {
|
|
58
|
-
req.destroy();
|
|
59
|
-
reject(new Error('Request timeout'));
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
if (body) {
|
|
63
|
-
req.write(JSON.stringify(body));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
req.end();
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async optimize(params) {
|
|
71
|
-
try {
|
|
72
|
-
const result = await this.request('POST', '/v1/optimize', {
|
|
73
|
-
messages: params.messages,
|
|
74
|
-
model: params.model,
|
|
75
|
-
max_tokens: params.max_tokens,
|
|
76
|
-
task_hint: params.task_hint,
|
|
77
|
-
context: params.context
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
request_id: result.request_id || `req_${Date.now()}`,
|
|
82
|
-
original_tokens: result.original_tokens || 0,
|
|
83
|
-
optimized_tokens: result.optimized_tokens || 0,
|
|
84
|
-
tokens_saved: result.tokens_saved || 0,
|
|
85
|
-
optimized_messages: result.optimized_messages || params.messages,
|
|
86
|
-
recommended_provider: result.recommended_provider || 'anthropic',
|
|
87
|
-
recommended_model: result.recommended_model || params.model,
|
|
88
|
-
estimated_cost: result.estimated_cost || 0,
|
|
89
|
-
baseline_cost: result.baseline_cost || 0
|
|
90
|
-
};
|
|
91
|
-
} catch (error) {
|
|
92
|
-
// Return passthrough on error
|
|
93
|
-
const tokens = Math.round(JSON.stringify(params.messages).length / 4);
|
|
94
|
-
return {
|
|
95
|
-
request_id: `req_${Date.now()}`,
|
|
96
|
-
original_tokens: tokens,
|
|
97
|
-
optimized_tokens: tokens,
|
|
98
|
-
tokens_saved: 0,
|
|
99
|
-
optimized_messages: params.messages,
|
|
100
|
-
recommended_provider: 'anthropic',
|
|
101
|
-
recommended_model: params.model,
|
|
102
|
-
estimated_cost: 0,
|
|
103
|
-
baseline_cost: 0,
|
|
104
|
-
error: error.message
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async getUser() {
|
|
110
|
-
return this.request('GET', '/v1/user');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async getStats() {
|
|
114
|
-
return this.request('GET', '/v1/stats');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async trackResponse(requestId, metrics) {
|
|
118
|
-
return this.request('POST', `/v1/track/${requestId}`, metrics);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
module.exports = PlexorClient;
|
package/lib/server-sync.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor Server Sync Module
|
|
3
|
-
*
|
|
4
|
-
* Issue #701: Syncs local plugin session state to the Plexor server.
|
|
5
|
-
* This ensures accurate token tracking even for passthrough requests.
|
|
6
|
-
*
|
|
7
|
-
* Phase 2 of the hypervisor architecture implementation.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const https = require('https');
|
|
11
|
-
const http = require('http');
|
|
12
|
-
|
|
13
|
-
class ServerSync {
|
|
14
|
-
constructor(options = {}) {
|
|
15
|
-
this.apiKey = options.apiKey;
|
|
16
|
-
this.baseUrl = options.baseUrl || 'https://api.plexor.dev';
|
|
17
|
-
this.timeout = options.timeout || 5000;
|
|
18
|
-
this.enabled = options.enabled !== false;
|
|
19
|
-
this.lastSyncTime = null;
|
|
20
|
-
this.syncInterval = options.syncInterval || 60000; // 1 minute default
|
|
21
|
-
this.pendingSync = null;
|
|
22
|
-
this.serverSessionId = null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Sync local session stats to the server.
|
|
27
|
-
* Issue #701: Ensures server has accurate metrics even for passthroughs.
|
|
28
|
-
*
|
|
29
|
-
* @param {Object} localSession - Local session stats from SessionManager
|
|
30
|
-
* @returns {Promise<Object>} Server response with synced metrics
|
|
31
|
-
*/
|
|
32
|
-
async syncSession(localSession) {
|
|
33
|
-
if (!this.enabled || !this.apiKey) {
|
|
34
|
-
return { synced: false, message: 'Server sync disabled or no API key' };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const syncData = {
|
|
39
|
-
session_id: this.serverSessionId,
|
|
40
|
-
requests: localSession.requests || 0,
|
|
41
|
-
optimizations: localSession.optimizations || 0,
|
|
42
|
-
cache_hits: localSession.cache_hits || 0,
|
|
43
|
-
passthroughs: localSession.passthroughs || 0,
|
|
44
|
-
original_tokens: localSession.original_tokens || 0,
|
|
45
|
-
optimized_tokens: localSession.optimized_tokens || 0,
|
|
46
|
-
tokens_saved: localSession.tokens_saved || 0,
|
|
47
|
-
output_tokens: localSession.output_tokens || 0,
|
|
48
|
-
baseline_cost: localSession.baseline_cost || 0,
|
|
49
|
-
actual_cost: localSession.actual_cost || 0,
|
|
50
|
-
cost_saved: localSession.cost_saved || 0
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const response = await this._postToServer('/v1/plugin/session/sync', syncData);
|
|
54
|
-
|
|
55
|
-
if (response.synced) {
|
|
56
|
-
this.serverSessionId = response.server_session_id;
|
|
57
|
-
this.lastSyncTime = Date.now();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return response;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
// Don't throw - server sync failures shouldn't break the plugin
|
|
63
|
-
return {
|
|
64
|
-
synced: false,
|
|
65
|
-
message: `Server sync failed: ${error.message}`,
|
|
66
|
-
error: error.message
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get server-side session state.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} sessionId - Session ID to query
|
|
75
|
-
* @returns {Promise<Object>} Server session state
|
|
76
|
-
*/
|
|
77
|
-
async getServerSession(sessionId) {
|
|
78
|
-
if (!this.enabled || !this.apiKey) {
|
|
79
|
-
return { found: false, message: 'Server sync disabled or no API key' };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
return await this._getFromServer(`/v1/plugin/session/${sessionId}`);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
return {
|
|
86
|
-
found: false,
|
|
87
|
-
message: `Failed to get server session: ${error.message}`
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Schedule periodic sync (debounced).
|
|
94
|
-
*
|
|
95
|
-
* @param {Object} localSession - Local session stats
|
|
96
|
-
*/
|
|
97
|
-
scheduleSync(localSession) {
|
|
98
|
-
// Debounce: don't sync more than once per interval
|
|
99
|
-
if (this.pendingSync) {
|
|
100
|
-
clearTimeout(this.pendingSync);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
this.pendingSync = setTimeout(async () => {
|
|
104
|
-
await this.syncSession(localSession);
|
|
105
|
-
this.pendingSync = null;
|
|
106
|
-
}, 1000); // Sync 1 second after last request
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* POST data to the Plexor server.
|
|
111
|
-
*
|
|
112
|
-
* @param {string} path - API path
|
|
113
|
-
* @param {Object} data - Data to send
|
|
114
|
-
* @returns {Promise<Object>} Parsed JSON response
|
|
115
|
-
*/
|
|
116
|
-
_postToServer(path, data) {
|
|
117
|
-
return new Promise((resolve, reject) => {
|
|
118
|
-
const url = new URL(path, this.baseUrl);
|
|
119
|
-
const isHttps = url.protocol === 'https:';
|
|
120
|
-
const client = isHttps ? https : http;
|
|
121
|
-
|
|
122
|
-
const options = {
|
|
123
|
-
hostname: url.hostname,
|
|
124
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
125
|
-
path: url.pathname,
|
|
126
|
-
method: 'POST',
|
|
127
|
-
headers: {
|
|
128
|
-
'Content-Type': 'application/json',
|
|
129
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
130
|
-
'X-Plexor-Plugin-Version': '0.1.0-beta.5',
|
|
131
|
-
'X-Plexor-Issue': '701'
|
|
132
|
-
},
|
|
133
|
-
timeout: this.timeout
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const req = client.request(options, (res) => {
|
|
137
|
-
let body = '';
|
|
138
|
-
res.on('data', chunk => body += chunk);
|
|
139
|
-
res.on('end', () => {
|
|
140
|
-
try {
|
|
141
|
-
const parsed = JSON.parse(body);
|
|
142
|
-
resolve(parsed);
|
|
143
|
-
} catch (e) {
|
|
144
|
-
reject(new Error(`Invalid JSON response: ${body.substring(0, 100)}`));
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
req.on('error', reject);
|
|
150
|
-
req.on('timeout', () => {
|
|
151
|
-
req.destroy();
|
|
152
|
-
reject(new Error('Request timeout'));
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
req.write(JSON.stringify(data));
|
|
156
|
-
req.end();
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* GET data from the Plexor server.
|
|
162
|
-
*
|
|
163
|
-
* @param {string} path - API path
|
|
164
|
-
* @returns {Promise<Object>} Parsed JSON response
|
|
165
|
-
*/
|
|
166
|
-
_getFromServer(path) {
|
|
167
|
-
return new Promise((resolve, reject) => {
|
|
168
|
-
const url = new URL(path, this.baseUrl);
|
|
169
|
-
const isHttps = url.protocol === 'https:';
|
|
170
|
-
const client = isHttps ? https : http;
|
|
171
|
-
|
|
172
|
-
const options = {
|
|
173
|
-
hostname: url.hostname,
|
|
174
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
175
|
-
path: url.pathname,
|
|
176
|
-
method: 'GET',
|
|
177
|
-
headers: {
|
|
178
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
179
|
-
'X-Plexor-Plugin-Version': '0.1.0-beta.5'
|
|
180
|
-
},
|
|
181
|
-
timeout: this.timeout
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const req = client.request(options, (res) => {
|
|
185
|
-
let body = '';
|
|
186
|
-
res.on('data', chunk => body += chunk);
|
|
187
|
-
res.on('end', () => {
|
|
188
|
-
try {
|
|
189
|
-
resolve(JSON.parse(body));
|
|
190
|
-
} catch (e) {
|
|
191
|
-
reject(new Error(`Invalid JSON response: ${body.substring(0, 100)}`));
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
req.on('error', reject);
|
|
197
|
-
req.on('timeout', () => {
|
|
198
|
-
req.destroy();
|
|
199
|
-
reject(new Error('Request timeout'));
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
req.end();
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Check if sync is needed based on time elapsed.
|
|
208
|
-
*/
|
|
209
|
-
needsSync() {
|
|
210
|
-
if (!this.lastSyncTime) return true;
|
|
211
|
-
return Date.now() - this.lastSyncTime > this.syncInterval;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Singleton instance
|
|
216
|
-
let _serverSync = null;
|
|
217
|
-
|
|
218
|
-
function getServerSync(options = {}) {
|
|
219
|
-
if (!_serverSync) {
|
|
220
|
-
_serverSync = new ServerSync(options);
|
|
221
|
-
} else if (options.apiKey && options.apiKey !== _serverSync.apiKey) {
|
|
222
|
-
// Update API key if changed
|
|
223
|
-
_serverSync.apiKey = options.apiKey;
|
|
224
|
-
_serverSync.baseUrl = options.baseUrl || _serverSync.baseUrl;
|
|
225
|
-
}
|
|
226
|
-
return _serverSync;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function resetServerSync() {
|
|
230
|
-
_serverSync = null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
module.exports = {
|
|
234
|
-
ServerSync,
|
|
235
|
-
getServerSync,
|
|
236
|
-
resetServerSync
|
|
237
|
-
};
|
package/lib/session.js
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor Session Manager
|
|
3
|
-
*
|
|
4
|
-
* Tracks session statistics for the current Claude Code session.
|
|
5
|
-
* Session expires after 30 minutes of inactivity.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const { SESSION_PATH, PLEXOR_DIR, SESSION_TIMEOUT_MS } = require('./constants');
|
|
11
|
-
|
|
12
|
-
class SessionManager {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.sessionPath = SESSION_PATH;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
load() {
|
|
18
|
-
try {
|
|
19
|
-
const data = fs.readFileSync(this.sessionPath, 'utf8');
|
|
20
|
-
const session = JSON.parse(data);
|
|
21
|
-
|
|
22
|
-
// Check if session has expired (30 min inactivity)
|
|
23
|
-
if (Date.now() - session.last_activity > SESSION_TIMEOUT_MS) {
|
|
24
|
-
return this.createNew();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return session;
|
|
28
|
-
} catch {
|
|
29
|
-
return this.createNew();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
createNew() {
|
|
34
|
-
const session = {
|
|
35
|
-
session_id: `session_${Date.now()}`,
|
|
36
|
-
started_at: new Date().toISOString(),
|
|
37
|
-
last_activity: Date.now(),
|
|
38
|
-
requests: 0,
|
|
39
|
-
optimizations: 0,
|
|
40
|
-
cache_hits: 0,
|
|
41
|
-
passthroughs: 0, // Issue #701: Track passthroughs
|
|
42
|
-
original_tokens: 0,
|
|
43
|
-
optimized_tokens: 0,
|
|
44
|
-
tokens_saved: 0,
|
|
45
|
-
output_tokens: 0, // Issue #701: Track output tokens
|
|
46
|
-
baseline_cost: 0,
|
|
47
|
-
actual_cost: 0,
|
|
48
|
-
cost_saved: 0
|
|
49
|
-
};
|
|
50
|
-
this.save(session);
|
|
51
|
-
return session;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
save(session) {
|
|
55
|
-
try {
|
|
56
|
-
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
57
|
-
fs.mkdirSync(PLEXOR_DIR, { recursive: true });
|
|
58
|
-
}
|
|
59
|
-
session.last_activity = Date.now();
|
|
60
|
-
fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2));
|
|
61
|
-
return true;
|
|
62
|
-
} catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
recordRequest(stats = {}) {
|
|
68
|
-
const session = this.load();
|
|
69
|
-
session.requests++;
|
|
70
|
-
|
|
71
|
-
if (stats.optimized) {
|
|
72
|
-
session.optimizations++;
|
|
73
|
-
session.original_tokens += stats.original_tokens || 0;
|
|
74
|
-
session.optimized_tokens += stats.optimized_tokens || 0;
|
|
75
|
-
session.tokens_saved += stats.tokens_saved || 0;
|
|
76
|
-
session.output_tokens = (session.output_tokens || 0) + (stats.output_tokens || 0); // Issue #701
|
|
77
|
-
session.baseline_cost += stats.baseline_cost || 0;
|
|
78
|
-
session.actual_cost += stats.actual_cost || 0;
|
|
79
|
-
session.cost_saved += stats.cost_saved || 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (stats.cache_hit) {
|
|
83
|
-
session.cache_hits++;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Issue #701: Track passthroughs and their output tokens
|
|
87
|
-
if (stats.passthrough) {
|
|
88
|
-
session.passthroughs = (session.passthroughs || 0) + 1;
|
|
89
|
-
session.output_tokens = (session.output_tokens || 0) + (stats.output_tokens || 0);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
this.save(session);
|
|
93
|
-
return session;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
recordOptimization(result) {
|
|
97
|
-
return this.recordRequest({
|
|
98
|
-
optimized: true,
|
|
99
|
-
original_tokens: result.original_tokens || 0,
|
|
100
|
-
optimized_tokens: result.optimized_tokens || 0,
|
|
101
|
-
tokens_saved: result.tokens_saved || 0,
|
|
102
|
-
output_tokens: result.output_tokens || 0, // Issue #701: Track output tokens
|
|
103
|
-
baseline_cost: result.baseline_cost || 0,
|
|
104
|
-
actual_cost: result.estimated_cost || result.actual_cost || 0,
|
|
105
|
-
cost_saved: (result.baseline_cost || 0) - (result.estimated_cost || result.actual_cost || 0)
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
recordCacheHit() {
|
|
110
|
-
return this.recordRequest({ cache_hit: true });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
recordPassthrough(outputTokens = 0) {
|
|
114
|
-
// Issue #701: Track passthroughs and their output tokens
|
|
115
|
-
return this.recordRequest({ passthrough: true, output_tokens: outputTokens });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
getStats() {
|
|
119
|
-
const session = this.load();
|
|
120
|
-
const duration = Date.now() - new Date(session.started_at).getTime();
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
...session,
|
|
124
|
-
duration_ms: duration,
|
|
125
|
-
duration_formatted: this.formatDuration(duration),
|
|
126
|
-
tokens_saved_percent: session.original_tokens > 0
|
|
127
|
-
? ((session.tokens_saved / session.original_tokens) * 100).toFixed(1)
|
|
128
|
-
: '0.0',
|
|
129
|
-
cost_saved_percent: session.baseline_cost > 0
|
|
130
|
-
? ((session.cost_saved / session.baseline_cost) * 100).toFixed(1)
|
|
131
|
-
: '0.0'
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
formatDuration(ms) {
|
|
136
|
-
const minutes = Math.floor(ms / 60000);
|
|
137
|
-
if (minutes < 60) {
|
|
138
|
-
return `${minutes}m`;
|
|
139
|
-
}
|
|
140
|
-
const hours = Math.floor(minutes / 60);
|
|
141
|
-
const remainingMinutes = minutes % 60;
|
|
142
|
-
return `${hours}h ${remainingMinutes}m`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
isExpired() {
|
|
146
|
-
try {
|
|
147
|
-
const data = fs.readFileSync(this.sessionPath, 'utf8');
|
|
148
|
-
const session = JSON.parse(data);
|
|
149
|
-
return Date.now() - session.last_activity > SESSION_TIMEOUT_MS;
|
|
150
|
-
} catch {
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
module.exports = SessionManager;
|
package/lib/settings-manager.js
DELETED
|
@@ -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 = 'LOCALHOST_TUNNEL_URL/gateway/anthropic';
|
|
24
|
-
const PLEXOR_PROD_URL = 'https://api.plexor.dev/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
|
-
};
|