scalemax 0.1.22 → 0.1.24

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
@@ -113,13 +113,17 @@ Or ask one small question and exit:
113
113
  npx scalemax@latest helper-bot "Why is my ScaleMax API key rejected in Codex?"
114
114
  ```
115
115
 
116
- The helper uses the same stored key as the installer, verifies authentication, detects the API-credit or token-pack base URL automatically, and selects a model available to that key. Each response is capped at 512 tokens and interactive history is bounded to the latest two turns. To choose a specific model from `scalemax models`, use:
116
+ The helper uses the same stored key as the installer, verifies authentication, and detects the API-credit or token-pack base URL automatically. General support questions default to `gpt-5.4` when that model is available to the key; each response is capped at 512 tokens and interactive history is bounded to the latest two turns. To choose a different model from `scalemax models`, use:
117
117
 
118
118
  ```bash
119
119
  npx scalemax@latest helper-bot --model fable-5 "Help me configure ScaleMax in Cursor"
120
120
  ```
121
121
 
122
- The helper is restricted to ScaleMax tasks, refuses unrelated requests, redacts ScaleMax-style secrets before requests and output, and never receives local shell or filesystem tools. Use `/help` inside the session for commands and `/quit` to exit.
122
+ Recognized setup, repair, and verification requests execute the CLI's allowlisted client operations directly. Requests such as `setup all providers`, `fix Hermes request failed`, `configure Claude Code`, `verify OpenCode`, and `setup vs code roo extension for claude` are handled by the controller before any model request. The controller detects the relevant client, runs a safe installer when the client has one, verifies existing ScaleMax configuration, repairs ScaleMax-managed files when verification fails, re-verifies, and reports each step.
123
+
124
+ For Roo Code, helper-bot can install VS Code on Windows with the fixed Microsoft winget package, install the official Roo Code extension, write OpenAI and Claude ScaleMax profiles when the key exposes both families, and verify the import handoff. For general troubleshooting that cannot be completed by a fixed client action, helper-bot sends a small `gpt-5.4` request grounded with bundled ScaleMax documentation about stored authentication, credit/token base URLs, provider scope, client setup, and common errors. The language model never generates or executes shell commands; only fixed ScaleMax client detection, installation, configuration, verification, and rollback operations are available to the controller.
125
+
126
+ The helper refuses unrelated requests and redacts ScaleMax-style secrets from prompts, provider output, errors, and progress notifications. Use `/status` for non-secret connection details, `/help` for commands, and `/quit` to exit.
123
127
 
124
128
  ## Supported clients
125
129
 
@@ -1,6 +1,7 @@
1
1
  import { type ChatCompletionOptions, type ChatCompletionResult, type ChatMessage, type ModelInfo } from '../utils/api.js';
2
2
  import { loadApiKey } from '../utils/keyStore.js';
3
3
  import { validateApiKey } from '../utils/api.js';
4
+ import type { ClientDefinition } from '../types/client.js';
4
5
  export declare const SCALEMAX_HELPER_REFUSAL: string;
5
6
  export declare const SCALEMAX_HELPER_SYSTEM_PROMPT: string;
6
7
  export declare const SCALEMAX_HELPER_INVALID_PROVIDER_RESPONSE: string;
@@ -16,6 +17,7 @@ export interface HelperConnection {
16
17
  providerScope?: 'openai' | 'anthropic' | 'both';
17
18
  keyMode?: string;
18
19
  availableModels?: string[];
20
+ modelCatalog?: ModelInfo[];
19
21
  }
20
22
  export type HelperCompletion = (options: ChatCompletionOptions) => Promise<ChatCompletionResult>;
21
23
  export interface HelperBotDependencies {
@@ -23,6 +25,7 @@ export interface HelperBotDependencies {
23
25
  validateKey: typeof validateApiKey;
24
26
  complete: HelperCompletion;
25
27
  write: (message: string) => void;
28
+ clientCatalog: ClientDefinition[];
26
29
  }
27
30
  export declare function parseHelperBotArgs(args: string[]): HelperBotArgs;
28
31
  export declare function redactHelperSecrets(value: unknown, activeKey?: string): string;
@@ -38,4 +41,6 @@ export declare function helperConnectionSummary(connection: HelperConnection): s
38
41
  export declare function localHelperReply(question: string, connection: HelperConnection): string | undefined;
39
42
  export declare function selectHelperModel(models: ModelInfo[], requested?: string): string;
40
43
  export declare function requestHelperReply(question: string, history: ChatMessage[], connection: HelperConnection, complete?: HelperCompletion): Promise<string>;
44
+ /** Execute only narrowly allowlisted ScaleMax client actions; never model-generated shell commands. */
45
+ export declare function executeHelperAction(question: string, connection: HelperConnection, home: string, clientCatalog?: ClientDefinition[], write?: (message: string) => void): Promise<string | undefined>;
41
46
  export declare function helperBotCommand(args?: string[], home?: string, dependencies?: Partial<HelperBotDependencies>): Promise<void>;
@@ -1,10 +1,15 @@
1
1
  import chalk from 'chalk';
2
+ import path from 'node:path';
2
3
  import { createInterface } from 'node:readline/promises';
3
- import { createChatCompletion } from '../utils/api.js';
4
+ import { execa } from 'execa';
5
+ import { clients } from '../clients.js';
6
+ import { helperDocsContext } from './helper-docs.js';
7
+ import { createChatCompletion, isAnthropicModel } from '../utils/api.js';
4
8
  import { redactApiKey } from '../utils/apiKey.js';
5
9
  import { loadApiKey } from '../utils/keyStore.js';
6
10
  import { homeDir } from '../utils/paths.js';
7
11
  import { validateApiKey } from '../utils/api.js';
12
+ import { commandExists } from '../utils/exec.js';
8
13
  export const SCALEMAX_HELPER_REFUSAL = `I am the official ScaleMax Helper.
