@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.
@@ -4,66 +4,10 @@ description: Show Plexor optimization statistics and savings (user)
4
4
 
5
5
  # Plexor Status
6
6
 
7
- Display Plexor optimization statistics in a clean, professional format.
7
+ Run this command to display Plexor statistics:
8
8
 
9
- ## Instructions
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
- IMPORTANT:
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.
@@ -13,145 +13,138 @@
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
15
 
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');
19
-
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
- };
25
-
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;
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
- // Session stats tracking
60
- const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
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
- 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();
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
- return session;
71
- } catch {
72
- return createNewSession();
73
- }
74
- }
73
+ };
75
74
 
76
- function createNewSession() {
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
- 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
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
- function saveSessionStats(session) {
94
- try {
95
- const dir = path.dirname(SESSION_PATH);
96
- if (!fs.existsSync(dir)) {
97
- fs.mkdirSync(dir, { recursive: true });
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
- 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
- }
111
+ };
131
112
 
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
- }
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
- 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
- }
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
- updateSessionStats(result);
307
+ session.recordOptimization(result);
315
308
 
316
309
  return output(optimizedRequest);
317
310
 
@@ -10,14 +10,56 @@
10
10
  * Output: Passthrough (no modifications)
11
11
  */
12
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');
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
- // 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
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(`[Plexor] Tracking error: ${error.message}`);
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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin",
3
- "version": "0.1.0-beta.14",
3
+ "version": "0.1.0-beta.16",
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": {