@plexor-dev/claude-code-plugin-staging 0.1.0-beta.24 → 0.1.0-beta.26

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.
@@ -115,9 +115,9 @@ function main() {
115
115
  process.exit(1);
116
116
  }
117
117
 
118
- // CRITICAL FIX: Disable Claude Code routing in ~/.claude/settings.json
119
- // This removes ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN so Claude Code
120
- // no longer tries to route through Plexor with removed credentials
118
+ // Disable Claude Code routing in ~/.claude/settings.json.
119
+ // This removes the Plexor gateway URL and managed Plexor headers so Claude
120
+ // no longer routes through Plexor with removed credentials.
121
121
  const routingDisabled = settingsManager.disablePlexorRouting();
122
122
 
123
123
  // Clear session
@@ -93,7 +93,7 @@ function getGatewayLabel(apiUrl) {
93
93
  }
94
94
 
95
95
  function isRunningInsideClaudeSession(env = process.env) {
96
- return Boolean(env.CLAUDECODE);
96
+ return Boolean(env.CLAUDECODE || env.PLEXOR_SETUP_IN_CLAUDE);
97
97
  }
98
98
 
99
99
  function createSkipVerifyResult() {
@@ -143,6 +143,14 @@ function printUsage() {
143
143
  console.log(' PLEXOR_API_KEY=plx_... /plexor-setup');
144
144
  }
145
145
 
146
+ function printRestartBanner() {
147
+ const BRIGHT_YELLOW = '\x1b[93m';
148
+ const BOLD = '\x1b[1m';
149
+ const RESET = '\x1b[0m';
150
+ console.log('');
151
+ console.log(`${BOLD}${BRIGHT_YELLOW}RESTART CLAUDE NOW.${RESET}`);
152
+ }
153
+
146
154
  function printReceipt({ user, gateway, previousAuthPreserved, verifyResult }) {
147
155
  const line = (content) => `│ ${String(content).slice(0, 43).padEnd(43)}│`;
148
156
 
@@ -169,7 +177,11 @@ function printReceipt({ user, gateway, previousAuthPreserved, verifyResult }) {
169
177
  }
170
178
  console.log('└─────────────────────────────────────────────┘');
171
179
 
172
- if (!verifyResult.ok) {
180
+ if (verifyResult.pendingRestart) {
181
+ printRestartBanner();
182
+ }
183
+
184
+ if (!verifyResult.ok && !verifyResult.pendingRestart) {
173
185
  console.log('');
174
186
  console.log(`Verification failed: ${verifyResult.reason}`);
175
187
  }
@@ -1,14 +1,14 @@
1
1
  description: Guided first-run setup for Plexor with Claude Code (user)
2
2
  ---
3
3
 
4
- **RULE: Execute this workflow EXACTLY ONCE. After the Bash tool returns output, your ONLY action is to present that output to the user. DO NOT restart the workflow.**
4
+ **RULE: Execute this workflow EXACTLY ONCE. After the Bash tool returns output, your ONLY action is to present that output to the user. If the output says restart is required, add one standalone line exactly `RESTART CLAUDE NOW.` after the tool output. DO NOT restart the workflow.**
5
5
 
6
6
  Plexor setup is the primary human setup flow.
7
7
 
8
8
  If `$ARGUMENTS` already contains a Plexor API key (`plx_...`), run:
9
9
 
10
10
  ```bash
11
- node ~/.claude/plugins/plexor/commands/plexor-setup.js $ARGUMENTS
11
+ PLEXOR_SETUP_IN_CLAUDE=1 node ~/.claude/plugins/plexor/commands/plexor-setup.js $ARGUMENTS
12
12
  ```
13
13
 
14
14
  If the user did not provide a key yet, ask them:
@@ -18,11 +18,11 @@ If the user did not provide a key yet, ask them:
18
18
  After the user replies with the key, run:
19
19
 
20
20
  ```bash
21
- node ~/.claude/plugins/plexor/commands/plexor-setup.js <user_key>
21
+ PLEXOR_SETUP_IN_CLAUDE=1 node ~/.claude/plugins/plexor/commands/plexor-setup.js <user_key>
22
22
  ```
23
23
 
24
24
  This command:
25
25
  - saves the Plexor key
26
26
  - routes Claude through the Plexor staging gateway
27
- - preserves prior direct Claude auth for restore on logout/uninstall
27
+ - keeps Claude's existing direct auth intact and adds Plexor auth via managed headers
28
28
  - runs a deterministic Claude verification step
@@ -8,6 +8,7 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const https = require('https');
11
+ const { parseCustomHeaders } = require('../lib/config-utils');
11
12
 
12
13
  // Import centralized constants with HOME directory validation
13
14
  const { HOME_DIR, CONFIG_PATH, SESSION_PATH, SESSION_TIMEOUT_MS } = require('../lib/constants');
@@ -26,12 +27,15 @@ function isManagedGatewayUrl(baseUrl = '') {
26
27
  }
27
28
 
28
29
  function getPlexorAuthKey(env = {}) {
30
+ const headers = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
31
+ const headerApiKey = headers['x-plexor-key'] || '';
29
32
  const apiKey = env.ANTHROPIC_API_KEY || '';
30
33
  const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
31
34
 
35
+ if (headerApiKey.startsWith('plx_')) return headerApiKey;
32
36
  if (apiKey.startsWith('plx_')) return apiKey;
33
37
  if (authToken.startsWith('plx_')) return authToken;
34
- return apiKey || authToken || '';
38
+ return '';
35
39
  }
36
40
 
37
41
  function getDirectClaudeAuthState() {
@@ -58,7 +62,7 @@ function getDirectClaudeAuthState() {
58
62
  try {
59
63
  const data = fs.readFileSync(CLAUDE_STATE_PATH, 'utf8');
60
64
  const state = JSON.parse(data);
61
- if (state.primaryApiKey) {
65
+ if (state.primaryApiKey && !state.primaryApiKey.startsWith('plx_')) {
62
66
  return { present: true, source: '/login managed key' };
63
67
  }
64
68
  } catch {
@@ -18,15 +18,24 @@
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
20
  const os = require('os');
21
+ const { execSync } = require('child_process');
22
+ const { removeManagedStatusLine } = require('../lib/statusline-manager');
23
+ const { removeManagedHooks, cleanupLegacyManagedHooksFile } = require('../lib/hooks-manager');
24
+ const { removeManagedClaudeCustomHeadersFromEnv } = require('../lib/config-utils');
21
25
 
22
26
  // Get home directory, handling sudo case
23
27
  function getHomeDir() {
24
- if (process.env.SUDO_USER) {
25
- const platform = os.platform();
26
- if (platform === 'darwin') {
27
- return path.join('/Users', process.env.SUDO_USER);
28
- } else if (platform === 'linux') {
29
- return path.join('/home', process.env.SUDO_USER);
28
+ if (os.platform() !== 'win32' && process.env.SUDO_USER) {
29
+ try {
30
+ if (typeof process.getuid === 'function' && process.getuid() === 0) {
31
+ const entry = execSync(`getent passwd ${process.env.SUDO_USER}`, { encoding: 'utf8' }).trim();
32
+ const fields = entry.split(':');
33
+ if (fields.length >= 6 && fields[5]) {
34
+ return fields[5];
35
+ }
36
+ }
37
+ } catch {
38
+ // Fall through to HOME/os.homedir below.
30
39
  }
31
40
  }
32
41
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -36,22 +45,12 @@ const HOME_DIR = getHomeDir();
36
45
  const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
37
46
  const CLAUDE_COMMANDS_DIR = path.join(CLAUDE_DIR, 'commands');
38
47
  const CLAUDE_PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins', 'plexor');
48
+ const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
49
+ const CLAUDE_HOOKS_PATH = path.join(CLAUDE_DIR, 'settings.json');
50
+ const CLAUDE_LEGACY_HOOKS_PATH = path.join(CLAUDE_DIR, 'hooks.json');
39
51
  const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
40
52
 
41
53
  // All Plexor slash command files
42
- const PLEXOR_COMMANDS = [
43
- 'plexor-enabled.md',
44
- 'plexor-login.md',
45
- 'plexor-logout.md',
46
- 'plexor-setup.md',
47
- 'plexor-status.md',
48
- 'plexor-uninstall.md',
49
- 'plexor-mode.md',
50
- 'plexor-provider.md',
51
- 'plexor-settings.md',
52
- 'plexor-config.md'
53
- ];
54
-
55
54
  /**
56
55
  * Load settings manager if available
57
56
  */
@@ -115,10 +114,11 @@ function disableRoutingManually() {
115
114
  const hasManagedBaseUrl = isManagedGatewayUrl(settings.env.ANTHROPIC_BASE_URL || '');
116
115
  const hasPlexorAuthToken = isPlexorApiKey(settings.env.ANTHROPIC_AUTH_TOKEN || '');
117
116
  const hasPlexorApiKey = isPlexorApiKey(settings.env.ANTHROPIC_API_KEY || '');
117
+ const removedManagedHeaders = removeManagedClaudeCustomHeadersFromEnv(settings.env);
118
118
 
119
119
  const hasPreviousPrimaryApiKey = Boolean(settings.env[previousPrimaryApiKeyEnv]);
120
120
 
121
- if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey && !hasPreviousPrimaryApiKey) {
121
+ if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey && !hasPreviousPrimaryApiKey && !removedManagedHeaders) {
122
122
  return { success: true, message: 'Plexor routing not active' };
123
123
  }
124
124
 
@@ -191,7 +191,12 @@ function removeSlashCommands() {
191
191
  let removed = 0;
192
192
  let restored = 0;
193
193
 
194
- for (const cmd of PLEXOR_COMMANDS) {
194
+ if (!fs.existsSync(CLAUDE_COMMANDS_DIR)) {
195
+ return { removed, restored };
196
+ }
197
+
198
+ const plexorCommands = fs.readdirSync(CLAUDE_COMMANDS_DIR).filter((entry) => /^plexor-.*\.md$/i.test(entry));
199
+ for (const cmd of plexorCommands) {
195
200
  const cmdPath = path.join(CLAUDE_COMMANDS_DIR, cmd);
196
201
  const backupPath = cmdPath + '.backup';
197
202
 
@@ -229,6 +234,30 @@ function removePluginDirectory() {
229
234
  return false;
230
235
  }
231
236
 
237
+ function removeManagedStatusLineConfig() {
238
+ try {
239
+ return removeManagedStatusLine(CLAUDE_SETTINGS_PATH, HOME_DIR);
240
+ } catch (err) {
241
+ return { changed: false, restored: false, error: err.message };
242
+ }
243
+ }
244
+
245
+ function removeManagedHooksConfig() {
246
+ try {
247
+ return removeManagedHooks(CLAUDE_HOOKS_PATH);
248
+ } catch (err) {
249
+ return { changed: false, removed: 0, error: err.message };
250
+ }
251
+ }
252
+
253
+ function removeLegacyManagedHooksConfig() {
254
+ try {
255
+ return cleanupLegacyManagedHooksFile(CLAUDE_LEGACY_HOOKS_PATH);
256
+ } catch (err) {
257
+ return { changed: false, removed: 0, error: err.message };
258
+ }
259
+ }
260
+
232
261
  /**
233
262
  * Remove config directory
234
263
  */
@@ -255,7 +284,8 @@ function main() {
255
284
  console.log(' Cleans up Plexor integration before npm uninstall.');
256
285
  console.log('');
257
286
  console.log(' Options:');
258
- console.log(' --remove-config, -c Also remove ~/.plexor/ config directory');
287
+ console.log(' --keep-config, -k Preserve ~/.plexor/ config directory');
288
+ console.log(' --remove-config, -c Legacy alias (config is removed by default)');
259
289
  console.log(' --quiet, -q Suppress output messages');
260
290
  console.log(' --help, -h Show this help message');
261
291
  console.log('');
@@ -265,7 +295,7 @@ function main() {
265
295
  process.exit(0);
266
296
  }
267
297
 
268
- const removeConfig = args.includes('--remove-config') || args.includes('-c');
298
+ const removeConfig = !(args.includes('--keep-config') || args.includes('-k'));
269
299
  const quiet = args.includes('--quiet') || args.includes('-q');
270
300
 
271
301
  if (!quiet) {
@@ -295,7 +325,36 @@ function main() {
295
325
  : ` ✗ Failed to remove routing: ${routingResult.message}`);
296
326
  }
297
327
 
298
- // 2. Remove slash command .md files
328
+ // 2. Remove managed Plexor status line
329
+ const statusLineResult = removeManagedStatusLineConfig();
330
+ if (!quiet) {
331
+ if (statusLineResult.error) {
332
+ console.log(` ✗ Failed to clean Plexor status line: ${statusLineResult.error}`);
333
+ } else if (statusLineResult.changed) {
334
+ console.log(' ✓ Restored Claude status line configuration');
335
+ } else {
336
+ console.log(' ○ Claude status line already clean');
337
+ }
338
+ }
339
+
340
+ const hooksResult = removeManagedHooksConfig();
341
+ const legacyHooksResult = removeLegacyManagedHooksConfig();
342
+ if (!quiet) {
343
+ if (hooksResult.error) {
344
+ console.log(` ✗ Failed to clean Plexor hooks: ${hooksResult.error}`);
345
+ } else if (hooksResult.changed) {
346
+ console.log(' ✓ Restored Claude hook configuration');
347
+ } else {
348
+ console.log(' ○ Claude hooks already clean');
349
+ }
350
+ if (legacyHooksResult.error) {
351
+ console.log(` ✗ Failed to clean legacy ~/.claude/hooks.json: ${legacyHooksResult.error}`);
352
+ } else if (legacyHooksResult.changed) {
353
+ console.log(' ✓ Cleaned legacy ~/.claude/hooks.json');
354
+ }
355
+ }
356
+
357
+ // 3. Remove slash command .md files
299
358
  const cmdResult = removeSlashCommands();
300
359
  if (!quiet) {
301
360
  console.log(` ✓ Removed ${cmdResult.removed} slash command files`);
@@ -304,7 +363,7 @@ function main() {
304
363
  }
305
364
  }
306
365
 
307
- // 3. Remove plugin directory
366
+ // 4. Remove plugin directory
308
367
  const pluginRemoved = removePluginDirectory();
309
368
  if (!quiet) {
310
369
  console.log(pluginRemoved
@@ -312,7 +371,7 @@ function main() {
312
371
  : ' ○ Plugin directory not found (already clean)');
313
372
  }
314
373
 
315
- // 4. Optionally remove config directory
374
+ // 5. Remove config directory unless explicitly preserved
316
375
  if (removeConfig) {
317
376
  const configRemoved = removeConfigDirectory();
318
377
  if (!quiet) {
@@ -337,8 +396,7 @@ function main() {
337
396
 
338
397
  if (!removeConfig) {
339
398
  console.log(' Note: ~/.plexor/ config directory was preserved.');
340
- console.log(' To also remove it: plexor-uninstall --remove-config');
341
- console.log(' Or manually: rm -rf ~/.plexor');
399
+ console.log(' To remove it manually: rm -rf ~/.plexor');
342
400
  console.log('');
343
401
  }
344
402
  }
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const https = require('https');
6
+ const http = require('http');
7
+ const { CONFIG_PATH, SESSION_PATH, PLEXOR_DIR } = require('../lib/constants');
8
+
9
+ function readJson(filePath) {
10
+ try {
11
+ if (!fs.existsSync(filePath)) return null;
12
+ const raw = fs.readFileSync(filePath, 'utf8');
13
+ return raw && raw.trim() ? JSON.parse(raw) : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function writeJsonAtomically(filePath, value) {
20
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
21
+ const tempPath = `${filePath}.tmp.${Date.now()}`;
22
+ fs.writeFileSync(tempPath, JSON.stringify(value, null, 2), { mode: 0o600 });
23
+ fs.renameSync(tempPath, filePath);
24
+ }
25
+
26
+ function readStdin() {
27
+ return new Promise((resolve) => {
28
+ let data = '';
29
+ process.stdin.setEncoding('utf8');
30
+ process.stdin.on('data', (chunk) => { data += chunk; });
31
+ process.stdin.on('end', () => resolve(data));
32
+ process.stdin.resume();
33
+ });
34
+ }
35
+
36
+ function clampNonNegative(value) {
37
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
38
+ }
39
+
40
+ function finiteOrZero(value) {
41
+ return Number.isFinite(value) ? value : 0;
42
+ }
43
+
44
+ function parseStats(payload) {
45
+ const summary = payload?.summary || payload || {};
46
+ return {
47
+ total_requests: Number(summary.total_requests ?? payload?.total_requests ?? 0),
48
+ total_optimizations: Number(summary.total_optimizations ?? payload?.total_optimizations ?? 0),
49
+ original_tokens: Number(summary.original_tokens ?? payload?.original_tokens ?? 0),
50
+ optimized_tokens: Number(summary.optimized_tokens ?? payload?.optimized_tokens ?? 0),
51
+ tokens_saved: Number(summary.tokens_saved ?? payload?.tokens_saved ?? 0),
52
+ total_cost: Number(summary.total_cost ?? payload?.total_cost ?? 0),
53
+ baseline_cost: Number(summary.baseline_cost ?? payload?.baseline_cost ?? payload?.total_baseline_cost ?? 0),
54
+ cost_saved: Number(summary.cost_saved ?? payload?.cost_saved ?? payload?.total_savings ?? 0)
55
+ };
56
+ }
57
+
58
+ function fetchJson(apiUrl, endpoint, apiKey) {
59
+ return new Promise((resolve, reject) => {
60
+ const url = new URL(endpoint, apiUrl.replace(/\/$/, '') + '/');
61
+ const client = url.protocol === 'https:' ? https : http;
62
+ const req = client.request({
63
+ hostname: url.hostname,
64
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
65
+ path: `${url.pathname}${url.search}`,
66
+ method: 'GET',
67
+ headers: {
68
+ 'X-API-Key': apiKey,
69
+ 'X-Plexor-Key': apiKey,
70
+ 'User-Agent': 'plexor-session-sync/0.1.0-beta.24'
71
+ },
72
+ timeout: 5000
73
+ }, (res) => {
74
+ let body = '';
75
+ res.on('data', (chunk) => { body += chunk; });
76
+ res.on('end', () => {
77
+ try {
78
+ const parsed = JSON.parse(body);
79
+ if (res.statusCode >= 200 && res.statusCode < 300) {
80
+ resolve(parsed);
81
+ } else {
82
+ reject(new Error(parsed.message || `HTTP ${res.statusCode}`));
83
+ }
84
+ } catch {
85
+ reject(new Error(`Invalid JSON response: ${body.slice(0, 120)}`));
86
+ }
87
+ });
88
+ });
89
+ req.on('error', reject);
90
+ req.on('timeout', () => {
91
+ req.destroy(new Error('Request timeout'));
92
+ });
93
+ req.end();
94
+ });
95
+ }
96
+
97
+ function loadConfig() {
98
+ const config = readJson(CONFIG_PATH);
99
+ const apiKey = config?.auth?.api_key || '';
100
+ const apiUrl = config?.settings?.apiUrl || 'https://staging.api.plexor.dev';
101
+ const enabled = config?.settings?.enabled !== false;
102
+ if (!enabled || !apiKey.startsWith('plx_')) {
103
+ return null;
104
+ }
105
+ return { config, apiKey, apiUrl };
106
+ }
107
+
108
+ function buildSessionRecord(sessionId, baseline, nowIso) {
109
+ return {
110
+ session_id: sessionId || `session_${Date.now()}`,
111
+ started_at: nowIso,
112
+ last_activity: Date.now(),
113
+ requests: 0,
114
+ optimizations: 0,
115
+ cache_hits: 0,
116
+ passthroughs: 0,
117
+ original_tokens: 0,
118
+ optimized_tokens: 0,
119
+ tokens_saved: 0,
120
+ output_tokens: 0,
121
+ baseline_cost: 0,
122
+ actual_cost: 0,
123
+ cost_delta: 0,
124
+ cost_saved: 0,
125
+ _baseline: baseline
126
+ };
127
+ }
128
+
129
+ async function main() {
130
+ const mode = process.argv[2] || 'stop';
131
+ const inputRaw = await readStdin();
132
+ let input = {};
133
+ try {
134
+ input = inputRaw && inputRaw.trim() ? JSON.parse(inputRaw) : {};
135
+ } catch {
136
+ input = {};
137
+ }
138
+
139
+ if (mode === 'end') {
140
+ try {
141
+ fs.rmSync(SESSION_PATH, { force: true });
142
+ } catch {
143
+ // Session cleanup must not interrupt Claude shutdown.
144
+ }
145
+ process.exit(0);
146
+ }
147
+
148
+ const loaded = loadConfig();
149
+ if (!loaded) {
150
+ process.exit(0);
151
+ }
152
+
153
+ try {
154
+ const statsPayload = await fetchJson(loaded.apiUrl, '/v1/stats', loaded.apiKey);
155
+ const current = parseStats(statsPayload);
156
+ const nowIso = new Date().toISOString();
157
+ const sessionId = input.session_id || input.sessionId || null;
158
+ const existing = readJson(SESSION_PATH);
159
+
160
+ if (mode === 'start' || !existing || !existing._baseline || (sessionId && existing.session_id !== sessionId)) {
161
+ const baseline = { ...current };
162
+ writeJsonAtomically(SESSION_PATH, buildSessionRecord(sessionId, baseline, nowIso));
163
+ process.exit(0);
164
+ }
165
+
166
+ const baseline = existing._baseline || {};
167
+ const baselineCost = clampNonNegative(current.baseline_cost - Number(baseline.baseline_cost || 0));
168
+ const actualCost = clampNonNegative(current.total_cost - Number(baseline.total_cost || 0));
169
+ const next = {
170
+ ...existing,
171
+ session_id: sessionId || existing.session_id,
172
+ last_activity: Date.now(),
173
+ requests: clampNonNegative(current.total_requests - Number(baseline.total_requests || 0)),
174
+ optimizations: clampNonNegative(current.total_optimizations - Number(baseline.total_optimizations || 0)),
175
+ original_tokens: clampNonNegative(current.original_tokens - Number(baseline.original_tokens || 0)),
176
+ optimized_tokens: clampNonNegative(current.optimized_tokens - Number(baseline.optimized_tokens || 0)),
177
+ tokens_saved: clampNonNegative(current.tokens_saved - Number(baseline.tokens_saved || 0)),
178
+ baseline_cost: baselineCost,
179
+ actual_cost: actualCost,
180
+ cost_delta: finiteOrZero(baselineCost - actualCost),
181
+ cost_saved: clampNonNegative(current.cost_saved - Number(baseline.cost_saved || 0)),
182
+ _baseline: baseline
183
+ };
184
+
185
+ if (!fs.existsSync(PLEXOR_DIR)) {
186
+ fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
187
+ }
188
+ writeJsonAtomically(SESSION_PATH, next);
189
+ } catch {
190
+ // Hook failures must not interrupt Claude.
191
+ }
192
+ }
193
+
194
+ main();
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { execSync } = require('child_process');
7
+ const { PREVIOUS_STATUS_LINE_KEY, getManagedStatusLineCommand } = require('../lib/statusline-manager');
8
+
9
+ const ANSI_BLUE = '\x1b[38;5;153m';
10
+ const ANSI_GREEN = '\x1b[32m';
11
+ const ANSI_RESET = '\x1b[0m';
12
+
13
+ function readJson(filePath) {
14
+ try {
15
+ if (!fs.existsSync(filePath)) return null;
16
+ const raw = fs.readFileSync(filePath, 'utf8');
17
+ return raw && raw.trim() ? JSON.parse(raw) : null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function isManagedGatewayUrl(baseUrl = '') {
24
+ return (
25
+ baseUrl.includes('plexor') ||
26
+ baseUrl.includes('staging.api') ||
27
+ baseUrl.includes('localhost') ||
28
+ baseUrl.includes('127.0.0.1') ||
29
+ baseUrl.includes('ngrok') ||
30
+ baseUrl.includes('localtunnel')
31
+ );
32
+ }
33
+
34
+ function getGatewayLabel(baseUrl = '') {
35
+ if (baseUrl.includes('staging.api')) return 'staging';
36
+ if (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1')) return 'localhost';
37
+ return 'prod';
38
+ }
39
+
40
+ function formatUsd(value) {
41
+ if (!Number.isFinite(value)) {
42
+ return '';
43
+ }
44
+
45
+ if (Math.abs(value) === 0) {
46
+ return '$0.00';
47
+ }
48
+ if (Math.abs(value) >= 1) {
49
+ return `$${value.toFixed(2)}`;
50
+ }
51
+ if (Math.abs(value) >= 0.01) {
52
+ return `$${value.toFixed(3)}`;
53
+ }
54
+ return `$${value.toFixed(4)}`;
55
+ }
56
+
57
+ function runPreviousStatusLine(statusLine, homeDir) {
58
+ if (!statusLine || typeof statusLine !== 'object' || statusLine.type !== 'command' || !statusLine.command) {
59
+ return '';
60
+ }
61
+
62
+ const managedCommand = getManagedStatusLineCommand(homeDir);
63
+ if (String(statusLine.command) === managedCommand) {
64
+ return '';
65
+ }
66
+
67
+ try {
68
+ const output = execSync(String(statusLine.command), {
69
+ cwd: homeDir,
70
+ encoding: 'utf8',
71
+ stdio: ['ignore', 'pipe', 'ignore'],
72
+ timeout: 1000
73
+ }).trim();
74
+ return output.split('\n')[0] || '';
75
+ } catch {
76
+ return '';
77
+ }
78
+ }
79
+
80
+ function buildPlexorSegment(config, settings) {
81
+ const authKey = config?.auth?.api_key || '';
82
+ const enabled = config?.settings?.enabled !== false;
83
+ const baseUrl = settings?.env?.ANTHROPIC_BASE_URL || '';
84
+ if (!enabled || !authKey.startsWith('plx_') || !isManagedGatewayUrl(baseUrl)) {
85
+ return '';
86
+ }
87
+
88
+ const gateway = getGatewayLabel(baseUrl);
89
+ const mode = config?.settings?.mode || 'balanced';
90
+ const provider = config?.settings?.preferred_provider || 'auto';
91
+ return `PLEXOR ${gateway} ${mode} ${provider}`;
92
+ }
93
+
94
+ function buildSavingsSegment(session) {
95
+ const explicitDelta = Number(session?.cost_delta);
96
+ const costSaved = Number(session?.cost_saved);
97
+ const rawCostDelta = Number.isFinite(explicitDelta)
98
+ ? explicitDelta
99
+ : ((Number.isFinite(costSaved) && costSaved !== 0)
100
+ ? costSaved
101
+ : (Number(session?.baseline_cost) - Number(session?.actual_cost)));
102
+ const costDelta = Number.isFinite(rawCostDelta) ? rawCostDelta : 0;
103
+ return `cost-delta ${costDelta < 0 ? '-' : ''}${formatUsd(Math.abs(costDelta))}`;
104
+ }
105
+
106
+ function main() {
107
+ const homeDir = os.homedir();
108
+ const config = readJson(path.join(homeDir, '.plexor', 'config.json')) || {};
109
+ const settings = readJson(path.join(homeDir, '.claude', 'settings.json')) || {};
110
+ const session = readJson(path.join(homeDir, '.plexor', 'session.json')) || {};
111
+
112
+ const plexorSegment = buildPlexorSegment(config, settings);
113
+ const savingsSegment = plexorSegment ? buildSavingsSegment(session) : '';
114
+ const previous = runPreviousStatusLine(settings[PREVIOUS_STATUS_LINE_KEY], homeDir);
115
+
116
+ const coloredPlexor = plexorSegment ? `${ANSI_BLUE}${plexorSegment}${ANSI_RESET}` : '';
117
+ const coloredSavings = savingsSegment ? `${ANSI_GREEN}${savingsSegment}${ANSI_RESET}` : '';
118
+
119
+ if (!coloredPlexor && previous) {
120
+ process.stdout.write(previous);
121
+ return;
122
+ }
123
+
124
+ const line = [coloredPlexor, coloredSavings, previous].filter(Boolean).join(' · ');
125
+ if (line) {
126
+ process.stdout.write(line);
127
+ }
128
+ }
129
+
130
+ main();