9
14
 
10
15
  I can only assist with installing, configuring, repairing, verifying, and troubleshooting ScaleMax.
@@ -292,6 +297,8 @@ Never stop after only giving instructions if you can continue solving the issue.
292
297
 
293
298
  Always prefer automatic configuration.
294
299
 
300
+ When bundled ScaleMax documentation is supplied in the conversation, use it for exact ScaleMax configuration facts before relying on general model knowledge.
301
+
295
302
  Only ask the user to perform manual actions when automation is impossible.
296
303
 
297
304
  If multiple configuration methods exist:
@@ -531,13 +538,17 @@ const DEFAULT_MAX_TOKENS = 512;
531
538
  const SECRET_PATTERN = /\b(?:sm_(?:live|test)_[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9_-]{12,})\b/g;
532
539
  const PROMPT_INJECTION_PATTERN = /\b(?:ignore|override|bypass|disable|reveal|show|print|expose)\b.{0,80}\b(?:system|hidden|previous|developer|instruction|prompt|rule|secret)\b/i;
533
540
  const CLEARLY_UNRELATED_PATTERN = /\b(?:homework|poem|story|roleplay|translate|translation|medical|legal|politics|religion|dating|entertainment|game|malware|hacking|reverse\s+engineering|weather|sports|recipe|joke)\b/i;
534
- const SCALEMAX_TOPIC_PATTERN = /\b(?:scalemax|api\s*key|base\s*url|endpoint|provider|model|token|credit|billing|pricing|authentication|configuration|configure|install|update|repair|verify|health\s*check|environment\s*variable|vscode|visual\s+studio\s+code|cursor|codex(?:\s+cli)?|claude(?:\s+code)?|opencode|openclaw|roo\s+code|continue|windsurf|hermes|cherry\s+studio|api\s+code)\b/i;
541
+ const SCALEMAX_TOPIC_PATTERN = /\b(?:scalemax|api\s*key|stored\s+key|customer\s+key|base\s*url|endpoint|provider|openai|anthropic|model|fable|token|credit|billing|pricing|authentication|login|log\s+in|configuration|configure|setup|set\s*up|install|update|repair|verify|health\s*check|environment\s*variable|vscode|visual\s+studio\s+code|vs\s*code\s+extension|cursor|codex(?:\s+cli)?|claude(?:\s+code)?|opencode|openclaw|roo(?:\s+code|\s+extension)?|continue|windsurf|hermes|cherry\s+studio|api\s+code)\b/i;
535
542
  const TROUBLESHOOTING_PATTERN = /\b(?:error|failed?|failure|broken|not\s+working|doesn['’]?t\s+work|cannot|can['’]?t|timeout|invalid|unauthorized|forbidden|help)\b/i;
536
543
  const GREETING_PATTERN = /^(?:hi|hello|hey|help|what can you do)[!.?\s]*$/i;
537
544
  const CLAUDE_DESKTOP_PATTERN = /\bclaude\s+desktop\b/i;
538
545
  const CLAUDE_DESKTOP_CONFIGURATION_PATTERN = /\b(?:configur(?:e|ation)|setup|set\s*up|gateway|base\s*url|api\s*key|connect(?:ion)?|third[- ]party|inference|field|install|verify)\b/i;
539
546
  const CODEX_CLI_PATTERN = /\bcodex(?:\s+cli)?\b/i;
540
547
  const CLIENT_CONFIGURATION_PATTERN = /\b(?:configur(?:e|ation)|setup|set\s*up|base\s*url|api\s*key|connect(?:ion)?|provider|install|repair|verify|update)\b/i;
548
+ const STORED_KEY_EXPLAIN_PATTERN = /\b(?:why\s+(?:did\s+)?i\s+(?:login|log\s+in)|helper\b.{0,80}\b(?:customer|stored)\s+key|(?:customer|stored)\s+key\b.{0,80}\bhelper|does\s+(?:it|helper)\s+use\s+(?:my\s+)?(?:api\s+)?key)\b/i;
549
+ const HELPER_MUTATION_PATTERN = /(?:^|\b(?:please|can\s+you|could\s+you|help\s+me(?:\s+to)?|i\s+(?:want|need)\s+you\s+to|how\s+(?:do|can)\s+i)\s+)(?:set\s*up|setup|configure|connect|install|repair|fix|update|reconfigure|make\b.{0,80}\bwork)\b/i;
550
+ const HELPER_VERIFICATION_PATTERN = /(?:^|\b(?:please|can\s+you|could\s+you|help\s+me(?:\s+to)?|i\s+(?:want|need)\s+you\s+to)\s+)(?:verify|check|test|diagnose)\b/i;
551
+ const ALL_CLIENTS_PATTERN = /\b(?:all|every)\b.{0,30}\b(?:client|provider|integration|app)s?\b/i;
541
552
  const KNOWN_CONTEXT_REASK_PATTERN = /(?:\b(?:need|send|provide|share|tell)\b.{0,100}\b(?:api\s*(?:key|endpoint)|base\s*url|operating\s+system|\bos\b|model\s+(?:name|catalog))\b|YOUR-SCALEMAX-ENDPOINT|your-key)/is;
