scalemax 0.1.16 → 0.1.18

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/README.md CHANGED
@@ -17,6 +17,8 @@ It detects supported AI coding clients, safely backs up existing config files, a
17
17
  - A ScaleMax API key
18
18
  - Network access to `https://api.scalemax.pro`
19
19
 
20
+ The CLI identifies whether the supplied key belongs to the API-credit surface or the token-pack surface. It then stores and configures the matching base URL automatically; users do not need to choose it manually.
21
+
20
22
  ## Installation
21
23
 
22
24
  ### One-time run with npx
@@ -114,7 +116,7 @@ For clients without a stable official config file, the installer writes a ScaleM
114
116
 
115
117
  ### Claude Code
116
118
 
117
- ScaleMax updates `~/.claude/settings.json` with `ANTHROPIC_BASE_URL=https://api.scalemax.pro`, gateway model discovery, and a cached `apiKeyHelper` that reads a private `~/.claude/scalemax.key` file. The raw key is not stored in the settings JSON. The Anthropic base URL intentionally has no `/v1` suffix because Claude Code adds `/v1/messages` itself. The CLI preserves unrelated settings, selects a live main model from the authenticated catalog (preferring `claude-opus-4-8`), pins authenticated Haiku for lightweight background work when available, and verifies a one-token Messages request. If verification fails, it removes only the ScaleMax-managed changes. Claude Code picker aliases such as `claude-opus-4-8[1m]` are normalized by the gateway.
119
+ ScaleMax updates `~/.claude/settings.json` with `ANTHROPIC_BASE_URL=https://api.scalemax.pro`, gateway model discovery, and a cached `apiKeyHelper` that reads a private `~/.claude/scalemax.key` file. The raw key is not stored in the settings JSON. The Anthropic base URL intentionally has no `/v1` suffix because Claude Code adds `/v1/messages` itself. The CLI preserves unrelated settings, selects a live main model from the authenticated catalog (preferring `claude-opus-4-8`), pins authenticated Haiku for lightweight background work when available, and sets `CLAUDE_CODE_DISABLE_THINKING=1` because third-party Anthropic-compatible providers do not support Claude Code's proprietary extended-thinking control envelope. It verifies a one-token Messages request and, if verification fails, removes only the ScaleMax-managed changes. Claude Code picker aliases such as `claude-opus-4-8[1m]` are normalized by the gateway.
118
120
 
119
121
  ### OpenCode
120
122
 
