scalemax 0.1.6 → 0.1.8

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.
@@ -4,6 +4,7 @@ import ora from 'ora';
4
4
  import { clients } from '../clients.js';
5
5
  import { SCALEMAX_BASE_URL } from '../constants.js';
6
6
  import { loadApiKey } from '../utils/keyStore.js';
7
+ import { validateApiKey } from '../utils/api.js';
7
8
  import { homeDir } from '../utils/paths.js';
8
9
  export async function detectAll(home = homeDir()) {
9
10
  const ctx = { home, platform: process.platform, env: process.env };
@@ -38,33 +39,49 @@ async function maybeLaunchCodex(chosen, successful) {
38
39
  }
39
40
  console.log('\nIf Codex asks for authentication,\nit should already be configured to use ScaleMax.\n\nIf you still see the official login screen,\nthe configuration is incomplete or Codex rejected the generated auth/config files.\n');
40
41
  }
42
+ function clientAllowedForScope(clientId, scope) {
43
+ if (scope === 'both')
44
+ return true;
45
+ if (clientId === 'claude')
46
+ return scope === 'anthropic';
47
+ if (clientId === 'api-code')
48
+ return true;
49
+ return scope === 'openai';
50
+ }
51
+ function scopeHint(scope) {
52
+ return scope === 'both' ? 'OpenAI + Claude' : scope === 'anthropic' ? 'Claude only' : 'OpenAI only';
53
+ }
41
54
  export async function configureCommand(opts = {}) {
42
55
  const home = opts.home || homeDir();
43
56
  const apiKey = await loadApiKey(home);
44
57
  if (!apiKey)
45
58
  throw new Error('No ScaleMax API key found. Run `scalemax login` first.');
59
+ const keyInfo = await validateApiKey(apiKey);
60
+ const providerScope = keyInfo.key?.provider_scope || 'both';
61
+ const availableClients = clients.filter(c => clientAllowedForScope(c.id, providerScope));
62
+ console.log(chalk.dim(`API key scope: ${scopeHint(providerScope)}. Incompatible clients are hidden.`));
46
63
  const detections = await printDetected(home);
47
64
  const detectedIds = new Set(detections.filter(d => d.installed).map(d => d.id));
48
65
  let chosen;
49
66
  if (opts.all)
50
- chosen = clients.filter(c => detectedIds.has(c.id)).map(c => c.id);
67
+ chosen = availableClients.filter(c => detectedIds.has(c.id)).map(c => c.id);
51
68
  else {
52
69
  const action = await select({ message: 'Configure', options: [
53
- ...clients.map((c, idx) => ({ value: c.id, label: `${idx + 1}. ${c.name}${detectedIds.has(c.id) ? '' : ' (not detected)'}` })),
70
+ ...availableClients.map((c, idx) => ({ value: c.id, label: `${idx + 1}. ${c.name}${detectedIds.has(c.id) ? '' : ' (not detected)'}` })),
54
71
  { value: 'all', label: 'A. Configure ALL detected clients' }
55
72
  ] });
56
73
  if (isCancel(action)) {
57
74
  cancel('Configuration cancelled.');
58
75
  return;
59
76
  }
60
- chosen = action === 'all' ? clients.filter(c => detectedIds.has(c.id)).map(c => c.id) : [action];
77
+ chosen = action === 'all' ? availableClients.filter(c => detectedIds.has(c.id)).map(c => c.id) : [action];
61
78
  }
62
79
  if (chosen.length === 0) {
63
80
  console.log(chalk.yellow('No detected clients to configure.'));
64
81
  return;
65
82
  }
66
83
  const successful = new Set();
67
- for (const client of clients.filter(c => chosen.includes(c.id))) {
84
+ for (const client of availableClients.filter(c => chosen.includes(c.id))) {
68
85
  const baseCtx = { home, platform: process.platform, env: process.env };
69
86
  if (client.install) {
70
87
  const spinner = ora(`Checking ${client.name}`).start();
@@ -79,7 +96,7 @@ export async function configureCommand(opts = {}) {
79
96
  }
80
97
  const spinner = ora(`Configuring ${client.name}`).start();
81
98
  try {
82
- const ctx = { ...baseCtx, apiKey, baseUrl: SCALEMAX_BASE_URL };
99
+ const ctx = { ...baseCtx, apiKey, baseUrl: SCALEMAX_BASE_URL, providerScope };
83
100
  const res = await client.configure(ctx);
84
101
  spinner.succeed(`${client.name} ${res.message}`);
85
102
  if (client.verify) {
@@ -24,7 +24,7 @@ export async function doctorCommand(home = homeDir()) {
24
24
  failures++;
25
25
  }
26
26
  const st = ora('Checking streaming').start();
27
- const stream = await testStreaming(key, res.models.find(m => m.id === 'gpt-5.6-terra')?.id || res.models[0]?.id);
27
+ const stream = await testStreaming(key, res.models.find(m => m.id === 'fable-5')?.id || res.models[0]?.id);
28
28
  if (stream.ok)
29
29
  st.succeed('Streaming available');
30
30
  else {
@@ -5,6 +5,7 @@ import { validateApiKey } from '../utils/api.js';
5
5
  import { saveApiKey } from '../utils/keyStore.js';
6
6
  import { SCALEMAX_BASE_URL } from '../constants.js';
7
7
  import { homeDir } from '../utils/paths.js';
8
+ import { sanitizeApiKey } from '../utils/apiKey.js';
8
9
  export async function loginCommand(home = homeDir()) {
9
10
  intro(chalk.cyan('ScaleMax Login'));
10
11
  const key = await password({ message: 'Paste your ScaleMax API Key:', mask: '•', validate(value) { return value.trim().length < 12 ? 'Enter a valid ScaleMax API key.' : undefined; } });
@@ -12,14 +13,15 @@ export async function loginCommand(home = homeDir()) {
12
13
  cancel('Login cancelled.');
13
14
  return;
14
15
  }
16
+ const sanitizedKey = sanitizeApiKey(key);
15
17
  const spinner = ora('Verifying API key').start();
16
- const result = await validateApiKey(String(key).trim());
18
+ const result = await validateApiKey(sanitizedKey);
17
19
  if (!result.ok) {
18
20
  spinner.fail('Invalid API Key');
19
21
  throw new Error(result.error || 'Invalid API Key');
20
22
  }
21
- await saveApiKey(String(key).trim(), SCALEMAX_BASE_URL, home);
22
- spinner.succeed('API Key verified');
23
+ await saveApiKey(sanitizedKey, SCALEMAX_BASE_URL, home);
24
+ spinner.succeed(`API Key verified (${result.key?.provider_scope || 'both'} provider scope)`);
23
25
  outro(chalk.green('ScaleMax API key saved.'));
24
- return String(key).trim();
26
+ return sanitizedKey;
25
27
  }
@@ -5,7 +5,7 @@ const id = 'cherry-studio';
5
5
  const name = 'Cherry Studio';
6
6
  export async function configure(ctx) {
7
7
  const file = clientConfigPath(id, ctx.home, ctx.platform);
8
- const content = jsonConfig(name, ctx.apiKey, ctx.baseUrl, { "officialSetup": ["Settings > Model Service / Model Providers", "Provider type: OpenAI or OpenAI Compatible", "API host/base URL: https://api.scalemax.pro/v1", "Add ScaleMax model IDs or fetch from /v1/models"], "recommendedModel": "gpt-5.6-terra" });
8
+ const content = jsonConfig(name, ctx.apiKey, ctx.baseUrl, { "officialSetup": ["Settings > Model Service / Model Providers", "Provider type: OpenAI or OpenAI Compatible", "API host/base URL: https://api.scalemax.pro/v1", "Add ScaleMax model IDs or fetch from /v1/models"], "recommendedModel": "fable-5" });
9
9
  const result = await writeManagedConfig(ctx, id, name, file, content);
10
10
  return { ...result, message: 'setup guide written; enter these values in Cherry Studio model provider settings' };
11
11
  }
@@ -8,7 +8,7 @@ export async function configure(ctx) {
8
8
  env: {
9
9
  ANTHROPIC_AUTH_TOKEN: ctx.apiKey,
10
10
  ANTHROPIC_BASE_URL: ctx.baseUrl,
11
- ANTHROPIC_MODEL: 'gpt-5.6-terra'
11
+ ANTHROPIC_MODEL: 'fable-5'
12
12
  },
13
13
  scalemax: {
14
14
  managedBy: 'ScaleMax CLI',
@@ -8,9 +8,9 @@ import { commandExists } from '../utils/exec.js';
8
8
  const id = 'codex';
9
9
  const name = 'Codex';
10
10
  const provider = 'scalemax';
11
- const model = 'gpt-5.6-terra';
11
+ const model = 'fable-5';
12
12
  function codexConfig(baseUrl) {
13
- return `# ${GENERATED_HEADER}\nmodel = "${model}"\nmodel_provider = "${provider}"\ndisable_response_storage = true\n# Recommended ScaleMax Codex models: gpt-5.6-terra, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex.\n# Provider-internal gpt-5.3-codex-spark is routed behind the public gpt-5.3-codex Codex slug.\n\n[model_providers.${provider}]\nname = "ScaleMax"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
13
+ return `# ${GENERATED_HEADER}\nmodel = "${model}"\nmodel_provider = "${provider}"\ndisable_response_storage = true\n# Recommended ScaleMax Codex models: fable-5, fable-5-ultra, gpt-5.6-luna, gpt-5.6-terra, gpt-5.6-sol, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex.\n# Provider-internal gpt-5.3-codex-spark is routed behind the public gpt-5.3-codex Codex slug.\n\n[model_providers.${provider}]\nname = "ScaleMax"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
14
14
  }
15
15
  function codexAuth(apiKey) {
16
16
  return JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2) + '\n';
@@ -12,6 +12,8 @@ export async function configure(ctx) {
12
12
  name: 'ScaleMax',
13
13
  options: { baseURL: ctx.baseUrl, apiKey: ctx.apiKey },
14
14
  models: {
15
+ 'fable-5': { name: 'Fable 5' },
16
+ 'fable-5-ultra': { name: 'Fable 5 Ultra' },
15
17
  'gpt-5.6-luna': { name: 'GPT-5.6 Luna' },
16
18
  'gpt-5.6-terra': { name: 'GPT-5.6 Terra' },
17
19
  'gpt-5.6-sol': { name: 'GPT-5.6 Sol' },
@@ -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-luna', 'gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'codex-auto-review'];
14
+ const models = ['fable-5', 'fable-5-ultra', 'gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'codex-auto-review'];
15
15
  return JSON.stringify({
16
16
  models: models.map(model => ({
17
17
  title: `ScaleMax ${model}`,
@@ -5,7 +5,7 @@ const id = 'trae-solo';
5
5
  const name = 'TRAE SOLO';
6
6
  export async function configure(ctx) {
7
7
  const file = clientConfigPath(id, ctx.home, ctx.platform);
8
- const content = jsonConfig(name, ctx.apiKey, ctx.baseUrl, { "officialSetup": ["Settings > Models > Add Model", "API format: OpenAI Chat Completions", "Request URL: https://api.scalemax.pro/v1/chat/completions or base URL https://api.scalemax.pro/v1", "Model ID: gpt-5.6-terra or another ScaleMax model"], "recommendedModel": "gpt-5.6-terra" });
8
+ const content = jsonConfig(name, ctx.apiKey, ctx.baseUrl, { "officialSetup": ["Settings > Models > Add Model", "API format: OpenAI Chat Completions", "Request URL: https://api.scalemax.pro/v1/chat/completions or base URL https://api.scalemax.pro/v1", "Model ID: fable-5 or another ScaleMax model"], "recommendedModel": "fable-5" });
9
9
  const result = await writeManagedConfig(ctx, id, name, file, content);
10
10
  return { ...result, message: 'setup guide written; enter these values in TRAE SOLO custom model settings' };
11
11
  }
package/dist/index.js CHANGED
@@ -31,6 +31,15 @@ function welcome() {
31
31
  console.log(chalk.cyan('╚══════════════════════════════╝'));
32
32
  console.log('\nThe fastest way to connect every AI coding client.\n');
33
33
  }
34
+ async function runInteractiveInstaller() {
35
+ welcome();
36
+ await runSystemAudit();
37
+ const apiKey = await ensureInstallerApiKey(process.env.HOME || process.env.USERPROFILE || process.cwd());
38
+ if (!apiKey)
39
+ return;
40
+ await configureCommand(process.stdin.isTTY ? { promptLaunch: true } : { all: true, promptLaunch: false });
41
+ outro(chalk.green('ScaleMax is ready.'));
42
+ }
34
43
  async function main() {
35
44
  if (!isNodeSupported()) {
36
45
  console.error(chalk.red(`ScaleMax requires Node.js >=18.17. Current: ${process.versions.node}`));
@@ -41,16 +50,14 @@ async function main() {
41
50
  const [, , command] = process.argv;
42
51
  try {
43
52
  if (!command) {
44
- welcome();
45
- await runSystemAudit();
46
- const apiKey = await ensureInstallerApiKey(process.env.HOME || process.env.USERPROFILE || process.cwd());
47
- if (!apiKey)
48
- return;
49
- await configureCommand({ promptLaunch: true });
50
- outro(chalk.green('ScaleMax is ready.'));
53
+ await runInteractiveInstaller();
51
54
  return;
52
55
  }
53
56
  switch (command) {
57
+ case 'init':
58
+ case 'setup':
59
+ await runInteractiveInstaller();
60
+ break;
54
61
  case 'login':
55
62
  await loginCommand();
56
63
  break;
@@ -79,6 +86,8 @@ async function main() {
79
86
 
80
87
  Commands:
81
88
  scalemax Interactive installer
89
+ scalemax init Interactive installer (alias for no-arg scalemax)
90
+ scalemax setup Interactive installer (alias)
82
91
  scalemax login Store or replace API key
83
92
  scalemax logout Remove API key
84
93
  scalemax doctor Run diagnostics
@@ -1,4 +1,5 @@
1
1
  export type ClientId = 'codex' | 'cursor' | 'trae-solo' | 'claude' | 'opencode' | 'openclaw' | 'hermes' | 'cherry-studio' | 'api-code';
2
+ export type ProviderScope = 'openai' | 'anthropic' | 'both';
2
3
  export interface ClientDefinition {
3
4
  id: ClientId;
4
5
  name: string;
@@ -1,12 +1,20 @@
1
+ export type ProviderScope = 'openai' | 'anthropic' | 'both';
1
2
  export interface ModelInfo {
2
3
  id: string;
3
4
  object?: string;
4
5
  owned_by?: string;
5
6
  display_name?: string;
6
7
  }
8
+ export interface KeyMetadata {
9
+ provider_scope: ProviderScope;
10
+ mode?: string;
11
+ prefix?: string;
12
+ label?: string;
13
+ }
7
14
  export declare function validateApiKey(apiKey: string): Promise<{
8
15
  ok: boolean;
9
16
  models: ModelInfo[];
17
+ key?: KeyMetadata;
10
18
  error?: string;
11
19
  }>;
12
20
  export declare function testStreaming(apiKey: string, model?: string): Promise<{
package/dist/utils/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { SCALEMAX_BASE_URL } from '../constants.js';
2
+ import { sanitizeApiKey } from './apiKey.js';
2
3
  async function friendlyError(res) {
3
4
  let body = '';
4
5
  try {
@@ -22,7 +23,7 @@ async function friendlyError(res) {
22
23
  async function request(path, apiKey, init = {}) {
23
24
  const headers = new Headers(init.headers);
24
25
  if (apiKey)
25
- headers.set('Authorization', `Bearer ${apiKey}`);
26
+ headers.set('Authorization', `Bearer ${sanitizeApiKey(apiKey)}`);
26
27
  headers.set('Cache-Control', 'no-cache');
27
28
  if (!headers.has('Content-Type') && init.body)
28
29
  headers.set('Content-Type', 'application/json');
@@ -30,18 +31,23 @@ async function request(path, apiKey, init = {}) {
30
31
  }
31
32
  export async function validateApiKey(apiKey) {
32
33
  try {
33
- const res = await request('/models', apiKey);
34
+ const authCheck = await request('/usage', apiKey);
35
+ if (!authCheck.ok)
36
+ return { ok: false, models: [], error: await friendlyError(authCheck) };
37
+ const usage = await authCheck.json().catch(() => ({}));
38
+ const keyMeta = { provider_scope: usage?.key?.provider_scope || 'both', mode: usage?.key?.mode, prefix: usage?.key?.prefix, label: usage?.key?.label };
39
+ const res = await request('/models');
34
40
  if (!res.ok)
35
- return { ok: false, models: [], error: await friendlyError(res) };
41
+ return { ok: true, models: [], key: keyMeta, error: await friendlyError(res) };
36
42
  const data = await res.json();
37
- return { ok: Array.isArray(data.data), models: data.data || [] };
43
+ return { ok: true, models: Array.isArray(data.data) ? data.data : [], key: keyMeta };
38
44
  }
39
45
  catch (err) {
40
46
  return { ok: false, models: [], error: err instanceof Error ? err.message : String(err) };
41
47
  }
42
48
  }
43
49
  export async function testStreaming(apiKey, model) {
44
- const selected = model || 'gpt-5.6-terra';
50
+ const selected = model || 'fable-5';
45
51
  try {
46
52
  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' }] }) });
47
53
  if (!res.ok)
@@ -0,0 +1 @@
1
+ export declare function sanitizeApiKey(input: unknown): string;
@@ -0,0 +1,8 @@
1
+ const ZERO_WIDTH_RE = /[\u200B-\u200D\uFEFF]/g;
2
+ const ASCII_WHITESPACE_RE = /\s+/g;
3
+ export function sanitizeApiKey(input) {
4
+ return String(input ?? '')
5
+ .replace(ZERO_WIDTH_RE, '')
6
+ .replace(ASCII_WHITESPACE_RE, '')
7
+ .trim();
8
+ }
@@ -1,10 +1,11 @@
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 { SCALEMAX_BASE_URL } from '../constants.js';
4
+ import { KEY_ENV, SCALEMAX_BASE_URL } 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';
8
+ import { sanitizeApiKey } from './apiKey.js';
8
9
  export function maskApiKey(apiKey) {
9
10
  const trimmed = apiKey.trim();
10
11
  if (trimmed.length <= 4)
@@ -17,7 +18,7 @@ export async function promptForValidatedKey(home, message = 'Enter your ScaleMax
17
18
  cancel('Installer cancelled.');
18
19
  return undefined;
19
20
  }
20
- const candidate = String(key).trim();
21
+ const candidate = sanitizeApiKey(key);
21
22
  const spinner = ora('Validating API key').start();
22
23
  const result = await validateApiKey(candidate);
23
24
  if (!result.ok) {
@@ -43,11 +44,18 @@ export async function ensureInstallerApiKey(home = homeDir()) {
43
44
  intro(chalk.cyan('ScaleMax Installer'));
44
45
  const existing = await loadApiKey(home);
45
46
  if (!existing) {
47
+ if (!process.stdin.isTTY)
48
+ throw new Error(`No ScaleMax API key found. Set ${KEY_ENV} or run \`scalemax login\` in an interactive terminal.`);
46
49
  const key = await promptForValidatedKey(home, 'Enter your ScaleMax API key:');
47
50
  if (key)
48
51
  outro(chalk.green('ScaleMax configuration saved.'));
49
52
  return key;
50
53
  }
54
+ if (!process.stdin.isTTY) {
55
+ console.log(chalk.green('✓ Existing ScaleMax configuration detected.'));
56
+ console.log(chalk.green('✓ Non-interactive mode: keeping existing key.'));
57
+ return existing;
58
+ }
51
59
  console.log(chalk.green('✓ Existing ScaleMax configuration detected.'));
52
60
  console.log('\nAPI key:');
53
61
  console.log(maskApiKey(existing));
@@ -2,15 +2,16 @@ import fs from 'fs-extra';
2
2
  import { KEY_ENV } from '../constants.js';
3
3
  import { scaleMaxConfigPath, scaleMaxDir } from './paths.js';
4
4
  import { readJson, writeJsonAtomic } from './fs.js';
5
+ import { sanitizeApiKey } from './apiKey.js';
5
6
  export async function saveApiKey(apiKey, baseUrl, home) {
6
7
  await fs.ensureDir(scaleMaxDir(home), 0o700);
7
- await writeJsonAtomic(scaleMaxConfigPath(home), { apiKey, baseUrl, updatedAt: new Date().toISOString() }, 0o600);
8
+ await writeJsonAtomic(scaleMaxConfigPath(home), { apiKey: sanitizeApiKey(apiKey), baseUrl, updatedAt: new Date().toISOString() }, 0o600);
8
9
  }
9
10
  export async function loadApiKey(home) {
10
11
  if (process.env[KEY_ENV])
11
- return process.env[KEY_ENV];
12
+ return sanitizeApiKey(process.env[KEY_ENV]);
12
13
  const cfg = await readJson(scaleMaxConfigPath(home), {});
13
- return cfg.apiKey;
14
+ return cfg.apiKey ? sanitizeApiKey(cfg.apiKey) : undefined;
14
15
  }
15
16
  export async function removeApiKey(home) {
16
17
  await fs.remove(scaleMaxConfigPath(home));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scalemax",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Universal ScaleMax installer for AI coding clients",
5
5
  "type": "module",
6
6
  "bin": {