@plexor-dev/claude-code-plugin-staging 0.1.0-beta.24 → 0.1.0-beta.25
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-status.js +1 -1
- package/commands/plexor-uninstall.js +74 -22
- package/hooks/session-sync.js +194 -0
- package/hooks/statusline.js +130 -0
- package/lib/config-utils.js +25 -0
- package/lib/hooks-manager.js +209 -0
- package/lib/settings-manager.js +5 -1
- package/lib/statusline-manager.js +135 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +73 -14
- package/scripts/uninstall.js +80 -29
|
@@ -58,7 +58,7 @@ function getDirectClaudeAuthState() {
|
|
|
58
58
|
try {
|
|
59
59
|
const data = fs.readFileSync(CLAUDE_STATE_PATH, 'utf8');
|
|
60
60
|
const state = JSON.parse(data);
|
|
61
|
-
if (state.primaryApiKey) {
|
|
61
|
+
if (state.primaryApiKey && !state.primaryApiKey.startsWith('plx_')) {
|
|
62
62
|
return { present: true, source: '/login managed key' };
|
|
63
63
|
}
|
|
64
64
|
} catch {
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const os = require('os');
|
|
21
|
+
const { removeManagedStatusLine } = require('../lib/statusline-manager');
|
|
22
|
+
const { removeManagedHooks, cleanupLegacyManagedHooksFile } = require('../lib/hooks-manager');
|
|
23
|
+
const { removeManagedClaudeCustomHeadersFromEnv } = require('../lib/config-utils');
|
|
21
24
|
|
|
22
25
|
// Get home directory, handling sudo case
|
|
23
26
|
function getHomeDir() {
|
|
@@ -36,22 +39,12 @@ const HOME_DIR = getHomeDir();
|
|
|
36
39
|
const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
|
|
37
40
|
const CLAUDE_COMMANDS_DIR = path.join(CLAUDE_DIR, 'commands');
|
|
38
41
|
const CLAUDE_PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins', 'plexor');
|
|
42
|
+
const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
43
|
+
const CLAUDE_HOOKS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
44
|
+
const CLAUDE_LEGACY_HOOKS_PATH = path.join(CLAUDE_DIR, 'hooks.json');
|
|
39
45
|
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
40
46
|
|
|
41
47
|
// 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
48
|
/**
|
|
56
49
|
* Load settings manager if available
|
|
57
50
|
*/
|
|
@@ -115,10 +108,11 @@ function disableRoutingManually() {
|
|
|
115
108
|
const hasManagedBaseUrl = isManagedGatewayUrl(settings.env.ANTHROPIC_BASE_URL || '');
|
|
116
109
|
const hasPlexorAuthToken = isPlexorApiKey(settings.env.ANTHROPIC_AUTH_TOKEN || '');
|
|
117
110
|
const hasPlexorApiKey = isPlexorApiKey(settings.env.ANTHROPIC_API_KEY || '');
|
|
111
|
+
const removedManagedHeaders = removeManagedClaudeCustomHeadersFromEnv(settings.env);
|
|
118
112
|
|
|
119
113
|
const hasPreviousPrimaryApiKey = Boolean(settings.env[previousPrimaryApiKeyEnv]);
|
|
120
114
|
|
|
121
|
-
if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey && !hasPreviousPrimaryApiKey) {
|
|
115
|
+
if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey && !hasPreviousPrimaryApiKey && !removedManagedHeaders) {
|
|
122
116
|
return { success: true, message: 'Plexor routing not active' };
|
|
123
117
|
}
|
|
124
118
|
|
|
@@ -191,7 +185,12 @@ function removeSlashCommands() {
|
|
|
191
185
|
let removed = 0;
|
|
192
186
|
let restored = 0;
|
|
193
187
|
|
|
194
|
-
|
|
188
|
+
if (!fs.existsSync(CLAUDE_COMMANDS_DIR)) {
|
|
189
|
+
return { removed, restored };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const plexorCommands = fs.readdirSync(CLAUDE_COMMANDS_DIR).filter((entry) => /^plexor-.*\.md$/i.test(entry));
|
|
193
|
+
for (const cmd of plexorCommands) {
|
|
195
194
|
const cmdPath = path.join(CLAUDE_COMMANDS_DIR, cmd);
|
|
196
195
|
const backupPath = cmdPath + '.backup';
|
|
197
196
|
|
|
@@ -229,6 +228,30 @@ function removePluginDirectory() {
|
|
|
229
228
|
return false;
|
|
230
229
|
}
|
|
231
230
|
|
|
231
|
+
function removeManagedStatusLineConfig() {
|
|
232
|
+
try {
|
|
233
|
+
return removeManagedStatusLine(CLAUDE_SETTINGS_PATH, HOME_DIR);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return { changed: false, restored: false, error: err.message };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function removeManagedHooksConfig() {
|
|
240
|
+
try {
|
|
241
|
+
return removeManagedHooks(CLAUDE_HOOKS_PATH);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return { changed: false, removed: 0, error: err.message };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function removeLegacyManagedHooksConfig() {
|
|
248
|
+
try {
|
|
249
|
+
return cleanupLegacyManagedHooksFile(CLAUDE_LEGACY_HOOKS_PATH);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return { changed: false, removed: 0, error: err.message };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
232
255
|
/**
|
|
233
256
|
* Remove config directory
|
|
234
257
|
*/
|
|
@@ -255,7 +278,8 @@ function main() {
|
|
|
255
278
|
console.log(' Cleans up Plexor integration before npm uninstall.');
|
|
256
279
|
console.log('');
|
|
257
280
|
console.log(' Options:');
|
|
258
|
-
console.log(' --
|
|
281
|
+
console.log(' --keep-config, -k Preserve ~/.plexor/ config directory');
|
|
282
|
+
console.log(' --remove-config, -c Legacy alias (config is removed by default)');
|
|
259
283
|
console.log(' --quiet, -q Suppress output messages');
|
|
260
284
|
console.log(' --help, -h Show this help message');
|
|
261
285
|
console.log('');
|
|
@@ -265,7 +289,7 @@ function main() {
|
|
|
265
289
|
process.exit(0);
|
|
266
290
|
}
|
|
267
291
|
|
|
268
|
-
const removeConfig = args.includes('--
|
|
292
|
+
const removeConfig = !(args.includes('--keep-config') || args.includes('-k'));
|
|
269
293
|
const quiet = args.includes('--quiet') || args.includes('-q');
|
|
270
294
|
|
|
271
295
|
if (!quiet) {
|
|
@@ -295,7 +319,36 @@ function main() {
|
|
|
295
319
|
: ` ✗ Failed to remove routing: ${routingResult.message}`);
|
|
296
320
|
}
|
|
297
321
|
|
|
298
|
-
// 2. Remove
|
|
322
|
+
// 2. Remove managed Plexor status line
|
|
323
|
+
const statusLineResult = removeManagedStatusLineConfig();
|
|
324
|
+
if (!quiet) {
|
|
325
|
+
if (statusLineResult.error) {
|
|
326
|
+
console.log(` ✗ Failed to clean Plexor status line: ${statusLineResult.error}`);
|
|
327
|
+
} else if (statusLineResult.changed) {
|
|
328
|
+
console.log(' ✓ Restored Claude status line configuration');
|
|
329
|
+
} else {
|
|
330
|
+
console.log(' ○ Claude status line already clean');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const hooksResult = removeManagedHooksConfig();
|
|
335
|
+
const legacyHooksResult = removeLegacyManagedHooksConfig();
|
|
336
|
+
if (!quiet) {
|
|
337
|
+
if (hooksResult.error) {
|
|
338
|
+
console.log(` ✗ Failed to clean Plexor hooks: ${hooksResult.error}`);
|
|
339
|
+
} else if (hooksResult.changed) {
|
|
340
|
+
console.log(' ✓ Restored Claude hook configuration');
|
|
341
|
+
} else {
|
|
342
|
+
console.log(' ○ Claude hooks already clean');
|
|
343
|
+
}
|
|
344
|
+
if (legacyHooksResult.error) {
|
|
345
|
+
console.log(` ✗ Failed to clean legacy ~/.claude/hooks.json: ${legacyHooksResult.error}`);
|
|
346
|
+
} else if (legacyHooksResult.changed) {
|
|
347
|
+
console.log(' ✓ Cleaned legacy ~/.claude/hooks.json');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 3. Remove slash command .md files
|
|
299
352
|
const cmdResult = removeSlashCommands();
|
|
300
353
|
if (!quiet) {
|
|
301
354
|
console.log(` ✓ Removed ${cmdResult.removed} slash command files`);
|
|
@@ -304,7 +357,7 @@ function main() {
|
|
|
304
357
|
}
|
|
305
358
|
}
|
|
306
359
|
|
|
307
|
-
//
|
|
360
|
+
// 4. Remove plugin directory
|
|
308
361
|
const pluginRemoved = removePluginDirectory();
|
|
309
362
|
if (!quiet) {
|
|
310
363
|
console.log(pluginRemoved
|
|
@@ -312,7 +365,7 @@ function main() {
|
|
|
312
365
|
: ' ○ Plugin directory not found (already clean)');
|
|
313
366
|
}
|
|
314
367
|
|
|
315
|
-
//
|
|
368
|
+
// 5. Remove config directory unless explicitly preserved
|
|
316
369
|
if (removeConfig) {
|
|
317
370
|
const configRemoved = removeConfigDirectory();
|
|
318
371
|
if (!quiet) {
|
|
@@ -337,8 +390,7 @@ function main() {
|
|
|
337
390
|
|
|
338
391
|
if (!removeConfig) {
|
|
339
392
|
console.log(' Note: ~/.plexor/ config directory was preserved.');
|
|
340
|
-
console.log(' To
|
|
341
|
-
console.log(' Or manually: rm -rf ~/.plexor');
|
|
393
|
+
console.log(' To remove it manually: rm -rf ~/.plexor');
|
|
342
394
|
console.log('');
|
|
343
395
|
}
|
|
344
396
|
}
|
|
@@ -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();
|
package/lib/config-utils.js
CHANGED
|
@@ -113,6 +113,30 @@ function serializeCustomHeaders(headers) {
|
|
|
113
113
|
.join('\n');
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function removeManagedClaudeCustomHeadersFromEnv(env = {}) {
|
|
117
|
+
const existing = parseCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
|
|
118
|
+
let removed = false;
|
|
119
|
+
|
|
120
|
+
for (const key of MANAGED_HEADER_KEYS) {
|
|
121
|
+
if (Object.prototype.hasOwnProperty.call(existing, key)) {
|
|
122
|
+
delete existing[key];
|
|
123
|
+
removed = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!removed) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (Object.keys(existing).length) {
|
|
132
|
+
env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(existing);
|
|
133
|
+
} else {
|
|
134
|
+
delete env.ANTHROPIC_CUSTOM_HEADERS;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
116
140
|
function buildManagedAnthropicHeaders(config) {
|
|
117
141
|
const settings = config?.settings || {};
|
|
118
142
|
const headers = {};
|
|
@@ -308,6 +332,7 @@ module.exports = {
|
|
|
308
332
|
readSetting,
|
|
309
333
|
hasForcedHintConflict,
|
|
310
334
|
validateForcedHintConfig,
|
|
335
|
+
removeManagedClaudeCustomHeadersFromEnv,
|
|
311
336
|
parseCustomHeaders,
|
|
312
337
|
serializeCustomHeaders,
|
|
313
338
|
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,6 +14,7 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
|
+
const { removeManagedClaudeCustomHeadersFromEnv } = require('./config-utils');
|
|
17
18
|
|
|
18
19
|
const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
|
|
19
20
|
const CLAUDE_STATE_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude.json');
|
|
@@ -92,8 +93,9 @@ function clearPlexorRoutingEnv(env = {}) {
|
|
|
92
93
|
const hasManagedBaseUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '');
|
|
93
94
|
const hasPlexorApiKey = isPlexorApiKey(env.ANTHROPIC_API_KEY || '');
|
|
94
95
|
const hasPlexorAuthToken = isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '');
|
|
96
|
+
const removedManagedHeaders = removeManagedClaudeCustomHeadersFromEnv(env);
|
|
95
97
|
|
|
96
|
-
if (!hasManagedBaseUrl && !hasPlexorApiKey && !hasPlexorAuthToken) {
|
|
98
|
+
if (!hasManagedBaseUrl && !hasPlexorApiKey && !hasPlexorAuthToken && !removedManagedHeaders) {
|
|
97
99
|
return false;
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -346,6 +348,7 @@ class ClaudeSettingsManager {
|
|
|
346
348
|
|
|
347
349
|
try {
|
|
348
350
|
const settings = this.load();
|
|
351
|
+
const previousSettings = JSON.parse(JSON.stringify(settings));
|
|
349
352
|
const claudeState = this.loadClaudeState();
|
|
350
353
|
|
|
351
354
|
// Initialize env block if doesn't exist
|
|
@@ -365,6 +368,7 @@ class ClaudeSettingsManager {
|
|
|
365
368
|
}
|
|
366
369
|
|
|
367
370
|
if (claudeStateChanged && !this.saveClaudeState(claudeState)) {
|
|
371
|
+
this.save(previousSettings);
|
|
368
372
|
return false;
|
|
369
373
|
}
|
|
370
374
|
|
|
@@ -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/postinstall.js
CHANGED
|
@@ -11,6 +11,8 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const os = require('os');
|
|
13
13
|
const { execSync } = require('child_process');
|
|
14
|
+
const { upsertManagedStatusLine } = require('../lib/statusline-manager');
|
|
15
|
+
const { upsertManagedHooks, cleanupLegacyManagedHooksFile } = require('../lib/hooks-manager');
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Resolve the home directory for a given username by querying /etc/passwd.
|
|
@@ -137,12 +139,16 @@ function chownRecursive(dirPath, uid, gid) {
|
|
|
137
139
|
|
|
138
140
|
const HOME_DIR = getHomeDir();
|
|
139
141
|
const COMMANDS_SOURCE = path.join(__dirname, '..', 'commands');
|
|
142
|
+
const HOOKS_SOURCE = path.join(__dirname, '..', 'hooks');
|
|
140
143
|
const LIB_SOURCE = path.join(__dirname, '..', 'lib');
|
|
141
144
|
const CLAUDE_COMMANDS_DIR = path.join(HOME_DIR, '.claude', 'commands');
|
|
142
145
|
const PLEXOR_PLUGINS_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'commands');
|
|
146
|
+
const PLEXOR_HOOKS_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'hooks');
|
|
143
147
|
const PLEXOR_LIB_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'lib');
|
|
144
148
|
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
145
149
|
const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
|
|
150
|
+
const CLAUDE_SETTINGS_FILE = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
151
|
+
const CLAUDE_LEGACY_HOOKS_FILE = path.join(HOME_DIR, '.claude', 'hooks.json');
|
|
146
152
|
|
|
147
153
|
/**
|
|
148
154
|
* Check if a base URL is a Plexor-managed gateway URL.
|
|
@@ -199,33 +205,41 @@ function syncManagedAuthEnv(env, managedAuthKey) {
|
|
|
199
205
|
env.ANTHROPIC_AUTH_TOKEN = managedAuthKey;
|
|
200
206
|
}
|
|
201
207
|
|
|
208
|
+
function writeJsonAtomically(filePath, value) {
|
|
209
|
+
const tempPath = `${filePath}.tmp.${Date.now()}`;
|
|
210
|
+
fs.writeFileSync(tempPath, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
211
|
+
fs.renameSync(tempPath, filePath);
|
|
212
|
+
}
|
|
213
|
+
|
|
202
214
|
function syncManagedPrimaryApiKey(env, managedAuthKey) {
|
|
203
215
|
const statePath = path.join(HOME_DIR, '.claude.json');
|
|
204
216
|
try {
|
|
205
217
|
if (!fs.existsSync(statePath)) {
|
|
206
|
-
return false;
|
|
218
|
+
return { changed: false };
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
const data = fs.readFileSync(statePath, 'utf8');
|
|
210
222
|
if (!data || !data.trim()) {
|
|
211
|
-
return false;
|
|
223
|
+
return { changed: false };
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
const claudeState = JSON.parse(data);
|
|
215
227
|
const primaryApiKey = claudeState.primaryApiKey || '';
|
|
216
228
|
if (!primaryApiKey || primaryApiKey.startsWith('plx_') || primaryApiKey === managedAuthKey) {
|
|
217
|
-
return false;
|
|
229
|
+
return { changed: false };
|
|
218
230
|
}
|
|
219
231
|
|
|
220
232
|
env[PREVIOUS_PRIMARY_API_KEY_ENV] = primaryApiKey;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
const nextClaudeState = { ...claudeState };
|
|
234
|
+
delete nextClaudeState.primaryApiKey;
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
changed: true,
|
|
238
|
+
statePath,
|
|
239
|
+
claudeState: nextClaudeState
|
|
240
|
+
};
|
|
227
241
|
} catch {
|
|
228
|
-
return false;
|
|
242
|
+
return { changed: false };
|
|
229
243
|
}
|
|
230
244
|
}
|
|
231
245
|
|
|
@@ -240,6 +254,7 @@ function checkOrphanedRouting() {
|
|
|
240
254
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
241
255
|
const env = settings.env || {};
|
|
242
256
|
let settingsChanged = false;
|
|
257
|
+
let managedPrimaryApiKeySync = null;
|
|
243
258
|
|
|
244
259
|
const hasPlexorUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL);
|
|
245
260
|
|
|
@@ -254,10 +269,11 @@ function checkOrphanedRouting() {
|
|
|
254
269
|
settingsChanged = true;
|
|
255
270
|
console.log('\n Synced Plexor auth into ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN');
|
|
256
271
|
}
|
|
257
|
-
|
|
272
|
+
const primaryApiKeySync = managedAuthKey ? syncManagedPrimaryApiKey(env, managedAuthKey) : { changed: false };
|
|
273
|
+
if (primaryApiKeySync.changed) {
|
|
258
274
|
settings.env = env;
|
|
259
275
|
settingsChanged = true;
|
|
260
|
-
|
|
276
|
+
managedPrimaryApiKeySync = primaryApiKeySync;
|
|
261
277
|
}
|
|
262
278
|
// Check if there's a valid Plexor config
|
|
263
279
|
let hasValidConfig = false;
|
|
@@ -299,6 +315,16 @@ function checkOrphanedRouting() {
|
|
|
299
315
|
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
300
316
|
fs.renameSync(tempPath, settingsPath);
|
|
301
317
|
}
|
|
318
|
+
|
|
319
|
+
if (managedPrimaryApiKeySync?.changed) {
|
|
320
|
+
try {
|
|
321
|
+
writeJsonAtomically(managedPrimaryApiKeySync.statePath, managedPrimaryApiKeySync.claudeState);
|
|
322
|
+
console.log('\n Suspended Claude managed API key while Plexor is active');
|
|
323
|
+
} catch (e) {
|
|
324
|
+
console.log('\n Warning: Saved Claude key backup but could not suspend Claude managed API key');
|
|
325
|
+
console.log(` ${e.message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
302
328
|
} catch (e) {
|
|
303
329
|
// Ignore errors in detection - don't break install
|
|
304
330
|
}
|
|
@@ -334,6 +360,9 @@ function main() {
|
|
|
334
360
|
// Create ~/.claude/plugins/plexor/commands/ for JS executors
|
|
335
361
|
fs.mkdirSync(PLEXOR_PLUGINS_DIR, { recursive: true });
|
|
336
362
|
|
|
363
|
+
// Create ~/.claude/plugins/plexor/hooks/ for hook scripts
|
|
364
|
+
fs.mkdirSync(PLEXOR_HOOKS_DIR, { recursive: true });
|
|
365
|
+
|
|
337
366
|
// Create ~/.claude/plugins/plexor/lib/ for shared modules
|
|
338
367
|
fs.mkdirSync(PLEXOR_LIB_DIR, { recursive: true });
|
|
339
368
|
|
|
@@ -356,6 +385,9 @@ function main() {
|
|
|
356
385
|
.filter(f => f.endsWith('.md'));
|
|
357
386
|
const jsFiles = fs.readdirSync(COMMANDS_SOURCE)
|
|
358
387
|
.filter(f => f.endsWith('.js'));
|
|
388
|
+
const hookFiles = fs.existsSync(HOOKS_SOURCE)
|
|
389
|
+
? fs.readdirSync(HOOKS_SOURCE).filter(f => f.endsWith('.js'))
|
|
390
|
+
: [];
|
|
359
391
|
|
|
360
392
|
if (mdFiles.length === 0) {
|
|
361
393
|
console.error('No command files found in package. Installation may be corrupt.');
|
|
@@ -397,6 +429,16 @@ function main() {
|
|
|
397
429
|
jsInstalled.push(file);
|
|
398
430
|
}
|
|
399
431
|
|
|
432
|
+
// Copy hook files to ~/.claude/plugins/plexor/hooks/
|
|
433
|
+
const hooksInstalled = [];
|
|
434
|
+
for (const file of hookFiles) {
|
|
435
|
+
const src = path.join(HOOKS_SOURCE, file);
|
|
436
|
+
const dest = path.join(PLEXOR_HOOKS_DIR, file);
|
|
437
|
+
fs.copyFileSync(src, dest);
|
|
438
|
+
fs.chmodSync(dest, 0o755);
|
|
439
|
+
hooksInstalled.push(file);
|
|
440
|
+
}
|
|
441
|
+
|
|
400
442
|
// Copy lib files to ~/.claude/plugins/plexor/lib/
|
|
401
443
|
// CRITICAL: These are required for commands to work
|
|
402
444
|
const libInstalled = [];
|
|
@@ -431,6 +473,10 @@ function main() {
|
|
|
431
473
|
console.error('');
|
|
432
474
|
}
|
|
433
475
|
|
|
476
|
+
const statusLineRegistration = upsertManagedStatusLine(CLAUDE_SETTINGS_FILE, HOME_DIR);
|
|
477
|
+
const hooksRegistration = upsertManagedHooks(CLAUDE_SETTINGS_FILE, HOME_DIR);
|
|
478
|
+
const legacyHooksCleanup = cleanupLegacyManagedHooksFile(CLAUDE_LEGACY_HOOKS_FILE);
|
|
479
|
+
|
|
434
480
|
// Fix file ownership when running with sudo
|
|
435
481
|
// Files are created as root but should be owned by the original user
|
|
436
482
|
if (targetUser) {
|
|
@@ -457,9 +503,21 @@ function main() {
|
|
|
457
503
|
if (jsInstalled.length > 0) {
|
|
458
504
|
console.log(` ✓ Installed ${jsInstalled.length} executors to ~/.claude/plugins/plexor/commands/`);
|
|
459
505
|
}
|
|
506
|
+
if (hooksInstalled.length > 0) {
|
|
507
|
+
console.log(` ✓ Installed ${hooksInstalled.length} hook scripts to ~/.claude/plugins/plexor/hooks/`);
|
|
508
|
+
}
|
|
460
509
|
if (libInstalled.length > 0) {
|
|
461
510
|
console.log(` ✓ Installed ${libInstalled.length} lib modules to ~/.claude/plugins/plexor/lib/`);
|
|
462
511
|
}
|
|
512
|
+
if (statusLineRegistration.changed || statusLineRegistration.existed) {
|
|
513
|
+
console.log(' ✓ Registered Plexor status line in ~/.claude/settings.json');
|
|
514
|
+
}
|
|
515
|
+
if (hooksRegistration.changed || hooksRegistration.existed) {
|
|
516
|
+
console.log(' ✓ Registered Plexor hooks in ~/.claude/settings.json');
|
|
517
|
+
}
|
|
518
|
+
if (legacyHooksCleanup.changed) {
|
|
519
|
+
console.log(' ✓ Cleaned up legacy ~/.claude/hooks.json entries');
|
|
520
|
+
}
|
|
463
521
|
if (targetUser) {
|
|
464
522
|
console.log(` ✓ Set file ownership to ${targetUser.user}`);
|
|
465
523
|
}
|
|
@@ -474,10 +532,11 @@ function main() {
|
|
|
474
532
|
console.log(' 2. Write ~/.plexor/config.json');
|
|
475
533
|
console.log(' 3. Point Claude at the Plexor staging gateway');
|
|
476
534
|
console.log(' 4. Preserve prior Claude auth for restore on logout');
|
|
477
|
-
console.log(' 5.
|
|
535
|
+
console.log(' 5. Show Plexor state in Claude footer via status line');
|
|
536
|
+
console.log(' 6. Update session savings in real time via Claude hooks');
|
|
478
537
|
console.log('');
|
|
479
538
|
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
|
480
|
-
console.log(' │
|
|
539
|
+
console.log(' │ After /plexor-setup, restart Claude before first prompt │');
|
|
481
540
|
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
482
541
|
console.log('');
|
|
483
542
|
console.log(' Available commands:');
|
package/scripts/uninstall.js
CHANGED
|
@@ -17,6 +17,9 @@ const fs = require('fs');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const os = require('os');
|
|
19
19
|
const { execSync } = require('child_process');
|
|
20
|
+
const { removeManagedStatusLine } = require('../lib/statusline-manager');
|
|
21
|
+
const { removeManagedHooks, cleanupLegacyManagedHooksFile } = require('../lib/hooks-manager');
|
|
22
|
+
const { removeManagedClaudeCustomHeadersFromEnv } = require('../lib/config-utils');
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Get the correct home directory for the process's effective user.
|
|
@@ -52,11 +55,16 @@ console.log('');
|
|
|
52
55
|
|
|
53
56
|
const results = {
|
|
54
57
|
routing: false,
|
|
58
|
+
statusLine: false,
|
|
59
|
+
hooks: false,
|
|
55
60
|
commands: [],
|
|
56
61
|
restored: [],
|
|
57
62
|
pluginDir: false
|
|
58
63
|
};
|
|
59
64
|
const PREVIOUS_PRIMARY_API_KEY_ENV = 'PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY';
|
|
65
|
+
const CLAUDE_SETTINGS_PATH = path.join(home, '.claude', 'settings.json');
|
|
66
|
+
const CLAUDE_HOOKS_PATH = path.join(home, '.claude', 'settings.json');
|
|
67
|
+
const CLAUDE_LEGACY_HOOKS_PATH = path.join(home, '.claude', 'hooks.json');
|
|
60
68
|
|
|
61
69
|
function isManagedGatewayUrl(baseUrl = '') {
|
|
62
70
|
return (
|
|
@@ -77,8 +85,9 @@ function clearPlexorRoutingEnv(env = {}) {
|
|
|
77
85
|
const hasManagedBaseUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL || '');
|
|
78
86
|
const hasPlexorAuthToken = isPlexorApiKey(env.ANTHROPIC_AUTH_TOKEN || '');
|
|
79
87
|
const hasPlexorApiKey = isPlexorApiKey(env.ANTHROPIC_API_KEY || '');
|
|
88
|
+
const removedManagedHeaders = removeManagedClaudeCustomHeadersFromEnv(env);
|
|
80
89
|
|
|
81
|
-
if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey) {
|
|
90
|
+
if (!hasManagedBaseUrl && !hasPlexorAuthToken && !hasPlexorApiKey && !removedManagedHeaders) {
|
|
82
91
|
return false;
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -105,10 +114,16 @@ function clearPlexorRoutingEnv(env = {}) {
|
|
|
105
114
|
return true;
|
|
106
115
|
}
|
|
107
116
|
|
|
117
|
+
function writeJsonAtomically(filePath, value) {
|
|
118
|
+
const tempPath = `${filePath}.tmp.${Date.now()}`;
|
|
119
|
+
fs.writeFileSync(tempPath, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
120
|
+
fs.renameSync(tempPath, filePath);
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
function restoreClaudePrimaryApiKey(env = {}) {
|
|
109
124
|
const previousPrimaryApiKey = env[PREVIOUS_PRIMARY_API_KEY_ENV] || '';
|
|
110
125
|
if (!previousPrimaryApiKey) {
|
|
111
|
-
return false;
|
|
126
|
+
return { restored: false, warning: null };
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
const statePath = path.join(home, '.claude.json');
|
|
@@ -127,12 +142,14 @@ function restoreClaudePrimaryApiKey(env = {}) {
|
|
|
127
142
|
if (!claudeState.primaryApiKey) {
|
|
128
143
|
claudeState.primaryApiKey = previousPrimaryApiKey;
|
|
129
144
|
}
|
|
130
|
-
delete env[PREVIOUS_PRIMARY_API_KEY_ENV];
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
try {
|
|
147
|
+
writeJsonAtomically(statePath, claudeState);
|
|
148
|
+
delete env[PREVIOUS_PRIMARY_API_KEY_ENV];
|
|
149
|
+
return { restored: true, warning: null };
|
|
150
|
+
} catch (e) {
|
|
151
|
+
return { restored: false, warning: e.message };
|
|
152
|
+
}
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
// 1. Remove routing from settings.json
|
|
@@ -145,17 +162,20 @@ try {
|
|
|
145
162
|
const settings = JSON.parse(data);
|
|
146
163
|
if (settings.env) {
|
|
147
164
|
const routingChanged = clearPlexorRoutingEnv(settings.env);
|
|
148
|
-
const
|
|
165
|
+
const primaryApiKeyRestore = restoreClaudePrimaryApiKey(settings.env);
|
|
149
166
|
|
|
150
167
|
// Clean up empty env block
|
|
151
168
|
if (Object.keys(settings.env).length === 0) {
|
|
152
169
|
delete settings.env;
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
if (routingChanged ||
|
|
172
|
+
if (routingChanged || primaryApiKeyRestore.restored) {
|
|
156
173
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
157
174
|
results.routing = true;
|
|
158
175
|
}
|
|
176
|
+
if (primaryApiKeyRestore.warning) {
|
|
177
|
+
console.log(` Warning: Could not restore Claude managed API key: ${primaryApiKeyRestore.warning}`);
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
181
|
}
|
|
@@ -163,24 +183,34 @@ try {
|
|
|
163
183
|
console.log(` Warning: Could not clean settings.json: ${e.message}`);
|
|
164
184
|
}
|
|
165
185
|
|
|
166
|
-
// 2. Remove
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
// 2. Remove managed Claude status line
|
|
187
|
+
try {
|
|
188
|
+
const statusLineRemoval = removeManagedStatusLine(CLAUDE_SETTINGS_PATH, home);
|
|
189
|
+
results.statusLine = statusLineRemoval.changed;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.log(` Warning: Could not clean Plexor status line: ${e.message}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 2b. Remove managed Claude hooks
|
|
195
|
+
try {
|
|
196
|
+
const hooksRemoval = removeManagedHooks(CLAUDE_HOOKS_PATH);
|
|
197
|
+
results.hooks = hooksRemoval.changed;
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.log(` Warning: Could not clean Plexor hooks: ${e.message}`);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const legacyHooksRemoval = cleanupLegacyManagedHooksFile(CLAUDE_LEGACY_HOOKS_PATH);
|
|
203
|
+
results.hooks = results.hooks || legacyHooksRemoval.changed;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.log(` Warning: Could not clean legacy Plexor hooks file: ${e.message}`);
|
|
206
|
+
}
|
|
180
207
|
|
|
208
|
+
// 3. Remove slash command files
|
|
209
|
+
// These are the Plexor-specific command files that get installed to ~/.claude/commands/
|
|
181
210
|
try {
|
|
182
211
|
const commandsDir = path.join(home, '.claude', 'commands');
|
|
183
212
|
if (fs.existsSync(commandsDir)) {
|
|
213
|
+
const plexorCommands = fs.readdirSync(commandsDir).filter((entry) => /^plexor-.*\.md$/i.test(entry));
|
|
184
214
|
for (const cmd of plexorCommands) {
|
|
185
215
|
const cmdPath = path.join(commandsDir, cmd);
|
|
186
216
|
const backupPath = cmdPath + '.backup';
|
|
@@ -201,7 +231,7 @@ try {
|
|
|
201
231
|
console.log(` Warning: Could not clean commands: ${e.message}`);
|
|
202
232
|
}
|
|
203
233
|
|
|
204
|
-
//
|
|
234
|
+
// 4. Remove plugin directory
|
|
205
235
|
try {
|
|
206
236
|
const pluginDir = path.join(home, '.claude', 'plugins', 'plexor');
|
|
207
237
|
if (fs.existsSync(pluginDir)) {
|
|
@@ -212,8 +242,19 @@ try {
|
|
|
212
242
|
console.log(` Warning: Could not remove plugin directory: ${e.message}`);
|
|
213
243
|
}
|
|
214
244
|
|
|
245
|
+
// 5. Remove config directory
|
|
246
|
+
try {
|
|
247
|
+
const configDir = path.join(home, '.plexor');
|
|
248
|
+
if (fs.existsSync(configDir)) {
|
|
249
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
250
|
+
results.configDir = true;
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
console.log(` Warning: Could not remove ~/.plexor config directory: ${e.message}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
215
256
|
// Output results
|
|
216
|
-
if (results.routing || results.commands.length > 0 || results.pluginDir) {
|
|
257
|
+
if (results.routing || results.statusLine || results.hooks || results.commands.length > 0 || results.pluginDir) {
|
|
217
258
|
console.log(' Plexor plugin uninstalled');
|
|
218
259
|
console.log('');
|
|
219
260
|
|
|
@@ -223,6 +264,16 @@ if (results.routing || results.commands.length > 0 || results.pluginDir) {
|
|
|
223
264
|
console.log('');
|
|
224
265
|
}
|
|
225
266
|
|
|
267
|
+
if (results.statusLine) {
|
|
268
|
+
console.log(' Removed Plexor Claude status line');
|
|
269
|
+
console.log('');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (results.hooks) {
|
|
273
|
+
console.log(' Removed Plexor Claude hooks');
|
|
274
|
+
console.log('');
|
|
275
|
+
}
|
|
276
|
+
|
|
226
277
|
if (results.commands.length > 0) {
|
|
227
278
|
console.log(' Removed commands:');
|
|
228
279
|
results.commands.forEach(cmd => console.log(` /${cmd}`));
|
|
@@ -239,10 +290,10 @@ if (results.routing || results.commands.length > 0 || results.pluginDir) {
|
|
|
239
290
|
console.log(' Removed plugin directory');
|
|
240
291
|
console.log('');
|
|
241
292
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
293
|
+
if (results.configDir) {
|
|
294
|
+
console.log(' Removed ~/.plexor config directory');
|
|
295
|
+
console.log('');
|
|
296
|
+
}
|
|
246
297
|
} else {
|
|
247
298
|
console.log(' No Plexor components found to clean up.');
|
|
248
299
|
console.log('');
|