scalemax 0.1.21 → 0.1.23
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 +4 -2
- package/dist/commands/helper-bot.d.ts +19 -0
- package/dist/commands/helper-bot.js +320 -10
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,13 +113,15 @@ 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
|
|
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
|
-
|
|
122
|
+
Recognized setup, repair, and verification requests execute the CLI's allowlisted client operations directly. For example, `setup vs code roo extension for claude` detects or installs the official Roo Code extension, writes its ScaleMax Claude profile with the stored key, verifies the handoff, and reports each step. The language model never generates or executes shell commands; only fixed ScaleMax client detection, configuration, verification, rollback, and the official Roo Code extension installer are available to the controller.
|
|
123
|
+
|
|
124
|
+
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
125
|
|
|
124
126
|
## Supported clients
|
|
125
127
|
|
|
@@ -1,8 +1,10 @@
|
|
|
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;
|
|
7
|
+
export declare const SCALEMAX_HELPER_INVALID_PROVIDER_RESPONSE: string;
|
|
6
8
|
export interface HelperBotArgs {
|
|
7
9
|
question?: string;
|
|
8
10
|
model?: string;
|
|
@@ -11,6 +13,11 @@ export interface HelperConnection {
|
|
|
11
13
|
apiKey: string;
|
|
12
14
|
baseUrl: string;
|
|
13
15
|
model: string;
|
|
16
|
+
platform?: NodeJS.Platform;
|
|
17
|
+
providerScope?: 'openai' | 'anthropic' | 'both';
|
|
18
|
+
keyMode?: string;
|
|
19
|
+
availableModels?: string[];
|
|
20
|
+
modelCatalog?: ModelInfo[];
|
|
14
21
|
}
|
|
15
22
|
export type HelperCompletion = (options: ChatCompletionOptions) => Promise<ChatCompletionResult>;
|
|
16
23
|
export interface HelperBotDependencies {
|
|
@@ -18,10 +25,22 @@ export interface HelperBotDependencies {
|
|
|
18
25
|
validateKey: typeof validateApiKey;
|
|
19
26
|
complete: HelperCompletion;
|
|
20
27
|
write: (message: string) => void;
|
|
28
|
+
clientCatalog: ClientDefinition[];
|
|
21
29
|
}
|
|
22
30
|
export declare function parseHelperBotArgs(args: string[]): HelperBotArgs;
|
|
23
31
|
export declare function redactHelperSecrets(value: unknown, activeKey?: string): string;
|
|
24
32
|
export declare function isScaleMaxHelperQuestion(input: string, hasHistory?: boolean): boolean;
|
|
33
|
+
export declare function isProviderIdentityOverride(value: string): boolean;
|
|
34
|
+
export declare function helperRuntimeContext(connection: HelperConnection): string;
|
|
35
|
+
export declare function helperConnectionSummary(connection: HelperConnection): string;
|
|
36
|
+
/**
|
|
37
|
+
* Answer stable configuration questions locally. Besides being faster and free,
|
|
38
|
+
* this keeps provider-owned personas out of the installation path where exact
|
|
39
|
+
* field values matter more than generated prose.
|
|
40
|
+
*/
|
|
41
|
+
export declare function localHelperReply(question: string, connection: HelperConnection): string | undefined;
|
|
25
42
|
export declare function selectHelperModel(models: ModelInfo[], requested?: string): string;
|
|
26
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>;
|
|
27
46
|
export declare function helperBotCommand(args?: string[], home?: string, dependencies?: Partial<HelperBotDependencies>): Promise<void>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { createInterface } from 'node:readline/promises';
|
|
3
|
-
import {
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { clients } from '../clients.js';
|
|
6
|
+
import { createChatCompletion, isAnthropicModel } from '../utils/api.js';
|
|
4
7
|
import { redactApiKey } from '../utils/apiKey.js';
|
|
5
8
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
6
9
|
import { homeDir } from '../utils/paths.js';
|
|
@@ -534,11 +537,35 @@ const CLEARLY_UNRELATED_PATTERN = /\b(?:homework|poem|story|roleplay|translate|t
|
|
|
534
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\s+code|continue|windsurf|hermes|cherry\s+studio|api\s+code)\b/i;
|
|
535
538
|
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
539
|
const GREETING_PATTERN = /^(?:hi|hello|hey|help|what can you do)[!.?\s]*$/i;
|
|
540
|
+
const CLAUDE_DESKTOP_PATTERN = /\bclaude\s+desktop\b/i;
|
|
541
|
+
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
|
+
const CODEX_CLI_PATTERN = /\bcodex(?:\s+cli)?\b/i;
|
|
543
|
+
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 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)\b/i;
|
|
545
|
+
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
|
+
const ALL_CLIENTS_PATTERN = /\b(?:all|every)\b.{0,30}\b(?:client|provider|integration|app)s?\b/i;
|
|
547
|
+
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;
|
|
548
|
+
const PROVIDER_IDENTITY_OVERRIDE_PATTERNS = [
|
|
549
|
+
/\bkiro\b/i,
|
|
550
|
+
/\b(?:i\s+am|i['’]m)\s+(?:an?\s+)?(?:claude|chatgpt|codex)\b/i,
|
|
551
|
+
/\bas\s+(?:claude|chatgpt|codex)(?:\s+(?:ai|assistant|model))?\s*[,—-]/i,
|
|
552
|
+
/\b(?:i\s+am|i['’]m)\s+(?:an?\s+)?(?:ai[- ]powered\s+)?(?:development\s+environment|ai\s+assistant|assistant|language\s+model)\s+(?:made|built|created|developed)\s+by\s+(?:anthropic|openai)\b/i,
|
|
553
|
+
/\bi\s+(?:was|am)\s+(?:made|built|created|developed)\s+by\s+(?:anthropic|openai)\b/i,
|
|
554
|
+
/\b(?:i\s+am|i['’]m)\s+not\s+(?:the\s+)?(?:official\s+)?scalemax\s+helper\b/i,
|
|
555
|
+
/\bi\s+(?:cannot|can['’]?t|won['’]?t)\s+(?:become|act\s+as|identify\s+as)\s+(?:the\s+)?(?:official\s+)?scalemax\s+helper\b/i,
|
|
556
|
+
/\b(?:not|isn['’]?t)\s+(?:the\s+)?(?:official\s+)?scalemax\s+helper\b/i,
|
|
557
|
+
/\b(?:that|the|your|supplied)\s+(?:system\s+)?prompt\b.{0,120}\b(?:does\s+not|doesn['’]?t|cannot|can['’]?t|won['’]?t)\s+(?:override|change|replace)\b.{0,100}\b(?:identity|instructions?|role)\b/is,
|
|
558
|
+
/\bscalemax\b.{0,100}\b(?:does\s+not|doesn['’]?t)\s+(?:appear\s+to\s+)?(?:exist|be\s+real)\b/is
|
|
559
|
+
];
|
|
560
|
+
export const SCALEMAX_HELPER_INVALID_PROVIDER_RESPONSE = `ScaleMax Helper blocked an invalid provider response because it conflicted with the Helper's required identity.
|
|
561
|
+
|
|
562
|
+
The requested ScaleMax task was not completed or verified. Run \`scalemax models\`, then retry with another available model using \`scalemax helper-bot --model <id> "your ScaleMax question"\`.`;
|
|
537
563
|
const defaultDependencies = {
|
|
538
564
|
loadKey: loadApiKey,
|
|
539
565
|
validateKey: validateApiKey,
|
|
540
566
|
complete: createChatCompletion,
|
|
541
|
-
write: message => console.log(message)
|
|
567
|
+
write: message => console.log(message),
|
|
568
|
+
clientCatalog: clients
|
|
542
569
|
};
|
|
543
570
|
export function parseHelperBotArgs(args) {
|
|
544
571
|
let model;
|
|
@@ -573,6 +600,91 @@ export function isScaleMaxHelperQuestion(input, hasHistory = false) {
|
|
|
573
600
|
return false;
|
|
574
601
|
return hasHistory || GREETING_PATTERN.test(value) || SCALEMAX_TOPIC_PATTERN.test(value) || TROUBLESHOOTING_PATTERN.test(value);
|
|
575
602
|
}
|
|
603
|
+
export function isProviderIdentityOverride(value) {
|
|
604
|
+
return PROVIDER_IDENTITY_OVERRIDE_PATTERNS.some(pattern => pattern.test(value));
|
|
605
|
+
}
|
|
606
|
+
function claudeDesktopGatewayBaseUrl(baseUrl) {
|
|
607
|
+
return baseUrl.trim().replace(/\/+$/, '').replace(/\/v1$/i, '');
|
|
608
|
+
}
|
|
609
|
+
function helperBillingMode(connection) {
|
|
610
|
+
return /\/token(?:\/|$)/i.test(connection.baseUrl) || /token/i.test(connection.keyMode || '') ? 'API token' : 'API credit';
|
|
611
|
+
}
|
|
612
|
+
function helperPlatformName(platform = process.platform) {
|
|
613
|
+
if (platform === 'win32')
|
|
614
|
+
return 'Windows';
|
|
615
|
+
if (platform === 'darwin')
|
|
616
|
+
return 'macOS';
|
|
617
|
+
if (platform === 'linux')
|
|
618
|
+
return 'Linux';
|
|
619
|
+
return platform;
|
|
620
|
+
}
|
|
621
|
+
export function helperRuntimeContext(connection) {
|
|
622
|
+
const models = (connection.availableModels || []).filter(Boolean);
|
|
623
|
+
return `# AUTHENTICATED SCALEMAX RUNTIME CONTEXT
|
|
624
|
+
|
|
625
|
+
The ScaleMax CLI already verified these authoritative facts before starting this conversation:
|
|
626
|
+
|
|
627
|
+
- Stored customer API key: present and authenticated. Never ask the user to paste, provide, repeat, or reveal it.
|
|
628
|
+
- Billing mode: ${helperBillingMode(connection)}
|
|
629
|
+
- OpenAI-compatible base URL: ${connection.baseUrl}
|
|
630
|
+
- Claude Desktop gateway base URL: ${claudeDesktopGatewayBaseUrl(connection.baseUrl)}
|
|
631
|
+
- Operating system: ${helperPlatformName(connection.platform)}
|
|
632
|
+
- Provider scope: ${connection.providerScope || 'both'}
|
|
633
|
+
- Active Helper model: ${connection.model}
|
|
634
|
+
- Authenticated model catalog: ${models.length ? models.join(', ') : 'already validated by the CLI'}
|
|
635
|
+
|
|
636
|
+
Mandatory behavior:
|
|
637
|
+
|
|
638
|
+
1. Treat the stored key, base URLs, operating system, provider scope, and model catalog as already known. Do not request them again.
|
|
639
|
+
2. Never output placeholders such as YOUR-SCALEMAX-ENDPOINT or your-key when an exact known value can be used.
|
|
640
|
+
3. The CLI controller executes recognized safe configuration and verification requests before asking the model. Do not claim that an unrecognized action was executed.
|
|
641
|
+
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.
|
|
642
|
+
5. When user action is genuinely required, give the shortest exact instruction and clearly distinguish it from work already completed by the CLI controller.`;
|
|
643
|
+
}
|
|
644
|
+
export function helperConnectionSummary(connection) {
|
|
645
|
+
return `Authenticated ${helperBillingMode(connection)} key · ${helperPlatformName(connection.platform)} · ${connection.providerScope || 'both'} scope · ${connection.baseUrl} · helper model ${connection.model}`;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Answer stable configuration questions locally. Besides being faster and free,
|
|
649
|
+
* this keeps provider-owned personas out of the installation path where exact
|
|
650
|
+
* field values matter more than generated prose.
|
|
651
|
+
*/
|
|
652
|
+
export function localHelperReply(question, connection) {
|
|
653
|
+
if (CLAUDE_DESKTOP_PATTERN.test(question) && CLAUDE_DESKTOP_CONFIGURATION_PATTERN.test(question)) {
|
|
654
|
+
const gatewayBaseUrl = claudeDesktopGatewayBaseUrl(connection.baseUrl);
|
|
655
|
+
return `ScaleMax Helper detected and authenticated the stored ${helperBillingMode(connection)} key.
|
|
656
|
+
|
|
657
|
+
Configure Claude Desktop → Settings → Developer → Edit Config → Configure third-party inference with:
|
|
658
|
+
|
|
659
|
+
- Connection: Gateway
|
|
660
|
+
- Gateway base URL: ${gatewayBaseUrl}
|
|
661
|
+
- Gateway API key: use the stored ScaleMax key (do not paste it into chat)
|
|
662
|
+
- Authentication scheme: Bearer
|
|
663
|
+
- Credential kind: Static API key
|
|
664
|
+
- Model discovery: On
|
|
665
|
+
|
|
666
|
+
Select Save Changes, then Apply Changes. Start a new Claude Desktop chat and send a short test message with one discovered model. The key and endpoint are verified; the Claude Desktop GUI is only verified after that test succeeds.`;
|
|
667
|
+
}
|
|
668
|
+
if (CODEX_CLI_PATTERN.test(question) && CLIENT_CONFIGURATION_PATTERN.test(question)) {
|
|
669
|
+
return `ScaleMax Helper already authenticated the stored ${helperBillingMode(connection)} key for ${helperPlatformName(connection.platform)}.
|
|
670
|
+
|
|
671
|
+
You do not need to provide the API key or base URL again. This key uses:
|
|
672
|
+
|
|
673
|
+
- Base URL: ${connection.baseUrl}
|
|
674
|
+
- Provider scope: ${connection.providerScope || 'both'}
|
|
675
|
+
|
|
676
|
+
Exit the Helper with \`/quit\`, then run:
|
|
677
|
+
|
|
678
|
+
\`\`\`powershell
|
|
679
|
+
scalemax update
|
|
680
|
+
scalemax doctor
|
|
681
|
+
codex
|
|
682
|
+
\`\`\`
|
|
683
|
+
|
|
684
|
+
\`scalemax update\` configures detected Codex CLI from the stored key, and \`scalemax doctor\` verifies the resulting ScaleMax connection before Codex starts.`;
|
|
685
|
+
}
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
576
688
|
export function selectHelperModel(models, requested) {
|
|
577
689
|
if (requested) {
|
|
578
690
|
const exact = models.find(model => model.id.toLowerCase() === requested.toLowerCase());
|
|
@@ -580,7 +692,7 @@ export function selectHelperModel(models, requested) {
|
|
|
580
692
|
throw new Error(`Model "${requested}" is not available to this API key. Run \`scalemax models\` to see available models.`);
|
|
581
693
|
return exact.id;
|
|
582
694
|
}
|
|
583
|
-
const preferred = ['gpt-5.
|
|
695
|
+
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'];
|
|
584
696
|
for (const id of preferred) {
|
|
585
697
|
const match = models.find(model => model.id.toLowerCase() === id);
|
|
586
698
|
if (match)
|
|
@@ -596,20 +708,200 @@ export async function requestHelperReply(question, history, connection, complete
|
|
|
596
708
|
throw new Error(`Please keep each helper request under ${MAX_USER_CHARS} characters.`);
|
|
597
709
|
if (!isScaleMaxHelperQuestion(cleanQuestion, history.length > 0))
|
|
598
710
|
return SCALEMAX_HELPER_REFUSAL;
|
|
711
|
+
const localReply = localHelperReply(cleanQuestion, connection);
|
|
712
|
+
if (localReply)
|
|
713
|
+
return localReply;
|
|
599
714
|
const recentHistory = history.slice(-MAX_HISTORY_MESSAGES).map(message => ({ ...message, content: redactHelperSecrets(message.content, connection.apiKey) }));
|
|
600
715
|
const result = await complete({
|
|
601
716
|
apiKey: connection.apiKey,
|
|
602
717
|
baseUrl: connection.baseUrl,
|
|
603
718
|
model: connection.model,
|
|
604
719
|
maxTokens: DEFAULT_MAX_TOKENS,
|
|
605
|
-
messages: [
|
|
720
|
+
messages: [
|
|
721
|
+
{ role: 'system', content: SCALEMAX_HELPER_SYSTEM_PROMPT },
|
|
722
|
+
{ role: 'system', content: helperRuntimeContext(connection) },
|
|
723
|
+
...recentHistory,
|
|
724
|
+
{ role: 'user', content: cleanQuestion }
|
|
725
|
+
]
|
|
606
726
|
});
|
|
607
727
|
if (!result.ok || !result.content)
|
|
608
728
|
throw new Error(redactHelperSecrets(result.error || 'ScaleMax Helper did not return a response.', connection.apiKey));
|
|
609
|
-
|
|
729
|
+
const reply = redactHelperSecrets(result.content, connection.apiKey);
|
|
730
|
+
if (isProviderIdentityOverride(reply))
|
|
731
|
+
return SCALEMAX_HELPER_INVALID_PROVIDER_RESPONSE;
|
|
732
|
+
if (KNOWN_CONTEXT_REASK_PATTERN.test(reply)) {
|
|
733
|
+
return `ScaleMax Helper already authenticated the stored ${helperBillingMode(connection)} key. You do not need to provide the key, endpoint, operating system, or model catalog again.
|
|
734
|
+
|
|
735
|
+
Verified base URL: ${connection.baseUrl}
|
|
736
|
+
|
|
737
|
+
For automatic configuration, exit with \`/quit\`, run \`scalemax update\`, then run \`scalemax doctor\`.`;
|
|
738
|
+
}
|
|
739
|
+
return reply;
|
|
740
|
+
}
|
|
741
|
+
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 },
|
|
743
|
+
{ id: 'openclaw', pattern: /\bopen\s*claw\b/i },
|
|
744
|
+
{ id: 'opencode', pattern: /\bopen\s*code\b/i },
|
|
745
|
+
{ id: 'cherry-studio', pattern: /\bcherry\s*studio\b/i },
|
|
746
|
+
{ id: 'api-code', pattern: /\bapi\s*code\b/i },
|
|
747
|
+
{ id: 'trae-solo', pattern: /\btrae(?:\s+solo)?\b/i },
|
|
748
|
+
{ id: 'cursor', pattern: /\bcursor\b/i },
|
|
749
|
+
{ id: 'hermes', pattern: /\bhermes\b/i },
|
|
750
|
+
{ id: 'codex', pattern: /\bcodex(?:\s+cli)?\b/i },
|
|
751
|
+
{ id: 'claude', pattern: /\bclaude\s+code\b/i }
|
|
752
|
+
];
|
|
753
|
+
function helperActionKind(question) {
|
|
754
|
+
if (HELPER_MUTATION_PATTERN.test(question))
|
|
755
|
+
return 'configure';
|
|
756
|
+
if (HELPER_VERIFICATION_PATTERN.test(question))
|
|
757
|
+
return 'verify';
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
function helperActionTarget(question) {
|
|
761
|
+
if (CLAUDE_DESKTOP_PATTERN.test(question))
|
|
762
|
+
return undefined;
|
|
763
|
+
if (ALL_CLIENTS_PATTERN.test(question))
|
|
764
|
+
return 'all';
|
|
765
|
+
return HELPER_CLIENT_PATTERNS.find(candidate => candidate.pattern.test(question))?.id;
|
|
766
|
+
}
|
|
767
|
+
function requestedProviderScope(question, target) {
|
|
768
|
+
if (/\b(?:both|openai\s*\+\s*claude|claude\s*\+\s*openai)\b/i.test(question))
|
|
769
|
+
return 'both';
|
|
770
|
+
if (/\b(?:claude|anthropic)\b/i.test(question))
|
|
771
|
+
return 'anthropic';
|
|
772
|
+
if (/\b(?:openai|chatgpt|gpt)\b/i.test(question) || target === 'codex')
|
|
773
|
+
return 'openai';
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
function actionModels(connection, requestedScope) {
|
|
777
|
+
const catalog = connection.modelCatalog?.length
|
|
778
|
+
? connection.modelCatalog
|
|
779
|
+
: (connection.availableModels || []).map(id => ({ id }));
|
|
780
|
+
if (!requestedScope || requestedScope === 'both')
|
|
781
|
+
return catalog;
|
|
782
|
+
return catalog.filter(model => requestedScope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model));
|
|
783
|
+
}
|
|
784
|
+
function hasRooCodeExtension(paths) {
|
|
785
|
+
return paths.some(candidate => /^rooveterinaryinc\.roo-cline-/i.test(path.basename(candidate)));
|
|
786
|
+
}
|
|
787
|
+
async function installRooCodeExtension(ctx, client, write) {
|
|
788
|
+
let detection = await client.detect(ctx);
|
|
789
|
+
if (hasRooCodeExtension(detection.paths))
|
|
790
|
+
return;
|
|
791
|
+
if (!detection.executable)
|
|
792
|
+
throw new Error('VS Code and the Roo Code extension were not detected. Install VS Code first, then retry this request.');
|
|
793
|
+
write('→ Installing the official Roo Code extension in VS Code…');
|
|
794
|
+
const result = await execa('code', ['--install-extension', 'RooVeterinaryInc.roo-cline', '--force'], {
|
|
795
|
+
env: ctx.env,
|
|
796
|
+
reject: false,
|
|
797
|
+
all: true
|
|
798
|
+
});
|
|
799
|
+
if (result.exitCode !== 0)
|
|
800
|
+
throw new Error(`Roo Code extension installation failed: ${result.all?.trim() || `code exited with ${result.exitCode}`}`);
|
|
801
|
+
detection = await client.detect(ctx);
|
|
802
|
+
if (!hasRooCodeExtension(detection.paths))
|
|
803
|
+
throw new Error('VS Code reported a successful Roo Code installation, but the extension could not be detected afterward. Restart VS Code and retry.');
|
|
804
|
+
write('✓ Roo Code extension installed and detected.');
|
|
805
|
+
}
|
|
806
|
+
async function runOneHelperClientAction(kind, client, connection, home, requestedScope, write) {
|
|
807
|
+
const models = actionModels(connection, requestedScope);
|
|
808
|
+
if (!models.length)
|
|
809
|
+
throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : 'compatible'} models for ${client.name}. No configuration was changed.`);
|
|
810
|
+
if (client.id === 'codex' && models.every(isAnthropicModel))
|
|
811
|
+
throw new Error('Codex requires an OpenAI-compatible model, but this key exposes only Claude models. No configuration was changed.');
|
|
812
|
+
if (client.id === 'cursor' && connection.platform !== 'darwin')
|
|
813
|
+
throw new Error(`Automatic Cursor configuration is currently verified on macOS only. ${helperPlatformName(connection.platform)} was not modified.`);
|
|
814
|
+
const ctx = {
|
|
815
|
+
home,
|
|
816
|
+
platform: connection.platform || process.platform,
|
|
817
|
+
env: process.env,
|
|
818
|
+
apiKey: connection.apiKey,
|
|
819
|
+
baseUrl: connection.baseUrl,
|
|
820
|
+
providerScope: requestedScope || connection.providerScope || 'both',
|
|
821
|
+
models,
|
|
822
|
+
interactive: false,
|
|
823
|
+
allowAppModification: false
|
|
824
|
+
};
|
|
825
|
+
write(`→ Detecting ${client.name}…`);
|
|
826
|
+
let detection = await client.detect(ctx);
|
|
827
|
+
if (kind === 'configure' && client.id === 'roo-code') {
|
|
828
|
+
await installRooCodeExtension(ctx, client, write);
|
|
829
|
+
detection = await client.detect(ctx);
|
|
830
|
+
}
|
|
831
|
+
if (!detection.installed)
|
|
832
|
+
throw new Error(`${client.name} was not detected. No configuration was changed.`);
|
|
833
|
+
write(`✓ ${client.name} detected.`);
|
|
834
|
+
if (kind === 'verify') {
|
|
835
|
+
if (!client.verify) {
|
|
836
|
+
if (!detection.configured)
|
|
837
|
+
throw new Error(`${client.name} is installed but its ScaleMax configuration was not detected.`);
|
|
838
|
+
return `✓ ${client.name} ScaleMax configuration is present.`;
|
|
839
|
+
}
|
|
840
|
+
write(`→ Verifying ${client.name}…`);
|
|
841
|
+
const verified = await client.verify(ctx);
|
|
842
|
+
return `✓ ${client.name} ${verified.message}`;
|
|
843
|
+
}
|
|
844
|
+
write(`→ Configuring ${client.name} with the authenticated stored key…`);
|
|
845
|
+
const configured = await client.configure(ctx);
|
|
846
|
+
write(`✓ ${client.name} ${configured.message}`);
|
|
847
|
+
if (client.verify) {
|
|
848
|
+
write(`→ Verifying ${client.name} configuration…`);
|
|
849
|
+
try {
|
|
850
|
+
const verified = await client.verify(ctx);
|
|
851
|
+
write(`✓ ${client.name} ${verified.message}`);
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
if (configured.changed) {
|
|
855
|
+
write(`→ Verification failed; removing the incomplete ScaleMax-managed ${client.name} configuration…`);
|
|
856
|
+
await client.remove(ctx).catch(() => undefined);
|
|
857
|
+
}
|
|
858
|
+
throw error;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
detection = await client.detect(ctx);
|
|
863
|
+
if (!detection.configured)
|
|
864
|
+
throw new Error(`${client.name} configuration was written but could not be detected afterward.`);
|
|
865
|
+
write(`✓ ${client.name} configuration detected after write.`);
|
|
866
|
+
}
|
|
867
|
+
const activation = client.id === 'roo-code' ? ' Reload VS Code once so Roo Code imports the verified ScaleMax profile.' : '';
|
|
868
|
+
return `✅ Successfully completed: ${client.name} is configured and locally verified.${activation}`;
|
|
869
|
+
}
|
|
870
|
+
/** Execute only narrowly allowlisted ScaleMax client actions; never model-generated shell commands. */
|
|
871
|
+
export async function executeHelperAction(question, connection, home, clientCatalog = clients, write = message => console.log(message)) {
|
|
872
|
+
const cleanQuestion = redactHelperSecrets(question.trim(), connection.apiKey);
|
|
873
|
+
if (!isScaleMaxHelperQuestion(cleanQuestion))
|
|
874
|
+
return undefined;
|
|
875
|
+
const kind = helperActionKind(cleanQuestion);
|
|
876
|
+
const target = helperActionTarget(cleanQuestion);
|
|
877
|
+
if (!kind || !target)
|
|
878
|
+
return undefined;
|
|
879
|
+
const scope = requestedProviderScope(cleanQuestion, target);
|
|
880
|
+
const selected = target === 'all'
|
|
881
|
+
? clientCatalog.filter(client => client.id !== 'cursor')
|
|
882
|
+
: clientCatalog.filter(client => client.id === target);
|
|
883
|
+
if (!selected.length)
|
|
884
|
+
throw new Error('The requested ScaleMax integration is not supported by this CLI build.');
|
|
885
|
+
write(`✓ Stored ${helperBillingMode(connection)} key is authenticated; secrets remain hidden.`);
|
|
886
|
+
const completed = [];
|
|
887
|
+
const failures = [];
|
|
888
|
+
for (const client of selected) {
|
|
889
|
+
try {
|
|
890
|
+
completed.push(await runOneHelperClientAction(kind, client, connection, home, scope, write));
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
const message = redactHelperSecrets(error instanceof Error ? error.message : String(error), connection.apiKey);
|
|
894
|
+
if (target !== 'all')
|
|
895
|
+
throw new Error(message);
|
|
896
|
+
failures.push(`${client.name}: ${message}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (!completed.length)
|
|
900
|
+
throw new Error(`No ScaleMax client action completed.\n${failures.map(item => `- ${item}`).join('\n')}`);
|
|
901
|
+
return `${completed.join('\n')}${failures.length ? `\n\nSkipped or failed:\n${failures.map(item => `- ${item}`).join('\n')}` : ''}`;
|
|
610
902
|
}
|
|
611
903
|
function printSessionHelp(write) {
|
|
612
|
-
write('Commands: /help shows this help; /quit exits.
|
|
904
|
+
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.');
|
|
613
905
|
}
|
|
614
906
|
export async function helperBotCommand(args = [], home = homeDir(), dependencies = {}) {
|
|
615
907
|
const deps = { ...defaultDependencies, ...dependencies };
|
|
@@ -621,15 +913,29 @@ export async function helperBotCommand(args = [], home = homeDir(), dependencies
|
|
|
621
913
|
if (!validation.ok || !validation.baseUrl)
|
|
622
914
|
throw new Error(redactHelperSecrets(validation.error || 'Unable to validate the ScaleMax API key.', key));
|
|
623
915
|
const model = selectHelperModel(validation.models, options.model);
|
|
624
|
-
const connection = {
|
|
916
|
+
const connection = {
|
|
917
|
+
apiKey: key,
|
|
918
|
+
baseUrl: validation.baseUrl,
|
|
919
|
+
model,
|
|
920
|
+
platform: process.platform,
|
|
921
|
+
providerScope: validation.key?.provider_scope || 'both',
|
|
922
|
+
keyMode: validation.key?.mode,
|
|
923
|
+
availableModels: validation.models.map(item => item.id),
|
|
924
|
+
modelCatalog: validation.models
|
|
925
|
+
};
|
|
926
|
+
const respond = async (question, history) => {
|
|
927
|
+
const action = await executeHelperAction(question, connection, home, deps.clientCatalog, deps.write);
|
|
928
|
+
return action || requestHelperReply(question, history, connection, deps.complete);
|
|
929
|
+
};
|
|
625
930
|
if (options.question) {
|
|
626
|
-
deps.write(await
|
|
931
|
+
deps.write(await respond(options.question, []));
|
|
627
932
|
return;
|
|
628
933
|
}
|
|
629
934
|
if (!process.stdin.isTTY)
|
|
630
935
|
throw new Error('Interactive helper-bot requires a terminal. Pass a quoted question for non-interactive use.');
|
|
631
936
|
deps.write(chalk.cyan('ScaleMax Helper'));
|
|
632
|
-
deps.write(chalk.dim(
|
|
937
|
+
deps.write(chalk.dim(helperConnectionSummary(connection)));
|
|
938
|
+
deps.write(chalk.dim('The stored customer key is reused for Helper requests; it will not be requested or displayed. Type /help or /quit.'));
|
|
633
939
|
const reader = createInterface({ input: process.stdin, output: process.stdout });
|
|
634
940
|
const history = [];
|
|
635
941
|
try {
|
|
@@ -650,8 +956,12 @@ export async function helperBotCommand(args = [], home = homeDir(), dependencies
|
|
|
650
956
|
printSessionHelp(deps.write);
|
|
651
957
|
continue;
|
|
652
958
|
}
|
|
959
|
+
if (/^\/?status$/i.test(input)) {
|
|
960
|
+
deps.write(helperConnectionSummary(connection));
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
653
963
|
try {
|
|
654
|
-
const reply = await
|
|
964
|
+
const reply = await respond(input, history);
|
|
655
965
|
deps.write(reply);
|
|
656
966
|
if (reply !== SCALEMAX_HELPER_REFUSAL) {
|
|
657
967
|
history.push({ role: 'user', content: redactHelperSecrets(input, key) }, { role: 'assistant', content: reply });
|
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\
|
|
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
|
|
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
|