@@ -313,4 +315,4 @@ Yes. The CLI uses Windows-aware config paths and writes `%APPDATA%\scalemax\conf
313
315
 
314
316
  ### What endpoint does it configure?
315
317
 
316
- OpenAI-compatible clients use `https://api.scalemax.pro/v1`. Anthropic-compatible clients such as Claude Code and OpenClaw use the origin `https://api.scalemax.pro`, because those clients append `/v1/messages` themselves.
318
+ API-credit keys use `https://api.scalemax.pro/v1` for OpenAI-compatible clients and `https://api.scalemax.pro` for Anthropic-compatible clients. Token-pack keys use `https://api.scalemax.pro/token/v1` and `https://api.scalemax.pro/token` respectively. The CLI detects the key type and configures the correct surface automatically; the two key types are intentionally not interchangeable.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  function cmp(a,b){a=String(a).replace(/^v/,'').split('.').map(Number);b=String(b).split('.').map(Number);for(var i=0;i<3;i++){var d=(a[i]||0)-(b[i]||0);if(d)return d;}return 0;}
4
- var min='18.17.0';
4
+ var min="18.19.0";
5
5
  if (cmp(process.versions.node,min)<0) {
6
6
  console.error('ScaleMax requires Node.js >= '+min+'. Current: '+process.versions.node);
7
7
  console.error('Install Node.js LTS, then rerun: npx scalemax@latest');
@@ -18,7 +18,7 @@ export async function printDetected(home = homeDir()) {
18
18
  console.log('');
19
19
  return detections;
20
20
  }
21
- async function maybeLaunchCodex(chosen, successful) {
21
+ async function maybeLaunchCodex(chosen, successful, baseUrl = SCALEMAX_BASE_URL) {
22
22
  if (!chosen.includes('codex') || !successful.has('codex'))
23
23
  return;
24
24
  console.log(chalk.green('\nConfiguration completed.\n'));
@@ -31,7 +31,7 @@ async function maybeLaunchCodex(chosen, successful) {
31
31
  const apiKey = await loadApiKey(homeDir());
32
32
  if (!apiKey)
33
33
  throw new Error('No API key found.');
34
- const result = await codex?.launch?.({ home: homeDir(), platform: process.platform, env: process.env, apiKey, baseUrl: SCALEMAX_BASE_URL });
34
+ const result = await codex?.launch?.({ home: homeDir(), platform: process.platform, env: process.env, apiKey, baseUrl });
35
35
  console.log(chalk.green(`✓ ${result?.message || 'Codex launched.'}`));
36
36
  }
37
37
  catch (err) {
@@ -61,6 +61,7 @@ export async function configureCommand(opts = {}) {
61
61
  if (!keyInfo.models.length)
62
62
  throw new Error(keyInfo.error || 'ScaleMax returned no models for this API key. No client configuration was changed.');
63
63
  const providerScope = keyInfo.key?.provider_scope || 'both';
64
+ const selectedBaseUrl = keyInfo.baseUrl || SCALEMAX_BASE_URL;
64
65
  const availableClients = clients.filter(c => clientAllowedForModels(c.id, keyInfo.models));
65
66
  console.log(chalk.dim(`API key scope: ${scopeHint(providerScope)}. Clients are filtered against the authenticated model catalog.`));
66
67
  const detections = await printDetected(home);
@@ -119,7 +120,7 @@ export async function configureCommand(opts = {}) {
119
120
  }
120
121
  const spinner = ora(`Configuring ${client.name}`).start();
121
122
  try {
122
- const ctx = { ...baseCtx, apiKey, baseUrl: SCALEMAX_BASE_URL, providerScope, models: keyInfo.models, interactive: Boolean(process.stdin.isTTY), allowAppModification };
123
+ const ctx = { ...baseCtx, apiKey, baseUrl: selectedBaseUrl, providerScope, models: keyInfo.models, interactive: Boolean(process.stdin.isTTY), allowAppModification };
123
124
  const res = await client.configure(ctx);
124
125
  spinner.succeed(`${client.name} ${res.message}`);
125
126
  if (client.verify) {
@@ -148,7 +149,7 @@ export async function configureCommand(opts = {}) {
148
149
  }
149
150
  }
150
151
  if (opts.promptLaunch !== false && !opts.all && process.stdin.isTTY)
151
- await maybeLaunchCodex(chosen, successful);
152
+ await maybeLaunchCodex(chosen, successful, selectedBaseUrl);
152
153
  if (failures.length)
153
154
  throw new Error(`${failures.length} client configuration(s) failed:\n${failures.map(f => `- ${f}`).join('\n')}`);
154
155
  }
@@ -22,7 +22,7 @@ export async function doctorCommand(home = homeDir()) {
22
22
  s.succeed(`API reachable, key valid, ${models.length} models loaded`);
23
23
  console.log(`${chalk.green('✓')} Usage and key metadata accessible through the authenticated API`);
24
24
  const st = ora('Checking streaming (small billable request)').start();
25
- const stream = await testStreaming(key, models.find(m => m.id === 'fable-5')?.id || models[0]?.id);
25
+ const stream = await testStreaming(key, models.find(m => m.id === 'fable-5')?.id || models[0]?.id, res.baseUrl);
26
26
  if (stream.ok)
27
27
  st.succeed('Streaming available');
28
28
  else {
@@ -49,7 +49,8 @@ export async function doctorCommand(home = homeDir()) {
49
49
  }
50
50
  if (client.verify && key && models.length) {
51
51
  try {
52
- const v = await client.verify({ home, platform: process.platform, env: process.env, apiKey: key, baseUrl: SCALEMAX_BASE_URL, models });
52
+ const checked = await validateApiKey(key);
53
+ const v = await client.verify({ home, platform: process.platform, env: process.env, apiKey: key, baseUrl: checked.baseUrl || SCALEMAX_BASE_URL, models });
53
54
  console.log(`${chalk.green('✓')} ${client.name} ${v.message}`);
54
55
  }
55
56
  catch (error) {
@@ -3,7 +3,6 @@ import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { validateApiKey } from '../utils/api.js';
5
5
  import { saveApiKey } from '../utils/keyStore.js';
6
- import { SCALEMAX_BASE_URL } from '../constants.js';
7
6
  import { homeDir } from '../utils/paths.js';
8
7
  import { sanitizeApiKey } from '../utils/apiKey.js';
9
8
  export async function loginCommand(home = homeDir()) {
@@ -20,7 +19,7 @@ export async function loginCommand(home = homeDir()) {
20
19
  spinner.fail('Invalid API Key');
21
20
  throw new Error(result.error || 'Invalid API Key');
22
21
  }
23
- await saveApiKey(sanitizedKey, SCALEMAX_BASE_URL, home);
22
+ await saveApiKey(sanitizedKey, result.baseUrl, home);
24
23
  spinner.succeed(`API Key verified (${result.key?.provider_scope || 'both'} provider scope)`);
25
24
  outro(chalk.green('ScaleMax API key saved.'));
26
25
  return sanitizedKey;
@@ -13,7 +13,8 @@ const name = 'Claude Code';
13
13
  const discoveryKey = 'CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY';
14
14
  const fastModelKey = 'ANTHROPIC_DEFAULT_HAIKU_MODEL';
15
15
  const helperTtlKey = 'CLAUDE_CODE_API_KEY_HELPER_TTL_MS';
16
- const managedEnvKeys = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL', discoveryKey, fastModelKey, helperTtlKey];
16
+ const disableThinkingKey = 'CLAUDE_CODE_DISABLE_THINKING';
17
+ const managedEnvKeys = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL', discoveryKey, fastModelKey, helperTtlKey, disableThinkingKey];
17
18
  function secretPath(home) { return path.join(home, '.claude', 'scalemax.key'); }
18
19
  function helperCommand(home, platform) {
19
20
  const file = secretPath(home);
@@ -47,7 +48,8 @@ export async function configure(ctx) {
47
48
  ANTHROPIC_BASE_URL: baseUrl,
48
49
  ANTHROPIC_MODEL: model,
49
50
  [discoveryKey]: '1',
50
- [helperTtlKey]: '900000'
51
+ [helperTtlKey]: '900000',
52
+ [disableThinkingKey]: '1'
51
53
  };
52
54
  delete nextEnv.ANTHROPIC_AUTH_TOKEN;
53
55
  delete nextEnv.ANTHROPIC_API_KEY;
@@ -67,6 +69,7 @@ export async function configure(ctx) {
67
69
  gatewayDiscovery: '1',
68
70
  fastModel: quick || null,
69
71
  helperTtl: '900000',
72
+ disableThinking: '1',
70
73
  apiKeyHelper,
71
74
  authTokenSha256: crypto.createHash('sha256').update(ctx.apiKey).digest('hex'),
72
75
  previousEnv,
@@ -86,7 +89,7 @@ export async function verify(ctx) {
86
89
  const settings = await fs.readJson(file);
87
90
  const env = asRecord(settings?.env);
88
91
  const model = String(env.ANTHROPIC_MODEL || '');
89
- if (!modelIds(ctx).includes(model) || env.ANTHROPIC_BASE_URL !== anthropicOrigin(ctx.baseUrl) || settings.apiKeyHelper !== helperCommand(ctx.home, ctx.platform) || env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || env[discoveryKey] !== '1') {
92
+ if (!modelIds(ctx).includes(model) || env.ANTHROPIC_BASE_URL !== anthropicOrigin(ctx.baseUrl) || settings.apiKeyHelper !== helperCommand(ctx.home, ctx.platform) || env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || env[discoveryKey] !== '1' || env[disableThinkingKey] !== '1') {
90
93
  throw new Error('Claude Code settings do not match the authenticated ScaleMax model catalog.');
91
94
  }
92
95
  await verifyClientSecret(secretPath(ctx.home), ctx);
@@ -138,6 +141,8 @@ export async function remove(ctx) {
138
141
  restore(fastModelKey);
139
142
  if (env[helperTtlKey] === managed.helperTtl)
140
143
  restore(helperTtlKey);
144
+ if (env[disableThinkingKey] === managed.disableThinking)
145
+ restore(disableThinkingKey);
141
146
  if (!Object.prototype.hasOwnProperty.call(env, 'ANTHROPIC_AUTH_TOKEN'))
142
147
  restore('ANTHROPIC_AUTH_TOKEN');
143
148
  if (!Object.prototype.hasOwnProperty.call(env, 'ANTHROPIC_API_KEY'))
@@ -1,4 +1,5 @@
1
1
  export declare const SCALEMAX_BASE_URL = "https://api.scalemax.pro/v1";
2
+ export declare const SCALEMAX_TOKEN_BASE_URL = "https://api.scalemax.pro/token/v1";
2
3
  export declare const APP_NAME = "ScaleMax";
3
4
  export declare const GENERATED_HEADER = "ScaleMax managed configuration";
4
5
  export declare const KEY_ENV = "SCALEMAX_API_KEY";
package/dist/constants.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export const SCALEMAX_BASE_URL = 'https://api.scalemax.pro/v1';
2
+ export const SCALEMAX_TOKEN_BASE_URL = 'https://api.scalemax.pro/token/v1';
2
3
  export const APP_NAME = 'ScaleMax';
3
4
  export const GENERATED_HEADER = 'ScaleMax managed configuration';
4
5
  export const KEY_ENV = 'SCALEMAX_API_KEY';
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { modelsCommand } from './commands/models.js';
12
12
  import { updateCommand } from './commands/update.js';
13
13
  import { removeCommand } from './commands/remove.js';
14
14
  import { ensureInstallerApiKey } from './utils/installerConfig.js';
15
- import { isNodeSupported, nodeUpgradeHint } from './utils/runtime.js';
15
+ import { MIN_NODE, isNodeSupported, nodeUpgradeHint } from './utils/runtime.js';
16
16
  import { runSystemAudit } from './utils/systemAudit.js';
17
17
  function packageVersion() {
18
18
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -63,7 +63,7 @@ async function runInteractiveInstaller() {
63
63
  }
64
64
  async function main() {
65
65
  if (!isNodeSupported()) {
66
- console.error(chalk.red(`ScaleMax requires Node.js >=18.19. Current: ${process.versions.node}`));
66
+ console.error(chalk.red(`ScaleMax requires Node.js >=${MIN_NODE}. Current: ${process.versions.node}`));
67
67
  console.error(nodeUpgradeHint());
68
68
  process.exitCode = 1;
69
69
  return;
@@ -14,15 +14,18 @@ export interface KeyMetadata {
14
14
  prefix?: string;
15
15
  label?: string;
16
16
  }
17
- export declare function isAnthropicModel(model: ModelInfo): boolean;
18
- export declare function filterModelsForScope(models: ModelInfo[], scope: ProviderScope): ModelInfo[];
19
- export declare function validateApiKey(apiKey: string): Promise<{
17
+ export interface ApiValidation {
20
18
  ok: boolean;
21
19
  models: ModelInfo[];
22
20
  key?: KeyMetadata;
21
+ baseUrl?: string;
23
22
  error?: string;
24
- }>;
25
- export declare function testStreaming(apiKey: string, model?: string): Promise<{
23
+ surfaceMismatch?: boolean;
24
+ }
25
+ export declare function isAnthropicModel(model: ModelInfo): boolean;
26
+ export declare function filterModelsForScope(models: ModelInfo[], scope: ProviderScope): ModelInfo[];
27
+ export declare function validateApiKey(apiKey: string): Promise<ApiValidation>;
28
+ export declare function testStreaming(apiKey: string, model?: string, baseUrl?: string): Promise<{
26
29
  ok: boolean;
27
30
  error?: string;
28
31
  }>;
package/dist/utils/api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { SCALEMAX_BASE_URL } from '../constants.js';
1
+ import { SCALEMAX_BASE_URL, SCALEMAX_TOKEN_BASE_URL } from '../constants.js';
2
2
  import { redactApiKey, sanitizeApiKey } from './apiKey.js';
3
3
  export function isAnthropicModel(model) {
4
4
  const provider = String(model.provider || model.owned_by || '').trim().toLowerCase();
@@ -34,7 +34,7 @@ async function friendlyError(res) {
34
34
  function retryableStatus(status) {
35
35
  return status === 408 || status === 429 || status >= 500;
36
36
  }
37
- async function request(path, apiKey, init = {}, retryGet = false) {
37
+ async function request(path, apiKey, init = {}, retryGet = false, baseUrl = SCALEMAX_BASE_URL) {
38
38
  const headers = new Headers(init.headers);
39
39
  if (apiKey)
40
40
  headers.set('Authorization', `Bearer ${sanitizeApiKey(apiKey)}`);
@@ -47,7 +47,7 @@ async function request(path, apiKey, init = {}, retryGet = false) {
47
47
  const timeout = AbortSignal.timeout(15000);
48
48
  const signal = init.signal && typeof AbortSignal.any === 'function' ? AbortSignal.any([init.signal, timeout]) : timeout;
49
49
  try {
50
- const response = await fetch(`${SCALEMAX_BASE_URL}${path}`, { ...init, headers, signal });
50
+ const response = await fetch(`${baseUrl}${path}`, { ...init, headers, signal });
51
51
  if (attempt + 1 === attempts || !retryableStatus(response.status))
52
52
  return response;
53
53
  await response.body?.cancel().catch(() => undefined);
@@ -61,14 +61,14 @@ async function request(path, apiKey, init = {}, retryGet = false) {
61
61
  }
62
62
  throw lastError;
63
63
  }
64
- export async function validateApiKey(apiKey) {
64
+ async function validateOnSurface(apiKey, baseUrl) {
65
65
  try {
66
66
  const [authCheck, modelsRes] = await Promise.all([
67
- request('/usage', apiKey, {}, true),
68
- request('/models', apiKey, {}, true)
67
+ request('/usage', apiKey, {}, true, baseUrl),
68
+ request('/models', apiKey, {}, true, baseUrl)
69
69
  ]);
70
70
  if (!authCheck.ok)
71
- return { ok: false, models: [], error: redactApiKey(await friendlyError(authCheck), apiKey) };
71
+ return { ok: false, models: [], error: redactApiKey(await friendlyError(authCheck), apiKey), surfaceMismatch: authCheck.status === 401 };
72
72
  const usage = await authCheck.json().catch(() => ({}));
73
73
  const keyMeta = { provider_scope: usage?.key?.provider_scope || 'both', mode: usage?.key?.mode, prefix: usage?.key?.prefix, label: usage?.key?.label };
74
74
  if (!modelsRes.ok)
@@ -78,17 +78,28 @@ export async function validateApiKey(apiKey) {
78
78
  const models = filterModelsForScope(catalog, keyMeta.provider_scope);
79
79
  if (!models.length)
80
80
  return { ok: false, models: [], key: keyMeta, error: 'ScaleMax returned an empty model catalog.' };
81
- return { ok: true, models, key: keyMeta };
81
+ return { ok: true, models, key: keyMeta, baseUrl };
82
82
  }
83
83
  catch (err) {
84
84
  const message = redactApiKey(err instanceof Error ? err.message : String(err), apiKey);
85
85
  return { ok: false, models: [], error: /abort|timeout/i.test(message) ? 'ScaleMax request timed out after 15 seconds.' : message };
86
86
  }
87
87
  }
88
- export async function testStreaming(apiKey, model) {
88
+ export async function validateApiKey(apiKey) {
89
+ const credit = await validateOnSurface(apiKey, SCALEMAX_BASE_URL);
90
+ if (credit.ok)
91
+ return credit;
92
+ if (!credit.surfaceMismatch)
93
+ return credit;
94
+ const token = await validateOnSurface(apiKey, SCALEMAX_TOKEN_BASE_URL);
95
+ if (token.ok)
96
+ return token;
97
+ return credit.error && !/Invalid API key|rejected|401/i.test(credit.error) ? credit : token;
98
+ }
99
+ export async function testStreaming(apiKey, model, baseUrl = SCALEMAX_BASE_URL) {
89
100
  const selected = model || 'fable-5';
90
101
  try {
91
- const res = await request('/chat/completions', apiKey, { method: 'POST', body: JSON.stringify({ model: selected, stream: true, max_tokens: 8, messages: [{ role: 'user', content: 'Say OK' }] }) });
102
+ const res = await request('/chat/completions', apiKey, { method: 'POST', body: JSON.stringify({ model: selected, stream: true, max_tokens: 8, messages: [{ role: 'user', content: 'Say OK' }] }) }, false, baseUrl);
92
103
  if (!res.ok)
93
104
  return { ok: false, error: redactApiKey(await friendlyError(res), apiKey) };
94
105
  const ct = res.headers.get('content-type') || '';
@@ -1,7 +1,7 @@
1
1
  import { intro, outro, password, select, confirm, isCancel, cancel } from '@clack/prompts';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
- import { KEY_ENV, SCALEMAX_BASE_URL } from '../constants.js';
4
+ import { KEY_ENV } from '../constants.js';
5
5
  import { validateApiKey } from './api.js';
6
6
  import { loadApiKey, removeApiKey, saveApiKey } from './keyStore.js';
7
7
  import { homeDir } from './paths.js';
@@ -25,7 +25,7 @@ export async function promptForValidatedKey(home, message = 'Enter your ScaleMax
25
25
  spinner.fail('Invalid API key');
26
26
  throw new Error(result.error || 'Invalid API key');
27
27
  }
28
- await saveApiKey(candidate, SCALEMAX_BASE_URL, home);
28
+ await saveApiKey(candidate, result.baseUrl, home);
29
29
  spinner.succeed('API key verified and saved');
30
30
  return candidate;
31
31
  }
@@ -1,3 +1,4 @@
1
1
  export declare function saveApiKey(apiKey: string, baseUrl: string, home: string): Promise<void>;
2
2
  export declare function loadApiKey(home: string): Promise<string | undefined>;
3
+ export declare function loadBaseUrl(home: string): Promise<string | undefined>;
3
4
  export declare function removeApiKey(home: string): Promise<void>;
@@ -13,6 +13,10 @@ export async function loadApiKey(home) {
13
13
  const cfg = await readJson(scaleMaxConfigPath(home), {});
14
14
  return cfg.apiKey ? sanitizeApiKey(cfg.apiKey) : undefined;
15
15
  }
16
+ export async function loadBaseUrl(home) {
17
+ const cfg = await readJson(scaleMaxConfigPath(home), {});
18
+ return cfg.baseUrl;
19
+ }
16
20
  export async function removeApiKey(home) {
17
21
  await fs.remove(scaleMaxConfigPath(home));
18
22
  }
@@ -1,4 +1,4 @@
1
- export declare const MIN_NODE = "18.17.0";
1
+ export declare const MIN_NODE = "18.19.0";
2
2
  export declare const RECOMMENDED_NODE = "22";
3
3
  export type OsFamily = 'macos' | 'ubuntu' | 'debian' | 'fedora' | 'arch' | 'windows' | 'wsl' | 'linux' | 'unknown';
4
4
  export type RuntimeAudit = {
@@ -1,7 +1,7 @@
1
1
  import os from 'node:os';
2
2
  import { execa } from 'execa';
3
3
  import { commandExists } from './exec.js';
4
- export const MIN_NODE = '18.17.0';
4
+ export const MIN_NODE = '18.19.0';
5
5
  export const RECOMMENDED_NODE = '22';
6
6
  export function compareSemver(a, b) { const pa = a.replace(/^v/, '').split('.').map(Number), pb = b.replace(/^v/, '').split('.').map(Number); for (let i = 0; i < 3; i++) {
7
7
  const d = (pa[i] || 0) - (pb[i] || 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scalemax",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Universal ScaleMax installer for AI coding clients",
5
5
  "type": "module",
6
6
  "bin": {