maxpool 1.0.2 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Multi-account Claude Code proxy with adaptive, rate-aware load balancing across Claude accounts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -14,7 +14,8 @@
14
14
  "scripts": {
15
15
  "start": "node src/index.js",
16
16
  "test": "node --test",
17
- "lint": "eslint src/ test/"
17
+ "lint": "eslint src/ test/",
18
+ "release": "bash scripts/release.sh"
18
19
  },
19
20
  "keywords": [
20
21
  "claude",
package/src/config.js CHANGED
@@ -36,6 +36,11 @@ export function createDefaultConfig() {
36
36
  apiKey: 'mp-' + randomBytes(24).toString('base64url'),
37
37
  },
38
38
  upstream: 'https://api.anthropic.com',
39
+ // On startup, check npm for a newer maxpool and notify. Set false to disable.
40
+ updateCheck: true,
41
+ // When true, a newer version is installed automatically (npm i -g maxpool@latest)
42
+ // and applied on the NEXT restart — running sessions are never interrupted.
43
+ autoUpdate: false,
39
44
  // Per-account "stop using this account" gate, applied to BOTH the 5h
40
45
  // session window and the 7d weekly window (whichever utilization is
41
46
  // higher). 0.90 = stop routing to an account once it crosses 90% of a
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ import { importCredentials, loginOAuth, fetchProfile, refreshAccessToken, isToke
10
10
  import { TUI } from './tui.js';
11
11
  import { RestartController } from './restart-controller.js';
12
12
  import { resolveAccounts } from './account-config.js';
13
+ import { maybeCheckForUpdate } from './updater.js';
13
14
 
14
15
  const args = process.argv.slice(2);
15
16
  const command = args[0];
@@ -361,6 +362,11 @@ async function serverWorkerCommand() {
361
362
  } else {
362
363
  logPlainServerStart({ host, port, accounts, threshold, config });
363
364
  }
365
+
366
+ // Non-blocking update check. Notifies (or self-updates if config.autoUpdate);
367
+ // never interrupts the running proxy. Failures are swallowed.
368
+ const notify = msg => (tui?._addLog ? tui._addLog(msg) : console.log(`[Maxpool] ${msg}`));
369
+ maybeCheckForUpdate(config, notify).catch(() => {});
364
370
  });
365
371
 
366
372
  process.on('SIGINT', () => shutdownGracefully('SIGINT'));
package/src/server.js CHANGED
@@ -846,7 +846,7 @@ function unavailableMessage(accountManager, requestInfo = {}, retryAfter, willRe
846
846
  return `All ${n} accounts exhausted. Retry in ${retryAfter}s.`;
847
847
  }
848
848
 
849
- export const __serverTest = { unavailableMessage, isRetriableUpstreamStatus };
849
+ export const __serverTest = { unavailableMessage, isRetriableUpstreamStatus, headerValue, getMaxpoolProfile };
850
850
 
851
851
  async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
852
852
  if (!upstreamRes.body) return '';
@@ -1227,7 +1227,8 @@ function containsThinkingBlock(value) {
1227
1227
  }
1228
1228
 
1229
1229
  function getMaxpoolProfile(headers) {
1230
- const profile = String(headers['x-maxpool-profile'] || 'claude').trim().toLowerCase();
1230
+ // headerValue() handles the x-teamclaude-* legacy fallback.
1231
+ const profile = String(headerValue(headers, 'x-maxpool-profile') || 'claude').trim().toLowerCase();
1231
1232
  return profile || 'claude';
1232
1233
  }
1233
1234
 
@@ -1272,7 +1273,15 @@ function prepareRuntimeProviders(accountManager, headers) {
1272
1273
  }
1273
1274
 
