@plexor-dev/claude-code-plugin 0.1.0-beta.14 → 0.1.0-beta.16
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.md +4 -60
- package/hooks/intercept.js +125 -132
- package/hooks/track-response.js +72 -18
- package/lib/cache.js +107 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +25 -0
- package/lib/index.js +19 -0
- package/lib/logger.js +36 -0
- package/lib/plexor-client.js +122 -0
- package/lib/session.js +145 -0
- package/package.json +1 -1
|
@@ -4,66 +4,10 @@ description: Show Plexor optimization statistics and savings (user)
|
|
|
4
4
|
|
|
5
5
|
# Plexor Status
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Run this command to display Plexor statistics:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
1. Read `~/.plexor/config.json` to get settings and API key
|
|
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:
|
|
14
|
-
- `GET {apiUrl}/v1/user` with header `X-Plexor-Key: {api_key}`
|
|
15
|
-
- `GET {apiUrl}/v1/stats` with header `X-Plexor-Key: {api_key}`
|
|
16
|
-
4. Output the formatted status box directly as text (not in a code block):
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
┌─────────────────────────────────────────────┐
|
|
20
|
-
│ Plexor Status │
|
|
21
|
-
├─────────────────────────────────────────────┤
|
|
22
|
-
│ Account: {tier.name} │
|
|
23
|
-
│ Email: {email} │
|
|
24
|
-
│ Status: ● Active │
|
|
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
|
-
├─────────────────────────────────────────────┤
|
|
33
|
-
│ This Week ({period.start} - {period.end}) │
|
|
34
|
-
│ ├── Requests: {total_requests} │
|
|
35
|
-
│ ├── Original tokens: {original_tokens} │
|
|
36
|
-
│ ├── Optimized tokens: {optimized_tokens} │
|
|
37
|
-
│ ├── Tokens saved: {tokens_saved} ({%}) │
|
|
38
|
-
│ ├── Baseline cost: ${baseline_cost} │
|
|
39
|
-
│ ├── Actual cost: ${total_cost} │
|
|
40
|
-
│ └── Cost saved: ${cost_saved} ({%}) │
|
|
41
|
-
├─────────────────────────────────────────────┤
|
|
42
|
-
│ Performance │
|
|
43
|
-
│ └── Cache hit rate: {cache_hit_rate}% │
|
|
44
|
-
├─────────────────────────────────────────────┤
|
|
45
|
-
│ Limits │
|
|
46
|
-
│ ├── Monthly optimizations: {limit} │
|
|
47
|
-
│ └── Monthly completions: {limit} │
|
|
48
|
-
├─────────────────────────────────────────────┤
|
|
49
|
-
│ Settings │
|
|
50
|
-
│ ├── Optimization: {Enabled/Disabled} │
|
|
51
|
-
│ ├── Local cache: {Enabled/Disabled} │
|
|
52
|
-
│ ├── Mode: {mode} │
|
|
53
|
-
│ └── Provider routing: {provider} │
|
|
54
|
-
└─────────────────────────────────────────────┘
|
|
55
|
-
|
|
56
|
-
Dashboard: {dashboard_url}
|
|
9
|
+
```bash
|
|
10
|
+
node ~/.claude/plugins/plexor/commands/plexor-status.js
|
|
57
11
|
```
|
|
58
12
|
|
|
59
|
-
|
|
60
|
-
- Output ONLY the status box as plain text, exactly as shown above
|
|
61
|
-
- Do NOT wrap in markdown code blocks
|
|
62
|
-
- Do NOT add any commentary before or after
|
|
63
|
-
- Format numbers with commas (e.g., 50,000)
|
|
64
|
-
- Format costs with 2 decimals (e.g., $4.50)
|
|
65
|
-
- Format dates as "Mon D" (e.g., "Jan 7")
|
|
66
|
-
- Use "● Active" if enabled, "○ Inactive" if disabled
|
|
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
|
|
13
|
+
Use the Bash tool to execute this single command. Do not read files manually or format output yourself.
|
package/hooks/intercept.js
CHANGED
|
@@ -13,145 +13,138 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
};
|
|
16
|
+
// Try to load lib modules, fall back to inline implementations
|
|
17
|
+
let ConfigManager, SessionManager, LocalCache, Logger, PlexorClient;
|
|
18
|
+
let config, session, cache, logger;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
ConfigManager = require('../lib/config');
|
|
22
|
+
SessionManager = require('../lib/session');
|
|
23
|
+
LocalCache = require('../lib/cache');
|
|
24
|
+
Logger = require('../lib/logger');
|
|
25
|
+
PlexorClient = require('../lib/plexor-client');
|
|
26
|
+
|
|
27
|
+
config = new ConfigManager();
|
|
28
|
+
session = new SessionManager();
|
|
29
|
+
cache = new LocalCache();
|
|
30
|
+
logger = new Logger('intercept');
|
|
31
|
+
} catch {
|
|
32
|
+
// Fallback inline implementations
|
|
33
|
+
const CONFIG_PATH = path.join(process.env.HOME || '', '.plexor', 'config.json');
|
|
34
|
+
const SESSION_PATH = path.join(process.env.HOME || '', '.plexor', 'session.json');
|
|
35
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
logger = {
|
|
38
|
+
debug: (msg) => process.env.PLEXOR_DEBUG && console.error(`[DEBUG] ${msg}`),
|
|
39
|
+
info: (msg) => console.error(msg),
|
|
40
|
+
error: (msg) => console.error(`[ERROR] ${msg}`)
|
|
41
|
+
};
|
|
58
42
|
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
config = {
|
|
44
|
+
load: async () => {
|
|
45
|
+
try {
|
|
46
|
+
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
47
|
+
const cfg = JSON.parse(data);
|
|
48
|
+
return {
|
|
49
|
+
enabled: cfg.settings?.enabled ?? false,
|
|
50
|
+
apiKey: cfg.auth?.api_key,
|
|
51
|
+
apiUrl: cfg.settings?.apiUrl || 'https://api.plexor.dev',
|
|
52
|
+
timeout: cfg.settings?.timeout || 5000,
|
|
53
|
+
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
54
|
+
mode: cfg.settings?.mode || 'balanced'
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return { enabled: false };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
const loadSession = () => {
|
|
63
|
+
try {
|
|
64
|
+
const data = fs.readFileSync(SESSION_PATH, 'utf8');
|
|
65
|
+
const s = JSON.parse(data);
|
|
66
|
+
if (Date.now() - s.last_activity > SESSION_TIMEOUT_MS) {
|
|
67
|
+
return createSession();
|
|
68
|
+
}
|
|
69
|
+
return s;
|
|
70
|
+
} catch {
|
|
71
|
+
return createSession();
|
|
69
72
|
}
|
|
70
|
-
|
|
71
|
-
} catch {
|
|
72
|
-
return createNewSession();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
73
|
+
};
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
return {
|
|
75
|
+
const createSession = () => ({
|
|
78
76
|
session_id: `session_${Date.now()}`,
|
|
79
77
|
started_at: new Date().toISOString(),
|
|
80
78
|
last_activity: Date.now(),
|
|
81
|
-
requests: 0,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
requests: 0, optimizations: 0, cache_hits: 0,
|
|
80
|
+
original_tokens: 0, optimized_tokens: 0, tokens_saved: 0,
|
|
81
|
+
baseline_cost: 0, actual_cost: 0, cost_saved: 0
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const saveSession = (s) => {
|
|
85
|
+
try {
|
|
86
|
+
const dir = path.dirname(SESSION_PATH);
|
|
87
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
s.last_activity = Date.now();
|
|
89
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2));
|
|
90
|
+
} catch {}
|
|
90
91
|
};
|
|
91
|
-
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
session = {
|
|
94
|
+
recordOptimization: (result) => {
|
|
95
|
+
const s = loadSession();
|
|
96
|
+
s.requests++; s.optimizations++;
|
|
97
|
+
s.original_tokens += result.original_tokens || 0;
|
|
98
|
+
s.optimized_tokens += result.optimized_tokens || 0;
|
|
99
|
+
s.tokens_saved += result.tokens_saved || 0;
|
|
100
|
+
s.baseline_cost += result.baseline_cost || 0;
|
|
101
|
+
s.actual_cost += result.estimated_cost || 0;
|
|
102
|
+
s.cost_saved += (result.baseline_cost || 0) - (result.estimated_cost || 0);
|
|
103
|
+
saveSession(s);
|
|
104
|
+
},
|
|
105
|
+
recordCacheHit: () => {
|
|
106
|
+
const s = loadSession(); s.requests++; s.cache_hits++; saveSession(s);
|
|
107
|
+
},
|
|
108
|
+
recordPassthrough: () => {
|
|
109
|
+
const s = loadSession(); s.requests++; saveSession(s);
|
|
98
110
|
}
|
|
99
|
-
|
|
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
|
-
}
|
|
125
|
-
|
|
126
|
-
function recordPassthrough() {
|
|
127
|
-
const session = loadSessionStats();
|
|
128
|
-
session.requests++;
|
|
129
|
-
saveSessionStats(session);
|
|
130
|
-
}
|
|
111
|
+
};
|
|
131
112
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
113
|
+
cache = {
|
|
114
|
+
generateKey: (messages) => {
|
|
115
|
+
const str = JSON.stringify(messages);
|
|
116
|
+
let hash = 0;
|
|
117
|
+
for (let i = 0; i < str.length; i++) {
|
|
118
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
119
|
+
hash |= 0;
|
|
120
|
+
}
|
|
121
|
+
return `cache_${Math.abs(hash)}`;
|
|
122
|
+
},
|
|
123
|
+
get: async () => null,
|
|
124
|
+
setMetadata: async () => {}
|
|
125
|
+
};
|
|
139
126
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
127
|
+
PlexorClient = class {
|
|
128
|
+
constructor(opts) {
|
|
129
|
+
this.apiKey = opts.apiKey;
|
|
130
|
+
this.baseUrl = opts.baseUrl;
|
|
131
|
+
this.timeout = opts.timeout;
|
|
132
|
+
}
|
|
133
|
+
async optimize(params) {
|
|
134
|
+
const tokens = Math.round(JSON.stringify(params.messages).length / 4);
|
|
135
|
+
return {
|
|
136
|
+
request_id: `req_${Date.now()}`,
|
|
137
|
+
original_tokens: tokens,
|
|
138
|
+
optimized_tokens: Math.round(tokens * 0.7),
|
|
139
|
+
tokens_saved: Math.round(tokens * 0.3),
|
|
140
|
+
optimized_messages: params.messages,
|
|
141
|
+
recommended_provider: 'anthropic',
|
|
142
|
+
recommended_model: params.model,
|
|
143
|
+
estimated_cost: tokens * 0.00001,
|
|
144
|
+
baseline_cost: tokens * 0.00003
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
155
148
|
}
|
|
156
149
|
|
|
157
150
|
async function main() {
|
|
@@ -177,7 +170,7 @@ async function main() {
|
|
|
177
170
|
// Must check before isAgenticRequest since all Claude Code requests have tools
|
|
178
171
|
if (isSlashCommand(request)) {
|
|
179
172
|
logger.debug('Slash command detected, passing through unchanged');
|
|
180
|
-
recordPassthrough();
|
|
173
|
+
session.recordPassthrough();
|
|
181
174
|
return output({
|
|
182
175
|
...request,
|
|
183
176
|
plexor_cwd: process.cwd(),
|
|
@@ -194,7 +187,7 @@ async function main() {
|
|
|
194
187
|
// Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
|
|
195
188
|
if (requiresToolExecution(request)) {
|
|
196
189
|
logger.debug('CLI tool execution detected, passing through unchanged');
|
|
197
|
-
recordPassthrough();
|
|
190
|
+
session.recordPassthrough();
|
|
198
191
|
return output({
|
|
199
192
|
...request,
|
|
200
193
|
plexor_cwd: process.cwd(),
|
|
@@ -211,7 +204,7 @@ async function main() {
|
|
|
211
204
|
// Modifying messages breaks the agent loop and causes infinite loops
|
|
212
205
|
if (isAgenticRequest(request)) {
|
|
213
206
|
logger.debug('Agentic request detected, passing through unchanged');
|
|
214
|
-
recordPassthrough();
|
|
207
|
+
session.recordPassthrough();
|
|
215
208
|
return output({
|
|
216
209
|
...request,
|
|
217
210
|
plexor_cwd: process.cwd(),
|
|
@@ -251,7 +244,7 @@ async function main() {
|
|
|
251
244
|
|
|
252
245
|
if (cachedResponse && settings.localCacheEnabled) {
|
|
253
246
|
logger.info('[Plexor] Local cache hit');
|
|
254
|
-
recordCacheHit();
|
|
247
|
+
session.recordCacheHit();
|
|
255
248
|
return output({
|
|
256
249
|
...request,
|
|
257
250
|
_plexor: {
|
|
@@ -311,7 +304,7 @@ async function main() {
|
|
|
311
304
|
});
|
|
312
305
|
|
|
313
306
|
// Update session stats
|
|
314
|
-
|
|
307
|
+
session.recordOptimization(result);
|
|
315
308
|
|
|
316
309
|
return output(optimizedRequest);
|
|
317
310
|
|
package/hooks/track-response.js
CHANGED
|
@@ -10,14 +10,56 @@
|
|
|
10
10
|
* Output: Passthrough (no modifications)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Use lib modules
|
|
16
|
+
let ConfigManager, SessionManager, LocalCache, Logger, PlexorClient;
|
|
17
|
+
try {
|
|
18
|
+
ConfigManager = require('../lib/config');
|
|
19
|
+
SessionManager = require('../lib/session');
|
|
20
|
+
LocalCache = require('../lib/cache');
|
|
21
|
+
Logger = require('../lib/logger');
|
|
22
|
+
PlexorClient = require('../lib/plexor-client');
|
|
23
|
+
} catch {
|
|
24
|
+
// Fallback inline implementations if lib not found
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const CONFIG_PATH = path.join(process.env.HOME || '', '.plexor', 'config.json');
|
|
27
|
+
|
|
28
|
+
ConfigManager = class {
|
|
29
|
+
async load() {
|
|
30
|
+
try {
|
|
31
|
+
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
32
|
+
const cfg = JSON.parse(data);
|
|
33
|
+
return {
|
|
34
|
+
enabled: cfg.settings?.enabled ?? false,
|
|
35
|
+
apiKey: cfg.auth?.api_key
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
return { enabled: false };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
SessionManager = class {
|
|
44
|
+
recordOptimization() {}
|
|
45
|
+
recordPassthrough() {}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
LocalCache = class {
|
|
49
|
+
async getMetadata() { return null; }
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
Logger = class {
|
|
53
|
+
info(msg) { console.error(msg); }
|
|
54
|
+
error(msg) { console.error(`[ERROR] ${msg}`); }
|
|
55
|
+
debug() {}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
17
58
|
|
|
18
59
|
const logger = new Logger('track-response');
|
|
19
60
|
const config = new ConfigManager();
|
|
20
61
|
const cache = new LocalCache();
|
|
62
|
+
const session = new SessionManager();
|
|
21
63
|
|
|
22
64
|
async function main() {
|
|
23
65
|
try {
|
|
@@ -34,34 +76,45 @@ async function main() {
|
|
|
34
76
|
// Check if this response has Plexor metadata
|
|
35
77
|
const plexorMeta = response._plexor;
|
|
36
78
|
if (!plexorMeta || !plexorMeta.request_id) {
|
|
79
|
+
// No Plexor metadata, but still record the request
|
|
80
|
+
session.recordPassthrough();
|
|
37
81
|
return output(response);
|
|
38
82
|
}
|
|
39
83
|
|
|
40
84
|
// Get stored metadata for this request
|
|
41
85
|
const metadata = await cache.getMetadata(plexorMeta.request_id);
|
|
42
|
-
if (!metadata) {
|
|
43
|
-
return output(response);
|
|
44
|
-
}
|
|
45
86
|
|
|
46
87
|
// Calculate output tokens (approximate)
|
|
47
88
|
const outputTokens = estimateTokens(response.content || '');
|
|
48
89
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
90
|
+
// Update session stats with response data
|
|
91
|
+
if (plexorMeta.source === 'plexor_api') {
|
|
92
|
+
session.recordOptimization({
|
|
93
|
+
original_tokens: plexorMeta.original_tokens || 0,
|
|
94
|
+
optimized_tokens: plexorMeta.optimized_tokens || 0,
|
|
95
|
+
tokens_saved: plexorMeta.tokens_saved || 0,
|
|
96
|
+
baseline_cost: plexorMeta.baseline_cost || 0,
|
|
97
|
+
estimated_cost: plexorMeta.estimated_cost || 0
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
logger.info('[Plexor] Response tracked', {
|
|
101
|
+
request_id: plexorMeta.request_id,
|
|
102
|
+
input_tokens: plexorMeta.optimized_tokens,
|
|
103
|
+
output_tokens: outputTokens,
|
|
104
|
+
provider: plexorMeta.recommended_provider
|
|
105
|
+
});
|
|
106
|
+
} else if (plexorMeta.source === 'local_cache') {
|
|
107
|
+
session.recordCacheHit();
|
|
108
|
+
logger.info('[Plexor] Cache hit recorded');
|
|
109
|
+
} else {
|
|
110
|
+
session.recordPassthrough();
|
|
111
|
+
}
|
|
59
112
|
|
|
60
113
|
// Pass through unchanged
|
|
61
114
|
return output(response);
|
|
62
115
|
|
|
63
116
|
} catch (error) {
|
|
64
|
-
logger.error(`
|
|
117
|
+
logger.error(`Tracking error: ${error.message}`);
|
|
65
118
|
|
|
66
119
|
// On any error, pass through unchanged
|
|
67
120
|
try {
|
|
@@ -100,6 +153,7 @@ function output(data) {
|
|
|
100
153
|
}
|
|
101
154
|
|
|
102
155
|
function estimateTokens(text) {
|
|
156
|
+
if (!text) return 0;
|
|
103
157
|
// Approximate: ~4 characters per token
|
|
104
158
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
105
159
|
}
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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));
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = ConfigManager;
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plexor Plugin Constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const PLEXOR_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.plexor');
|
|
8
|
+
const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
|
|
9
|
+
const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
|
|
10
|
+
const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
|
|
11
|
+
|
|
12
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
13
|
+
|
|
14
|
+
const DEFAULT_API_URL = 'https://api.plexor.dev';
|
|
15
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
PLEXOR_DIR,
|
|
19
|
+
CONFIG_PATH,
|
|
20
|
+
SESSION_PATH,
|
|
21
|
+
CACHE_PATH,
|
|
22
|
+
SESSION_TIMEOUT_MS,
|
|
23
|
+
DEFAULT_API_URL,
|
|
24
|
+
DEFAULT_TIMEOUT
|
|
25
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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;
|
|
@@ -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'
|
|
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/session.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
original_tokens: 0,
|
|
42
|
+
optimized_tokens: 0,
|
|
43
|
+
tokens_saved: 0,
|
|
44
|
+
baseline_cost: 0,
|
|
45
|
+
actual_cost: 0,
|
|
46
|
+
cost_saved: 0
|
|
47
|
+
};
|
|
48
|
+
this.save(session);
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
save(session) {
|
|
53
|
+
try {
|
|
54
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
55
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
session.last_activity = Date.now();
|
|
58
|
+
fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2));
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
recordRequest(stats = {}) {
|
|
66
|
+
const session = this.load();
|
|
67
|
+
session.requests++;
|
|
68
|
+
|
|
69
|
+
if (stats.optimized) {
|
|
70
|
+
session.optimizations++;
|
|
71
|
+
session.original_tokens += stats.original_tokens || 0;
|
|
72
|
+
session.optimized_tokens += stats.optimized_tokens || 0;
|
|
73
|
+
session.tokens_saved += stats.tokens_saved || 0;
|
|
74
|
+
session.baseline_cost += stats.baseline_cost || 0;
|
|
75
|
+
session.actual_cost += stats.actual_cost || 0;
|
|
76
|
+
session.cost_saved += stats.cost_saved || 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (stats.cache_hit) {
|
|
80
|
+
session.cache_hits++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.save(session);
|
|
84
|
+
return session;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
recordOptimization(result) {
|
|
88
|
+
return this.recordRequest({
|
|
89
|
+
optimized: true,
|
|
90
|
+
original_tokens: result.original_tokens || 0,
|
|
91
|
+
optimized_tokens: result.optimized_tokens || 0,
|
|
92
|
+
tokens_saved: result.tokens_saved || 0,
|
|
93
|
+
baseline_cost: result.baseline_cost || 0,
|
|
94
|
+
actual_cost: result.estimated_cost || result.actual_cost || 0,
|
|
95
|
+
cost_saved: (result.baseline_cost || 0) - (result.estimated_cost || result.actual_cost || 0)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
recordCacheHit() {
|
|
100
|
+
return this.recordRequest({ cache_hit: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
recordPassthrough() {
|
|
104
|
+
return this.recordRequest({});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getStats() {
|
|
108
|
+
const session = this.load();
|
|
109
|
+
const duration = Date.now() - new Date(session.started_at).getTime();
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...session,
|
|
113
|
+
duration_ms: duration,
|
|
114
|
+
duration_formatted: this.formatDuration(duration),
|
|
115
|
+
tokens_saved_percent: session.original_tokens > 0
|
|
116
|
+
? ((session.tokens_saved / session.original_tokens) * 100).toFixed(1)
|
|
117
|
+
: '0.0',
|
|
118
|
+
cost_saved_percent: session.baseline_cost > 0
|
|
119
|
+
? ((session.cost_saved / session.baseline_cost) * 100).toFixed(1)
|
|
120
|
+
: '0.0'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
formatDuration(ms) {
|
|
125
|
+
const minutes = Math.floor(ms / 60000);
|
|
126
|
+
if (minutes < 60) {
|
|
127
|
+
return `${minutes}m`;
|
|
128
|
+
}
|
|
129
|
+
const hours = Math.floor(minutes / 60);
|
|
130
|
+
const remainingMinutes = minutes % 60;
|
|
131
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
isExpired() {
|
|
135
|
+
try {
|
|
136
|
+
const data = fs.readFileSync(this.sessionPath, 'utf8');
|
|
137
|
+
const session = JSON.parse(data);
|
|
138
|
+
return Date.now() - session.last_activity > SESSION_TIMEOUT_MS;
|
|
139
|
+
} catch {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = SessionManager;
|
package/package.json
CHANGED