@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.
@@ -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();
@@ -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
+ };
@@ -31,7 +31,7 @@ class PlexorClient {
31
31
  'Content-Type': 'application/json',
32
32
  'X-API-Key': this.apiKey,
33
33
  'X-Plexor-Key': this.apiKey,
34
- 'User-Agent': 'plexor-claude-code-plugin/0.1.0-beta.23'
34
+ 'User-Agent': 'plexor-claude-code-plugin/0.1.0-beta.24'
35
35
  },
36
36
  timeout: this.timeout
37
37
  };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin-staging",
3
- "version": "0.1.0-beta.23",
3
+ "version": "0.1.0-beta.25",
4
4
  "description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
5
5
  "main": "lib/constants.js",
6
6
  "bin": {