scalemax 0.1.47 → 0.1.49
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 +3 -0
- package/dist/commands/configure.js +2 -0
- package/dist/commands/doctor.d.ts +7 -0
- package/dist/commands/doctor.js +22 -0
- package/dist/commands/fix-roo.d.ts +1 -0
- package/dist/commands/fix-roo.js +64 -0
- package/dist/commands/helper-bot.js +3 -1
- package/dist/commands/helper-docs.js +6 -0
- package/dist/configure/api-code.js +2 -2
- package/dist/configure/codex.d.ts +17 -1
- package/dist/configure/codex.js +207 -39
- package/dist/configure/models.js +4 -4
- package/dist/configure/openclaw.js +3 -3
- package/dist/configure/opencode.js +3 -3
- package/dist/configure/roo-code.js +9 -0
- package/dist/configure/templates.js +2 -2
- 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 +6 -0
- package/dist/utils/fs.d.ts +1 -1
- package/dist/utils/fs.js +2 -1
- 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
|
|
@@ -172,6 +173,8 @@ ScaleMax creates family-specific Hermes providers in `~/.hermes/config.yaml` and
|
|
|
172
173
|
|
|
173
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. 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
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.
|
|
177
|
+
|
|
175
178
|
### Cline (CLI and VS Code)
|
|
176
179
|
|
|
177
180
|
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.
|
|
@@ -209,6 +209,8 @@ export async function configureCommand(opts = {}) {
|
|
|
209
209
|
const ctx = { ...baseCtx, apiKey, baseUrl: selectedBaseUrl, providerScope, models: keyInfo.models, interactive: Boolean(process.stdin.isTTY), allowAppModification };
|
|
210
210
|
const res = await client.configure(ctx);
|
|
211
211
|
spinner.succeed(`${client.name} ${res.message}`);
|
|
212
|
+
if (res.note)
|
|
213
|
+
console.log(chalk.dim(` ${res.note}`));
|
|
212
214
|
if (client.verify) {
|
|
213
215
|
const verify = ora(`Verifying ${client.name}`).start();
|
|
214
216
|
try {
|
|
@@ -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
|
@@ -6,6 +6,26 @@ import { validateApiKey, testStreaming } from '../utils/api.js';
|
|
|
6
6
|
import { homeDir } from '../utils/paths.js';
|
|
7
7
|
import { clients } from '../clients.js';
|
|
8
8
|
import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
9
|
+
import { repairRooRipgrep, rooInstallLabel } from '../repair/roo-ripgrep.js';
|
|
10
|
+
/**
|
|
11
|
+
* Roo Code 3.54 cannot find newer VS Code's relocated ripgrep, so every task
|
|
12
|
+
* hangs at "API Request…". Read-only: reports installs that still need
|
|
13
|
+
* `scalemax fix-roo`. A warning, not a failure, because older VS Code builds
|
|
14
|
+
* still work unpatched.
|
|
15
|
+
*/
|
|
16
|
+
export async function rooRipgrepDoctorLines(home) {
|
|
17
|
+
const results = await repairRooRipgrep(home, { dryRun: true });
|
|
18
|
+
return results.map(result => {
|
|
19
|
+
const label = rooInstallLabel(result);
|
|
20
|
+
if (result.status === 'patched')
|
|
21
|
+
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.`;
|
|
22
|
+
if (result.status === 'already_patched')
|
|
23
|
+
return `${chalk.green('✓')} ${label} ripgrep fix for newer VS Code applied`;
|
|
24
|
+
if (result.status === 'unsupported')
|
|
25
|
+
return `${chalk.dim('•')} ${label} ripgrep lookup not recognised; fix-roo not needed`;
|
|
26
|
+
return `${chalk.yellow('!')} ${label} could not be checked for the ripgrep fix: ${result.error}`;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
9
29
|
export async function doctorCommand(home = homeDir()) {
|
|
10
30
|
let failures = 0;
|
|
11
31
|
const key = await loadApiKey(home);
|
|
@@ -61,6 +81,8 @@ export async function doctorCommand(home = homeDir()) {
|
|
|
61
81
|
else
|
|
62
82
|
console.log(`${chalk.green('✓')} ${client.name} configured (${d.reason || 'detector confirmed'})`);
|
|
63
83
|
}
|
|
84
|
+
for (const line of await rooRipgrepDoctorLines(home))
|
|
85
|
+
console.log(line);
|
|
64
86
|
console.log('');
|
|
65
87
|
if (failures === 0)
|
|
66
88
|
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
|
+
}
|
|
@@ -714,7 +714,7 @@ export function selectHelperModel(models, requested) {
|
|
|
714
714
|
throw new Error(`Model "${requested}" is not available to this API key. Run \`scalemax models\` to see available models.`);
|
|
715
715
|
return exact.id;
|
|
716
716
|
}
|
|
717
|
-
const preferred = ['gpt-5.
|
|
717
|
+
const preferred = ['gpt-5.6-luna', 'claude-haiku-4-5-20251001', 'fable-5', 'gpt-5.6-terra', 'gpt-5.6-sol'];
|
|
718
718
|
for (const id of preferred) {
|
|
719
719
|
const match = models.find(model => model.id.toLowerCase() === id);
|
|
720
720
|
if (match)
|
|
@@ -1020,6 +1020,8 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
1020
1020
|
write(`→ Configuring ${client.name} with the authenticated stored key…`);
|
|
1021
1021
|
const configured = await client.configure(ctx);
|
|
1022
1022
|
write(`✓ ${client.name} ${configured.message}`);
|
|
1023
|
+
if (configured.note)
|
|
1024
|
+
write(` ${configured.note}`);
|
|
1023
1025
|
if (client.verify) {
|
|
1024
1026
|
write(`→ Verifying ${client.name} configuration…`);
|
|
1025
1027
|
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
|
}
|
|
@@ -6,7 +6,23 @@ export interface CodexVerificationResult {
|
|
|
6
6
|
configPath: string;
|
|
7
7
|
authPath: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function mergeCodexConfig(existing: string, baseUrl: string, model?: string, authHash?: string): string;
|
|
9
|
+
export declare function mergeCodexConfig(existing: string, baseUrl: string, model?: string, authHash?: string, keyHash?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Take ScaleMax OUT of a config.toml, keeping everything else the user has --
|
|
12
|
+
* including what Codex or the user added after setup (trusted projects,
|
|
13
|
+
* notices, other providers).
|
|
14
|
+
*
|
|
15
|
+
* Ownership is read from the CONTENT, not only from the generated header: a
|
|
16
|
+
* Codex rewrite that drops comments still leaves `model_provider = "scalemax"`
|
|
17
|
+
* and the `[model_providers.scalemax]` table, and those must go. The three
|
|
18
|
+
* top-level keys are removed only while ScaleMax is still the provider; if the
|
|
19
|
+
* user switched provider after setup they are the user's.
|
|
20
|
+
*/
|
|
21
|
+
export declare function stripScaleMaxCodexConfig(existing: string): {
|
|
22
|
+
text: string;
|
|
23
|
+
found: boolean;
|
|
24
|
+
strippedTopLevel: boolean;
|
|
25
|
+
};
|
|
10
26
|
export declare function verifyCodexConfiguration(ctx: ConfigureContext): Promise<CodexVerificationResult>;
|
|
11
27
|
export declare function install(ctx: InstallContext): Promise<ConfigureResult>;
|
|
12
28
|
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
package/dist/configure/codex.js
CHANGED
|
@@ -10,17 +10,29 @@ import { AUTO_MODEL_ID, preferredOpenAiResponsesModel } from './models.js';
|
|
|
10
10
|
const id = 'codex';
|
|
11
11
|
const name = 'Codex';
|
|
12
12
|
const provider = 'scalemax';
|
|
13
|
-
const fallbackModel = 'gpt-5.
|
|
13
|
+
const fallbackModel = 'gpt-5.6-luna'; // gpt-5.5 retired 2026-09-30; same price
|
|
14
14
|
const reasoningEffort = 'medium';
|
|
15
15
|
const managedAuthHashPrefix = '# ScaleMax managed auth.json SHA-256: ';
|
|
16
|
+
// Fingerprint of the API key setup wrote into auth.json. Unlike the auth.json
|
|
17
|
+
// hash above it survives Codex rewriting auth.json (a token refresh, its own
|
|
18
|
+
// serialisation), so remove() can still tell OUR key from one the user put
|
|
19
|
+
// there later.
|
|
20
|
+
const managedKeyHashPrefix = '# ScaleMax managed API key SHA-256: ';
|
|
21
|
+
const modelsComment = '# ScaleMax models are loaded from the authenticated live model catalog.';
|
|
22
|
+
// The top-level keys setup owns while ScaleMax is the provider. remove() puts
|
|
23
|
+
// the user's pre-ScaleMax values for exactly these back from the backup.
|
|
24
|
+
const managedTopLevelKey = /^(model|model_provider|model_reasoning_effort)\s*=/;
|
|
16
25
|
function contentSha256(content) {
|
|
17
26
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
18
27
|
}
|
|
19
|
-
function
|
|
20
|
-
const marker = config.split(/\r?\n/).find(line => line.startsWith(
|
|
21
|
-
const hash = marker?.slice(
|
|
28
|
+
function markerHash(config, prefix) {
|
|
29
|
+
const marker = config.split(/\r?\n/).find(line => line.startsWith(prefix));
|
|
30
|
+
const hash = marker?.slice(prefix.length).trim();
|
|
22
31
|
return hash && /^[a-f0-9]{64}$/.test(hash) ? hash : undefined;
|
|
23
32
|
}
|
|
33
|
+
function managedAuthHash(config) {
|
|
34
|
+
return markerHash(config, managedAuthHashPrefix);
|
|
35
|
+
}
|
|
24
36
|
function selectedModel(ctx) {
|
|
25
37
|
if (!ctx.models) {
|
|
26
38
|
// An auto-api key's ONLY model is `auto`; gpt-5.5 (the generic fallback
|
|
@@ -30,11 +42,22 @@ function selectedModel(ctx) {
|
|
|
30
42
|
}
|
|
31
43
|
return preferredOpenAiResponsesModel(ctx);
|
|
32
44
|
}
|
|
33
|
-
function scalemaxCodexBlock(baseUrl, model = fallbackModel, authHash) {
|
|
45
|
+
function scalemaxCodexBlock(baseUrl, model = fallbackModel, authHash, keyHash) {
|
|
34
46
|
const authMarker = authHash ? `${managedAuthHashPrefix}${authHash}\n` : '';
|
|
35
|
-
|
|
47
|
+
const keyMarker = keyHash ? `${managedKeyHashPrefix}${keyHash}\n` : '';
|
|
48
|
+
return `# ${GENERATED_HEADER}\n${authMarker}${keyMarker}model = "${model}"\nmodel_provider = "${provider}"\nmodel_reasoning_effort = "${reasoningEffort}"\n${modelsComment}\n\n[model_providers.${provider}]\nname = "ScaleMax"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
|
|
49
|
+
}
|
|
50
|
+
/** A line setup writes as a comment. Never user content, and never preserved. */
|
|
51
|
+
function isManagedCommentLine(line) {
|
|
52
|
+
return line === `# ${GENERATED_HEADER}`
|
|
53
|
+
|| line === modelsComment
|
|
54
|
+
|| line.startsWith(managedAuthHashPrefix)
|
|
55
|
+
|| line.startsWith(managedKeyHashPrefix);
|
|
36
56
|
}
|
|
37
|
-
|
|
57
|
+
function isScaleMaxProviderSection(section) {
|
|
58
|
+
return section === `model_providers.${provider}` || section.startsWith(`model_providers.${provider}.`);
|
|
59
|
+
}
|
|
60
|
+
export function mergeCodexConfig(existing, baseUrl, model = fallbackModel, authHash, keyHash) {
|
|
38
61
|
const preserved = [];
|
|
39
62
|
let section = '';
|
|
40
63
|
let skipScaleMaxProvider = false;
|
|
@@ -43,20 +66,94 @@ export function mergeCodexConfig(existing, baseUrl, model = fallbackModel, authH
|
|
|
43
66
|
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
44
67
|
if (sectionMatch) {
|
|
45
68
|
section = sectionMatch[1];
|
|
46
|
-
skipScaleMaxProvider = section
|
|
69
|
+
skipScaleMaxProvider = isScaleMaxProviderSection(section);
|
|
47
70
|
if (skipScaleMaxProvider)
|
|
48
71
|
continue;
|
|
49
72
|
}
|
|
50
73
|
if (skipScaleMaxProvider)
|
|
51
74
|
continue;
|
|
52
|
-
if (!section &&
|
|
75
|
+
if (!section && managedTopLevelKey.test(line))
|
|
53
76
|
continue;
|
|
54
|
-
|
|
77
|
+
// Includes the models comment: it used to be kept here, so every re-run of
|
|
78
|
+
// setup stacked one more copy of it into the preserved tail.
|
|
79
|
+
if (isManagedCommentLine(line))
|
|
55
80
|
continue;
|
|
56
81
|
preserved.push(raw);
|
|
57
82
|
}
|
|
58
83
|
const tail = preserved.join('\n').trim();
|
|
59
|
-
return `${scalemaxCodexBlock(baseUrl, model, authHash)}${tail ? `\n${tail}\n` : ''}`;
|
|
84
|
+
return `${scalemaxCodexBlock(baseUrl, model, authHash, keyHash)}${tail ? `\n${tail}\n` : ''}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Take ScaleMax OUT of a config.toml, keeping everything else the user has --
|
|
88
|
+
* including what Codex or the user added after setup (trusted projects,
|
|
89
|
+
* notices, other providers).
|
|
90
|
+
*
|
|
91
|
+
* Ownership is read from the CONTENT, not only from the generated header: a
|
|
92
|
+
* Codex rewrite that drops comments still leaves `model_provider = "scalemax"`
|
|
93
|
+
* and the `[model_providers.scalemax]` table, and those must go. The three
|
|
94
|
+
* top-level keys are removed only while ScaleMax is still the provider; if the
|
|
95
|
+
* user switched provider after setup they are the user's.
|
|
96
|
+
*/
|
|
97
|
+
export function stripScaleMaxCodexConfig(existing) {
|
|
98
|
+
const lines = existing.split(/\r?\n/);
|
|
99
|
+
let section = '';
|
|
100
|
+
let topProvider;
|
|
101
|
+
let found = false;
|
|
102
|
+
for (const raw of lines) {
|
|
103
|
+
const line = raw.trim();
|
|
104
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
105
|
+
if (sectionMatch) {
|
|
106
|
+
section = sectionMatch[1];
|
|
107
|
+
if (isScaleMaxProviderSection(section))
|
|
108
|
+
found = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (isManagedCommentLine(line))
|
|
112
|
+
found = true;
|
|
113
|
+
if (!section) {
|
|
114
|
+
const providerMatch = line.match(/^model_provider\s*=\s*"([^"]*)"/);
|
|
115
|
+
if (providerMatch)
|
|
116
|
+
topProvider = providerMatch[1];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const strippedTopLevel = topProvider === provider;
|
|
120
|
+
if (strippedTopLevel)
|
|
121
|
+
found = true;
|
|
122
|
+
if (!found)
|
|
123
|
+
return { text: existing, found: false, strippedTopLevel: false };
|
|
124
|
+
const kept = [];
|
|
125
|
+
section = '';
|
|
126
|
+
let skip = false;
|
|
127
|
+
for (const raw of lines) {
|
|
128
|
+
const line = raw.trim();
|
|
129
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
130
|
+
if (sectionMatch) {
|
|
131
|
+
section = sectionMatch[1];
|
|
132
|
+
skip = isScaleMaxProviderSection(section);
|
|
133
|
+
if (skip)
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (skip)
|
|
137
|
+
continue;
|
|
138
|
+
if (!section && strippedTopLevel && managedTopLevelKey.test(line))
|
|
139
|
+
continue;
|
|
140
|
+
if (isManagedCommentLine(line))
|
|
141
|
+
continue;
|
|
142
|
+
kept.push(raw);
|
|
143
|
+
}
|
|
144
|
+
return { text: kept.join('\n').trim(), found: true, strippedTopLevel };
|
|
145
|
+
}
|
|
146
|
+
/** The pre-ScaleMax top-level model / model_provider / model_reasoning_effort lines of a backup. */
|
|
147
|
+
function originalTopLevelLines(backup) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const raw of backup.split(/\r?\n/)) {
|
|
150
|
+
const line = raw.trim();
|
|
151
|
+
if (/^\[[^\]]+\]$/.test(line))
|
|
152
|
+
break;
|
|
153
|
+
if (managedTopLevelKey.test(line))
|
|
154
|
+
out.push(line);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
60
157
|
}
|
|
61
158
|
function codexAuth(apiKey, existing = {}) {
|
|
62
159
|
return JSON.stringify({ ...existing, auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2) + '\n';
|
|
@@ -111,22 +208,99 @@ async function latestRestorableBackup(file, home) {
|
|
|
111
208
|
}
|
|
112
209
|
return undefined;
|
|
113
210
|
}
|
|
114
|
-
async function
|
|
211
|
+
async function removeCodexConfig(file, ctx) {
|
|
115
212
|
if (!(await fs.pathExists(file)))
|
|
116
213
|
return { changed: false, message: 'no ScaleMax configuration found' };
|
|
117
214
|
const current = await fs.readFile(file, 'utf8').catch(() => '');
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
215
|
+
const stripped = stripScaleMaxCodexConfig(current);
|
|
216
|
+
if (!stripped.found)
|
|
217
|
+
return { changed: false, message: 'left unmanaged configuration unchanged' };
|
|
218
|
+
// Put back the user's own model settings from before setup (the first
|
|
219
|
+
// pre-ScaleMax backup) -- only when ScaleMax's were actually taken out.
|
|
220
|
+
let restoredTop = [];
|
|
221
|
+
if (stripped.strippedTopLevel) {
|
|
222
|
+
const backup = await latestRestorableBackup(file, ctx.home);
|
|
223
|
+
if (backup)
|
|
224
|
+
restoredTop = originalTopLevelLines(await fs.readFile(backup, 'utf8').catch(() => ''));
|
|
225
|
+
}
|
|
226
|
+
const next = [restoredTop.join('\n'), stripped.text].filter(Boolean).join('\n\n');
|
|
227
|
+
if (!next.trim()) {
|
|
228
|
+
await fs.remove(file);
|
|
229
|
+
return { changed: true, message: 'removed ScaleMax configuration' };
|
|
230
|
+
}
|
|
231
|
+
await atomicWrite(file, `${next}\n`, 0o600, ctx.platform);
|
|
232
|
+
return {
|
|
233
|
+
changed: true,
|
|
234
|
+
message: restoredTop.length
|
|
235
|
+
? 'removed ScaleMax settings and restored your previous model settings'
|
|
236
|
+
: 'removed ScaleMax settings; your other settings were kept',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Take the ScaleMax credential OUT of auth.json.
|
|
241
|
+
*
|
|
242
|
+
* Owned when ANY of: the file is byte-identical to what setup wrote (auth
|
|
243
|
+
* hash marker); its key is the key the CLI has saved; its key matches the
|
|
244
|
+
* key fingerprint setup wrote into config.toml. The last two survive Codex
|
|
245
|
+
* rewriting the file -- a ChatGPT token refresh or its own serialisation --
|
|
246
|
+
* which the exact hash alone did not, and which left the ScaleMax key in
|
|
247
|
+
* place after "remove". A DIFFERENT key the user put there after setup is
|
|
248
|
+
* none of these, and is left alone.
|
|
249
|
+
*
|
|
250
|
+
* Only the two fields setup sets are touched: auth_mode and OPENAI_API_KEY go
|
|
251
|
+
* back to their pre-setup values from the backup, or away if there were none.
|
|
252
|
+
* Tokens and refresh times stay as Codex last wrote them (they are fresher
|
|
253
|
+
* than the backup). A file left with nothing in it is deleted.
|
|
254
|
+
*/
|
|
255
|
+
async function removeCodexAuth(file, ctx, ownership) {
|
|
256
|
+
if (!(await fs.pathExists(file)))
|
|
257
|
+
return { changed: false, message: 'no ScaleMax configuration found' };
|
|
258
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
259
|
+
let current;
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(text);
|
|
262
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
263
|
+
throw new Error('not an object');
|
|
264
|
+
current = parsed;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
122
267
|
return { changed: false, message: 'left unmanaged configuration unchanged' };
|
|
123
|
-
const restore = await latestRestorableBackup(file, ctx.home);
|
|
124
|
-
if (restore) {
|
|
125
|
-
await atomicWrite(file, await fs.readFile(restore, 'utf8'), 0o600, ctx.platform);
|
|
126
|
-
return { changed: true, message: `restored backup ${restore}` };
|
|
127
268
|
}
|
|
128
|
-
|
|
129
|
-
|
|
269
|
+
const key = typeof current.OPENAI_API_KEY === 'string' ? current.OPENAI_API_KEY : undefined;
|
|
270
|
+
const owned = (ownership.authHash !== undefined && contentSha256(text) === ownership.authHash)
|
|
271
|
+
|| (key !== undefined && typeof ctx.apiKey === 'string' && ctx.apiKey.length > 0 && key === ctx.apiKey)
|
|
272
|
+
|| (key !== undefined && ownership.keyHash !== undefined && contentSha256(key) === ownership.keyHash)
|
|
273
|
+
|| (ownership.authHash === undefined && ownership.keyHash === undefined && ownership.legacyStructural && isManagedCodexAuth(text));
|
|
274
|
+
if (!owned)
|
|
275
|
+
return { changed: false, message: 'left unmanaged configuration unchanged' };
|
|
276
|
+
let original;
|
|
277
|
+
const backup = await latestRestorableBackup(file, ctx.home);
|
|
278
|
+
if (backup) {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(await fs.readFile(backup, 'utf8'));
|
|
281
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
282
|
+
original = parsed;
|
|
283
|
+
}
|
|
284
|
+
catch { /* an unreadable backup restores nothing */ }
|
|
285
|
+
}
|
|
286
|
+
const next = { ...current };
|
|
287
|
+
for (const field of ['auth_mode', 'OPENAI_API_KEY']) {
|
|
288
|
+
if (original && Object.prototype.hasOwnProperty.call(original, field))
|
|
289
|
+
next[field] = original[field];
|
|
290
|
+
else
|
|
291
|
+
delete next[field];
|
|
292
|
+
}
|
|
293
|
+
const meaningful = Object.values(next).some(value => value !== null && value !== undefined
|
|
294
|
+
&& !(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0));
|
|
295
|
+
if (!original && !meaningful) {
|
|
296
|
+
await fs.remove(file);
|
|
297
|
+
return { changed: true, message: 'removed ScaleMax configuration' };
|
|
298
|
+
}
|
|
299
|
+
await atomicWrite(file, JSON.stringify(next, null, 2) + '\n', 0o600, ctx.platform);
|
|
300
|
+
return {
|
|
301
|
+
changed: true,
|
|
302
|
+
message: original ? 'removed the ScaleMax key and restored your previous sign-in' : 'removed the ScaleMax key',
|
|
303
|
+
};
|
|
130
304
|
}
|
|
131
305
|
export async function verifyCodexConfiguration(ctx) {
|
|
132
306
|
const configPath = codexConfigPath(ctx.home);
|
|
@@ -261,7 +435,7 @@ export async function configure(ctx) {
|
|
|
261
435
|
}
|
|
262
436
|
catch { }
|
|
263
437
|
const nextAuth = codexAuth(ctx.apiKey, existingAuth);
|
|
264
|
-
const nextConfig = mergeCodexConfig(originalConfig, ctx.baseUrl, selectedModel(ctx), contentSha256(nextAuth));
|
|
438
|
+
const nextConfig = mergeCodexConfig(originalConfig, ctx.baseUrl, selectedModel(ctx), contentSha256(nextAuth), contentSha256(ctx.apiKey));
|
|
265
439
|
const changed = originalConfig !== nextConfig || originalAuth !== nextAuth;
|
|
266
440
|
const alreadyManaged = originalConfig.includes(GENERATED_HEADER);
|
|
267
441
|
let verification;
|
|
@@ -312,24 +486,18 @@ export async function configure(ctx) {
|
|
|
312
486
|
export async function remove(ctx) {
|
|
313
487
|
const configPath = codexConfigPath(ctx.home);
|
|
314
488
|
const authPath = codexAuthPath(ctx.home);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// configs without the marker retain the conservative structural fallback.
|
|
324
|
-
const authManagedOverride = expectedAuthHash
|
|
325
|
-
? contentSha256(authText) === expectedAuthHash
|
|
326
|
-
: configText.includes(GENERATED_HEADER)
|
|
327
|
-
? undefined
|
|
328
|
-
: false;
|
|
489
|
+
// Read the ownership markers BEFORE config.toml is rewritten below.
|
|
490
|
+
const configText = await fs.readFile(configPath, 'utf8').catch(() => '');
|
|
491
|
+
const ownership = {
|
|
492
|
+
authHash: managedAuthHash(configText),
|
|
493
|
+
keyHash: markerHash(configText, managedKeyHashPrefix),
|
|
494
|
+
// Configs written before either marker existed: the original structural rule.
|
|
495
|
+
legacyStructural: configText.includes(GENERATED_HEADER),
|
|
496
|
+
};
|
|
329
497
|
const snapshots = await captureFileSnapshots([configPath, authPath]);
|
|
330
498
|
try {
|
|
331
|
-
const config = await
|
|
332
|
-
const auth = await
|
|
499
|
+
const config = await removeCodexConfig(configPath, ctx);
|
|
500
|
+
const auth = await removeCodexAuth(authPath, ctx, ownership);
|
|
333
501
|
return {
|
|
334
502
|
id,
|
|
335
503
|
name,
|
package/dist/configure/models.js
CHANGED
|
@@ -15,9 +15,8 @@ const preferredOpenAi = [
|
|
|
15
15
|
'gpt-5.6-sol',
|
|
16
16
|
'gpt-6-sol',
|
|
17
17
|
'gpt-6-luna',
|
|
18
|
-
|
|
19
|
-
'gpt-5.
|
|
20
|
-
'gpt-5.4-mini',
|
|
18
|
+
// gpt-5.5 / gpt-5.4 / gpt-5.4-mini retired 2026-09-30.
|
|
19
|
+
'gpt-5.6-luna',
|
|
21
20
|
'gpt-5.3-codex',
|
|
22
21
|
'codex-auto-review',
|
|
23
22
|
// Gemini / Grok / Qwen ride the same OpenAI-compatible route, so they are
|
|
@@ -126,7 +125,8 @@ export function fastModel(ctx) {
|
|
|
126
125
|
}
|
|
127
126
|
export function fastOpenAiModel(ctx) {
|
|
128
127
|
const ids = openAiModelIds(ctx);
|
|
129
|
-
return ids.find(id => id === 'gpt-5.
|
|
128
|
+
return ids.find(id => id === 'gpt-5.6-luna')
|
|
129
|
+
|| ids.find(id => id === 'gpt-6-luna')
|
|
130
130
|
|| ids.find(id => /(^|[-.])(mini|nano)([-.]|$)/i.test(id));
|
|
131
131
|
}
|
|
132
132
|
/** The lightweight model for an OpenAI-compatible chat client, if one exists. */
|
|
@@ -117,7 +117,7 @@ async function scrubGeneratedPlaintextCredentials(ctx, files) {
|
|
|
117
117
|
return changed;
|
|
118
118
|
}
|
|
119
119
|
export async function configure(ctx) {
|
|
120
|
-
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
120
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
121
121
|
const credentialFile = secretPath(ctx.home);
|
|
122
122
|
const userAgentFile = userAgentPath(ctx.home);
|
|
123
123
|
const legacyFile = path.join(ctx.home, '.openclaw', 'config.json');
|
|
@@ -272,7 +272,7 @@ export async function configure(ctx) {
|
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
export async function verify(ctx) {
|
|
275
|
-
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
275
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
276
276
|
const config = await fs.readJson(file);
|
|
277
277
|
const codex = config?.models?.providers?.[codexProviderName];
|
|
278
278
|
const claude = config?.models?.providers?.[claudeProviderName];
|
|
@@ -319,7 +319,7 @@ export async function verify(ctx) {
|
|
|
319
319
|
return { id, name, changed: false, path: file, message: `verified ${codexIds.length} Responses, ${deepSeekIds.length} ${chatLabel}, and ${claudeIds.length} Claude models` };
|
|
320
320
|
}
|
|
321
321
|
export async function remove(ctx) {
|
|
322
|
-
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
322
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
323
323
|
if (!(await fs.pathExists(file))) {
|
|
324
324
|
const removed = await removeClientSecret(secretPath(ctx.home), ctx);
|
|
325
325
|
const removedUserAgent = await removeClientSecret(userAgentPath(ctx.home), ctx);
|
|
@@ -38,7 +38,7 @@ function providerConfig(ctx) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
export async function configure(ctx) {
|
|
41
|
-
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
41
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
42
42
|
const selected = `scalemax/${preferredChatCompletionsModel(ctx)}`;
|
|
43
43
|
const quick = fastChatCompletionsModel(ctx);
|
|
44
44
|
const credentialFile = secretPath(ctx.home);
|
|
@@ -78,7 +78,7 @@ export async function configure(ctx) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
export async function verify(ctx) {
|
|
81
|
-
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
81
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
82
82
|
const config = await fs.readJson(file);
|
|
83
83
|
const provider = config?.provider?.scalemax;
|
|
84
84
|
if (provider?.npm !== '@ai-sdk/openai-compatible' || provider?.options?.baseURL !== openAiBaseUrl(ctx.baseUrl) || provider?.options?.apiKey !== secretReference) {
|
|
@@ -98,7 +98,7 @@ export async function verify(ctx) {
|
|
|
98
98
|
return { id, name, changed: false, path: file, message: `verified ${configured.length} ScaleMax models` };
|
|
99
99
|
}
|
|
100
100
|
export async function remove(ctx) {
|
|
101
|
-
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
101
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
102
102
|
if (!(await fs.pathExists(file))) {
|
|
103
103
|
const removed = await removeClientSecret(secretPath(ctx.home), ctx);
|
|
104
104
|
return { id, name, changed: removed, path: file, message: removed ? 'removed orphaned ScaleMax OpenCode credential' : 'no ScaleMax configuration found' };
|
|
@@ -5,6 +5,7 @@ import { atomicWrite, backupFile, captureFileSnapshots, restoreFileSnapshots, sn
|
|
|
5
5
|
import { clientConfigPath } from './paths.js';
|
|
6
6
|
import { anthropicModelIds, deepSeekChatModelIds, isDeepSeekChatModelId, modelName, modelSupportsTools, openAiBaseUrl, openAiResponsesModelIds, preferredAnthropicModel, preferredChatCompletionsModel, preferredOpenAiResponsesModel } from './models.js';
|
|
7
7
|
import { isAnthropicModel } from '../utils/api.js';
|
|
8
|
+
import { rooRipgrepRepairNote } from '../repair/roo-ripgrep.js';
|
|
8
9
|
const id = 'roo-code';
|
|
9
10
|
const name = 'Roo Code (VS Code)';
|
|
10
11
|
const importSetting = 'roo-cline.autoImportSettingsPath';
|
|
@@ -149,6 +150,14 @@ function handoff(ctx) {
|
|
|
149
150
|
}, null, 2) + '\n';
|
|
150
151
|
}
|
|
151
152
|
export async function configure(ctx) {
|
|
153
|
+
const result = await configureHandoff(ctx);
|
|
154
|
+
// Roo Code 3.54 cannot find newer VS Code's relocated ripgrep, so every task
|
|
155
|
+
// hangs at "API Request…" whatever the provider. Patch the installed
|
|
156
|
+
// extension too; this is non-fatal and independent of the handoff above.
|
|
157
|
+
const note = await rooRipgrepRepairNote(ctx.home);
|
|
158
|
+
return note ? { ...result, note } : result;
|
|
159
|
+
}
|
|
160
|
+
async function configureHandoff(ctx) {
|
|
152
161
|
const settingsFile = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
153
162
|
const profileFile = importPath(ctx.home);
|
|
154
163
|
const snapshots = await captureFileSnapshots([settingsFile, profileFile]);
|
|
@@ -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-6-astra', 'gpt-6-sol', 'gpt-6-luna', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.
|
|
14
|
+
const models = ['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', '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}`,
|
|
@@ -21,7 +21,7 @@ export function continueConfig(apiKey, baseUrl) {
|
|
|
21
21
|
apiKey,
|
|
22
22
|
...(model === 'deepseek-v4-flash' ? { useResponsesApi: false } : {})
|
|
23
23
|
})),
|
|
24
|
-
tabAutocompleteModel: { title: 'ScaleMax gpt-5.
|
|
24
|
+
tabAutocompleteModel: { title: 'ScaleMax gpt-5.6-luna', provider: 'openai', model: 'gpt-5.6-luna', apiBase: baseUrl, apiKey },
|
|
25
25
|
contextProviders: [{ name: 'code' }, { name: 'docs' }, { name: 'diff' }, { name: 'terminal' }]
|
|
26
26
|
}, null, 2) + '\n';
|
|
27
27
|
}
|
|
@@ -1,2 +1,29 @@
|
|
|
1
1
|
import { DetectContext, DetectionResult } from '../types/client.js';
|
|
2
|
+
export declare const ROO_EXTENSION_PATTERN: RegExp;
|
|
3
|
+
export interface EditorExtensionRoot {
|
|
4
|
+
/** Human label used in CLI output, e.g. "VS Code" or "Cursor". */
|
|
5
|
+
editor: string;
|
|
6
|
+
/** Path segments below the home folder. */
|
|
7
|
+
segments: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* VS Code's own per-user extension folders. Detection stays limited to these
|
|
11
|
+
* because configure writes VS Code's settings.json, which forks do not read.
|
|
12
|
+
*/
|
|
13
|
+
export declare const VSCODE_EXTENSION_ROOTS: readonly EditorExtensionRoot[];
|
|
14
|
+
/**
|
|
15
|
+
* Every per-user extension folder Roo Code can be installed into: VS Code,
|
|
16
|
+
* its forks, and their remote (SSH/WSL) servers. Used by repairs that fix the
|
|
17
|
+
* installed extension itself rather than writing editor settings.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ALL_EDITOR_EXTENSION_ROOTS: readonly EditorExtensionRoot[];
|
|
20
|
+
export interface RooExtensionDir {
|
|
21
|
+
editor: string;
|
|
22
|
+
/** Absolute extension folder, e.g. ~/.vscode/extensions/rooveterinaryinc.roo-cline-3.54.0 */
|
|
23
|
+
dir: string;
|
|
24
|
+
/** Version parsed from the folder name ("3.54.0"), when present. */
|
|
25
|
+
version?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Roo Code extension folders under the given roots; missing roots are skipped. */
|
|
28
|
+
export declare function findRooExtensionDirs(home: string, roots?: readonly EditorExtensionRoot[]): Promise<RooExtensionDir[]>;
|
|
2
29
|
export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
|
package/dist/detect/roo-code.js
CHANGED
|
@@ -3,20 +3,46 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import JSON5 from 'json5';
|
|
4
4
|
import { commandExists } from '../utils/exec.js';
|
|
5
5
|
import { clientConfigPath } from '../configure/paths.js';
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export const ROO_EXTENSION_PATTERN = /^rooveterinaryinc\.roo-cline-/i;
|
|
7
|
+
/**
|
|
8
|
+
* VS Code's own per-user extension folders. Detection stays limited to these
|
|
9
|
+
* because configure writes VS Code's settings.json, which forks do not read.
|
|
10
|
+
*/
|
|
11
|
+
export const VSCODE_EXTENSION_ROOTS = [
|
|
12
|
+
{ editor: 'VS Code', segments: ['.vscode', 'extensions'] },
|
|
13
|
+
{ editor: 'VS Code Insiders', segments: ['.vscode-insiders', 'extensions'] },
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Every per-user extension folder Roo Code can be installed into: VS Code,
|
|
17
|
+
* its forks, and their remote (SSH/WSL) servers. Used by repairs that fix the
|
|
18
|
+
* installed extension itself rather than writing editor settings.
|
|
19
|
+
*/
|
|
20
|
+
export const ALL_EDITOR_EXTENSION_ROOTS = [
|
|
21
|
+
...VSCODE_EXTENSION_ROOTS,
|
|
22
|
+
{ editor: 'Cursor', segments: ['.cursor', 'extensions'] },
|
|
23
|
+
{ editor: 'Windsurf', segments: ['.windsurf', 'extensions'] },
|
|
24
|
+
{ editor: 'VSCodium', segments: ['.vscode-oss', 'extensions'] },
|
|
25
|
+
{ editor: 'VS Code Server', segments: ['.vscode-server', 'extensions'] },
|
|
26
|
+
{ editor: 'Cursor Server', segments: ['.cursor-server', 'extensions'] },
|
|
27
|
+
{ editor: 'Trae', segments: ['.trae', 'extensions'] },
|
|
28
|
+
{ editor: 'Kiro', segments: ['.kiro', 'extensions'] },
|
|
29
|
+
];
|
|
30
|
+
/** Roo Code extension folders under the given roots; missing roots are skipped. */
|
|
31
|
+
export async function findRooExtensionDirs(home, roots = VSCODE_EXTENSION_ROOTS) {
|
|
9
32
|
const hits = [];
|
|
10
33
|
for (const root of roots) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
34
|
+
const rootDir = path.join(home, ...root.segments);
|
|
35
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true }).catch(() => []);
|
|
36
|
+
const names = entries.filter(entry => entry.isDirectory() && ROO_EXTENSION_PATTERN.test(entry.name)).map(entry => entry.name).sort();
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
const version = name.replace(ROO_EXTENSION_PATTERN, '');
|
|
39
|
+
hits.push({ editor: root.editor, dir: path.join(rootDir, name), ...(version ? { version } : {}) });
|
|
40
|
+
}
|
|
15
41
|
}
|
|
16
42
|
return hits;
|
|
17
43
|
}
|
|
18
44
|
export async function detect(ctx) {
|
|
19
|
-
const paths = await
|
|
45
|
+
const paths = (await findRooExtensionDirs(ctx.home)).map(hit => hit.dir);
|
|
20
46
|
const executable = await commandExists('code', ctx.env);
|
|
21
47
|
if (executable)
|
|
22
48
|
paths.unshift('command:code');
|
|
@@ -31,6 +57,6 @@ export async function detect(ctx) {
|
|
|
31
57
|
}
|
|
32
58
|
catch { }
|
|
33
59
|
}
|
|
34
|
-
const installed = paths.some(entry => entry === 'command:code' ||
|
|
60
|
+
const installed = paths.some(entry => entry === 'command:code' || ROO_EXTENSION_PATTERN.test(path.basename(entry)));
|
|
35
61
|
return { id: 'roo-code', name: 'Roo Code (VS Code)', installed, configured, executable, reason: installed ? undefined : 'Roo Code extension was not detected', paths };
|
|
36
62
|
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { updateCommand } from './commands/update.js';
|
|
|
13
13
|
import { removeCommand } from './commands/remove.js';
|
|
14
14
|
import { helperBotCommand } from './commands/helper-bot.js';
|
|
15
15
|
import { claudeToolsCommand } from './commands/claude-tools.js';
|
|
16
|
+
import { fixRooCommand } from './commands/fix-roo.js';
|
|
16
17
|
import { ensureInstallerApiKey } from './utils/installerConfig.js';
|
|
17
18
|
import { MIN_NODE, isNodeSupported, nodeUpgradeHint } from './utils/runtime.js';
|
|
18
19
|
import { runSystemAudit } from './utils/systemAudit.js';
|
|
@@ -38,6 +39,10 @@ function printHelp(command) {
|
|
|
38
39
|
console.log('Usage: scalemax helper-bot [--model <id>] ["question"]\n\nExecute safe ScaleMax setup and verification actions, or open the ScaleMax-only troubleshooting assistant.');
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
42
|
+
if (command === 'fix-roo') {
|
|
43
|
+
console.log('Usage: scalemax fix-roo [--undo]\n\nFix Roo Code tasks stuck at "API Request…" on recent VS Code versions ("Could not find ripgrep binary").\nPatches the installed Roo Code extension in your home folder (VS Code, Insiders, Cursor, Windsurf,\nVSCodium, Trae, Kiro, and remote servers) so it finds VS Code\'s relocated ripgrep. The VS Code app\nitself is not changed. The original extension file is kept next to it as extension.js.scalemax-orig.\nReload VS Code (Developer: Reload Window) afterwards.\n\n --undo Restore the original Roo Code extension files');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
41
46
|
const details = { update: 'Revalidate the API key and reconfigure every detected client.', remove: 'Restore backups or remove only ScaleMax-managed client configuration.', doctor: 'Validate the API key and verify detected client configuration.', models: 'List models available to the stored API key.', login: 'Store or replace the ScaleMax API key.', logout: 'Remove the locally stored ScaleMax API key.', 'claude-tools': 'Install and verify bounded public-GitHub skill tools for Claude Desktop/Cowork and Claude Code.' };
|
|
42
47
|
if (command && details[command]) {
|
|
43
48
|
console.log(`Usage: scalemax ${command}\n\n${details[command]}`);
|
|
@@ -55,6 +60,7 @@ Commands:
|
|
|
55
60
|
scalemax models List available models
|
|
56
61
|
scalemax helper-bot Execute setup actions or open the ScaleMax Helper
|
|
57
62
|
scalemax claude-tools Install bounded GitHub skill tools for Claude
|
|
63
|
+
scalemax fix-roo Fix Roo Code stuck at "API Request…" (recent VS Code)
|
|
58
64
|
scalemax update Reconfigure detected clients
|
|
59
65
|
scalemax remove Remove ScaleMax-generated configs
|
|
60
66
|
scalemax --version Show installed version
|
|
@@ -86,7 +92,8 @@ async function main() {
|
|
|
86
92
|
printHelp(command);
|
|
87
93
|
return;
|
|
88
94
|
}
|
|
89
|
-
|
|
95
|
+
const allowedOptions = { 'fix-roo': ['--undo'] };
|
|
96
|
+
if (command !== 'helper-bot' && args.some(arg => !(allowedOptions[command] || []).includes(arg)))
|
|
90
97
|
throw new Error(`Unknown option for ${command}: ${args.join(' ')}`);
|
|
91
98
|
switch (command) {
|
|
92
99
|
case 'init':
|
|
@@ -111,6 +118,9 @@ async function main() {
|
|
|
111
118
|
case 'claude-tools':
|
|
112
119
|
await claudeToolsCommand();
|
|
113
120
|
break;
|
|
121
|
+
case 'fix-roo':
|
|
122
|
+
await fixRooCommand(args);
|
|
123
|
+
break;
|
|
114
124
|
case 'update':
|
|
115
125
|
await updateCommand();
|
|
116
126
|
break;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roo Code (last release 3.54.0, 2026-05-15) finds ripgrep by probing fixed
|
|
3
|
+
* folders under `vscode.env.appRoot`. Its bundled dist/extension.js contains,
|
|
4
|
+
* minified (identifiers differ per build):
|
|
5
|
+
*
|
|
6
|
+
* async function fne(t){let e=async r=>{let a=Uwe.join(t,r,S8i);return await Ln(a)?a:void 0};
|
|
7
|
+
* return await e("node_modules/@vscode/ripgrep/bin/")||await e("node_modules/vscode-ripgrep/bin")||...}
|
|
8
|
+
*
|
|
9
|
+
* Newer VS Code releases (seen on 1.138 and 1.140) moved its ripgrep to
|
|
10
|
+
* `<appRoot>/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/<platform>-<arch>/rg[.exe]`,
|
|
11
|
+
* so every probe misses, Roo throws "Could not find ripgrep binary" and every
|
|
12
|
+
* task hangs at "API Request…". We patch the user's INSTALLED extension (home
|
|
13
|
+
* folder, no admin rights) to try the new folders first and then fall through
|
|
14
|
+
* to the original probes, so older VS Code keeps working. The VS Code app
|
|
15
|
+
* bundle is never touched.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ROO_RIPGREP_FIX_MARKER = "/*scalemax-rg-fix-1*/";
|
|
18
|
+
export declare const ROO_RIPGREP_BACKUP_SUFFIX = ".scalemax-orig";
|
|
19
|
+
export type RooRipgrepPatchStatus = 'patched' | 'already_patched' | 'unsupported';
|
|
20
|
+
export interface RooRipgrepPatch {
|
|
21
|
+
status: RooRipgrepPatchStatus;
|
|
22
|
+
/** The rewritten source, or the input unchanged when nothing was patched. */
|
|
23
|
+
source: string;
|
|
24
|
+
/** How many ripgrep lookups were rewritten. */
|
|
25
|
+
occurrences: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Pure rewrite of Roo's minified ripgrep lookup. New candidate folders come
|
|
29
|
+
* first so newer VS Code resolves immediately; the original probes follow.
|
|
30
|
+
* Idempotent: patched text no longer matches, and the marker reports it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function patchRooRipgrepSource(src: string): RooRipgrepPatch;
|
|
33
|
+
export interface RooExtensionScript {
|
|
34
|
+
editor: string;
|
|
35
|
+
/** Roo Code version parsed from the extension folder name. */
|
|
36
|
+
version?: string;
|
|
37
|
+
extensionDir: string;
|
|
38
|
+
/** Absolute path of the extension's dist/extension.js. */
|
|
39
|
+
script: string;
|
|
40
|
+
}
|
|
41
|
+
/** Installed Roo Code bundles across VS Code, its forks, and remote servers. */
|
|
42
|
+
export declare function findRooExtensionScripts(home: string): Promise<RooExtensionScript[]>;
|
|
43
|
+
/** "Roo Code 3.54.0 (VS Code)" */
|
|
44
|
+
export declare function rooInstallLabel(target: Pick<RooExtensionScript, 'editor' | 'version'>): string;
|
|
45
|
+
export type RooRipgrepRepairStatus = RooRipgrepPatchStatus | 'error';
|
|
46
|
+
export interface RooRipgrepRepairResult extends RooExtensionScript {
|
|
47
|
+
/** With `dryRun`, 'patched' means the bundle still needs the fix; nothing was written. */
|
|
48
|
+
status: RooRipgrepRepairStatus;
|
|
49
|
+
occurrences?: number;
|
|
50
|
+
/** Byte-exact copy of the bundle as it was before ScaleMax first patched it. */
|
|
51
|
+
backup?: string;
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface RooRipgrepRepairOptions {
|
|
55
|
+
/** Report what would change without writing anything (used by `scalemax doctor`). */
|
|
56
|
+
dryRun?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Patch every installed Roo Code bundle. Writes `extension.js.scalemax-orig`
|
|
60
|
+
* once (never overwritten), then replaces the bundle atomically. One bad file
|
|
61
|
+
* is reported, never thrown.
|
|
62
|
+
*/
|
|
63
|
+
export declare function repairRooRipgrep(home: string, opts?: RooRipgrepRepairOptions): Promise<RooRipgrepRepairResult[]>;
|
|
64
|
+
export type RooRipgrepRestoreStatus = 'restored' | 'not_patched' | 'no_backup' | 'mismatch' | 'error';
|
|
65
|
+
export interface RooRipgrepRestoreResult extends RooExtensionScript {
|
|
66
|
+
status: RooRipgrepRestoreStatus;
|
|
67
|
+
backup?: string;
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Undo `repairRooRipgrep` from the backup. Restores only when re-patching the
|
|
72
|
+
* backup reproduces the current bundle exactly, so a bundle changed since (or
|
|
73
|
+
* a damaged backup) is never clobbered. The backup is removed once restored.
|
|
74
|
+
*/
|
|
75
|
+
export declare function restoreRooRipgrep(home: string): Promise<RooRipgrepRestoreResult[]>;
|
|
76
|
+
/**
|
|
77
|
+
* One line for the Roo Code configure flow, or undefined when Roo Code is not
|
|
78
|
+
* installed in any editor. Never throws.
|
|
79
|
+
*/
|
|
80
|
+
export declare function rooRipgrepRepairNote(home: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import { atomicWrite } from '../utils/fs.js';
|
|
4
|
+
import { ALL_EDITOR_EXTENSION_ROOTS, findRooExtensionDirs } from '../detect/roo-code.js';
|
|
5
|
+
/**
|
|
6
|
+
* Roo Code (last release 3.54.0, 2026-05-15) finds ripgrep by probing fixed
|
|
7
|
+
* folders under `vscode.env.appRoot`. Its bundled dist/extension.js contains,
|
|
8
|
+
* minified (identifiers differ per build):
|
|
9
|
+
*
|
|
10
|
+
* async function fne(t){let e=async r=>{let a=Uwe.join(t,r,S8i);return await Ln(a)?a:void 0};
|
|
11
|
+
* return await e("node_modules/@vscode/ripgrep/bin/")||await e("node_modules/vscode-ripgrep/bin")||...}
|
|
12
|
+
*
|
|
13
|
+
* Newer VS Code releases (seen on 1.138 and 1.140) moved its ripgrep to
|
|
14
|
+
* `<appRoot>/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/<platform>-<arch>/rg[.exe]`,
|
|
15
|
+
* so every probe misses, Roo throws "Could not find ripgrep binary" and every
|
|
16
|
+
* task hangs at "API Request…". We patch the user's INSTALLED extension (home
|
|
17
|
+
* folder, no admin rights) to try the new folders first and then fall through
|
|
18
|
+
* to the original probes, so older VS Code keeps working. The VS Code app
|
|
19
|
+
* bundle is never touched.
|
|
20
|
+
*/
|
|
21
|
+
export const ROO_RIPGREP_FIX_MARKER = '/*scalemax-rg-fix-1*/';
|
|
22
|
+
export const ROO_RIPGREP_BACKUP_SUFFIX = '.scalemax-orig';
|
|
23
|
+
const LEGACY_LOOKUP = /return\s+await\s+([A-Za-z_$][\w$]*)\(\s*"node_modules\/@vscode\/ripgrep\/bin\/"\s*\)/g;
|
|
24
|
+
const PLATFORM_FOLDER = '"+process.platform+"-"+process.arch+"/';
|
|
25
|
+
function patchedLookup(probe) {
|
|
26
|
+
return `return await ${probe}(${ROO_RIPGREP_FIX_MARKER}"node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/${PLATFORM_FOLDER}")`
|
|
27
|
+
+ `||await ${probe}("node_modules/@vscode/ripgrep-universal/bin/${PLATFORM_FOLDER}")`
|
|
28
|
+
+ `||await ${probe}("node_modules/@vscode/ripgrep/bin/")`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pure rewrite of Roo's minified ripgrep lookup. New candidate folders come
|
|
32
|
+
* first so newer VS Code resolves immediately; the original probes follow.
|
|
33
|
+
* Idempotent: patched text no longer matches, and the marker reports it.
|
|
34
|
+
*/
|
|
35
|
+
export function patchRooRipgrepSource(src) {
|
|
36
|
+
let occurrences = 0;
|
|
37
|
+
const source = src.replace(LEGACY_LOOKUP, (_match, probe) => {
|
|
38
|
+
occurrences++;
|
|
39
|
+
return patchedLookup(probe);
|
|
40
|
+
});
|
|
41
|
+
if (occurrences)
|
|
42
|
+
return { status: 'patched', source, occurrences };
|
|
43
|
+
return { status: src.includes(ROO_RIPGREP_FIX_MARKER) ? 'already_patched' : 'unsupported', source: src, occurrences: 0 };
|
|
44
|
+
}
|
|
45
|
+
/** Installed Roo Code bundles across VS Code, its forks, and remote servers. */
|
|
46
|
+
export async function findRooExtensionScripts(home) {
|
|
47
|
+
const scripts = [];
|
|
48
|
+
for (const hit of await findRooExtensionDirs(home, ALL_EDITOR_EXTENSION_ROOTS)) {
|
|
49
|
+
const script = path.join(hit.dir, 'dist', 'extension.js');
|
|
50
|
+
const stat = await fsp.lstat(script).catch(() => undefined);
|
|
51
|
+
if (!stat)
|
|
52
|
+
continue;
|
|
53
|
+
scripts.push({ editor: hit.editor, ...(hit.version ? { version: hit.version } : {}), extensionDir: hit.dir, script });
|
|
54
|
+
}
|
|
55
|
+
return scripts;
|
|
56
|
+
}
|
|
57
|
+
/** "Roo Code 3.54.0 (VS Code)" */
|
|
58
|
+
export function rooInstallLabel(target) {
|
|
59
|
+
return `Roo Code${target.version ? ` ${target.version}` : ''} (${target.editor})`;
|
|
60
|
+
}
|
|
61
|
+
function errorMessage(error) {
|
|
62
|
+
return error instanceof Error ? error.message : String(error);
|
|
63
|
+
}
|
|
64
|
+
async function readRegularFile(file) {
|
|
65
|
+
const stat = await fsp.lstat(file);
|
|
66
|
+
if (!stat.isFile())
|
|
67
|
+
throw new Error(`refusing to modify a non-regular file: ${file}`);
|
|
68
|
+
return { bytes: await fsp.readFile(file), mode: stat.mode & 0o777 };
|
|
69
|
+
}
|
|
70
|
+
/** Bundles are handled as latin1 so every byte round-trips exactly; the inserted text is ASCII. */
|
|
71
|
+
const asText = (bytes) => bytes.toString('latin1');
|
|
72
|
+
const asBytes = (text) => Buffer.from(text, 'latin1');
|
|
73
|
+
async function writeBackupOnce(backup, bytes, mode) {
|
|
74
|
+
try {
|
|
75
|
+
await fsp.writeFile(backup, bytes, { flag: 'wx', mode });
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Never overwrite: an existing backup is the pristine pre-ScaleMax bundle.
|
|
79
|
+
if (error.code !== 'EEXIST')
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Patch every installed Roo Code bundle. Writes `extension.js.scalemax-orig`
|
|
85
|
+
* once (never overwritten), then replaces the bundle atomically. One bad file
|
|
86
|
+
* is reported, never thrown.
|
|
87
|
+
*/
|
|
88
|
+
export async function repairRooRipgrep(home, opts = {}) {
|
|
89
|
+
const results = [];
|
|
90
|
+
for (const target of await findRooExtensionScripts(home)) {
|
|
91
|
+
try {
|
|
92
|
+
const { bytes, mode } = await readRegularFile(target.script);
|
|
93
|
+
const patch = patchRooRipgrepSource(asText(bytes));
|
|
94
|
+
if (patch.status !== 'patched' || opts.dryRun) {
|
|
95
|
+
results.push({ ...target, status: patch.status, occurrences: patch.occurrences });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const backup = target.script + ROO_RIPGREP_BACKUP_SUFFIX;
|
|
99
|
+
await writeBackupOnce(backup, bytes, mode);
|
|
100
|
+
await atomicWrite(target.script, asBytes(patch.source), mode);
|
|
101
|
+
results.push({ ...target, status: 'patched', occurrences: patch.occurrences, backup });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
results.push({ ...target, status: 'error', error: errorMessage(error) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Undo `repairRooRipgrep` from the backup. Restores only when re-patching the
|
|
111
|
+
* backup reproduces the current bundle exactly, so a bundle changed since (or
|
|
112
|
+
* a damaged backup) is never clobbered. The backup is removed once restored.
|
|
113
|
+
*/
|
|
114
|
+
export async function restoreRooRipgrep(home) {
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const target of await findRooExtensionScripts(home)) {
|
|
117
|
+
const backup = target.script + ROO_RIPGREP_BACKUP_SUFFIX;
|
|
118
|
+
try {
|
|
119
|
+
const { bytes, mode } = await readRegularFile(target.script);
|
|
120
|
+
const current = asText(bytes);
|
|
121
|
+
if (!current.includes(ROO_RIPGREP_FIX_MARKER)) {
|
|
122
|
+
results.push({ ...target, status: 'not_patched' });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const original = await fsp.readFile(backup).catch((error) => {
|
|
126
|
+
if (error.code === 'ENOENT')
|
|
127
|
+
return undefined;
|
|
128
|
+
throw error;
|
|
129
|
+
});
|
|
130
|
+
if (!original) {
|
|
131
|
+
results.push({ ...target, status: 'no_backup' });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (patchRooRipgrepSource(asText(original)).source !== current) {
|
|
135
|
+
results.push({ ...target, status: 'mismatch', backup });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
await atomicWrite(target.script, original, mode);
|
|
139
|
+
await fsp.rm(backup, { force: true });
|
|
140
|
+
results.push({ ...target, status: 'restored', backup });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
results.push({ ...target, status: 'error', backup, error: errorMessage(error) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* One line for the Roo Code configure flow, or undefined when Roo Code is not
|
|
150
|
+
* installed in any editor. Never throws.
|
|
151
|
+
*/
|
|
152
|
+
export async function rooRipgrepRepairNote(home) {
|
|
153
|
+
let results;
|
|
154
|
+
try {
|
|
155
|
+
results = await repairRooRipgrep(home);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return `Roo Code ripgrep fix skipped (${errorMessage(error)}); run \`npx scalemax@latest fix-roo\` to retry.`;
|
|
159
|
+
}
|
|
160
|
+
if (!results.length)
|
|
161
|
+
return undefined;
|
|
162
|
+
const failed = results.filter(result => result.status === 'error');
|
|
163
|
+
if (failed.length)
|
|
164
|
+
return `Could not apply the Roo Code ripgrep fix (${failed[0].error}); run \`npx scalemax@latest fix-roo\` for details.`;
|
|
165
|
+
if (results.some(result => result.status === 'patched'))
|
|
166
|
+
return 'Patched Roo Code to find newer VS Code\'s ripgrep (fixes tasks stuck at "API Request…"); reload VS Code to apply.';
|
|
167
|
+
if (results.some(result => result.status === 'already_patched'))
|
|
168
|
+
return 'Roo Code\'s ripgrep fix for newer VS Code is already applied.';
|
|
169
|
+
return 'Roo Code\'s ripgrep lookup was not recognised, so no ripgrep fix was applied (this Roo Code version may not need it).';
|
|
170
|
+
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -75,6 +75,12 @@ export interface ConfigureResult {
|
|
|
75
75
|
message: string;
|
|
76
76
|
/** One-line usage advice printed after a successful configure (e.g. which agent mode can run tools). */
|
|
77
77
|
tip?: string;
|
|
78
|
+
/**
|
|
79
|
+
* One-line report of a side repair made while configuring (e.g. the Roo Code
|
|
80
|
+
* ripgrep fix). Printed right after the configure step, before verification,
|
|
81
|
+
* because the repair stands even if the configuration is later rolled back.
|
|
82
|
+
*/
|
|
83
|
+
note?: string;
|
|
78
84
|
/** Restore the exact state captured immediately before this configure call. */
|
|
79
85
|
rollback?: () => Promise<void>;
|
|
80
86
|
}
|
package/dist/utils/fs.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare function exists(p: string): Promise<boolean>;
|
|
2
2
|
export declare function syncParentDirectory(directory: string, platform?: NodeJS.Platform): Promise<void>;
|
|
3
|
-
export declare function atomicWrite(file: string, content: string, mode?: number, platform?: NodeJS.Platform): Promise<void>;
|
|
3
|
+
export declare function atomicWrite(file: string, content: string | Uint8Array, mode?: number, platform?: NodeJS.Platform): Promise<void>;
|
|
4
4
|
export declare function backupNameFor(file: string): string;
|
|
5
5
|
export declare function backupFile(file: string, home: string): Promise<string | undefined>;
|
|
6
6
|
export declare function listBackups(file: string, home: string): Promise<string[]>;
|
package/dist/utils/fs.js
CHANGED
|
@@ -34,7 +34,8 @@ export async function atomicWrite(file, content, mode = 0o600, platform = proces
|
|
|
34
34
|
let handle;
|
|
35
35
|
try {
|
|
36
36
|
handle = await fsp.open(tmp, 'wx', mode);
|
|
37
|
-
|
|
37
|
+
// Strings are written as UTF-8; byte content (e.g. a patched bundle) is written verbatim.
|
|
38
|
+
await handle.writeFile(content, typeof content === 'string' ? 'utf8' : undefined);
|
|
38
39
|
await handle.sync();
|
|
39
40
|
await handle.close();
|
|
40
41
|
handle = undefined;
|