@plexor-dev/claude-code-plugin 0.1.0-beta.10 → 0.1.0-beta.12
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-status.js +50 -1
- package/commands/plexor-status.md +12 -2
- package/hooks/intercept.js +191 -32
- package/package.json +1 -1
|
@@ -10,6 +10,22 @@ const path = require('path');
|
|
|
10
10
|
const https = require('https');
|
|
11
11
|
|
|
12
12
|
const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
|
|
13
|
+
const SESSION_PATH = path.join(process.env.HOME, '.plexor', 'session.json');
|
|
14
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
15
|
+
|
|
16
|
+
function loadSessionStats() {
|
|
17
|
+
try {
|
|
18
|
+
const data = fs.readFileSync(SESSION_PATH, 'utf8');
|
|
19
|
+
const session = JSON.parse(data);
|
|
20
|
+
// Check if session has expired
|
|
21
|
+
if (Date.now() - session.last_activity > SESSION_TIMEOUT_MS) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return session;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
13
29
|
|
|
14
30
|
async function main() {
|
|
15
31
|
// Read config
|
|
@@ -47,6 +63,9 @@ async function main() {
|
|
|
47
63
|
// Continue with defaults if API fails
|
|
48
64
|
}
|
|
49
65
|
|
|
66
|
+
// Load session stats
|
|
67
|
+
const session = loadSessionStats();
|
|
68
|
+
|
|
50
69
|
// Extract data
|
|
51
70
|
const email = user.email || 'Unknown';
|
|
52
71
|
const tierName = user.tier?.name || 'Free';
|
|
@@ -79,13 +98,43 @@ async function main() {
|
|
|
79
98
|
// Output formatted status - each line is exactly 43 chars inner width
|
|
80
99
|
const line = (content) => ` │ ${content.padEnd(43)}│`;
|
|
81
100
|
|
|
101
|
+
// Session stats formatting
|
|
102
|
+
const formatDuration = (startedAt) => {
|
|
103
|
+
if (!startedAt) return '0m';
|
|
104
|
+
const elapsed = Date.now() - new Date(startedAt).getTime();
|
|
105
|
+
const minutes = Math.floor(elapsed / 60000);
|
|
106
|
+
if (minutes < 60) return `${minutes}m`;
|
|
107
|
+
const hours = Math.floor(minutes / 60);
|
|
108
|
+
return `${hours}h ${minutes % 60}m`;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const sessionDuration = session ? formatDuration(session.started_at) : '0m';
|
|
112
|
+
const sessionRequests = session ? formatNum(session.requests) : '0';
|
|
113
|
+
const sessionOptimizations = session ? formatNum(session.optimizations) : '0';
|
|
114
|
+
const sessionCacheHits = session ? formatNum(session.cache_hits) : '0';
|
|
115
|
+
const sessionTokensSaved = session ? formatNum(session.tokens_saved) : '0';
|
|
116
|
+
const sessionTokensSavedPct = session && session.original_tokens > 0
|
|
117
|
+
? formatPct((session.tokens_saved / session.original_tokens) * 100)
|
|
118
|
+
: '0.0';
|
|
119
|
+
const sessionCostSaved = session ? formatCost(session.cost_saved) : '0.00';
|
|
120
|
+
|
|
121
|
+
// Build session section (only show if session exists)
|
|
122
|
+
const sessionSection = session ? ` ├─────────────────────────────────────────────┤
|
|
123
|
+
${line(`This Session (${sessionDuration})`)}
|
|
124
|
+
${line(`├── Requests: ${sessionRequests}`)}
|
|
125
|
+
${line(`├── Optimizations: ${sessionOptimizations}`)}
|
|
126
|
+
${line(`├── Cache hits: ${sessionCacheHits}`)}
|
|
127
|
+
${line(`├── Tokens saved: ${sessionTokensSaved} (${sessionTokensSavedPct}%)`)}
|
|
128
|
+
${line(`└── Cost saved: $${sessionCostSaved}`)}
|
|
129
|
+
` : '';
|
|
130
|
+
|
|
82
131
|
console.log(` ┌─────────────────────────────────────────────┐
|
|
83
132
|
${line('Plexor Status')}
|
|
84
133
|
├─────────────────────────────────────────────┤
|
|
85
134
|
${line(`Account: ${tierName}`)}
|
|
86
135
|
${line(`Email: ${email}`)}
|
|
87
136
|
${line(`Status: ${status}`)}
|
|
88
|
-
├─────────────────────────────────────────────┤
|
|
137
|
+
${sessionSection} ├─────────────────────────────────────────────┤
|
|
89
138
|
${line(`This Week (${weekRange})`)}
|
|
90
139
|
${line(`├── Requests: ${formatNum(summary.total_requests)}`)}
|
|
91
140
|
${line(`├── Original tokens: ${formatNum(summary.original_tokens)}`)}
|
|
@@ -9,10 +9,11 @@ Display Plexor optimization statistics in a clean, professional format.
|
|
|
9
9
|
## Instructions
|
|
10
10
|
|
|
11
11
|
1. Read `~/.plexor/config.json` to get settings and API key
|
|
12
|
-
2.
|
|
12
|
+
2. Read `~/.plexor/session.json` to get current session stats (if exists and not expired after 30min inactivity)
|
|
13
|
+
3. Call the Plexor APIs to get user info and stats:
|
|
13
14
|
- `GET {apiUrl}/v1/user` with header `X-Plexor-Key: {api_key}`
|
|
14
15
|
- `GET {apiUrl}/v1/stats` with header `X-Plexor-Key: {api_key}`
|
|
15
|
-
|
|
16
|
+
4. Output the formatted status box directly as text (not in a code block):
|
|
16
17
|
|
|
17
18
|
```
|
|
18
19
|
┌─────────────────────────────────────────────┐
|
|
@@ -22,6 +23,13 @@ Display Plexor optimization statistics in a clean, professional format.
|
|
|
22
23
|
│ Email: {email} │
|
|
23
24
|
│ Status: ● Active │
|
|
24
25
|
├─────────────────────────────────────────────┤
|
|
26
|
+
│ This Session ({duration}) │
|
|
27
|
+
│ ├── Requests: {session_requests} │
|
|
28
|
+
│ ├── Optimizations: {session_optimizations} │
|
|
29
|
+
│ ├── Cache hits: {session_cache_hits} │
|
|
30
|
+
│ ├── Tokens saved: {tokens} ({%}) │
|
|
31
|
+
│ └── Cost saved: ${session_cost_saved} │
|
|
32
|
+
├─────────────────────────────────────────────┤
|
|
25
33
|
│ This Week ({period.start} - {period.end}) │
|
|
26
34
|
│ ├── Requests: {total_requests} │
|
|
27
35
|
│ ├── Original tokens: {original_tokens} │
|
|
@@ -57,3 +65,5 @@ IMPORTANT:
|
|
|
57
65
|
- Format dates as "Mon D" (e.g., "Jan 7")
|
|
58
66
|
- Use "● Active" if enabled, "○ Inactive" if disabled
|
|
59
67
|
- Multiply cache_hit_rate by 100 for percentage
|
|
68
|
+
- Only show "This Session" section if session.json exists and is not expired (30min timeout)
|
|
69
|
+
- Session duration format: "Xm" for minutes, "Xh Ym" for hours
|
package/hooks/intercept.js
CHANGED
|
@@ -10,42 +10,166 @@
|
|
|
10
10
|
* Output: Modified JSON object with optimized messages
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const LocalCache = require('../lib/cache');
|
|
16
|
-
const Logger = require('../lib/logger');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
16
|
+
// Inline implementations to avoid missing module errors
|
|
17
|
+
const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
|
|
18
|
+
const SESSION_PATH = path.join(process.env.HOME, '.plexor', 'session.json');
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const logger = {
|
|
21
|
+
debug: (msg) => process.env.PLEXOR_DEBUG && console.error(`[DEBUG] ${msg}`),
|
|
22
|
+
info: (msg) => console.error(msg),
|
|
23
|
+
error: (msg) => console.error(`[ERROR] ${msg}`)
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const config = {
|
|
27
|
+
load: async () => {
|
|
28
|
+
try {
|
|
29
|
+
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
30
|
+
const cfg = JSON.parse(data);
|
|
31
|
+
return {
|
|
32
|
+
enabled: cfg.settings?.enabled ?? false,
|
|
33
|
+
apiKey: cfg.auth?.api_key,
|
|
34
|
+
apiUrl: cfg.settings?.apiUrl || 'https://api.plexor.dev',
|
|
35
|
+
timeout: cfg.settings?.timeout || 5000,
|
|
36
|
+
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
37
|
+
mode: cfg.settings?.mode || 'balanced'
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return { enabled: false };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const cache = {
|
|
46
|
+
generateKey: (messages) => {
|
|
47
|
+
const str = JSON.stringify(messages);
|
|
48
|
+
let hash = 0;
|
|
49
|
+
for (let i = 0; i < str.length; i++) {
|
|
50
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
51
|
+
hash |= 0;
|
|
52
|
+
}
|
|
53
|
+
return `cache_${hash}`;
|
|
54
|
+
},
|
|
55
|
+
get: async () => null,
|
|
56
|
+
setMetadata: async () => {}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Session stats tracking
|
|
60
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
24
61
|
|
|
62
|
+
function loadSessionStats() {
|
|
25
63
|
try {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
64
|
+
const data = fs.readFileSync(SESSION_PATH, 'utf8');
|
|
65
|
+
const session = JSON.parse(data);
|
|
66
|
+
// Check if session has expired
|
|
67
|
+
if (Date.now() - session.last_activity > SESSION_TIMEOUT_MS) {
|
|
68
|
+
return createNewSession();
|
|
69
|
+
}
|
|
70
|
+
return session;
|
|
71
|
+
} catch {
|
|
72
|
+
return createNewSession();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
28
75
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
function createNewSession() {
|
|
77
|
+
return {
|
|
78
|
+
session_id: `session_${Date.now()}`,
|
|
79
|
+
started_at: new Date().toISOString(),
|
|
80
|
+
last_activity: Date.now(),
|
|
81
|
+
requests: 0,
|
|
82
|
+
optimizations: 0,
|
|
83
|
+
cache_hits: 0,
|
|
84
|
+
original_tokens: 0,
|
|
85
|
+
optimized_tokens: 0,
|
|
86
|
+
tokens_saved: 0,
|
|
87
|
+
baseline_cost: 0,
|
|
88
|
+
actual_cost: 0,
|
|
89
|
+
cost_saved: 0
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function saveSessionStats(session) {
|
|
94
|
+
try {
|
|
95
|
+
const dir = path.dirname(SESSION_PATH);
|
|
96
|
+
if (!fs.existsSync(dir)) {
|
|
97
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
98
|
}
|
|
99
|
+
session.last_activity = Date.now();
|
|
100
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
logger.debug(`Failed to save session stats: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function updateSessionStats(result) {
|
|
107
|
+
const session = loadSessionStats();
|
|
108
|
+
session.requests++;
|
|
109
|
+
session.optimizations++;
|
|
110
|
+
session.original_tokens += result.original_tokens || 0;
|
|
111
|
+
session.optimized_tokens += result.optimized_tokens || 0;
|
|
112
|
+
session.tokens_saved += result.tokens_saved || 0;
|
|
113
|
+
session.baseline_cost += result.baseline_cost || 0;
|
|
114
|
+
session.actual_cost += result.estimated_cost || 0;
|
|
115
|
+
session.cost_saved += (result.baseline_cost || 0) - (result.estimated_cost || 0);
|
|
116
|
+
saveSessionStats(session);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function recordCacheHit() {
|
|
120
|
+
const session = loadSessionStats();
|
|
121
|
+
session.requests++;
|
|
122
|
+
session.cache_hits++;
|
|
123
|
+
saveSessionStats(session);
|
|
124
|
+
}
|
|
44
125
|
|
|
45
|
-
|
|
126
|
+
function recordPassthrough() {
|
|
127
|
+
const session = loadSessionStats();
|
|
128
|
+
session.requests++;
|
|
129
|
+
saveSessionStats(session);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Placeholder PlexorClient
|
|
133
|
+
class PlexorClient {
|
|
134
|
+
constructor(opts) {
|
|
135
|
+
this.apiKey = opts.apiKey;
|
|
136
|
+
this.baseUrl = opts.baseUrl;
|
|
137
|
+
this.timeout = opts.timeout;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async optimize(params) {
|
|
141
|
+
// Return passthrough result - real API call would go here
|
|
142
|
+
const tokens = JSON.stringify(params.messages).length / 4;
|
|
143
|
+
return {
|
|
144
|
+
request_id: `req_${Date.now()}`,
|
|
145
|
+
original_tokens: Math.round(tokens),
|
|
146
|
+
optimized_tokens: Math.round(tokens * 0.7),
|
|
147
|
+
tokens_saved: Math.round(tokens * 0.3),
|
|
148
|
+
optimized_messages: params.messages,
|
|
149
|
+
recommended_provider: 'anthropic',
|
|
150
|
+
recommended_model: params.model,
|
|
151
|
+
estimated_cost: tokens * 0.00001,
|
|
152
|
+
baseline_cost: tokens * 0.00003
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function main() {
|
|
158
|
+
const startTime = Date.now();
|
|
159
|
+
|
|
160
|
+
let input;
|
|
161
|
+
let request;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
input = await readStdin();
|
|
165
|
+
request = JSON.parse(input);
|
|
166
|
+
|
|
167
|
+
// CRITICAL: Check for slash commands FIRST (before agentic check)
|
|
46
168
|
// Slash commands like /plexor-status should pass through unchanged
|
|
169
|
+
// Must check before isAgenticRequest since all Claude Code requests have tools
|
|
47
170
|
if (isSlashCommand(request)) {
|
|
48
171
|
logger.debug('Slash command detected, passing through unchanged');
|
|
172
|
+
recordPassthrough();
|
|
49
173
|
return output({
|
|
50
174
|
...request,
|
|
51
175
|
plexor_cwd: process.cwd(),
|
|
@@ -58,10 +182,11 @@ async function main() {
|
|
|
58
182
|
});
|
|
59
183
|
}
|
|
60
184
|
|
|
61
|
-
// CRITICAL: Skip optimization for CLI commands requiring tool execution
|
|
185
|
+
// CRITICAL: Skip optimization for CLI commands requiring tool execution
|
|
62
186
|
// Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
|
|
63
187
|
if (requiresToolExecution(request)) {
|
|
64
188
|
logger.debug('CLI tool execution detected, passing through unchanged');
|
|
189
|
+
recordPassthrough();
|
|
65
190
|
return output({
|
|
66
191
|
...request,
|
|
67
192
|
plexor_cwd: process.cwd(),
|
|
@@ -74,6 +199,23 @@ async function main() {
|
|
|
74
199
|
});
|
|
75
200
|
}
|
|
76
201
|
|
|
202
|
+
// CRITICAL: Skip optimization for agentic/tool-using requests
|
|
203
|
+
// Modifying messages breaks the agent loop and causes infinite loops
|
|
204
|
+
if (isAgenticRequest(request)) {
|
|
205
|
+
logger.debug('Agentic request detected, passing through unchanged');
|
|
206
|
+
recordPassthrough();
|
|
207
|
+
return output({
|
|
208
|
+
...request,
|
|
209
|
+
plexor_cwd: process.cwd(),
|
|
210
|
+
_plexor: {
|
|
211
|
+
source: 'passthrough_agentic',
|
|
212
|
+
reason: 'tool_use_detected',
|
|
213
|
+
cwd: process.cwd(),
|
|
214
|
+
latency_ms: Date.now() - startTime
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
77
219
|
const settings = await config.load();
|
|
78
220
|
|
|
79
221
|
if (!settings.enabled) {
|
|
@@ -101,6 +243,7 @@ async function main() {
|
|
|
101
243
|
|
|
102
244
|
if (cachedResponse && settings.localCacheEnabled) {
|
|
103
245
|
logger.info('[Plexor] Local cache hit');
|
|
246
|
+
recordCacheHit();
|
|
104
247
|
return output({
|
|
105
248
|
...request,
|
|
106
249
|
_plexor: {
|
|
@@ -159,23 +302,39 @@ async function main() {
|
|
|
159
302
|
timestamp: Date.now()
|
|
160
303
|
});
|
|
161
304
|
|
|
305
|
+
// Update session stats
|
|
306
|
+
updateSessionStats(result);
|
|
307
|
+
|
|
162
308
|
return output(optimizedRequest);
|
|
163
309
|
|
|
164
310
|
} catch (error) {
|
|
165
311
|
logger.error(`[Plexor] Error: ${error.message}`);
|
|
166
312
|
logger.debug(error.stack);
|
|
167
313
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const request = JSON.parse(input);
|
|
314
|
+
// Use already-parsed request if available, otherwise pass through raw
|
|
315
|
+
if (request) {
|
|
171
316
|
return output({
|
|
172
317
|
...request,
|
|
173
318
|
_plexor: {
|
|
174
319
|
error: error.message,
|
|
175
|
-
source: '
|
|
320
|
+
source: 'passthrough_error'
|
|
176
321
|
}
|
|
177
322
|
});
|
|
178
|
-
}
|
|
323
|
+
} else if (input) {
|
|
324
|
+
// Try to parse the input we already read
|
|
325
|
+
try {
|
|
326
|
+
const req = JSON.parse(input);
|
|
327
|
+
return output({
|
|
328
|
+
...req,
|
|
329
|
+
_plexor: {
|
|
330
|
+
error: error.message,
|
|
331
|
+
source: 'passthrough_error'
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
} catch {
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
179
338
|
process.exit(1);
|
|
180
339
|
}
|
|
181
340
|
}
|
package/package.json
CHANGED