@plexor-dev/claude-code-plugin 0.1.0-beta.16 → 0.1.0-beta.19
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.md +2 -0
- package/commands/plexor-enabled.md +2 -0
- package/commands/plexor-login.md +2 -0
- package/commands/plexor-logout.md +2 -0
- package/commands/plexor-mode.md +2 -0
- package/commands/plexor-provider.md +2 -0
- package/commands/plexor-settings.md +2 -0
- package/commands/plexor-status.js +12 -2
- package/commands/plexor-status.md +9 -1
- package/hooks/intercept.js +80 -16
- package/hooks/track-response.js +261 -49
- package/lib/server-sync.js +237 -0
- package/lib/session.js +13 -2
- package/package.json +1 -1
- package/scripts/plexor-cli.sh +31 -24
- package/scripts/postinstall.js +50 -6
package/commands/plexor-login.md
CHANGED
package/commands/plexor-mode.md
CHANGED
|
@@ -92,8 +92,18 @@ async function main() {
|
|
|
92
92
|
const cacheEnabled = localCache ? 'Enabled' : 'Disabled';
|
|
93
93
|
const cacheRate = formatPct((summary.cache_hit_rate || 0) * 100);
|
|
94
94
|
|
|
95
|
-
// Build dashboard URL
|
|
96
|
-
|
|
95
|
+
// Build dashboard URL from API URL
|
|
96
|
+
// API: https://api.plexor.dev or https://staging.api.plexor.dev
|
|
97
|
+
// Dashboard: https://plexor.dev/dashboard or https://staging.plexor.dev/dashboard
|
|
98
|
+
let dashboardUrl = 'https://plexor.dev/dashboard';
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(apiUrl);
|
|
101
|
+
// Remove 'api.' prefix from hostname if present
|
|
102
|
+
const host = url.hostname.replace(/^api\./, '').replace(/\.api\./, '.');
|
|
103
|
+
dashboardUrl = `${url.protocol}//${host}/dashboard`;
|
|
104
|
+
} catch {
|
|
105
|
+
// If URL parsing fails, use default
|
|
106
|
+
}
|
|
97
107
|
|
|
98
108
|
// Output formatted status - each line is exactly 43 chars inner width
|
|
99
109
|
const line = (content) => ` │ ${content.padEnd(43)}│`;
|
|
@@ -10,4 +10,12 @@ Run this command to display Plexor statistics:
|
|
|
10
10
|
node ~/.claude/plugins/plexor/commands/plexor-status.js
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Use the Bash tool to execute this single command.
|
|
13
|
+
Use the Bash tool to execute this single command.
|
|
14
|
+
|
|
15
|
+
**IMPORTANT**: After running this command and displaying the output, STOP. Do not:
|
|
16
|
+
- Read any files
|
|
17
|
+
- Explore the codebase
|
|
18
|
+
- Run additional commands
|
|
19
|
+
- Ask follow-up questions
|
|
20
|
+
|
|
21
|
+
The command output is the complete response. Simply show the output and wait for the user's next input.
|
package/hooks/intercept.js
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Plexor Interception Hook
|
|
4
|
+
* Plexor Interception Hook - Phase 3 Hypervisor Architecture
|
|
5
5
|
*
|
|
6
|
-
* This script intercepts Claude Code
|
|
7
|
-
*
|
|
6
|
+
* This script intercepts Claude Code requests before they reach the LLM.
|
|
7
|
+
*
|
|
8
|
+
* In HYPERVISOR MODE (ANTHROPIC_BASE_URL points to Plexor):
|
|
9
|
+
* - All requests pass through unchanged
|
|
10
|
+
* - Server handles all optimization, routing, and quality control
|
|
11
|
+
* - Plugin just adds session tracking metadata
|
|
12
|
+
* - This is the recommended setup for best user experience
|
|
13
|
+
*
|
|
14
|
+
* In PLUGIN MODE (direct to Anthropic):
|
|
15
|
+
* - Plugin does local optimization (legacy behavior)
|
|
16
|
+
* - Less reliable due to client-side state management
|
|
8
17
|
*
|
|
9
18
|
* Input: JSON object with messages, model, max_tokens, etc.
|
|
10
|
-
* Output:
|
|
19
|
+
* Output: Passthrough (hypervisor mode) or optimized messages (plugin mode)
|
|
11
20
|
*/
|
|
12
21
|
|
|
13
22
|
const fs = require('fs');
|
|
14
23
|
const path = require('path');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a unique request ID for tracking.
|
|
28
|
+
* Format: pass_<timestamp>_<random> for passthrough requests
|
|
29
|
+
* This ensures every request can be tracked, even passthroughs.
|
|
30
|
+
* Issue #701: Missing request_id caused response tracking to fail.
|
|
31
|
+
*/
|
|
32
|
+
function generateRequestId(prefix = 'pass') {
|
|
33
|
+
const timestamp = Date.now().toString(36);
|
|
34
|
+
const random = crypto.randomBytes(4).toString('hex');
|
|
35
|
+
return `${prefix}_${timestamp}_${random}`;
|
|
36
|
+
}
|
|
15
37
|
|
|
16
38
|
// Try to load lib modules, fall back to inline implementations
|
|
17
39
|
let ConfigManager, SessionManager, LocalCache, Logger, PlexorClient;
|
|
@@ -157,12 +179,27 @@ async function main() {
|
|
|
157
179
|
input = await readStdin();
|
|
158
180
|
request = JSON.parse(input);
|
|
159
181
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
182
|
+
// Phase 3 Hypervisor Mode Detection
|
|
183
|
+
// When ANTHROPIC_BASE_URL points to Plexor, all intelligence is server-side
|
|
184
|
+
// The plugin just passes through - server handles optimization, routing, quality
|
|
162
185
|
const baseUrl = process.env.ANTHROPIC_BASE_URL || '';
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
const isHypervisorMode = baseUrl.includes('plexor') || baseUrl.includes('staging.api');
|
|
187
|
+
|
|
188
|
+
if (isHypervisorMode) {
|
|
189
|
+
// HYPERVISOR MODE: Server handles everything
|
|
190
|
+
// Just pass through with minimal metadata for session tracking
|
|
191
|
+
logger.debug('[Plexor] Hypervisor mode active - server handles all optimization');
|
|
192
|
+
|
|
193
|
+
// Add session tracking metadata (server will use this for analytics)
|
|
194
|
+
return output({
|
|
195
|
+
...request,
|
|
196
|
+
_plexor_client: {
|
|
197
|
+
mode: 'hypervisor',
|
|
198
|
+
plugin_version: '0.1.0-beta.18',
|
|
199
|
+
cwd: process.cwd(),
|
|
200
|
+
timestamp: Date.now(),
|
|
201
|
+
}
|
|
202
|
+
});
|
|
166
203
|
}
|
|
167
204
|
|
|
168
205
|
// CRITICAL: Check for slash commands FIRST (before agentic check)
|
|
@@ -175,6 +212,7 @@ async function main() {
|
|
|
175
212
|
...request,
|
|
176
213
|
plexor_cwd: process.cwd(),
|
|
177
214
|
_plexor: {
|
|
215
|
+
request_id: generateRequestId('slash'), // Issue #701: Add request_id for tracking
|
|
178
216
|
source: 'passthrough_slash_command',
|
|
179
217
|
reason: 'slash_command_detected',
|
|
180
218
|
cwd: process.cwd(),
|
|
@@ -192,6 +230,7 @@ async function main() {
|
|
|
192
230
|
...request,
|
|
193
231
|
plexor_cwd: process.cwd(),
|
|
194
232
|
_plexor: {
|
|
233
|
+
request_id: generateRequestId('cli'), // Issue #701: Add request_id for tracking
|
|
195
234
|
source: 'passthrough_cli',
|
|
196
235
|
reason: 'cli_tool_execution_detected',
|
|
197
236
|
cwd: process.cwd(),
|
|
@@ -209,6 +248,7 @@ async function main() {
|
|
|
209
248
|
...request,
|
|
210
249
|
plexor_cwd: process.cwd(),
|
|
211
250
|
_plexor: {
|
|
251
|
+
request_id: generateRequestId('agent'), // Issue #701: Add request_id for tracking
|
|
212
252
|
source: 'passthrough_agentic',
|
|
213
253
|
reason: 'tool_use_detected',
|
|
214
254
|
cwd: process.cwd(),
|
|
@@ -221,12 +261,30 @@ async function main() {
|
|
|
221
261
|
|
|
222
262
|
if (!settings.enabled) {
|
|
223
263
|
logger.debug('Plexor disabled, passing through');
|
|
224
|
-
|
|
264
|
+
session.recordPassthrough();
|
|
265
|
+
return output({
|
|
266
|
+
...request,
|
|
267
|
+
_plexor: {
|
|
268
|
+
request_id: generateRequestId('disabled'), // Issue #701: Add request_id for tracking
|
|
269
|
+
source: 'passthrough_disabled',
|
|
270
|
+
reason: 'plexor_disabled',
|
|
271
|
+
latency_ms: Date.now() - startTime
|
|
272
|
+
}
|
|
273
|
+
});
|
|
225
274
|
}
|
|
226
275
|
|
|
227
276
|
if (!settings.apiKey) {
|
|
228
277
|
logger.info('Not authenticated. Run /plexor-login to enable optimization.');
|
|
229
|
-
|
|
278
|
+
session.recordPassthrough();
|
|
279
|
+
return output({
|
|
280
|
+
...request,
|
|
281
|
+
_plexor: {
|
|
282
|
+
request_id: generateRequestId('noauth'), // Issue #701: Add request_id for tracking
|
|
283
|
+
source: 'passthrough_no_auth',
|
|
284
|
+
reason: 'not_authenticated',
|
|
285
|
+
latency_ms: Date.now() - startTime
|
|
286
|
+
}
|
|
287
|
+
});
|
|
230
288
|
}
|
|
231
289
|
|
|
232
290
|
const client = new PlexorClient({
|
|
@@ -248,6 +306,7 @@ async function main() {
|
|
|
248
306
|
return output({
|
|
249
307
|
...request,
|
|
250
308
|
_plexor: {
|
|
309
|
+
request_id: generateRequestId('cache'), // Issue #701: Add request_id for tracking
|
|
251
310
|
source: 'local_cache',
|
|
252
311
|
latency_ms: Date.now() - startTime
|
|
253
312
|
}
|
|
@@ -312,11 +371,15 @@ async function main() {
|
|
|
312
371
|
logger.error(`[Plexor] Error: ${error.message}`);
|
|
313
372
|
logger.debug(error.stack);
|
|
314
373
|
|
|
374
|
+
const errorRequestId = generateRequestId('error'); // Issue #701: Add request_id for tracking
|
|
375
|
+
|
|
315
376
|
// Use already-parsed request if available, otherwise pass through raw
|
|
316
377
|
if (request) {
|
|
378
|
+
session.recordPassthrough();
|
|
317
379
|
return output({
|
|
318
380
|
...request,
|
|
319
381
|
_plexor: {
|
|
382
|
+
request_id: errorRequestId,
|
|
320
383
|
error: error.message,
|
|
321
384
|
source: 'passthrough_error'
|
|
322
385
|
}
|
|
@@ -325,9 +388,11 @@ async function main() {
|
|
|
325
388
|
// Try to parse the input we already read
|
|
326
389
|
try {
|
|
327
390
|
const req = JSON.parse(input);
|
|
391
|
+
session.recordPassthrough();
|
|
328
392
|
return output({
|
|
329
393
|
...req,
|
|
330
394
|
_plexor: {
|
|
395
|
+
request_id: errorRequestId,
|
|
331
396
|
error: error.message,
|
|
332
397
|
source: 'passthrough_error'
|
|
333
398
|
}
|
|
@@ -476,11 +541,10 @@ function isAgenticRequest(request) {
|
|
|
476
541
|
}
|
|
477
542
|
}
|
|
478
543
|
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
544
|
+
// NOTE: Removed multi-turn check (Issue #701)
|
|
545
|
+
// Multi-turn conversations WITHOUT tool use are NOT agentic
|
|
546
|
+
// The old check caused normal conversations to be marked as agentic
|
|
547
|
+
// after just 2 assistant messages, leading to infinite loops
|
|
484
548
|
|
|
485
549
|
return false;
|
|
486
550
|
}
|
package/hooks/track-response.js
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* This script runs after the LLM response is received.
|
|
7
7
|
* It tracks response metrics for analytics and updates session stats.
|
|
8
8
|
*
|
|
9
|
+
* Issue #701 Fix: Now tracks ALL responses including passthroughs.
|
|
10
|
+
* Previously, responses without request_id would exit early without tracking,
|
|
11
|
+
* leading to "0 tokens" display and potential context issues.
|
|
12
|
+
*
|
|
9
13
|
* Input: JSON object with response content, tokens used, etc.
|
|
10
14
|
* Output: Passthrough (no modifications)
|
|
11
15
|
*/
|
|
@@ -13,17 +17,22 @@
|
|
|
13
17
|
const path = require('path');
|
|
14
18
|
|
|
15
19
|
// Use lib modules
|
|
16
|
-
let ConfigManager, SessionManager, LocalCache, Logger,
|
|
20
|
+
let ConfigManager, SessionManager, LocalCache, Logger, ServerSync;
|
|
17
21
|
try {
|
|
18
22
|
ConfigManager = require('../lib/config');
|
|
19
23
|
SessionManager = require('../lib/session');
|
|
20
24
|
LocalCache = require('../lib/cache');
|
|
21
25
|
Logger = require('../lib/logger');
|
|
22
|
-
|
|
26
|
+
// Issue #701: Phase 2 - Server sync for persistent session state
|
|
27
|
+
const serverSyncModule = require('../lib/server-sync');
|
|
28
|
+
ServerSync = serverSyncModule.getServerSync;
|
|
23
29
|
} catch {
|
|
24
30
|
// Fallback inline implementations if lib not found
|
|
25
31
|
const fs = require('fs');
|
|
26
32
|
const CONFIG_PATH = path.join(process.env.HOME || '', '.plexor', 'config.json');
|
|
33
|
+
const SESSION_PATH = path.join(process.env.HOME || '', '.plexor', 'session.json');
|
|
34
|
+
const PLEXOR_DIR = path.join(process.env.HOME || '', '.plexor');
|
|
35
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
27
36
|
|
|
28
37
|
ConfigManager = class {
|
|
29
38
|
async load() {
|
|
@@ -40,9 +49,84 @@ try {
|
|
|
40
49
|
}
|
|
41
50
|
};
|
|
42
51
|
|
|
52
|
+
// Inline SessionManager for fallback (Issue #701: proper tracking)
|
|
43
53
|
SessionManager = class {
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
constructor() {
|
|
55
|
+
this.sessionPath = SESSION_PATH;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
load() {
|
|
59
|
+
try {
|
|
60
|
+
const data = fs.readFileSync(this.sessionPath, 'utf8');
|
|
61
|
+
const session = JSON.parse(data);
|
|
62
|
+
if (Date.now() - session.last_activity > SESSION_TIMEOUT_MS) {
|
|
63
|
+
return this.createNew();
|
|
64
|
+
}
|
|
65
|
+
return session;
|
|
66
|
+
} catch {
|
|
67
|
+
return this.createNew();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
createNew() {
|
|
72
|
+
const session = {
|
|
73
|
+
session_id: `session_${Date.now()}`,
|
|
74
|
+
started_at: new Date().toISOString(),
|
|
75
|
+
last_activity: Date.now(),
|
|
76
|
+
requests: 0, optimizations: 0, cache_hits: 0, passthroughs: 0,
|
|
77
|
+
original_tokens: 0, optimized_tokens: 0, tokens_saved: 0,
|
|
78
|
+
output_tokens: 0, // Issue #701: Track output tokens
|
|
79
|
+
baseline_cost: 0, actual_cost: 0, cost_saved: 0
|
|
80
|
+
};
|
|
81
|
+
this.save(session);
|
|
82
|
+
return session;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
save(session) {
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
88
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
session.last_activity = Date.now();
|
|
91
|
+
fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2));
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
recordOptimization(result) {
|
|
96
|
+
const s = this.load();
|
|
97
|
+
s.requests++;
|
|
98
|
+
s.optimizations++;
|
|
99
|
+
s.original_tokens += result.original_tokens || 0;
|
|
100
|
+
s.optimized_tokens += result.optimized_tokens || 0;
|
|
101
|
+
s.tokens_saved += result.tokens_saved || 0;
|
|
102
|
+
s.output_tokens += result.output_tokens || 0; // Issue #701
|
|
103
|
+
s.baseline_cost += result.baseline_cost || 0;
|
|
104
|
+
s.actual_cost += result.estimated_cost || result.actual_cost || 0;
|
|
105
|
+
s.cost_saved += (result.baseline_cost || 0) - (result.estimated_cost || result.actual_cost || 0);
|
|
106
|
+
this.save(s);
|
|
107
|
+
return s;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
recordCacheHit() {
|
|
111
|
+
const s = this.load();
|
|
112
|
+
s.requests++;
|
|
113
|
+
s.cache_hits++;
|
|
114
|
+
this.save(s);
|
|
115
|
+
return s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
recordPassthrough(outputTokens = 0) {
|
|
119
|
+
const s = this.load();
|
|
120
|
+
s.requests++;
|
|
121
|
+
s.passthroughs = (s.passthroughs || 0) + 1;
|
|
122
|
+
s.output_tokens += outputTokens; // Issue #701: Track output even for passthroughs
|
|
123
|
+
this.save(s);
|
|
124
|
+
return s;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getStats() {
|
|
128
|
+
return this.load();
|
|
129
|
+
}
|
|
46
130
|
};
|
|
47
131
|
|
|
48
132
|
LocalCache = class {
|
|
@@ -50,10 +134,28 @@ try {
|
|
|
50
134
|
};
|
|
51
135
|
|
|
52
136
|
Logger = class {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
137
|
+
constructor(name) { this.name = name; }
|
|
138
|
+
info(msg, data) {
|
|
139
|
+
if (data) {
|
|
140
|
+
console.error(`[${this.name}] ${msg}`, JSON.stringify(data));
|
|
141
|
+
} else {
|
|
142
|
+
console.error(`[${this.name}] ${msg}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
error(msg) { console.error(`[${this.name}] [ERROR] ${msg}`); }
|
|
146
|
+
debug(msg) {
|
|
147
|
+
if (process.env.PLEXOR_DEBUG) {
|
|
148
|
+
console.error(`[${this.name}] [DEBUG] ${msg}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
56
151
|
};
|
|
152
|
+
|
|
153
|
+
// Issue #701: Fallback ServerSync that does nothing
|
|
154
|
+
ServerSync = () => ({
|
|
155
|
+
syncSession: async () => ({ synced: false, message: 'Server sync not available' }),
|
|
156
|
+
scheduleSync: () => {},
|
|
157
|
+
needsSync: () => false
|
|
158
|
+
});
|
|
57
159
|
}
|
|
58
160
|
|
|
59
161
|
const logger = new Logger('track-response');
|
|
@@ -61,69 +163,185 @@ const config = new ConfigManager();
|
|
|
61
163
|
const cache = new LocalCache();
|
|
62
164
|
const session = new SessionManager();
|
|
63
165
|
|
|
166
|
+
// Issue #701: Phase 2 - Initialize server sync (lazy, initialized on first use)
|
|
167
|
+
let serverSync = null;
|
|
168
|
+
|
|
169
|
+
async function getServerSync() {
|
|
170
|
+
if (serverSync) return serverSync;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const settings = await config.load();
|
|
174
|
+
if (settings.apiKey && settings.enabled) {
|
|
175
|
+
serverSync = ServerSync({
|
|
176
|
+
apiKey: settings.apiKey,
|
|
177
|
+
baseUrl: settings.apiUrl || 'https://api.plexor.dev',
|
|
178
|
+
enabled: settings.serverSyncEnabled !== false
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
serverSync = ServerSync({ enabled: false });
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
serverSync = ServerSync({ enabled: false });
|
|
185
|
+
}
|
|
186
|
+
return serverSync;
|
|
187
|
+
}
|
|
188
|
+
|
|
64
189
|
async function main() {
|
|
190
|
+
let response;
|
|
191
|
+
let input;
|
|
192
|
+
|
|
65
193
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
194
|
+
input = await readStdin();
|
|
195
|
+
response = JSON.parse(input);
|
|
68
196
|
|
|
69
|
-
|
|
197
|
+
// Calculate output tokens for ALL responses (Issue #701)
|
|
198
|
+
const outputTokens = estimateOutputTokens(response);
|
|
70
199
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
200
|
+
// Get Plexor metadata if present
|
|
201
|
+
const plexorMeta = response._plexor;
|
|
202
|
+
|
|
203
|
+
// Issue #701: Track ALL responses, not just when enabled
|
|
204
|
+
// This ensures session stats are always accurate
|
|
205
|
+
if (plexorMeta) {
|
|
206
|
+
await trackResponseWithMetadata(plexorMeta, outputTokens);
|
|
207
|
+
} else {
|
|
208
|
+
// No Plexor metadata - still track the output tokens
|
|
209
|
+
session.recordPassthrough(outputTokens);
|
|
210
|
+
logger.debug('Response tracked without Plexor metadata');
|
|
74
211
|
}
|
|
75
212
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
session.
|
|
213
|
+
// Issue #701: Phase 2 - Schedule server sync (non-blocking)
|
|
214
|
+
// This syncs local session stats to the server for persistence
|
|
215
|
+
try {
|
|
216
|
+
const sync = await getServerSync();
|
|
217
|
+
const localStats = session.getStats();
|
|
218
|
+
sync.scheduleSync(localStats);
|
|
219
|
+
} catch (syncError) {
|
|
220
|
+
// Server sync failures should not affect response passthrough
|
|
221
|
+
logger.debug(`Server sync scheduling failed: ${syncError.message}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Pass through unchanged
|
|
225
|
+
return output(response);
|
|
226
|
+
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logger.error(`Tracking error: ${error.message}`);
|
|
229
|
+
|
|
230
|
+
// On any error, try to pass through the response unchanged
|
|
231
|
+
if (response) {
|
|
81
232
|
return output(response);
|
|
233
|
+
} else if (input) {
|
|
234
|
+
try {
|
|
235
|
+
return output(JSON.parse(input));
|
|
236
|
+
} catch {
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
process.exit(1);
|
|
82
241
|
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
83
244
|
|
|
84
|
-
|
|
85
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Track response using Plexor metadata.
|
|
247
|
+
* Issue #701: Handles ALL source types, not just plexor_api.
|
|
248
|
+
*/
|
|
249
|
+
async function trackResponseWithMetadata(plexorMeta, outputTokens) {
|
|
250
|
+
const source = plexorMeta.source || 'unknown';
|
|
251
|
+
const requestId = plexorMeta.request_id;
|
|
86
252
|
|
|
87
|
-
|
|
88
|
-
const outputTokens = estimateTokens(response.content || '');
|
|
253
|
+
logger.debug(`Tracking response: source=${source}, request_id=${requestId}`);
|
|
89
254
|
|
|
90
|
-
|
|
91
|
-
|
|
255
|
+
switch (source) {
|
|
256
|
+
case 'plexor_api':
|
|
257
|
+
// Full optimization was applied
|
|
92
258
|
session.recordOptimization({
|
|
93
259
|
original_tokens: plexorMeta.original_tokens || 0,
|
|
94
260
|
optimized_tokens: plexorMeta.optimized_tokens || 0,
|
|
95
261
|
tokens_saved: plexorMeta.tokens_saved || 0,
|
|
96
262
|
baseline_cost: plexorMeta.baseline_cost || 0,
|
|
97
|
-
estimated_cost: plexorMeta.estimated_cost || 0
|
|
263
|
+
estimated_cost: plexorMeta.estimated_cost || 0,
|
|
264
|
+
output_tokens: outputTokens
|
|
98
265
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
request_id: plexorMeta.request_id,
|
|
266
|
+
logger.info('[Plexor] Optimized response tracked', {
|
|
267
|
+
request_id: requestId,
|
|
102
268
|
input_tokens: plexorMeta.optimized_tokens,
|
|
103
269
|
output_tokens: outputTokens,
|
|
270
|
+
savings_percent: plexorMeta.savings_percent,
|
|
104
271
|
provider: plexorMeta.recommended_provider
|
|
105
272
|
});
|
|
106
|
-
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case 'local_cache':
|
|
107
276
|
session.recordCacheHit();
|
|
108
|
-
logger.info('[Plexor] Cache hit recorded');
|
|
109
|
-
|
|
110
|
-
session.recordPassthrough();
|
|
111
|
-
}
|
|
277
|
+
logger.info('[Plexor] Cache hit recorded', { request_id: requestId });
|
|
278
|
+
break;
|
|
112
279
|
|
|
113
|
-
|
|
114
|
-
|
|
280
|
+
case 'passthrough_agentic':
|
|
281
|
+
case 'passthrough_slash_command':
|
|
282
|
+
case 'passthrough_cli':
|
|
283
|
+
case 'passthrough_disabled':
|
|
284
|
+
case 'passthrough_no_auth':
|
|
285
|
+
case 'passthrough_error':
|
|
286
|
+
case 'passthrough':
|
|
287
|
+
// Passthrough - still track output tokens
|
|
288
|
+
session.recordPassthrough(outputTokens);
|
|
289
|
+
logger.debug(`Passthrough recorded: ${source}`, {
|
|
290
|
+
request_id: requestId,
|
|
291
|
+
reason: plexorMeta.reason,
|
|
292
|
+
output_tokens: outputTokens
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
115
295
|
|
|
116
|
-
|
|
117
|
-
|
|
296
|
+
default:
|
|
297
|
+
// Unknown source - treat as passthrough
|
|
298
|
+
session.recordPassthrough(outputTokens);
|
|
299
|
+
logger.debug(`Unknown source tracked as passthrough: ${source}`, {
|
|
300
|
+
request_id: requestId,
|
|
301
|
+
output_tokens: outputTokens
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
118
305
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Estimate output tokens from response content.
|
|
308
|
+
* Handles both string and structured content formats.
|
|
309
|
+
*/
|
|
310
|
+
function estimateOutputTokens(response) {
|
|
311
|
+
if (!response) return 0;
|
|
312
|
+
|
|
313
|
+
// Try to get content from various response formats
|
|
314
|
+
let text = '';
|
|
315
|
+
|
|
316
|
+
// Anthropic format: content array with text blocks
|
|
317
|
+
if (Array.isArray(response.content)) {
|
|
318
|
+
for (const block of response.content) {
|
|
319
|
+
if (block.type === 'text' && block.text) {
|
|
320
|
+
text += block.text;
|
|
321
|
+
} else if (block.type === 'tool_use') {
|
|
322
|
+
// Tool use blocks contribute to token count
|
|
323
|
+
text += JSON.stringify(block.input || {});
|
|
324
|
+
}
|
|
125
325
|
}
|
|
126
326
|
}
|
|
327
|
+
// Simple string content
|
|
328
|
+
else if (typeof response.content === 'string') {
|
|
329
|
+
text = response.content;
|
|
330
|
+
}
|
|
331
|
+
// OpenAI format: choices array
|
|
332
|
+
else if (response.choices && response.choices[0]) {
|
|
333
|
+
const choice = response.choices[0];
|
|
334
|
+
if (choice.message && choice.message.content) {
|
|
335
|
+
text = choice.message.content;
|
|
336
|
+
} else if (choice.text) {
|
|
337
|
+
text = choice.text;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!text) return 0;
|
|
342
|
+
|
|
343
|
+
// Approximate: ~4 characters per token (rough estimate)
|
|
344
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
127
345
|
}
|
|
128
346
|
|
|
129
347
|
async function readStdin() {
|
|
@@ -152,12 +370,6 @@ function output(data) {
|
|
|
152
370
|
process.exit(0);
|
|
153
371
|
}
|
|
154
372
|
|
|
155
|
-
function estimateTokens(text) {
|
|
156
|
-
if (!text) return 0;
|
|
157
|
-
// Approximate: ~4 characters per token
|
|
158
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
373
|
main().catch((error) => {
|
|
162
374
|
console.error(`[Plexor] Fatal error: ${error.message}`);
|
|
163
375
|
process.exit(1);
|
|
@@ -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
CHANGED
|
@@ -38,9 +38,11 @@ class SessionManager {
|
|
|
38
38
|
requests: 0,
|
|
39
39
|
optimizations: 0,
|
|
40
40
|
cache_hits: 0,
|
|
41
|
+
passthroughs: 0, // Issue #701: Track passthroughs
|
|
41
42
|
original_tokens: 0,
|
|
42
43
|
optimized_tokens: 0,
|
|
43
44
|
tokens_saved: 0,
|
|
45
|
+
output_tokens: 0, // Issue #701: Track output tokens
|
|
44
46
|
baseline_cost: 0,
|
|
45
47
|
actual_cost: 0,
|
|
46
48
|
cost_saved: 0
|
|
@@ -71,6 +73,7 @@ class SessionManager {
|
|
|
71
73
|
session.original_tokens += stats.original_tokens || 0;
|
|
72
74
|
session.optimized_tokens += stats.optimized_tokens || 0;
|
|
73
75
|
session.tokens_saved += stats.tokens_saved || 0;
|
|
76
|
+
session.output_tokens = (session.output_tokens || 0) + (stats.output_tokens || 0); // Issue #701
|
|
74
77
|
session.baseline_cost += stats.baseline_cost || 0;
|
|
75
78
|
session.actual_cost += stats.actual_cost || 0;
|
|
76
79
|
session.cost_saved += stats.cost_saved || 0;
|
|
@@ -80,6 +83,12 @@ class SessionManager {
|
|
|
80
83
|
session.cache_hits++;
|
|
81
84
|
}
|
|
82
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
|
+
|
|
83
92
|
this.save(session);
|
|
84
93
|
return session;
|
|
85
94
|
}
|
|
@@ -90,6 +99,7 @@ class SessionManager {
|
|
|
90
99
|
original_tokens: result.original_tokens || 0,
|
|
91
100
|
optimized_tokens: result.optimized_tokens || 0,
|
|
92
101
|
tokens_saved: result.tokens_saved || 0,
|
|
102
|
+
output_tokens: result.output_tokens || 0, // Issue #701: Track output tokens
|
|
93
103
|
baseline_cost: result.baseline_cost || 0,
|
|
94
104
|
actual_cost: result.estimated_cost || result.actual_cost || 0,
|
|
95
105
|
cost_saved: (result.baseline_cost || 0) - (result.estimated_cost || result.actual_cost || 0)
|
|
@@ -100,8 +110,9 @@ class SessionManager {
|
|
|
100
110
|
return this.recordRequest({ cache_hit: true });
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
recordPassthrough() {
|
|
104
|
-
|
|
113
|
+
recordPassthrough(outputTokens = 0) {
|
|
114
|
+
// Issue #701: Track passthroughs and their output tokens
|
|
115
|
+
return this.recordRequest({ passthrough: true, output_tokens: outputTokens });
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
getStats() {
|
package/package.json
CHANGED
package/scripts/plexor-cli.sh
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Plexor CLI - Claude Code with intelligent optimization
|
|
3
|
+
# This wrapper auto-enables hypervisor mode for direct gateway routing
|
|
3
4
|
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
DIM='\033[2m'
|
|
11
|
-
NC='\033[0m' # No Color
|
|
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
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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'
|
|
16
29
|
____ __
|
|
17
30
|
/ __ \/ /__ _ ______ _____
|
|
18
31
|
/ /_/ / / _ \| |/_/ __ \/ ___/
|
|
@@ -20,19 +33,13 @@ if [[ "$ANTHROPIC_BASE_URL" == *"plexor"* ]]; then
|
|
|
20
33
|
/_/ /_/\___/_/|_|\____/_/
|
|
21
34
|
|
|
22
35
|
EOF
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if [ "$ENABLED" = "true" ]; then
|
|
32
|
-
echo -e "${GREEN}●${NC} Optimization: ${GREEN}Active${NC} (${MODE} mode)"
|
|
33
|
-
else
|
|
34
|
-
echo -e "${YELLOW}○${NC} Optimization: ${DIM}Disabled${NC}"
|
|
35
|
-
fi
|
|
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)"
|
|
36
43
|
echo ""
|
|
37
44
|
fi
|
|
38
45
|
fi
|
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,6 +112,12 @@ 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');
|