@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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/commands/plexor-config.js +170 -0
- package/commands/plexor-config.md +28 -0
- package/commands/plexor-enabled.js +122 -0
- package/commands/plexor-enabled.md +28 -0
- package/commands/plexor-login.js +189 -0
- package/commands/plexor-login.md +27 -0
- package/commands/plexor-logout.js +92 -0
- package/commands/plexor-logout.md +27 -0
- package/commands/plexor-mode.js +107 -0
- package/commands/plexor-mode.md +27 -0
- package/commands/plexor-provider.js +110 -0
- package/commands/plexor-provider.md +27 -0
- package/commands/plexor-settings.js +155 -0
- package/commands/plexor-settings.md +28 -0
- package/commands/plexor-setup.md +172 -0
- package/commands/plexor-status.js +240 -0
- package/commands/plexor-status.md +21 -0
- package/hooks/intercept.js +634 -0
- package/hooks/track-response.js +376 -0
- package/lib/cache.js +107 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +26 -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/lib/settings-manager.js +239 -0
- package/package.json +60 -0
- package/scripts/plexor-cli.sh +48 -0
- package/scripts/postinstall.js +251 -0
- package/scripts/uninstall.js +67 -0
|
@@ -0,0 +1,376 @@
|
|
|
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
|
+
* Issue #701 Fix: Now tracks ALL responses including passthroughs.
|
|
10
|
+
* Previously, responses without request_id would exit early without tracking,
|
|
11
|
+
* leading to "0 tokens" display and potential context issues.
|
|
12
|
+
*
|
|
13
|
+
* Input: JSON object with response content, tokens used, etc.
|
|
14
|
+
* Output: Passthrough (no modifications)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// Use lib modules
|
|
20
|
+
let ConfigManager, SessionManager, LocalCache, Logger, ServerSync;
|
|
21
|
+
try {
|
|
22
|
+
ConfigManager = require('../lib/config');
|
|
23
|
+
SessionManager = require('../lib/session');
|
|
24
|
+
LocalCache = require('../lib/cache');
|
|
25
|
+
Logger = require('../lib/logger');
|
|
26
|
+
// Issue #701: Phase 2 - Server sync for persistent session state
|
|
27
|
+
const serverSyncModule = require('../lib/server-sync');
|
|
28
|
+
ServerSync = serverSyncModule.getServerSync;
|
|
29
|
+
} catch {
|
|
30
|
+
// Fallback inline implementations if lib not found
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const CONFIG_PATH = path.join(process.env.HOME || '', '.plexor', 'config.json');
|
|
33
|
+
const SESSION_PATH = path.join(process.env.HOME || '', '.plexor', 'session.json');
|
|
34
|
+
const PLEXOR_DIR = path.join(process.env.HOME || '', '.plexor');
|
|
35
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
ConfigManager = class {
|
|
38
|
+
async load() {
|
|
39
|
+
try {
|
|
40
|
+
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
41
|
+
const cfg = JSON.parse(data);
|
|
42
|
+
return {
|
|
43
|
+
enabled: cfg.settings?.enabled ?? false,
|
|
44
|
+
apiKey: cfg.auth?.api_key
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return { enabled: false };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Inline SessionManager for fallback (Issue #701: proper tracking)
|
|
53
|
+
SessionManager = class {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.sessionPath = SESSION_PATH;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
load() {
|
|
59
|
+
try {
|
|
60
|
+
const data = fs.readFileSync(this.sessionPath, 'utf8');
|
|
61
|
+
const session = JSON.parse(data);
|
|
62
|
+
if (Date.now() - session.last_activity > SESSION_TIMEOUT_MS) {
|
|
63
|
+
return this.createNew();
|
|
64
|
+
}
|
|
65
|
+
return session;
|
|
66
|
+
} catch {
|
|
67
|
+
return this.createNew();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
createNew() {
|
|
72
|
+
const session = {
|
|
73
|
+
session_id: `session_${Date.now()}`,
|
|
74
|
+
started_at: new Date().toISOString(),
|
|
75
|
+
last_activity: Date.now(),
|
|
76
|
+
requests: 0, optimizations: 0, cache_hits: 0, passthroughs: 0,
|
|
77
|
+
original_tokens: 0, optimized_tokens: 0, tokens_saved: 0,
|
|
78
|
+
output_tokens: 0, // Issue #701: Track output tokens
|
|
79
|
+
baseline_cost: 0, actual_cost: 0, cost_saved: 0
|
|
80
|
+
};
|
|
81
|
+
this.save(session);
|
|
82
|
+
return session;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
save(session) {
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
88
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
session.last_activity = Date.now();
|
|
91
|
+
fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2));
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
recordOptimization(result) {
|
|
96
|
+
const s = this.load();
|
|
97
|
+
s.requests++;
|
|
98
|
+
s.optimizations++;
|
|
99
|
+
s.original_tokens += result.original_tokens || 0;
|
|
100
|
+
s.optimized_tokens += result.optimized_tokens || 0;
|
|
101
|
+
s.tokens_saved += result.tokens_saved || 0;
|
|
102
|
+
s.output_tokens += result.output_tokens || 0; // Issue #701
|
|
103
|
+
s.baseline_cost += result.baseline_cost || 0;
|
|
104
|
+
s.actual_cost += result.estimated_cost || result.actual_cost || 0;
|
|
105
|
+
s.cost_saved += (result.baseline_cost || 0) - (result.estimated_cost || result.actual_cost || 0);
|
|
106
|
+
this.save(s);
|
|
107
|
+
return s;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
recordCacheHit() {
|
|
111
|
+
const s = this.load();
|
|
112
|
+
s.requests++;
|
|
113
|
+
s.cache_hits++;
|
|
114
|
+
this.save(s);
|
|
115
|
+
return s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
recordPassthrough(outputTokens = 0) {
|
|
119
|
+
const s = this.load();
|
|
120
|
+
s.requests++;
|
|
121
|
+
s.passthroughs = (s.passthroughs || 0) + 1;
|
|
122
|
+
s.output_tokens += outputTokens; // Issue #701: Track output even for passthroughs
|
|
123
|
+
this.save(s);
|
|
124
|
+
return s;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getStats() {
|
|
128
|
+
return this.load();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
LocalCache = class {
|
|
133
|
+
async getMetadata() { return null; }
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
Logger = class {
|
|
137
|
+
constructor(name) { this.name = name; }
|
|
138
|
+
info(msg, data) {
|
|
139
|
+
if (data) {
|
|
140
|
+
console.error(`[${this.name}] ${msg}`, JSON.stringify(data));
|
|
141
|
+
} else {
|
|
142
|
+
console.error(`[${this.name}] ${msg}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
error(msg) { console.error(`[${this.name}] [ERROR] ${msg}`); }
|
|
146
|
+
debug(msg) {
|
|
147
|
+
if (process.env.PLEXOR_DEBUG) {
|
|
148
|
+
console.error(`[${this.name}] [DEBUG] ${msg}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Issue #701: Fallback ServerSync that does nothing
|
|
154
|
+
ServerSync = () => ({
|
|
155
|
+
syncSession: async () => ({ synced: false, message: 'Server sync not available' }),
|
|
156
|
+
scheduleSync: () => {},
|
|
157
|
+
needsSync: () => false
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const logger = new Logger('track-response');
|
|
162
|
+
const config = new ConfigManager();
|
|
163
|
+
const cache = new LocalCache();
|
|
164
|
+
const session = new SessionManager();
|
|
165
|
+
|
|
166
|
+
// Issue #701: Phase 2 - Initialize server sync (lazy, initialized on first use)
|
|
167
|
+
let serverSync = null;
|
|
168
|
+
|
|
169
|
+
async function getServerSync() {
|
|
170
|
+
if (serverSync) return serverSync;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const settings = await config.load();
|
|
174
|
+
if (settings.apiKey && settings.enabled) {
|
|
175
|
+
serverSync = ServerSync({
|
|
176
|
+
apiKey: settings.apiKey,
|
|
177
|
+
baseUrl: settings.apiUrl || 'https://api.plexor.dev',
|
|
178
|
+
enabled: settings.serverSyncEnabled !== false
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
serverSync = ServerSync({ enabled: false });
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
serverSync = ServerSync({ enabled: false });
|
|
185
|
+
}
|
|
186
|
+
return serverSync;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function main() {
|
|
190
|
+
let response;
|
|
191
|
+
let input;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
input = await readStdin();
|
|
195
|
+
response = JSON.parse(input);
|
|
196
|
+
|
|
197
|
+
// Calculate output tokens for ALL responses (Issue #701)
|
|
198
|
+
const outputTokens = estimateOutputTokens(response);
|
|
199
|
+
|
|
200
|
+
// Get Plexor metadata if present
|
|
201
|
+
const plexorMeta = response._plexor;
|
|
202
|
+
|
|
203
|
+
// Issue #701: Track ALL responses, not just when enabled
|
|
204
|
+
// This ensures session stats are always accurate
|
|
205
|
+
if (plexorMeta) {
|
|
206
|
+
await trackResponseWithMetadata(plexorMeta, outputTokens);
|
|
207
|
+
} else {
|
|
208
|
+
// No Plexor metadata - still track the output tokens
|
|
209
|
+
session.recordPassthrough(outputTokens);
|
|
210
|
+
logger.debug('Response tracked without Plexor metadata');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Issue #701: Phase 2 - Schedule server sync (non-blocking)
|
|
214
|
+
// This syncs local session stats to the server for persistence
|
|
215
|
+
try {
|
|
216
|
+
const sync = await getServerSync();
|
|
217
|
+
const localStats = session.getStats();
|
|
218
|
+
sync.scheduleSync(localStats);
|
|
219
|
+
} catch (syncError) {
|
|
220
|
+
// Server sync failures should not affect response passthrough
|
|
221
|
+
logger.debug(`Server sync scheduling failed: ${syncError.message}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Pass through unchanged
|
|
225
|
+
return output(response);
|
|
226
|
+
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logger.error(`Tracking error: ${error.message}`);
|
|
229
|
+
|
|
230
|
+
// On any error, try to pass through the response unchanged
|
|
231
|
+
if (response) {
|
|
232
|
+
return output(response);
|
|
233
|
+
} else if (input) {
|
|
234
|
+
try {
|
|
235
|
+
return output(JSON.parse(input));
|
|
236
|
+
} catch {
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Track response using Plexor metadata.
|
|
247
|
+
* Issue #701: Handles ALL source types, not just plexor_api.
|
|
248
|
+
*/
|
|
249
|
+
async function trackResponseWithMetadata(plexorMeta, outputTokens) {
|
|
250
|
+
const source = plexorMeta.source || 'unknown';
|
|
251
|
+
const requestId = plexorMeta.request_id;
|
|
252
|
+
|
|
253
|
+
logger.debug(`Tracking response: source=${source}, request_id=${requestId}`);
|
|
254
|
+
|
|
255
|
+
switch (source) {
|
|
256
|
+
case 'plexor_api':
|
|
257
|
+
// Full optimization was applied
|
|
258
|
+
session.recordOptimization({
|
|
259
|
+
original_tokens: plexorMeta.original_tokens || 0,
|
|
260
|
+
optimized_tokens: plexorMeta.optimized_tokens || 0,
|
|
261
|
+
tokens_saved: plexorMeta.tokens_saved || 0,
|
|
262
|
+
baseline_cost: plexorMeta.baseline_cost || 0,
|
|
263
|
+
estimated_cost: plexorMeta.estimated_cost || 0,
|
|
264
|
+
output_tokens: outputTokens
|
|
265
|
+
});
|
|
266
|
+
logger.info('[Plexor] Optimized response tracked', {
|
|
267
|
+
request_id: requestId,
|
|
268
|
+
input_tokens: plexorMeta.optimized_tokens,
|
|
269
|
+
output_tokens: outputTokens,
|
|
270
|
+
savings_percent: plexorMeta.savings_percent,
|
|
271
|
+
provider: plexorMeta.recommended_provider
|
|
272
|
+
});
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case 'local_cache':
|
|
276
|
+
session.recordCacheHit();
|
|
277
|
+
logger.info('[Plexor] Cache hit recorded', { request_id: requestId });
|
|
278
|
+
break;
|
|
279
|
+
|
|
280
|
+
case 'passthrough_agentic':
|
|
281
|
+
case 'passthrough_slash_command':
|
|
282
|
+
case 'passthrough_cli':
|
|
283
|
+
case 'passthrough_disabled':
|
|
284
|
+
case 'passthrough_no_auth':
|
|
285
|
+
case 'passthrough_error':
|
|
286
|
+
case 'passthrough':
|
|
287
|
+
// Passthrough - still track output tokens
|
|
288
|
+
session.recordPassthrough(outputTokens);
|
|
289
|
+
logger.debug(`Passthrough recorded: ${source}`, {
|
|
290
|
+
request_id: requestId,
|
|
291
|
+
reason: plexorMeta.reason,
|
|
292
|
+
output_tokens: outputTokens
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
default:
|
|
297
|
+
// Unknown source - treat as passthrough
|
|
298
|
+
session.recordPassthrough(outputTokens);
|
|
299
|
+
logger.debug(`Unknown source tracked as passthrough: ${source}`, {
|
|
300
|
+
request_id: requestId,
|
|
301
|
+
output_tokens: outputTokens
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Estimate output tokens from response content.
|
|
308
|
+
* Handles both string and structured content formats.
|
|
309
|
+
*/
|
|
310
|
+
function estimateOutputTokens(response) {
|
|
311
|
+
if (!response) return 0;
|
|
312
|
+
|
|
313
|
+
// Try to get content from various response formats
|
|
314
|
+
let text = '';
|
|
315
|
+
|
|
316
|
+
// Anthropic format: content array with text blocks
|
|
317
|
+
if (Array.isArray(response.content)) {
|
|
318
|
+
for (const block of response.content) {
|
|
319
|
+
if (block.type === 'text' && block.text) {
|
|
320
|
+
text += block.text;
|
|
321
|
+
} else if (block.type === 'tool_use') {
|
|
322
|
+
// Tool use blocks contribute to token count
|
|
323
|
+
text += JSON.stringify(block.input || {});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Simple string content
|
|
328
|
+
else if (typeof response.content === 'string') {
|
|
329
|
+
text = response.content;
|
|
330
|
+
}
|
|
331
|
+
// OpenAI format: choices array
|
|
332
|
+
else if (response.choices && response.choices[0]) {
|
|
333
|
+
const choice = response.choices[0];
|
|
334
|
+
if (choice.message && choice.message.content) {
|
|
335
|
+
text = choice.message.content;
|
|
336
|
+
} else if (choice.text) {
|
|
337
|
+
text = choice.text;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!text) return 0;
|
|
342
|
+
|
|
343
|
+
// Approximate: ~4 characters per token (rough estimate)
|
|
344
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function readStdin() {
|
|
348
|
+
return new Promise((resolve, reject) => {
|
|
349
|
+
const chunks = [];
|
|
350
|
+
|
|
351
|
+
process.stdin.on('data', (chunk) => {
|
|
352
|
+
chunks.push(chunk);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
process.stdin.on('end', () => {
|
|
356
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
process.stdin.on('error', reject);
|
|
360
|
+
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
reject(new Error('Stdin read timeout'));
|
|
363
|
+
}, 2000);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function output(data) {
|
|
368
|
+
const json = JSON.stringify(data);
|
|
369
|
+
process.stdout.write(json);
|
|
370
|
+
process.exit(0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
main().catch((error) => {
|
|
374
|
+
console.error(`[Plexor] Fatal error: ${error.message}`);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
});
|
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;
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
// STAGING PACKAGE - uses staging API
|
|
15
|
+
const DEFAULT_API_URL = 'https://staging.api.plexor.dev';
|
|
16
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
PLEXOR_DIR,
|
|
20
|
+
CONFIG_PATH,
|
|
21
|
+
SESSION_PATH,
|
|
22
|
+
CACHE_PATH,
|
|
23
|
+
SESSION_TIMEOUT_MS,
|
|
24
|
+
DEFAULT_API_URL,
|
|
25
|
+
DEFAULT_TIMEOUT
|
|
26
|
+
};
|
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;
|