scalemax 0.1.48 → 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 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>;
@@ -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
+ }
@@ -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
  }
@@ -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]);
@@ -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>;
@@ -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 extensionPattern = /^rooveterinaryinc\.roo-cline-/i;
7
- async function extensionPaths(home) {
8
- const roots = [path.join(home, '.vscode', 'extensions'), path.join(home, '.vscode-insiders', 'extensions')];
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 entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
12
- for (const entry of entries)
13
- if (entry.isDirectory() && extensionPattern.test(entry.name))
14
- hits.push(path.join(root, entry.name));
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 extensionPaths(ctx.home);
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' || extensionPattern.test(path.basename(entry)));
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
- if (command !== 'helper-bot' && args.length)
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
+ }
@@ -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
  }
@@ -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
- await handle.writeFile(content, 'utf8');
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scalemax",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "Universal ScaleMax installer for AI coding clients",
5
5
  "type": "module",
6
6
  "bin": {