@plexor-dev/claude-code-plugin-staging 0.1.0-beta.2 → 0.1.0-beta.20
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/README.md +4 -7
- package/commands/plexor-agent.js +84 -0
- package/commands/plexor-agent.md +36 -0
- package/commands/plexor-enabled.js +177 -18
- package/commands/plexor-enabled.md +31 -13
- package/commands/plexor-login.js +211 -42
- package/commands/plexor-login.md +4 -21
- package/commands/plexor-logout.js +72 -14
- package/commands/plexor-logout.md +2 -20
- package/commands/plexor-provider.js +62 -81
- package/commands/plexor-provider.md +23 -13
- package/commands/plexor-routing.js +77 -0
- package/commands/plexor-routing.md +37 -0
- package/commands/plexor-settings.js +161 -123
- package/commands/plexor-settings.md +38 -14
- package/commands/plexor-setup.js +253 -0
- package/commands/plexor-setup.md +16 -160
- package/commands/plexor-status.js +244 -18
- package/commands/plexor-status.md +1 -13
- package/commands/plexor-uninstall.js +319 -0
- package/commands/plexor-uninstall.md +12 -0
- package/hooks/intercept.js +211 -32
- package/hooks/track-response.js +302 -2
- package/lib/config-utils.js +314 -0
- package/lib/config.js +22 -3
- package/lib/constants.js +19 -1
- package/lib/logger.js +64 -5
- package/lib/settings-manager.js +233 -24
- package/lib/verify-route.js +77 -0
- package/package.json +6 -4
- package/scripts/postinstall.js +271 -44
- package/scripts/uninstall.js +194 -41
- package/commands/plexor-config.js +0 -170
- package/commands/plexor-config.md +0 -28
- package/commands/plexor-mode.js +0 -107
- package/commands/plexor-mode.md +0 -27
package/hooks/intercept.js
CHANGED
|
@@ -35,6 +35,129 @@ 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
|
+
const DISABLED_MODEL_HINTS = new Set(['auto', 'none', 'off']);
|
|
58
|
+
|
|
59
|
+
function normalizeOrchestrationMode(mode) {
|
|
60
|
+
if (typeof mode !== 'string') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const normalized = mode.trim().toLowerCase();
|
|
64
|
+
if (!VALID_ORCHESTRATION_MODES.has(normalized)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeGatewayMode(mode) {
|
|
71
|
+
if (typeof mode !== 'string') {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const normalized = mode.trim().toLowerCase();
|
|
75
|
+
if (!VALID_GATEWAY_MODES.has(normalized)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return normalized === 'cost' ? 'eco' : normalized;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizePreferredProvider(provider) {
|
|
82
|
+
if (typeof provider !== 'string') {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const normalized = provider.trim().toLowerCase();
|
|
86
|
+
if (!VALID_PROVIDER_HINTS.has(normalized)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveOrchestrationMode(settings) {
|
|
93
|
+
const envMode = normalizeOrchestrationMode(process.env.PLEXOR_ORCHESTRATION_MODE);
|
|
94
|
+
if (envMode) {
|
|
95
|
+
return envMode;
|
|
96
|
+
}
|
|
97
|
+
const cfgMode = normalizeOrchestrationMode(
|
|
98
|
+
settings?.orchestrationMode || settings?.orchestration_mode
|
|
99
|
+
);
|
|
100
|
+
return cfgMode || 'autonomous';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveGatewayMode(settings) {
|
|
104
|
+
const envMode = normalizeGatewayMode(process.env.PLEXOR_MODE);
|
|
105
|
+
if (envMode) {
|
|
106
|
+
return envMode;
|
|
107
|
+
}
|
|
108
|
+
const cfgMode = normalizeGatewayMode(settings?.mode);
|
|
109
|
+
return cfgMode || 'balanced';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolvePreferredProvider(settings) {
|
|
113
|
+
const envProvider = normalizePreferredProvider(
|
|
114
|
+
process.env.PLEXOR_PROVIDER || process.env.PLEXOR_PREFERRED_PROVIDER
|
|
115
|
+
);
|
|
116
|
+
if (envProvider) {
|
|
117
|
+
return envProvider;
|
|
118
|
+
}
|
|
119
|
+
const cfgProvider = normalizePreferredProvider(
|
|
120
|
+
settings?.preferredProvider || settings?.preferred_provider
|
|
121
|
+
);
|
|
122
|
+
return cfgProvider || 'auto';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizePreferredModel(model) {
|
|
126
|
+
if (typeof model !== 'string') {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const trimmed = model.trim();
|
|
130
|
+
if (!trimmed) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (DISABLED_MODEL_HINTS.has(trimmed.toLowerCase())) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return trimmed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolvePreferredModel(settings) {
|
|
140
|
+
const envModel = normalizePreferredModel(
|
|
141
|
+
process.env.PLEXOR_MODEL || process.env.PLEXOR_PREFERRED_MODEL
|
|
142
|
+
);
|
|
143
|
+
if (envModel) {
|
|
144
|
+
return envModel;
|
|
145
|
+
}
|
|
146
|
+
const cfgModel = normalizePreferredModel(settings?.preferredModel || settings?.preferred_model);
|
|
147
|
+
return cfgModel || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateForceHintSelection(preferredProvider, preferredModel) {
|
|
151
|
+
if (!preferredModel || preferredProvider === 'auto') {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const error = new Error(
|
|
155
|
+
'Invalid Plexor config: force only one hint. Set preferred_provider OR preferred_model, not both.'
|
|
156
|
+
);
|
|
157
|
+
error.code = 'PLEXOR_CONFIG_CONFLICT';
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
|
|
38
161
|
// Try to load lib modules, fall back to inline implementations
|
|
39
162
|
let ConfigManager, SessionManager, LocalCache, Logger, PlexorClient;
|
|
40
163
|
let config, session, cache, logger;
|
|
@@ -56,10 +179,25 @@ try {
|
|
|
56
179
|
const SESSION_PATH = path.join(process.env.HOME || '', '.plexor', 'session.json');
|
|
57
180
|
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
58
181
|
|
|
182
|
+
const uxMessagesEnabled = !/^(0|false|no|off)$/i.test(
|
|
183
|
+
String(process.env.PLEXOR_UX_MESSAGES ?? process.env.PLEXOR_UX_DEBUG_MESSAGES ?? 'true')
|
|
184
|
+
);
|
|
185
|
+
const CYAN = '\x1b[36m';
|
|
186
|
+
const RESET = '\x1b[0m';
|
|
187
|
+
const formatUx = (msg) => {
|
|
188
|
+
const text = String(msg || '').trim();
|
|
189
|
+
if (!text) return '[PLEXOR: message]';
|
|
190
|
+
return text.startsWith('[PLEXOR:') ? text : `[PLEXOR: ${text}]`;
|
|
191
|
+
};
|
|
192
|
+
|
|
59
193
|
logger = {
|
|
60
194
|
debug: (msg) => process.env.PLEXOR_DEBUG && console.error(`[DEBUG] ${msg}`),
|
|
61
195
|
info: (msg) => console.error(msg),
|
|
62
|
-
error: (msg) => console.error(`[ERROR] ${msg}`)
|
|
196
|
+
error: (msg) => console.error(`[ERROR] ${msg}`),
|
|
197
|
+
ux: (msg) => {
|
|
198
|
+
if (!uxMessagesEnabled) return;
|
|
199
|
+
console.error(`${CYAN}${formatUx(msg)}${RESET}`);
|
|
200
|
+
}
|
|
63
201
|
};
|
|
64
202
|
|
|
65
203
|
config = {
|
|
@@ -73,7 +211,11 @@ try {
|
|
|
73
211
|
apiUrl: cfg.settings?.apiUrl || 'https://api.plexor.dev',
|
|
74
212
|
timeout: cfg.settings?.timeout || 5000,
|
|
75
213
|
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
76
|
-
mode: cfg.settings?.mode || 'balanced'
|
|
214
|
+
mode: cfg.settings?.mode || 'balanced',
|
|
215
|
+
preferredProvider: cfg.settings?.preferred_provider || 'auto',
|
|
216
|
+
preferredModel: cfg.settings?.preferredModel || cfg.settings?.preferred_model || null,
|
|
217
|
+
orchestrationMode:
|
|
218
|
+
cfg.settings?.orchestrationMode || cfg.settings?.orchestration_mode || 'autonomous'
|
|
77
219
|
};
|
|
78
220
|
} catch {
|
|
79
221
|
return { enabled: false };
|
|
@@ -106,9 +248,9 @@ try {
|
|
|
106
248
|
const saveSession = (s) => {
|
|
107
249
|
try {
|
|
108
250
|
const dir = path.dirname(SESSION_PATH);
|
|
109
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
251
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
110
252
|
s.last_activity = Date.now();
|
|
111
|
-
fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2));
|
|
253
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(s, null, 2), { mode: 0o600 });
|
|
112
254
|
} catch {}
|
|
113
255
|
};
|
|
114
256
|
|
|
@@ -179,52 +321,69 @@ async function main() {
|
|
|
179
321
|
input = await readStdin();
|
|
180
322
|
request = JSON.parse(input);
|
|
181
323
|
|
|
324
|
+
// Issue #2042: Check for slash commands FIRST, before ANY other processing
|
|
325
|
+
// Slash commands must pass through completely clean — no metadata injection
|
|
326
|
+
// Adding _plexor_client or _plexor to slash command requests adds context noise
|
|
327
|
+
// that causes the model to re-execute commands in a loop
|
|
328
|
+
// Note: session.recordPassthrough() intentionally omitted — slash commands are
|
|
329
|
+
// not API requests and should not pollute session analytics
|
|
330
|
+
if (isSlashCommand(request)) {
|
|
331
|
+
logger.debug('Slash command detected, clean passthrough (no metadata)');
|
|
332
|
+
logger.ux('Slash command passthrough (no optimization applied)');
|
|
333
|
+
return output(request); // Completely clean — no metadata added
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const settings = await config.load();
|
|
337
|
+
const orchestrationMode = resolveOrchestrationMode(settings);
|
|
338
|
+
const gatewayMode = resolveGatewayMode(settings);
|
|
339
|
+
const preferredProvider = resolvePreferredProvider(settings);
|
|
340
|
+
const preferredModel = resolvePreferredModel(settings);
|
|
341
|
+
validateForceHintSelection(preferredProvider, preferredModel);
|
|
342
|
+
|
|
182
343
|
// Phase 3 Hypervisor Mode Detection
|
|
183
344
|
// When ANTHROPIC_BASE_URL points to Plexor, all intelligence is server-side
|
|
184
345
|
// The plugin just passes through - server handles optimization, routing, quality
|
|
185
346
|
const baseUrl = process.env.ANTHROPIC_BASE_URL || '';
|
|
186
|
-
const isHypervisorMode =
|
|
347
|
+
const isHypervisorMode =
|
|
348
|
+
baseUrl.includes('plexor') ||
|
|
349
|
+
baseUrl.includes('staging.api') ||
|
|
350
|
+
baseUrl.includes('localhost') ||
|
|
351
|
+
baseUrl.includes('127.0.0.1');
|
|
187
352
|
|
|
188
353
|
if (isHypervisorMode) {
|
|
189
354
|
// HYPERVISOR MODE: Server handles everything
|
|
190
355
|
// Just pass through with minimal metadata for session tracking
|
|
191
|
-
logger.debug('
|
|
356
|
+
logger.debug('Hypervisor mode active - server handles all optimization');
|
|
357
|
+
logger.ux(
|
|
358
|
+
`Hypervisor mode active (${gatewayMode}/${orchestrationMode}); routing handled server-side`
|
|
359
|
+
);
|
|
192
360
|
|
|
193
361
|
// Add session tracking metadata (server will use this for analytics)
|
|
194
362
|
return output({
|
|
195
363
|
...request,
|
|
364
|
+
...(preferredModel ? { model: preferredModel } : {}),
|
|
365
|
+
plexor_mode: gatewayMode,
|
|
366
|
+
...(preferredProvider !== 'auto' ? { plexor_provider: preferredProvider } : {}),
|
|
367
|
+
plexor_orchestration_mode: orchestrationMode,
|
|
196
368
|
_plexor_client: {
|
|
369
|
+
...(request._plexor_client || {}),
|
|
197
370
|
mode: 'hypervisor',
|
|
198
371
|
plugin_version: '0.1.0-beta.18',
|
|
372
|
+
orchestration_mode: orchestrationMode,
|
|
373
|
+
plexor_mode: gatewayMode,
|
|
374
|
+
preferred_provider: preferredProvider,
|
|
375
|
+
preferred_model: preferredModel,
|
|
199
376
|
cwd: process.cwd(),
|
|
200
377
|
timestamp: Date.now(),
|
|
201
378
|
}
|
|
202
379
|
});
|
|
203
380
|
}
|
|
204
381
|
|
|
205
|
-
// CRITICAL: Check for slash commands FIRST (before agentic check)
|
|
206
|
-
// Slash commands like /plexor-status should pass through unchanged
|
|
207
|
-
// Must check before isAgenticRequest since all Claude Code requests have tools
|
|
208
|
-
if (isSlashCommand(request)) {
|
|
209
|
-
logger.debug('Slash command detected, passing through unchanged');
|
|
210
|
-
session.recordPassthrough();
|
|
211
|
-
return output({
|
|
212
|
-
...request,
|
|
213
|
-
plexor_cwd: process.cwd(),
|
|
214
|
-
_plexor: {
|
|
215
|
-
request_id: generateRequestId('slash'), // Issue #701: Add request_id for tracking
|
|
216
|
-
source: 'passthrough_slash_command',
|
|
217
|
-
reason: 'slash_command_detected',
|
|
218
|
-
cwd: process.cwd(),
|
|
219
|
-
latency_ms: Date.now() - startTime
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
382
|
// CRITICAL: Skip optimization for CLI commands requiring tool execution
|
|
225
383
|
// Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
|
|
226
384
|
if (requiresToolExecution(request)) {
|
|
227
385
|
logger.debug('CLI tool execution detected, passing through unchanged');
|
|
386
|
+
logger.ux('Tool-execution request detected; preserving tools via passthrough');
|
|
228
387
|
session.recordPassthrough();
|
|
229
388
|
return output({
|
|
230
389
|
...request,
|
|
@@ -243,6 +402,7 @@ async function main() {
|
|
|
243
402
|
// Modifying messages breaks the agent loop and causes infinite loops
|
|
244
403
|
if (isAgenticRequest(request)) {
|
|
245
404
|
logger.debug('Agentic request detected, passing through unchanged');
|
|
405
|
+
logger.ux('Agentic/tool-use request detected; passthrough to avoid loop breakage');
|
|
246
406
|
session.recordPassthrough();
|
|
247
407
|
return output({
|
|
248
408
|
...request,
|
|
@@ -257,10 +417,9 @@ async function main() {
|
|
|
257
417
|
});
|
|
258
418
|
}
|
|
259
419
|
|
|
260
|
-
const settings = await config.load();
|
|
261
|
-
|
|
262
420
|
if (!settings.enabled) {
|
|
263
421
|
logger.debug('Plexor disabled, passing through');
|
|
422
|
+
logger.ux('Plexor plugin is disabled; forwarding request unchanged');
|
|
264
423
|
session.recordPassthrough();
|
|
265
424
|
return output({
|
|
266
425
|
...request,
|
|
@@ -275,6 +434,7 @@ async function main() {
|
|
|
275
434
|
|
|
276
435
|
if (!settings.apiKey) {
|
|
277
436
|
logger.info('Not authenticated. Run /plexor-login to enable optimization.');
|
|
437
|
+
logger.ux('Not authenticated. Run /plexor-login to enable Plexor routing');
|
|
278
438
|
session.recordPassthrough();
|
|
279
439
|
return output({
|
|
280
440
|
...request,
|
|
@@ -301,7 +461,8 @@ async function main() {
|
|
|
301
461
|
const cachedResponse = await cache.get(cacheKey);
|
|
302
462
|
|
|
303
463
|
if (cachedResponse && settings.localCacheEnabled) {
|
|
304
|
-
logger.info('
|
|
464
|
+
logger.info('Local cache hit');
|
|
465
|
+
logger.ux('Local cache hit');
|
|
305
466
|
session.recordCacheHit();
|
|
306
467
|
return output({
|
|
307
468
|
...request,
|
|
@@ -329,10 +490,16 @@ async function main() {
|
|
|
329
490
|
|
|
330
491
|
const savingsPercent = ((result.original_tokens - result.optimized_tokens) / result.original_tokens * 100).toFixed(1);
|
|
331
492
|
|
|
332
|
-
logger.info(`
|
|
493
|
+
logger.info(`Optimized: ${result.original_tokens} → ${result.optimized_tokens} tokens (${savingsPercent}% saved)`);
|
|
494
|
+
logger.ux(
|
|
495
|
+
`Optimized ${result.original_tokens} -> ${result.optimized_tokens} tokens (${savingsPercent}% saved)`
|
|
496
|
+
);
|
|
333
497
|
|
|
334
498
|
if (result.recommended_provider !== 'anthropic') {
|
|
335
|
-
logger.info(`
|
|
499
|
+
logger.info(`Recommended: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`);
|
|
500
|
+
logger.ux(
|
|
501
|
+
`Provider recommendation: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`
|
|
502
|
+
);
|
|
336
503
|
}
|
|
337
504
|
|
|
338
505
|
const optimizedRequest = {
|
|
@@ -368,7 +535,15 @@ async function main() {
|
|
|
368
535
|
return output(optimizedRequest);
|
|
369
536
|
|
|
370
537
|
} catch (error) {
|
|
371
|
-
|
|
538
|
+
if (error?.code === 'PLEXOR_CONFIG_CONFLICT') {
|
|
539
|
+
logger.error(`Configuration error: ${error.message}`);
|
|
540
|
+
logger.ux(error.message);
|
|
541
|
+
process.stderr.write(`\n[Plexor] ${error.message}\n`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
logger.error(`Error: ${error.message}`);
|
|
546
|
+
logger.ux(`Optimization hook error: ${error.message}`);
|
|
372
547
|
logger.debug(error.stack);
|
|
373
548
|
|
|
374
549
|
const errorRequestId = generateRequestId('error'); // Issue #701: Add request_id for tracking
|
|
@@ -578,12 +753,16 @@ function isSlashCommand(request) {
|
|
|
578
753
|
}
|
|
579
754
|
|
|
580
755
|
// Check for system messages with skill instructions
|
|
756
|
+
// Issue #2042: Updated to match new RULE-based .md format (old H1 headers removed)
|
|
581
757
|
for (const msg of messages) {
|
|
582
758
|
if (msg.role === 'system') {
|
|
583
759
|
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
584
760
|
if (/# Plexor (?:Status|Login|Logout|Mode|Provider|Enabled|Settings)/i.test(content)) {
|
|
585
761
|
return true;
|
|
586
762
|
}
|
|
763
|
+
if (/plexor\/commands\/plexor-/i.test(content)) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
587
766
|
}
|
|
588
767
|
}
|
|
589
768
|
|
|
@@ -629,6 +808,6 @@ function requiresToolExecution(request) {
|
|
|
629
808
|
}
|
|
630
809
|
|
|
631
810
|
main().catch((error) => {
|
|
632
|
-
console.error(
|
|
811
|
+
console.error(`\x1b[1m\x1b[37m\x1b[41m PLEXOR \x1b[0m \x1b[31mFatal: ${error.message}\x1b[0m`);
|
|
633
812
|
process.exit(1);
|
|
634
813
|
});
|
package/hooks/track-response.js
CHANGED
|
@@ -85,10 +85,10 @@ try {
|
|
|
85
85
|
save(session) {
|
|
86
86
|
try {
|
|
87
87
|
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
88
|
-
fs.mkdirSync(PLEXOR_DIR, { recursive: true });
|
|
88
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
89
89
|
}
|
|
90
90
|
session.last_activity = Date.now();
|
|
91
|
-
fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2));
|
|
91
|
+
fs.writeFileSync(this.sessionPath, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
92
92
|
} catch {}
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -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
|
+
}
|