@plexor-dev/claude-code-plugin 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/commands/plexor-config.js +170 -0
- package/commands/plexor-config.md +31 -30
- package/commands/plexor-enabled.js +91 -0
- package/commands/plexor-enabled.md +48 -28
- package/commands/plexor-login.js +169 -0
- package/commands/plexor-login.md +51 -52
- package/commands/plexor-logout.js +92 -0
- package/commands/plexor-logout.md +21 -27
- package/commands/plexor-mode.js +107 -0
- package/commands/plexor-mode.md +41 -17
- package/commands/plexor-provider.js +110 -0
- package/commands/plexor-provider.md +42 -18
- package/commands/plexor-settings.js +155 -0
- package/commands/plexor-settings.md +39 -72
- package/commands/plexor-setup.md +134 -0
- package/commands/plexor-status.js +213 -0
- package/commands/plexor-status.md +12 -37
- package/hooks/intercept.js +634 -0
- package/hooks/track-response.js +376 -0
- package/lib/cache.js +107 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +16 -31
- package/lib/index.js +19 -0
- package/lib/logger.js +36 -0
- package/lib/plexor-client.js +122 -0
- package/lib/server-sync.js +237 -0
- package/lib/session.js +156 -0
- package/package.json +12 -1
- package/scripts/plexor-cli.sh +48 -0
- package/scripts/postinstall.js +53 -7
|
@@ -0,0 +1,122 @@
|
|
|
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.21'
|
|
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;
|
|
@@ -0,0 +1,237 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
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/package.json
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plexor-dev/claude-code-plugin",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.21",
|
|
4
4
|
"description": "LLM cost optimization plugin for Claude Code - Save up to 90% on AI costs",
|
|
5
5
|
"main": "lib/constants.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"plexor-status": "./commands/plexor-status.js",
|
|
8
|
+
"plexor-mode": "./commands/plexor-mode.js",
|
|
9
|
+
"plexor-enabled": "./commands/plexor-enabled.js",
|
|
10
|
+
"plexor-provider": "./commands/plexor-provider.js",
|
|
11
|
+
"plexor-login": "./commands/plexor-login.js",
|
|
12
|
+
"plexor-logout": "./commands/plexor-logout.js",
|
|
13
|
+
"plexor-settings": "./commands/plexor-settings.js",
|
|
14
|
+
"plexor-config": "./commands/plexor-config.js"
|
|
15
|
+
},
|
|
6
16
|
"scripts": {
|
|
7
17
|
"postinstall": "node scripts/postinstall.js",
|
|
8
18
|
"preuninstall": "node scripts/uninstall.js",
|
|
@@ -10,6 +20,7 @@
|
|
|
10
20
|
},
|
|
11
21
|
"files": [
|
|
12
22
|
"commands/",
|
|
23
|
+
"hooks/",
|
|
13
24
|
"scripts/",
|
|
14
25
|
"lib/",
|
|
15
26
|
"README.md",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Plexor CLI - Claude Code with intelligent optimization
|
|
3
|
+
# This wrapper auto-enables hypervisor mode for direct gateway routing
|
|
4
|
+
|
|
5
|
+
# Colors
|
|
6
|
+
CYAN='\033[0;36m'
|
|
7
|
+
GREEN='\033[0;32m'
|
|
8
|
+
YELLOW='\033[0;33m'
|
|
9
|
+
DIM='\033[2m'
|
|
10
|
+
NC='\033[0m' # No Color
|
|
11
|
+
|
|
12
|
+
CONFIG_FILE="$HOME/.plexor/config.json"
|
|
13
|
+
|
|
14
|
+
# Auto-configure ANTHROPIC_BASE_URL if Plexor is enabled
|
|
15
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
16
|
+
ENABLED=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('settings',{}).get('enabled', False))" 2>/dev/null)
|
|
17
|
+
API_URL=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('settings',{}).get('apiUrl', ''))" 2>/dev/null)
|
|
18
|
+
API_KEY=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('auth',{}).get('api_key', ''))" 2>/dev/null)
|
|
19
|
+
MODE=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('settings',{}).get('mode', 'balanced'))" 2>/dev/null)
|
|
20
|
+
|
|
21
|
+
if [ "$ENABLED" = "True" ] && [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
|
|
22
|
+
# Set ANTHROPIC_BASE_URL to Plexor gateway (hypervisor mode)
|
|
23
|
+
export ANTHROPIC_BASE_URL="${API_URL}/gateway/anthropic/v1"
|
|
24
|
+
export ANTHROPIC_API_KEY="$API_KEY"
|
|
25
|
+
|
|
26
|
+
# Show Plexor branding
|
|
27
|
+
echo -e "${CYAN}"
|
|
28
|
+
cat << 'EOF'
|
|
29
|
+
____ __
|
|
30
|
+
/ __ \/ /__ _ ______ _____
|
|
31
|
+
/ /_/ / / _ \| |/_/ __ \/ ___/
|
|
32
|
+
/ ____/ / __/> </ /_/ / /
|
|
33
|
+
/_/ /_/\___/_/|_|\____/_/
|
|
34
|
+
|
|
35
|
+
EOF
|
|
36
|
+
echo -e "${NC}"
|
|
37
|
+
echo -e "${GREEN}●${NC} Hypervisor Mode: ${GREEN}Active${NC}"
|
|
38
|
+
echo -e " Gateway: ${DIM}${ANTHROPIC_BASE_URL}${NC}"
|
|
39
|
+
echo -e " Mode: ${MODE}"
|
|
40
|
+
echo ""
|
|
41
|
+
else
|
|
42
|
+
echo -e "${YELLOW}○${NC} Plexor: ${DIM}Disabled${NC} (run /plexor-login to enable)"
|
|
43
|
+
echo ""
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Launch claude with all arguments passed through
|
|
48
|
+
exec claude "$@"
|
package/scripts/postinstall.js
CHANGED
|
@@ -11,23 +11,49 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const os = require('os');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Get the correct home directory, accounting for sudo.
|
|
16
|
+
* When running with sudo, os.homedir() returns /root, but we want
|
|
17
|
+
* the actual user's home directory.
|
|
18
|
+
*/
|
|
19
|
+
function getHomeDir() {
|
|
20
|
+
// Check if running with sudo - SUDO_USER contains the original username
|
|
21
|
+
if (process.env.SUDO_USER) {
|
|
22
|
+
// On Linux/Mac, home directories are typically /home/<user> or /Users/<user>
|
|
23
|
+
const platform = os.platform();
|
|
24
|
+
if (platform === 'darwin') {
|
|
25
|
+
return path.join('/Users', process.env.SUDO_USER);
|
|
26
|
+
} else if (platform === 'linux') {
|
|
27
|
+
return path.join('/home', process.env.SUDO_USER);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return os.homedir();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const HOME_DIR = getHomeDir();
|
|
14
34
|
const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
|
|
15
|
-
const CLAUDE_COMMANDS_DIR = path.join(
|
|
16
|
-
const
|
|
35
|
+
const CLAUDE_COMMANDS_DIR = path.join(HOME_DIR, '.claude', 'commands');
|
|
36
|
+
const PLEXOR_PLUGINS_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'commands');
|
|
37
|
+
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
17
38
|
|
|
18
39
|
function main() {
|
|
19
40
|
try {
|
|
20
41
|
// Create ~/.claude/commands/ if not exists
|
|
21
42
|
fs.mkdirSync(CLAUDE_COMMANDS_DIR, { recursive: true });
|
|
22
43
|
|
|
44
|
+
// Create ~/.claude/plugins/plexor/commands/ for JS executors
|
|
45
|
+
fs.mkdirSync(PLEXOR_PLUGINS_DIR, { recursive: true });
|
|
46
|
+
|
|
23
47
|
// Create ~/.plexor/ with secure permissions (owner only)
|
|
24
48
|
fs.mkdirSync(PLEXOR_CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
25
49
|
|
|
26
|
-
// Get list of command files
|
|
27
|
-
const
|
|
50
|
+
// Get list of command files (.md for Claude, .js for executors)
|
|
51
|
+
const mdFiles = fs.readdirSync(COMMANDS_SOURCE)
|
|
28
52
|
.filter(f => f.endsWith('.md'));
|
|
53
|
+
const jsFiles = fs.readdirSync(COMMANDS_SOURCE)
|
|
54
|
+
.filter(f => f.endsWith('.js'));
|
|
29
55
|
|
|
30
|
-
if (
|
|
56
|
+
if (mdFiles.length === 0) {
|
|
31
57
|
console.error('No command files found in package. Installation may be corrupt.');
|
|
32
58
|
process.exit(1);
|
|
33
59
|
}
|
|
@@ -35,7 +61,8 @@ function main() {
|
|
|
35
61
|
const installed = [];
|
|
36
62
|
const backed_up = [];
|
|
37
63
|
|
|
38
|
-
|
|
64
|
+
// Copy .md command files to ~/.claude/commands/
|
|
65
|
+
for (const file of mdFiles) {
|
|
39
66
|
const src = path.join(COMMANDS_SOURCE, file);
|
|
40
67
|
const dest = path.join(CLAUDE_COMMANDS_DIR, file);
|
|
41
68
|
|
|
@@ -55,6 +82,17 @@ function main() {
|
|
|
55
82
|
installed.push(file.replace('.md', ''));
|
|
56
83
|
}
|
|
57
84
|
|
|
85
|
+
// Copy .js executor files to ~/.claude/plugins/plexor/commands/
|
|
86
|
+
const jsInstalled = [];
|
|
87
|
+
for (const file of jsFiles) {
|
|
88
|
+
const src = path.join(COMMANDS_SOURCE, file);
|
|
89
|
+
const dest = path.join(PLEXOR_PLUGINS_DIR, file);
|
|
90
|
+
fs.copyFileSync(src, dest);
|
|
91
|
+
// Make executable
|
|
92
|
+
fs.chmodSync(dest, 0o755);
|
|
93
|
+
jsInstalled.push(file);
|
|
94
|
+
}
|
|
95
|
+
|
|
58
96
|
// Print success message
|
|
59
97
|
console.log('');
|
|
60
98
|
console.log(' ╔═══════════════════════════════════════════════════════════╗');
|
|
@@ -74,12 +112,20 @@ function main() {
|
|
|
74
112
|
backed_up.forEach(f => console.log(` ${f}`));
|
|
75
113
|
}
|
|
76
114
|
|
|
115
|
+
if (jsInstalled.length > 0) {
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(' Executors installed to ~/.claude/plugins/plexor/commands/:');
|
|
118
|
+
jsInstalled.forEach(f => console.log(` ${f}`));
|
|
119
|
+
}
|
|
120
|
+
|
|
77
121
|
console.log('');
|
|
78
122
|
console.log(' Next steps:');
|
|
79
123
|
console.log(' 1. Open Claude Code');
|
|
80
|
-
console.log(' 2. Run /plexor-
|
|
124
|
+
console.log(' 2. Run /plexor-setup to configure (handles MAX + API key users)');
|
|
81
125
|
console.log(' 3. Start saving on LLM costs!');
|
|
82
126
|
console.log('');
|
|
127
|
+
console.log(' Have Claude MAX? Just set: export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"');
|
|
128
|
+
console.log('');
|
|
83
129
|
console.log(' Documentation: https://plexor.dev/docs');
|
|
84
130
|
console.log('');
|
|
85
131
|
|