1274
1275
  function headerValue(headers, name) {
1275
- const value = headers[name.toLowerCase()];
1276
+ const lname = name.toLowerCase();
1277
+ let value = headers[lname];
1278
+ // Backward compatibility: sessions launched before the teamclaude→maxpool
1279
+ // rename send x-teamclaude-* headers (a process's ANTHROPIC_CUSTOM_HEADERS is
1280
+ // fixed at launch). Fall back to the legacy name so already-running sessions
1281
+ // keep full routing/fallback without needing a restart.
1282
+ if ((value == null || value === '') && lname.startsWith('x-maxpool-')) {
1283
+ value = headers['x-teamclaude-' + lname.slice('x-maxpool-'.length)];
1284
+ }
1276
1285
  if (Array.isArray(value)) return value[0];
1277
1286
  return value ? String(value).trim() : '';
1278
1287
  }
package/src/updater.js ADDED
@@ -0,0 +1,93 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+ const PACKAGE = 'maxpool';
9
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
10
+
11
+ /** Read the running maxpool version from its own package.json. Null on failure. */
12
+ export async function getCurrentVersion() {
13
+ try {
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const pkg = JSON.parse(await readFile(join(here, '..', 'package.json'), 'utf-8'));
16
+ return pkg.version || null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ /** Compare two dotted versions. Returns 1 if a>b, -1 if a<b, 0 if equal. */
23
+ export function compareVersions(a, b) {
24
+ const pa = String(a).split('.').map(n => Number(n) || 0);
25
+ const pb = String(b).split('.').map(n => Number(n) || 0);
26
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
27
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
28
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ /**
34
+ * Check npm for a newer published version. Network-failure-safe: returns null
35
+ * on any error (offline, timeout, bad response) so a check never breaks startup.
36
+ */
37
+ export async function checkForUpdate(currentVersion, { timeoutMs = 4000, registry = DEFAULT_REGISTRY } = {}) {
38
+ try {
39
+ const ctrl = new AbortController();
40
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
41
+ let res;
42
+ try {
43
+ res = await fetch(`${registry}/${PACKAGE}/latest`, { signal: ctrl.signal });
44
+ } finally {
45
+ clearTimeout(timer);
46
+ }
47
+ if (!res.ok) return null;
48
+ const data = await res.json();
49
+ const latest = data.version;
50
+ if (!latest) return null;
51
+ return {
52
+ latest,
53
+ current: currentVersion,
54
+ hasUpdate: currentVersion ? compareVersions(latest, currentVersion) > 0 : false,
55
+ };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /** Run `npm install -g maxpool@latest`. Returns {ok, output|error}. */
62
+ export async function selfUpdate({ timeoutMs = 120_000 } = {}) {
63
+ try {
64
+ const { stdout, stderr } = await execFileAsync('npm', ['install', '-g', `${PACKAGE}@latest`], { timeout: timeoutMs });
65
+ return { ok: true, output: (stdout || stderr || '').trim() };
66
+ } catch (err) {
67
+ return { ok: false, error: err.message };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Startup hook: check for an update and either notify (default) or self-install
73
+ * (config.autoUpdate). Never auto-restarts a running proxy — the new version
74
+ * applies on the next restart, so in-flight sessions are never interrupted.
75
+ * Fire-and-forget; all failures are swallowed.
76
+ */
77
+ export async function maybeCheckForUpdate(config, notify) {
78
+ if (config?.updateCheck === false) return;
79
+ const current = await getCurrentVersion();
80
+ const result = await checkForUpdate(current);
81
+ if (!result || !result.hasUpdate) return;
82
+
83
+ notify(`Update available: ${result.current} → ${result.latest}`);
84
+ if (config?.autoUpdate) {
85
+ notify(`Auto-updating to ${result.latest}…`);
86
+ const r = await selfUpdate();
87
+ notify(r.ok
88
+ ? `Updated to ${result.latest}. Restart maxpool to apply (running sessions are not interrupted).`
89
+ : `Auto-update failed: ${r.error}. Run: npm i -g ${PACKAGE}`);
90
+ } else {
91
+ notify(`Run 'npm i -g ${PACKAGE}' to update, or set "autoUpdate": true in your config.`);
92
+ }
93
+ }