scalemax 0.1.31 → 0.1.33

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
@@ -136,6 +136,7 @@ The helper refuses unrelated requests and redacts ScaleMax-style secrets from pr
136
136
  - Codex
137
137
  - Cursor
138
138
  - Roo Code (VS Code)
139
+ - Cline (CLI and VS Code)
139
140
  - Continue (VS Code)
140
141
  - TRAE SOLO
141
142
  - Claude Code
@@ -161,7 +162,7 @@ ScaleMax merges a named `scalemax` provider into `~/.config/opencode/opencode.js
161
162
 
162
163
  ### OpenClaw
163
164
 
164
- ScaleMax partitions OpenClaw models into native `openai-responses` (`scalemax-codex`, `/v1`), OpenAI-compatible `openai-completions` (`scalemax-deepseek`, `/v1`), and `anthropic-messages` (`scalemax-claude`, origin) providers. `deepseek-v4-flash` is always isolated in the Chat Completions provider and never advertised through Responses. Its agent flags come from authenticated catalog discovery: the official route enables tools, reasoning effort, streaming usage, and mandatory `reasoning_content` replay while disabling unsupported thinking-mode `tool_choice`; a fallback route remains explicitly tool-free. The API key and compatibility User-Agent are OpenClaw file-backed SecretRefs stored in private `0600` files under `~/.openclaw/secrets/`; neither value is embedded in the main provider JSON. Migration removes the obsolete mixed provider, preserves the active model family, and scrubs plaintext credentials from generated ScaleMax agent registries even when they contain an older key. Output is capped at 4096 tokens, private-network routing is denied for the public gateway, and unrelated providers and agent settings are preserved.
165
+ ScaleMax partitions OpenClaw models into native `openai-responses` (`scalemax-codex`, `/v1`), OpenAI-compatible `openai-completions` (`scalemax-deepseek`, `/v1`), and `anthropic-messages` (`scalemax-claude`, origin) providers. `deepseek-v4-flash` is always isolated in the Chat Completions provider and never advertised through Responses. Its agent flags come only from authenticated catalog discovery: tools, forced `tool_choice`, reasoning, reasoning effort, streamed usage, and `reasoning_content` replay are each enabled only when the exact model advertises that capability. In particular, a tool-capable route is never forced to replay reasoning content unless `capabilities.reasoning_replay=true`; a fallback route remains explicitly tool-free. The API key and compatibility User-Agent are OpenClaw file-backed SecretRefs stored in private `0600` files under `~/.openclaw/secrets/`; neither value is embedded in the main provider JSON. Migration removes the obsolete mixed provider, preserves the active model family, and scrubs plaintext credentials from generated ScaleMax agent registries even when they contain an older key. Output is capped at 4096 tokens, private-network routing is denied for the public gateway, and unrelated providers and agent settings are preserved.
165
166
 
166
167
  ### Hermes
167
168
 
@@ -171,6 +172,10 @@ ScaleMax creates family-specific Hermes providers in `~/.hermes/config.yaml` and
171
172
 
172
173
  Roo Code stores provider profiles and secrets in VS Code extension storage, so ScaleMax does not mutate its private database. Instead, the CLI writes a private `0600` Roo settings-import file at `~/.scalemax/roo-code-import.json` and points the supported `roo-cline.autoImportSettingsPath` VS Code setting at it. Codex, Claude, and tool-capable DeepSeek profiles remain separate. DeepSeek is offered only when authenticated discovery reports `capabilities.tools=true`; otherwise Roo fails closed and points the user to Continue Chat/Edit. Reload VS Code once so Roo imports supported profiles into its SecretStorage. CLI verification intentionally reports the handoff as verified, not the extension runtime, until that reload occurs.
173
174
 
175
+ ### Cline (CLI and VS Code)
176
+
177
+ ScaleMax supports the verified Cline CLI `3.0.47` and VS Code extension `4.0.12` formats. The CLI surface uses Cline's built-in `openai-native` provider and Responses protocol. The extension uses its built-in OpenAI-compatible provider. Both receive the raw ScaleMax key and the matching credit or token base URL; ScaleMax never writes a custom `Authorization` or `x-api-key` header because Cline generates `Authorization: Bearer …` itself. Configuration is attempted only while Cline and VS Code are stopped, preserves unrelated state, uses private atomic files with backups and rollback, and fails closed on unknown versions or malformed/symlinked state.
178
+
174
179
  ### Continue (VS Code)
175
180
 
176
181
  Continue is the universal VS Code Chat/Edit route for `deepseek-v4-flash`. The CLI installs the official `Continue.continue` extension when VS Code is available, merges an OpenAI-compatible model into `~/.continue/config.yaml`, sets `useResponsesApi: false`, and keeps the authenticated ScaleMax key in private `~/.continue/.env` rather than the YAML. Existing Continue models and environment entries are preserved. Reload VS Code and select **ScaleMax DeepSeek V4 Flash**. For agent workflows, prefer Roo/OpenClaw/Hermes when the authenticated catalog reports `capabilities.tools=true`.
package/dist/clients.js CHANGED
@@ -4,6 +4,8 @@ import * as cursorDetect from './detect/cursor.js';
4
4
  import * as cursorConfig from './configure/cursor.js';
5
5
  import * as rooCodeDetect from './detect/roo-code.js';
6
6
  import * as rooCodeConfig from './configure/roo-code.js';
7
+ import * as clineDetect from './detect/cline.js';
8
+ import * as clineConfig from './configure/cline.js';
7
9
  import * as continueDetect from './detect/continue.js';
8
10
  import * as continueConfig from './configure/continue.js';
9
11
  import * as traeSoloDetect from './detect/trae-solo.js';
@@ -24,6 +26,7 @@ export const clients = [
24
26
  { id: 'codex', name: 'Codex', detect: codexDetect.detect, install: codexConfig.install, configure: codexConfig.configure, verify: codexConfig.verify, launch: codexConfig.launch, remove: codexConfig.remove },
25
27
  { id: 'cursor', name: 'Cursor', detect: cursorDetect.detect, configure: cursorConfig.configure, verify: cursorConfig.verify, remove: cursorConfig.remove },
26
28
  { id: 'roo-code', name: 'Roo Code (VS Code)', detect: rooCodeDetect.detect, configure: rooCodeConfig.configure, verify: rooCodeConfig.verify, remove: rooCodeConfig.remove },
29
+ { id: 'cline', name: 'Cline', detect: clineDetect.detect, install: clineConfig.install, configure: clineConfig.configure, verify: clineConfig.verify, remove: clineConfig.remove },
27
30
  { id: 'continue', name: 'Continue (VS Code)', detect: continueDetect.detect, install: continueConfig.install, configure: continueConfig.configure, verify: continueConfig.verify, remove: continueConfig.remove },
28
31
  { id: 'trae-solo', name: 'TRAE SOLO', detect: traeSoloDetect.detect, configure: traeSoloConfig.configure, remove: traeSoloConfig.remove },
29
32
  { id: 'claude', name: 'Claude Code', detect: claudeDetect.detect, configure: claudeConfig.configure, verify: claudeConfig.verify, remove: claudeConfig.remove },
@@ -4,9 +4,9 @@ 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, isAnthropicModel } from '../utils/api.js';
7
+ import { validateApiKey, isAnthropicModel, isModelRouteAvailable } from '../utils/api.js';
8
8
  import { homeDir } from '../utils/paths.js';
9
- import { isDeepSeekChatModelId } from '../configure/models.js';
9
+ import { isExactChatCompletionsModelId } from '../configure/models.js';
10
10
  export async function detectAll(home = homeDir()) {
11
11
  const ctx = { home, platform: process.platform, env: process.env };
12
12
  return Promise.all(clients.map(c => c.detect(ctx)));
@@ -41,23 +41,27 @@ async function maybeLaunchCodex(chosen, successful, baseUrl = SCALEMAX_BASE_URL)
41
41
  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');
42
42
  }
43
43
  export function clientAllowedForModels(clientId, models) {
44
+ models = models.filter(isModelRouteAvailable);
44
45
  if (!models.length)
45
46
  return false;
46
47
  // These clients use either a native family-specific adapter or the gateway's
47
48
  // normalized OpenAI-compatible Chat Completions route, which supports both
48
49
  // authenticated model families. Codex remains Responses-only; TRAE uses
49
50
  // OpenAI-compatible Chat Completions.
50
- const crossProtocolClients = new Set(['cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
51
+ const crossProtocolClients = new Set(['cursor', 'roo-code', 'cline', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
51
52
  if (clientId === 'continue')
52
- return models.some(model => isDeepSeekChatModelId(model.id));
53
+ return models.some(model => isExactChatCompletionsModelId(model.id));
53
54
  if (clientId === 'claude')
54
55
  return models.some(isAnthropicModel);
55
56
  if (clientId === 'codex')
56
- return models.some(model => !isAnthropicModel(model) && !isDeepSeekChatModelId(model.id));
57
- if (clientId === 'hermes' && models.every(model => isDeepSeekChatModelId(model.id)))
58
- return false;
59
- if (clientId === 'roo-code' && models.every(model => isDeepSeekChatModelId(model.id)))
60
- return false;
57
+ return models.some(model => !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id));
58
+ if (clientId === 'hermes' && models.every(model => isExactChatCompletionsModelId(model.id)))
59
+ return models.some(model => model.capabilities?.tools === true);
60
+ // Roo is an agent, not a text-only chat surface. Its task loop depends on
61
+ // native function calls for every model family, so offer it only when the
62
+ // authenticated catalog confirms at least one viable route.
63
+ if (clientId === 'roo-code' || clientId === 'cline')
64
+ return models.some(model => model.capabilities?.tools === true);
61
65
  if (crossProtocolClients.has(clientId))
62
66
  return true;
63
67
  return models.some(model => !isAnthropicModel(model));
@@ -65,6 +69,8 @@ export function clientAllowedForModels(clientId, models) {
65
69
  function scopeHint(scope) {
66
70
  if (scope === 'deepseek-v4-flash')
67
71
  return 'DeepSeek V4 Flash only';
72
+ if (scope === 'glm-5.2')
73
+ return 'GLM 5.2 only';
68
74
  return scope === 'both' ? 'OpenAI + Claude' : scope === 'anthropic' ? 'Claude only' : 'OpenAI only';
69
75
  }
70
76
  export async function configureCommand(opts = {}) {
@@ -4,13 +4,14 @@ import { createInterface } from 'node:readline/promises';
4
4
  import { execa } from 'execa';
5
5
  import { clients } from '../clients.js';
6
6
  import { helperDocsContext } from './helper-docs.js';
7
- import { createChatCompletion, isAnthropicModel } from '../utils/api.js';
7
+ import { createChatCompletion, filterModelsForScope, isAnthropicModel } from '../utils/api.js';
8
8
  import { redactApiKey } from '../utils/apiKey.js';
9
9
  import { loadApiKey } from '../utils/keyStore.js';
10
10
  import { homeDir } from '../utils/paths.js';
11
11
  import { validateApiKey } from '../utils/api.js';
12
12
  import { commandExists } from '../utils/exec.js';
13
13
  import { installClaudeGithubTools } from '../configure/claude-tools.js';
14
+ import { isExactChatCompletionsModelId } from '../configure/models.js';
14
15
  export const SCALEMAX_HELPER_REFUSAL = `I am the official ScaleMax Helper.