542
553
  const PROVIDER_IDENTITY_OVERRIDE_PATTERNS = [
543
554
  /\bkiro\b/i,
@@ -558,7 +569,8 @@ const defaultDependencies = {
558
569
  loadKey: loadApiKey,
559
570
  validateKey: validateApiKey,
560
571
  complete: createChatCompletion,
561
- write: message => console.log(message)
572
+ write: message => console.log(message),
573
+ clientCatalog: clients
562
574
  };
563
575
  export function parseHelperBotArgs(args) {
564
576
  let model;
@@ -630,9 +642,9 @@ Mandatory behavior:
630
642
 
631
643
  1. Treat the stored key, base URLs, operating system, provider scope, and model catalog as already known. Do not request them again.
632
644
  2. Never output placeholders such as YOUR-SCALEMAX-ENDPOINT or your-key when an exact known value can be used.
633
- 3. Prefer the automatic CLI workflow. For an installed supported client, tell the user to exit the Helper with /quit, run \`scalemax update\`, then run \`scalemax doctor\` to verify it.
645
+ 3. The CLI controller executes recognized safe configuration and verification requests before asking the model. Do not claim that an unrecognized action was executed.
634
646
  4. The Helper request itself uses the stored customer key and therefore consumes the small number of tokens used by the selected Helper model. The key is never included in the prompt text.
635
- 5. Do not claim you executed terminal commands. The Helper has no shell tools; give the shortest exact ScaleMax command sequence when user action is required.`;
647
+ 5. When user action is genuinely required, give the shortest exact instruction and clearly distinguish it from work already completed by the CLI controller.`;
636
648
  }
637
649
  export function helperConnectionSummary(connection) {
638
650
  return `Authenticated ${helperBillingMode(connection)} key · ${helperPlatformName(connection.platform)} · ${connection.providerScope || 'both'} scope · ${connection.baseUrl} · helper model ${connection.model}`;
@@ -643,6 +655,18 @@ export function helperConnectionSummary(connection) {
643
655
  * field values matter more than generated prose.
644
656
  */
645
657
  export function localHelperReply(question, connection) {
658
+ if (STORED_KEY_EXPLAIN_PATTERN.test(question)) {
659
+ return `ScaleMax Helper uses the key saved by \`scalemax login\`.
660
+
661
+ - The key is validated before Helper starts.
662
+ - The Helper's own small replies use the stored customer key with the selected low-cost model, usually \`${connection.model}\`.
663
+ - Client setup actions reuse the same stored key in the target client's private config.
664
+ - The key is never pasted into the prompt, displayed in chat, or requested again after login.
665
+
666
+ Authenticated base URL: ${connection.baseUrl}
667
+ Billing mode: ${helperBillingMode(connection)}
668
+ Provider scope: ${connection.providerScope || 'both'}`;
669
+ }
646
670
  if (CLAUDE_DESKTOP_PATTERN.test(question) && CLAUDE_DESKTOP_CONFIGURATION_PATTERN.test(question)) {
647
671
  const gatewayBaseUrl = claudeDesktopGatewayBaseUrl(connection.baseUrl);
648
672
  return `ScaleMax Helper detected and authenticated the stored ${helperBillingMode(connection)} key.
@@ -685,7 +709,7 @@ export function selectHelperModel(models, requested) {
685
709
  throw new Error(`Model "${requested}" is not available to this API key. Run \`scalemax models\` to see available models.`);
686
710
  return exact.id;
687
711
  }
