scalemax 0.1.23 → 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 +3 -1
- package/dist/commands/helper-bot.js +120 -11
- package/dist/commands/helper-docs.d.ts +9 -0
- package/dist/commands/helper-docs.js +70 -0
- package/dist/configure/roo-code.js +44 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,7 +119,9 @@ The helper uses the same stored key as the installer, verifies authentication, a
|
|
|
119
119
|
npx scalemax@latest helper-bot --model fable-5 "Help me configure ScaleMax in Cursor"
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
Recognized setup, repair, and verification requests execute the CLI's allowlisted client operations directly.
|
|
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.
|
|
123
125
|
|
|
124
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.
|
|
125
127
|
|
|
@@ -3,11 +3,13 @@ import path from 'node:path';
|
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import { clients } from '../clients.js';
|
|
6
|
+
import { helperDocsContext } from './helper-docs.js';
|
|
6
7
|
import { createChatCompletion, isAnthropicModel } from '../utils/api.js';
|
|
7
8
|
import { redactApiKey } from '../utils/apiKey.js';
|
|
8
9
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
9
10
|
import { homeDir } from '../utils/paths.js';
|
|
10
11
|
import { validateApiKey } from '../utils/api.js';
|
|
12
|
+
import { commandExists } from '../utils/exec.js';
|
|
11
13
|
export const SCALEMAX_HELPER_REFUSAL = `I am the official ScaleMax Helper.
|
|
12
14
|
|
|
13
15
|
I can only assist with installing, configuring, repairing, verifying, and troubleshooting ScaleMax.
|
|
@@ -295,6 +297,8 @@ Never stop after only giving instructions if you can continue solving the issue.
|
|
|
295
297
|
|
|
296
298
|
Always prefer automatic configuration.
|
|
297
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
|
+
|
|
298
302
|
Only ask the user to perform manual actions when automation is impossible.
|
|
299
303
|
|
|
300
304
|
If multiple configuration methods exist:
|
|
@@ -534,14 +538,15 @@ const DEFAULT_MAX_TOKENS = 512;
|
|
|
534
538
|
const SECRET_PATTERN = /\b(?:sm_(?:live|test)_[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9_-]{12,})\b/g;
|
|
535
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;
|
|
536
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;
|
|
537
|
-
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
|
|
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;
|
|
538
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;
|
|
539
543
|
const GREETING_PATTERN = /^(?:hi|hello|hey|help|what can you do)[!.?\s]*$/i;
|
|
540
544
|
const CLAUDE_DESKTOP_PATTERN = /\bclaude\s+desktop\b/i;
|
|
541
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;
|
|
542
546
|
const CODEX_CLI_PATTERN = /\bcodex(?:\s+cli)?\b/i;
|
|
543
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;
|
|
544
|
-
const
|
|
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;
|
|
545
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;
|
|
546
551
|
const ALL_CLIENTS_PATTERN = /\b(?:all|every)\b.{0,30}\b(?:client|provider|integration|app)s?\b/i;
|
|
547
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;
|
|
@@ -650,6 +655,18 @@ export function helperConnectionSummary(connection) {
|
|
|
650
655
|
* field values matter more than generated prose.
|
|
651
656
|
*/
|
|
652
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
|
+
}
|
|
653
670
|
if (CLAUDE_DESKTOP_PATTERN.test(question) && CLAUDE_DESKTOP_CONFIGURATION_PATTERN.test(question)) {
|
|
654
671
|
const gatewayBaseUrl = claudeDesktopGatewayBaseUrl(connection.baseUrl);
|
|
655
672
|
return `ScaleMax Helper detected and authenticated the stored ${helperBillingMode(connection)} key.
|
|
@@ -712,6 +729,7 @@ export async function requestHelperReply(question, history, connection, complete
|
|
|
712
729
|
if (localReply)
|
|
713
730
|
return localReply;
|
|
714
731
|
const recentHistory = history.slice(-MAX_HISTORY_MESSAGES).map(message => ({ ...message, content: redactHelperSecrets(message.content, connection.apiKey) }));
|
|
732
|
+
const docsContext = helperDocsContext(cleanQuestion);
|
|
715
733
|
const result = await complete({
|
|
716
734
|
apiKey: connection.apiKey,
|
|
717
735
|
baseUrl: connection.baseUrl,
|
|
@@ -720,6 +738,7 @@ export async function requestHelperReply(question, history, connection, complete
|
|
|
720
738
|
messages: [
|
|
721
739
|
{ role: 'system', content: SCALEMAX_HELPER_SYSTEM_PROMPT },
|
|
722
740
|
{ role: 'system', content: helperRuntimeContext(connection) },
|
|
741
|
+
...(docsContext ? [{ role: 'system', content: docsContext }] : []),
|
|
723
742
|
...recentHistory,
|
|
724
743
|
{ role: 'user', content: cleanQuestion }
|
|
725
744
|
]
|
|
@@ -739,7 +758,7 @@ For automatic configuration, exit with \`/quit\`, run \`scalemax update\`, then
|
|
|
739
758
|
return reply;
|
|
740
759
|
}
|
|
741
760
|
const HELPER_CLIENT_PATTERNS = [
|
|
742
|
-
{ id: 'roo-code', pattern: /\b(?:roo(?:\s+code)?|roo\s+(?:extension|plugin)|vscode\s+roo|vs\s+code\s+roo)\b/i },
|
|
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 },
|
|
743
762
|
{ id: 'openclaw', pattern: /\bopen\s*claw\b/i },
|
|
744
763
|
{ id: 'opencode', pattern: /\bopen\s*code\b/i },
|
|
745
764
|
{ id: 'cherry-studio', pattern: /\bcherry\s*studio\b/i },
|
|
@@ -748,9 +767,11 @@ const HELPER_CLIENT_PATTERNS = [
|
|
|
748
767
|
{ id: 'cursor', pattern: /\bcursor\b/i },
|
|
749
768
|
{ id: 'hermes', pattern: /\bhermes\b/i },
|
|
750
769
|
{ id: 'codex', pattern: /\bcodex(?:\s+cli)?\b/i },
|
|
751
|
-
{ id: 'claude', pattern: /\bclaude
|
|
770
|
+
{ id: 'claude', pattern: /\bclaude(?:\s+code|\s+cli)?\b/i }
|
|
752
771
|
];
|
|
753
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';
|
|
754
775
|
if (HELPER_MUTATION_PATTERN.test(question))
|
|
755
776
|
return 'configure';
|
|
756
777
|
if (HELPER_VERIFICATION_PATTERN.test(question))
|
|
@@ -760,9 +781,14 @@ function helperActionKind(question) {
|
|
|
760
781
|
function helperActionTarget(question) {
|
|
761
782
|
if (CLAUDE_DESKTOP_PATTERN.test(question))
|
|
762
783
|
return undefined;
|
|
763
|
-
if (ALL_CLIENTS_PATTERN.test(question))
|
|
784
|
+
if (ALL_CLIENTS_PATTERN.test(question) || /\b(?:all|every)\b.{0,30}\bproviders?\b/i.test(question))
|
|
764
785
|
return 'all';
|
|
765
|
-
|
|
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;
|
|
766
792
|
}
|
|
767
793
|
function requestedProviderScope(question, target) {
|
|
768
794
|
if (/\b(?:both|openai\s*\+\s*claude|claude\s*\+\s*openai)\b/i.test(question))
|
|
@@ -781,13 +807,78 @@ function actionModels(connection, requestedScope) {
|
|
|
781
807
|
return catalog;
|
|
782
808
|
return catalog.filter(model => requestedScope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model));
|
|
783
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
|
+
}
|
|
784
818
|
function hasRooCodeExtension(paths) {
|
|
785
819
|
return paths.some(candidate => /^rooveterinaryinc\.roo-cline-/i.test(path.basename(candidate)));
|
|
786
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
|
+
}
|
|
787
874
|
async function installRooCodeExtension(ctx, client, write) {
|
|
788
875
|
let detection = await client.detect(ctx);
|
|
789
876
|
if (hasRooCodeExtension(detection.paths))
|
|
790
877
|
return;
|
|
878
|
+
if (!detection.executable && ctx.platform === 'win32') {
|
|
879
|
+
await installVSCodeOnWindows(ctx, write);
|
|
880
|
+
detection = await client.detect(ctx);
|
|
881
|
+
}
|
|
791
882
|
if (!detection.executable)
|
|
792
883
|
throw new Error('VS Code and the Roo Code extension were not detected. Install VS Code first, then retry this request.');
|
|
793
884
|
write('→ Installing the official Roo Code extension in VS Code…');
|
|
@@ -814,7 +905,7 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
814
905
|
const ctx = {
|
|
815
906
|
home,
|
|
816
907
|
platform: connection.platform || process.platform,
|
|
817
|
-
env: process.env,
|
|
908
|
+
env: { ...process.env },
|
|
818
909
|
apiKey: connection.apiKey,
|
|
819
910
|
baseUrl: connection.baseUrl,
|
|
820
911
|
providerScope: requestedScope || connection.providerScope || 'both',
|
|
@@ -822,15 +913,20 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
822
913
|
interactive: false,
|
|
823
914
|
allowAppModification: false
|
|
824
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
|
+
}
|
|
825
921
|
write(`→ Detecting ${client.name}…`);
|
|
826
922
|
let detection = await client.detect(ctx);
|
|
827
|
-
if (kind === 'configure' && client.id === 'roo-code') {
|
|
923
|
+
if ((kind === 'configure' || kind === 'repair') && client.id === 'roo-code') {
|
|
828
924
|
await installRooCodeExtension(ctx, client, write);
|
|
829
925
|
detection = await client.detect(ctx);
|
|
830
926
|
}
|
|
831
|
-
if (!detection.installed)
|
|
927
|
+
if (!detection.installed && client.id !== 'api-code')
|
|
832
928
|
throw new Error(`${client.name} was not detected. No configuration was changed.`);
|
|
833
|
-
write(`✓ ${client.name} detected.`);
|
|
929
|
+
write(client.id === 'api-code' && !detection.installed ? `✓ ${client.name} output path is available.` : `✓ ${client.name} detected.`);
|
|
834
930
|
if (kind === 'verify') {
|
|
835
931
|
if (!client.verify) {
|
|
836
932
|
if (!detection.configured)
|
|
@@ -841,6 +937,16 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
841
937
|
const verified = await client.verify(ctx);
|
|
842
938
|
return `✓ ${client.name} ${verified.message}`;
|
|
843
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
|
+
}
|
|
844
950
|
write(`→ Configuring ${client.name} with the authenticated stored key…`);
|
|
845
951
|
const configured = await client.configure(ctx);
|
|
846
952
|
write(`✓ ${client.name} ${configured.message}`);
|
|
@@ -877,11 +983,14 @@ export async function executeHelperAction(question, connection, home, clientCata
|
|
|
877
983
|
if (!kind || !target)
|
|
878
984
|
return undefined;
|
|
879
985
|
const scope = requestedProviderScope(cleanQuestion, target);
|
|
986
|
+
const scopedModels = actionModels(connection, scope);
|
|
880
987
|
const selected = target === 'all'
|
|
881
|
-
? clientCatalog.filter(client => client.id !== 'cursor')
|
|
988
|
+
? clientCatalog.filter(client => client.id !== 'cursor' && helperClientAllowedForModels(client.id, scopedModels))
|
|
882
989
|
: clientCatalog.filter(client => client.id === target);
|
|
883
990
|
if (!selected.length)
|
|
884
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.`);
|
|
885
994
|
write(`✓ Stored ${helperBillingMode(connection)} key is authenticated; secrets remain hidden.`);
|
|
886
995
|
const completed = [];
|
|
887
996
|
const failures = [];
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
94
|
-
const
|
|
95
|
-
if (settings[importSetting] !== importSettingValue || profile?.providerProfiles?.currentApiConfigName !==
|
|
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)' };
|