@plexor-dev/claude-code-plugin-staging 0.1.0-beta.23 → 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.
@@ -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
- delete claudeState.primaryApiKey;
222
-
223
- const tempPath = `${statePath}.tmp.${Date.now()}`;
224
- fs.writeFileSync(tempPath, JSON.stringify(claudeState, null, 2), { mode: 0o600 });
225
- fs.renameSync(tempPath, statePath);
226
- return true;
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
- if (managedAuthKey && syncManagedPrimaryApiKey(env, managedAuthKey)) {
272
+ const primaryApiKeySync = managedAuthKey ? syncManagedPrimaryApiKey(env, managedAuthKey) : { changed: false };
273
+ if (primaryApiKeySync.changed) {
258
274
  settings.env = env;
259
275
  settingsChanged = true;
260
- console.log('\n Suspended Claude managed API key while Plexor is active');
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. Verify Claude routing with a deterministic check');
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(' │ No shell edits or Claude restart required after setup │');
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:');
@@ -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
- const tempPath = `${statePath}.tmp.${Date.now()}`;
133
- fs.writeFileSync(tempPath, JSON.stringify(claudeState, null, 2), { mode: 0o600 });
134
- fs.renameSync(tempPath, statePath);
135
- return true;
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 primaryApiKeyRestored = restoreClaudePrimaryApiKey(settings.env);
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 || primaryApiKeyRestored) {
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 slash command files
167
- // These are the Plexor-specific command files that get installed to ~/.claude/commands/
168
- const plexorCommands = [
169
- 'plexor-config.md',
170
- 'plexor-enabled.md',
171
- 'plexor-login.md',
172
- 'plexor-logout.md',
173
- 'plexor-mode.md',
174
- 'plexor-provider.md',
175
- 'plexor-settings.md',
176
- 'plexor-setup.md',
177
- 'plexor-status.md',
178
- 'plexor-uninstall.md'
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
- // 3. Remove plugin directory
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
- console.log(' Note: ~/.plexor/ config directory was preserved.');
244
- console.log(' To remove it: rm -rf ~/.plexor');
245
- console.log('');
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('');