688
- const preferred = ['gpt-5.6-luna', 'claude-haiku-4-5-20251001', 'fable-5', 'gpt-5.6-terra', 'gpt-5.6-sol'];
712
+ const preferred = ['gpt-5.4', 'gpt-5.4-mini', 'claude-haiku-4-5-20251001', 'gpt-5.6-luna', 'fable-5', 'gpt-5.6-terra', 'gpt-5.6-sol'];
689
713
  for (const id of preferred) {
690
714
  const match = models.find(model => model.id.toLowerCase() === id);
691
715
  if (match)
@@ -705,6 +729,7 @@ export async function requestHelperReply(question, history, connection, complete
705
729
  if (localReply)
706
730
  return localReply;
707
731
  const recentHistory = history.slice(-MAX_HISTORY_MESSAGES).map(message => ({ ...message, content: redactHelperSecrets(message.content, connection.apiKey) }));
732
+ const docsContext = helperDocsContext(cleanQuestion);
708
733
  const result = await complete({
709
734
  apiKey: connection.apiKey,
710
735
  baseUrl: connection.baseUrl,
@@ -713,6 +738,7 @@ export async function requestHelperReply(question, history, connection, complete
713
738
  messages: [
714
739
  { role: 'system', content: SCALEMAX_HELPER_SYSTEM_PROMPT },
715
740
  { role: 'system', content: helperRuntimeContext(connection) },
741
+ ...(docsContext ? [{ role: 'system', content: docsContext }] : []),
716
742
  ...recentHistory,
717
743
  { role: 'user', content: cleanQuestion }
718
744
  ]
@@ -731,8 +757,260 @@ For automatic configuration, exit with \`/quit\`, run \`scalemax update\`, then
731
757
  }
732
758
  return reply;
733
759
  }
760
+ const HELPER_CLIENT_PATTERNS = [
761
+ { id: 'roo-code', pattern: /\b(?:roo(?:\s+code)?|roo\s+(?:extension|plugin)|vscode\s+roo|vs\s+code\s+roo|vs\s*code\s+extension|visual\s+studio\s+code\s+extension)\b/i },
762
+ { id: 'openclaw', pattern: /\bopen\s*claw\b/i },
763
+ { id: 'opencode', pattern: /\bopen\s*code\b/i },
764
+ { id: 'cherry-studio', pattern: /\bcherry\s*studio\b/i },
765
+ { id: 'api-code', pattern: /\bapi\s*code\b/i },
766
+ { id: 'trae-solo', pattern: /\btrae(?:\s+solo)?\b/i },
767
+ { id: 'cursor', pattern: /\bcursor\b/i },
768
+ { id: 'hermes', pattern: /\bhermes\b/i },
769
+ { id: 'codex', pattern: /\bcodex(?:\s+cli)?\b/i },
770
+ { id: 'claude', pattern: /\bclaude(?:\s+code|\s+cli)?\b/i }
771
+ ];
772
+ function helperActionKind(question) {
773
+ if (/\b(?:repair|fix|troubleshoot|diagnose|not\s+working|doesn['’]?t\s+work|broken|failed?|invalid|timeout|rate\s*limit(?:ed)?)\b/i.test(question))
774
+ return 'repair';
775
+ if (HELPER_MUTATION_PATTERN.test(question))
776
+ return 'configure';
777
+ if (HELPER_VERIFICATION_PATTERN.test(question))
778
+ return 'verify';
779
+ return undefined;
780
+ }
781
+ function helperActionTarget(question) {
782
+ if (CLAUDE_DESKTOP_PATTERN.test(question))
783
+ return undefined;
784
+ if (ALL_CLIENTS_PATTERN.test(question) || /\b(?:all|every)\b.{0,30}\bproviders?\b/i.test(question))
785
+ return 'all';
786
+ const explicit = HELPER_CLIENT_PATTERNS.find(candidate => candidate.pattern.test(question))?.id;
787
+ if (explicit)
788
+ return explicit;
789
+ if (/\b(?:clients?|integrations?|apps?|providers?)\b/i.test(question) && /\b(?:scalemax|configure|setup|set\s*up|repair|fix|update)\b/i.test(question))
790
+ return 'all';
791
+ return undefined;
792
+ }
793
+ function requestedProviderScope(question, target) {
794
+ if (/\b(?:both|openai\s*\+\s*claude|claude\s*\+\s*openai)\b/i.test(question))
795
+ return 'both';
796
+ if (/\b(?:claude|anthropic)\b/i.test(question))
797
+ return 'anthropic';
798
+ if (/\b(?:openai|chatgpt|gpt)\b/i.test(question) || target === 'codex')
799
+ return 'openai';
800
+ return undefined;
801
+ }
802
+ function actionModels(connection, requestedScope) {
803
+ const catalog = connection.modelCatalog?.length
804
+ ? connection.modelCatalog
805
+ : (connection.availableModels || []).map(id => ({ id }));
806
+ if (!requestedScope || requestedScope === 'both')
807
+ return catalog;
808
+ return catalog.filter(model => requestedScope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model));
809
+ }
810
+ function helperClientAllowedForModels(clientId, models) {
811
+ if (!models.length)
812
+ return false;
813
+ const crossProtocolClients = new Set(['claude', 'cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
814
+ if (crossProtocolClients.has(clientId))
815
+ return true;
816
+ return models.some(model => !isAnthropicModel(model));
817
+ }
818
+ function hasRooCodeExtension(paths) {
819
+ return paths.some(candidate => /^rooveterinaryinc\.roo-cline-/i.test(path.basename(candidate)));
820
+ }
821
+ function pathEnvKey(env) {
822
+ return Object.keys(env).find(key => key.toLowerCase() === 'path') || 'PATH';
823
+ }
824
+ function windowsVSCodeBinPaths(ctx) {
825
+ const localAppData = ctx.env.LOCALAPPDATA || path.win32.join(ctx.home, 'AppData', 'Local');
826
+ const programFiles = ctx.env.ProgramFiles || 'C:\\Program Files';
827
+ const programFilesX86 = ctx.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
828
+ return [
829
+ path.win32.join(localAppData, 'Programs', 'Microsoft VS Code', 'bin'),
830
+ path.win32.join(programFiles, 'Microsoft VS Code', 'bin'),
831
+ path.win32.join(programFilesX86, 'Microsoft VS Code', 'bin')
832
+ ];
833
+ }
834
+ function refreshWindowsVSCodePath(ctx) {
835
+ if (ctx.platform !== 'win32')
836
+ return;
837
+ const key = pathEnvKey(ctx.env);
838
+ const delimiter = ';';
839
+ const existing = new Set((ctx.env[key] || '').split(delimiter).filter(Boolean).map(item => item.toLowerCase()));
840
+ const additions = windowsVSCodeBinPaths(ctx).filter(candidate => !existing.has(candidate.toLowerCase()));
841
+ if (additions.length)
842
+ ctx.env[key] = [...additions, ctx.env[key] || ''].filter(Boolean).join(delimiter);
843
+ }
844
+ async function installVSCodeOnWindows(ctx, write) {
845
+ if (ctx.platform !== 'win32')
846
+ return;
847
+ refreshWindowsVSCodePath(ctx);
848
+ if (await commandExists('code', ctx.env))
849
+ return;
850
+ if (!(await commandExists('winget', ctx.env))) {
851
+ throw new Error('VS Code and the Roo Code extension were not detected. Install VS Code first, then retry this request.');
852
+ }
853
+ write('→ VS Code was not detected; installing Microsoft Visual Studio Code with winget…');
854
+ const result = await execa('winget', [
855
+ 'install',
856
+ '--id',
857
+ 'Microsoft.VisualStudioCode',
858
+ '--exact',
859
+ '--accept-package-agreements',
860
+ '--accept-source-agreements',
861
+ '--silent'
862
+ ], {
863
+ env: ctx.env,
864
+ reject: false,
865
+ all: true
866
+ });
867
+ if (result.exitCode !== 0)
868
+ throw new Error(`VS Code installation failed: ${result.all?.trim() || `winget exited with ${result.exitCode}`}`);
869
+ refreshWindowsVSCodePath(ctx);
870
+ if (!(await commandExists('code', ctx.env)))
871
+ throw new Error('VS Code installed, but the code command was not detected. Restart the terminal and retry this request.');
872
+ write('✓ VS Code installed and detected.');
873
+ }
874
+ async function installRooCodeExtension(ctx, client, write) {
875
+ let detection = await client.detect(ctx);
876
+ if (hasRooCodeExtension(detection.paths))
877
+ return;
878
+ if (!detection.executable && ctx.platform === 'win32') {
879
+ await installVSCodeOnWindows(ctx, write);
880
+ detection = await client.detect(ctx);
881
+ }
882
+ if (!detection.executable)
883
+ throw new Error('VS Code and the Roo Code extension were not detected. Install VS Code first, then retry this request.');
884
+ write('→ Installing the official Roo Code extension in VS Code…');
885
+ const result = await execa('code', ['--install-extension', 'RooVeterinaryInc.roo-cline', '--force'], {
886
+ env: ctx.env,
887
+ reject: false,
888
+ all: true
889
+ });
890
+ if (result.exitCode !== 0)
891
+ throw new Error(`Roo Code extension installation failed: ${result.all?.trim() || `code exited with ${result.exitCode}`}`);
892
+ detection = await client.detect(ctx);
893
+ if (!hasRooCodeExtension(detection.paths))
894
+ throw new Error('VS Code reported a successful Roo Code installation, but the extension could not be detected afterward. Restart VS Code and retry.');
895
+ write('✓ Roo Code extension installed and detected.');
896
+ }
897
+ async function runOneHelperClientAction(kind, client, connection, home, requestedScope, write) {
898
+ const models = actionModels(connection, requestedScope);
899
+ if (!models.length)
900
+ throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : 'compatible'} models for ${client.name}. No configuration was changed.`);
901
+ if (client.id === 'codex' && models.every(isAnthropicModel))
902
+ throw new Error('Codex requires an OpenAI-compatible model, but this key exposes only Claude models. No configuration was changed.');
903
+ if (client.id === 'cursor' && connection.platform !== 'darwin')
904
+ throw new Error(`Automatic Cursor configuration is currently verified on macOS only. ${helperPlatformName(connection.platform)} was not modified.`);
905
+ const ctx = {
906
+ home,
907
+ platform: connection.platform || process.platform,
908
+ env: { ...process.env },
909
+ apiKey: connection.apiKey,
910
+ baseUrl: connection.baseUrl,
911
+ providerScope: requestedScope || connection.providerScope || 'both',
912
+ models,
913
+ interactive: false,
914
+ allowAppModification: false
915
+ };
916
+ if (kind !== 'verify' && client.install) {
917
+ write(`→ Checking ${client.name} installer…`);
918
+ const installed = await client.install({ home, platform: ctx.platform, env: ctx.env, interactive: Boolean(process.stdin.isTTY) });
919
+ write(`✓ ${client.name} ${installed.message}`);
920
+ }
921
+ write(`→ Detecting ${client.name}…`);
922
+ let detection = await client.detect(ctx);
923
+ if ((kind === 'configure' || kind === 'repair') && client.id === 'roo-code') {
924
+ await installRooCodeExtension(ctx, client, write);
925
+ detection = await client.detect(ctx);
926
+ }
927
+ if (!detection.installed && client.id !== 'api-code')
928
+ throw new Error(`${client.name} was not detected. No configuration was changed.`);
929
+ write(client.id === 'api-code' && !detection.installed ? `✓ ${client.name} output path is available.` : `✓ ${client.name} detected.`);
930
+ if (kind === 'verify') {
931
+ if (!client.verify) {
932
+ if (!detection.configured)
933
+ throw new Error(`${client.name} is installed but its ScaleMax configuration was not detected.`);
934
+ return `✓ ${client.name} ScaleMax configuration is present.`;
935
+ }
936
+ write(`→ Verifying ${client.name}…`);
937
+ const verified = await client.verify(ctx);
938
+ return `✓ ${client.name} ${verified.message}`;
939
+ }
940
+ if (kind === 'repair' && client.verify) {
941
+ write(`→ Diagnosing ${client.name} configuration…`);
942
+ try {
943
+ const verified = await client.verify(ctx);
944
+ return `✓ ${client.name} is already healthy: ${verified.message}`;
945
+ }
946
+ catch (error) {
947
+ write(`→ ${client.name} verification failed; repairing ScaleMax-managed configuration…`);
948
+ }
949
+ }
950
+ write(`→ Configuring ${client.name} with the authenticated stored key…`);
951
+ const configured = await client.configure(ctx);
952
+ write(`✓ ${client.name} ${configured.message}`);
953
+ if (client.verify) {
954
+ write(`→ Verifying ${client.name} configuration…`);
955
+ try {
956
+ const verified = await client.verify(ctx);
957
+ write(`✓ ${client.name} ${verified.message}`);
958
+ }
959
+ catch (error) {
960
+ if (configured.changed) {
961
+ write(`→ Verification failed; removing the incomplete ScaleMax-managed ${client.name} configuration…`);
962
+ await client.remove(ctx).catch(() => undefined);
963
+ }
964
+ throw error;
965
+ }
966
+ }
967
+ else {
968
+ detection = await client.detect(ctx);
969
+ if (!detection.configured)
970
+ throw new Error(`${client.name} configuration was written but could not be detected afterward.`);
971
+ write(`✓ ${client.name} configuration detected after write.`);
972
+ }
973
+ const activation = client.id === 'roo-code' ? ' Reload VS Code once so Roo Code imports the verified ScaleMax profile.' : '';
974
+ return `✅ Successfully completed: ${client.name} is configured and locally verified.${activation}`;
975
+ }
976
+ /** Execute only narrowly allowlisted ScaleMax client actions; never model-generated shell commands. */
977
+ export async function executeHelperAction(question, connection, home, clientCatalog = clients, write = message => console.log(message)) {
978
+ const cleanQuestion = redactHelperSecrets(question.trim(), connection.apiKey);
979
+ if (!isScaleMaxHelperQuestion(cleanQuestion))
980
+ return undefined;
981
+ const kind = helperActionKind(cleanQuestion);
982
+ const target = helperActionTarget(cleanQuestion);
983
+ if (!kind || !target)
984
+ return undefined;
985
+ const scope = requestedProviderScope(cleanQuestion, target);
986
+ const scopedModels = actionModels(connection, scope);
987
+ const selected = target === 'all'
988
+ ? clientCatalog.filter(client => client.id !== 'cursor' && helperClientAllowedForModels(client.id, scopedModels))
989
+ : clientCatalog.filter(client => client.id === target);
990
+ if (!selected.length)
991
+ throw new Error('The requested ScaleMax integration is not supported by this CLI build.');
992
+ if (target !== 'all' && !helperClientAllowedForModels(selected[0].id, scopedModels))
993
+ throw new Error(`The authenticated key does not expose models compatible with ${selected[0].name}. No configuration was changed.`);
994
+ write(`✓ Stored ${helperBillingMode(connection)} key is authenticated; secrets remain hidden.`);
995
+ const completed = [];
996
+ const failures = [];
997
+ for (const client of selected) {
998
+ try {
999
+ completed.push(await runOneHelperClientAction(kind, client, connection, home, scope, write));
1000
+ }
1001
+ catch (error) {
1002
+ const message = redactHelperSecrets(error instanceof Error ? error.message : String(error), connection.apiKey);
1003
+ if (target !== 'all')
1004
+ throw new Error(message);
1005
+ failures.push(`${client.name}: ${message}`);
1006
+ }
1007
+ }
1008
+ if (!completed.length)
1009
+ throw new Error(`No ScaleMax client action completed.\n${failures.map(item => `- ${item}`).join('\n')}`);
1010
+ return `${completed.join('\n')}${failures.length ? `\n\nSkipped or failed:\n${failures.map(item => `- ${item}`).join('\n')}` : ''}`;
1011
+ }
734
1012
  function printSessionHelp(write) {
735
- write('Commands: /help shows this help; /status shows the authenticated non-secret connection details; /quit exits. Ask only ScaleMax installation, configuration, authentication, or troubleshooting questions.');
1013
+ write('Commands: /help shows this help; /status shows the authenticated non-secret connection details; /quit exits. Setup, repair, and verification requests for supported clients are executed automatically with progress notifications.');
736
1014
  }
737
1015
  export async function helperBotCommand(args = [], home = homeDir(), dependencies = {}) {
738
1016
  const deps = { ...defaultDependencies, ...dependencies };
@@ -751,10 +1029,15 @@ export async function helperBotCommand(args = [], home = homeDir(), dependencies
751
1029
  platform: process.platform,
752
1030
  providerScope: validation.key?.provider_scope || 'both',
753
1031
  keyMode: validation.key?.mode,
754
- availableModels: validation.models.map(item => item.id)
1032
+ availableModels: validation.models.map(item => item.id),
1033
+ modelCatalog: validation.models
1034
+ };
1035
+ const respond = async (question, history) => {
1036
+ const action = await executeHelperAction(question, connection, home, deps.clientCatalog, deps.write);
1037
+ return action || requestHelperReply(question, history, connection, deps.complete);
755
1038
  };
756
1039
  if (options.question) {
757
- deps.write(await requestHelperReply(options.question, [], connection, deps.complete));
1040
+ deps.write(await respond(options.question, []));
758
1041
  return;
759
1042
  }
760
1043
  if (!process.stdin.isTTY)
@@ -787,7 +1070,7 @@ export async function helperBotCommand(args = [], home = homeDir(), dependencies
787
1070
  continue;
788
1071
  }
789
1072
  try {
790
- const reply = await requestHelperReply(input, history, connection, deps.complete);
1073
+ const reply = await respond(input, history);
791
1074
  deps.write(reply);
792
1075
  if (reply !== SCALEMAX_HELPER_REFUSAL) {
793
1076
  history.push({ role: 'user', content: redactHelperSecrets(input, key) }, { role: 'assistant', content: reply });
@@ -0,0 +1,9 @@
1
+ export interface HelperDocSection {
2
+ id: string;
3
+ title: string;
4
+ keywords: string[];
5
+ content: string;
6
+ }
7
+ export declare const HELPER_DOC_SECTIONS: HelperDocSection[];
8
+ export declare function selectHelperDocs(question: string, limit?: number): HelperDocSection[];
9
+ export declare function helperDocsContext(question: string): string | undefined;
@@ -0,0 +1,70 @@
1
+ export const HELPER_DOC_SECTIONS = [
2
+ {
3
+ id: 'stored-auth',
4
+ title: 'Stored ScaleMax authentication',
5
+ keywords: ['login', 'authenticate', 'authentication', 'stored key', 'api key', 'customer key', 'why login', 'base url'],
6
+ content: `After scalemax login succeeds, the CLI stores the customer's ScaleMax API key privately and validates it before helper-bot starts. The helper must not ask the user to paste the key, base URL, operating system, or model catalog again. The helper uses the stored customer key for its own small ScaleMax model requests and for supported client configuration files. The key is never inserted into prompts or displayed in chat.`
7
+ },
8
+ {
9
+ id: 'credit-token-urls',
10
+ title: 'API credit and API token base URLs',
11
+ keywords: ['credit', 'token', 'base url', 'endpoint', 'renew', 'billing', 'api credit', 'api token'],
12
+ content: `ScaleMax API credit keys use https://api.scalemax.pro/v1. ScaleMax API token budget keys use https://api.scalemax.pro/token/v1. Claude Desktop gateway configuration removes the trailing /v1, so credit keys use https://api.scalemax.pro and token keys use https://api.scalemax.pro/token. Do not mix credit and token base URLs; the authenticated key validation result is authoritative.`
13
+ },
14
+ {
15
+ id: 'provider-scope',
16
+ title: 'Provider scope and visible models',
17
+ keywords: ['provider', 'scope', 'openai', 'claude', 'anthropic', 'both', 'models', 'fable'],
18
+ content: `A ScaleMax key may be OpenAI only, Claude only, or both. OpenAI-only keys should configure OpenAI-compatible models only. Claude-only keys should configure Claude-compatible models only. Both-scope keys may configure both model families in clients that can switch profiles. Fable 5 is a Claude-family frontend model through ScaleMax and should only appear for Claude-capable or both-scope keys, not OpenAI-only keys.`
19
+ },
20
+ {
21
+ id: 'roo-code',
22
+ title: 'Roo Code setup through VS Code',
23
+ keywords: ['roo', 'roo code', 'vscode', 'vs code', 'extension', 'visual studio code'],
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. 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
+ },
26
+ {
27
+ id: 'codex-and-clients',
28
+ title: 'Supported client update flow',
29
+ keywords: ['codex', 'cursor', 'claude code', 'opencode', 'openclaw', 'hermes', 'cherry studio', 'update', 'doctor'],
30
+ content: `scalemax update detects supported clients and configures them from the stored key. scalemax doctor validates the stored key and performs a minimal ScaleMax request. Supported clients include Codex CLI, Cursor, Claude Code, OpenCode, OpenClaw, Hermes, Cherry Studio, Roo Code, and API Code. Recognized helper-bot setup and verification requests are executed by the CLI controller before any model fallback is used.`
31
+ },
32
+ {
33
+ id: 'troubleshooting',
34
+ title: 'Common troubleshooting',
35
+ keywords: ['error', 'failed', 'invalid', 'rate limit', 'timeout', 'kiro', 'request failed', 'provider returned invalid response'],
36
+ content: `If a provider response claims to be Kiro, Claude, ChatGPT, Codex, or says it cannot act as ScaleMax Helper, the helper must reject that response and not show the provider persona to the user. For timeouts or rate limits, retry with a tiny request and prefer the lowest-cost available helper model. For invalid key errors, re-run scalemax login and then scalemax doctor. For a client that still shows old settings, reload or restart that client after scalemax update.`
37
+ }
38
+ ];
39
+ function words(value) {
40
+ return [...new Set(value.toLowerCase().match(/[a-z0-9]+(?:-[a-z0-9]+)?/g) || [])];
41
+ }
42
+ function scoreSection(questionWords, section) {
43
+ const haystack = words(`${section.title} ${section.keywords.join(' ')} ${section.content}`);
44
+ let score = 0;
45
+ for (const word of haystack)
46
+ if (questionWords.has(word))
47
+ score += section.keywords.includes(word) ? 4 : 1;
48
+ return score;
49
+ }
50
+ export function selectHelperDocs(question, limit = 3) {
51
+ const questionWords = new Set(words(question));
52
+ if (!questionWords.size)
53
+ return [];
54
+ return HELPER_DOC_SECTIONS
55
+ .map(section => ({ section, score: scoreSection(questionWords, section) }))
56
+ .filter(item => item.score > 1)
57
+ .sort((a, b) => b.score - a.score || a.section.id.localeCompare(b.section.id))
58
+ .slice(0, limit)
59
+ .map(item => item.section);
60
+ }
61
+ export function helperDocsContext(question) {
62
+ const sections = selectHelperDocs(question);
63
+ if (!sections.length)
64
+ return undefined;
65
+ return `# SCALEMAX DOCUMENTATION CONTEXT
66
+
67
+ Use this bundled ScaleMax documentation for exact configuration facts. If it does not fully answer the request, continue from the authenticated runtime context and ScaleMax Helper rules.
68
+
69
+ ${sections.map(section => `## ${section.title}\n${section.content}`).join('\n\n')}`;
70
+ }
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
3
3
  import JSON5 from 'json5';
4
4
  import { atomicWrite, backupFile } from '../utils/fs.js';
5
5
  import { clientConfigPath } from './paths.js';
6
- import { openAiBaseUrl, preferredChatCompletionsModel } from './models.js';
6
+ import { anthropicModelIds, openAiBaseUrl, openAiModelIds, preferredAnthropicModel, preferredChatCompletionsModel, preferredOpenAiModel } from './models.js';
7
7
  import { isAnthropicModel } from '../utils/api.js';
8
8
  const id = 'roo-code';
9
9
  const name = 'Roo Code (VS Code)';
@@ -14,13 +14,31 @@ const claudeProfileName = 'ScaleMax Claude';
14
14
  const codexProfileId = 'scalemax-codex';
15
15
  const claudeProfileId = 'scalemax-claude';
16
16
  function importPath(home) { return path.join(home, '.scalemax', 'roo-code-import.json'); }
17
- function profileFor(ctx) {
17
+ function fallbackProfileFor(ctx) {
18
18
  const model = preferredChatCompletionsModel(ctx);
19
19
  const metadata = (ctx.models || []).find(candidate => candidate.id === model) || { id: model };
20
20
  return isAnthropicModel(metadata)
21
21
  ? { name: claudeProfileName, id: claudeProfileId, model }
22
22
  : { name: codexProfileName, id: codexProfileId, model };
23
23
  }
24
+ function profilesFor(ctx) {
25
+ const scope = ctx.providerScope || 'both';
26
+ const profiles = [];
27
+ if (scope !== 'anthropic' && openAiModelIds(ctx).length) {
28
+ profiles.push({ name: codexProfileName, id: codexProfileId, model: preferredOpenAiModel(ctx) });
29
+ }
30
+ if (scope !== 'openai' && anthropicModelIds(ctx).length) {
31
+ profiles.push({ name: claudeProfileName, id: claudeProfileId, model: preferredAnthropicModel(ctx) });
32
+ }
33
+ return profiles.length ? profiles : [fallbackProfileFor(ctx)];
34
+ }
35
+ function currentProfileFor(ctx, profiles = profilesFor(ctx)) {
36
+ if (ctx.providerScope === 'anthropic')
37
+ return profiles.find(profile => profile.id === claudeProfileId) || profiles[0];
38
+ if (ctx.providerScope === 'openai')
39
+ return profiles.find(profile => profile.id === codexProfileId) || profiles[0];
40
+ return profiles.find(profile => profile.id === codexProfileId) || profiles[0];
41
+ }
24
42
  function parseSettings(text, file) {
25
43
  if (!text)
26
44
  return {};
@@ -35,21 +53,21 @@ function parseSettings(text, file) {
35
53
  }
36
54
  }
37
55
  function handoff(ctx) {
38
- const profile = profileFor(ctx);
56
+ const profiles = profilesFor(ctx);
57
+ const current = currentProfileFor(ctx, profiles);
58
+ const apiConfigs = Object.fromEntries(profiles.map(profile => [profile.name, {
59
+ id: profile.id,
60
+ apiProvider: 'openai',
61
+ openAiBaseUrl: openAiBaseUrl(ctx.baseUrl),
62
+ openAiApiKey: ctx.apiKey,
63
+ openAiModelId: profile.model,
64
+ openAiStreamingEnabled: true,
65
+ includeMaxTokens: true
66
+ }]));
39
67
  return JSON.stringify({
40
68
  providerProfiles: {
41
- currentApiConfigName: profile.name,
42
- apiConfigs: {
43
- [profile.name]: {
44
- id: profile.id,
45
- apiProvider: 'openai',
46
- openAiBaseUrl: openAiBaseUrl(ctx.baseUrl),
47
- openAiApiKey: ctx.apiKey,
48
- openAiModelId: profile.model,
49
- openAiStreamingEnabled: true,
50
- includeMaxTokens: true
51
- }
52
- },
69
+ currentApiConfigName: current.name,
70
+ apiConfigs,
53
71
  modeApiConfigs: {}
54
72
  },
55
73
  globalSettings: {}
@@ -90,14 +108,19 @@ export async function verify(ctx) {
90
108
  const profileFile = importPath(ctx.home);
91
109
  const settings = parseSettings(await fs.readFile(settingsFile, 'utf8'), settingsFile);
92
110
  const profile = await fs.readJson(profileFile).catch(() => undefined);
93
- const expected = profileFor(ctx);
94
- const config = profile?.providerProfiles?.apiConfigs?.[expected.name];
95
- if (settings[importSetting] !== importSettingValue || profile?.providerProfiles?.currentApiConfigName !== expected.name
96
- || config?.id !== expected.id || config?.apiProvider !== 'openai' || config?.openAiBaseUrl !== openAiBaseUrl(ctx.baseUrl)
97
- || config?.openAiApiKey !== ctx.apiKey || config?.openAiModelId !== expected.model || config?.openAiStreamingEnabled !== true
98
- || config?.includeMaxTokens !== true) {
111
+ const expectedProfiles = profilesFor(ctx);
112
+ const current = currentProfileFor(ctx, expectedProfiles);
113
+ if (settings[importSetting] !== importSettingValue || profile?.providerProfiles?.currentApiConfigName !== current.name) {
99
114
  throw new Error('Roo Code auto-import handoff is incomplete.');
100
115
  }
116
+ for (const expected of expectedProfiles) {
117
+ const config = profile?.providerProfiles?.apiConfigs?.[expected.name];
118
+ if (config?.id !== expected.id || config?.apiProvider !== 'openai' || config?.openAiBaseUrl !== openAiBaseUrl(ctx.baseUrl)
119
+ || config?.openAiApiKey !== ctx.apiKey || config?.openAiModelId !== expected.model || config?.openAiStreamingEnabled !== true
120
+ || config?.includeMaxTokens !== true) {
121
+ throw new Error('Roo Code auto-import handoff is incomplete.');
122
+ }
123
+ }
101
124
  if (ctx.platform !== 'win32' && ((await fs.stat(profileFile)).mode & 0o077) !== 0)
102
125
  throw new Error('Roo Code import handoff permissions are too broad.');
103
126
  return { id, name, changed: false, path: profileFile, message: 'verified supported auto-import handoff (runtime activation requires a VS Code reload)' };
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ function welcome() {
34
34
  }
35
35
  function printHelp(command) {
36
36
  if (command === 'helper-bot') {
37
- console.log('Usage: scalemax helper-bot [--model <id>] ["question"]\n\nOpen the ScaleMax-only installation, configuration, authentication, and troubleshooting assistant.');
37
+ console.log('Usage: scalemax helper-bot [--model <id>] ["question"]\n\nExecute safe ScaleMax setup and verification actions, or open the ScaleMax-only troubleshooting assistant.');
38
38
  return;
39
39
  }
40
40
  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.' };
@@ -52,7 +52,7 @@ Commands:
52
52
  scalemax logout Remove API key
53
53
  scalemax doctor Run diagnostics
54
54
  scalemax models List available models
55
- scalemax helper-bot Open the ScaleMax Helper
55
+ scalemax helper-bot Execute setup actions or open the ScaleMax Helper
56
56
  scalemax update Reconfigure detected clients
57
57
  scalemax remove Remove ScaleMax-generated configs
58
58
  scalemax --version Show installed version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scalemax",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Universal ScaleMax installer for AI coding clients",
5
5
  "type": "module",
6
6
  "bin": {