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

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,552 @@
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 fs = require('fs');
14
+ const path = require('path');
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
+ };
58
+
59
+ // Session stats tracking
60
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
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();
69
+ }
70
+ return session;
71
+ } catch {
72
+ return createNewSession();
73
+ }
74
+ }
75
+
76
+ function createNewSession() {
77
+ return {
78
+ session_id: `session_${Date.now()}`,
79
+ started_at: new Date().toISOString(),
80
+ 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
90
+ };
91
+ }
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 });
98
+ }
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
+ }
131
+
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
+ }
139
+
140
+ async optimize(params) {
141
+ // Return passthrough result - real API call would go here
142
+ const tokens = JSON.stringify(params.messages).length / 4;
143
+ return {
144
+ request_id: `req_${Date.now()}`,
145
+ original_tokens: Math.round(tokens),
146
+ optimized_tokens: Math.round(tokens * 0.7),
147
+ tokens_saved: Math.round(tokens * 0.3),
148
+ optimized_messages: params.messages,
149
+ recommended_provider: 'anthropic',
150
+ recommended_model: params.model,
151
+ estimated_cost: tokens * 0.00001,
152
+ baseline_cost: tokens * 0.00003
153
+ };
154
+ }
155
+ }
156
+
157
+ async function main() {
158
+ const startTime = Date.now();
159
+
160
+ try {
161
+ const input = await readStdin();
162
+ const request = JSON.parse(input);
163
+
164
+ // CRITICAL: Skip optimization for agentic/tool-using requests
165
+ // Modifying messages breaks the agent loop and causes infinite loops
166
+ if (isAgenticRequest(request)) {
167
+ logger.debug('Agentic request detected, passing through unchanged');
168
+ recordPassthrough();
169
+ return output({
170
+ ...request,
171
+ plexor_cwd: process.cwd(),
172
+ _plexor: {
173
+ source: 'passthrough_agentic',
174
+ reason: 'tool_use_detected',
175
+ cwd: process.cwd(),
176
+ latency_ms: Date.now() - startTime
177
+ }
178
+ });
179
+ }
180
+
181
+ // CRITICAL: Skip optimization for slash commands (Issue #683)
182
+ // Slash commands like /plexor-status should pass through unchanged
183
+ if (isSlashCommand(request)) {
184
+ logger.debug('Slash command detected, passing through unchanged');
185
+ recordPassthrough();
186
+ return output({
187
+ ...request,
188
+ plexor_cwd: process.cwd(),
189
+ _plexor: {
190
+ source: 'passthrough_slash_command',
191
+ reason: 'slash_command_detected',
192
+ cwd: process.cwd(),
193
+ latency_ms: Date.now() - startTime
194
+ }
195
+ });
196
+ }
197
+
198
+ // CRITICAL: Skip optimization for CLI commands requiring tool execution (Issue #683)
199
+ // Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
200
+ if (requiresToolExecution(request)) {
201
+ logger.debug('CLI tool execution detected, passing through unchanged');
202
+ recordPassthrough();
203
+ return output({
204
+ ...request,
205
+ plexor_cwd: process.cwd(),
206
+ _plexor: {
207
+ source: 'passthrough_cli',
208
+ reason: 'cli_tool_execution_detected',
209
+ cwd: process.cwd(),
210
+ latency_ms: Date.now() - startTime
211
+ }
212
+ });
213
+ }
214
+
215
+ const settings = await config.load();
216
+
217
+ if (!settings.enabled) {
218
+ logger.debug('Plexor disabled, passing through');
219
+ return output(request);
220
+ }
221
+
222
+ if (!settings.apiKey) {
223
+ logger.info('Not authenticated. Run /plexor-login to enable optimization.');
224
+ return output(request);
225
+ }
226
+
227
+ const client = new PlexorClient({
228
+ apiKey: settings.apiKey,
229
+ baseUrl: settings.apiUrl || 'https://api.plexor.dev',
230
+ timeout: settings.timeout || 5000
231
+ });
232
+
233
+ const messages = extractMessages(request);
234
+ const model = request.model || 'claude-sonnet-4-20250514';
235
+ const maxTokens = request.max_tokens || 4096;
236
+
237
+ const cacheKey = cache.generateKey(messages);
238
+ const cachedResponse = await cache.get(cacheKey);
239
+
240
+ if (cachedResponse && settings.localCacheEnabled) {
241
+ logger.info('[Plexor] Local cache hit');
242
+ recordCacheHit();
243
+ return output({
244
+ ...request,
245
+ _plexor: {
246
+ source: 'local_cache',
247
+ latency_ms: Date.now() - startTime
248
+ }
249
+ });
250
+ }
251
+
252
+ logger.debug('Calling Plexor API...');
253
+
254
+ const result = await client.optimize({
255
+ messages: messages,
256
+ model: model,
257
+ max_tokens: maxTokens,
258
+ task_hint: detectTaskType(messages),
259
+ context: {
260
+ session_id: request._session_id,
261
+ turn_number: request._turn_number,
262
+ cwd: process.cwd()
263
+ }
264
+ });
265
+
266
+ const savingsPercent = ((result.original_tokens - result.optimized_tokens) / result.original_tokens * 100).toFixed(1);
267
+
268
+ logger.info(`[Plexor] Optimized: ${result.original_tokens} → ${result.optimized_tokens} tokens (${savingsPercent}% saved)`);
269
+
270
+ if (result.recommended_provider !== 'anthropic') {
271
+ logger.info(`[Plexor] Recommended: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`);
272
+ }
273
+
274
+ const optimizedRequest = {
275
+ ...request,
276
+ messages: result.optimized_messages,
277
+ plexor_cwd: process.cwd(),
278
+ _plexor: {
279
+ request_id: result.request_id,
280
+ original_tokens: result.original_tokens,
281
+ optimized_tokens: result.optimized_tokens,
282
+ tokens_saved: result.tokens_saved,
283
+ savings_percent: parseFloat(savingsPercent),
284
+ recommended_provider: result.recommended_provider,
285
+ recommended_model: result.recommended_model,
286
+ estimated_cost: result.estimated_cost,
287
+ baseline_cost: result.baseline_cost,
288
+ latency_ms: Date.now() - startTime,
289
+ source: 'plexor_api',
290
+ cwd: process.cwd()
291
+ }
292
+ };
293
+
294
+ await cache.setMetadata(result.request_id, {
295
+ original_tokens: result.original_tokens,
296
+ optimized_tokens: result.optimized_tokens,
297
+ recommended_provider: result.recommended_provider,
298
+ timestamp: Date.now()
299
+ });
300
+
301
+ // Update session stats
302
+ updateSessionStats(result);
303
+
304
+ return output(optimizedRequest);
305
+
306
+ } catch (error) {
307
+ logger.error(`[Plexor] Error: ${error.message}`);
308
+ logger.debug(error.stack);
309
+
310
+ try {
311
+ const input = await readStdin();
312
+ const request = JSON.parse(input);
313
+ return output({
314
+ ...request,
315
+ _plexor: {
316
+ error: error.message,
317
+ source: 'passthrough'
318
+ }
319
+ });
320
+ } catch {
321
+ process.exit(1);
322
+ }
323
+ }
324
+ }
325
+
326
+ async function readStdin() {
327
+ return new Promise((resolve, reject) => {
328
+ const chunks = [];
329
+
330
+ process.stdin.on('data', (chunk) => {
331
+ chunks.push(chunk);
332
+ });
333
+
334
+ process.stdin.on('end', () => {
335
+ resolve(Buffer.concat(chunks).toString('utf8'));
336
+ });
337
+
338
+ process.stdin.on('error', reject);
339
+
340
+ setTimeout(() => {
341
+ reject(new Error('Stdin read timeout'));
342
+ }, 5000);
343
+ });
344
+ }
345
+
346
+ function output(data) {
347
+ const json = JSON.stringify(data);
348
+ process.stdout.write(json);
349
+ process.exit(0);
350
+ }
351
+
352
+ function extractMessages(request) {
353
+ if (Array.isArray(request.messages)) {
354
+ return request.messages;
355
+ }
356
+
357
+ if (request.prompt) {
358
+ return [{ role: 'user', content: request.prompt }];
359
+ }
360
+
361
+ if (request.system && request.user) {
362
+ return [
363
+ { role: 'system', content: request.system },
364
+ { role: 'user', content: request.user }
365
+ ];
366
+ }
367
+
368
+ return [];
369
+ }
370
+
371
+ function detectTaskType(messages) {
372
+ if (!messages || messages.length === 0) {
373
+ return 'general';
374
+ }
375
+
376
+ const lastUserMessage = [...messages]
377
+ .reverse()
378
+ .find(m => m.role === 'user');
379
+
380
+ if (!lastUserMessage) {
381
+ return 'general';
382
+ }
383
+
384
+ const content = lastUserMessage.content.toLowerCase();
385
+
386
+ if (/```|function|class|import|export|const |let |var |def |async |await/.test(content)) {
387
+ return 'code_generation';
388
+ }
389
+
390
+ if (/test|spec|jest|pytest|unittest|describe\(|it\(|expect\(/.test(content)) {
391
+ return 'test_generation';
392
+ }
393
+
394
+ if (/fix|bug|error|issue|debug|trace|exception|crash/.test(content)) {
395
+ return 'debugging';
396
+ }
397
+
398
+ if (/refactor|improve|optimize|clean|restructure/.test(content)) {
399
+ return 'refactoring';
400
+ }
401
+
402
+ if (/document|readme|comment|explain|docstring/.test(content)) {
403
+ return 'documentation';
404
+ }
405
+
406
+ if (/review|check|audit|assess|evaluate/.test(content)) {
407
+ return 'code_review';
408
+ }
409
+
410
+ if (/analyze|understand|what does|how does|explain/.test(content)) {
411
+ return 'analysis';
412
+ }
413
+
414
+ return 'general';
415
+ }
416
+
417
+ /**
418
+ * Detect if this is an agentic/tool-using request that should not be optimized.
419
+ * Modifying messages in agent loops breaks the loop detection and causes infinite loops.
420
+ */
421
+ function isAgenticRequest(request) {
422
+ // Check if request has tools defined
423
+ if (request.tools && request.tools.length > 0) {
424
+ return true;
425
+ }
426
+
427
+ // Check if any message contains tool use or tool results
428
+ const messages = request.messages || [];
429
+ for (const msg of messages) {
430
+ // Tool use in content (Claude format)
431
+ if (msg.content && Array.isArray(msg.content)) {
432
+ for (const block of msg.content) {
433
+ if (block.type === 'tool_use' || block.type === 'tool_result') {
434
+ return true;
435
+ }
436
+ }
437
+ }
438
+
439
+ // Tool role (OpenAI format)
440
+ if (msg.role === 'tool') {
441
+ return true;
442
+ }
443
+
444
+ // Function call (OpenAI format)
445
+ if (msg.function_call || msg.tool_calls) {
446
+ return true;
447
+ }
448
+ }
449
+
450
+ // Check for assistant messages with tool indicators
451
+ for (const msg of messages) {
452
+ if (msg.role === 'assistant' && msg.content) {
453
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
454
+ // Detect common tool use patterns in Claude Code
455
+ if (/\[Bash\]|\[Read\]|\[Write\]|\[Edit\]|\[Glob\]|\[Grep\]/.test(content)) {
456
+ return true;
457
+ }
458
+ }
459
+ }
460
+
461
+ // Check for multi-turn conversations (likely agentic)
462
+ const assistantMessages = messages.filter(m => m.role === 'assistant');
463
+ if (assistantMessages.length > 2) {
464
+ return true;
465
+ }
466
+
467
+ return false;
468
+ }
469
+
470
+ /**
471
+ * Detect if this is a slash command request that should not be optimized.
472
+ * Slash commands like /plexor-status need to pass through unchanged.
473
+ */
474
+ function isSlashCommand(request) {
475
+ const messages = request.messages || [];
476
+
477
+ // Check the last user message for slash command patterns
478
+ for (let i = messages.length - 1; i >= 0; i--) {
479
+ const msg = messages[i];
480
+ if (msg.role === 'user') {
481
+ const content = typeof msg.content === 'string' ? msg.content : '';
482
+ // Detect slash commands at the start of user message
483
+ if (/^\/[a-z-]+/i.test(content.trim())) {
484
+ return true;
485
+ }
486
+ // Detect <command-name> tags (Claude Code skill invocation)
487
+ if (/<command-name>/.test(content)) {
488
+ return true;
489
+ }
490
+ // Detect plexor-related commands
491
+ if (/plexor-(?:status|login|logout|mode|provider|enabled|settings)/i.test(content)) {
492
+ return true;
493
+ }
494
+ break; // Only check last user message
495
+ }
496
+ }
497
+
498
+ // Check for system messages with skill instructions
499
+ for (const msg of messages) {
500
+ if (msg.role === 'system') {
501
+ const content = typeof msg.content === 'string' ? msg.content : '';
502
+ if (/# Plexor (?:Status|Login|Logout|Mode|Provider|Enabled|Settings)/i.test(content)) {
503
+ return true;
504
+ }
505
+ }
506
+ }
507
+
508
+ return false;
509
+ }
510
+
511
+ /**
512
+ * Detect if this request involves CLI/shell commands that need tool execution.
513
+ * These should pass through to ensure proper tool calling behavior.
514
+ */
515
+ function requiresToolExecution(request) {
516
+ const messages = request.messages || [];
517
+
518
+ // Check user messages for CLI command patterns
519
+ for (const msg of messages) {
520
+ if (msg.role === 'user') {
521
+ const content = typeof msg.content === 'string' ? msg.content : '';
522
+ const contentLower = content.toLowerCase();
523
+
524
+ // Azure CLI patterns
525
+ if (/\baz\s+(login|logout|group|account|vm|storage|webapp|aks|acr|keyvault|sql|cosmos|network)/i.test(content)) {
526
+ return true;
527
+ }
528
+
529
+ // Common CLI execution requests
530
+ if (/\b(run|execute|show|list|create|delete|update)\b.*\b(az|aws|gcloud|kubectl|docker|npm|git)\b/i.test(content)) {
531
+ return true;
532
+ }
533
+
534
+ // Direct command patterns
535
+ if (/^(az|aws|gcloud|kubectl|docker|npm|yarn|pip|cargo|go)\s+/m.test(content)) {
536
+ return true;
537
+ }
538
+
539
+ // Imperative CLI requests
540
+ if (/\b(list|show|get|describe)\s+(resource\s*groups?|rgs?|vms?|instances?|clusters?|pods?|containers?)/i.test(content)) {
541
+ return true;
542
+ }
543
+ }
544
+ }
545
+
546
+ return false;
547
+ }
548
+
549
+ main().catch((error) => {
550
+ console.error(`[Plexor] Fatal error: ${error.message}`);
551
+ process.exit(1);
552
+ });
@@ -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.1",
3
+ "version": "0.1.0-beta.11",
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
- };