15
16
 
16
17
  I can only assist with installing, configuring, repairing, verifying, and troubleshooting ScaleMax.
@@ -762,6 +763,7 @@ For automatic configuration, exit with \`/quit\`, run \`scalemax update\`, then
762
763
  const HELPER_CLIENT_PATTERNS = [
763
764
  { id: 'continue', pattern: /\b(?:continue(?:\.dev)?|vscode\s+continue|vs\s+code\s+continue)\b/i },
764
765
  { id: 'roo-code', pattern: /\b(?:roo(?:\s+code)?|roo\s+(?:extension|plugin)|vscode\s+roo|vs\s+code\s+roo)\b/i },
766
+ { id: 'cline', pattern: /\bcline(?:\s+(?:extension|plugin|cli))?\b/i },
765
767
  { id: 'openclaw', pattern: /\bopen\s*claw\b/i },
766
768
  { id: 'opencode', pattern: /\bopen\s*code\b/i },
767
769
  { id: 'cherry-studio', pattern: /\bcherry\s*studio\b/i },
@@ -800,6 +802,8 @@ function helperActionTarget(question) {
800
802
  return undefined;
801
803
  }
802
804
  function requestedProviderScope(question, target) {
805
+ if (/\bglm[-\s]*5[._\s]*2\b/i.test(question))
806
+ return 'glm-5.2';
803
807
  if (/\bdeepseek(?:[-\s]+v4[-\s]+flash)?\b/i.test(question))
804
808
  return 'deepseek-v4-flash';
805
809
  if (/\b(?:both|openai\s*\+\s*claude|claude\s*\+\s*openai)\b/i.test(question))
@@ -811,35 +815,48 @@ function requestedProviderScope(question, target) {
811
815
  return undefined;
812
816
  }
813
817
  function actionModels(connection, requestedScope) {
814
- const catalog = connection.modelCatalog?.length
818
+ const rawCatalog = connection.modelCatalog?.length
815
819
  ? connection.modelCatalog
816
820
  : (connection.availableModels || []).map(id => ({ id }));
821
+ const entitlement = connection.providerScope || 'both';
822
+ // Always begin with the key's verified entitlement. Natural-language intent
823
+ // must never widen an exact-model key into an OpenAI/Claude family key.
824
+ const catalog = filterModelsForScope(rawCatalog, entitlement);
817
825
  if (!requestedScope)
818
826
  return catalog;
819
- if (requestedScope === 'deepseek-v4-flash') {
820
- return catalog.filter(model => model.id.trim().toLowerCase() === 'deepseek-v4-flash');
827
+ const exactEntitlement = entitlement === 'deepseek-v4-flash' || entitlement === 'glm-5.2';
828
+ if (exactEntitlement && requestedScope !== entitlement)
829
+ return [];
830
+ if (requestedScope === 'deepseek-v4-flash' || requestedScope === 'glm-5.2') {
831
+ return requestedScope === entitlement
832
+ ? catalog.filter(model => model.id.trim().toLowerCase() === requestedScope)
833
+ : [];
821
834
  }
822
835
  if (requestedScope === 'both') {
823
- return catalog.filter(model => model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
836
+ return catalog.filter(model => !isExactChatCompletionsModelId(model.id));
824
837
  }
825
838
  return catalog.filter(model => requestedScope === 'anthropic'
826
839
  ? isAnthropicModel(model)
827
- : !isAnthropicModel(model) && model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
840
+ : !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id));
828
841
  }
829
842
  function helperClientAllowedForModels(clientId, models) {
830
843
  if (!models.length)
831
844
  return false;
832
- const crossProtocolClients = new Set(['cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
833
- const onlyDeepSeek = models.every(model => model.id.trim().toLowerCase() === 'deepseek-v4-flash');
845
+ const crossProtocolClients = new Set(['cursor', 'roo-code', 'cline', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
846
+ const onlyExactChat = models.every(model => isExactChatCompletionsModelId(model.id));
834
847
  if (clientId === 'continue')
835
- return onlyDeepSeek;
848
+ return onlyExactChat;
836
849
  if (clientId === 'claude')
837
850
  return models.some(isAnthropicModel);
838
- if ((clientId === 'codex' || clientId === 'hermes' || clientId === 'roo-code') && onlyDeepSeek)
851
+ if (clientId === 'codex' && onlyExactChat)
839
852
  return false;
853
+ if (clientId === 'hermes' && onlyExactChat)
854
+ return models.some(model => model.capabilities?.tools === true);
855
+ if (clientId === 'roo-code' || clientId === 'cline')
856
+ return models.some(model => model.capabilities?.tools === true);
840
857
  if (crossProtocolClients.has(clientId))
841
858
  return true;
842
- return models.some(model => !isAnthropicModel(model) && model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
859
+ return models.some(model => !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id));
843
860
  }
844
861
  function hasRooCodeExtension(paths) {
845
862
  return paths.some(candidate => /^rooveterinaryinc\.roo-cline-/i.test(path.basename(candidate)));
@@ -923,8 +940,8 @@ async function installRooCodeExtension(ctx, client, write) {
923
940
  async function runOneHelperClientAction(kind, client, connection, home, requestedScope, write) {
924
941
  const models = actionModels(connection, requestedScope);
925
942
  if (!models.length)
926
- throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : requestedScope === 'deepseek-v4-flash' ? 'DeepSeek V4 Flash' : 'compatible'} models for ${client.name}. No configuration was changed.`);
927
- if (client.id === 'codex' && models.every(model => isAnthropicModel(model) || model.id.trim().toLowerCase() === 'deepseek-v4-flash'))
943
+ throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : requestedScope === 'deepseek-v4-flash' ? 'DeepSeek V4 Flash' : requestedScope === 'glm-5.2' ? 'GLM 5.2' : 'compatible'} models for ${client.name}. No configuration was changed.`);
944
+ if (client.id === 'codex' && models.every(model => isAnthropicModel(model) || isExactChatCompletionsModelId(model.id)))
928
945
  throw new Error('Codex requires a Responses-capable model, but this key exposes only Claude or Chat-Completions-only models. No configuration was changed.');
929
946
  if (client.id === 'cursor' && connection.platform !== 'darwin')
930
947
  throw new Error(`Automatic Cursor configuration is currently verified on macOS only. ${helperPlatformName(connection.platform)} was not modified.`);
@@ -934,7 +951,7 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
934
951
  env: { ...process.env },
935
952
  apiKey: connection.apiKey,
936
953
  baseUrl: connection.baseUrl,
937
- providerScope: requestedScope || connection.providerScope || 'both',
954
+ providerScope: connection.providerScope || 'both',
938
955
  models,
939
956
  interactive: false,
940
957
  allowAppModification: false
@@ -998,7 +1015,9 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
998
1015
  }
999
1016
  const activation = client.id === 'roo-code'
1000
1017
  ? ' Reload VS Code once so Roo Code imports the verified ScaleMax profile.'
1001
- : client.id === 'continue' ? ' Reload VS Code once so Continue loads the verified ScaleMax profile.' : '';
1018
+ : client.id === 'cline'
1019
+ ? ' Restart Cline; reload VS Code too when using the extension.'
1020
+ : client.id === 'continue' ? ' Reload VS Code once so Continue loads the verified ScaleMax profile.' : '';
1002
1021
  return `✅ Successfully completed: ${client.name} is configured and locally verified.${activation}`;
1003
1022
  }
1004
1023
  /** Execute only narrowly allowlisted ScaleMax client actions; never model-generated shell commands. */
@@ -0,0 +1,29 @@
1
+ import { ConfigureContext, ConfigureResult, DetectContext, InstallContext } from '../types/client.js';
2
+ export interface ClineExtensionSurface {
3
+ path: string;
4
+ version?: string;
5
+ supported: boolean;
6
+ }
7
+ export interface ClineSurfaces {
8
+ cliInstalled: boolean;
9
+ cliVersion?: string;
10
+ cliSupported: boolean;
11
+ extensions: ClineExtensionSurface[];
12
+ extensionInstalled: boolean;
13
+ extensionSupported: boolean;
14
+ }
15
+ export declare function clineExtensionStatePaths(ctx: Pick<DetectContext, 'home' | 'env'>): {
16
+ globalStateFile: string;
17
+ secretsFile: string;
18
+ };
19
+ export declare function inspectClineSurfaces(ctx: DetectContext): Promise<ClineSurfaces>;
20
+ export declare function clineProcessConflicts(output: string, platform: NodeJS.Platform): {
21
+ cli: boolean;
22
+ extension: boolean;
23
+ };
24
+ export declare function install(ctx: InstallContext): Promise<ConfigureResult>;
25
+ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
26
+ export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
27
+ export declare function isManagedCliStore(value: unknown): boolean;
28
+ export declare function isManagedExtensionState(globalState: unknown, secrets: unknown): boolean;
29
+ export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;