@plexor-dev/claude-code-plugin-staging 0.1.0-beta.15 → 0.1.0-beta.17
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.md +27 -2
- package/commands/plexor-provider.js +7 -0
- package/commands/plexor-provider.md +28 -2
- package/commands/plexor-routing.md +28 -2
- package/commands/plexor-settings.js +7 -0
- package/commands/plexor-settings.md +43 -6
- package/hooks/intercept.js +82 -3
- package/hooks/track-response.js +300 -0
- package/lib/config-utils.js +51 -1
- package/lib/config.js +12 -0
- package/lib/logger.js +43 -1
- package/lib/settings-manager.js +20 -6
- package/package.json +1 -1
- package/scripts/postinstall.js +161 -35
- package/scripts/uninstall.js +26 -3
package/commands/plexor-agent.md
CHANGED
|
@@ -2,10 +2,35 @@
|
|
|
2
2
|
description: Set Plexor agent autonomy — supervised, autonomous, or danger-full-auto (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Step 1: Check if arguments were provided.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
If `$ARGUMENTS` contains a value (e.g., `/plexor-agent autonomous`), apply it directly:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
node ~/.claude/plugins/plexor/commands/plexor-agent.js $ARGUMENTS
|
|
11
11
|
```
|
|
12
|
+
|
|
13
|
+
Present the output to the user and STOP. Do NOT call any other tools.
|
|
14
|
+
|
|
15
|
+
**Step 2: If NO arguments were provided** (just `/plexor-agent`), first show the current state:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
node ~/.claude/plugins/plexor/commands/plexor-agent.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then immediately use **AskUserQuestion** to let the user pick a mode:
|
|
22
|
+
|
|
23
|
+
- **Question**: "Which agent autonomy mode?"
|
|
24
|
+
- **Header**: "Agent mode"
|
|
25
|
+
- **Options** (exactly 3):
|
|
26
|
+
1. **supervised** — "Confirm before every action"
|
|
27
|
+
2. **autonomous (Recommended)** — "Acts within safe bounds, default mode"
|
|
28
|
+
3. **danger-full-auto** — "Full autonomy, no guardrails"
|
|
29
|
+
|
|
30
|
+
**Step 3: After the user selects**, apply their choice:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
node ~/.claude/plugins/plexor/commands/plexor-agent.js <selected_value>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Present the output and STOP. Do NOT re-execute commands or call other tools.
|
|
@@ -74,9 +74,16 @@ function main() {
|
|
|
74
74
|
|
|
75
75
|
config.settings = config.settings || {};
|
|
76
76
|
config.settings.preferred_provider = requested;
|
|
77
|
+
if (requested !== 'auto') {
|
|
78
|
+
delete config.settings.preferred_model;
|
|
79
|
+
delete config.settings.preferredModel;
|
|
80
|
+
}
|
|
77
81
|
if (!saveConfig(config)) { process.exit(1); }
|
|
78
82
|
|
|
79
83
|
console.log(`Provider: ${current.value} → ${requested}`);
|
|
84
|
+
if (requested !== 'auto') {
|
|
85
|
+
console.log(' Cleared preferred_model to keep provider/model force hints mutually exclusive.');
|
|
86
|
+
}
|
|
80
87
|
console.log(` ${DESC[requested]}`);
|
|
81
88
|
console.log(' Takes effect on next request.');
|
|
82
89
|
}
|
|
@@ -2,10 +2,36 @@
|
|
|
2
2
|
description: Set Plexor LLM provider — auto, anthropic, openai, deepseek, gemini, etc. (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Step 1: Check if arguments were provided.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
If `$ARGUMENTS` contains a value (e.g., `/plexor-provider anthropic`), apply it directly:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
node ~/.claude/plugins/plexor/commands/plexor-provider.js $ARGUMENTS
|
|
11
11
|
```
|
|
12
|
+
|
|
13
|
+
Present the output to the user and STOP. Do NOT call any other tools.
|
|
14
|
+
|
|
15
|
+
**Step 2: If NO arguments were provided** (just `/plexor-provider`), first show the current state:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
node ~/.claude/plugins/plexor/commands/plexor-provider.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then immediately use **AskUserQuestion** to let the user pick a provider:
|
|
22
|
+
|
|
23
|
+
- **Question**: "Which LLM provider?"
|
|
24
|
+
- **Header**: "Provider"
|
|
25
|
+
- **Options** (exactly 4 — "Other" auto-covers mistral, gemini, grok, cohere):
|
|
26
|
+
1. **auto (Recommended)** — "Let Plexor choose the best provider automatically"
|
|
27
|
+
2. **anthropic** — "Claude models (Opus, Sonnet, Haiku)"
|
|
28
|
+
3. **openai** — "GPT models (GPT-4o, GPT-4.1, o1)"
|
|
29
|
+
4. **deepseek** — "DeepSeek models (Chat, Reasoner)"
|
|
30
|
+
|
|
31
|
+
**Step 3: After the user selects**, apply their choice. If the user picks "Other", they will type the provider name (mistral, gemini, grok, cohere):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node ~/.claude/plugins/plexor/commands/plexor-provider.js <selected_value>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Present the output and STOP. Do NOT re-execute commands or call other tools.
|
|
@@ -2,10 +2,36 @@
|
|
|
2
2
|
description: Set Plexor routing mode — eco, balanced, quality, or passthrough (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Step 1: Check if arguments were provided.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
If `$ARGUMENTS` contains a value (e.g., `/plexor-routing eco`), apply it directly:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
node ~/.claude/plugins/plexor/commands/plexor-routing.js $ARGUMENTS
|
|
11
11
|
```
|
|
12
|
+
|
|
13
|
+
Present the output to the user and STOP. Do NOT call any other tools.
|
|
14
|
+
|
|
15
|
+
**Step 2: If NO arguments were provided** (just `/plexor-routing`), first show the current state:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
node ~/.claude/plugins/plexor/commands/plexor-routing.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then immediately use **AskUserQuestion** to let the user pick a mode:
|
|
22
|
+
|
|
23
|
+
- **Question**: "Which routing mode?"
|
|
24
|
+
- **Header**: "Routing"
|
|
25
|
+
- **Options** (exactly 4):
|
|
26
|
+
1. **eco (Recommended)** — "Cheapest models first, best cost efficiency"
|
|
27
|
+
2. **balanced** — "Balance cost and quality, smart model selection"
|
|
28
|
+
3. **quality** — "Premium models, best output quality"
|
|
29
|
+
4. **passthrough** — "Direct provider access, no routing logic"
|
|
30
|
+
|
|
31
|
+
**Step 3: After the user selects**, apply their choice:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node ~/.claude/plugins/plexor/commands/plexor-routing.js <selected_value>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Present the output and STOP. Do NOT re-execute commands or call other tools.
|
|
@@ -127,6 +127,10 @@ function setDimension(dimension, value, config) {
|
|
|
127
127
|
|
|
128
128
|
config.settings = config.settings || {};
|
|
129
129
|
config.settings[configKey] = resolved;
|
|
130
|
+
if (configKey === 'preferred_provider' && resolved !== 'auto') {
|
|
131
|
+
delete config.settings.preferred_model;
|
|
132
|
+
delete config.settings.preferredModel;
|
|
133
|
+
}
|
|
130
134
|
if (configKeyAlt && config.settings[configKeyAlt]) {
|
|
131
135
|
delete config.settings[configKeyAlt];
|
|
132
136
|
}
|
|
@@ -134,6 +138,9 @@ function setDimension(dimension, value, config) {
|
|
|
134
138
|
if (!saveConfig(config)) { process.exit(1); }
|
|
135
139
|
|
|
136
140
|
console.log(`${label} updated: ${current.value} → ${resolved}`);
|
|
141
|
+
if (configKey === 'preferred_provider' && resolved !== 'auto') {
|
|
142
|
+
console.log(' Cleared preferred_model to keep provider/model force hints mutually exclusive.');
|
|
143
|
+
}
|
|
137
144
|
console.log(` ${descMap[resolved]}`);
|
|
138
145
|
console.log(` Takes effect on next request.`);
|
|
139
146
|
}
|
|
@@ -2,14 +2,51 @@
|
|
|
2
2
|
description: View and change Plexor settings — routing mode, agent autonomy, LLM provider (user)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Step 1: Check if arguments were provided.**
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- `/plexor-settings` — dashboard
|
|
9
|
-
- `/plexor-settings routing eco` — set routing mode
|
|
10
|
-
- `/plexor-settings agent danger-full-auto` — set agent autonomy
|
|
11
|
-
- `/plexor-settings provider anthropic` — set LLM provider
|
|
7
|
+
If `$ARGUMENTS` contains both a dimension AND a value (e.g., `/plexor-settings routing eco`), apply it directly:
|
|
12
8
|
|
|
13
9
|
```bash
|
|
14
10
|
node ~/.claude/plugins/plexor/commands/plexor-settings.js $ARGUMENTS
|
|
15
11
|
```
|
|
12
|
+
|
|
13
|
+
Present the output to the user and STOP. Do NOT call any other tools.
|
|
14
|
+
|
|
15
|
+
**Step 2: If only a dimension was provided** (e.g., `/plexor-settings routing`), show current state then present a picker for that dimension:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
node ~/.claude/plugins/plexor/commands/plexor-settings.js $ARGUMENTS
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then use **AskUserQuestion** based on which dimension:
|
|
22
|
+
|
|
23
|
+
**If dimension is "routing":**
|
|
24
|
+
- **Question**: "Which routing mode?"
|
|
25
|
+
- **Header**: "Routing"
|
|
26
|
+
- **Options**: eco (Recommended) — "Cheapest first", balanced — "Cost/quality balance", quality — "Premium models", passthrough — "Direct access"
|
|
27
|
+
|
|
28
|
+
**If dimension is "agent":**
|
|
29
|
+
- **Question**: "Which agent autonomy mode?"
|
|
30
|
+
- **Header**: "Agent mode"
|
|
31
|
+
- **Options**: supervised — "Confirm every action", autonomous (Recommended) — "Safe bounds", danger-full-auto — "No guardrails"
|
|
32
|
+
|
|
33
|
+
**If dimension is "provider":**
|
|
34
|
+
- **Question**: "Which LLM provider?"
|
|
35
|
+
- **Header**: "Provider"
|
|
36
|
+
- **Options**: auto (Recommended) — "Auto-select", anthropic — "Claude models", openai — "GPT models", deepseek — "DeepSeek models"
|
|
37
|
+
|
|
38
|
+
After the user selects, apply:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node ~/.claude/plugins/plexor/commands/plexor-settings.js <dimension> <selected_value>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Present the output and STOP.
|
|
45
|
+
|
|
46
|
+
**Step 3: If NO arguments were provided** (just `/plexor-settings`), show the dashboard:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node ~/.claude/plugins/plexor/commands/plexor-settings.js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Present the output to the user and STOP. Do NOT call any other tools.
|
package/hooks/intercept.js
CHANGED
|
@@ -54,6 +54,8 @@ const VALID_PROVIDER_HINTS = new Set([
|
|
|
54
54
|
'cohere'
|
|
55
55
|
]);
|
|
56
56
|
|
|
57
|
+
const DISABLED_MODEL_HINTS = new Set(['auto', 'none', 'off']);
|
|
58
|
+
|
|
57
59
|
function normalizeOrchestrationMode(mode) {
|
|
58
60
|
if (typeof mode !== 'string') {
|
|
59
61
|
return null;
|
|
@@ -62,8 +64,6 @@ function normalizeOrchestrationMode(mode) {
|
|
|
62
64
|
if (!VALID_ORCHESTRATION_MODES.has(normalized)) {
|
|
63
65
|
return null;
|
|
64
66
|
}
|
|
65
|
-
// Accept legacy 'full-auto' as alias for 'danger-full-auto'
|
|
66
|
-
if (normalized === 'full-auto') return 'danger-full-auto';
|
|
67
67
|
return normalized;
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -122,6 +122,42 @@ function resolvePreferredProvider(settings) {
|
|
|
122
122
|
return cfgProvider || 'auto';
|
|
123
123
|
}
|
|
124
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
|
+
|
|
125
161
|
// Try to load lib modules, fall back to inline implementations
|
|
126
162
|
let ConfigManager, SessionManager, LocalCache, Logger, PlexorClient;
|
|
127
163
|
let config, session, cache, logger;
|
|
@@ -143,10 +179,25 @@ try {
|
|
|
143
179
|
const SESSION_PATH = path.join(process.env.HOME || '', '.plexor', 'session.json');
|
|
144
180
|
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
145
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
|
+
|
|
146
193
|
logger = {
|
|
147
194
|
debug: (msg) => process.env.PLEXOR_DEBUG && console.error(`[DEBUG] ${msg}`),
|
|
148
195
|
info: (msg) => console.error(msg),
|
|
149
|
-
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
|
+
}
|
|
150
201
|
};
|
|
151
202
|
|
|
152
203
|
config = {
|
|
@@ -162,6 +213,7 @@ try {
|
|
|
162
213
|
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
163
214
|
mode: cfg.settings?.mode || 'balanced',
|
|
164
215
|
preferredProvider: cfg.settings?.preferred_provider || 'auto',
|
|
216
|
+
preferredModel: cfg.settings?.preferredModel || cfg.settings?.preferred_model || null,
|
|
165
217
|
orchestrationMode:
|
|
166
218
|
cfg.settings?.orchestrationMode || cfg.settings?.orchestration_mode || 'autonomous'
|
|
167
219
|
};
|
|
@@ -277,6 +329,7 @@ async function main() {
|
|
|
277
329
|
// not API requests and should not pollute session analytics
|
|
278
330
|
if (isSlashCommand(request)) {
|
|
279
331
|
logger.debug('Slash command detected, clean passthrough (no metadata)');
|
|
332
|
+
logger.ux('Slash command passthrough (no optimization applied)');
|
|
280
333
|
return output(request); // Completely clean — no metadata added
|
|
281
334
|
}
|
|
282
335
|
|
|
@@ -284,6 +337,8 @@ async function main() {
|
|
|
284
337
|
const orchestrationMode = resolveOrchestrationMode(settings);
|
|
285
338
|
const gatewayMode = resolveGatewayMode(settings);
|
|
286
339
|
const preferredProvider = resolvePreferredProvider(settings);
|
|
340
|
+
const preferredModel = resolvePreferredModel(settings);
|
|
341
|
+
validateForceHintSelection(preferredProvider, preferredModel);
|
|
287
342
|
|
|
288
343
|
// Phase 3 Hypervisor Mode Detection
|
|
289
344
|
// When ANTHROPIC_BASE_URL points to Plexor, all intelligence is server-side
|
|
@@ -295,10 +350,14 @@ async function main() {
|
|
|
295
350
|
// HYPERVISOR MODE: Server handles everything
|
|
296
351
|
// Just pass through with minimal metadata for session tracking
|
|
297
352
|
logger.debug('Hypervisor mode active - server handles all optimization');
|
|
353
|
+
logger.ux(
|
|
354
|
+
`Hypervisor mode active (${gatewayMode}/${orchestrationMode}); routing handled server-side`
|
|
355
|
+
);
|
|
298
356
|
|
|
299
357
|
// Add session tracking metadata (server will use this for analytics)
|
|
300
358
|
return output({
|
|
301
359
|
...request,
|
|
360
|
+
...(preferredModel ? { model: preferredModel } : {}),
|
|
302
361
|
plexor_mode: gatewayMode,
|
|
303
362
|
...(preferredProvider !== 'auto' ? { plexor_provider: preferredProvider } : {}),
|
|
304
363
|
plexor_orchestration_mode: orchestrationMode,
|
|
@@ -309,6 +368,7 @@ async function main() {
|
|
|
309
368
|
orchestration_mode: orchestrationMode,
|
|
310
369
|
plexor_mode: gatewayMode,
|
|
311
370
|
preferred_provider: preferredProvider,
|
|
371
|
+
preferred_model: preferredModel,
|
|
312
372
|
cwd: process.cwd(),
|
|
313
373
|
timestamp: Date.now(),
|
|
314
374
|
}
|
|
@@ -319,6 +379,7 @@ async function main() {
|
|
|
319
379
|
// Azure CLI, AWS CLI, kubectl, etc. need tools to be preserved
|
|
320
380
|
if (requiresToolExecution(request)) {
|
|
321
381
|
logger.debug('CLI tool execution detected, passing through unchanged');
|
|
382
|
+
logger.ux('Tool-execution request detected; preserving tools via passthrough');
|
|
322
383
|
session.recordPassthrough();
|
|
323
384
|
return output({
|
|
324
385
|
...request,
|
|
@@ -337,6 +398,7 @@ async function main() {
|
|
|
337
398
|
// Modifying messages breaks the agent loop and causes infinite loops
|
|
338
399
|
if (isAgenticRequest(request)) {
|
|
339
400
|
logger.debug('Agentic request detected, passing through unchanged');
|
|
401
|
+
logger.ux('Agentic/tool-use request detected; passthrough to avoid loop breakage');
|
|
340
402
|
session.recordPassthrough();
|
|
341
403
|
return output({
|
|
342
404
|
...request,
|
|
@@ -353,6 +415,7 @@ async function main() {
|
|
|
353
415
|
|
|
354
416
|
if (!settings.enabled) {
|
|
355
417
|
logger.debug('Plexor disabled, passing through');
|
|
418
|
+
logger.ux('Plexor plugin is disabled; forwarding request unchanged');
|
|
356
419
|
session.recordPassthrough();
|
|
357
420
|
return output({
|
|
358
421
|
...request,
|
|
@@ -367,6 +430,7 @@ async function main() {
|
|
|
367
430
|
|
|
368
431
|
if (!settings.apiKey) {
|
|
369
432
|
logger.info('Not authenticated. Run /plexor-login to enable optimization.');
|
|
433
|
+
logger.ux('Not authenticated. Run /plexor-login to enable Plexor routing');
|
|
370
434
|
session.recordPassthrough();
|
|
371
435
|
return output({
|
|
372
436
|
...request,
|
|
@@ -394,6 +458,7 @@ async function main() {
|
|
|
394
458
|
|
|
395
459
|
if (cachedResponse && settings.localCacheEnabled) {
|
|
396
460
|
logger.info('Local cache hit');
|
|
461
|
+
logger.ux('Local cache hit');
|
|
397
462
|
session.recordCacheHit();
|
|
398
463
|
return output({
|
|
399
464
|
...request,
|
|
@@ -422,9 +487,15 @@ async function main() {
|
|
|
422
487
|
const savingsPercent = ((result.original_tokens - result.optimized_tokens) / result.original_tokens * 100).toFixed(1);
|
|
423
488
|
|
|
424
489
|
logger.info(`Optimized: ${result.original_tokens} → ${result.optimized_tokens} tokens (${savingsPercent}% saved)`);
|
|
490
|
+
logger.ux(
|
|
491
|
+
`Optimized ${result.original_tokens} -> ${result.optimized_tokens} tokens (${savingsPercent}% saved)`
|
|
492
|
+
);
|
|
425
493
|
|
|
426
494
|
if (result.recommended_provider !== 'anthropic') {
|
|
427
495
|
logger.info(`Recommended: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`);
|
|
496
|
+
logger.ux(
|
|
497
|
+
`Provider recommendation: ${result.recommended_provider} (~$${result.estimated_cost.toFixed(4)})`
|
|
498
|
+
);
|
|
428
499
|
}
|
|
429
500
|
|
|
430
501
|
const optimizedRequest = {
|
|
@@ -460,7 +531,15 @@ async function main() {
|
|
|
460
531
|
return output(optimizedRequest);
|
|
461
532
|
|
|
462
533
|
} catch (error) {
|
|
534
|
+
if (error?.code === 'PLEXOR_CONFIG_CONFLICT') {
|
|
535
|
+
logger.error(`Configuration error: ${error.message}`);
|
|
536
|
+
logger.ux(error.message);
|
|
537
|
+
process.stderr.write(`\n[Plexor] ${error.message}\n`);
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
|
|
463
541
|
logger.error(`Error: ${error.message}`);
|
|
542
|
+
logger.ux(`Optimization hook error: ${error.message}`);
|
|
464
543
|
logger.debug(error.stack);
|
|
465
544
|
|
|
466
545
|
const errorRequestId = generateRequestId('error'); // Issue #701: Add request_id for tracking
|
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
|
+
}
|
package/lib/config-utils.js
CHANGED
|
@@ -7,6 +7,50 @@ const path = require('path');
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { PLEXOR_DIR, CONFIG_PATH } = require('./constants');
|
|
9
9
|
|
|
10
|
+
const DISABLED_HINT_VALUES = new Set(['', 'auto', 'none', 'off']);
|
|
11
|
+
|
|
12
|
+
function normalizeForcedProvider(value) {
|
|
13
|
+
if (typeof value !== 'string') {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const normalized = value.trim().toLowerCase();
|
|
17
|
+
if (!normalized || normalized === 'auto') {
|
|
18
|
+
return 'auto';
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeForcedModel(value) {
|
|
24
|
+
if (typeof value !== 'string') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const normalized = value.trim();
|
|
28
|
+
if (!normalized || DISABLED_HINT_VALUES.has(normalized.toLowerCase())) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasForcedHintConflict(config) {
|
|
35
|
+
const settings = config?.settings || {};
|
|
36
|
+
const provider = normalizeForcedProvider(
|
|
37
|
+
settings.preferred_provider ?? settings.preferredProvider ?? 'auto'
|
|
38
|
+
);
|
|
39
|
+
const model = normalizeForcedModel(settings.preferred_model ?? settings.preferredModel);
|
|
40
|
+
return Boolean(model) && provider !== 'auto';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateForcedHintConfig(config) {
|
|
44
|
+
if (!hasForcedHintConflict(config)) {
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
message:
|
|
50
|
+
'Invalid Plexor config: set only one force hint. Use preferred_provider OR preferred_model, not both.'
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
10
54
|
function loadConfig() {
|
|
11
55
|
try {
|
|
12
56
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
@@ -31,6 +75,12 @@ function loadConfig() {
|
|
|
31
75
|
|
|
32
76
|
function saveConfig(config) {
|
|
33
77
|
try {
|
|
78
|
+
const validation = validateForcedHintConfig(config);
|
|
79
|
+
if (!validation.ok) {
|
|
80
|
+
console.error(`Error: ${validation.message}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
34
84
|
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
35
85
|
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
36
86
|
}
|
|
@@ -71,4 +121,4 @@ function readSetting(config, configKey, configKeyAlt, envVar, validValues, defau
|
|
|
71
121
|
return { value: defaultValue, source: 'default' };
|
|
72
122
|
}
|
|
73
123
|
|
|
74
|
-
module.exports = { loadConfig, saveConfig, readSetting };
|
|
124
|
+
module.exports = { loadConfig, saveConfig, readSetting, hasForcedHintConflict, validateForcedHintConfig };
|
package/lib/config.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { CONFIG_PATH, PLEXOR_DIR, DEFAULT_API_URL, DEFAULT_TIMEOUT } = require('./constants');
|
|
8
|
+
const { validateForcedHintConfig } = require('./config-utils');
|
|
8
9
|
|
|
9
10
|
class ConfigManager {
|
|
10
11
|
constructor() {
|
|
@@ -23,6 +24,7 @@ class ConfigManager {
|
|
|
23
24
|
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
24
25
|
mode: cfg.settings?.mode || 'balanced',
|
|
25
26
|
preferredProvider: cfg.settings?.preferred_provider || 'auto',
|
|
27
|
+
preferredModel: cfg.settings?.preferredModel || cfg.settings?.preferred_model || null,
|
|
26
28
|
orchestrationMode:
|
|
27
29
|
cfg.settings?.orchestrationMode || cfg.settings?.orchestration_mode || 'autonomous'
|
|
28
30
|
};
|
|
@@ -55,6 +57,11 @@ class ConfigManager {
|
|
|
55
57
|
localCacheEnabled: config.localCacheEnabled ?? existing.settings?.localCacheEnabled,
|
|
56
58
|
mode: config.mode ?? existing.settings?.mode,
|
|
57
59
|
preferred_provider: config.preferredProvider ?? existing.settings?.preferred_provider,
|
|
60
|
+
preferred_model:
|
|
61
|
+
config.preferredModel ??
|
|
62
|
+
config.preferred_model ??
|
|
63
|
+
existing.settings?.preferredModel ??
|
|
64
|
+
existing.settings?.preferred_model,
|
|
58
65
|
orchestrationMode:
|
|
59
66
|
config.orchestrationMode ??
|
|
60
67
|
config.orchestration_mode ??
|
|
@@ -63,6 +70,11 @@ class ConfigManager {
|
|
|
63
70
|
}
|
|
64
71
|
};
|
|
65
72
|
|
|
73
|
+
const validation = validateForcedHintConfig(updated);
|
|
74
|
+
if (!validation.ok) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
fs.writeFileSync(this.configPath, JSON.stringify(updated, null, 2), { mode: 0o600 });
|
|
67
79
|
return true;
|
|
68
80
|
} catch {
|
package/lib/logger.js
CHANGED
|
@@ -12,6 +12,7 @@ const DIM = '\x1b[2m';
|
|
|
12
12
|
const WHITE = '\x1b[37m';
|
|
13
13
|
const YELLOW_FG = '\x1b[33m';
|
|
14
14
|
const RED_FG = '\x1b[31m';
|
|
15
|
+
const CYAN_FG = '\x1b[36m';
|
|
15
16
|
const BLUE_BG = '\x1b[44m';
|
|
16
17
|
const YELLOW_BG = '\x1b[43m';
|
|
17
18
|
const RED_BG = '\x1b[41m';
|
|
@@ -21,10 +22,42 @@ const BADGE_INFO = `${BOLD}${WHITE}${BLUE_BG} PLEXOR ${RESET}`;
|
|
|
21
22
|
const BADGE_WARN = `${BOLD}${WHITE}${YELLOW_BG} PLEXOR ${RESET}`;
|
|
22
23
|
const BADGE_ERROR = `${BOLD}${WHITE}${RED_BG} PLEXOR ${RESET}`;
|
|
23
24
|
|
|
25
|
+
function parseBooleanEnv(name, defaultValue) {
|
|
26
|
+
const raw = process.env[name];
|
|
27
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
31
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return defaultValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatUxMessage(message) {
|
|
41
|
+
const text = String(message || '').trim();
|
|
42
|
+
if (!text) {
|
|
43
|
+
return '[PLEXOR: message]';
|
|
44
|
+
}
|
|
45
|
+
if (text.startsWith('[PLEXOR:')) {
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
if (text.startsWith('[PLEXOR') && text.endsWith(']')) {
|
|
49
|
+
return text.replace(/^\[PLEXOR[^\]]*\]\s*/i, '[PLEXOR: ');
|
|
50
|
+
}
|
|
51
|
+
return `[PLEXOR: ${text}]`;
|
|
52
|
+
}
|
|
53
|
+
|
|
24
54
|
class Logger {
|
|
25
55
|
constructor(component = 'plexor') {
|
|
26
56
|
this.component = component;
|
|
27
|
-
this.debug_enabled =
|
|
57
|
+
this.debug_enabled = parseBooleanEnv('PLEXOR_DEBUG', false);
|
|
58
|
+
this.ux_messages_enabled =
|
|
59
|
+
parseBooleanEnv('PLEXOR_UX_MESSAGES', true) &&
|
|
60
|
+
parseBooleanEnv('PLEXOR_UX_DEBUG_MESSAGES', true);
|
|
28
61
|
}
|
|
29
62
|
|
|
30
63
|
debug(msg, data = null) {
|
|
@@ -48,6 +81,15 @@ class Logger {
|
|
|
48
81
|
const output = data ? `${BADGE_ERROR} ${RED_FG}${msg} ${JSON.stringify(data)}${RESET}` : `${BADGE_ERROR} ${RED_FG}${msg}${RESET}`;
|
|
49
82
|
console.error(output);
|
|
50
83
|
}
|
|
84
|
+
|
|
85
|
+
ux(msg, data = null) {
|
|
86
|
+
if (!this.ux_messages_enabled) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const prefixed = formatUxMessage(msg);
|
|
90
|
+
const output = data ? `${prefixed} ${JSON.stringify(data)}` : prefixed;
|
|
91
|
+
console.error(`${CYAN_FG}${output}${RESET}`);
|
|
92
|
+
}
|
|
51
93
|
}
|
|
52
94
|
|
|
53
95
|
module.exports = Logger;
|
package/lib/settings-manager.js
CHANGED
|
@@ -23,6 +23,22 @@ const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
|
|
|
23
23
|
const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
24
24
|
const PLEXOR_PROD_URL = 'https://api.plexor.dev/gateway/anthropic';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Check if a base URL is a Plexor-managed gateway URL.
|
|
28
|
+
* Detects all variants: production, staging, localhost, tunnels.
|
|
29
|
+
*/
|
|
30
|
+
function isManagedGatewayUrl(baseUrl) {
|
|
31
|
+
if (!baseUrl) return false;
|
|
32
|
+
return (
|
|
33
|
+
baseUrl.includes('plexor') ||
|
|
34
|
+
baseUrl.includes('staging.api') ||
|
|
35
|
+
baseUrl.includes('localhost') ||
|
|
36
|
+
baseUrl.includes('127.0.0.1') ||
|
|
37
|
+
baseUrl.includes('ngrok') ||
|
|
38
|
+
baseUrl.includes('localtunnel')
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
class ClaudeSettingsManager {
|
|
27
43
|
constructor() {
|
|
28
44
|
this.settingsPath = SETTINGS_PATH;
|
|
@@ -248,11 +264,8 @@ class ClaudeSettingsManager {
|
|
|
248
264
|
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || null;
|
|
249
265
|
const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
250
266
|
|
|
251
|
-
// Check if routing to Plexor
|
|
252
|
-
const isPlexorRouting = baseUrl
|
|
253
|
-
baseUrl.includes('plexor') ||
|
|
254
|
-
baseUrl.includes('staging.api')
|
|
255
|
-
);
|
|
267
|
+
// Check if routing to Plexor (any variant: prod, staging, localhost, tunnel)
|
|
268
|
+
const isPlexorRouting = isManagedGatewayUrl(baseUrl);
|
|
256
269
|
|
|
257
270
|
return {
|
|
258
271
|
enabled: isPlexorRouting,
|
|
@@ -276,7 +289,7 @@ class ClaudeSettingsManager {
|
|
|
276
289
|
const settings = this.load();
|
|
277
290
|
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
|
|
278
291
|
const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
|
|
279
|
-
const isPlexorUrl =
|
|
292
|
+
const isPlexorUrl = isManagedGatewayUrl(baseUrl);
|
|
280
293
|
|
|
281
294
|
if (isPlexorUrl && !authToken) {
|
|
282
295
|
return { partial: true, issue: 'Plexor URL set but no auth token' };
|
|
@@ -345,6 +358,7 @@ const settingsManager = new ClaudeSettingsManager();
|
|
|
345
358
|
module.exports = {
|
|
346
359
|
ClaudeSettingsManager,
|
|
347
360
|
settingsManager,
|
|
361
|
+
isManagedGatewayUrl,
|
|
348
362
|
CLAUDE_DIR,
|
|
349
363
|
SETTINGS_PATH,
|
|
350
364
|
PLEXOR_STAGING_URL,
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -13,42 +13,109 @@ const os = require('os');
|
|
|
13
13
|
const { execSync } = require('child_process');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Resolve the home directory for a given username by querying /etc/passwd.
|
|
17
|
+
* This is the authoritative source and handles non-standard home paths
|
|
18
|
+
* (e.g., /root, /opt/users/foo, NIS/LDAP users, etc.).
|
|
19
|
+
* Returns null if lookup fails (Windows, missing getent, etc.).
|
|
20
|
+
*/
|
|
21
|
+
function getHomeDirFromPasswd(username) {
|
|
22
|
+
try {
|
|
23
|
+
const entry = execSync(`getent passwd ${username}`, { encoding: 'utf8' }).trim();
|
|
24
|
+
// Format: username:x:uid:gid:gecos:homedir:shell
|
|
25
|
+
const fields = entry.split(':');
|
|
26
|
+
if (fields.length >= 6 && fields[5]) {
|
|
27
|
+
return fields[5];
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// getent not available or user not found
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the correct home directory for the process's effective user.
|
|
37
|
+
*
|
|
38
|
+
* Handles three scenarios:
|
|
39
|
+
* 1. Normal execution: HOME is correct, os.homedir() is correct.
|
|
40
|
+
* 2. `sudo npm install`: SUDO_USER is set, os.homedir() returns /root,
|
|
41
|
+
* but we want the SUDO_USER's home.
|
|
42
|
+
* 3. `sudo -u target npm install`:
|
|
43
|
+
* HOME may still be the *caller's* home (e.g.,
|
|
44
|
+
* /home/azureuser), SUDO_USER is the *caller*
|
|
45
|
+
* (not the target), but process.getuid() returns
|
|
46
|
+
* the *target* UID. We must resolve home from
|
|
47
|
+
* /etc/passwd by UID.
|
|
48
|
+
*
|
|
49
|
+
* Resolution order (most authoritative first):
|
|
50
|
+
* a) Look up the effective UID in /etc/passwd via getent (handles sudo -u)
|
|
51
|
+
* b) Fall back to os.homedir() (works for normal execution)
|
|
52
|
+
* c) Fall back to HOME / USERPROFILE env vars (last resort)
|
|
19
53
|
*/
|
|
20
54
|
function getHomeDir() {
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
// On non-Windows, resolve via the effective UID's passwd entry.
|
|
56
|
+
// This is the most reliable method and correctly handles both
|
|
57
|
+
// `sudo` and `sudo -u <target>` scenarios.
|
|
58
|
+
if (os.platform() !== 'win32') {
|
|
59
|
+
try {
|
|
60
|
+
const uid = process.getuid();
|
|
61
|
+
const entry = execSync(`getent passwd ${uid}`, { encoding: 'utf8' }).trim();
|
|
62
|
+
const fields = entry.split(':');
|
|
63
|
+
if (fields.length >= 6 && fields[5]) {
|
|
64
|
+
return fields[5];
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Fall through to other methods
|
|
29
68
|
}
|
|
30
69
|
}
|
|
31
|
-
|
|
70
|
+
|
|
71
|
+
// Fallback: os.homedir() (reads HOME env var, then passwd on Unix)
|
|
72
|
+
const home = os.homedir();
|
|
73
|
+
if (home) return home;
|
|
74
|
+
|
|
75
|
+
// Last resort: environment variables
|
|
76
|
+
return process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
32
77
|
}
|
|
33
78
|
|
|
34
79
|
/**
|
|
35
|
-
* Get uid/gid for the
|
|
36
|
-
*
|
|
80
|
+
* Get uid/gid for the effective user running this process.
|
|
81
|
+
* Under `sudo`, the effective user is root but we want to chown to the
|
|
82
|
+
* original (SUDO_USER) or target (`sudo -u target`) user.
|
|
83
|
+
* Under `sudo -u target`, process.getuid() IS the target, so we use that.
|
|
84
|
+
* Returns null on Windows or if no privilege elevation detected.
|
|
37
85
|
*/
|
|
38
86
|
function getTargetUserIds() {
|
|
39
|
-
|
|
40
|
-
if (!sudoUser || os.platform() === 'win32') {
|
|
87
|
+
if (os.platform() === 'win32') {
|
|
41
88
|
return null;
|
|
42
89
|
}
|
|
43
90
|
|
|
44
91
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
92
|
+
const effectiveUid = process.getuid();
|
|
93
|
+
|
|
94
|
+
// If we're running as root (uid 0), we were likely invoked via `sudo`.
|
|
95
|
+
// Chown files to SUDO_USER (the human who ran sudo).
|
|
96
|
+
if (effectiveUid === 0 && process.env.SUDO_USER) {
|
|
97
|
+
const uid = parseInt(execSync(`id -u ${process.env.SUDO_USER}`, { encoding: 'utf8' }).trim(), 10);
|
|
98
|
+
const gid = parseInt(execSync(`id -g ${process.env.SUDO_USER}`, { encoding: 'utf8' }).trim(), 10);
|
|
99
|
+
return { uid, gid, user: process.env.SUDO_USER };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If we're NOT root but SUDO_USER is set, we were invoked via `sudo -u target`.
|
|
103
|
+
// The effective UID is already the target user. Chown to that user.
|
|
104
|
+
if (effectiveUid !== 0 && process.env.SUDO_USER) {
|
|
105
|
+
const entry = execSync(`getent passwd ${effectiveUid}`, { encoding: 'utf8' }).trim();
|
|
106
|
+
const fields = entry.split(':');
|
|
107
|
+
if (fields.length >= 4) {
|
|
108
|
+
const username = fields[0];
|
|
109
|
+
const uid = parseInt(fields[2], 10);
|
|
110
|
+
const gid = parseInt(fields[3], 10);
|
|
111
|
+
return { uid, gid, user: username };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
49
114
|
} catch {
|
|
50
|
-
|
|
115
|
+
// Fall through
|
|
51
116
|
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
52
119
|
}
|
|
53
120
|
|
|
54
121
|
/**
|
|
@@ -77,33 +144,71 @@ const PLEXOR_LIB_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'lib'
|
|
|
77
144
|
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
78
145
|
const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
|
|
79
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Check if a base URL is a Plexor-managed gateway URL.
|
|
149
|
+
* Detects all variants: production, staging, localhost, tunnels.
|
|
150
|
+
*/
|
|
151
|
+
function isManagedGatewayUrl(baseUrl) {
|
|
152
|
+
if (!baseUrl) return false;
|
|
153
|
+
return (
|
|
154
|
+
baseUrl.includes('plexor') ||
|
|
155
|
+
baseUrl.includes('staging.api') ||
|
|
156
|
+
baseUrl.includes('localhost') ||
|
|
157
|
+
baseUrl.includes('127.0.0.1') ||
|
|
158
|
+
baseUrl.includes('ngrok') ||
|
|
159
|
+
baseUrl.includes('localtunnel')
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The expected base URL for THIS plugin variant.
|
|
165
|
+
* Used to detect when a different variant was previously installed.
|
|
166
|
+
*/
|
|
167
|
+
const THIS_VARIANT_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
168
|
+
|
|
80
169
|
/**
|
|
81
170
|
* Check for orphaned Plexor routing in settings.json without valid config.
|
|
82
|
-
*
|
|
171
|
+
* Also detects variant mismatch (e.g., localhost plugin was installed, now
|
|
172
|
+
* installing staging plugin) and migrates ANTHROPIC_BASE_URL + fixes
|
|
173
|
+
* ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN (#2174).
|
|
83
174
|
*/
|
|
84
175
|
function checkOrphanedRouting() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
89
|
-
const configPath = path.join(home, '.plexor', 'config.json');
|
|
176
|
+
// Use the resolved HOME_DIR (not process.env.HOME which may be wrong under sudo -u)
|
|
177
|
+
const settingsPath = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
178
|
+
const configPath = path.join(HOME_DIR, '.plexor', 'config.json');
|
|
90
179
|
|
|
91
180
|
try {
|
|
92
181
|
if (!fs.existsSync(settingsPath)) return;
|
|
93
182
|
|
|
94
183
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
95
184
|
const env = settings.env || {};
|
|
185
|
+
let settingsChanged = false;
|
|
96
186
|
|
|
97
|
-
const hasPlexorUrl = env.ANTHROPIC_BASE_URL
|
|
98
|
-
env.ANTHROPIC_BASE_URL.includes('plexor');
|
|
187
|
+
const hasPlexorUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL);
|
|
99
188
|
|
|
100
189
|
if (hasPlexorUrl) {
|
|
190
|
+
// Fix #2174: Only migrate ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN
|
|
191
|
+
// when routing through a Plexor-managed URL. Non-Plexor setups
|
|
192
|
+
// (direct Anthropic, etc.) should not have their auth mutated.
|
|
193
|
+
if (env.ANTHROPIC_API_KEY && !env.ANTHROPIC_AUTH_TOKEN) {
|
|
194
|
+
env.ANTHROPIC_AUTH_TOKEN = env.ANTHROPIC_API_KEY;
|
|
195
|
+
delete env.ANTHROPIC_API_KEY;
|
|
196
|
+
settings.env = env;
|
|
197
|
+
settingsChanged = true;
|
|
198
|
+
console.log('\n Migrated ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN (fix #2174)');
|
|
199
|
+
} else if (env.ANTHROPIC_API_KEY && env.ANTHROPIC_AUTH_TOKEN) {
|
|
200
|
+
// Both exist — remove the lower-precedence one to avoid confusion
|
|
201
|
+
delete env.ANTHROPIC_API_KEY;
|
|
202
|
+
settings.env = env;
|
|
203
|
+
settingsChanged = true;
|
|
204
|
+
console.log('\n Removed redundant ANTHROPIC_API_KEY (ANTHROPIC_AUTH_TOKEN takes precedence)');
|
|
205
|
+
}
|
|
101
206
|
// Check if there's a valid Plexor config
|
|
102
207
|
let hasValidConfig = false;
|
|
103
208
|
try {
|
|
104
209
|
if (fs.existsSync(configPath)) {
|
|
105
210
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
106
|
-
hasValidConfig = config.
|
|
211
|
+
hasValidConfig = (config.auth?.api_key || config.apiKey || '').startsWith('plx_');
|
|
107
212
|
}
|
|
108
213
|
} catch (e) {}
|
|
109
214
|
|
|
@@ -113,10 +218,31 @@ function checkOrphanedRouting() {
|
|
|
113
218
|
console.log(' Run /plexor-login to reconfigure, or');
|
|
114
219
|
console.log(' Run /plexor-uninstall to clean up\n');
|
|
115
220
|
} else {
|
|
116
|
-
|
|
117
|
-
|
|
221
|
+
// Fix #2176: Detect variant mismatch and migrate URL
|
|
222
|
+
const currentUrl = env.ANTHROPIC_BASE_URL;
|
|
223
|
+
if (currentUrl !== THIS_VARIANT_URL) {
|
|
224
|
+
env.ANTHROPIC_BASE_URL = THIS_VARIANT_URL;
|
|
225
|
+
settings.env = env;
|
|
226
|
+
settingsChanged = true;
|
|
227
|
+
console.log(`\n Migrated ANTHROPIC_BASE_URL to this variant's gateway:`);
|
|
228
|
+
console.log(` Old: ${currentUrl}`);
|
|
229
|
+
console.log(` New: ${THIS_VARIANT_URL}\n`);
|
|
230
|
+
} else {
|
|
231
|
+
console.log('\n Existing Plexor configuration detected');
|
|
232
|
+
console.log(' Your previous settings have been preserved.\n');
|
|
233
|
+
}
|
|
118
234
|
}
|
|
119
235
|
}
|
|
236
|
+
|
|
237
|
+
// Write back settings if any migration was applied
|
|
238
|
+
if (settingsChanged) {
|
|
239
|
+
const crypto = require('crypto');
|
|
240
|
+
const claudeDir = path.join(HOME_DIR, '.claude');
|
|
241
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
242
|
+
const tempPath = path.join(claudeDir, `.settings.${tempId}.tmp`);
|
|
243
|
+
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
244
|
+
fs.renameSync(tempPath, settingsPath);
|
|
245
|
+
}
|
|
120
246
|
} catch (e) {
|
|
121
247
|
// Ignore errors in detection - don't break install
|
|
122
248
|
}
|
|
@@ -295,13 +421,13 @@ function main() {
|
|
|
295
421
|
console.log('');
|
|
296
422
|
console.log(' For Claude MAX users (OAuth):');
|
|
297
423
|
console.log('');
|
|
298
|
-
console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
424
|
+
console.log(` echo 'export ANTHROPIC_BASE_URL="https://staging.api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
299
425
|
console.log(` source ${shellRc}`);
|
|
300
426
|
console.log('');
|
|
301
427
|
console.log(' For API key users (get key at https://plexor.dev/dashboard):');
|
|
302
428
|
console.log('');
|
|
303
|
-
console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
304
|
-
console.log(` echo 'export
|
|
429
|
+
console.log(` echo 'export ANTHROPIC_BASE_URL="https://staging.api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
430
|
+
console.log(` echo 'export ANTHROPIC_AUTH_TOKEN="plx_your_key_here"' >> ${shellRc}`);
|
|
305
431
|
console.log(` source ${shellRc}`);
|
|
306
432
|
console.log('');
|
|
307
433
|
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
package/scripts/uninstall.js
CHANGED
|
@@ -15,11 +15,34 @@
|
|
|
15
15
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const { execSync } = require('child_process');
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Get the correct home directory for the process's effective user.
|
|
23
|
+
* Resolves via /etc/passwd to handle sudo and sudo -u correctly.
|
|
24
|
+
*/
|
|
25
|
+
function getHomeDir() {
|
|
26
|
+
if (os.platform() !== 'win32') {
|
|
27
|
+
try {
|
|
28
|
+
const uid = process.getuid();
|
|
29
|
+
const entry = execSync(`getent passwd ${uid}`, { encoding: 'utf8' }).trim();
|
|
30
|
+
const fields = entry.split(':');
|
|
31
|
+
if (fields.length >= 6 && fields[5]) {
|
|
32
|
+
return fields[5];
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Fall through
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const h = os.homedir();
|
|
39
|
+
if (h) return h;
|
|
40
|
+
return process.env.HOME || process.env.USERPROFILE || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const home = getHomeDir();
|
|
21
44
|
if (!home) {
|
|
22
|
-
console.log('Warning:
|
|
45
|
+
console.log('Warning: Could not determine home directory, skipping cleanup');
|
|
23
46
|
process.exit(0);
|
|
24
47
|
}
|
|
25
48
|
|