scalemax 0.1.41 → 0.1.42
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/dist/commands/configure.d.ts +2 -1
- package/dist/commands/configure.js +65 -15
- package/dist/commands/helper-bot.js +54 -23
- package/dist/commands/helper-docs.js +3 -3
- package/dist/commands/logout.js +9 -1
- package/dist/commands/remove.js +3 -4
- package/dist/configure/api-code.d.ts +2 -2
- package/dist/configure/cherry-studio.d.ts +2 -2
- package/dist/configure/claude.d.ts +2 -2
- package/dist/configure/claude.js +30 -7
- package/dist/configure/client-secret.d.ts +3 -6
- package/dist/configure/client-secret.js +11 -16
- package/dist/configure/cline.d.ts +2 -2
- package/dist/configure/cline.js +280 -73
- package/dist/configure/codex.d.ts +3 -3
- package/dist/configure/codex.js +120 -55
- package/dist/configure/common.d.ts +4 -2
- package/dist/configure/common.js +38 -7
- package/dist/configure/continue.d.ts +3 -2
- package/dist/configure/continue.js +55 -34
- package/dist/configure/cursor-bundle.js +6 -3
- package/dist/configure/cursor.d.ts +2 -2
- package/dist/configure/cursor.js +39 -7
- package/dist/configure/hermes.d.ts +2 -2
- package/dist/configure/hermes.js +91 -32
- package/dist/configure/models.d.ts +1 -0
- package/dist/configure/models.js +10 -8
- package/dist/configure/openclaw.d.ts +2 -2
- package/dist/configure/openclaw.js +155 -73
- package/dist/configure/opencode.d.ts +2 -2
- package/dist/configure/opencode.js +36 -8
- package/dist/configure/roo-code.d.ts +2 -2
- package/dist/configure/roo-code.js +15 -8
- package/dist/configure/trae-solo.d.ts +2 -2
- package/dist/detect/codex.js +31 -4
- package/dist/detect/continue.js +20 -4
- package/dist/types/client.d.ts +12 -2
- package/dist/types/client.js +21 -1
- package/dist/utils/api.d.ts +74 -2
- package/dist/utils/api.js +206 -68
- package/dist/utils/fs.d.ts +11 -0
- package/dist/utils/fs.js +53 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ModelInfo } from '../utils/api.js';
|
|
2
|
-
import { ClientId } from '../types/client.js';
|
|
2
|
+
import { ClientId, ConfigureResult } from '../types/client.js';
|
|
3
3
|
export declare function detectAll(home?: string): Promise<import("../types/client.js").DetectionResult[]>;
|
|
4
4
|
export declare function printDetected(home?: string): Promise<import("../types/client.js").DetectionResult[]>;
|
|
5
5
|
export declare function clientAllowedForModels(clientId: ClientId, models: ModelInfo[]): boolean;
|
|
6
|
+
export declare function rollbackAfterVerificationFailure(result: ConfigureResult, error: unknown): Promise<never>;
|
|
6
7
|
export declare function configureCommand(opts?: {
|
|
7
8
|
all?: boolean;
|
|
8
9
|
home?: string;
|
|
@@ -4,7 +4,7 @@ import ora from 'ora';
|
|
|
4
4
|
import { clients } from '../clients.js';
|
|
5
5
|
import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
6
6
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
7
|
-
import { validateApiKey, isAnthropicModel, isModelRouteAvailable, AUTO_MODEL_ID } from '../utils/api.js';
|
|
7
|
+
import { validateApiKey, isAnthropicModel, isModelRouteAvailable, AUTO_MODEL_ID, probeWorkingModels, pinClaudeAliasBackend, claudeAliasBackendPinTarget } from '../utils/api.js';
|
|
8
8
|
import { homeDir } from '../utils/paths.js';
|
|
9
9
|
import { isExactChatCompletionsModelId } from '../configure/models.js';
|
|
10
10
|
import { isContinueEligibleModelId } from '../configure/continue.js';
|
|
@@ -85,14 +85,38 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
85
85
|
return true;
|
|
86
86
|
return models.some(model => !isAnthropicModel(model));
|
|
87
87
|
}
|
|
88
|
+
export async function rollbackAfterVerificationFailure(result, error) {
|
|
89
|
+
if (!result.changed)
|
|
90
|
+
throw error;
|
|
91
|
+
if (!result.rollback) {
|
|
92
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; exact rollback was unavailable, so the current files were left in place rather than broadly removing the client configuration`);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await result.rollback();
|
|
96
|
+
}
|
|
97
|
+
catch (rollback) {
|
|
98
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; exact rollback also failed: ${rollback instanceof Error ? rollback.message : String(rollback)}`);
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
88
102
|
function scopeHint(scope) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
const labels = {
|
|
104
|
+
openai: 'OpenAI only',
|
|
105
|
+
anthropic: 'Claude only',
|
|
106
|
+
both: 'OpenAI + Claude',
|
|
107
|
+
gemini: 'Gemini only',
|
|
108
|
+
grok: 'Grok only',
|
|
109
|
+
qwen: 'Qwen only',
|
|
110
|
+
deepseek: 'DeepSeek only',
|
|
111
|
+
mimo: 'MiMo only',
|
|
112
|
+
local: 'Local Models',
|
|
113
|
+
reseller: 'reseller model allowlist',
|
|
114
|
+
'deepseek-v4-flash': 'DeepSeek V4 Flash only',
|
|
115
|
+
'glm-5.2': 'GLM 5.2 only',
|
|
116
|
+
'deepseek-v4-pro': 'DeepSeek V4 Pro only',
|
|
117
|
+
'auto-api': 'Auto only',
|
|
118
|
+
};
|
|
119
|
+
return labels[scope];
|
|
96
120
|
}
|
|
97
121
|
export async function configureCommand(opts = {}) {
|
|
98
122
|
const home = opts.home || homeDir();
|
|
@@ -106,6 +130,23 @@ export async function configureCommand(opts = {}) {
|
|
|
106
130
|
throw new Error(keyInfo.error || 'ScaleMax returned no models for this API key. No client configuration was changed.');
|
|
107
131
|
const providerScope = keyInfo.key?.provider_scope || 'both';
|
|
108
132
|
const selectedBaseUrl = keyInfo.baseUrl || SCALEMAX_BASE_URL;
|
|
133
|
+
// LIVE-PROBE the key's models with a small request and keep only the ones
|
|
134
|
+
// that actually answer, so a client is configured with working models and
|
|
135
|
+
// defaults to a working one. Bounded (concurrency + total time); if nothing
|
|
136
|
+
// probes clean we KEEP the entitlement-filtered list rather than fail — a
|
|
137
|
+
// slow/degraded upstream must not leave the user with zero models.
|
|
138
|
+
try {
|
|
139
|
+
const probeSpinner = ora('Checking which models respond').start();
|
|
140
|
+
const probe = await probeWorkingModels({ apiKey, baseUrl: selectedBaseUrl, models: keyInfo.models, providerScope });
|
|
141
|
+
if (probe.models.length && !probe.fallback) {
|
|
142
|
+
keyInfo.models = probe.models;
|
|
143
|
+
probeSpinner.succeed(`${probe.models.length}/${probe.probedCount} models responded`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
probeSpinner.warn(probe.warning || 'No model answered the probe in time; using the full authenticated catalog');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch { /* probing is best-effort; never blocks setup */ }
|
|
109
150
|
const availableClients = clients.filter(c => clientAllowedForModels(c.id, keyInfo.models));
|
|
110
151
|
console.log(chalk.dim(`API key scope: ${scopeHint(providerScope)}. Clients are filtered against the authenticated model catalog.`));
|
|
111
152
|
const detections = await printDetected(home);
|
|
@@ -175,13 +216,7 @@ export async function configureCommand(opts = {}) {
|
|
|
175
216
|
}
|
|
176
217
|
catch (error) {
|
|
177
218
|
verify.fail(`${client.name} verification failed`);
|
|
178
|
-
|
|
179
|
-
await client.remove(ctx);
|
|
180
|
-
}
|
|
181
|
-
catch (rollback) {
|
|
182
|
-
throw new Error(`${error instanceof Error ? error.message : String(error)}; rollback also failed: ${rollback instanceof Error ? rollback.message : String(rollback)}`);
|
|
183
|
-
}
|
|
184
|
-
throw error;
|
|
219
|
+
await rollbackAfterVerificationFailure(res, error);
|
|
185
220
|
}
|
|
186
221
|
}
|
|
187
222
|
successful.add(client.id);
|
|
@@ -192,6 +227,21 @@ export async function configureCommand(opts = {}) {
|
|
|
192
227
|
failures.push(`${client.name}: ${message}`);
|
|
193
228
|
}
|
|
194
229
|
}
|
|
230
|
+
// Pin the working default as this key's `claude-sonnet-4-6` backend so Claude
|
|
231
|
+
// Desktop (which always sends that id) routes to a model this key can serve.
|
|
232
|
+
// Best-effort: an older gateway without the endpoint, or a scope that already
|
|
233
|
+
// has a fixed default, is skipped silently. Never fails the run.
|
|
234
|
+
if (successful.size > 0) {
|
|
235
|
+
const target = claudeAliasBackendPinTarget(keyInfo.models);
|
|
236
|
+
if (target) {
|
|
237
|
+
try {
|
|
238
|
+
const pin = await pinClaudeAliasBackend({ apiKey, baseUrl: selectedBaseUrl, model: target });
|
|
239
|
+
if (pin.ok && !pin.skipped)
|
|
240
|
+
console.log(chalk.dim(`Claude Desktop's claude-sonnet-4-6 now routes to ${target} on this key.`));
|
|
241
|
+
}
|
|
242
|
+
catch { /* endpoint absent or transient; ignore */ }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
195
245
|
if (opts.promptLaunch !== false && !opts.all && process.stdin.isTTY)
|
|
196
246
|
await maybeLaunchCodex(chosen, successful, selectedBaseUrl);
|
|
197
247
|
if (failures.length)
|
|
@@ -4,6 +4,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import { clients } from '../clients.js';
|
|
6
6
|
import { helperDocsContext } from './helper-docs.js';
|
|
7
|
+
import { rollbackAfterVerificationFailure } from './configure.js';
|
|
7
8
|
import { createChatCompletion, filterModelsForScope, isAnthropicModel } from '../utils/api.js';
|
|
8
9
|
import { redactApiKey } from '../utils/apiKey.js';
|
|
9
10
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
@@ -12,6 +13,7 @@ import { validateApiKey } from '../utils/api.js';
|
|
|
12
13
|
import { commandExists } from '../utils/exec.js';
|
|
13
14
|
import { installClaudeGithubTools } from '../configure/claude-tools.js';
|
|
14
15
|
import { isExactChatCompletionsModelId } from '../configure/models.js';
|
|
16
|
+
import { isContinueEligibleModelId } from '../configure/continue.js';
|
|
15
17
|
export const SCALEMAX_HELPER_REFUSAL = `I am the official ScaleMax Helper.
|
|
16
18
|
|
|
17
19
|
I can only assist with installing, configuring, repairing, verifying, and troubleshooting ScaleMax.
|
|
@@ -804,8 +806,24 @@ function helperActionTarget(question) {
|
|
|
804
806
|
function requestedProviderScope(question, target) {
|
|
805
807
|
if (/\bglm[-\s]*5[._\s]*2\b/i.test(question))
|
|
806
808
|
return 'glm-5.2';
|
|
807
|
-
if (/\bdeepseek
|
|
809
|
+
if (/\bdeepseek[-\s]+v4[-\s]+pro\b/i.test(question))
|
|
810
|
+
return 'deepseek-v4-pro';
|
|
811
|
+
if (/\bdeepseek[-\s]+v4[-\s]+flash\b/i.test(question))
|
|
808
812
|
return 'deepseek-v4-flash';
|
|
813
|
+
if (/\bauto(?:[-\s]+api)?\b/i.test(question))
|
|
814
|
+
return 'auto-api';
|
|
815
|
+
if (/\bdeepseek\b/i.test(question))
|
|
816
|
+
return 'deepseek';
|
|
817
|
+
if (/\bgemini\b/i.test(question))
|
|
818
|
+
return 'gemini';
|
|
819
|
+
if (/\bgrok\b/i.test(question))
|
|
820
|
+
return 'grok';
|
|
821
|
+
if (/\bqwen\b/i.test(question))
|
|
822
|
+
return 'qwen';
|
|
823
|
+
if (/\bmimo\b/i.test(question))
|
|
824
|
+
return 'mimo';
|
|
825
|
+
if (/\blocal models?\b/i.test(question))
|
|
826
|
+
return 'local';
|
|
809
827
|
if (/\b(?:both|openai\s*\+\s*claude|claude\s*\+\s*openai)\b/i.test(question))
|
|
810
828
|
return 'both';
|
|
811
829
|
if (/\b(?:claude|anthropic)\b/i.test(question))
|
|
@@ -819,25 +837,34 @@ function actionModels(connection, requestedScope) {
|
|
|
819
837
|
? connection.modelCatalog
|
|
820
838
|
: (connection.availableModels || []).map(id => ({ id }));
|
|
821
839
|
const entitlement = connection.providerScope || 'both';
|
|
822
|
-
//
|
|
823
|
-
//
|
|
840
|
+
// The authenticated catalog already includes reseller and extra-family
|
|
841
|
+
// grants. Validate the primary scope, then narrow only for explicit user
|
|
842
|
+
// intent; never try to reconstruct effective entitlement client-side.
|
|
824
843
|
const catalog = filterModelsForScope(rawCatalog, entitlement);
|
|
825
|
-
if (!requestedScope)
|
|
844
|
+
if (!requestedScope || requestedScope === 'reseller' || requestedScope === 'local')
|
|
826
845
|
return catalog;
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
846
|
+
const exactModelByScope = {
|
|
847
|
+
'deepseek-v4-flash': 'deepseek-v4-flash',
|
|
848
|
+
'glm-5.2': 'glm-5.2',
|
|
849
|
+
'deepseek-v4-pro': 'deepseek-v4-pro',
|
|
850
|
+
'auto-api': 'auto',
|
|
851
|
+
};
|
|
852
|
+
const exactModel = exactModelByScope[requestedScope];
|
|
853
|
+
if (exactModel)
|
|
854
|
+
return catalog.filter(model => model.id.trim().toLowerCase() === exactModel);
|
|
835
855
|
if (requestedScope === 'both') {
|
|
836
|
-
return catalog.filter(model =>
|
|
856
|
+
return catalog.filter(model => isAnthropicModel(model)
|
|
857
|
+
|| String(model.provider || model.owned_by || '').trim().toLowerCase() === 'openai');
|
|
858
|
+
}
|
|
859
|
+
if (requestedScope === 'anthropic')
|
|
860
|
+
return catalog.filter(isAnthropicModel);
|
|
861
|
+
if (requestedScope === 'openai') {
|
|
862
|
+
return catalog.filter(model => {
|
|
863
|
+
const family = String(model.provider || model.owned_by || '').trim().toLowerCase();
|
|
864
|
+
return family ? family === 'openai' : !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id);
|
|
865
|
+
});
|
|
837
866
|
}
|
|
838
|
-
return catalog.filter(model =>
|
|
839
|
-
? isAnthropicModel(model)
|
|
840
|
-
: !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id));
|
|
867
|
+
return catalog.filter(model => String(model.provider || model.owned_by || '').trim().toLowerCase() === requestedScope);
|
|
841
868
|
}
|
|
842
869
|
function helperClientAllowedForModels(clientId, models) {
|
|
843
870
|
if (!models.length)
|
|
@@ -845,7 +872,7 @@ function helperClientAllowedForModels(clientId, models) {
|
|
|
845
872
|
const crossProtocolClients = new Set(['cursor', 'roo-code', 'cline', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
|
|
846
873
|
const onlyExactChat = models.every(model => isExactChatCompletionsModelId(model.id));
|
|
847
874
|
if (clientId === 'continue')
|
|
848
|
-
return
|
|
875
|
+
return models.some(model => isContinueEligibleModelId(model.id));
|
|
849
876
|
if (clientId === 'claude')
|
|
850
877
|
return models.some(isAnthropicModel);
|
|
851
878
|
if (clientId === 'codex' && onlyExactChat)
|
|
@@ -1000,17 +1027,21 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
1000
1027
|
write(`✓ ${client.name} ${verified.message}`);
|
|
1001
1028
|
}
|
|
1002
1029
|
catch (error) {
|
|
1003
|
-
if (configured.changed) {
|
|
1004
|
-
write(`→ Verification failed;
|
|
1005
|
-
await client.remove(ctx).catch(() => undefined);
|
|
1030
|
+
if (configured.changed && configured.rollback) {
|
|
1031
|
+
write(`→ Verification failed; restoring the exact pre-configuration ${client.name} state…`);
|
|
1006
1032
|
}
|
|
1007
|
-
|
|
1033
|
+
await rollbackAfterVerificationFailure(configured, error);
|
|
1008
1034
|
}
|
|
1009
1035
|
}
|
|
1010
1036
|
else {
|
|
1011
1037
|
detection = await client.detect(ctx);
|
|
1012
|
-
if (!detection.configured)
|
|
1013
|
-
|
|
1038
|
+
if (!detection.configured) {
|
|
1039
|
+
const error = new Error(`${client.name} configuration was written but could not be detected afterward.`);
|
|
1040
|
+
if (configured.changed && configured.rollback) {
|
|
1041
|
+
write(`→ Detection failed; restoring the exact pre-configuration ${client.name} state…`);
|
|
1042
|
+
}
|
|
1043
|
+
await rollbackAfterVerificationFailure(configured, error);
|
|
1044
|
+
}
|
|
1014
1045
|
write(`✓ ${client.name} configuration detected after write.`);
|
|
1015
1046
|
}
|
|
1016
1047
|
const activation = client.id === 'roo-code'
|
|
@@ -14,8 +14,8 @@ export const HELPER_DOC_SECTIONS = [
|
|
|
14
14
|
{
|
|
15
15
|
id: 'provider-scope',
|
|
16
16
|
title: 'Provider scope and visible models',
|
|
17
|
-
keywords: ['provider', 'scope', 'openai', 'claude', 'anthropic', 'deepseek', 'both', 'models', 'fable'],
|
|
18
|
-
content: `
|
|
17
|
+
keywords: ['provider', 'scope', 'openai', 'claude', 'anthropic', 'gemini', 'grok', 'qwen', 'deepseek', 'mimo', 'local', 'reseller', 'both', 'models', 'fable'],
|
|
18
|
+
content: `ScaleMax keys may grant an OpenAI, Claude, Gemini, Grok, Qwen, DeepSeek, MiMo, Local Models, combined OpenAI + Claude, exact-model, Auto, or reseller allowlist entitlement. The authenticated /models response is authoritative because it already includes per-key reseller grants and any extra families assigned to a credit key. The CLI must never add models absent from that response or strip models that it returned. Fable 5 is a Claude-family frontend model through ScaleMax. DeepSeek V4 Flash, GLM 5.2, and DeepSeek V4 Pro use /v1/chat/completions rather than Responses; tool support remains route-dependent, so trust capabilities.tools from the authenticated catalog.`
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
id: 'roo-code',
|
|
@@ -33,7 +33,7 @@ export const HELPER_DOC_SECTIONS = [
|
|
|
33
33
|
id: 'codex-and-clients',
|
|
34
34
|
title: 'Supported client update flow',
|
|
35
35
|
keywords: ['codex', 'cursor', 'claude code', 'opencode', 'openclaw', 'hermes', 'cherry studio', 'update', 'doctor'],
|
|
36
|
-
content: `scalemax update detects supported clients and configures them from the stored key. scalemax doctor validates the stored key and performs a
|
|
36
|
+
content: `scalemax update detects supported clients and configures them from the stored key. scalemax doctor validates the stored key and performs a small billable streaming request; configured client verifiers may perform an additional small billable request. Supported clients include Codex CLI, Cursor, Roo Code, Continue, TRAE SOLO, Claude Code, OpenCode, OpenClaw, Hermes, Cherry Studio, n8n, and API Code. The CLI obtains models from the authenticated matching /models endpoint rather than a hard-coded list. Recognized helper-bot setup and verification requests are executed by the CLI controller before any model fallback is used.`
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
id: 'claude-github-tools',
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { KEY_ENV } from '../constants.js';
|
|
2
3
|
import { removeApiKey } from '../utils/keyStore.js';
|
|
3
4
|
import { homeDir } from '../utils/paths.js';
|
|
4
|
-
export async function logoutCommand(home = homeDir()) {
|
|
5
|
+
export async function logoutCommand(home = homeDir()) {
|
|
6
|
+
await removeApiKey(home);
|
|
7
|
+
if (process.env[KEY_ENV]) {
|
|
8
|
+
console.log(chalk.yellow(`✓ Stored ScaleMax API key removed. ${KEY_ENV} is still set in the parent environment; a child CLI process cannot unset it. Unset it in your shell to stop using that key.`));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console.log(chalk.green('✓ Stored ScaleMax API key removed.'));
|
|
12
|
+
}
|
package/dist/commands/remove.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { clients } from '../clients.js';
|
|
4
|
-
import { loadApiKey } from '../utils/keyStore.js';
|
|
5
|
-
import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
4
|
+
import { loadApiKey, loadBaseUrl } from '../utils/keyStore.js';
|
|
6
5
|
import { homeDir } from '../utils/paths.js';
|
|
7
6
|
export async function removeCommand(home = homeDir()) {
|
|
8
|
-
const apiKey =
|
|
7
|
+
const [apiKey, baseUrl] = await Promise.all([loadApiKey(home), loadBaseUrl(home)]);
|
|
9
8
|
const failures = [];
|
|
10
9
|
for (const client of clients) {
|
|
11
10
|
const spinner = ora(`Removing ${client.name} ScaleMax config`).start();
|
|
12
11
|
try {
|
|
13
|
-
const res = await client.remove({ home, platform: process.platform, env: process.env, apiKey, baseUrl
|
|
12
|
+
const res = await client.remove({ home, platform: process.platform, env: process.env, apiKey, baseUrl });
|
|
14
13
|
spinner.succeed(`${client.name} ${res.message}`);
|
|
15
14
|
}
|
|
16
15
|
catch (err) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { ConfigureContext, ConfigureResult } from '../types/client.js';
|
|
1
|
+
import { ConfigureContext, ConfigureResult, RemoveContext } from '../types/client.js';
|
|
2
2
|
export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
3
|
-
export declare function remove(ctx:
|
|
3
|
+
export declare function remove(ctx: RemoveContext): Promise<ConfigureResult>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { ConfigureContext, ConfigureResult } from '../types/client.js';
|
|
1
|
+
import { ConfigureContext, ConfigureResult, RemoveContext } from '../types/client.js';
|
|
2
2
|
export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
3
|
-
export declare function remove(ctx:
|
|
3
|
+
export declare function remove(ctx: RemoveContext): Promise<ConfigureResult>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigureContext, ConfigureResult } from '../types/client.js';
|
|
1
|
+
import { ConfigureContext, ConfigureResult, RemoveContext } from '../types/client.js';
|
|
2
2
|
export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
3
3
|
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
4
|
-
export declare function remove(ctx:
|
|
4
|
+
export declare function remove(ctx: RemoveContext): Promise<ConfigureResult>;
|
package/dist/configure/claude.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { atomicWrite, backupFile } from '../utils/fs.js';
|
|
4
|
+
import { atomicWrite, backupFile, captureFileSnapshots, restoreFileSnapshots, snapshotRollback } from '../utils/fs.js';
|
|
5
5
|
import { clientConfigPath } from './paths.js';
|
|
6
6
|
import { writeMergedJsonConfig } from './common.js';
|
|
7
7
|
import { GENERATED_HEADER } from '../constants.js';
|
|
8
8
|
import { redactApiKey } from '../utils/apiKey.js';
|
|
9
9
|
import { anthropicOrigin, fastModel, modelIds, preferredModel, AUTO_MODEL_ID } from './models.js';
|
|
10
|
-
import { removeClientSecret,
|
|
10
|
+
import { removeClientSecret, verifyClientSecret, writeClientSecret } from './client-secret.js';
|
|
11
11
|
const id = 'claude';
|
|
12
12
|
const name = 'Claude Code';
|
|
13
13
|
const discoveryKey = 'CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY';
|
|
@@ -95,7 +95,9 @@ export async function configure(ctx) {
|
|
|
95
95
|
const customModel = managedCustomModel(ctx);
|
|
96
96
|
const baseUrl = anthropicOrigin(ctx.baseUrl);
|
|
97
97
|
const apiKeyHelper = helperCommand(ctx.home, ctx.platform);
|
|
98
|
-
const
|
|
98
|
+
const credentialFile = secretPath(ctx.home);
|
|
99
|
+
const snapshots = await captureFileSnapshots([file, credentialFile]);
|
|
100
|
+
const secret = await writeClientSecret(credentialFile, ctx);
|
|
99
101
|
let result;
|
|
100
102
|
try {
|
|
101
103
|
result = await writeMergedJsonConfig(ctx, id, name, file, existing => {
|
|
@@ -165,10 +167,21 @@ export async function configure(ctx) {
|
|
|
165
167
|
});
|
|
166
168
|
}
|
|
167
169
|
catch (error) {
|
|
168
|
-
|
|
170
|
+
try {
|
|
171
|
+
await restoreFileSnapshots(snapshots, ctx.platform);
|
|
172
|
+
}
|
|
173
|
+
catch (rollback) {
|
|
174
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; rollback also failed: ${rollback instanceof Error ? rollback.message : String(rollback)}`);
|
|
175
|
+
}
|
|
169
176
|
throw error;
|
|
170
177
|
}
|
|
171
|
-
|
|
178
|
+
const changed = result.changed || secret.changed;
|
|
179
|
+
return {
|
|
180
|
+
...result,
|
|
181
|
+
changed,
|
|
182
|
+
message: `configured Anthropic Messages routing with ${model}, file-backed authentication, and gateway discovery${quick ? `; ${quick} handles background work` : ''}`,
|
|
183
|
+
...(changed ? { rollback: snapshotRollback(snapshots, ctx.platform) } : {}),
|
|
184
|
+
};
|
|
172
185
|
}
|
|
173
186
|
export async function verify(ctx) {
|
|
174
187
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
@@ -222,8 +235,18 @@ export async function remove(ctx) {
|
|
|
222
235
|
return { id, name, changed: removed, path: file, message: removed ? 'removed orphaned ScaleMax Claude Code credential' : 'no ScaleMax configuration found' };
|
|
223
236
|
}
|
|
224
237
|
const settings = await fs.readJson(file).catch(() => undefined);
|
|
225
|
-
if (!settings || settings?.scalemax?.managedBy !== 'ScaleMax CLI')
|
|
226
|
-
|
|
238
|
+
if (!settings || settings?.scalemax?.managedBy !== 'ScaleMax CLI') {
|
|
239
|
+
const removed = await removeClientSecret(secretPath(ctx.home), ctx);
|
|
240
|
+
return {
|
|
241
|
+
id,
|
|
242
|
+
name,
|
|
243
|
+
changed: removed,
|
|
244
|
+
path: file,
|
|
245
|
+
message: removed
|
|
246
|
+
? 'removed orphaned ScaleMax Claude Code credential; left unmanaged configuration unchanged'
|
|
247
|
+
: 'left unmanaged configuration unchanged',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
227
250
|
const managed = asRecord(settings.scalemax);
|
|
228
251
|
const env = asRecord(settings.env);
|
|
229
252
|
const previous = asRecord(managed.previousEnv);
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { ConfigureContext } from '../types/client.js';
|
|
1
|
+
import { ConfigureContext, RemoveContext } from '../types/client.js';
|
|
2
2
|
export declare function writeClientSecret(file: string, ctx: ConfigureContext): Promise<{
|
|
3
3
|
changed: boolean;
|
|
4
4
|
backup?: string;
|
|
5
5
|
}>;
|
|
6
|
-
export declare function rollbackClientSecret(file: string, ctx: ConfigureContext, change: {
|
|
7
|
-
changed: boolean;
|
|
8
|
-
backup?: string;
|
|
9
|
-
}): Promise<void>;
|
|
10
6
|
export declare function verifyClientSecret(file: string, ctx: ConfigureContext): Promise<void>;
|
|
11
|
-
|
|
7
|
+
/** Dedicated scalemax.key paths are durable ownership markers; current login state is not. */
|
|
8
|
+
export declare function removeClientSecret(file: string, _ctx?: RemoveContext): Promise<boolean>;
|
|
@@ -15,16 +15,6 @@ export async function writeClientSecret(file, ctx) {
|
|
|
15
15
|
await atomicWrite(file, content, 0o600, ctx.platform);
|
|
16
16
|
return { changed: true, backup };
|
|
17
17
|
}
|
|
18
|
-
export async function rollbackClientSecret(file, ctx, change) {
|
|
19
|
-
if (!change.changed)
|
|
20
|
-
return;
|
|
21
|
-
if (change.backup) {
|
|
22
|
-
const previous = await fs.readFile(change.backup, 'utf8');
|
|
23
|
-
await atomicWrite(file, previous, 0o600, ctx.platform);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
await fs.remove(file).catch(() => undefined);
|
|
27
|
-
}
|
|
28
18
|
export async function verifyClientSecret(file, ctx) {
|
|
29
19
|
const content = await fs.readFile(file, 'utf8').catch(() => '');
|
|
30
20
|
if (sanitizeApiKey(content) !== sanitizeApiKey(ctx.apiKey))
|
|
@@ -35,13 +25,18 @@ export async function verifyClientSecret(file, ctx) {
|
|
|
35
25
|
throw new Error(`ScaleMax client credential permissions are ${mode.toString(8)}; expected 600.`);
|
|
36
26
|
}
|
|
37
27
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
/** Dedicated scalemax.key paths are durable ownership markers; current login state is not. */
|
|
29
|
+
export async function removeClientSecret(file, _ctx) {
|
|
30
|
+
const stat = await fs.lstat(file).catch((error) => {
|
|
31
|
+
if (error.code === 'ENOENT')
|
|
32
|
+
return undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
});
|
|
35
|
+
if (!stat)
|
|
43
36
|
return false;
|
|
44
|
-
|
|
37
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
38
|
+
throw new Error(`Refusing to remove non-regular ScaleMax credential path: ${file}`);
|
|
39
|
+
}
|
|
45
40
|
await fs.remove(file);
|
|
46
41
|
return true;
|
|
47
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigureContext, ConfigureResult, DetectContext, InstallContext } from '../types/client.js';
|
|
1
|
+
import { ConfigureContext, ConfigureResult, DetectContext, InstallContext, RemoveContext } from '../types/client.js';
|
|
2
2
|
export interface ClineExtensionSurface {
|
|
3
3
|
path: string;
|
|
4
4
|
version?: string;
|
|
@@ -26,4 +26,4 @@ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResul
|
|
|
26
26
|
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
27
27
|
export declare function isManagedCliStore(value: unknown): boolean;
|
|
28
28
|
export declare function isManagedExtensionState(globalState: unknown, secrets: unknown): boolean;
|
|
29
|
-
export declare function remove(ctx:
|
|
29
|
+
export declare function remove(ctx: RemoveContext): Promise<ConfigureResult>;
|