scalemax 0.1.48 → 0.1.50
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 +6 -3
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +60 -38
- package/dist/commands/doctor.d.ts +7 -0
- package/dist/commands/doctor.js +46 -3
- package/dist/commands/fix-roo.d.ts +1 -0
- package/dist/commands/fix-roo.js +64 -0
- package/dist/commands/helper-bot.js +38 -19
- package/dist/commands/helper-docs.js +6 -0
- package/dist/configure/api-code.js +2 -2
- package/dist/configure/claude.js +76 -45
- package/dist/configure/cline.js +3 -1
- package/dist/configure/codex.js +21 -6
- package/dist/configure/continue.js +2 -2
- package/dist/configure/cursor.js +5 -2
- package/dist/configure/hermes.js +3 -3
- package/dist/configure/models.d.ts +83 -21
- package/dist/configure/models.js +313 -55
- package/dist/configure/openclaw.js +6 -6
- package/dist/configure/opencode.js +6 -6
- package/dist/configure/roo-code.d.ts +1 -0
- package/dist/configure/roo-code.js +24 -6
- package/dist/detect/roo-code.d.ts +27 -0
- package/dist/detect/roo-code.js +35 -9
- package/dist/index.js +11 -1
- package/dist/repair/roo-ripgrep.d.ts +80 -0
- package/dist/repair/roo-ripgrep.js +170 -0
- package/dist/types/client.d.ts +23 -0
- package/dist/utils/api.d.ts +53 -27
- package/dist/utils/api.js +129 -92
- package/dist/utils/fs.d.ts +1 -1
- package/dist/utils/fs.js +2 -1
- package/dist/utils/modelCheck.d.ts +8 -0
- package/dist/utils/modelCheck.js +32 -0
- package/dist/utils/modelId.d.ts +8 -0
- package/dist/utils/modelId.js +35 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ scalemax doctor Run diagnostics
|
|
|
78
78
|
scalemax models List available ScaleMax models
|
|
79
79
|
scalemax helper-bot Open the ScaleMax-only support assistant
|
|
80
80
|
scalemax claude-tools Install bounded GitHub skill tools for Claude
|
|
81
|
+
scalemax fix-roo Fix Roo Code stuck at "API Request…" on recent VS Code
|
|
81
82
|
scalemax update Reconfigure detected clients with the stored key
|
|
82
83
|
scalemax remove Remove ScaleMax-generated client configuration
|
|
83
84
|
scalemax --help Show help
|
|
@@ -150,11 +151,11 @@ For clients without a stable official config file, the installer writes a ScaleM
|
|
|
150
151
|
|
|
151
152
|
### Codex
|
|
152
153
|
|
|
153
|
-
ScaleMax configures the official Codex custom-provider interface in `~/.codex/config.toml` with `wire_api = "responses"`, and stores the selected key in Codex's private `~/.codex/auth.json`. Detection is semantic: unrelated or ChatGPT-authenticated Codex files are not reported as ScaleMax-configured. Only authenticated
|
|
154
|
+
ScaleMax configures the official Codex custom-provider interface in `~/.codex/config.toml` with `wire_api = "responses"`, and stores the selected key in Codex's private `~/.codex/auth.json`. Detection is semantic: unrelated or ChatGPT-authenticated Codex files are not reported as ScaleMax-configured. Only GPT models from the authenticated catalog are eligible (`gpt-5.6-terra` first, or `auto` on an Auto key); a key with no GPT model is told so and Codex is left unchanged. The generated files are private, and verification performs a minimal `/v1/responses` request before reporting success.
|
|
154
155
|
|
|
155
156
|
### Claude Code
|
|
156
157
|
|
|
157
|
-
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. Because Claude Code's gateway discovery only accepts IDs beginning with `claude` or `anthropic`, catalogs that include Fable or Auto also receive a managed custom picker entry (`ANTHROPIC_CUSTOM_MODEL_OPTION=fable-5` or `auto`, with friendly names and descriptions), making `/model fable-5` and `/model auto` selectable; any prior custom picker values are restored when ScaleMax configuration is removed. The Anthropic base URL intentionally has no `/v1` suffix because Claude Code adds `/v1/messages` itself. The CLI preserves unrelated settings
|
|
158
|
+
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. Because Claude Code's gateway discovery only accepts IDs beginning with `claude` or `anthropic`, catalogs that include Fable or Auto also receive a managed custom picker entry (`ANTHROPIC_CUSTOM_MODEL_OPTION=fable-5` or `auto`, with friendly names and descriptions), making `/model fable-5` and `/model auto` selectable; any prior custom picker values are restored when ScaleMax configuration is removed. The Anthropic base URL intentionally has no `/v1` suffix because Claude Code adds `/v1/messages` itself. The CLI preserves unrelated settings and selects the main model from the key's own Claude models (Opus 5.5, then Opus 5, Sonnet 5.5, Sonnet 5, older Claude; never the `claude-sonnet-4-6` Claude Desktop alias or another provider's model). The background model (`ANTHROPIC_DEFAULT_HAIKU_MODEL`) is Haiku when the key lists it, otherwise the cheapest Claude it lists, so Claude Code never falls back to a Haiku id the key cannot use. The picker entry prefers an image-capable model; Fable 5.1 is labelled text only. The CLI does not write `CLAUDE_CODE_DISABLE_THINKING`: the ScaleMax gateway strips Claude Code's `thinking` control itself and locks native Claude requests to provider-side `low` effort. Fable uses its verified translated controls and supports `low`, `medium`, `high`, and `max`, with `low` as the default. Haiku 4.5 omits the unsupported effort field. 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-5[1m]` are normalized by the gateway.
|
|
158
159
|
|
|
159
160
|
### OpenCode
|
|
160
161
|
|
|
@@ -170,7 +171,9 @@ ScaleMax creates family-specific Hermes providers in `~/.hermes/config.yaml` and
|
|
|
170
171
|
|
|
171
172
|
### Roo Code (VS Code)
|
|
172
173
|
|
|
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.
|
|
174
|
+
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; the Codex profile is a GPT model on OpenAI keys and a strong Local model on Local Models keys, never the `claude-sonnet-4-6` alias, and every profile asks for at most 32,768 output tokens. 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.
|
|
175
|
+
|
|
176
|
+
Roo Code 3.54 (its latest release) looks for ripgrep only in folders newer VS Code releases no longer use, so on recent VS Code versions every task hangs at "API Request…" with "Could not find ripgrep binary". Configuring Roo Code also patches the installed extension's `dist/extension.js` (in the editor's per-user extensions folder, never the VS Code app) so it tries `node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/<platform>-<arch>/` first and then its original folders. The untouched file is kept once as `extension.js.scalemax-orig`. Run `npx scalemax@latest fix-roo` to apply only this fix (VS Code, Insiders, Cursor, Windsurf, VSCodium, Trae, Kiro, and remote servers), `scalemax fix-roo --undo` to restore the original, and reload VS Code afterwards. `scalemax doctor` flags installs that still need it.
|
|
174
177
|
|
|
175
178
|
### Cline (CLI and VS Code)
|
|
176
179
|
|
|
@@ -3,6 +3,8 @@ import { ClientId, ConfigureResult } from '../types/client.js';
|
|
|
3
3
|
export declare function detectAll(home?: string): Promise<import("../types/client.js").DetectionResult[]>;
|
|
4
4
|
export declare function printDetected(home?: string): Promise<import("../types/client.js").DetectionResult[]>;
|
|
5
5
|
export declare function clientAllowedForModels(clientId: ClientId, models: ModelInfo[]): boolean;
|
|
6
|
+
/** Why a detected client is not offered for this key, when the reason is worth saying. */
|
|
7
|
+
export declare function clientUnavailableReason(clientId: ClientId, models: ModelInfo[]): string | undefined;
|
|
6
8
|
export declare function rollbackAfterVerificationFailure(result: ConfigureResult, error: unknown): Promise<never>;
|
|
7
9
|
export declare function configureCommand(opts?: {
|
|
8
10
|
all?: boolean;
|
|
@@ -4,9 +4,10 @@ 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, isModelRouteAvailable, AUTO_MODEL_ID, probeWorkingModels
|
|
7
|
+
import { validateApiKey, isAnthropicModel, isChatModel, isModelRouteAvailable, AUTO_MODEL_ID, probeWorkingModels } from '../utils/api.js';
|
|
8
|
+
import { saveModelCheck } from '../utils/modelCheck.js';
|
|
8
9
|
import { homeDir } from '../utils/paths.js';
|
|
9
|
-
import { isExactChatCompletionsModelId } from '../configure/models.js';
|
|
10
|
+
import { CLAUDE_CODE_NEEDS_CLAUDE_MESSAGE, CODEX_NEEDS_GPT_MESSAGE, codexModelIds, defaultModelIdsFor, isExactChatCompletionsModelId, modelIdKey } from '../configure/models.js';
|
|
10
11
|
import { isContinueEligibleModelId } from '../configure/continue.js';
|
|
11
12
|
export async function detectAll(home = homeDir()) {
|
|
12
13
|
const ctx = { home, platform: process.platform, env: process.env };
|
|
@@ -57,10 +58,12 @@ async function maybeLaunchCodex(chosen, successful, baseUrl = SCALEMAX_BASE_URL)
|
|
|
57
58
|
* for "did Anthropic make this."
|
|
58
59
|
*/
|
|
59
60
|
function reachableByClaudeCode(model) {
|
|
60
|
-
return isAnthropicModel(model) || model.id === AUTO_MODEL_ID;
|
|
61
|
+
return isAnthropicModel(model) || modelIdKey(model.id) === AUTO_MODEL_ID;
|
|
61
62
|
}
|
|
62
63
|
export function clientAllowedForModels(clientId, models) {
|
|
63
|
-
|
|
64
|
+
// Image, video, embedding and voice rows share the catalog; an image model
|
|
65
|
+
// owned by `scalemax` must not make Codex look usable on a Claude-only key.
|
|
66
|
+
models = models.filter(model => isModelRouteAvailable(model) && isChatModel(model));
|
|
64
67
|
if (!models.length)
|
|
65
68
|
return false;
|
|
66
69
|
// These clients use either a native family-specific adapter or the gateway's
|
|
@@ -72,8 +75,11 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
72
75
|
return models.some(model => isContinueEligibleModelId(model.id));
|
|
73
76
|
if (clientId === 'claude')
|
|
74
77
|
return models.some(reachableByClaudeCode);
|
|
78
|
+
// Codex runs on GPT models only (and on Auto for an Auto key). A Claude id,
|
|
79
|
+
// the `claude-sonnet-4-6` alias, DeepSeek or a free preview model failed
|
|
80
|
+
// every Codex request it was configured with.
|
|
75
81
|
if (clientId === 'codex')
|
|
76
|
-
return models.some(model =>
|
|
82
|
+
return codexModelIds({ models }).length > 0 || models.some(model => modelIdKey(model.id) === AUTO_MODEL_ID);
|
|
77
83
|
if (clientId === 'hermes' && models.every(model => isExactChatCompletionsModelId(model.id)))
|
|
78
84
|
return models.some(model => model.capabilities?.tools === true);
|
|
79
85
|
// Roo is an agent, not a text-only chat surface. Its task loop depends on
|
|
@@ -85,6 +91,16 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
85
91
|
return true;
|
|
86
92
|
return models.some(model => !isAnthropicModel(model));
|
|
87
93
|
}
|
|
94
|
+
/** Why a detected client is not offered for this key, when the reason is worth saying. */
|
|
95
|
+
export function clientUnavailableReason(clientId, models) {
|
|
96
|
+
if (clientAllowedForModels(clientId, models))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (clientId === 'codex')
|
|
99
|
+
return CODEX_NEEDS_GPT_MESSAGE.replace('was not set up', 'is not offered');
|
|
100
|
+
if (clientId === 'claude')
|
|
101
|
+
return CLAUDE_CODE_NEEDS_CLAUDE_MESSAGE.replace('was not set up', 'is not offered');
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
88
104
|
export async function rollbackAfterVerificationFailure(result, error) {
|
|
89
105
|
if (!result.changed)
|
|
90
106
|
throw error;
|
|
@@ -131,27 +147,17 @@ export async function configureCommand(opts = {}) {
|
|
|
131
147
|
throw new Error(keyInfo.error || 'ScaleMax returned no models for this API key. No client configuration was changed.');
|
|
132
148
|
const providerScope = keyInfo.key?.provider_scope || 'both';
|
|
133
149
|
const selectedBaseUrl = keyInfo.baseUrl || SCALEMAX_BASE_URL;
|
|
134
|
-
// LIVE-PROBE the key's models with a small request and keep only the ones
|
|
135
|
-
// that actually answer, so a client is configured with working models and
|
|
136
|
-
// defaults to a working one. Bounded (concurrency + total time); if nothing
|
|
137
|
-
// probes clean we KEEP the entitlement-filtered list rather than fail — a
|
|
138
|
-
// slow/degraded upstream must not leave the user with zero models.
|
|
139
|
-
try {
|
|
140
|
-
const probeSpinner = ora('Checking which models respond').start();
|
|
141
|
-
const probe = await probeWorkingModels({ apiKey, baseUrl: selectedBaseUrl, models: keyInfo.models, providerScope });
|
|
142
|
-
if (probe.models.length && !probe.fallback) {
|
|
143
|
-
keyInfo.models = probe.models;
|
|
144
|
-
probeSpinner.succeed(`${probe.models.length}/${probe.probedCount} models responded`);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
probeSpinner.warn(probe.warning || 'No model answered the probe in time; using the full authenticated catalog');
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
catch { /* probing is best-effort; never blocks setup */ }
|
|
151
150
|
const availableClients = clients.filter(c => clientAllowedForModels(c.id, keyInfo.models));
|
|
152
151
|
console.log(chalk.dim(`API key scope: ${scopeHint(providerScope)}. Clients are filtered against the authenticated model catalog.`));
|
|
153
152
|
const detections = await printDetected(home);
|
|
154
153
|
const detectedIds = new Set(detections.filter(d => d.installed).map(d => d.id));
|
|
154
|
+
for (const client of clients) {
|
|
155
|
+
if (!detectedIds.has(client.id))
|
|
156
|
+
continue;
|
|
157
|
+
const reason = clientUnavailableReason(client.id, keyInfo.models);
|
|
158
|
+
if (reason)
|
|
159
|
+
console.log(chalk.yellow(`${client.name}: ${reason}`));
|
|
160
|
+
}
|
|
155
161
|
let chosen;
|
|
156
162
|
if (opts.all)
|
|
157
163
|
chosen = availableClients.filter(c => detectedIds.has(c.id)).map(c => c.id);
|
|
@@ -170,6 +176,35 @@ export async function configureCommand(opts = {}) {
|
|
|
170
176
|
console.log(chalk.yellow('No detected clients to configure.'));
|
|
171
177
|
return;
|
|
172
178
|
}
|
|
179
|
+
// Check, with one small request each, the models setup is about to write
|
|
180
|
+
// as defaults for the chosen and detected clients. A default that fails is
|
|
181
|
+
// skipped (the next pick for that slot is checked instead); every entitled
|
|
182
|
+
// model stays in the lists written to clients. Bounded in count and time,
|
|
183
|
+
// and if nothing answers nothing is skipped: a slow upstream or an empty
|
|
184
|
+
// balance must not leave a client with no model.
|
|
185
|
+
let avoidModelIds = [];
|
|
186
|
+
const probeClients = [...new Set([...chosen, ...availableClients.filter(c => detectedIds.has(c.id)).map(c => c.id)])];
|
|
187
|
+
try {
|
|
188
|
+
const probeSpinner = ora('Checking the default models (one small request each)').start();
|
|
189
|
+
const probe = await probeWorkingModels({
|
|
190
|
+
apiKey,
|
|
191
|
+
baseUrl: selectedBaseUrl,
|
|
192
|
+
models: keyInfo.models,
|
|
193
|
+
providerScope,
|
|
194
|
+
targets: models => defaultModelIdsFor({ models, providerScope }, probeClients),
|
|
195
|
+
});
|
|
196
|
+
if (probe.fallback)
|
|
197
|
+
probeSpinner.warn(probe.warning || 'No default model answered in time; nothing was skipped.');
|
|
198
|
+
else if (probe.failedIds.length) {
|
|
199
|
+
avoidModelIds = probe.failedIds;
|
|
200
|
+
probeSpinner.warn(`${probe.workingCount}/${probe.probedCount} default models answered; skipped ${probe.failedIds.join(', ')} for now`);
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
probeSpinner.succeed(`${probe.workingCount}/${probe.probedCount} default models answered`);
|
|
204
|
+
// doctor reads this so it expects the same defaults setup wrote.
|
|
205
|
+
await saveModelCheck(home, apiKey, avoidModelIds).catch(() => undefined);
|
|
206
|
+
}
|
|
207
|
+
catch { /* checking is best-effort; never blocks setup */ }
|
|
173
208
|
const successful = new Set();
|
|
174
209
|
const failures = [];
|
|
175
210
|
for (const client of availableClients.filter(c => chosen.includes(c.id))) {
|
|
@@ -206,9 +241,11 @@ export async function configureCommand(opts = {}) {
|
|
|
206
241
|
}
|
|
207
242
|
const spinner = ora(`Configuring ${client.name}`).start();
|
|
208
243
|
try {
|
|
209
|
-
const ctx = { ...baseCtx, apiKey, baseUrl: selectedBaseUrl, providerScope, models: keyInfo.models, interactive: Boolean(process.stdin.isTTY), allowAppModification };
|
|
244
|
+
const ctx = { ...baseCtx, apiKey, baseUrl: selectedBaseUrl, providerScope, models: keyInfo.models, avoidModelIds, interactive: Boolean(process.stdin.isTTY), allowAppModification };
|
|
210
245
|
const res = await client.configure(ctx);
|
|
211
246
|
spinner.succeed(`${client.name} ${res.message}`);
|
|
247
|
+
if (res.note)
|
|
248
|
+
console.log(chalk.dim(` ${res.note}`));
|
|
212
249
|
if (client.verify) {
|
|
213
250
|
const verify = ora(`Verifying ${client.name}`).start();
|
|
214
251
|
try {
|
|
@@ -231,21 +268,6 @@ export async function configureCommand(opts = {}) {
|
|
|
231
268
|
failures.push(`${client.name}: ${message}`);
|
|
232
269
|
}
|
|
233
270
|
}
|
|
234
|
-
// Pin the working default as this key's `claude-sonnet-4-6` backend so Claude
|
|
235
|
-
// Desktop (which always sends that id) routes to a model this key can serve.
|
|
236
|
-
// Best-effort: an older gateway without the endpoint, or a scope that already
|
|
237
|
-
// has a fixed default, is skipped silently. Never fails the run.
|
|
238
|
-
if (successful.size > 0) {
|
|
239
|
-
const target = claudeAliasBackendPinTarget(keyInfo.models);
|
|
240
|
-
if (target) {
|
|
241
|
-
try {
|
|
242
|
-
const pin = await pinClaudeAliasBackend({ apiKey, baseUrl: selectedBaseUrl, model: target });
|
|
243
|
-
if (pin.ok && !pin.skipped)
|
|
244
|
-
console.log(chalk.dim(`Claude Desktop's claude-sonnet-4-6 now routes to ${target} on this key.`));
|
|
245
|
-
}
|
|
246
|
-
catch { /* endpoint absent or transient; ignore */ }
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
271
|
if (opts.promptLaunch !== false && !opts.all && process.stdin.isTTY)
|
|
250
272
|
await maybeLaunchCodex(chosen, successful, selectedBaseUrl);
|
|
251
273
|
if (failures.length)
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roo Code 3.54 cannot find newer VS Code's relocated ripgrep, so every task
|
|
3
|
+
* hangs at "API Request…". Read-only: reports installs that still need
|
|
4
|
+
* `scalemax fix-roo`. A warning, not a failure, because older VS Code builds
|
|
5
|
+
* still work unpatched.
|
|
6
|
+
*/
|
|
7
|
+
export declare function rooRipgrepDoctorLines(home: string): Promise<string[]>;
|
|
1
8
|
export declare function doctorCommand(home?: string): Promise<void>;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,13 +3,42 @@ import ora from 'ora';
|
|
|
3
3
|
import { detectAll } from './configure.js';
|
|
4
4
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
5
5
|
import { validateApiKey, testStreaming } from '../utils/api.js';
|
|
6
|
+
import { claudeCodeBackgroundModel, fastChatCompletionsModel, preferredChatCompletionsModel } from '../configure/models.js';
|
|
7
|
+
import { loadModelCheck } from '../utils/modelCheck.js';
|
|
6
8
|
import { homeDir } from '../utils/paths.js';
|
|
7
9
|
import { clients } from '../clients.js';
|
|
8
10
|
import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
11
|
+
import { repairRooRipgrep, rooInstallLabel } from '../repair/roo-ripgrep.js';
|
|
12
|
+
/**
|
|
13
|
+
* Roo Code 3.54 cannot find newer VS Code's relocated ripgrep, so every task
|
|
14
|
+
* hangs at "API Request…". Read-only: reports installs that still need
|
|
15
|
+
* `scalemax fix-roo`. A warning, not a failure, because older VS Code builds
|
|
16
|
+
* still work unpatched.
|
|
17
|
+
*/
|
|
18
|
+
export async function rooRipgrepDoctorLines(home) {
|
|
19
|
+
const results = await repairRooRipgrep(home, { dryRun: true });
|
|
20
|
+
return results.map(result => {
|
|
21
|
+
const label = rooInstallLabel(result);
|
|
22
|
+
if (result.status === 'patched')
|
|
23
|
+
return `${chalk.yellow('!')} ${label} cannot find ripgrep on recent VS Code, so tasks hang at "API Request…". Run ${chalk.bold('npx scalemax@latest fix-roo')}, then Developer: Reload Window.`;
|
|
24
|
+
if (result.status === 'already_patched')
|
|
25
|
+
return `${chalk.green('✓')} ${label} ripgrep fix for newer VS Code applied`;
|
|
26
|
+
if (result.status === 'unsupported')
|
|
27
|
+
return `${chalk.dim('•')} ${label} ripgrep lookup not recognised; fix-roo not needed`;
|
|
28
|
+
return `${chalk.yellow('!')} ${label} could not be checked for the ripgrep fix: ${result.error}`;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
9
31
|
export async function doctorCommand(home = homeDir()) {
|
|
10
32
|
let failures = 0;
|
|
11
33
|
const key = await loadApiKey(home);
|
|
12
34
|
let models = [];
|
|
35
|
+
let baseUrl = SCALEMAX_BASE_URL;
|
|
36
|
+
// Setup passes the key's scope to every client (Roo writes its profiles by
|
|
37
|
+
// scope); without it doctor expected a different Roo profile set.
|
|
38
|
+
let providerScope;
|
|
39
|
+
// The models setup's live check skipped, so each client is checked against
|
|
40
|
+
// the same defaults setup wrote (configure/models.ts picks them).
|
|
41
|
+
const avoidModelIds = key ? await loadModelCheck(home, key) : [];
|
|
13
42
|
if (!key) {
|
|
14
43
|
console.log(`${chalk.red('✗')} API key missing`);
|
|
15
44
|
failures++;
|
|
@@ -18,11 +47,24 @@ export async function doctorCommand(home = homeDir()) {
|
|
|
18
47
|
const s = ora('Checking API key and models').start();
|
|
19
48
|
const res = await validateApiKey(key);
|
|
20
49
|
models = res.models;
|
|
50
|
+
baseUrl = res.baseUrl || SCALEMAX_BASE_URL;
|
|
51
|
+
providerScope = res.key?.provider_scope;
|
|
21
52
|
if (res.ok && models.length) {
|
|
22
53
|
s.succeed(`API reachable, key valid, ${models.length} models loaded`);
|
|
23
54
|
console.log(`${chalk.green('✓')} Usage and key metadata accessible through the authenticated API`);
|
|
24
55
|
const st = ora('Checking streaming (small billable request)').start();
|
|
25
|
-
|
|
56
|
+
// Stream a cheap chat default: the catalog also lists image, video and
|
|
57
|
+
// embedding rows (which /chat/completions 404s) and lists the
|
|
58
|
+
// claude-sonnet-4-6 alias first.
|
|
59
|
+
const catalog = { models, avoidModelIds };
|
|
60
|
+
let streamModel;
|
|
61
|
+
try {
|
|
62
|
+
streamModel = fastChatCompletionsModel(catalog) || claudeCodeBackgroundModel(catalog) || preferredChatCompletionsModel(catalog);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
streamModel = undefined;
|
|
66
|
+
}
|
|
67
|
+
const stream = await testStreaming(key, streamModel, baseUrl);
|
|
26
68
|
if (stream.ok)
|
|
27
69
|
st.succeed('Streaming available');
|
|
28
70
|
else {
|
|
@@ -49,8 +91,7 @@ export async function doctorCommand(home = homeDir()) {
|
|
|
49
91
|
}
|
|
50
92
|
if (client.verify && key && models.length) {
|
|
51
93
|
try {
|
|
52
|
-
const
|
|
53
|
-
const v = await client.verify({ home, platform: process.platform, env: process.env, apiKey: key, baseUrl: checked.baseUrl || SCALEMAX_BASE_URL, models });
|
|
94
|
+
const v = await client.verify({ home, platform: process.platform, env: process.env, apiKey: key, baseUrl, providerScope, models, avoidModelIds });
|
|
54
95
|
console.log(`${chalk.green('✓')} ${client.name} ${v.message}`);
|
|
55
96
|
}
|
|
56
97
|
catch (error) {
|
|
@@ -61,6 +102,8 @@ export async function doctorCommand(home = homeDir()) {
|
|
|
61
102
|
else
|
|
62
103
|
console.log(`${chalk.green('✓')} ${client.name} configured (${d.reason || 'detector confirmed'})`);
|
|
63
104
|
}
|
|
105
|
+
for (const line of await rooRipgrepDoctorLines(home))
|
|
106
|
+
console.log(line);
|
|
64
107
|
console.log('');
|
|
65
108
|
if (failures === 0)
|
|
66
109
|
console.log(chalk.green('Everything looks good!'));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fixRooCommand(args?: readonly string[], home?: string): Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { repairRooRipgrep, restoreRooRipgrep, rooInstallLabel } from '../repair/roo-ripgrep.js';
|
|
3
|
+
import { homeDir } from '../utils/paths.js';
|
|
4
|
+
const RELOAD_HINT = 'Reload VS Code (Developer: Reload Window) so Roo Code picks up the fix.';
|
|
5
|
+
export async function fixRooCommand(args = [], home = homeDir()) {
|
|
6
|
+
if (args.includes('--undo'))
|
|
7
|
+
return undoFixRoo(home);
|
|
8
|
+
const results = await repairRooRipgrep(home);
|
|
9
|
+
if (!results.length) {
|
|
10
|
+
console.log(chalk.yellow('! Roo Code is not installed in VS Code or a VS Code-based editor on this computer. Nothing to fix.'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log(chalk.dim(`Found ${results.length} Roo Code installation${results.length === 1 ? '' : 's'}.`));
|
|
14
|
+
let failures = 0;
|
|
15
|
+
for (const result of results) {
|
|
16
|
+
const label = rooInstallLabel(result);
|
|
17
|
+
if (result.status === 'patched') {
|
|
18
|
+
console.log(chalk.green(`✓ ${label}: patched — it now finds newer VS Code's ripgrep.`));
|
|
19
|
+
console.log(chalk.dim(` Original saved as ${result.backup}`));
|
|
20
|
+
}
|
|
21
|
+
else if (result.status === 'already_patched') {
|
|
22
|
+
console.log(chalk.green(`✓ ${label}: already patched.`));
|
|
23
|
+
}
|
|
24
|
+
else if (result.status === 'unsupported') {
|
|
25
|
+
console.log(chalk.yellow(`! ${label}: ripgrep lookup not recognised; left unchanged (this version may not need the fix).`));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
failures++;
|
|
29
|
+
console.log(chalk.red(`✗ ${label}: could not patch ${result.script}: ${result.error}`));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (results.some(result => result.status === 'patched' || result.status === 'already_patched'))
|
|
33
|
+
console.log(`\n${RELOAD_HINT}`);
|
|
34
|
+
if (failures)
|
|
35
|
+
throw new Error(`${failures} Roo Code installation${failures === 1 ? '' : 's'} could not be patched. Close VS Code and run \`npx scalemax@latest fix-roo\` again.`);
|
|
36
|
+
}
|
|
37
|
+
async function undoFixRoo(home) {
|
|
38
|
+
const results = await restoreRooRipgrep(home);
|
|
39
|
+
if (!results.length) {
|
|
40
|
+
console.log(chalk.yellow('! Roo Code is not installed in VS Code or a VS Code-based editor on this computer. Nothing to undo.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let failures = 0;
|
|
44
|
+
for (const result of results) {
|
|
45
|
+
const label = rooInstallLabel(result);
|
|
46
|
+
if (result.status === 'restored')
|
|
47
|
+
console.log(chalk.green(`✓ ${label}: original extension restored.`));
|
|
48
|
+
else if (result.status === 'not_patched')
|
|
49
|
+
console.log(chalk.dim(`• ${label}: not patched; nothing to undo.`));
|
|
50
|
+
else {
|
|
51
|
+
failures++;
|
|
52
|
+
const reason = result.status === 'no_backup'
|
|
53
|
+
? `no ScaleMax backup found next to ${result.script}`
|
|
54
|
+
: result.status === 'mismatch'
|
|
55
|
+
? `the extension changed after it was patched, so the backup at ${result.backup} was not applied`
|
|
56
|
+
: result.error;
|
|
57
|
+
console.log(chalk.red(`✗ ${label}: ${reason}. Reinstall Roo Code to get a clean copy.`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (results.some(result => result.status === 'restored'))
|
|
61
|
+
console.log('\nReload VS Code (Developer: Reload Window) so Roo Code loads the original files.');
|
|
62
|
+
if (failures)
|
|
63
|
+
throw new Error(`${failures} Roo Code installation${failures === 1 ? '' : 's'} could not be restored.`);
|
|
64
|
+
}
|
|
@@ -4,15 +4,16 @@ 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 { rollbackAfterVerificationFailure } from './configure.js';
|
|
8
|
-
import {
|
|
7
|
+
import { clientAllowedForModels, rollbackAfterVerificationFailure } from './configure.js';
|
|
8
|
+
import { loadModelCheck } from '../utils/modelCheck.js';
|
|
9
|
+
import { createChatCompletion, filterModelsForScope, isAnthropicModel, isChatModel } from '../utils/api.js';
|
|
9
10
|
import { redactApiKey } from '../utils/apiKey.js';
|
|
10
11
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
11
12
|
import { homeDir } from '../utils/paths.js';
|
|
12
13
|
import { validateApiKey } from '../utils/api.js';
|
|
13
14
|
import { commandExists } from '../utils/exec.js';
|
|
14
15
|
import { installClaudeGithubTools } from '../configure/claude-tools.js';
|
|
15
|
-
import { isExactChatCompletionsModelId } from '../configure/models.js';
|
|
16
|
+
import { CODEX_NEEDS_GPT_MESSAGE, isExactChatCompletionsModelId, modelIdKey, sameModelId } from '../configure/models.js';
|
|
16
17
|
import { isContinueEligibleModelId } from '../configure/continue.js';
|
|
17
18
|
export const SCALEMAX_HELPER_REFUSAL = `I am the official ScaleMax Helper.
|
|
18
19
|
|
|
@@ -708,21 +709,25 @@ codex
|
|
|
708
709
|
return undefined;
|
|
709
710
|
}
|
|
710
711
|
export function selectHelperModel(models, requested) {
|
|
712
|
+
// The helper talks over /chat/completions: never pick an image, video,
|
|
713
|
+
// embedding or voice row. Ids match in any `[1m]` spelling.
|
|
714
|
+
const chat = models.filter(isChatModel);
|
|
711
715
|
if (requested) {
|
|
712
|
-
const exact =
|
|
716
|
+
const exact = chat.find(model => model.id.trim().toLowerCase() === requested.trim().toLowerCase())
|
|
717
|
+
|| chat.find(model => sameModelId(model.id, requested));
|
|
713
718
|
if (!exact)
|
|
714
719
|
throw new Error(`Model "${requested}" is not available to this API key. Run \`scalemax models\` to see available models.`);
|
|
715
720
|
return exact.id;
|
|
716
721
|
}
|
|
717
722
|
const preferred = ['gpt-5.6-luna', 'claude-haiku-4-5-20251001', 'fable-5', 'gpt-5.6-terra', 'gpt-5.6-sol'];
|
|
718
723
|
for (const id of preferred) {
|
|
719
|
-
const match =
|
|
724
|
+
const match = chat.find(model => sameModelId(model.id, id));
|
|
720
725
|
if (match)
|
|
721
726
|
return match.id;
|
|
722
727
|
}
|
|
723
|
-
if (!
|
|
728
|
+
if (!chat[0])
|
|
724
729
|
throw new Error('No ScaleMax model is available to this API key.');
|
|
725
|
-
return
|
|
730
|
+
return chat[0].id;
|
|
726
731
|
}
|
|
727
732
|
export async function requestHelperReply(question, history, connection, complete = createChatCompletion) {
|
|
728
733
|
const cleanQuestion = redactHelperSecrets(question.trim(), connection.apiKey);
|
|
@@ -832,6 +837,19 @@ function requestedProviderScope(question, target) {
|
|
|
832
837
|
return 'openai';
|
|
833
838
|
return undefined;
|
|
834
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* The GPT/Codex family. The live catalog publishes those models under the
|
|
842
|
+
* `scalemax` owner (not `openai`, which now labels only embedding rows), so
|
|
843
|
+
* the owner alone matched no chat model at all.
|
|
844
|
+
*/
|
|
845
|
+
function isOpenAiFamilyModel(model) {
|
|
846
|
+
const family = String(model.provider || model.owned_by || '').trim().toLowerCase();
|
|
847
|
+
if (family === 'openai')
|
|
848
|
+
return true;
|
|
849
|
+
if (family === 'scalemax')
|
|
850
|
+
return /^(gpt-|codex-)/i.test(modelIdKey(model.id));
|
|
851
|
+
return !family && !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id);
|
|
852
|
+
}
|
|
835
853
|
function actionModels(connection, requestedScope) {
|
|
836
854
|
const rawCatalog = connection.modelCatalog?.length
|
|
837
855
|
? connection.modelCatalog
|
|
@@ -840,7 +858,9 @@ function actionModels(connection, requestedScope) {
|
|
|
840
858
|
// The authenticated catalog already includes reseller and extra-family
|
|
841
859
|
// grants. Validate the primary scope, then narrow only for explicit user
|
|
842
860
|
// intent; never try to reconstruct effective entitlement client-side.
|
|
843
|
-
|
|
861
|
+
// Client actions configure chat clients only; image, video, embedding and
|
|
862
|
+
// voice rows would otherwise be written into them as chat models.
|
|
863
|
+
const catalog = filterModelsForScope(rawCatalog, entitlement).filter(isChatModel);
|
|
844
864
|
if (!requestedScope || requestedScope === 'reseller' || requestedScope === 'local')
|
|
845
865
|
return catalog;
|
|
846
866
|
const exactModelByScope = {
|
|
@@ -851,19 +871,14 @@ function actionModels(connection, requestedScope) {
|
|
|
851
871
|
};
|
|
852
872
|
const exactModel = exactModelByScope[requestedScope];
|
|
853
873
|
if (exactModel)
|
|
854
|
-
return catalog.filter(model => model.id
|
|
874
|
+
return catalog.filter(model => modelIdKey(model.id) === exactModel);
|
|
855
875
|
if (requestedScope === 'both') {
|
|
856
|
-
return catalog.filter(model => isAnthropicModel(model)
|
|
857
|
-
|| String(model.provider || model.owned_by || '').trim().toLowerCase() === 'openai');
|
|
876
|
+
return catalog.filter(model => isAnthropicModel(model) || isOpenAiFamilyModel(model));
|
|
858
877
|
}
|
|
859
878
|
if (requestedScope === 'anthropic')
|
|
860
879
|
return catalog.filter(isAnthropicModel);
|
|
861
|
-
if (requestedScope === 'openai')
|
|
862
|
-
return catalog.filter(
|
|
863
|
-
const family = String(model.provider || model.owned_by || '').trim().toLowerCase();
|
|
864
|
-
return family ? family === 'openai' : !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id);
|
|
865
|
-
});
|
|
866
|
-
}
|
|
880
|
+
if (requestedScope === 'openai')
|
|
881
|
+
return catalog.filter(isOpenAiFamilyModel);
|
|
867
882
|
return catalog.filter(model => String(model.provider || model.owned_by || '').trim().toLowerCase() === requestedScope);
|
|
868
883
|
}
|
|
869
884
|
function helperClientAllowedForModels(clientId, models) {
|
|
@@ -968,8 +983,8 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
968
983
|
const models = actionModels(connection, requestedScope);
|
|
969
984
|
if (!models.length)
|
|
970
985
|
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.`);
|
|
971
|
-
if (client.id === 'codex' &&
|
|
972
|
-
throw new Error(
|
|
986
|
+
if (client.id === 'codex' && !clientAllowedForModels('codex', models))
|
|
987
|
+
throw new Error(`${CODEX_NEEDS_GPT_MESSAGE} No configuration was changed.`);
|
|
973
988
|
if (client.id === 'cursor' && connection.platform !== 'darwin')
|
|
974
989
|
throw new Error(`Automatic Cursor configuration is currently verified on macOS only. ${helperPlatformName(connection.platform)} was not modified.`);
|
|
975
990
|
const ctx = {
|
|
@@ -980,6 +995,8 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
980
995
|
baseUrl: connection.baseUrl,
|
|
981
996
|
providerScope: connection.providerScope || 'both',
|
|
982
997
|
models,
|
|
998
|
+
// The same defaults setup and doctor use for this key.
|
|
999
|
+
avoidModelIds: await loadModelCheck(home, connection.apiKey).catch(() => []),
|
|
983
1000
|
interactive: false,
|
|
984
1001
|
allowAppModification: false
|
|
985
1002
|
};
|
|
@@ -1020,6 +1037,8 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
1020
1037
|
write(`→ Configuring ${client.name} with the authenticated stored key…`);
|
|
1021
1038
|
const configured = await client.configure(ctx);
|
|
1022
1039
|
write(`✓ ${client.name} ${configured.message}`);
|
|
1040
|
+
if (configured.note)
|
|
1041
|
+
write(` ${configured.note}`);
|
|
1023
1042
|
if (client.verify) {
|
|
1024
1043
|
write(`→ Verifying ${client.name} configuration…`);
|
|
1025
1044
|
try {
|
|
@@ -23,6 +23,12 @@ export const HELPER_DOC_SECTIONS = [
|
|
|
23
23
|
keywords: ['roo', 'roo code', 'vscode', 'vs code', 'extension', 'visual studio code', 'deepseek'],
|
|
24
24
|
content: `For Roo Code, the ScaleMax CLI writes a private settings import file at ~/.scalemax/roo-code-import.json and sets roo-cline.autoImportSettingsPath in VS Code User settings. Roo imports that profile after VS Code is reloaded. A DeepSeek V4 Flash profile is offered only when the authenticated catalog reports capabilities.tools=true. If the active route is text-only, Roo fails closed because its task loop requires tools and the helper recommends Continue Chat/Edit instead. The CLI can install the official Roo Code extension with code --install-extension RooVeterinaryInc.roo-cline when VS Code is detected. On Windows, helper-bot may install VS Code first with winget using the fixed Microsoft.VisualStudioCode package id, then install Roo, write the import handoff, and verify the files.`
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
id: 'roo-code-ripgrep',
|
|
28
|
+
title: 'Roo Code stuck at API Request on recent VS Code versions',
|
|
29
|
+
keywords: ['ripgrep', 'stuck', 'hang', 'hangs', 'hanging', 'frozen', 'fix-roo'],
|
|
30
|
+
content: `Roo Code 3.54, its latest release, looks for VS Code's bundled ripgrep only in folders newer VS Code releases moved to node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/<platform>-<arch>/, so on recent VS Code versions every Roo task hangs at "API Request..." with "Could not find ripgrep binary", whatever the model or provider; the ScaleMax key is fine. Fix: run npx scalemax@latest fix-roo, then Developer: Reload Window. It patches only the installed Roo Code extension in the home folder, keeps the original as extension.js.scalemax-orig, never changes the VS Code app, and fix-roo --undo restores it. Configuring Roo Code with the CLI applies the same fix and scalemax doctor reports installs that still need it. Alternatives: copy rg from that folder to <VS Code app>/resources/app/node_modules/@vscode/ripgrep/bin/ (removed by every VS Code update), or use Cline or Kilo Code.`
|
|
31
|
+
},
|
|
26
32
|
{
|
|
27
33
|
id: 'continue-deepseek',
|
|
28
34
|
title: 'Continue setup for DeepSeek V4 Flash',
|
|
@@ -4,12 +4,12 @@ import { jsonConfig } from './templates.js';
|
|
|
4
4
|
const id = 'api-code';
|
|
5
5
|
const name = 'API Code';
|
|
6
6
|
export async function configure(ctx) {
|
|
7
|
-
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
7
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
8
8
|
const content = jsonConfig(name, '$SCALEMAX_API_KEY', ctx.baseUrl, { environment: { SCALEMAX_API_KEY: 'your ScaleMax API key', SCALEMAX_BASE_URL: ctx.baseUrl }, node: `import OpenAI from 'openai';\nconst client = new OpenAI({ apiKey: process.env.SCALEMAX_API_KEY, baseURL: '${ctx.baseUrl}' });`, python: `from openai import OpenAI\nclient = OpenAI(api_key='...', base_url='${ctx.baseUrl}')` });
|
|
9
9
|
const result = await writeManagedConfig(ctx, id, name, file, content);
|
|
10
10
|
return { ...result, message: 'API code examples written' };
|
|
11
11
|
}
|
|
12
12
|
export async function remove(ctx) {
|
|
13
|
-
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
13
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
14
14
|
return removeManagedConfig(ctx, id, name, file);
|
|
15
15
|
}
|