@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.
- package/commands/plexor-setup.js +51 -10
- package/commands/plexor-status.js +80 -10
- 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/plexor-client.js +1 -1
- 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
package/commands/plexor-setup.js
CHANGED
|
@@ -92,6 +92,32 @@ function getGatewayLabel(apiUrl) {
|
|
|
92
92
|
return apiUrl.includes('staging') ? 'staging' : 'production';
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function isRunningInsideClaudeSession(env = process.env) {
|
|
96
|
+
return Boolean(env.CLAUDECODE);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createSkipVerifyResult() {
|
|
100
|
+
return { ok: true, reason: '', stdout: EXPECTED_VERIFY_RESPONSE, stderr: '', code: 0, pendingRestart: false };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createPendingRestartVerifyResult() {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason: 'Restart Claude to finish Plexor activation in your current session.',
|
|
107
|
+
stdout: '',
|
|
108
|
+
stderr: '',
|
|
109
|
+
code: 0,
|
|
110
|
+
pendingRestart: true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getVerifyLabel(verifyResult) {
|
|
115
|
+
if (verifyResult.pendingRestart) {
|
|
116
|
+
return 'Restart Claude';
|
|
117
|
+
}
|
|
118
|
+
return verifyResult.ok ? 'OK' : 'FAILED';
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
function updateHealth(config, state) {
|
|
96
122
|
config.health = {
|
|
97
123
|
installed: true,
|
|
@@ -102,6 +128,7 @@ function updateHealth(config, state) {
|
|
|
102
128
|
verify_prompt: VERIFY_PROMPT,
|
|
103
129
|
verify_expected: EXPECTED_VERIFY_RESPONSE,
|
|
104
130
|
verify_error: state.verifyResult.ok ? null : state.verifyResult.reason,
|
|
131
|
+
activation_pending_restart: Boolean(state.verifyResult.pendingRestart),
|
|
105
132
|
gateway: state.gateway,
|
|
106
133
|
previous_auth_preserved: state.previousAuthPreserved
|
|
107
134
|
};
|
|
@@ -124,10 +151,14 @@ function printReceipt({ user, gateway, previousAuthPreserved, verifyResult }) {
|
|
|
124
151
|
console.log('├─────────────────────────────────────────────┤');
|
|
125
152
|
console.log(line(`Connected: OK (${user.email || 'Unknown'})`));
|
|
126
153
|
console.log(line(`Routing Active: OK (${gateway})`));
|
|
127
|
-
console.log(line(`Verified: ${verifyResult
|
|
154
|
+
console.log(line(`Verified: ${getVerifyLabel(verifyResult)}`));
|
|
128
155
|
console.log(line(`Previous Claude auth: ${previousAuthPreserved ? 'Saved' : 'None found'}`));
|
|
129
156
|
console.log('├─────────────────────────────────────────────┤');
|
|
130
|
-
if (verifyResult.
|
|
157
|
+
if (verifyResult.pendingRestart) {
|
|
158
|
+
console.log(line('Plexor routing is configured'));
|
|
159
|
+
console.log(line('Restart Claude to activate Plexor'));
|
|
160
|
+
console.log(line('Then run /plexor-status'));
|
|
161
|
+
} else if (verifyResult.ok) {
|
|
131
162
|
console.log(line('Plexor routing is configured'));
|
|
132
163
|
console.log(line('Restart Claude, then send a prompt'));
|
|
133
164
|
console.log(line('Logout/uninstall restores prior auth'));
|
|
@@ -221,9 +252,9 @@ async function main() {
|
|
|
221
252
|
currentSettings.env?.PLEXOR_PREVIOUS_CLAUDE_PRIMARY_API_KEY
|
|
222
253
|
);
|
|
223
254
|
|
|
224
|
-
const verifyResult =
|
|
225
|
-
?
|
|
226
|
-
: runClaudeRouteVerification();
|
|
255
|
+
const verifyResult = isRunningInsideClaudeSession()
|
|
256
|
+
? createPendingRestartVerifyResult()
|
|
257
|
+
: (skipVerify ? createSkipVerifyResult() : runClaudeRouteVerification());
|
|
227
258
|
|
|
228
259
|
updateHealth(updatedConfig, {
|
|
229
260
|
gateway: getGatewayLabel(apiUrl),
|
|
@@ -243,12 +274,22 @@ async function main() {
|
|
|
243
274
|
verifyResult
|
|
244
275
|
});
|
|
245
276
|
|
|
246
|
-
if (!verifyResult.ok) {
|
|
277
|
+
if (!verifyResult.ok && !verifyResult.pendingRestart) {
|
|
247
278
|
process.exit(1);
|
|
248
279
|
}
|
|
249
280
|
}
|
|
250
281
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
282
|
+
if (require.main === module) {
|
|
283
|
+
main().catch((err) => {
|
|
284
|
+
console.error(`Error: ${err.message}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
module.exports = {
|
|
289
|
+
createPendingRestartVerifyResult,
|
|
290
|
+
createSkipVerifyResult,
|
|
291
|
+
getVerifyLabel,
|
|
292
|
+
isRunningInsideClaudeSession,
|
|
293
|
+
updateHealth
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -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 {
|
|
@@ -172,6 +172,47 @@ function loadConfig() {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
function saveConfig(config) {
|
|
176
|
+
try {
|
|
177
|
+
const configDir = path.dirname(CONFIG_PATH);
|
|
178
|
+
if (!fs.existsSync(configDir)) {
|
|
179
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
180
|
+
}
|
|
181
|
+
const tempPath = path.join(configDir, `.config.${Date.now()}.tmp`);
|
|
182
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
183
|
+
fs.renameSync(tempPath, CONFIG_PATH);
|
|
184
|
+
return true;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isActivationPendingRestart(verification = {}) {
|
|
191
|
+
return verification.activation_pending_restart === true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function shouldPromotePendingRestart(verification = {}, routing = {}, userFetchWorked = false) {
|
|
195
|
+
return isActivationPendingRestart(verification) && routing.active && userFetchWorked;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getVerificationSummary(verification = {}, options = {}) {
|
|
199
|
+
const { routingActive = false, userFetchWorked = false } = options;
|
|
200
|
+
|
|
201
|
+
if (shouldPromotePendingRestart(verification, { active: routingActive }, userFetchWorked)) {
|
|
202
|
+
return { verified: 'OK', nextAction: '/plexor-status', shouldPromote: true };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (isActivationPendingRestart(verification)) {
|
|
206
|
+
return { verified: 'Restart Claude', nextAction: 'Restart Claude', shouldPromote: false };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
verified: verification.verified ? 'OK' : (verification.verify_error ? 'Failed' : 'Not yet'),
|
|
211
|
+
nextAction: null,
|
|
212
|
+
shouldPromote: false
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
/**
|
|
176
217
|
* Check for environment mismatch between config and routing
|
|
177
218
|
*/
|
|
@@ -385,12 +426,33 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
|
|
|
385
426
|
const envLabel = routing.isStaging ? '(staging)' : '(production)';
|
|
386
427
|
const stateMismatch = checkStateMismatch(enabled, routing.active);
|
|
387
428
|
const connectedState = userFetchWorked ? 'OK' : 'Unknown';
|
|
388
|
-
const
|
|
429
|
+
const verificationSummary = getVerificationSummary(verification, {
|
|
430
|
+
routingActive: routing.active,
|
|
431
|
+
userFetchWorked
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (verificationSummary.shouldPromote) {
|
|
435
|
+
config.health = {
|
|
436
|
+
...verification,
|
|
437
|
+
verified: true,
|
|
438
|
+
verified_at: new Date().toISOString(),
|
|
439
|
+
verify_error: null,
|
|
440
|
+
activation_pending_restart: false
|
|
441
|
+
};
|
|
442
|
+
saveConfig(config);
|
|
443
|
+
verification.verified = true;
|
|
444
|
+
verification.verified_at = config.health.verified_at;
|
|
445
|
+
verification.verify_error = null;
|
|
446
|
+
verification.activation_pending_restart = false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const verifiedState = verificationSummary.shouldPromote
|
|
389
450
|
? 'OK'
|
|
390
|
-
:
|
|
391
|
-
const nextAction =
|
|
392
|
-
|
|
393
|
-
|
|
451
|
+
: verificationSummary.verified;
|
|
452
|
+
const nextAction = verificationSummary.nextAction ||
|
|
453
|
+
(!routing.active || partialState.partial || stateMismatch || !verification.verified
|
|
454
|
+
? '/plexor-setup'
|
|
455
|
+
: '/plexor-status');
|
|
394
456
|
|
|
395
457
|
// Note: Environment mismatch warning removed - it caused false positives during
|
|
396
458
|
// concurrent operations and transient states. The partial state and config/routing
|
|
@@ -521,7 +583,15 @@ function fetchJson(apiUrl, endpoint, apiKey) {
|
|
|
521
583
|
});
|
|
522
584
|
}
|
|
523
585
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
586
|
+
if (require.main === module) {
|
|
587
|
+
main().catch(err => {
|
|
588
|
+
console.error('Error:', err.message);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
});
|
|
591
|
+
} else {
|
|
592
|
+
module.exports = {
|
|
593
|
+
getVerificationSummary,
|
|
594
|
+
isActivationPendingRestart,
|
|
595
|
+
shouldPromotePendingRestart
|
|
596
|
+
};
|
|
597
|
+
}
|
|
@@ -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();
|