@plexor-dev/claude-code-plugin-staging 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.
- package/commands/plexor-agent.js +84 -0
- package/commands/plexor-agent.md +36 -0
- package/commands/plexor-provider.js +84 -0
- package/commands/plexor-provider.md +37 -0
- package/commands/plexor-routing.js +77 -0
- package/commands/plexor-routing.md +37 -0
- package/commands/plexor-settings.js +186 -0
- package/commands/plexor-settings.md +52 -0
- package/hooks/intercept.js +139 -10
- package/hooks/track-response.js +300 -0
- package/lib/config-utils.js +74 -0
- package/lib/config.js +9 -2
- package/lib/logger.js +64 -5
- package/lib/settings-manager.js +20 -6
- package/package.json +6 -2
- package/scripts/postinstall.js +161 -35
- package/scripts/uninstall.js +26 -3
package/hooks/intercept.js
CHANGED
|
@@ -35,6 +35,91 @@ function generateRequestId(prefix = 'pass') {
|
|
|
35
35
|
return `${prefix}_${timestamp}_${random}`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const VALID_ORCHESTRATION_MODES = new Set([
|
|
39
|
+
'supervised',
|
|
40
|
+
'autonomous',
|
|
41
|
+
'danger-full-auto'
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const VALID_GATEWAY_MODES = new Set(['eco', 'balanced', 'quality', 'passthrough', 'cost']);
|
|
45
|
+
const VALID_PROVIDER_HINTS = new Set([
|
|
46
|
+
'auto',
|
|
47
|
+
'anthropic',
|
|
48
|
+
'claude',
|
|
49
|
+
'openai',
|
|
50
|
+
'deepseek',
|
|
51
|
+
'mistral',
|
|
52
|
+
'gemini',
|
|
53
|
+
'grok',
|
|
54
|
+
'cohere'
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
function normalizeOrchestrationMode(mode) {
|
|
58
|
+
if (typeof mode !== 'string') {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const normalized = mode.trim().toLowerCase();
|
|
62
|
+
if (!VALID_ORCHESTRATION_MODES.has(normalized)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeGatewayMode(mode) {
|
|
69
|
+
if (typeof mode !== 'string') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const normalized = mode.trim().toLowerCase();
|
|
73
|
+
if (!VALID_GATEWAY_MODES.has(normalized)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return normalized === 'cost' ? 'eco' : normalized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizePreferredProvider(provider) {
|
|
80
|
+
if (typeof provider !== 'string') {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const normalized = provider.trim().toLowerCase();
|
|
84
|
+
if (!VALID_PROVIDER_HINTS.has(normalized)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveOrchestrationMode(settings) {
|
|
91
|
+
const envMode = normalizeOrchestrationMode(process.env.PLEXOR_ORCHESTRATION_MODE);
|
|
92
|
+
if (envMode) {
|
|
93
|
+
return envMode;
|
|
94
|
+
}
|
|
95
|
+
const cfgMode = normalizeOrchestrationMode(
|
|
96
|
+
settings?.orchestrationMode || settings?.orchestration_mode
|
|
97
|
+
);
|
|
98
|
+
return cfgMode || 'autonomous';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveGatewayMode(settings) {
|
|
102
|
+
const envMode = normalizeGatewayMode(process.env.PLEXOR_MODE);
|
|
103
|
+
if (envMode) {
|
|
104
|
+
return envMode;
|
|
105
|
+
}
|
|
106
|
+
const cfgMode = normalizeGatewayMode(settings?.mode);
|
|
107
|
+
return cfgMode || 'balanced';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolvePreferredProvider(settings) {
|
|
111
|
+
const envProvider = normalizePreferredProvider(
|
|
112
|
+
process.env.PLEXOR_PROVIDER || process.env.PLEXOR_PREFERRED_PROVIDER
|
|
113
|
+
);
|
|
114
|
+
if (envProvider) {
|
|
115
|
+
return envProvider;
|
|
116
|
+
}
|
|
117
|
+
const cfgProvider = normalizePreferredProvider(
|
|
118
|
+
settings?.preferredProvider || settings?.preferred_provider
|
|
119
|
+
);
|
|
120
|
+
return cfgProvider || 'auto';
|
|
121
|
+
}
|
|
122
|
+
|
|
38
123
|
// Try to load lib modules, fall back to inline implementations
|
|
39
124
|
let ConfigManager, SessionManager, LocalCache, Logger, PlexorClient;
|
|
40
125
|
let config, session, cache, logger;
|
|
@@ -56,10 +141,25 @@ try {
|
|
|
56
141
|
const SESSION_PATH = path.join(process.env.HOME || '', '.plexor', 'session.json');
|
|
57
142
|
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
58
143
|
|
|
144
|
+
const uxMessagesEnabled = !/^(0|false|no|off)$/i.test(
|
|
145
|
+
String(process.env.PLEXOR_UX_MESSAGES ?? process.env.PLEXOR_UX_DEBUG_MESSAGES ?? 'true')
|
|
146
|
+
);
|
|
147
|
+
const CYAN = '\x1b[36m';
|
|
148
|
+
const RESET = '\x1b[0m';
|
|
149
|
+
const formatUx = (msg) => {
|
|
150
|
+
const text = String(msg || '').trim();
|
|
151
|
+
if (!text) return '[PLEXOR: message]';
|
|
152
|
+
return text.startsWith('[PLEXOR:') ? text : `[PLEXOR: ${text}]`;
|
|
153
|
+
};
|
|
154
|
+
|
|
59
155
|
logger = {
|
|
60
156
|
debug: (msg) => process.env.PLEXOR_DEBUG && console.error(`[DEBUG] ${msg}`),
|
|
61
157
|
info: (msg) => console.error(msg),
|
|
62
|
-
error: (msg) => console.error(`[ERROR] ${msg}`)
|
|
158
|
+
error: (msg) => console.error(`[ERROR] ${msg}`),
|
|
159
|
+
ux: (msg) => {
|
|
160
|
+
if (!uxMessagesEnabled) return;
|
|
161
|
+
console.error(`${CYAN}${formatUx(msg)}${RESET}`);
|
|
162
|
+
}
|
|
63
163
|
};
|
|
64
164
|
|
|
65
165
|
config = {
|
|
@@ -73,7 +173,10 @@ try {
|
|
|
73
173
|
apiUrl: cfg.settings?.apiUrl || 'https://api.plexor.dev',
|
|
74
174
|
timeout: cfg.settings?.timeout || 5000,
|
|
75
175
|
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
76
|
-
mode: cfg.settings?.mode || 'balanced'
|
|
176
|
+
mode: cfg.settings?.mode || 'balanced',
|
|
177
|
+
preferredProvider: cfg.settings?.preferred_provider || 'auto',
|
|
178
|
+
orchestrationMode:
|
|
179
|
+
cfg.settings?.orchestrationMode || cfg.settings?.orchestration_mode || 'autonomous'
|
|
77
180
|
};
|
|
78
181
|
} catch {
|
|
79
182
|
return { enabled: false };
|
|
@@ -187,9 +290,15 @@ async function main() {
|
|
|
187
290
|
// not API requests and should not pollute session analytics
|
|
188
291
|
if (isSlashCommand(request)) {
|
|
189
292
|
logger.debug('Slash command detected, clean passthrough (no metadata)');
|
|
293
|
+
logger.ux('Slash command passthrough (no optimization applied)');
|
|
190
294
|
return output(request); // Completely clean — no metadata added
|
|
191
295
|
}
|
|
192
296
|
|
|
297
|
+
const settings = await config.load();
|
|
298
|
+
const orchestrationMode = resolveOrchestrationMode(settings);
|
|
299
|
+
const gatewayMode = resolveGatewayMode(settings);
|
|
300
|
+
const preferredProvider = resolvePreferredProvider(settings);
|
|
301
|
+
|
|
193
302
|
// Phase 3 Hypervisor Mode Detection
|
|
194
303
|
// When ANTHROPIC_BASE_URL points to Plexor, all intelligence is server-side
|
|
195
304
|
// The plugin just passes through - server handles optimization, routing, quality
|
|
@@ -199,14 +308,24 @@ async function main() {
|
|
|
199
308
|
if (isHypervisorMode) {
|
|
200
309
|
// HYPERVISOR MODE: Server handles everything
|
|
201
310
|
// Just pass through with minimal metadata for session tracking
|
|
202
|
-
logger.debug('
|
|
311
|
+
logger.debug('Hypervisor mode active - server handles all optimization');
|
|
312
|
+
logger.ux(
|
|
313
|
+
`Hypervisor mode active (${gatewayMode}/${orchestrationMode}); routing handled server-side`
|
|
314
|
+
);
|
|
203
315
|
|
|
204
316
|
// Add session tracking metadata (server will use this for analytics)
|
|
205
317
|
return output({
|
|
206
318
|
...request,
|
|
319
|
+
plexor_mode: gatewayMode,
|
|
320
|
+
...(preferredProvider !== 'auto' ? { plexor_provider: preferredProvider } : {}),
|
|
321
|
+
plexor_orchestration_mode: orchestrationMode,
|
|
207
322
|
_plexor_client: {
|
|
323
|
+
...(request._plexor_client || {}),
|
|
208
324
|
mode: 'hypervisor',
|
|
209
325
|
plugin_version: '0.1.0-beta.18',
|
|
326
|
+
orchestration_mode: orchestrationMode,
|
|
327
|
+
plexor_mode: gatewayMode,
|
|
328
|
+
preferred_provider: preferredProvider,
|
|
210
329
|
cwd: process.cwd(),
|
|
211
330
|
timestamp: Date.now(),
|
|
212
331
|
}
|
|
@@ -217,6 +336,7 @@ async function main() {
|
|
|
217
336
|
// Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
|
|
218
337
|
if (requiresToolExecution(request)) {
|
|
219
338
|
logger.debug('CLI tool execution detected, passing through unchanged');
|
|
339
|
+
logger.ux('Tool-execution request detected; preserving tools via passthrough');
|
|
220
340
|
session.recordPassthrough();
|
|
221
341
|
return output({
|
|
222
342
|
...request,
|
|
@@ -235,6 +355,7 @@ async function main() {
|
|
|
235
355
|
// Modifying messages breaks the agent loop and causes infinite loops
|
|
236
356
|
if (isAgenticRequest(request)) {
|
|
237
357
|
logger.debug('Agentic request detected, passing through unchanged');
|
|
358
|
+
logger.ux('Agentic/tool-use request detected; passthrough to avoid loop breakage');
|
|
238
359
|
session.recordPassthrough();
|
|
239
360
|
return output({
|
|
240
361
|
...request,
|
|
@@ -249,10 +370,9 @@ async function main() {
|
|
|
249
370
|
});
|
|
250
371
|
}
|
|
251
372
|
|
|
252
|
-
const settings = await config.load();
|
|
253
|
-
|
|
254
373
|
if (!settings.enabled) {
|
|
255
374
|
logger.debug('Plexor disabled, passing through');
|
|
375
|
+
logger.ux('Plexor plugin is disabled; forwarding request unchanged');
|
|
256
376
|
session.recordPassthrough();
|
|
257
377
|
return output({
|
|
258
378
|
...request,
|
|
@@ -267,6 +387,7 @@ async function main() {
|
|
|
267
387
|
|
|
268
388
|
if (!settings.apiKey) {
|
|
269
389
|
logger.info('Not authenticated. Run /plexor-login to enable optimization.');
|
|
390
|
+
logger.ux('Not authenticated. Run /plexor-login to enable Plexor routing');
|
|
270
391
|
session.recordPassthrough();
|
|
271
392
|
return output({
|
|
272
393
|
...request,
|
|
@@ -293,7 +414,8 @@ async function main() {
|
|
|
293
414
|
const cachedResponse = await cache.get(cacheKey);
|
|
294
415
|
|
|
295
416
|
if (cachedResponse && settings.localCacheEnabled) {
|
|
296
|
-
logger.info('
|
|
417
|
+
logger.info('Local cache hit');
|
|
418
|
+
logger.ux('Local cache hit');
|
|
297
419
|
session.recordCacheHit();
|
|
298
420
|
return output({
|
|
299
421
|
...request,
|
|
@@ -321,10 +443,16 @@ async function main() {
|
|
|
321
443
|
|
|
322
444
|
const savingsPercent = ((result.original_tokens - result.optimized_tokens) / result.original_tokens * 100).toFixed(1);
|
|
323
445
|
|
|
324
|
-
logger.info(`
|
|
446
|
+
logger.info(`Optimized: ${result.original_tokens} → ${result.optimized_tokens} tokens (${savingsPercent}% saved)`);
|
|
447
|
+
logger.ux(
|
|
448
|
+
`Optimized ${result.original_tokens} -> ${result.optimized_tokens} tokens (${savingsPercent}% saved)`
|
|
449
|
+
);
|
|
325
450
|
|
|
326
451
|
if (result.recommended_provider !== 'anthropic') {
|
|
327
|
-
logger.info(`
|
|
452
|
+
logger.info(`Recommended: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`);
|
|
453
|
+
logger.ux(
|
|
454
|
+
`Provider recommendation: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`
|
|
455
|
+
);
|
|
328
456
|
}
|
|
329
457
|
|
|
330
458
|
const optimizedRequest = {
|
|
@@ -360,7 +488,8 @@ async function main() {
|
|
|
360
488
|
return output(optimizedRequest);
|
|
361
489
|
|
|
362
490
|
} catch (error) {
|
|
363
|
-
logger.error(`
|
|
491
|
+
logger.error(`Error: ${error.message}`);
|
|
492
|
+
logger.ux(`Optimization hook error: ${error.message}`);
|
|
364
493
|
logger.debug(error.stack);
|
|
365
494
|
|
|
366
495
|
const errorRequestId = generateRequestId('error'); // Issue #701: Add request_id for tracking
|
|
@@ -625,6 +754,6 @@ function requiresToolExecution(request) {
|
|
|
625
754
|
}
|
|
626
755
|
|
|
627
756
|
main().catch((error) => {
|
|
628
|
-
console.error(
|
|
757
|
+
console.error(`\x1b[1m\x1b[37m\x1b[41m PLEXOR \x1b[0m \x1b[31mFatal: ${error.message}\x1b[0m`);
|
|
629
758
|
process.exit(1);
|
|
630
759
|
});
|
package/hooks/track-response.js
CHANGED
|
@@ -133,6 +133,17 @@ try {
|
|
|
133
133
|
async getMetadata() { return null; }
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
const uxMessagesEnabled = !/^(0|false|no|off)$/i.test(
|
|
137
|
+
String(process.env.PLEXOR_UX_MESSAGES ?? process.env.PLEXOR_UX_DEBUG_MESSAGES ?? 'true')
|
|
138
|
+
);
|
|
139
|
+
const CYAN = '\x1b[36m';
|
|
140
|
+
const RESET = '\x1b[0m';
|
|
141
|
+
const formatUx = (msg) => {
|
|
142
|
+
const text = String(msg || '').trim();
|
|
143
|
+
if (!text) return '[PLEXOR: message]';
|
|
144
|
+
return text.startsWith('[PLEXOR:') ? text : `[PLEXOR: ${text}]`;
|
|
145
|
+
};
|
|
146
|
+
|
|
136
147
|
Logger = class {
|
|
137
148
|
constructor(name) { this.name = name; }
|
|
138
149
|
info(msg, data) {
|
|
@@ -143,6 +154,10 @@ try {
|
|
|
143
154
|
}
|
|
144
155
|
}
|
|
145
156
|
error(msg) { console.error(`[${this.name}] [ERROR] ${msg}`); }
|
|
157
|
+
ux(msg) {
|
|
158
|
+
if (!uxMessagesEnabled) return;
|
|
159
|
+
console.error(`${CYAN}${formatUx(msg)}${RESET}`);
|
|
160
|
+
}
|
|
146
161
|
debug(msg) {
|
|
147
162
|
if (process.env.PLEXOR_DEBUG) {
|
|
148
163
|
console.error(`[${this.name}] [DEBUG] ${msg}`);
|
|
@@ -194,11 +209,17 @@ async function main() {
|
|
|
194
209
|
input = await readStdin();
|
|
195
210
|
response = JSON.parse(input);
|
|
196
211
|
|
|
212
|
+
// Normalize known Plexor recovery text into explicit [PLEXOR: ...] format
|
|
213
|
+
// and mirror high-signal UX hints to stderr in cyan.
|
|
214
|
+
response = normalizePlexorResponseMessages(response);
|
|
215
|
+
emitPlexorUxMessages(response);
|
|
216
|
+
|
|
197
217
|
// Calculate output tokens for ALL responses (Issue #701)
|
|
198
218
|
const outputTokens = estimateOutputTokens(response);
|
|
199
219
|
|
|
200
220
|
// Get Plexor metadata if present
|
|
201
221
|
const plexorMeta = response._plexor;
|
|
222
|
+
emitPlexorOutcomeSummary(response, plexorMeta, outputTokens);
|
|
202
223
|
|
|
203
224
|
// Issue #701: Track ALL responses, not just when enabled
|
|
204
225
|
// This ensures session stats are always accurate
|
|
@@ -374,3 +395,282 @@ main().catch((error) => {
|
|
|
374
395
|
console.error(`[Plexor] Fatal error: ${error.message}`);
|
|
375
396
|
process.exit(1);
|
|
376
397
|
});
|
|
398
|
+
|
|
399
|
+
function normalizePlexorText(text) {
|
|
400
|
+
if (typeof text !== 'string' || !text) {
|
|
401
|
+
return text;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let normalized = text;
|
|
405
|
+
|
|
406
|
+
if (
|
|
407
|
+
normalized.includes("I've been repeating the same operation without making progress.") &&
|
|
408
|
+
!normalized.includes("[PLEXOR: I've been repeating the same operation without making progress.]")
|
|
409
|
+
) {
|
|
410
|
+
normalized = normalized.replace(
|
|
411
|
+
"I've been repeating the same operation without making progress.",
|
|
412
|
+
"[PLEXOR: I've been repeating the same operation without making progress.]"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
normalized = normalized.replace(
|
|
417
|
+
/\[The repeated operation has been blocked\.[^\]]*\]/g,
|
|
418
|
+
(match) => `[PLEXOR: ${match.slice(1, -1)}]`
|
|
419
|
+
);
|
|
420
|
+
normalized = normalized.replace(
|
|
421
|
+
/\[All tasks have been completed\. Session ending\.\]/g,
|
|
422
|
+
'[PLEXOR: All tasks have been completed. Session ending.]'
|
|
423
|
+
);
|
|
424
|
+
normalized = normalized.replace(
|
|
425
|
+
/\[Blocked destructive cleanup command while recovering from prior errors\.[^\]]*\]/g,
|
|
426
|
+
(match) => `[PLEXOR: ${match.slice(1, -1)}]`
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return normalized;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function normalizePlexorResponseMessages(response) {
|
|
433
|
+
if (!response || typeof response !== 'object') {
|
|
434
|
+
return response;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Anthropic format
|
|
438
|
+
if (Array.isArray(response.content)) {
|
|
439
|
+
response.content = response.content.map((block) => {
|
|
440
|
+
if (!block || block.type !== 'text' || typeof block.text !== 'string') {
|
|
441
|
+
return block;
|
|
442
|
+
}
|
|
443
|
+
return { ...block, text: normalizePlexorText(block.text) };
|
|
444
|
+
});
|
|
445
|
+
return response;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// String content format
|
|
449
|
+
if (typeof response.content === 'string') {
|
|
450
|
+
response.content = normalizePlexorText(response.content);
|
|
451
|
+
return response;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// OpenAI choices format
|
|
455
|
+
if (Array.isArray(response.choices)) {
|
|
456
|
+
response.choices = response.choices.map((choice) => {
|
|
457
|
+
if (!choice || typeof choice !== 'object') return choice;
|
|
458
|
+
const updated = { ...choice };
|
|
459
|
+
if (typeof updated.text === 'string') {
|
|
460
|
+
updated.text = normalizePlexorText(updated.text);
|
|
461
|
+
}
|
|
462
|
+
if (updated.message && typeof updated.message.content === 'string') {
|
|
463
|
+
updated.message = {
|
|
464
|
+
...updated.message,
|
|
465
|
+
content: normalizePlexorText(updated.message.content)
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return updated;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return response;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function collectResponseText(response) {
|
|
476
|
+
const texts = [];
|
|
477
|
+
if (!response || typeof response !== 'object') {
|
|
478
|
+
return texts;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (Array.isArray(response.content)) {
|
|
482
|
+
for (const block of response.content) {
|
|
483
|
+
if (block?.type === 'text' && typeof block.text === 'string') {
|
|
484
|
+
texts.push(block.text);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} else if (typeof response.content === 'string') {
|
|
488
|
+
texts.push(response.content);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (Array.isArray(response.choices)) {
|
|
492
|
+
for (const choice of response.choices) {
|
|
493
|
+
if (typeof choice?.text === 'string') {
|
|
494
|
+
texts.push(choice.text);
|
|
495
|
+
}
|
|
496
|
+
if (typeof choice?.message?.content === 'string') {
|
|
497
|
+
texts.push(choice.message.content);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return texts;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function emitPlexorUxMessages(response) {
|
|
506
|
+
if (!logger || typeof logger.ux !== 'function') {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const lines = collectResponseText(response).join('\n');
|
|
511
|
+
if (!lines) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const signals = new Set();
|
|
516
|
+
|
|
517
|
+
if (lines.includes("I've been repeating the same operation without making progress.")) {
|
|
518
|
+
signals.add("[PLEXOR: I've been repeating the same operation without making progress.]");
|
|
519
|
+
}
|
|
520
|
+
if (/repeated operation has been blocked/i.test(lines)) {
|
|
521
|
+
signals.add('[PLEXOR: The repeated operation has been blocked; switching approach.]');
|
|
522
|
+
}
|
|
523
|
+
if (/circuit breaker has fired/i.test(lines)) {
|
|
524
|
+
signals.add('[PLEXOR: Circuit breaker fired; recovery guidance active.]');
|
|
525
|
+
}
|
|
526
|
+
if (/blocked destructive cleanup command/i.test(lines)) {
|
|
527
|
+
signals.add('[PLEXOR: Blocked destructive cleanup command during recovery.]');
|
|
528
|
+
}
|
|
529
|
+
if (/all tasks have been completed\. session ending\./i.test(lines)) {
|
|
530
|
+
signals.add('[PLEXOR: All tasks completed; session ending.]');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const explicitPlexorMessages = lines.match(/\[PLEXOR:[^\]]+\]/g) || [];
|
|
534
|
+
for (const explicit of explicitPlexorMessages) {
|
|
535
|
+
signals.add(explicit);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
for (const signal of signals) {
|
|
539
|
+
logger.ux(signal);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function toNumber(value) {
|
|
544
|
+
if (value === null || value === undefined || value === '') {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
548
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function toBoolean(value) {
|
|
552
|
+
if (typeof value === 'boolean') return value;
|
|
553
|
+
if (typeof value === 'string') {
|
|
554
|
+
const normalized = value.trim().toLowerCase();
|
|
555
|
+
if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
|
|
556
|
+
if (['false', '0', 'no', 'off'].includes(normalized)) return false;
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function formatUsd(value) {
|
|
562
|
+
if (value === null || value === undefined || !Number.isFinite(value)) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
return `$${value.toFixed(6)}`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function extractGatewayOutcome(response, plexorMeta, outputTokens) {
|
|
569
|
+
const provider =
|
|
570
|
+
response?.plexor_provider_used ||
|
|
571
|
+
response?.plexor?.provider_used ||
|
|
572
|
+
plexorMeta?.recommended_provider ||
|
|
573
|
+
null;
|
|
574
|
+
const selectedModel =
|
|
575
|
+
response?.plexor_selected_model ||
|
|
576
|
+
response?.plexor?.selected_model ||
|
|
577
|
+
plexorMeta?.recommended_model ||
|
|
578
|
+
null;
|
|
579
|
+
const requestedModel = response?.model || null;
|
|
580
|
+
const routingSource = response?.plexor_routing_source || response?.plexor?.routing_source || null;
|
|
581
|
+
const authTier = response?.plexor_auth_tier || response?.plexor?.auth_tier || null;
|
|
582
|
+
const stopReason = response?.stop_reason || response?.choices?.[0]?.finish_reason || null;
|
|
583
|
+
const agentHalt = toBoolean(response?.plexor_agent_halt);
|
|
584
|
+
const fallbackUsed =
|
|
585
|
+
toBoolean(response?.plexor_fallback_used) ??
|
|
586
|
+
toBoolean(response?.fallback_used) ??
|
|
587
|
+
toBoolean(response?.plexor?.fallback_used);
|
|
588
|
+
const costUsd =
|
|
589
|
+
toNumber(response?.plexor_cost_usd) ??
|
|
590
|
+
toNumber(response?.cost_usd) ??
|
|
591
|
+
toNumber(plexorMeta?.estimated_cost) ??
|
|
592
|
+
null;
|
|
593
|
+
const savingsUsd =
|
|
594
|
+
toNumber(response?.plexor_savings_usd) ??
|
|
595
|
+
toNumber(response?.savings_usd) ??
|
|
596
|
+
null;
|
|
597
|
+
const inputTokens =
|
|
598
|
+
toNumber(response?.usage?.input_tokens) ??
|
|
599
|
+
toNumber(response?.usage?.prompt_tokens) ??
|
|
600
|
+
toNumber(plexorMeta?.optimized_tokens) ??
|
|
601
|
+
null;
|
|
602
|
+
const emittedOutputTokens =
|
|
603
|
+
toNumber(response?.usage?.output_tokens) ??
|
|
604
|
+
toNumber(response?.usage?.completion_tokens) ??
|
|
605
|
+
toNumber(outputTokens);
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
provider,
|
|
609
|
+
selectedModel,
|
|
610
|
+
requestedModel,
|
|
611
|
+
routingSource,
|
|
612
|
+
authTier,
|
|
613
|
+
stopReason,
|
|
614
|
+
agentHalt,
|
|
615
|
+
fallbackUsed,
|
|
616
|
+
costUsd,
|
|
617
|
+
savingsUsd,
|
|
618
|
+
inputTokens,
|
|
619
|
+
emittedOutputTokens,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function emitPlexorOutcomeSummary(response, plexorMeta, outputTokens) {
|
|
624
|
+
if (!logger || typeof logger.ux !== 'function') {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const outcome = extractGatewayOutcome(response, plexorMeta, outputTokens);
|
|
629
|
+
const messages = [];
|
|
630
|
+
|
|
631
|
+
if (outcome.provider || outcome.selectedModel || outcome.requestedModel) {
|
|
632
|
+
const parts = [];
|
|
633
|
+
if (outcome.provider) parts.push(`provider=${outcome.provider}`);
|
|
634
|
+
if (outcome.selectedModel) parts.push(`selected_model=${outcome.selectedModel}`);
|
|
635
|
+
if (!outcome.selectedModel && outcome.requestedModel) {
|
|
636
|
+
parts.push(`requested_model=${outcome.requestedModel}`);
|
|
637
|
+
}
|
|
638
|
+
if (outcome.routingSource) parts.push(`routing=${outcome.routingSource}`);
|
|
639
|
+
if (outcome.authTier) parts.push(`auth_tier=${outcome.authTier}`);
|
|
640
|
+
messages.push(`Routing summary: ${parts.join(' ')}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (outcome.stopReason || outcome.agentHalt !== null || outcome.fallbackUsed !== null) {
|
|
644
|
+
const parts = [];
|
|
645
|
+
if (outcome.stopReason) parts.push(`stop_reason=${outcome.stopReason}`);
|
|
646
|
+
if (outcome.agentHalt === true) parts.push('agent_halt=auto_continue');
|
|
647
|
+
if (outcome.fallbackUsed !== null) parts.push(`fallback_used=${outcome.fallbackUsed}`);
|
|
648
|
+
if (parts.length > 0) {
|
|
649
|
+
messages.push(`Execution status: ${parts.join(' ')}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (
|
|
654
|
+
outcome.costUsd !== null ||
|
|
655
|
+
outcome.savingsUsd !== null ||
|
|
656
|
+
outcome.inputTokens !== null ||
|
|
657
|
+
outcome.emittedOutputTokens !== null
|
|
658
|
+
) {
|
|
659
|
+
const parts = [];
|
|
660
|
+
const costStr = formatUsd(outcome.costUsd);
|
|
661
|
+
const savingsStr = formatUsd(outcome.savingsUsd);
|
|
662
|
+
if (costStr) parts.push(`cost=${costStr}`);
|
|
663
|
+
if (savingsStr) parts.push(`savings=${savingsStr}`);
|
|
664
|
+
if (outcome.inputTokens !== null) parts.push(`input_tokens=${Math.round(outcome.inputTokens)}`);
|
|
665
|
+
if (outcome.emittedOutputTokens !== null) {
|
|
666
|
+
parts.push(`output_tokens=${Math.round(outcome.emittedOutputTokens)}`);
|
|
667
|
+
}
|
|
668
|
+
if (parts.length > 0) {
|
|
669
|
+
messages.push(`Usage summary: ${parts.join(' ')}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
for (const msg of messages) {
|
|
674
|
+
logger.ux(msg);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared config utilities for Plexor slash commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { PLEXOR_DIR, CONFIG_PATH } = require('./constants');
|
|
9
|
+
|
|
10
|
+
function loadConfig() {
|
|
11
|
+
try {
|
|
12
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
13
|
+
return { version: 1, auth: {}, settings: {} };
|
|
14
|
+
}
|
|
15
|
+
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
16
|
+
if (!data || data.trim() === '') {
|
|
17
|
+
return { version: 1, auth: {}, settings: {} };
|
|
18
|
+
}
|
|
19
|
+
const config = JSON.parse(data);
|
|
20
|
+
if (typeof config !== 'object' || config === null) {
|
|
21
|
+
return { version: 1, auth: {}, settings: {} };
|
|
22
|
+
}
|
|
23
|
+
return config;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err instanceof SyntaxError) {
|
|
26
|
+
try { fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.corrupted'); } catch { /* ignore */ }
|
|
27
|
+
}
|
|
28
|
+
return { version: 1, auth: {}, settings: {} };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveConfig(config) {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
35
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
36
|
+
}
|
|
37
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
38
|
+
const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
|
|
39
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
40
|
+
fs.renameSync(tempPath, CONFIG_PATH);
|
|
41
|
+
return true;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
44
|
+
console.error('Error: Cannot write to ~/.plexor/config.json');
|
|
45
|
+
} else {
|
|
46
|
+
console.error('Failed to save config:', err.message);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read a setting with env-var → config → default precedence.
|
|
54
|
+
* @param {object} config - Loaded config object
|
|
55
|
+
* @param {string} configKey - Primary key in config.settings (camelCase)
|
|
56
|
+
* @param {string|null} configKeyAlt - Alternative key (snake_case legacy)
|
|
57
|
+
* @param {string} envVar - Environment variable name
|
|
58
|
+
* @param {string[]} validValues - Accepted values
|
|
59
|
+
* @param {string} defaultValue - Fallback
|
|
60
|
+
* @returns {{ value: string, source: string }}
|
|
61
|
+
*/
|
|
62
|
+
function readSetting(config, configKey, configKeyAlt, envVar, validValues, defaultValue) {
|
|
63
|
+
const env = process.env[envVar];
|
|
64
|
+
if (env && validValues.includes(env.toLowerCase())) {
|
|
65
|
+
return { value: env.toLowerCase(), source: 'environment' };
|
|
66
|
+
}
|
|
67
|
+
const cfg = config.settings?.[configKey] || (configKeyAlt && config.settings?.[configKeyAlt]);
|
|
68
|
+
if (cfg && validValues.includes(cfg.toLowerCase())) {
|
|
69
|
+
return { value: cfg.toLowerCase(), source: 'config' };
|
|
70
|
+
}
|
|
71
|
+
return { value: defaultValue, source: 'default' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { loadConfig, saveConfig, readSetting };
|
package/lib/config.js
CHANGED
|
@@ -22,7 +22,9 @@ class ConfigManager {
|
|
|
22
22
|
timeout: cfg.settings?.timeout || DEFAULT_TIMEOUT,
|
|
23
23
|
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
24
24
|
mode: cfg.settings?.mode || 'balanced',
|
|
25
|
-
preferredProvider: cfg.settings?.preferred_provider || 'auto'
|
|
25
|
+
preferredProvider: cfg.settings?.preferred_provider || 'auto',
|
|
26
|
+
orchestrationMode:
|
|
27
|
+
cfg.settings?.orchestrationMode || cfg.settings?.orchestration_mode || 'autonomous'
|
|
26
28
|
};
|
|
27
29
|
} catch {
|
|
28
30
|
return { enabled: false, apiKey: '', apiUrl: DEFAULT_API_URL };
|
|
@@ -52,7 +54,12 @@ class ConfigManager {
|
|
|
52
54
|
timeout: config.timeout ?? existing.settings?.timeout,
|
|
53
55
|
localCacheEnabled: config.localCacheEnabled ?? existing.settings?.localCacheEnabled,
|
|
54
56
|
mode: config.mode ?? existing.settings?.mode,
|
|
55
|
-
preferred_provider: config.preferredProvider ?? existing.settings?.preferred_provider
|
|
57
|
+
preferred_provider: config.preferredProvider ?? existing.settings?.preferred_provider,
|
|
58
|
+
orchestrationMode:
|
|
59
|
+
config.orchestrationMode ??
|
|
60
|
+
config.orchestration_mode ??
|
|
61
|
+
existing.settings?.orchestrationMode ??
|
|
62
|
+
existing.settings?.orchestration_mode
|
|
56
63
|
}
|
|
57
64
|
};
|
|
58
65
|
|