scalemax 0.1.42 → 0.1.44

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.
@@ -115,6 +115,7 @@ function scopeHint(scope) {
115
115
  'glm-5.2': 'GLM 5.2 only',
116
116
  'deepseek-v4-pro': 'DeepSeek V4 Pro only',
117
117
  'auto-api': 'Auto only',
118
+ voice: 'Voice only',
118
119
  };
119
120
  return labels[scope];
120
121
  }
@@ -23,6 +23,14 @@ const customModelOptionDescriptionKey = 'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTI
23
23
  const fableModelId = 'fable-5';
24
24
  const fableModelName = 'Fable 5';
25
25
  const fableModelDescription = 'Fable 5 via ScaleMax';
26
+ // Claude's picker exposes ONE custom model slot (shared with Auto). fable-5 keeps
27
+ // it whenever the key has fable-5, so re-running `update` never silently moves an
28
+ // existing user onto a different model and price; fable-5.1 takes the slot only
29
+ // when it is the Fable model this key can reach. Either id stays callable
30
+ // explicitly (e.g. `/model fable-5.1`).
31
+ const fable51ModelId = 'fable-5.1';
32
+ const fable51ModelName = 'Fable 5.1';
33
+ const fable51ModelDescription = 'Fable 5.1 via ScaleMax';
26
34
  const autoModelName = 'Auto';
27
35
  const autoModelDescription = 'Auto via ScaleMax';
28
36
  const verificationTimeoutMs = 60_000;
