@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.
- package/commands/plexor-logout.js +3 -3
- package/commands/plexor-setup.js +14 -2
- package/commands/plexor-setup.md +4 -4
- package/commands/plexor-status.js +6 -2
- package/commands/plexor-uninstall.js +86 -28
- package/hooks/session-sync.js +194 -0
- package/hooks/statusline.js +130 -0
- package/lib/config-utils.js +83 -13
- package/lib/hooks-manager.js +209 -0
- package/lib/settings-manager.js +52 -47
- package/lib/statusline-manager.js +135 -0
- package/package.json +1 -1
- package/scripts/plexor-cli.sh +6 -1
- package/scripts/postinstall.js +132 -55
- package/scripts/uninstall.js +89 -30
package/lib/config-utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
};
|
package/lib/settings-manager.js
CHANGED
|
@@ -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
|
|
62
|
+
return '';
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 (
|
|
79
|
-
env[
|
|
80
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
360
|
-
const claudeStateChanged =
|
|
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
|
-
|
|
515
|
-
|
|
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
package/scripts/plexor-cli.sh
CHANGED
|
@@ -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
|
-
|
|
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}"
|