@plexor-dev/claude-code-plugin-staging 0.1.0-beta.1

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.
@@ -0,0 +1,634 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Plexor Interception Hook - Phase 3 Hypervisor Architecture
5
+ *
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
17
+ *
18
+ * Input: JSON object with messages, model, max_tokens, etc.
19
+ * Output: Passthrough (hypervisor mode) or optimized messages (plugin mode)
20
+ */
21
+
22
+ const fs = require('fs');
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
+ }
37
+
38
+ // Try to load lib modules, fall back to inline implementations
39
+ let ConfigManager, SessionManager, LocalCache, Logger, PlexorClient;
40
+ let config, session, cache, logger;
41
+
42
+ try {
43
+ ConfigManager = require('../lib/config');
44
+ SessionManager = require('../lib/session');
45
+ LocalCache = require('../lib/cache');
46
+ Logger = require('../lib/logger');
47
+ PlexorClient = require('../lib/plexor-client');
48
+
49
+ config = new ConfigManager();
50
+ session = new SessionManager();
51
+ cache = new LocalCache();
52
+ logger = new Logger('intercept');
53
+ } catch {
54
+ // Fallback inline implementations
55
+ const CONFIG_PATH = path.join(process.env.HOME || '', '.plexor', 'config.json');
56
+ const SESSION_PATH = path.join(process.env.HOME || '', '.plexor', 'session.json');
57
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
58
+
59
+ logger = {
60
+ debug: (msg) => process.env.PLEXOR_DEBUG && console.error(`[DEBUG] ${msg}`),
61
+ info: (msg) => console.error(msg),
62
+ error: (msg) => console.error(`[ERROR] ${msg}`)
63
+ };
64
+
65
+ config = {
66
+ load: async () => {
67
+ try {
68
+ const data = fs.readFileSync(CONFIG_PATH, 'utf8');
69
+ const cfg = JSON.parse(data);
70
+ return {
71
+ enabled: cfg.settings?.enabled ?? false,
72
+ apiKey: cfg.auth?.api_key,
73
+ apiUrl: cfg.settings?.apiUrl || 'https://api.plexor.dev',
74
+ timeout: cfg.settings?.timeout || 5000,
75
+ localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
76
+ mode: cfg.settings?.mode || 'balanced'
77
+ };
78
+ } catch {
79
+ return { enabled: false };
80
+ }
81
+ }
82
+ };
83
+
84
+ const loadSession = () => {
85
+ try {
86
+ const data = fs.readFileSync(SESSION_PATH, 'utf8');
87
+ const s = JSON.parse(data);
88
+ if (Date.now() - s.last_activity > SESSION_TIMEOUT_MS) {
89
+ return createSession();
90
+ }
91
+ return s;
92
+ } catch {
93
+ return createSession();
94
+ }
95
+ };
96
+
97
+ const createSession = () => ({
98
+ session_id: `session_${Date.now()}`,
99
+ started_at: new Date().toISOString(),
100
+ last_activity: Date.now(),
101
+ requests: 0, optimizations: 0, cache_hits: 0,
102
+ original_tokens: 0, optimized_tokens: 0, tokens_saved: 0,
103
+ baseline_cost: 0, actual_cost: 0, cost_saved: 0
104
+ });
105
+
106
+ const saveSession = (s) => {
107
+ try {
108
+ const dir = path.dirname(SESSION_PATH);
109
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
110
+ s.last_activity = Date.now();
111
+ fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2));
112
+ } catch {}
113
+ };
114
+
115
+ session = {
116
+ recordOptimization: (result) => {
117
+ const s = loadSession();
118
+ s.requests++; s.optimizations++;
119
+ s.original_tokens += result.original_tokens || 0;
120
+ s.optimized_tokens += result.optimized_tokens || 0;
121
+ s.tokens_saved += result.tokens_saved || 0;
122
+ s.baseline_cost += result.baseline_cost || 0;
123
+ s.actual_cost += result.estimated_cost || 0;
124
+ s.cost_saved += (result.baseline_cost || 0) - (result.estimated_cost || 0);
125
+ saveSession(s);
126
+ },
127
+ recordCacheHit: () => {
128
+ const s = loadSession(); s.requests++; s.cache_hits++; saveSession(s);
129
+ },
130
+ recordPassthrough: () => {
131
+ const s = loadSession(); s.requests++; saveSession(s);
132
+ }
133
+ };
134
+
135
+ cache = {
136
+ generateKey: (messages) => {
137
+ const str = JSON.stringify(messages);
138
+ let hash = 0;
139
+ for (let i = 0; i < str.length; i++) {
140
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
141
+ hash |= 0;
142
+ }
143
+ return `cache_${Math.abs(hash)}`;
144
+ },
145
+ get: async () => null,
146
+ setMetadata: async () => {}
147
+ };
148
+
149
+ PlexorClient = class {
150
+ constructor(opts) {
151
+ this.apiKey = opts.apiKey;
152
+ this.baseUrl = opts.baseUrl;
153
+ this.timeout = opts.timeout;
154
+ }
155
+ async optimize(params) {
156
+ const tokens = Math.round(JSON.stringify(params.messages).length / 4);
157
+ return {
158
+ request_id: `req_${Date.now()}`,
159
+ original_tokens: tokens,
160
+ optimized_tokens: Math.round(tokens * 0.7),
161
+ tokens_saved: Math.round(tokens * 0.3),
162
+ optimized_messages: params.messages,
163
+ recommended_provider: 'anthropic',
164
+ recommended_model: params.model,
165
+ estimated_cost: tokens * 0.00001,
166
+ baseline_cost: tokens * 0.00003
167
+ };
168
+ }
169
+ };
170
+ }
171
+
172
+ async function main() {
173
+ const startTime = Date.now();
174
+
175
+ let input;
176
+ let request;
177
+
178
+ try {
179
+ input = await readStdin();
180
+ request = JSON.parse(input);
181
+
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
185
+ const baseUrl = process.env.ANTHROPIC_BASE_URL || '';
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
+ });
203
+ }
204
+
205
+ // CRITICAL: Check for slash commands FIRST (before agentic check)
206
+ // Slash commands like /plexor-status should pass through unchanged
207
+ // Must check before isAgenticRequest since all Claude Code requests have tools
208
+ if (isSlashCommand(request)) {
209
+ logger.debug('Slash command detected, passing through unchanged');
210
+ session.recordPassthrough();
211
+ return output({
212
+ ...request,
213
+ plexor_cwd: process.cwd(),
214
+ _plexor: {
215
+ request_id: generateRequestId('slash'), // Issue #701: Add request_id for tracking
216
+ source: 'passthrough_slash_command',
217
+ reason: 'slash_command_detected',
218
+ cwd: process.cwd(),
219
+ latency_ms: Date.now() - startTime
220
+ }
221
+ });
222
+ }
223
+
224
+ // CRITICAL: Skip optimization for CLI commands requiring tool execution
225
+ // Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
226
+ if (requiresToolExecution(request)) {
227
+ logger.debug('CLI tool execution detected, passing through unchanged');
228
+ session.recordPassthrough();
229
+ return output({
230
+ ...request,
231
+ plexor_cwd: process.cwd(),
232
+ _plexor: {
233
+ request_id: generateRequestId('cli'), // Issue #701: Add request_id for tracking
234
+ source: 'passthrough_cli',
235
+ reason: 'cli_tool_execution_detected',
236
+ cwd: process.cwd(),
237
+ latency_ms: Date.now() - startTime
238
+ }
239
+ });
240
+ }
241
+
242
+ // CRITICAL: Skip optimization for agentic/tool-using requests
243
+ // Modifying messages breaks the agent loop and causes infinite loops
244
+ if (isAgenticRequest(request)) {
245
+ logger.debug('Agentic request detected, passing through unchanged');
246
+ session.recordPassthrough();
247
+ return output({
248
+ ...request,
249
+ plexor_cwd: process.cwd(),
250
+ _plexor: {
251
+ request_id: generateRequestId('agent'), // Issue #701: Add request_id for tracking
252
+ source: 'passthrough_agentic',
253
+ reason: 'tool_use_detected',
254
+ cwd: process.cwd(),
255
+ latency_ms: Date.now() - startTime
256
+ }
257
+ });
258
+ }
259
+
260
+ const settings = await config.load();
261
+
262
+ if (!settings.enabled) {
263
+ logger.debug('Plexor disabled, passing through');
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
+ });
274
+ }
275
+
276
+ if (!settings.apiKey) {
277
+ logger.info('Not authenticated. Run /plexor-login to enable optimization.');
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
+ });
288
+ }
289
+
290
+ const client = new PlexorClient({
291
+ apiKey: settings.apiKey,
292
+ baseUrl: settings.apiUrl || 'https://api.plexor.dev',
293
+ timeout: settings.timeout || 5000
294
+ });
295
+
296
+ const messages = extractMessages(request);
297
+ const model = request.model || 'claude-sonnet-4-20250514';
298
+ const maxTokens = request.max_tokens || 4096;
299
+
300
+ const cacheKey = cache.generateKey(messages);
301
+ const cachedResponse = await cache.get(cacheKey);
302
+
303
+ if (cachedResponse && settings.localCacheEnabled) {
304
+ logger.info('[Plexor] Local cache hit');
305
+ session.recordCacheHit();
306
+ return output({
307
+ ...request,
308
+ _plexor: {
309
+ request_id: generateRequestId('cache'), // Issue #701: Add request_id for tracking
310
+ source: 'local_cache',
311
+ latency_ms: Date.now() - startTime
312
+ }
313
+ });
314
+ }
315
+
316
+ logger.debug('Calling Plexor API...');
317
+
318
+ const result = await client.optimize({
319
+ messages: messages,
320
+ model: model,
321
+ max_tokens: maxTokens,
322
+ task_hint: detectTaskType(messages),
323
+ context: {
324
+ session_id: request._session_id,
325
+ turn_number: request._turn_number,
326
+ cwd: process.cwd()
327
+ }
328
+ });
329
+
330
+ const savingsPercent = ((result.original_tokens - result.optimized_tokens) / result.original_tokens * 100).toFixed(1);
331
+
332
+ logger.info(`[Plexor] Optimized: ${result.original_tokens} → ${result.optimized_tokens} tokens (${savingsPercent}% saved)`);
333
+
334
+ if (result.recommended_provider !== 'anthropic') {
335
+ logger.info(`[Plexor] Recommended: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`);
336
+ }
337
+
338
+ const optimizedRequest = {
339
+ ...request,
340
+ messages: result.optimized_messages,
341
+ plexor_cwd: process.cwd(),
342
+ _plexor: {
343
+ request_id: result.request_id,
344
+ original_tokens: result.original_tokens,
345
+ optimized_tokens: result.optimized_tokens,
346
+ tokens_saved: result.tokens_saved,
347
+ savings_percent: parseFloat(savingsPercent),
348
+ recommended_provider: result.recommended_provider,
349
+ recommended_model: result.recommended_model,
350
+ estimated_cost: result.estimated_cost,
351
+ baseline_cost: result.baseline_cost,
352
+ latency_ms: Date.now() - startTime,
353
+ source: 'plexor_api',
354
+ cwd: process.cwd()
355
+ }
356
+ };
357
+
358
+ await cache.setMetadata(result.request_id, {
359
+ original_tokens: result.original_tokens,
360
+ optimized_tokens: result.optimized_tokens,
361
+ recommended_provider: result.recommended_provider,
362
+ timestamp: Date.now()
363
+ });
364
+
365
+ // Update session stats
366
+ session.recordOptimization(result);
367
+
368
+ return output(optimizedRequest);
369
+
370
+ } catch (error) {
371
+ logger.error(`[Plexor] Error: ${error.message}`);
372
+ logger.debug(error.stack);
373
+
374
+ const errorRequestId = generateRequestId('error'); // Issue #701: Add request_id for tracking
375
+
376
+ // Use already-parsed request if available, otherwise pass through raw
377
+ if (request) {
378
+ session.recordPassthrough();
379
+ return output({
380
+ ...request,
381
+ _plexor: {
382
+ request_id: errorRequestId,
383
+ error: error.message,
384
+ source: 'passthrough_error'
385
+ }
386
+ });
387
+ } else if (input) {
388
+ // Try to parse the input we already read
389
+ try {
390
+ const req = JSON.parse(input);
391
+ session.recordPassthrough();
392
+ return output({
393
+ ...req,
394
+ _plexor: {
395
+ request_id: errorRequestId,
396
+ error: error.message,
397
+ source: 'passthrough_error'
398
+ }
399
+ });
400
+ } catch {
401
+ process.exit(1);
402
+ }
403
+ } else {
404
+ process.exit(1);
405
+ }
406
+ }
407
+ }
408
+
409
+ async function readStdin() {
410
+ return new Promise((resolve, reject) => {
411
+ const chunks = [];
412
+
413
+ process.stdin.on('data', (chunk) => {
414
+ chunks.push(chunk);
415
+ });
416
+
417
+ process.stdin.on('end', () => {
418
+ resolve(Buffer.concat(chunks).toString('utf8'));
419
+ });
420
+
421
+ process.stdin.on('error', reject);
422
+
423
+ setTimeout(() => {
424
+ reject(new Error('Stdin read timeout'));
425
+ }, 5000);
426
+ });
427
+ }
428
+
429
+ function output(data) {
430
+ const json = JSON.stringify(data);
431
+ process.stdout.write(json);
432
+ process.exit(0);
433
+ }
434
+
435
+ function extractMessages(request) {
436
+ if (Array.isArray(request.messages)) {
437
+ return request.messages;
438
+ }
439
+
440
+ if (request.prompt) {
441
+ return [{ role: 'user', content: request.prompt }];
442
+ }
443
+
444
+ if (request.system && request.user) {
445
+ return [
446
+ { role: 'system', content: request.system },
447
+ { role: 'user', content: request.user }
448
+ ];
449
+ }
450
+
451
+ return [];
452
+ }
453
+
454
+ function detectTaskType(messages) {
455
+ if (!messages || messages.length === 0) {
456
+ return 'general';
457
+ }
458
+
459
+ const lastUserMessage = [...messages]
460
+ .reverse()
461
+ .find(m => m.role === 'user');
462
+
463
+ if (!lastUserMessage) {
464
+ return 'general';
465
+ }
466
+
467
+ const content = lastUserMessage.content.toLowerCase();
468
+
469
+ if (/```|function|class|import|export|const |let |var |def |async |await/.test(content)) {
470
+ return 'code_generation';
471
+ }
472
+
473
+ if (/test|spec|jest|pytest|unittest|describe\(|it\(|expect\(/.test(content)) {
474
+ return 'test_generation';
475
+ }
476
+
477
+ if (/fix|bug|error|issue|debug|trace|exception|crash/.test(content)) {
478
+ return 'debugging';
479
+ }
480
+
481
+ if (/refactor|improve|optimize|clean|restructure/.test(content)) {
482
+ return 'refactoring';
483
+ }
484
+
485
+ if (/document|readme|comment|explain|docstring/.test(content)) {
486
+ return 'documentation';
487
+ }
488
+
489
+ if (/review|check|audit|assess|evaluate/.test(content)) {
490
+ return 'code_review';
491
+ }
492
+
493
+ if (/analyze|understand|what does|how does|explain/.test(content)) {
494
+ return 'analysis';
495
+ }
496
+
497
+ return 'general';
498
+ }
499
+
500
+ /**
501
+ * Detect if this is an agentic/tool-using request that should not be optimized.
502
+ * Modifying messages in agent loops breaks the loop detection and causes infinite loops.
503
+ */
504
+ function isAgenticRequest(request) {
505
+ // Check if request has tools defined
506
+ if (request.tools && request.tools.length > 0) {
507
+ return true;
508
+ }
509
+
510
+ // Check if any message contains tool use or tool results
511
+ const messages = request.messages || [];
512
+ for (const msg of messages) {
513
+ // Tool use in content (Claude format)
514
+ if (msg.content && Array.isArray(msg.content)) {
515
+ for (const block of msg.content) {
516
+ if (block.type === 'tool_use' || block.type === 'tool_result') {
517
+ return true;
518
+ }
519
+ }
520
+ }
521
+
522
+ // Tool role (OpenAI format)
523
+ if (msg.role === 'tool') {
524
+ return true;
525
+ }
526
+
527
+ // Function call (OpenAI format)
528
+ if (msg.function_call || msg.tool_calls) {
529
+ return true;
530
+ }
531
+ }
532
+
533
+ // Check for assistant messages with tool indicators
534
+ for (const msg of messages) {
535
+ if (msg.role === 'assistant' && msg.content) {
536
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
537
+ // Detect common tool use patterns in Claude Code
538
+ if (/\[Bash\]|\[Read\]|\[Write\]|\[Edit\]|\[Glob\]|\[Grep\]/.test(content)) {
539
+ return true;
540
+ }
541
+ }
542
+ }
543
+
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
548
+
549
+ return false;
550
+ }
551
+
552
+ /**
553
+ * Detect if this is a slash command request that should not be optimized.
554
+ * Slash commands like /plexor-status need to pass through unchanged.
555
+ */
556
+ function isSlashCommand(request) {
557
+ const messages = request.messages || [];
558
+
559
+ // Check the last user message for slash command patterns
560
+ for (let i = messages.length - 1; i >= 0; i--) {
561
+ const msg = messages[i];
562
+ if (msg.role === 'user') {
563
+ const content = typeof msg.content === 'string' ? msg.content : '';
564
+ // Detect slash commands at the start of user message
565
+ if (/^\/[a-z-]+/i.test(content.trim())) {
566
+ return true;
567
+ }
568
+ // Detect <command-name> tags (Claude Code skill invocation)
569
+ if (/<command-name>/.test(content)) {
570
+ return true;
571
+ }
572
+ // Detect plexor-related commands
573
+ if (/plexor-(?:status|login|logout|mode|provider|enabled|settings)/i.test(content)) {
574
+ return true;
575
+ }
576
+ break; // Only check last user message
577
+ }
578
+ }
579
+
580
+ // Check for system messages with skill instructions
581
+ for (const msg of messages) {
582
+ if (msg.role === 'system') {
583
+ const content = typeof msg.content === 'string' ? msg.content : '';
584
+ if (/# Plexor (?:Status|Login|Logout|Mode|Provider|Enabled|Settings)/i.test(content)) {
585
+ return true;
586
+ }
587
+ }
588
+ }
589
+
590
+ return false;
591
+ }
592
+
593
+ /**
594
+ * Detect if this request involves CLI/shell commands that need tool execution.
595
+ * These should pass through to ensure proper tool calling behavior.
596
+ */
597
+ function requiresToolExecution(request) {
598
+ const messages = request.messages || [];
599
+
600
+ // Check user messages for CLI command patterns
601
+ for (const msg of messages) {
602
+ if (msg.role === 'user') {
603
+ const content = typeof msg.content === 'string' ? msg.content : '';
604
+ const contentLower = content.toLowerCase();
605
+
606
+ // Azure CLI patterns
607
+ if (/\baz\s+(login|logout|group|account|vm|storage|webapp|aks|acr|keyvault|sql|cosmos|network)/i.test(content)) {
608
+ return true;
609
+ }
610
+
611
+ // Common CLI execution requests
612
+ if (/\b(run|execute|show|list|create|delete|update)\b.*\b(az|aws|gcloud|kubectl|docker|npm|git)\b/i.test(content)) {
613
+ return true;
614
+ }
615
+
616
+ // Direct command patterns
617
+ if (/^(az|aws|gcloud|kubectl|docker|npm|yarn|pip|cargo|go)\s+/m.test(content)) {
618
+ return true;
619
+ }
620
+
621
+ // Imperative CLI requests
622
+ if (/\b(list|show|get|describe)\s+(resource\s*groups?|rgs?|vms?|instances?|clusters?|pods?|containers?)/i.test(content)) {
623
+ return true;
624
+ }
625
+ }
626
+ }
627
+
628
+ return false;
629
+ }
630
+
631
+ main().catch((error) => {
632
+ console.error(`[Plexor] Fatal error: ${error.message}`);
633
+ process.exit(1);
634
+ });