@plexor-dev/claude-code-plugin 0.1.0-beta.15 → 0.1.0-beta.17
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/hooks/intercept.js +172 -137
- package/hooks/track-response.js +309 -43
- 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/server-sync.js +237 -0
- package/lib/session.js +156 -0
- package/package.json +1 -1
package/hooks/intercept.js
CHANGED
|
@@ -12,146 +12,152 @@
|
|
|
12
12
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Generate a unique request ID for tracking.
|
|
19
|
+
* Format: pass_<timestamp>_<random> for passthrough requests
|
|
20
|
+
* This ensures every request can be tracked, even passthroughs.
|
|
21
|
+
* Issue #701: Missing request_id caused response tracking to fail.
|
|
22
|
+
*/
|
|
23
|
+
function generateRequestId(prefix = 'pass') {
|
|
24
|
+
const timestamp = Date.now().toString(36);
|
|
25
|
+
const random = crypto.randomBytes(4).toString('hex');
|
|
26
|
+
return `${prefix}_${timestamp}_${random}`;
|
|
27
|
+
}
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
// Try to load lib modules, fall back to inline implementations
|
|
30
|
+
let ConfigManager, SessionManager, LocalCache, Logger, PlexorClient;
|
|
31
|
+
let config, session, cache, logger;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
ConfigManager = require('../lib/config');
|
|
35
|
+
SessionManager = require('../lib/session');
|
|
36
|
+
LocalCache = require('../lib/cache');
|
|
37
|
+
Logger = require('../lib/logger');
|
|
38
|
+
PlexorClient = require('../lib/plexor-client');
|
|
39
|
+
|
|
40
|
+
config = new ConfigManager();
|
|
41
|
+
session = new SessionManager();
|
|
42
|
+
cache = new LocalCache();
|
|
43
|
+
logger = new Logger('intercept');
|
|
44
|
+
} catch {
|
|
45
|
+
// Fallback inline implementations
|
|
46
|
+
const CONFIG_PATH = path.join(process.env.HOME || '', '.plexor', 'config.json');
|
|
47
|
+
const SESSION_PATH = path.join(process.env.HOME || '', '.plexor', 'session.json');
|
|
48
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
49
|
+
|
|
50
|
+
logger = {
|
|
51
|
+
debug: (msg) => process.env.PLEXOR_DEBUG && console.error(`[DEBUG] ${msg}`),
|
|
52
|
+
info: (msg) => console.error(msg),
|
|
53
|
+
error: (msg) => console.error(`[ERROR] ${msg}`)
|
|
54
|
+
};
|
|
25
55
|
|
|
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;
|
|
56
|
+
config = {
|
|
57
|
+
load: async () => {
|
|
58
|
+
try {
|
|
59
|
+
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
60
|
+
const cfg = JSON.parse(data);
|
|
61
|
+
return {
|
|
62
|
+
enabled: cfg.settings?.enabled ?? false,
|
|
63
|
+
apiKey: cfg.auth?.api_key,
|
|
64
|
+
apiUrl: cfg.settings?.apiUrl || 'https://api.plexor.dev',
|
|
65
|
+
timeout: cfg.settings?.timeout || 5000,
|
|
66
|
+
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
67
|
+
mode: cfg.settings?.mode || 'balanced'
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return { enabled: false };
|
|
71
|
+
}
|
|
52
72
|
}
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
get: async () => null,
|
|
56
|
-
setMetadata: async () => {}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Session stats tracking
|
|
60
|
-
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
73
|
+
};
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
const loadSession = () => {
|
|
76
|
+
try {
|
|
77
|
+
const data = fs.readFileSync(SESSION_PATH, 'utf8');
|
|
78
|
+
const s = JSON.parse(data);
|
|
79
|
+
if (Date.now() - s.last_activity > SESSION_TIMEOUT_MS) {
|
|
80
|
+
return createSession();
|
|
81
|
+
}
|
|
82
|
+
return s;
|
|
83
|
+
} catch {
|
|
84
|
+
return createSession();
|
|
69
85
|
}
|
|
70
|
-
|
|
71
|
-
} catch {
|
|
72
|
-
return createNewSession();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
86
|
+
};
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
return {
|
|
88
|
+
const createSession = () => ({
|
|
78
89
|
session_id: `session_${Date.now()}`,
|
|
79
90
|
started_at: new Date().toISOString(),
|
|
80
91
|
last_activity: Date.now(),
|
|
81
|
-
requests: 0,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
requests: 0, optimizations: 0, cache_hits: 0,
|
|
93
|
+
original_tokens: 0, optimized_tokens: 0, tokens_saved: 0,
|
|
94
|
+
baseline_cost: 0, actual_cost: 0, cost_saved: 0
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const saveSession = (s) => {
|
|
98
|
+
try {
|
|
99
|
+
const dir = path.dirname(SESSION_PATH);
|
|
100
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
s.last_activity = Date.now();
|
|
102
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2));
|
|
103
|
+
} catch {}
|
|
90
104
|
};
|
|
91
|
-
}
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
session = {
|
|
107
|
+
recordOptimization: (result) => {
|
|
108
|
+
const s = loadSession();
|
|
109
|
+
s.requests++; s.optimizations++;
|
|
110
|
+
s.original_tokens += result.original_tokens || 0;
|
|
111
|
+
s.optimized_tokens += result.optimized_tokens || 0;
|
|
112
|
+
s.tokens_saved += result.tokens_saved || 0;
|
|
113
|
+
s.baseline_cost += result.baseline_cost || 0;
|
|
114
|
+
s.actual_cost += result.estimated_cost || 0;
|
|
115
|
+
s.cost_saved += (result.baseline_cost || 0) - (result.estimated_cost || 0);
|
|
116
|
+
saveSession(s);
|
|
117
|
+
},
|
|
118
|
+
recordCacheHit: () => {
|
|
119
|
+
const s = loadSession(); s.requests++; s.cache_hits++; saveSession(s);
|
|
120
|
+
},
|
|
121
|
+
recordPassthrough: () => {
|
|
122
|
+
const s = loadSession(); s.requests++; saveSession(s);
|
|
98
123
|
}
|
|
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
|
-
}
|
|
124
|
+
};
|
|
131
125
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
cache = {
|
|
127
|
+
generateKey: (messages) => {
|
|
128
|
+
const str = JSON.stringify(messages);
|
|
129
|
+
let hash = 0;
|
|
130
|
+
for (let i = 0; i < str.length; i++) {
|
|
131
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
132
|
+
hash |= 0;
|
|
133
|
+
}
|
|
134
|
+
return `cache_${Math.abs(hash)}`;
|
|
135
|
+
},
|
|
136
|
+
get: async () => null,
|
|
137
|
+
setMetadata: async () => {}
|
|
138
|
+
};
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
PlexorClient = class {
|
|
141
|
+
constructor(opts) {
|
|
142
|
+
this.apiKey = opts.apiKey;
|
|
143
|
+
this.baseUrl = opts.baseUrl;
|
|
144
|
+
this.timeout = opts.timeout;
|
|
145
|
+
}
|
|
146
|
+
async optimize(params) {
|
|
147
|
+
const tokens = Math.round(JSON.stringify(params.messages).length / 4);
|
|
148
|
+
return {
|
|
149
|
+
request_id: `req_${Date.now()}`,
|
|
150
|
+
original_tokens: tokens,
|
|
151
|
+
optimized_tokens: Math.round(tokens * 0.7),
|
|
152
|
+
tokens_saved: Math.round(tokens * 0.3),
|
|
153
|
+
optimized_messages: params.messages,
|
|
154
|
+
recommended_provider: 'anthropic',
|
|
155
|
+
recommended_model: params.model,
|
|
156
|
+
estimated_cost: tokens * 0.00001,
|
|
157
|
+
baseline_cost: tokens * 0.00003
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
async function main() {
|
|
@@ -169,6 +175,8 @@ async function main() {
|
|
|
169
175
|
const baseUrl = process.env.ANTHROPIC_BASE_URL || '';
|
|
170
176
|
if (baseUrl.includes('plexor')) {
|
|
171
177
|
logger.debug('Plexor proxy detected via ANTHROPIC_BASE_URL, passing through');
|
|
178
|
+
// Note: Don't add _plexor metadata here - the proxy will add its own
|
|
179
|
+
// This passthrough is transparent to avoid double-processing
|
|
172
180
|
return output(request);
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -177,11 +185,12 @@ async function main() {
|
|
|
177
185
|
// Must check before isAgenticRequest since all Claude Code requests have tools
|
|
178
186
|
if (isSlashCommand(request)) {
|
|
179
187
|
logger.debug('Slash command detected, passing through unchanged');
|
|
180
|
-
recordPassthrough();
|
|
188
|
+
session.recordPassthrough();
|
|
181
189
|
return output({
|
|
182
190
|
...request,
|
|
183
191
|
plexor_cwd: process.cwd(),
|
|
184
192
|
_plexor: {
|
|
193
|
+
request_id: generateRequestId('slash'), // Issue #701: Add request_id for tracking
|
|
185
194
|
source: 'passthrough_slash_command',
|
|
186
195
|
reason: 'slash_command_detected',
|
|
187
196
|
cwd: process.cwd(),
|
|
@@ -194,11 +203,12 @@ async function main() {
|
|
|
194
203
|
// Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
|
|
195
204
|
if (requiresToolExecution(request)) {
|
|
196
205
|
logger.debug('CLI tool execution detected, passing through unchanged');
|
|
197
|
-
recordPassthrough();
|
|
206
|
+
session.recordPassthrough();
|
|
198
207
|
return output({
|
|
199
208
|
...request,
|
|
200
209
|
plexor_cwd: process.cwd(),
|
|
201
210
|
_plexor: {
|
|
211
|
+
request_id: generateRequestId('cli'), // Issue #701: Add request_id for tracking
|
|
202
212
|
source: 'passthrough_cli',
|
|
203
213
|
reason: 'cli_tool_execution_detected',
|
|
204
214
|
cwd: process.cwd(),
|
|
@@ -211,11 +221,12 @@ async function main() {
|
|
|
211
221
|
// Modifying messages breaks the agent loop and causes infinite loops
|
|
212
222
|
if (isAgenticRequest(request)) {
|
|
213
223
|
logger.debug('Agentic request detected, passing through unchanged');
|
|
214
|
-
recordPassthrough();
|
|
224
|
+
session.recordPassthrough();
|
|
215
225
|
return output({
|
|
216
226
|
...request,
|
|
217
227
|
plexor_cwd: process.cwd(),
|
|
218
228
|
_plexor: {
|
|
229
|
+
request_id: generateRequestId('agent'), // Issue #701: Add request_id for tracking
|
|
219
230
|
source: 'passthrough_agentic',
|
|
220
231
|
reason: 'tool_use_detected',
|
|
221
232
|
cwd: process.cwd(),
|
|
@@ -228,12 +239,30 @@ async function main() {
|
|
|
228
239
|
|
|
229
240
|
if (!settings.enabled) {
|
|
230
241
|
logger.debug('Plexor disabled, passing through');
|
|
231
|
-
|
|
242
|
+
session.recordPassthrough();
|
|
243
|
+
return output({
|
|
244
|
+
...request,
|
|
245
|
+
_plexor: {
|
|
246
|
+
request_id: generateRequestId('disabled'), // Issue #701: Add request_id for tracking
|
|
247
|
+
source: 'passthrough_disabled',
|
|
248
|
+
reason: 'plexor_disabled',
|
|
249
|
+
latency_ms: Date.now() - startTime
|
|
250
|
+
}
|
|
251
|
+
});
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
if (!settings.apiKey) {
|
|
235
255
|
logger.info('Not authenticated. Run /plexor-login to enable optimization.');
|
|
236
|
-
|
|
256
|
+
session.recordPassthrough();
|
|
257
|
+
return output({
|
|
258
|
+
...request,
|
|
259
|
+
_plexor: {
|
|
260
|
+
request_id: generateRequestId('noauth'), // Issue #701: Add request_id for tracking
|
|
261
|
+
source: 'passthrough_no_auth',
|
|
262
|
+
reason: 'not_authenticated',
|
|
263
|
+
latency_ms: Date.now() - startTime
|
|
264
|
+
}
|
|
265
|
+
});
|
|
237
266
|
}
|
|
238
267
|
|
|
239
268
|
const client = new PlexorClient({
|
|
@@ -251,10 +280,11 @@ async function main() {
|
|
|
251
280
|
|
|
252
281
|
if (cachedResponse && settings.localCacheEnabled) {
|
|
253
282
|
logger.info('[Plexor] Local cache hit');
|
|
254
|
-
recordCacheHit();
|
|
283
|
+
session.recordCacheHit();
|
|
255
284
|
return output({
|
|
256
285
|
...request,
|
|
257
286
|
_plexor: {
|
|
287
|
+
request_id: generateRequestId('cache'), // Issue #701: Add request_id for tracking
|
|
258
288
|
source: 'local_cache',
|
|
259
289
|
latency_ms: Date.now() - startTime
|
|
260
290
|
}
|
|
@@ -311,7 +341,7 @@ async function main() {
|
|
|
311
341
|
});
|
|
312
342
|
|
|
313
343
|
// Update session stats
|
|
314
|
-
|
|
344
|
+
session.recordOptimization(result);
|
|
315
345
|
|
|
316
346
|
return output(optimizedRequest);
|
|
317
347
|
|
|
@@ -319,11 +349,15 @@ async function main() {
|
|
|
319
349
|
logger.error(`[Plexor] Error: ${error.message}`);
|
|
320
350
|
logger.debug(error.stack);
|
|
321
351
|
|
|
352
|
+
const errorRequestId = generateRequestId('error'); // Issue #701: Add request_id for tracking
|
|
353
|
+
|
|
322
354
|
// Use already-parsed request if available, otherwise pass through raw
|
|
323
355
|
if (request) {
|
|
356
|
+
session.recordPassthrough();
|
|
324
357
|
return output({
|
|
325
358
|
...request,
|
|
326
359
|
_plexor: {
|
|
360
|
+
request_id: errorRequestId,
|
|
327
361
|
error: error.message,
|
|
328
362
|
source: 'passthrough_error'
|
|
329
363
|
}
|
|
@@ -332,9 +366,11 @@ async function main() {
|
|
|
332
366
|
// Try to parse the input we already read
|
|
333
367
|
try {
|
|
334
368
|
const req = JSON.parse(input);
|
|
369
|
+
session.recordPassthrough();
|
|
335
370
|
return output({
|
|
336
371
|
...req,
|
|
337
372
|
_plexor: {
|
|
373
|
+
request_id: errorRequestId,
|
|
338
374
|
error: error.message,
|
|
339
375
|
source: 'passthrough_error'
|
|
340
376
|
}
|
|
@@ -483,11 +519,10 @@ function isAgenticRequest(request) {
|
|
|
483
519
|
}
|
|
484
520
|
}
|
|
485
521
|
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
522
|
+
// NOTE: Removed multi-turn check (Issue #701)
|
|
523
|
+
// Multi-turn conversations WITHOUT tool use are NOT agentic
|
|
524
|
+
// The old check caused normal conversations to be marked as agentic
|
|
525
|
+
// after just 2 assistant messages, leading to infinite loops
|
|
491
526
|
|
|
492
527
|
return false;
|
|
493
528
|
}
|