@plexor-dev/claude-code-plugin 0.1.0-beta.3 → 0.1.0-beta.4
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 +299 -0
- package/hooks/track-response.js +110 -0
- package/package.json +2 -1
- package/lib/constants.js +0 -40
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plexor Interception Hook
|
|
5
|
+
*
|
|
6
|
+
* This script intercepts Claude Code prompts before they are sent to the LLM.
|
|
7
|
+
* It optimizes the prompt and optionally routes to a cheaper provider.
|
|
8
|
+
*
|
|
9
|
+
* Input: JSON object with messages, model, max_tokens, etc.
|
|
10
|
+
* Output: Modified JSON object with optimized messages
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PlexorClient = require('../lib/plexor-client');
|
|
14
|
+
const ConfigManager = require('../lib/config');
|
|
15
|
+
const LocalCache = require('../lib/cache');
|
|
16
|
+
const Logger = require('../lib/logger');
|
|
17
|
+
|
|
18
|
+
const logger = new Logger('intercept');
|
|
19
|
+
const config = new ConfigManager();
|
|
20
|
+
const cache = new LocalCache();
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const input = await readStdin();
|
|
27
|
+
const request = JSON.parse(input);
|
|
28
|
+
|
|
29
|
+
// CRITICAL: Skip optimization for agentic/tool-using requests
|
|
30
|
+
// Modifying messages breaks the agent loop and causes infinite loops
|
|
31
|
+
if (isAgenticRequest(request)) {
|
|
32
|
+
logger.debug('Agentic request detected, passing through unchanged');
|
|
33
|
+
return output({
|
|
34
|
+
...request,
|
|
35
|
+
plexor_cwd: process.cwd(),
|
|
36
|
+
_plexor: {
|
|
37
|
+
source: 'passthrough_agentic',
|
|
38
|
+
reason: 'tool_use_detected',
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
latency_ms: Date.now() - startTime
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const settings = await config.load();
|
|
46
|
+
|
|
47
|
+
if (!settings.enabled) {
|
|
48
|
+
logger.debug('Plexor disabled, passing through');
|
|
49
|
+
return output(request);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!settings.apiKey) {
|
|
53
|
+
logger.info('Not authenticated. Run /plexor-login to enable optimization.');
|
|
54
|
+
return output(request);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const client = new PlexorClient({
|
|
58
|
+
apiKey: settings.apiKey,
|
|
59
|
+
baseUrl: settings.apiUrl || 'https://api.plexor.dev',
|
|
60
|
+
timeout: settings.timeout || 5000
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const messages = extractMessages(request);
|
|
64
|
+
const model = request.model || 'claude-sonnet-4-20250514';
|
|
65
|
+
const maxTokens = request.max_tokens || 4096;
|
|
66
|
+
|
|
67
|
+
const cacheKey = cache.generateKey(messages);
|
|
68
|
+
const cachedResponse = await cache.get(cacheKey);
|
|
69
|
+
|
|
70
|
+
if (cachedResponse && settings.localCacheEnabled) {
|
|
71
|
+
logger.info('[Plexor] Local cache hit');
|
|
72
|
+
return output({
|
|
73
|
+
...request,
|
|
74
|
+
_plexor: {
|
|
75
|
+
source: 'local_cache',
|
|
76
|
+
latency_ms: Date.now() - startTime
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logger.debug('Calling Plexor API...');
|
|
82
|
+
|
|
83
|
+
const result = await client.optimize({
|
|
84
|
+
messages: messages,
|
|
85
|
+
model: model,
|
|
86
|
+
max_tokens: maxTokens,
|
|
87
|
+
task_hint: detectTaskType(messages),
|
|
88
|
+
context: {
|
|
89
|
+
session_id: request._session_id,
|
|
90
|
+
turn_number: request._turn_number,
|
|
91
|
+
cwd: process.cwd()
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const savingsPercent = ((result.original_tokens - result.optimized_tokens) / result.original_tokens * 100).toFixed(1);
|
|
96
|
+
|
|
97
|
+
logger.info(`[Plexor] Optimized: ${result.original_tokens} → ${result.optimized_tokens} tokens (${savingsPercent}% saved)`);
|
|
98
|
+
|
|
99
|
+
if (result.recommended_provider !== 'anthropic') {
|
|
100
|
+
logger.info(`[Plexor] Recommended: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const optimizedRequest = {
|
|
104
|
+
...request,
|
|
105
|
+
messages: result.optimized_messages,
|
|
106
|
+
plexor_cwd: process.cwd(),
|
|
107
|
+
_plexor: {
|
|
108
|
+
request_id: result.request_id,
|
|
109
|
+
original_tokens: result.original_tokens,
|
|
110
|
+
optimized_tokens: result.optimized_tokens,
|
|
111
|
+
tokens_saved: result.tokens_saved,
|
|
112
|
+
savings_percent: parseFloat(savingsPercent),
|
|
113
|
+
recommended_provider: result.recommended_provider,
|
|
114
|
+
recommended_model: result.recommended_model,
|
|
115
|
+
estimated_cost: result.estimated_cost,
|
|
116
|
+
baseline_cost: result.baseline_cost,
|
|
117
|
+
latency_ms: Date.now() - startTime,
|
|
118
|
+
source: 'plexor_api',
|
|
119
|
+
cwd: process.cwd()
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await cache.setMetadata(result.request_id, {
|
|
124
|
+
original_tokens: result.original_tokens,
|
|
125
|
+
optimized_tokens: result.optimized_tokens,
|
|
126
|
+
recommended_provider: result.recommended_provider,
|
|
127
|
+
timestamp: Date.now()
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return output(optimizedRequest);
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logger.error(`[Plexor] Error: ${error.message}`);
|
|
134
|
+
logger.debug(error.stack);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const input = await readStdin();
|
|
138
|
+
const request = JSON.parse(input);
|
|
139
|
+
return output({
|
|
140
|
+
...request,
|
|
141
|
+
_plexor: {
|
|
142
|
+
error: error.message,
|
|
143
|
+
source: 'passthrough'
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function readStdin() {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const chunks = [];
|
|
155
|
+
|
|
156
|
+
process.stdin.on('data', (chunk) => {
|
|
157
|
+
chunks.push(chunk);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
process.stdin.on('end', () => {
|
|
161
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
process.stdin.on('error', reject);
|
|
165
|
+
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
reject(new Error('Stdin read timeout'));
|
|
168
|
+
}, 5000);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function output(data) {
|
|
173
|
+
const json = JSON.stringify(data);
|
|
174
|
+
process.stdout.write(json);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function extractMessages(request) {
|
|
179
|
+
if (Array.isArray(request.messages)) {
|
|
180
|
+
return request.messages;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (request.prompt) {
|
|
184
|
+
return [{ role: 'user', content: request.prompt }];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (request.system && request.user) {
|
|
188
|
+
return [
|
|
189
|
+
{ role: 'system', content: request.system },
|
|
190
|
+
{ role: 'user', content: request.user }
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function detectTaskType(messages) {
|
|
198
|
+
if (!messages || messages.length === 0) {
|
|
199
|
+
return 'general';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const lastUserMessage = [...messages]
|
|
203
|
+
.reverse()
|
|
204
|
+
.find(m => m.role === 'user');
|
|
205
|
+
|
|
206
|
+
if (!lastUserMessage) {
|
|
207
|
+
return 'general';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const content = lastUserMessage.content.toLowerCase();
|
|
211
|
+
|
|
212
|
+
if (/```|function|class|import|export|const |let |var |def |async |await/.test(content)) {
|
|
213
|
+
return 'code_generation';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (/test|spec|jest|pytest|unittest|describe\(|it\(|expect\(/.test(content)) {
|
|
217
|
+
return 'test_generation';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (/fix|bug|error|issue|debug|trace|exception|crash/.test(content)) {
|
|
221
|
+
return 'debugging';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (/refactor|improve|optimize|clean|restructure/.test(content)) {
|
|
225
|
+
return 'refactoring';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (/document|readme|comment|explain|docstring/.test(content)) {
|
|
229
|
+
return 'documentation';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (/review|check|audit|assess|evaluate/.test(content)) {
|
|
233
|
+
return 'code_review';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (/analyze|understand|what does|how does|explain/.test(content)) {
|
|
237
|
+
return 'analysis';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return 'general';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Detect if this is an agentic/tool-using request that should not be optimized.
|
|
245
|
+
* Modifying messages in agent loops breaks the loop detection and causes infinite loops.
|
|
246
|
+
*/
|
|
247
|
+
function isAgenticRequest(request) {
|
|
248
|
+
// Check if request has tools defined
|
|
249
|
+
if (request.tools && request.tools.length > 0) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check if any message contains tool use or tool results
|
|
254
|
+
const messages = request.messages || [];
|
|
255
|
+
for (const msg of messages) {
|
|
256
|
+
// Tool use in content (Claude format)
|
|
257
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
258
|
+
for (const block of msg.content) {
|
|
259
|
+
if (block.type === 'tool_use' || block.type === 'tool_result') {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Tool role (OpenAI format)
|
|
266
|
+
if (msg.role === 'tool') {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Function call (OpenAI format)
|
|
271
|
+
if (msg.function_call || msg.tool_calls) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check for assistant messages with tool indicators
|
|
277
|
+
for (const msg of messages) {
|
|
278
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
279
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
280
|
+
// Detect common tool use patterns in Claude Code
|
|
281
|
+
if (/\[Bash\]|\[Read\]|\[Write\]|\[Edit\]|\[Glob\]|\[Grep\]/.test(content)) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check for multi-turn conversations (likely agentic)
|
|
288
|
+
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
|
289
|
+
if (assistantMessages.length > 2) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
main().catch((error) => {
|
|
297
|
+
console.error(`[Plexor] Fatal error: ${error.message}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plexor Response Tracking Hook
|
|
5
|
+
*
|
|
6
|
+
* This script runs after the LLM response is received.
|
|
7
|
+
* It tracks response metrics for analytics and updates session stats.
|
|
8
|
+
*
|
|
9
|
+
* Input: JSON object with response content, tokens used, etc.
|
|
10
|
+
* Output: Passthrough (no modifications)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PlexorClient = require('../lib/plexor-client');
|
|
14
|
+
const ConfigManager = require('../lib/config');
|
|
15
|
+
const LocalCache = require('../lib/cache');
|
|
16
|
+
const Logger = require('../lib/logger');
|
|
17
|
+
|
|
18
|
+
const logger = new Logger('track-response');
|
|
19
|
+
const config = new ConfigManager();
|
|
20
|
+
const cache = new LocalCache();
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
try {
|
|
24
|
+
const input = await readStdin();
|
|
25
|
+
const response = JSON.parse(input);
|
|
26
|
+
|
|
27
|
+
const settings = await config.load();
|
|
28
|
+
|
|
29
|
+
// If Plexor is disabled or no API key, just pass through
|
|
30
|
+
if (!settings.enabled || !settings.apiKey) {
|
|
31
|
+
return output(response);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if this response has Plexor metadata
|
|
35
|
+
const plexorMeta = response._plexor;
|
|
36
|
+
if (!plexorMeta || !plexorMeta.request_id) {
|
|
37
|
+
return output(response);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get stored metadata for this request
|
|
41
|
+
const metadata = await cache.getMetadata(plexorMeta.request_id);
|
|
42
|
+
if (!metadata) {
|
|
43
|
+
return output(response);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Calculate output tokens (approximate)
|
|
47
|
+
const outputTokens = estimateTokens(response.content || '');
|
|
48
|
+
|
|
49
|
+
// Log response tracking
|
|
50
|
+
logger.info('[Plexor] Response tracked', {
|
|
51
|
+
request_id: plexorMeta.request_id,
|
|
52
|
+
input_tokens: metadata.optimized_tokens,
|
|
53
|
+
output_tokens: outputTokens,
|
|
54
|
+
provider: metadata.recommended_provider
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// In production, we would send this data to the API for analytics
|
|
58
|
+
// For now, just log locally
|
|
59
|
+
|
|
60
|
+
// Pass through unchanged
|
|
61
|
+
return output(response);
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error(`[Plexor] Tracking error: ${error.message}`);
|
|
65
|
+
|
|
66
|
+
// On any error, pass through unchanged
|
|
67
|
+
try {
|
|
68
|
+
const input = await readStdin();
|
|
69
|
+
return output(JSON.parse(input));
|
|
70
|
+
} catch {
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function readStdin() {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const chunks = [];
|
|
79
|
+
|
|
80
|
+
process.stdin.on('data', (chunk) => {
|
|
81
|
+
chunks.push(chunk);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
process.stdin.on('end', () => {
|
|
85
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
process.stdin.on('error', reject);
|
|
89
|
+
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
reject(new Error('Stdin read timeout'));
|
|
92
|
+
}, 2000);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function output(data) {
|
|
97
|
+
const json = JSON.stringify(data);
|
|
98
|
+
process.stdout.write(json);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function estimateTokens(text) {
|
|
103
|
+
// Approximate: ~4 characters per token
|
|
104
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main().catch((error) => {
|
|
108
|
+
console.error(`[Plexor] Fatal error: ${error.message}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plexor-dev/claude-code-plugin",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"description": "LLM cost optimization plugin for Claude Code - Save up to 90% on AI costs",
|
|
5
5
|
"main": "lib/constants.js",
|
|
6
6
|
"scripts": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"commands/",
|
|
13
|
+
"hooks/",
|
|
13
14
|
"scripts/",
|
|
14
15
|
"lib/",
|
|
15
16
|
"README.md",
|
package/lib/constants.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plexor Claude Code Plugin - Constants
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
|
|
8
|
-
module.exports = {
|
|
9
|
-
// API endpoints
|
|
10
|
-
PLEXOR_API_URL: process.env.PLEXOR_API_URL || 'https://api.plexor.dev',
|
|
11
|
-
PLEXOR_GATEWAY_URL: process.env.PLEXOR_GATEWAY_URL || 'https://api.plexor.dev/v1',
|
|
12
|
-
PLEXOR_AUTH_URL: 'https://plexor.dev/auth/device',
|
|
13
|
-
|
|
14
|
-
// File paths
|
|
15
|
-
PLEXOR_CONFIG_DIR: process.env.PLEXOR_CONFIG_DIR || path.join(os.homedir(), '.plexor'),
|
|
16
|
-
PLEXOR_CONFIG_FILE: path.join(
|
|
17
|
-
process.env.PLEXOR_CONFIG_DIR || path.join(os.homedir(), '.plexor'),
|
|
18
|
-
'config.json'
|
|
19
|
-
),
|
|
20
|
-
CLAUDE_COMMANDS_DIR: path.join(os.homedir(), '.claude', 'commands'),
|
|
21
|
-
|
|
22
|
-
// Config schema version
|
|
23
|
-
CONFIG_VERSION: 1,
|
|
24
|
-
|
|
25
|
-
// Default settings
|
|
26
|
-
DEFAULTS: {
|
|
27
|
-
enabled: true,
|
|
28
|
-
preferred_provider: 'auto',
|
|
29
|
-
telemetry: true,
|
|
30
|
-
local_cache: false
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
// API key prefix for identification
|
|
34
|
-
API_KEY_PREFIX: 'plx_',
|
|
35
|
-
|
|
36
|
-
// Timeouts (ms)
|
|
37
|
-
DEVICE_CODE_POLL_INTERVAL: 5000,
|
|
38
|
-
DEVICE_CODE_TIMEOUT: 900000, // 15 minutes
|
|
39
|
-
API_TIMEOUT: 30000
|
|
40
|
-
};
|