@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.
@@ -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
- // 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');
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
- 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
- };
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
- 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;
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
- 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
73
+ };
61
74
 
62
- function loadSessionStats() {
63
- try {
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();
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
- return session;
71
- } catch {
72
- return createNewSession();
73
- }
74
- }
86
+ };
75
87
 
76
- function createNewSession() {
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
- 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
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
- function saveSessionStats(session) {
94
- try {
95
- const dir = path.dirname(SESSION_PATH);
96
- if (!fs.existsSync(dir)) {
97
- fs.mkdirSync(dir, { recursive: true });
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
- 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
- }
125
-
126
- function recordPassthrough() {
127
- const session = loadSessionStats();
128
- session.requests++;
129
- saveSessionStats(session);
130
- }
124
+ };
131
125
 
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
- }
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
- 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
- }
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
- return output(request);
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
- return output(request);
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
- updateSessionStats(result);
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
- // Check for multi-turn conversations (likely agentic)
487
- const assistantMessages = messages.filter(m => m.role === 'assistant');
488
- if (assistantMessages.length > 2) {
489
- return true;
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
  }