@@ -43,6 +51,9 @@ const managedCustomModelValues = new Set([
43
51
  fableModelId,
44
52
  fableModelName,
45
53
  fableModelDescription,
54
+ fable51ModelId,
55
+ fable51ModelName,
56
+ fable51ModelDescription,
46
57
  AUTO_MODEL_ID,
47
58
  autoModelName,
48
59
  autoModelDescription
@@ -57,11 +68,11 @@ function helperCommand(home, platform) {
57
68
  function asRecord(value) {
58
69
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
59
70
  }
71
+ function hasModelId(ctx, ...ids) {
72
+ return (ctx.models || []).some(model => ids.includes(String(model.id || '').trim().toLowerCase()));
73
+ }
60
74
  function hasFableModel(ctx) {
61
- return (ctx.models || []).some(model => {
62
- const id = String(model.id || '').trim().toLowerCase();
63
- return id === fableModelId || id === 'claude-fable-5';
64
- });
75
+ return hasModelId(ctx, fableModelId, 'claude-fable-5');
65
76
  }
66
77
  function hasAutoModel(ctx) {
67
78
  return modelIds(ctx).some(id => id.trim().toLowerCase() === AUTO_MODEL_ID);
@@ -74,6 +85,13 @@ function managedCustomModel(ctx) {
74
85
  description: fableModelDescription
75
86
  };
76
87
  }
88
+ if (hasModelId(ctx, fable51ModelId)) {
89
+ return {
90
+ option: fable51ModelId,
91
+ name: fable51ModelName,
92
+ description: fable51ModelDescription
93
+ };
94
+ }
77
95
  if (hasAutoModel(ctx)) {
78
96
  return {
79
97
  option: AUTO_MODEL_ID,
@@ -11,7 +11,7 @@ export function jsonConfig(client, apiKey, baseUrl, extra = {}) {
11
11
  }, null, 2) + '\n';
12
12
  }
13
13
  export function continueConfig(apiKey, baseUrl) {
14
- const models = ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'codex-auto-review', 'deepseek-v4-flash'];
14
+ const models = ['gpt-6-astra', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'codex-auto-review', 'deepseek-v4-flash'];
15
15
  return JSON.stringify({
16
16
  models: models.map(model => ({
17
17
  title: model === 'deepseek-v4-flash' ? 'ScaleMax DeepSeek V4 Flash' : `ScaleMax ${model}`,
@@ -1,5 +1,8 @@
1
+ export declare function parseCodexSemver(text?: string): string | undefined;
2
+ export declare function compareCodexSemver(a: string, b: string): number;
1
3
  export declare function isCodexExecutableAvailable(): Promise<boolean>;
2
4
  export declare function getCodexVersion(): Promise<string | undefined>;
5
+ export declare function resolveLatestCodexVersion(): Promise<string | undefined>;
3
6
  export declare function installCodexWithNpm(): Promise<{
4
7
  ok: boolean;
5
8
  message: string;
@@ -1,6 +1,27 @@
1
1
  import { confirm, isCancel } from '@clack/prompts';
2
2
  import { execa } from 'execa';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
3
5
  import { commandExists } from '../utils/exec.js';
6
+ const PKG = '@openai/codex';
7
+ const NPMJS = 'https://registry.npmjs.org/';
8
+ function npmCmd() { return process.platform === 'win32' ? 'npm.cmd' : 'npm'; }
9
+ function specFor(ver) { return ver ? `${PKG}@${ver}` : `${PKG}@latest`; }
10
+ function installHint(ver) { return `npm install -g ${specFor(ver)} --prefer-online`; }
11
+ export function parseCodexSemver(text) {
12
+ const m = String(text ?? '').match(/(?<![\d.])(\d+\.\d+\.\d+)(?![\d.])/);
13
+ return m?.[1];
14
+ }
15
+ export function compareCodexSemver(a, b) {
16
+ const pa = a.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
17
+ const pb = b.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
18
+ for (let i = 0; i < 3; i++) {
19
+ const d = (pa[i] || 0) - (pb[i] || 0);
20
+ if (d)
21
+ return d;
22
+ }
23
+ return 0;
24
+ }
4
25
  export async function isCodexExecutableAvailable() { return commandExists('codex'); }
5
26
  export async function getCodexVersion() { try {
6
27
  return (await execa('codex', ['--version'], { reject: false })).stdout.trim();
@@ -8,32 +29,140 @@ export async function getCodexVersion() { try {
8
29
  catch {
9
30
  return undefined;
10
31
  } }
32
+ async function viewLatest(useNpmjs) {
33
+ const args = ['view', `${PKG}@latest`, 'version', '--prefer-online'];
34
+ if (useNpmjs)
35
+ args.push(`--registry=${NPMJS}`);
36
+ try {
37
+ const res = await execa(npmCmd(), args, { reject: false, timeout: 15000 });
38
+ if (res.exitCode !== 0)
39
+ return undefined;
40
+ return parseCodexSemver(res.stdout);
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ export async function resolveLatestCodexVersion() {
47
+ return (await viewLatest(true)) ?? (await viewLatest(false));
48
+ }
49
+ async function npmInstall(spec, registry) {
50
+ const args = ['install', '-g', spec, '--prefer-online'];
51
+ if (registry)
52
+ args.push(`--registry=${registry}`);
53
+ return execa(npmCmd(), args, { all: true, reject: false, stdio: 'pipe' });
54
+ }
55
+ async function npmGlobalBinDir() {
56
+ try {
57
+ const prefix = (await execa(npmCmd(), ['prefix', '-g'], { reject: false })).stdout.trim();
58
+ if (!prefix)
59
+ return undefined;
60
+ return process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ function npmGlobalCodexPath(binDir) {
67
+ return path.join(binDir, process.platform === 'win32' ? 'codex.cmd' : 'codex');
68
+ }
69
+ async function whichCodex() {
70
+ try {
71
+ if (process.platform === 'win32') {
72
+ const res = await execa('where', ['codex'], { reject: false });
73
+ return res.stdout.trim().split(/\r?\n/)[0] || undefined;
74
+ }
75
+ const res = await execa('which', ['codex'], { reject: false });
76
+ return res.stdout.trim() || undefined;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ async function versionAt(bin) {
83
+ try {
84
+ return parseCodexSemver((await execa(bin, ['--version'], { reject: false })).stdout);
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ }
90
+ async function inspectNpmCodex() {
91
+ const binDir = await npmGlobalBinDir();
92
+ if (!binDir)
93
+ return {};
94
+ const bin = npmGlobalCodexPath(binDir);
95
+ if (!fs.existsSync(bin))
96
+ return { binDir };
97
+ return { binDir, bin, version: await versionAt(bin) };
98
+ }
11
99
  export async function installCodexWithNpm() {
12
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
13
- const args = ['install', '-g', '@openai/codex@latest'];
100
+ const target = await resolveLatestCodexVersion();
101
+ const spec = specFor(target);
102
+ const hint = installHint(target);
14
103
  try {
15
- const res = await execa(npm, args, { all: true, reject: false, stdio: 'pipe' });
16
- if (res.exitCode === 0)
17
- return { ok: true, message: 'Codex installed with npm.' };
18
- return { ok: false, message: `npm install exited with ${res.exitCode}.`, output: res.all };
104
+ let res = await npmInstall(spec);
105
+ let inspected = await inspectNpmCodex();
106
+ let pathVer = parseCodexSemver(await getCodexVersion());
107
+ const yielded = inspected.version ?? pathVer;
108
+ const older = Boolean(target && yielded && compareCodexSemver(yielded, target) < 0);
109
+ if (res.exitCode !== 0 || older) {
110
+ const retry = await npmInstall(spec, NPMJS);
111
+ if (retry.exitCode === 0) {
112
+ res = retry;
113
+ inspected = await inspectNpmCodex();
114
+ pathVer = parseCodexSemver(await getCodexVersion());
115
+ }
116
+ else if (res.exitCode !== 0) {
117
+ return { ok: false, message: `npm install exited with ${retry.exitCode}.`, output: retry.all };
118
+ }
119
+ }
120
+ pathVer = parseCodexSemver(await getCodexVersion());
121
+ if (target && pathVer && compareCodexSemver(pathVer, target) < 0) {
122
+ const npmVer = inspected.version;
123
+ if (inspected.bin && inspected.binDir && npmVer && compareCodexSemver(npmVer, target) >= 0) {
124
+ const shadowed = (await whichCodex()) || 'codex';
125
+ return { ok: true, message: `Codex ${npmVer} installed at ${inspected.bin}, but an older Codex (${pathVer}) at ${shadowed} is shadowing it on PATH. Remove the older binary or put ${inspected.binDir} first on PATH.`, output: res.all };
126
+ }
127
+ }
128
+ if (res.exitCode !== 0)
129
+ return { ok: false, message: `npm install exited with ${res.exitCode}.`, output: res.all };
130
+ return { ok: true, message: 'Codex installed with npm.', output: res.all };
19
131
  }
20
132
  catch (err) {
21
- return { ok: false, message: `Could not run npm install -g @openai/codex@latest: ${err instanceof Error ? err.message : String(err)}` };
133
+ return { ok: false, message: `Could not run ${hint}: ${err instanceof Error ? err.message : String(err)}` };
22
134
  }
23
135
  }
24
136
  export async function ensureCodexInstalled(interactive = true) {
25
- if (await isCodexExecutableAvailable())
26
- return { ok: true, installed: false, message: `Codex detected${(await getCodexVersion()) ? ` (${await getCodexVersion()})` : ''}.` };
137
+ if (await isCodexExecutableAvailable()) {
138
+ const raw = await getCodexVersion();
139
+ const current = parseCodexSemver(raw);
140
+ const latest = await resolveLatestCodexVersion().catch(() => undefined);
141
+ if (!latest || !current || compareCodexSemver(current, latest) >= 0) {
142
+ return { ok: true, installed: false, message: `Codex detected${raw ? ` (${raw})` : ''}.` };
143
+ }
144
+ if (!interactive) {
145
+ return { ok: true, installed: false, message: `Codex detected (${raw ?? current}). Update available: ${latest}. Run ${installHint(latest)} to update.` };
146
+ }
147
+ const ans = await confirm({ message: `Codex ${current} is installed; update to latest ${latest}?`, initialValue: true });
148
+ if (isCancel(ans) || ans === false)
149
+ return { ok: true, installed: false, message: `Codex detected (${raw ?? current}).` };
150
+ const res = await installCodexWithNpm();
151
+ if (!res.ok)
152
+ return { ok: false, installed: false, message: `${res.message}\nIf this is a permission issue, install Node with nvm/fnm/volta or run: ${installHint(latest)}` };
153
+ return { ok: true, installed: true, message: res.message.includes('shadowing') ? res.message : 'Codex installed and detected.' };
154
+ }
27
155
  if (!interactive)
28
156
  return { ok: false, installed: false, message: 'Codex executable was not found on PATH.' };
29
- const ans = await confirm({ message: 'Codex CLI is not installed. Install it now with npm install -g @openai/codex@latest?', initialValue: true });
157
+ const latest = await resolveLatestCodexVersion().catch(() => undefined);
158
+ const hint = installHint(latest);
159
+ const ans = await confirm({ message: `Codex CLI is not installed. Install it now with ${hint}?`, initialValue: true });
30
160
  if (isCancel(ans) || ans === false)
31
161
  return { ok: false, installed: false, message: 'Codex installation skipped.' };
32
162
  const res = await installCodexWithNpm();
33
163
  if (!res.ok)
34
- return { ok: false, installed: false, message: `${res.message}
35
- If this is a permission issue, install Node with nvm/fnm/volta or run: npm install -g @openai/codex@latest` };
164
+ return { ok: false, installed: false, message: `${res.message}\nIf this is a permission issue, install Node with nvm/fnm/volta or run: ${hint}` };
36
165
  if (!(await isCodexExecutableAvailable()))
37
166
  return { ok: false, installed: true, message: 'Codex install finished but codex is not on PATH. Open a new terminal or refresh PATH, then rerun npx scalemax.' };
38
- return { ok: true, installed: true, message: 'Codex installed and detected.' };
167
+ return { ok: true, installed: true, message: res.message.includes('shadowing') ? res.message : 'Codex installed and detected.' };
39
168
  }
@@ -1,6 +1,6 @@
1
1
  export type ClientId = 'codex' | 'cursor' | 'roo-code' | 'cline' | 'continue' | 'trae-solo' | 'claude' | 'opencode' | 'openclaw' | 'hermes' | 'cherry-studio' | 'api-code';
2
2
  /** Keep this mirror aligned with shared/constants/api-key-scopes.ts. */
3
- export declare const API_KEY_SCOPE_VALUES: readonly ["openai", "anthropic", "both", "deepseek-v4-flash", "glm-5.2", "deepseek-v4-pro", "auto-api", "reseller", "gemini", "grok", "qwen", "deepseek", "mimo", "local"];
3
+ export declare const API_KEY_SCOPE_VALUES: readonly ["openai", "anthropic", "both", "deepseek-v4-flash", "glm-5.2", "deepseek-v4-pro", "auto-api", "reseller", "gemini", "grok", "qwen", "deepseek", "mimo", "local", "voice"];
4
4
  export type ProviderScope = (typeof API_KEY_SCOPE_VALUES)[number];
5
5
  export declare function isProviderScope(value: unknown): value is ProviderScope;
6
6
  export interface ClientDefinition {
@@ -14,6 +14,7 @@ export const API_KEY_SCOPE_VALUES = [
14
14
  'deepseek',
15
15
  'mimo',
16
16
  'local',
17
+ 'voice',
17
18
  ];
18
19
  export function isProviderScope(value) {
19
20
  return typeof value === 'string'
package/dist/utils/api.js CHANGED
@@ -18,7 +18,9 @@ export const MODEL_PROBE_ENOUGH_WORKING = 8;
18
18
  export const MODEL_PROBE_MAX_MODELS = 16;
19
19
  // Keep only aliases that are no longer offered. Direct GPT-5.6 Sol is an
20
20
  // active Codex model and must remain available to all generated clients.
21
- export const RETIRED_MODEL_IDS = new Set(['fable-5-ultra', 'gpt-5.6-luna']);
21
+ // gpt-5.6-luna was removed from this list: the API revived it 2026-08-14
22
+ // (shared/constants/retired-models.ts), so hiding it here hid a model on sale.
23
+ export const RETIRED_MODEL_IDS = new Set(['fable-5-ultra']);
22
24
  export function isRetiredModelId(id) {
23
25
  return RETIRED_MODEL_IDS.has(id.trim());
24
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scalemax",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Universal ScaleMax installer for AI coding clients",
5
5
  "type": "module",
6
6
  "bin": {