@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.
@@ -12,6 +12,7 @@ const DISABLED_HINT_VALUES = new Set(['', 'auto', 'none', 'off']);
12
12
  const VALID_ORCHESTRATION_MODES = new Set(['supervised', 'autonomous', 'danger-full-auto']);
13
13
  const VALID_ROUTING_MODES = new Set(['eco', 'balanced', 'quality', 'passthrough', 'cost']);
14
14
  const MANAGED_HEADER_KEYS = new Set([
15
+ 'x-plexor-key',
15
16
  'x-force-provider',
16
17
  'x-force-model',
17
18
  'x-allow-providers',
@@ -23,6 +24,10 @@ const MANAGED_HEADER_KEYS = new Set([
23
24
  ]);
24
25
  const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
25
26
 
27
+ function isPlexorApiKey(value = '') {
28
+ return typeof value === 'string' && value.startsWith('plx_');
29
+ }
30
+
26
31
  function normalizeForcedProvider(value) {
27
32
  if (typeof value !== 'string') {
28
33
  return null;
@@ -113,9 +118,63 @@ function serializeCustomHeaders(headers) {
113
118
  .join('\n');
114
119
  }
115
120
 
121
+ function removeManagedClaudeCustomHeadersFromEnv(env = {}) {
122
+ const existing = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
123
+ let removed = false;
124
+
125
+ for (const key of MANAGED_HEADER_KEYS) {
126
+ if (Object.prototype.hasOwnProperty.call(existing, key)) {
127
+ delete existing[key];
128
+ removed = true;
129
+ }
130
+ }
131
+
132
+ if (!removed) {
133
+ return false;
134
+ }
135
+
136
+ if (Object.keys(existing).length) {
137
+ env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(existing);
138
+ } else {
139
+ delete env.ANTHROPIC_CUSTOM_HEADERS;
140
+ }
141
+
142
+ return true;
143
+ }
144
+
145
+ function getManagedPlexorAuthHeader(env = {}) {
146
+ const existing = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
147
+ const managedAuthKey = existing['x-plexor-key'] || '';
148
+ return isPlexorApiKey(managedAuthKey) ? managedAuthKey : '';
149
+ }
150
+
151
+ function upsertManagedPlexorAuthHeader(env = {}, apiKey = '') {
152
+ const existing = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
153
+ const previousHeaders = env.ANTHROPIC_CUSTOM_HEADERS || '';
154
+
155
+ if (isPlexorApiKey(apiKey)) {
156
+ existing['x-plexor-key'] = apiKey;
157
+ } else {
158
+ delete existing['x-plexor-key'];
159
+ }
160
+
161
+ if (Object.keys(existing).length) {
162
+ env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(existing);
163
+ } else {
164
+ delete env.ANTHROPIC_CUSTOM_HEADERS;
165
+ }
166
+
167
+ return (env.ANTHROPIC_CUSTOM_HEADERS || '') !== previousHeaders;
168
+ }
169
+
116
170
  function buildManagedAnthropicHeaders(config) {
117
171
  const settings = config?.settings || {};
118
172
  const headers = {};
173
+ const apiKey = config?.auth?.api_key || config?.auth?.apiKey || config?.apiKey || '';
174
+
175
+ if (isPlexorApiKey(apiKey)) {
176
+ headers['x-plexor-key'] = apiKey;
177
+ }
119
178
 
120
179
  const modeRaw = String(settings.mode || '')
121
180
  .trim()
@@ -170,6 +229,25 @@ function buildManagedAnthropicHeaders(config) {
170
229
  return headers;
171
230
  }
172
231
 
232
+ function applyManagedClaudeCustomHeadersToEnv(env = {}, config = {}) {
233
+ const previousHeaders = env.ANTHROPIC_CUSTOM_HEADERS || '';
234
+ const existing = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
235
+ for (const key of MANAGED_HEADER_KEYS) {
236
+ delete existing[key];
237
+ }
238
+
239
+ const managed = buildManagedAnthropicHeaders(config);
240
+ const merged = { ...existing, ...managed };
241
+
242
+ if (Object.keys(merged).length) {
243
+ env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(merged);
244
+ } else {
245
+ delete env.ANTHROPIC_CUSTOM_HEADERS;
246
+ }
247
+
248
+ return (env.ANTHROPIC_CUSTOM_HEADERS || '') !== previousHeaders;
249
+ }
250
+
173
251
  function syncClaudeCustomHeaders(config) {
174
252
  try {
175
253
  let settings = {};
@@ -183,19 +261,7 @@ function syncClaudeCustomHeaders(config) {
183
261
  }
184
262
  settings.env = settings.env && typeof settings.env === 'object' ? settings.env : {};
185
263
 
186
- const existing = parseCustomHeaders(settings.env.ANTHROPIC_CUSTOM_HEADERS);
187
- for (const key of MANAGED_HEADER_KEYS) {
188
- delete existing[key];
189
- }
190
-
191
- const managed = buildManagedAnthropicHeaders(config);
192
- const merged = { ...existing, ...managed };
193
-
194
- if (Object.keys(merged).length) {
195
- settings.env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(merged);
196
- } else {
197
- delete settings.env.ANTHROPIC_CUSTOM_HEADERS;
198
- }
264
+ applyManagedClaudeCustomHeadersToEnv(settings.env, config);
199
265
 
200
266
  const claudeDir = path.dirname(CLAUDE_SETTINGS_PATH);
201
267
  if (!fs.existsSync(claudeDir)) {
@@ -308,6 +374,10 @@ module.exports = {
308
374
  readSetting,
309
375
  hasForcedHintConflict,
310
376
  validateForcedHintConfig,
377
+ applyManagedClaudeCustomHeadersToEnv,
378
+ getManagedPlexorAuthHeader,
379
+ upsertManagedPlexorAuthHeader,
380
+ removeManagedClaudeCustomHeadersFromEnv,
311
381
  parseCustomHeaders,
312
382
  serializeCustomHeaders,
313
383
  buildManagedAnthropicHeaders
@@ -0,0 +1,209 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function quoteForPosixShell(value) {
5
+ return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
6
+ }
7
+
8
+ function writeJsonAtomically(filePath, value) {
9
+ const dir = path.dirname(filePath);
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ const tempPath = `${filePath}.tmp.${Date.now()}`;
12
+ fs.writeFileSync(tempPath, JSON.stringify(value, null, 2), { mode: 0o600 });
13
+ fs.renameSync(tempPath, filePath);
14
+ }
15
+
16
+ function normalizeSettings(value) {
17
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
18
+ return {};
19
+ }
20
+ return { ...value };
21
+ }
22
+
23
+ function normalizeHookMatchers(value) {
24
+ if (!Array.isArray(value)) {
25
+ return [];
26
+ }
27
+ return value.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry));
28
+ }
29
+
30
+ function normalizeHookList(value) {
31
+ if (!Array.isArray(value)) {
32
+ return [];
33
+ }
34
+ return value.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry));
35
+ }
36
+
37
+ function readSettings(settingsPath) {
38
+ if (!fs.existsSync(settingsPath)) {
39
+ return { settings: {}, existed: false };
40
+ }
41
+
42
+ try {
43
+ const raw = fs.readFileSync(settingsPath, 'utf8');
44
+ if (!raw || !raw.trim()) {
45
+ return { settings: {}, existed: true };
46
+ }
47
+ return { settings: normalizeSettings(JSON.parse(raw)), existed: true };
48
+ } catch {
49
+ return { settings: {}, existed: true };
50
+ }
51
+ }
52
+
53
+ function getManagedSessionSyncPath(homeDir) {
54
+ return path.join(homeDir, '.claude', 'plugins', 'plexor', 'hooks', 'session-sync.js');
55
+ }
56
+
57
+ function getSafeManagedCommand(scriptPath, args = []) {
58
+ const quotedPath = quoteForPosixShell(scriptPath);
59
+ const quotedArgs = args.map(quoteForPosixShell).join(' ');
60
+ const argSuffix = quotedArgs ? ` ${quotedArgs}` : '';
61
+ return `/bin/sh -lc 'script=$1; shift; [ -x \"$script\" ] || exit 0; exec \"$script\" \"$@\"' -- ${quotedPath}${argSuffix}`;
62
+ }
63
+
64
+ function getManagedSessionSyncCommand(homeDir, mode) {
65
+ return getSafeManagedCommand(getManagedSessionSyncPath(homeDir), [mode]);
66
+ }
67
+
68
+ function getManagedHookMatchers(homeDir) {
69
+ return {
70
+ SessionStart: [{
71
+ matcher: 'startup|resume|clear|compact',
72
+ hooks: [{
73
+ type: 'command',
74
+ command: getManagedSessionSyncCommand(homeDir, 'start'),
75
+ timeout: 5
76
+ }]
77
+ }],
78
+ Stop: [{
79
+ hooks: [{
80
+ type: 'command',
81
+ command: getManagedSessionSyncCommand(homeDir, 'stop'),
82
+ timeout: 5
83
+ }]
84
+ }],
85
+ SessionEnd: [{
86
+ hooks: [{
87
+ type: 'command',
88
+ command: getManagedSessionSyncCommand(homeDir, 'end'),
89
+ timeout: 5
90
+ }]
91
+ }]
92
+ };
93
+ }
94
+
95
+ function isManagedCommandHook(hook) {
96
+ const command = String(hook?.command || '');
97
+ return command.includes('/.claude/plugins/plexor/hooks/session-sync.js');
98
+ }
99
+
100
+ function isManagedMatcher(entry) {
101
+ const hooks = normalizeHookList(entry?.hooks);
102
+ return hooks.some(isManagedCommandHook);
103
+ }
104
+
105
+ function upsertManagedHooks(settingsPath, homeDir) {
106
+ const { settings, existed } = readSettings(settingsPath);
107
+ const currentSettings = normalizeSettings(settings);
108
+ const currentHooks = normalizeSettings(currentSettings.hooks);
109
+ const managedMatchers = getManagedHookMatchers(homeDir);
110
+ const nextHooks = { ...currentHooks };
111
+
112
+ for (const [eventName, managedEntries] of Object.entries(managedMatchers)) {
113
+ const preserved = normalizeHookMatchers(currentHooks[eventName]).filter((entry) => !isManagedMatcher(entry));
114
+ nextHooks[eventName] = [...preserved, ...managedEntries];
115
+ }
116
+
117
+ const nextSettings = { ...currentSettings, hooks: nextHooks };
118
+ const changed = JSON.stringify(currentSettings) !== JSON.stringify(nextSettings);
119
+ if (changed) {
120
+ writeJsonAtomically(settingsPath, nextSettings);
121
+ }
122
+
123
+ return { changed, existed };
124
+ }
125
+
126
+ function removeManagedHooks(settingsPath) {
127
+ const { settings, existed } = readSettings(settingsPath);
128
+ if (!existed) {
129
+ return { changed: false, removed: 0 };
130
+ }
131
+
132
+ const currentSettings = normalizeSettings(settings);
133
+ const currentHooks = normalizeSettings(currentSettings.hooks);
134
+ const nextHooks = { ...currentHooks };
135
+ let removed = 0;
136
+
137
+ for (const [eventName, entries] of Object.entries(currentHooks)) {
138
+ const normalizedEntries = normalizeHookMatchers(entries);
139
+ const filteredEntries = normalizedEntries.filter((entry) => !isManagedMatcher(entry));
140
+ removed += normalizedEntries.length - filteredEntries.length;
141
+ if (filteredEntries.length > 0) {
142
+ nextHooks[eventName] = filteredEntries;
143
+ } else {
144
+ delete nextHooks[eventName];
145
+ }
146
+ }
147
+
148
+ const nextSettings = { ...currentSettings };
149
+ if (Object.keys(nextHooks).length > 0) {
150
+ nextSettings.hooks = nextHooks;
151
+ } else {
152
+ delete nextSettings.hooks;
153
+ }
154
+
155
+ const changed = JSON.stringify(currentSettings) !== JSON.stringify(nextSettings);
156
+ if (changed) {
157
+ writeJsonAtomically(settingsPath, nextSettings);
158
+ }
159
+
160
+ return { changed, removed };
161
+ }
162
+
163
+ function cleanupLegacyManagedHooksFile(legacyHooksPath) {
164
+ if (!fs.existsSync(legacyHooksPath)) {
165
+ return { changed: false, removed: 0 };
166
+ }
167
+
168
+ try {
169
+ const raw = fs.readFileSync(legacyHooksPath, 'utf8');
170
+ if (!raw || !raw.trim()) {
171
+ fs.rmSync(legacyHooksPath, { force: true });
172
+ return { changed: true, removed: 0 };
173
+ }
174
+
175
+ const parsed = JSON.parse(raw);
176
+ const currentHooks = normalizeHookList(parsed.hooks);
177
+ const nextHooks = currentHooks.filter((entry) => {
178
+ const event = String(entry?.event || '');
179
+ const script = String(entry?.script || '');
180
+ const isLegacyManagedEvent = event === 'pre_model_request' || event === 'post_model_response';
181
+ const isLegacyManagedScript = script.includes('/.claude/plugins/plexor/hooks/intercept.js') ||
182
+ script.includes('/.claude/plugins/plexor/hooks/track-response.js');
183
+ return !(isLegacyManagedEvent && isLegacyManagedScript);
184
+ });
185
+ const removed = currentHooks.length - nextHooks.length;
186
+
187
+ if (removed === 0) {
188
+ return { changed: false, removed: 0 };
189
+ }
190
+
191
+ if (nextHooks.length === 0) {
192
+ fs.rmSync(legacyHooksPath, { force: true });
193
+ } else {
194
+ writeJsonAtomically(legacyHooksPath, { ...parsed, hooks: nextHooks });
195
+ }
196
+ return { changed: true, removed };
197
+ } catch {
198
+ return { changed: false, removed: 0 };
199
+ }
200
+ }
201
+
202
+ module.exports = {
203
+ getManagedSessionSyncPath,
204
+ getManagedSessionSyncCommand,
205
+ getManagedHookMatchers,
206
+ upsertManagedHooks,
207
+ removeManagedHooks,
208
+ cleanupLegacyManagedHooksFile
209
+ };
@@ -14,11 +14,15 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
  const os = require('os');
16
16
  const crypto = require('crypto');
17
+ const {
18
+ getManagedPlexorAuthHeader,
19
+ removeManagedClaudeCustomHeadersFromEnv,
20
+ upsertManagedPlexorAuthHeader
21
+ } = require('./config-utils');
17
22
 
18
23
  const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
19
24
  const CLAUDE_STATE_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude.json');
20
25
  const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
21
- const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
22
26
 
23
27
  // Plexor gateway endpoints
24
28
  const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
@@ -48,52 +52,56 @@ function isPlexorApiKey(value = '') {
48
52
  }
49
53
 
50
54
  function getPlexorAuthKey(env = {}) {
55
+ const headerKey = getManagedPlexorAuthHeader(env);
51
56
  const apiKey = env.ANTHROPIC_API_KEY || '';
52
57
  const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
53
58
 
59
+ if (isPlexorApiKey(headerKey)) return headerKey;
54
60
  if (isPlexorApiKey(apiKey)) return apiKey;
55
61
  if (isPlexorApiKey(authToken)) return authToken;
56
- return apiKey || authToken || '';
62
+ return '';
57
63
  }
58
64
 
59
- function hasPlexorManagedAuth(env = {}) {
60
- return (
61
- isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '') ||
62
- isPlexorApiKey(env.ANTHROPIC_API_KEY || '') ||
63
- isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '')
64
- );
65
- }
66
-
67
- function stashDirectAuthEnv(env, plexorApiKey) {
68
- const alreadyManaged = hasPlexorManagedAuth(env);
69
- const currentApiKey = env.ANTHROPIC_API_KEY || '';
70
- const currentAuthToken = env.ANTHROPIC_AUTH_TOKEN || '';
65
+ function clearLegacyPlexorAuthValue(env = {}, field, backupField) {
66
+ let changed = false;
67
+ const currentValue = env[field] || '';
68
+ const backupValue = env[backupField] || '';
71
69
 
72
- if (currentApiKey && !isPlexorApiKey(currentApiKey) && currentApiKey !== plexorApiKey) {
73
- env[PREVIOUS_API_KEY_ENV] = currentApiKey;
74
- } else if (!alreadyManaged) {
75
- delete env[PREVIOUS_API_KEY_ENV];
70
+ if (isPlexorApiKey(currentValue)) {
71
+ if (backupValue && !isPlexorApiKey(backupValue)) {
72
+ env[field] = backupValue;
73
+ } else {
74
+ delete env[field];
75
+ }
76
+ changed = true;
76
77
  }
77
78
 
78
- if (currentAuthToken && !isPlexorApiKey(currentAuthToken) && currentAuthToken !== plexorApiKey) {
79
- env[PREVIOUS_AUTH_TOKEN_ENV] = currentAuthToken;
80
- } else if (!alreadyManaged) {
81
- delete env[PREVIOUS_AUTH_TOKEN_ENV];
79
+ if (backupValue) {
80
+ delete env[backupField];
81
+ changed = true;
82
82
  }
83
+
84
+ return changed;
83
85
  }
84
86
 
85
- function setPlexorAuthKey(env, apiKey) {
86
- stashDirectAuthEnv(env, apiKey);
87
- env.ANTHROPIC_API_KEY = apiKey;
88
- env.ANTHROPIC_AUTH_TOKEN = apiKey;
87
+ function migrateLegacyPlexorAuthEnv(env = {}, claudeState = {}) {
88
+ let changed = false;
89
+
90
+ changed = clearLegacyPlexorAuthValue(env, 'ANTHROPIC_API_KEY', PREVIOUS_API_KEY_ENV) || changed;
91
+ changed = clearLegacyPlexorAuthValue(env, 'ANTHROPIC_AUTH_TOKEN', PREVIOUS_AUTH_TOKEN_ENV) || changed;
92
+
93
+ changed = restoreDirectPrimaryApiKey(env, claudeState) || changed;
94
+
95
+ return changed;
89
96
  }
90
97
 
91
98
  function clearPlexorRoutingEnv(env = {}) {
92
99
  const hasManagedBaseUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '');
93
100
  const hasPlexorApiKey = isPlexorApiKey(env.ANTHROPIC_API_KEY || '');
94
101
  const hasPlexorAuthToken = isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '');
102
+ const removedManagedHeaders = removeManagedClaudeCustomHeadersFromEnv(env);
95
103
 
96
- if (!hasManagedBaseUrl && !hasPlexorApiKey && !hasPlexorAuthToken) {
104
+ if (!hasManagedBaseUrl && !hasPlexorApiKey && !hasPlexorAuthToken && !removedManagedHeaders) {
97
105
  return false;
98
106
  }
99
107
 
@@ -120,17 +128,6 @@ function clearPlexorRoutingEnv(env = {}) {
120
128
  return true;
121
129
  }
122
130
 
123
- function stashDirectPrimaryApiKey(env = {}, claudeState = {}, plexorApiKey = '') {
124
- const currentPrimaryApiKey = claudeState.primaryApiKey || '';
125
- if (!currentPrimaryApiKey || isPlexorApiKey(currentPrimaryApiKey) || currentPrimaryApiKey === plexorApiKey) {
126
- return false;
127
- }
128
-
129
- env[PREVIOUS_PRIMARY_API_KEY_ENV] = currentPrimaryApiKey;
130
- delete claudeState.primaryApiKey;
131
- return true;
132
- }
133
-
134
131
  function restoreDirectPrimaryApiKey(env = {}, claudeState = {}, options = {}) {
135
132
  const { consumeBackup = true } = options;
136
133
  const previousPrimaryApiKey = env[PREVIOUS_PRIMARY_API_KEY_ENV] || '';
@@ -346,6 +343,7 @@ class ClaudeSettingsManager {
346
343
 
347
344
  try {
348
345
  const settings = this.load();
346
+ const previousSettings = JSON.parse(JSON.stringify(settings));
349
347
  const claudeState = this.loadClaudeState();
350
348
 
351
349
  // Initialize env block if doesn't exist
@@ -353,11 +351,9 @@ class ClaudeSettingsManager {
353
351
  settings.env = {};
354
352
  }
355
353
 
356
- // Mirror the Plexor key into both Claude auth env vars so every Claude
357
- // runtime path uses the gateway instead of any previously saved direct key.
358
354
  settings.env.ANTHROPIC_BASE_URL = apiUrl;
359
- setPlexorAuthKey(settings.env, apiKey);
360
- const claudeStateChanged = stashDirectPrimaryApiKey(settings.env, claudeState, apiKey);
355
+ upsertManagedPlexorAuthHeader(settings.env, apiKey);
356
+ const claudeStateChanged = migrateLegacyPlexorAuthEnv(settings.env, claudeState);
361
357
 
362
358
  const success = this.save(settings);
363
359
  if (!success) {
@@ -365,6 +361,7 @@ class ClaudeSettingsManager {
365
361
  }
366
362
 
367
363
  if (claudeStateChanged && !this.saveClaudeState(claudeState)) {
364
+ this.save(previousSettings);
368
365
  return false;
369
366
  }
370
367
 
@@ -489,9 +486,6 @@ class ClaudeSettingsManager {
489
486
  if (isPlexorUrl && !isPlexorApiKey(authKey)) {
490
487
  return { partial: true, issue: 'Plexor URL set but auth key is not a Plexor key' };
491
488
  }
492
- if (isPlexorUrl && claudeState.primaryApiKey && !isPlexorApiKey(claudeState.primaryApiKey)) {
493
- return { partial: true, issue: 'Plexor URL set but Claude managed API key is still active' };
494
- }
495
489
  return { partial: false, issue: null };
496
490
  } catch {
497
491
  return { partial: false, issue: null };
@@ -511,8 +505,19 @@ class ClaudeSettingsManager {
511
505
  settings.env = {};
512
506
  }
513
507
 
514
- setPlexorAuthKey(settings.env, apiKey);
515
- return this.save(settings);
508
+ const previousSettings = JSON.parse(JSON.stringify(settings));
509
+ const claudeState = this.loadClaudeState();
510
+ upsertManagedPlexorAuthHeader(settings.env, apiKey);
511
+ const claudeStateChanged = migrateLegacyPlexorAuthEnv(settings.env, claudeState);
512
+ const success = this.save(settings);
513
+ if (!success) {
514
+ return false;
515
+ }
516
+ if (claudeStateChanged && !this.saveClaudeState(claudeState)) {
517
+ this.save(previousSettings);
518
+ return false;
519
+ }
520
+ return true;
516
521
  } catch (err) {
517
522
  console.error('Failed to update API key:', err.message);
518
523
  return false;
@@ -0,0 +1,135 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const PREVIOUS_STATUS_LINE_KEY = 'PLEXOR_PREVIOUS_STATUS_LINE';
5
+
6
+ function quoteForPosixShell(value) {
7
+ return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
8
+ }
9
+
10
+ function writeJsonAtomically(filePath, value) {
11
+ const dir = path.dirname(filePath);
12
+ fs.mkdirSync(dir, { recursive: true });
13
+ const tempPath = `${filePath}.tmp.${Date.now()}`;
14
+ fs.writeFileSync(tempPath, JSON.stringify(value, null, 2), { mode: 0o600 });
15
+ fs.renameSync(tempPath, filePath);
16
+ }
17
+
18
+ function normalizeSettings(value) {
19
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
20
+ return {};
21
+ }
22
+ return { ...value };
23
+ }
24
+
25
+ function readSettings(settingsPath) {
26
+ if (!fs.existsSync(settingsPath)) {
27
+ return { settings: {}, existed: false };
28
+ }
29
+
30
+ try {
31
+ const raw = fs.readFileSync(settingsPath, 'utf8');
32
+ if (!raw || !raw.trim()) {
33
+ return { settings: {}, existed: true };
34
+ }
35
+ return { settings: normalizeSettings(JSON.parse(raw)), existed: true };
36
+ } catch {
37
+ return { settings: {}, existed: true };
38
+ }
39
+ }
40
+
41
+ function getManagedStatusLinePath(homeDir) {
42
+ return path.join(homeDir, '.claude', 'plugins', 'plexor', 'hooks', 'statusline.js');
43
+ }
44
+
45
+ function getManagedStatusLineCommand(homeDir) {
46
+ const scriptPath = getManagedStatusLinePath(homeDir);
47
+ return `/bin/sh -lc 'script=$1; [ -x \"$script\" ] || exit 0; exec \"$script\"' -- ${quoteForPosixShell(scriptPath)}`;
48
+ }
49
+
50
+ function isManagedStatusLine(statusLine, homeDir) {
51
+ const managedCommand = getManagedStatusLineCommand(homeDir);
52
+ const normalizedManaged = managedCommand.replace(/^"|"$/g, '');
53
+ const command = String(statusLine?.command || '');
54
+
55
+ return (
56
+ statusLine?.type === 'command' &&
57
+ (
58
+ command === managedCommand ||
59
+ command === normalizedManaged ||
60
+ command.includes('/.claude/plugins/plexor/hooks/statusline.js')
61
+ )
62
+ );
63
+ }
64
+
65
+ function getManagedStatusLine(homeDir) {
66
+ return {
67
+ type: 'command',
68
+ command: getManagedStatusLineCommand(homeDir),
69
+ padding: 0
70
+ };
71
+ }
72
+
73
+ function upsertManagedStatusLine(settingsPath, homeDir) {
74
+ const { settings, existed } = readSettings(settingsPath);
75
+ const currentSettings = normalizeSettings(settings);
76
+ const nextSettings = { ...currentSettings };
77
+ const currentStatusLine = currentSettings.statusLine;
78
+
79
+ if (
80
+ currentStatusLine &&
81
+ !isManagedStatusLine(currentStatusLine, homeDir) &&
82
+ !Object.prototype.hasOwnProperty.call(currentSettings, PREVIOUS_STATUS_LINE_KEY)
83
+ ) {
84
+ nextSettings[PREVIOUS_STATUS_LINE_KEY] = currentStatusLine;
85
+ }
86
+
87
+ nextSettings.statusLine = getManagedStatusLine(homeDir);
88
+
89
+ const changed = JSON.stringify(currentSettings) !== JSON.stringify(nextSettings);
90
+ if (changed) {
91
+ writeJsonAtomically(settingsPath, nextSettings);
92
+ }
93
+
94
+ return { changed, existed };
95
+ }
96
+
97
+ function removeManagedStatusLine(settingsPath, homeDir) {
98
+ const { settings, existed } = readSettings(settingsPath);
99
+ if (!existed) {
100
+ return { changed: false, restored: false };
101
+ }
102
+
103
+ const currentSettings = normalizeSettings(settings);
104
+ const nextSettings = { ...currentSettings };
105
+ const managed = isManagedStatusLine(currentSettings.statusLine, homeDir);
106
+ const previousStatusLine = currentSettings[PREVIOUS_STATUS_LINE_KEY];
107
+ let restored = false;
108
+
109
+ if (managed) {
110
+ if (previousStatusLine && typeof previousStatusLine === 'object') {
111
+ nextSettings.statusLine = previousStatusLine;
112
+ restored = true;
113
+ } else {
114
+ delete nextSettings.statusLine;
115
+ }
116
+ }
117
+
118
+ delete nextSettings[PREVIOUS_STATUS_LINE_KEY];
119
+
120
+ const changed = JSON.stringify(currentSettings) !== JSON.stringify(nextSettings);
121
+ if (changed) {
122
+ writeJsonAtomically(settingsPath, nextSettings);
123
+ }
124
+
125
+ return { changed, restored };
126
+ }
127
+
128
+ module.exports = {
129
+ PREVIOUS_STATUS_LINE_KEY,
130
+ getManagedStatusLine,
131
+ getManagedStatusLineCommand,
132
+ isManagedStatusLine,
133
+ upsertManagedStatusLine,
134
+ removeManagedStatusLine
135
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin-staging",
3
- "version": "0.1.0-beta.24",
3
+ "version": "0.1.0-beta.26",
4
4
  "description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
5
5
  "main": "lib/constants.js",
6
6
  "bin": {
@@ -21,7 +21,12 @@ if [ -f "$CONFIG_FILE" ]; then
21
21
  if [ "$ENABLED" = "True" ] && [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
22
22
  # Set ANTHROPIC_BASE_URL to Plexor gateway (hypervisor mode)
23
23
  export ANTHROPIC_BASE_URL="${API_URL}/gateway/anthropic/v1"
24
- export ANTHROPIC_API_KEY="$API_KEY"
24
+ if [ -n "$ANTHROPIC_CUSTOM_HEADERS" ]; then
25
+ export ANTHROPIC_CUSTOM_HEADERS="x-plexor-key: ${API_KEY}
26
+ ${ANTHROPIC_CUSTOM_HEADERS}"
27
+ else
28
+ export ANTHROPIC_CUSTOM_HEADERS="x-plexor-key: ${API_KEY}"
29
+ fi
25
30
 
26
31
  # Show Plexor branding
27
32
  echo -e "${CYAN}"