scalemax 0.1.45 → 0.1.48
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.js +3 -0
- package/dist/commands/helper-bot.js +3 -2
- package/dist/configure/cline.d.ts +6 -0
- package/dist/configure/cline.js +8 -0
- package/dist/configure/codex.d.ts +17 -1
- package/dist/configure/codex.js +207 -39
- package/dist/configure/models.js +7 -4
- package/dist/configure/openclaw.js +1 -1
- package/dist/configure/opencode.js +1 -1
- package/dist/configure/roo-code.d.ts +7 -0
- package/dist/configure/roo-code.js +9 -1
- package/dist/configure/templates.js +2 -2
- package/dist/types/client.d.ts +2 -0
- package/package.json +1 -1
|
@@ -221,6 +221,9 @@ export async function configureCommand(opts = {}) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
successful.add(client.id);
|
|
224
|
+
// Only after verification: a rolled-back client gets no usage advice.
|
|
225
|
+
if (res.tip)
|
|
226
|
+
console.log(chalk.cyan(` Tip: ${res.tip}`));
|
|
224
227
|
}
|
|
225
228
|
catch (err) {
|
|
226
229
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -714,7 +714,7 @@ export function selectHelperModel(models, requested) {
|
|
|
714
714
|
throw new Error(`Model "${requested}" is not available to this API key. Run \`scalemax models\` to see available models.`);
|
|
715
715
|
return exact.id;
|
|
716
716
|
}
|
|
717
|
-
const preferred = ['gpt-5.
|
|
717
|
+
const preferred = ['gpt-5.6-luna', 'claude-haiku-4-5-20251001', 'fable-5', 'gpt-5.6-terra', 'gpt-5.6-sol'];
|
|
718
718
|
for (const id of preferred) {
|
|
719
719
|
const match = models.find(model => model.id.toLowerCase() === id);
|
|
720
720
|
if (match)
|
|
@@ -1049,7 +1049,8 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
1049
1049
|
: client.id === 'cline'
|
|
1050
1050
|
? ' Restart Cline; reload VS Code too when using the extension.'
|
|
1051
1051
|
: client.id === 'continue' ? ' Reload VS Code once so Continue loads the verified ScaleMax profile.' : '';
|
|
1052
|
-
|
|
1052
|
+
const tip = configured.tip ? ` Tip: ${configured.tip}` : '';
|
|
1053
|
+
return `✅ Successfully completed: ${client.name} is configured and locally verified.${activation}${tip}`;
|
|
1053
1054
|
}
|
|
1054
1055
|
/** Execute only narrowly allowlisted ScaleMax client actions; never model-generated shell commands. */
|
|
1055
1056
|
export async function executeHelperAction(question, connection, home, clientCatalog = clients, write = message => console.log(message)) {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { ConfigureContext, ConfigureResult, DetectContext, InstallContext, RemoveContext } from '../types/client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Printed after every successful configure. Cline only edits files and carries
|
|
4
|
+
* out a task in Act mode; Plan mode explores and plans, which customers read as
|
|
5
|
+
* a broken key. Same advice as the docs' Cline guide and troubleshooting page.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CLINE_MODE_TIP = "Switch Cline to Act mode to let it edit files and run the task. Plan mode only reads and plans; nothing is wrong with your key.";
|
|
2
8
|
export interface ClineExtensionSurface {
|
|
3
9
|
path: string;
|
|
4
10
|
version?: string;
|
package/dist/configure/cline.js
CHANGED
|
@@ -16,6 +16,12 @@ const extensionId = 'saoudrizwan.claude-dev';
|
|
|
16
16
|
const cliProviderId = 'openai-native';
|
|
17
17
|
const legacyProviderId = 'scalemax';
|
|
18
18
|
const extensionCredentialHashKey = 'scalemaxManagedOpenAiApiKeySha256';
|
|
19
|
+
/**
|
|
20
|
+
* Printed after every successful configure. Cline only edits files and carries
|
|
21
|
+
* out a task in Act mode; Plan mode explores and plans, which customers read as
|
|
22
|
+
* a broken key. Same advice as the docs' Cline guide and troubleshooting page.
|
|
23
|
+
*/
|
|
24
|
+
export const CLINE_MODE_TIP = 'Switch Cline to Act mode to let it edit files and run the task. Plan mode only reads and plans; nothing is wrong with your key.';
|
|
19
25
|
function credentialSha256(value) {
|
|
20
26
|
return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
21
27
|
}
|
|
@@ -518,6 +524,7 @@ export async function configure(ctx) {
|
|
|
518
524
|
changed: false,
|
|
519
525
|
path: configuredPaths[0],
|
|
520
526
|
message: 'verified Cline configuration is already current',
|
|
527
|
+
tip: CLINE_MODE_TIP,
|
|
521
528
|
};
|
|
522
529
|
}
|
|
523
530
|
let snapshots;
|
|
@@ -541,6 +548,7 @@ export async function configure(ctx) {
|
|
|
541
548
|
changed: true,
|
|
542
549
|
path: configuredPaths[0],
|
|
543
550
|
message: `configured ${surfaces.cliSupported ? `CLI ${cliVersion}` : ''}${surfaces.cliSupported && surfaces.extensionSupported ? ' and ' : ''}${surfaces.extensionSupported ? `VS Code extension ${extensionVersion}` : ''} with one raw ScaleMax credential; restart Cline to activate`,
|
|
551
|
+
tip: CLINE_MODE_TIP,
|
|
544
552
|
rollback,
|
|
545
553
|
};
|
|
546
554
|
}
|
|
@@ -6,7 +6,23 @@ export interface CodexVerificationResult {
|
|
|
6
6
|
configPath: string;
|
|
7
7
|
authPath: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function mergeCodexConfig(existing: string, baseUrl: string, model?: string, authHash?: string): string;
|
|
9
|
+
export declare function mergeCodexConfig(existing: string, baseUrl: string, model?: string, authHash?: string, keyHash?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Take ScaleMax OUT of a config.toml, keeping everything else the user has --
|
|
12
|
+
* including what Codex or the user added after setup (trusted projects,
|
|
13
|
+
* notices, other providers).
|
|
14
|
+
*
|
|
15
|
+
* Ownership is read from the CONTENT, not only from the generated header: a
|
|
16
|
+
* Codex rewrite that drops comments still leaves `model_provider = "scalemax"`
|
|
17
|
+
* and the `[model_providers.scalemax]` table, and those must go. The three
|
|
18
|
+
* top-level keys are removed only while ScaleMax is still the provider; if the
|
|
19
|
+
* user switched provider after setup they are the user's.
|
|
20
|
+
*/
|
|
21
|
+
export declare function stripScaleMaxCodexConfig(existing: string): {
|
|
22
|
+
text: string;
|
|
23
|
+
found: boolean;
|
|
24
|
+
strippedTopLevel: boolean;
|
|
25
|
+
};
|
|
10
26
|
export declare function verifyCodexConfiguration(ctx: ConfigureContext): Promise<CodexVerificationResult>;
|
|
11
27
|
export declare function install(ctx: InstallContext): Promise<ConfigureResult>;
|
|
12
28
|
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
package/dist/configure/codex.js
CHANGED
|
@@ -10,17 +10,29 @@ import { AUTO_MODEL_ID, preferredOpenAiResponsesModel } from './models.js';
|
|
|
10
10
|
const id = 'codex';
|
|
11
11
|
const name = 'Codex';
|
|
12
12
|
const provider = 'scalemax';
|
|
13
|
-
const fallbackModel = 'gpt-5.
|
|
13
|
+
const fallbackModel = 'gpt-5.6-luna'; // gpt-5.5 retired 2026-09-30; same price
|
|
14
14
|
const reasoningEffort = 'medium';
|
|
15
15
|
const managedAuthHashPrefix = '# ScaleMax managed auth.json SHA-256: ';
|
|
16
|
+
// Fingerprint of the API key setup wrote into auth.json. Unlike the auth.json
|
|
17
|
+
// hash above it survives Codex rewriting auth.json (a token refresh, its own
|
|
18
|
+
// serialisation), so remove() can still tell OUR key from one the user put
|
|
19
|
+
// there later.
|
|
20
|
+
const managedKeyHashPrefix = '# ScaleMax managed API key SHA-256: ';
|
|
21
|
+
const modelsComment = '# ScaleMax models are loaded from the authenticated live model catalog.';
|
|
22
|
+
// The top-level keys setup owns while ScaleMax is the provider. remove() puts
|
|
23
|
+
// the user's pre-ScaleMax values for exactly these back from the backup.
|
|
24
|
+
const managedTopLevelKey = /^(model|model_provider|model_reasoning_effort)\s*=/;
|
|
16
25
|
function contentSha256(content) {
|
|
17
26
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
18
27
|
}
|
|
19
|
-
function
|
|
20
|
-
const marker = config.split(/\r?\n/).find(line => line.startsWith(
|
|
21
|
-
const hash = marker?.slice(
|
|
28
|
+
function markerHash(config, prefix) {
|
|
29
|
+
const marker = config.split(/\r?\n/).find(line => line.startsWith(prefix));
|
|
30
|
+
const hash = marker?.slice(prefix.length).trim();
|
|
22
31
|
return hash && /^[a-f0-9]{64}$/.test(hash) ? hash : undefined;
|
|
23
32
|
}
|
|
33
|
+
function managedAuthHash(config) {
|
|
34
|
+
return markerHash(config, managedAuthHashPrefix);
|
|
35
|
+
}
|
|
24
36
|
function selectedModel(ctx) {
|
|
25
37
|
if (!ctx.models) {
|
|
26
38
|
// An auto-api key's ONLY model is `auto`; gpt-5.5 (the generic fallback
|
|
@@ -30,11 +42,22 @@ function selectedModel(ctx) {
|
|
|
30
42
|
}
|
|
31
43
|
return preferredOpenAiResponsesModel(ctx);
|
|
32
44
|
}
|
|
33
|
-
function scalemaxCodexBlock(baseUrl, model = fallbackModel, authHash) {
|
|
45
|
+
function scalemaxCodexBlock(baseUrl, model = fallbackModel, authHash, keyHash) {
|
|
34
46
|
const authMarker = authHash ? `${managedAuthHashPrefix}${authHash}\n` : '';
|
|
35
|
-
|
|
47
|
+
const keyMarker = keyHash ? `${managedKeyHashPrefix}${keyHash}\n` : '';
|
|
48
|
+
return `# ${GENERATED_HEADER}\n${authMarker}${keyMarker}model = "${model}"\nmodel_provider = "${provider}"\nmodel_reasoning_effort = "${reasoningEffort}"\n${modelsComment}\n\n[model_providers.${provider}]\nname = "ScaleMax"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
|
|
49
|
+
}
|
|
50
|
+
/** A line setup writes as a comment. Never user content, and never preserved. */
|
|
51
|
+
function isManagedCommentLine(line) {
|
|
52
|
+
return line === `# ${GENERATED_HEADER}`
|
|
53
|
+
|| line === modelsComment
|
|
54
|
+
|| line.startsWith(managedAuthHashPrefix)
|
|
55
|
+
|| line.startsWith(managedKeyHashPrefix);
|
|
36
56
|
}
|
|
37
|
-
|
|
57
|
+
function isScaleMaxProviderSection(section) {
|
|
58
|
+
return section === `model_providers.${provider}` || section.startsWith(`model_providers.${provider}.`);
|
|
59
|
+
}
|
|
60
|
+
export function mergeCodexConfig(existing, baseUrl, model = fallbackModel, authHash, keyHash) {
|
|
38
61
|
const preserved = [];
|
|
39
62
|
let section = '';
|
|
40
63
|
let skipScaleMaxProvider = false;
|
|
@@ -43,20 +66,94 @@ export function mergeCodexConfig(existing, baseUrl, model = fallbackModel, authH
|
|
|
43
66
|
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
44
67
|
if (sectionMatch) {
|
|
45
68
|
section = sectionMatch[1];
|
|
46
|
-
skipScaleMaxProvider = section
|
|
69
|
+
skipScaleMaxProvider = isScaleMaxProviderSection(section);
|
|
47
70
|
if (skipScaleMaxProvider)
|
|
48
71
|
continue;
|
|
49
72
|
}
|
|
50
73
|
if (skipScaleMaxProvider)
|
|
51
74
|
continue;
|
|
52
|
-
if (!section &&
|
|
75
|
+
if (!section && managedTopLevelKey.test(line))
|
|
53
76
|
continue;
|
|
54
|
-
|
|
77
|
+
// Includes the models comment: it used to be kept here, so every re-run of
|
|
78
|
+
// setup stacked one more copy of it into the preserved tail.
|
|
79
|
+
if (isManagedCommentLine(line))
|
|
55
80
|
continue;
|
|
56
81
|
preserved.push(raw);
|
|
57
82
|
}
|
|
58
83
|
const tail = preserved.join('\n').trim();
|
|
59
|
-
return `${scalemaxCodexBlock(baseUrl, model, authHash)}${tail ? `\n${tail}\n` : ''}`;
|
|
84
|
+
return `${scalemaxCodexBlock(baseUrl, model, authHash, keyHash)}${tail ? `\n${tail}\n` : ''}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Take ScaleMax OUT of a config.toml, keeping everything else the user has --
|
|
88
|
+
* including what Codex or the user added after setup (trusted projects,
|
|
89
|
+
* notices, other providers).
|
|
90
|
+
*
|
|
91
|
+
* Ownership is read from the CONTENT, not only from the generated header: a
|
|
92
|
+
* Codex rewrite that drops comments still leaves `model_provider = "scalemax"`
|
|
93
|
+
* and the `[model_providers.scalemax]` table, and those must go. The three
|
|
94
|
+
* top-level keys are removed only while ScaleMax is still the provider; if the
|
|
95
|
+
* user switched provider after setup they are the user's.
|
|
96
|
+
*/
|
|
97
|
+
export function stripScaleMaxCodexConfig(existing) {
|
|
98
|
+
const lines = existing.split(/\r?\n/);
|
|
99
|
+
let section = '';
|
|
100
|
+
let topProvider;
|
|
101
|
+
let found = false;
|
|
102
|
+
for (const raw of lines) {
|
|
103
|
+
const line = raw.trim();
|
|
104
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
105
|
+
if (sectionMatch) {
|
|
106
|
+
section = sectionMatch[1];
|
|
107
|
+
if (isScaleMaxProviderSection(section))
|
|
108
|
+
found = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (isManagedCommentLine(line))
|
|
112
|
+
found = true;
|
|
113
|
+
if (!section) {
|
|
114
|
+
const providerMatch = line.match(/^model_provider\s*=\s*"([^"]*)"/);
|
|
115
|
+
if (providerMatch)
|
|
116
|
+
topProvider = providerMatch[1];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const strippedTopLevel = topProvider === provider;
|
|
120
|
+
if (strippedTopLevel)
|
|
121
|
+
found = true;
|
|
122
|
+
if (!found)
|
|
123
|
+
return { text: existing, found: false, strippedTopLevel: false };
|
|
124
|
+
const kept = [];
|
|
125
|
+
section = '';
|
|
126
|
+
let skip = false;
|
|
127
|
+
for (const raw of lines) {
|
|
128
|
+
const line = raw.trim();
|
|
129
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
130
|
+
if (sectionMatch) {
|
|
131
|
+
section = sectionMatch[1];
|
|
132
|
+
skip = isScaleMaxProviderSection(section);
|
|
133
|
+
if (skip)
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (skip)
|
|
137
|
+
continue;
|
|
138
|
+
if (!section && strippedTopLevel && managedTopLevelKey.test(line))
|
|
139
|
+
continue;
|
|
140
|
+
if (isManagedCommentLine(line))
|
|
141
|
+
continue;
|
|
142
|
+
kept.push(raw);
|
|
143
|
+
}
|
|
144
|
+
return { text: kept.join('\n').trim(), found: true, strippedTopLevel };
|
|
145
|
+
}
|
|
146
|
+
/** The pre-ScaleMax top-level model / model_provider / model_reasoning_effort lines of a backup. */
|
|
147
|
+
function originalTopLevelLines(backup) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const raw of backup.split(/\r?\n/)) {
|
|
150
|
+
const line = raw.trim();
|
|
151
|
+
if (/^\[[^\]]+\]$/.test(line))
|
|
152
|
+
break;
|
|
153
|
+
if (managedTopLevelKey.test(line))
|
|
154
|
+
out.push(line);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
60
157
|
}
|
|
61
158
|
function codexAuth(apiKey, existing = {}) {
|
|
62
159
|
return JSON.stringify({ ...existing, auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2) + '\n';
|
|
@@ -111,22 +208,99 @@ async function latestRestorableBackup(file, home) {
|
|
|
111
208
|
}
|
|
112
209
|
return undefined;
|
|
113
210
|
}
|
|
114
|
-
async function
|
|
211
|
+
async function removeCodexConfig(file, ctx) {
|
|
115
212
|
if (!(await fs.pathExists(file)))
|
|
116
213
|
return { changed: false, message: 'no ScaleMax configuration found' };
|
|
117
214
|
const current = await fs.readFile(file, 'utf8').catch(() => '');
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
215
|
+
const stripped = stripScaleMaxCodexConfig(current);
|
|
216
|
+
if (!stripped.found)
|
|
217
|
+
return { changed: false, message: 'left unmanaged configuration unchanged' };
|
|
218
|
+
// Put back the user's own model settings from before setup (the first
|
|
219
|
+
// pre-ScaleMax backup) -- only when ScaleMax's were actually taken out.
|
|
220
|
+
let restoredTop = [];
|
|
221
|
+
if (stripped.strippedTopLevel) {
|
|
222
|
+
const backup = await latestRestorableBackup(file, ctx.home);
|
|
223
|
+
if (backup)
|
|
224
|
+
restoredTop = originalTopLevelLines(await fs.readFile(backup, 'utf8').catch(() => ''));
|
|
225
|
+
}
|
|
226
|
+
const next = [restoredTop.join('\n'), stripped.text].filter(Boolean).join('\n\n');
|
|
227
|
+
if (!next.trim()) {
|
|
228
|
+
await fs.remove(file);
|
|
229
|
+
return { changed: true, message: 'removed ScaleMax configuration' };
|
|
230
|
+
}
|
|
231
|
+
await atomicWrite(file, `${next}\n`, 0o600, ctx.platform);
|
|
232
|
+
return {
|
|
233
|
+
changed: true,
|
|
234
|
+
message: restoredTop.length
|
|
235
|
+
? 'removed ScaleMax settings and restored your previous model settings'
|
|
236
|
+
: 'removed ScaleMax settings; your other settings were kept',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Take the ScaleMax credential OUT of auth.json.
|
|
241
|
+
*
|
|
242
|
+
* Owned when ANY of: the file is byte-identical to what setup wrote (auth
|
|
243
|
+
* hash marker); its key is the key the CLI has saved; its key matches the
|
|
244
|
+
* key fingerprint setup wrote into config.toml. The last two survive Codex
|
|
245
|
+
* rewriting the file -- a ChatGPT token refresh or its own serialisation --
|
|
246
|
+
* which the exact hash alone did not, and which left the ScaleMax key in
|
|
247
|
+
* place after "remove". A DIFFERENT key the user put there after setup is
|
|
248
|
+
* none of these, and is left alone.
|
|
249
|
+
*
|
|
250
|
+
* Only the two fields setup sets are touched: auth_mode and OPENAI_API_KEY go
|
|
251
|
+
* back to their pre-setup values from the backup, or away if there were none.
|
|
252
|
+
* Tokens and refresh times stay as Codex last wrote them (they are fresher
|
|
253
|
+
* than the backup). A file left with nothing in it is deleted.
|
|
254
|
+
*/
|
|
255
|
+
async function removeCodexAuth(file, ctx, ownership) {
|
|
256
|
+
if (!(await fs.pathExists(file)))
|
|
257
|
+
return { changed: false, message: 'no ScaleMax configuration found' };
|
|
258
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
259
|
+
let current;
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(text);
|
|
262
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
263
|
+
throw new Error('not an object');
|
|
264
|
+
current = parsed;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
122
267
|
return { changed: false, message: 'left unmanaged configuration unchanged' };
|
|
123
|
-
const restore = await latestRestorableBackup(file, ctx.home);
|
|
124
|
-
if (restore) {
|
|
125
|
-
await atomicWrite(file, await fs.readFile(restore, 'utf8'), 0o600, ctx.platform);
|
|
126
|
-
return { changed: true, message: `restored backup ${restore}` };
|
|
127
268
|
}
|
|
128
|
-
|
|
129
|
-
|
|
269
|
+
const key = typeof current.OPENAI_API_KEY === 'string' ? current.OPENAI_API_KEY : undefined;
|
|
270
|
+
const owned = (ownership.authHash !== undefined && contentSha256(text) === ownership.authHash)
|
|
271
|
+
|| (key !== undefined && typeof ctx.apiKey === 'string' && ctx.apiKey.length > 0 && key === ctx.apiKey)
|
|
272
|
+
|| (key !== undefined && ownership.keyHash !== undefined && contentSha256(key) === ownership.keyHash)
|
|
273
|
+
|| (ownership.authHash === undefined && ownership.keyHash === undefined && ownership.legacyStructural && isManagedCodexAuth(text));
|
|
274
|
+
if (!owned)
|
|
275
|
+
return { changed: false, message: 'left unmanaged configuration unchanged' };
|
|
276
|
+
let original;
|
|
277
|
+
const backup = await latestRestorableBackup(file, ctx.home);
|
|
278
|
+
if (backup) {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(await fs.readFile(backup, 'utf8'));
|
|
281
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
282
|
+
original = parsed;
|
|
283
|
+
}
|
|
284
|
+
catch { /* an unreadable backup restores nothing */ }
|
|
285
|
+
}
|
|
286
|
+
const next = { ...current };
|
|
287
|
+
for (const field of ['auth_mode', 'OPENAI_API_KEY']) {
|
|
288
|
+
if (original && Object.prototype.hasOwnProperty.call(original, field))
|
|
289
|
+
next[field] = original[field];
|
|
290
|
+
else
|
|
291
|
+
delete next[field];
|
|
292
|
+
}
|
|
293
|
+
const meaningful = Object.values(next).some(value => value !== null && value !== undefined
|
|
294
|
+
&& !(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0));
|
|
295
|
+
if (!original && !meaningful) {
|
|
296
|
+
await fs.remove(file);
|
|
297
|
+
return { changed: true, message: 'removed ScaleMax configuration' };
|
|
298
|
+
}
|
|
299
|
+
await atomicWrite(file, JSON.stringify(next, null, 2) + '\n', 0o600, ctx.platform);
|
|
300
|
+
return {
|
|
301
|
+
changed: true,
|
|
302
|
+
message: original ? 'removed the ScaleMax key and restored your previous sign-in' : 'removed the ScaleMax key',
|
|
303
|
+
};
|
|
130
304
|
}
|
|
131
305
|
export async function verifyCodexConfiguration(ctx) {
|
|
132
306
|
const configPath = codexConfigPath(ctx.home);
|
|
@@ -261,7 +435,7 @@ export async function configure(ctx) {
|
|
|
261
435
|
}
|
|
262
436
|
catch { }
|
|
263
437
|
const nextAuth = codexAuth(ctx.apiKey, existingAuth);
|
|
264
|
-
const nextConfig = mergeCodexConfig(originalConfig, ctx.baseUrl, selectedModel(ctx), contentSha256(nextAuth));
|
|
438
|
+
const nextConfig = mergeCodexConfig(originalConfig, ctx.baseUrl, selectedModel(ctx), contentSha256(nextAuth), contentSha256(ctx.apiKey));
|
|
265
439
|
const changed = originalConfig !== nextConfig || originalAuth !== nextAuth;
|
|
266
440
|
const alreadyManaged = originalConfig.includes(GENERATED_HEADER);
|
|
267
441
|
let verification;
|
|
@@ -312,24 +486,18 @@ export async function configure(ctx) {
|
|
|
312
486
|
export async function remove(ctx) {
|
|
313
487
|
const configPath = codexConfigPath(ctx.home);
|
|
314
488
|
const authPath = codexAuthPath(ctx.home);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// configs without the marker retain the conservative structural fallback.
|
|
324
|
-
const authManagedOverride = expectedAuthHash
|
|
325
|
-
? contentSha256(authText) === expectedAuthHash
|
|
326
|
-
: configText.includes(GENERATED_HEADER)
|
|
327
|
-
? undefined
|
|
328
|
-
: false;
|
|
489
|
+
// Read the ownership markers BEFORE config.toml is rewritten below.
|
|
490
|
+
const configText = await fs.readFile(configPath, 'utf8').catch(() => '');
|
|
491
|
+
const ownership = {
|
|
492
|
+
authHash: managedAuthHash(configText),
|
|
493
|
+
keyHash: markerHash(configText, managedKeyHashPrefix),
|
|
494
|
+
// Configs written before either marker existed: the original structural rule.
|
|
495
|
+
legacyStructural: configText.includes(GENERATED_HEADER),
|
|
496
|
+
};
|
|
329
497
|
const snapshots = await captureFileSnapshots([configPath, authPath]);
|
|
330
498
|
try {
|
|
331
|
-
const config = await
|
|
332
|
-
const auth = await
|
|
499
|
+
const config = await removeCodexConfig(configPath, ctx);
|
|
500
|
+
const auth = await removeCodexAuth(authPath, ctx, ownership);
|
|
333
501
|
return {
|
|
334
502
|
id,
|
|
335
503
|
name,
|
package/dist/configure/models.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isAnthropicModel, isModelRouteAvailable, isRetiredModelId } from '../utils/api.js';
|
|
2
2
|
const preferredAnthropic = [
|
|
3
3
|
'claude-opus-5',
|
|
4
|
+
// Opus 5.5 ranks right after Opus 5, so claude-opus-5 stays the default
|
|
5
|
+
// for new installs; promoting 5.5 to the default is a separate decision.
|
|
6
|
+
'claude-opus-5-5',
|
|
4
7
|
'claude-opus-4-8',
|
|
5
8
|
'claude-sonnet-5',
|
|
6
9
|
'claude-opus-4-7',
|
|
@@ -12,9 +15,8 @@ const preferredOpenAi = [
|
|
|
12
15
|
'gpt-5.6-sol',
|
|
13
16
|
'gpt-6-sol',
|
|
14
17
|
'gpt-6-luna',
|
|
15
|
-
|
|
16
|
-
'gpt-5.
|
|
17
|
-
'gpt-5.4-mini',
|
|
18
|
+
// gpt-5.5 / gpt-5.4 / gpt-5.4-mini retired 2026-09-30.
|
|
19
|
+
'gpt-5.6-luna',
|
|
18
20
|
'gpt-5.3-codex',
|
|
19
21
|
'codex-auto-review',
|
|
20
22
|
// Gemini / Grok / Qwen ride the same OpenAI-compatible route, so they are
|
|
@@ -123,7 +125,8 @@ export function fastModel(ctx) {
|
|
|
123
125
|
}
|
|
124
126
|
export function fastOpenAiModel(ctx) {
|
|
125
127
|
const ids = openAiModelIds(ctx);
|
|
126
|
-
return ids.find(id => id === 'gpt-5.
|
|
128
|
+
return ids.find(id => id === 'gpt-5.6-luna')
|
|
129
|
+
|| ids.find(id => id === 'gpt-6-luna')
|
|
127
130
|
|| ids.find(id => /(^|[-.])(mini|nano)([-.]|$)/i.test(id));
|
|
128
131
|
}
|
|
129
132
|
/** The lightweight model for an OpenAI-compatible chat client, if one exists. */
|
|
@@ -42,7 +42,7 @@ function entries(ctx, ids, family) {
|
|
|
42
42
|
id: modelId,
|
|
43
43
|
name: modelName(ctx, modelId),
|
|
44
44
|
input: family === 'anthropic' ? ['text', 'image'] : ['text'],
|
|
45
|
-
maxTokens: modelId === 'claude-opus-5'
|
|
45
|
+
maxTokens: (modelId === 'claude-opus-5' || modelId === 'claude-opus-5-5')
|
|
46
46
|
? Math.min(metadata?.max_output_tokens || 128000, 128000)
|
|
47
47
|
: Math.min(metadata?.max_output_tokens || 4096, 4096)
|
|
48
48
|
};
|
|
@@ -24,7 +24,7 @@ function providerConfig(ctx) {
|
|
|
24
24
|
tool_call: modelSupportsTools(ctx, modelId),
|
|
25
25
|
limit: {
|
|
26
26
|
context: metadata?.context_window || (modelId.startsWith('claude-') ? 200000 : 128000),
|
|
27
|
-
output: modelId === 'claude-opus-5'
|
|
27
|
+
output: (modelId === 'claude-opus-5' || modelId === 'claude-opus-5-5')
|
|
28
28
|
? Math.min(metadata?.max_output_tokens || 128000, 128000)
|
|
29
29
|
: Math.min(metadata?.max_output_tokens || 4096, 4096)
|
|
30
30
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { ConfigureContext, ConfigureResult, RemoveContext } from '../types/client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Printed after every successful configure. Roo's built-in Ask and Architect
|
|
4
|
+
* modes carry no `command` tool group, so the model truthfully answers that
|
|
5
|
+
* terminal tools are not available and nothing runs -- customers read that as
|
|
6
|
+
* a broken key. Same advice as the docs' Roo Code guide and troubleshooting page.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ROO_CODE_MODE_TIP = "Run tasks in Code or Debug mode. Ask and Architect modes cannot run terminal commands, so Roo says \"terminal tools are not available\" there. Switch modes; nothing is wrong with your key.";
|
|
2
9
|
export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
3
10
|
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
4
11
|
export declare function remove(ctx: RemoveContext): Promise<ConfigureResult>;
|
|
@@ -18,6 +18,13 @@ const claudeProfileId = 'scalemax-claude';
|
|
|
18
18
|
const deepSeekProfileId = 'scalemax-deepseek';
|
|
19
19
|
const glmProfileId = 'scalemax-glm-5-2';
|
|
20
20
|
const rooMaximumOutputTokens = 384_000;
|
|
21
|
+
/**
|
|
22
|
+
* Printed after every successful configure. Roo's built-in Ask and Architect
|
|
23
|
+
* modes carry no `command` tool group, so the model truthfully answers that
|
|
24
|
+
* terminal tools are not available and nothing runs -- customers read that as
|
|
25
|
+
* a broken key. Same advice as the docs' Roo Code guide and troubleshooting page.
|
|
26
|
+
*/
|
|
27
|
+
export const ROO_CODE_MODE_TIP = 'Run tasks in Code or Debug mode. Ask and Architect modes cannot run terminal commands, so Roo says "terminal tools are not available" there. Switch modes; nothing is wrong with your key.';
|
|
21
28
|
function exactChatProfile(ctx, model) {
|
|
22
29
|
if (model.trim().toLowerCase() === 'glm-5.2')
|
|
23
30
|
return { name: glmProfileName, id: glmProfileId, model };
|
|
@@ -152,7 +159,7 @@ export async function configure(ctx) {
|
|
|
152
159
|
const nextProfile = handoff(ctx);
|
|
153
160
|
if (originalSettings === nextSettings && originalProfile === nextProfile) {
|
|
154
161
|
await fs.chmod(profileFile, 0o600).catch(() => undefined);
|
|
155
|
-
return { id, name, changed: false, path: profileFile, message: 'supported Roo Code auto-import handoff is already current; reload VS Code to consume it' };
|
|
162
|
+
return { id, name, changed: false, path: profileFile, message: 'supported Roo Code auto-import handoff is already current; reload VS Code to consume it', tip: ROO_CODE_MODE_TIP };
|
|
156
163
|
}
|
|
157
164
|
await backupFile(settingsFile, ctx.home);
|
|
158
165
|
await backupFile(profileFile, ctx.home);
|
|
@@ -175,6 +182,7 @@ export async function configure(ctx) {
|
|
|
175
182
|
changed: true,
|
|
176
183
|
path: profileFile,
|
|
177
184
|
message: 'created a private, supported auto-import handoff; reload VS Code so Roo Code imports the profile into SecretStorage',
|
|
185
|
+
tip: ROO_CODE_MODE_TIP,
|
|
178
186
|
rollback: snapshotRollback(snapshots, ctx.platform),
|
|
179
187
|
};
|
|
180
188
|
}
|
|
@@ -11,7 +11,7 @@ export function jsonConfig(client, apiKey, baseUrl, extra = {}) {
|
|
|
11
11
|
}, null, 2) + '\n';
|
|
12
12
|
}
|
|
13
13
|
export function continueConfig(apiKey, baseUrl) {
|
|
14
|
-
const models = ['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.
|
|
14
|
+
const models = ['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.3-codex', 'codex-auto-review', 'deepseek-v4-flash'];
|
|
15
15
|
return JSON.stringify({
|
|
16
16
|
models: models.map(model => ({
|
|
17
17
|
title: model === 'deepseek-v4-flash' ? 'ScaleMax DeepSeek V4 Flash' : `ScaleMax ${model}`,
|
|
@@ -21,7 +21,7 @@ export function continueConfig(apiKey, baseUrl) {
|
|
|
21
21
|
apiKey,
|
|
22
22
|
...(model === 'deepseek-v4-flash' ? { useResponsesApi: false } : {})
|
|
23
23
|
})),
|
|
24
|
-
tabAutocompleteModel: { title: 'ScaleMax gpt-5.
|
|
24
|
+
tabAutocompleteModel: { title: 'ScaleMax gpt-5.6-luna', provider: 'openai', model: 'gpt-5.6-luna', apiBase: baseUrl, apiKey },
|
|
25
25
|
contextProviders: [{ name: 'code' }, { name: 'docs' }, { name: 'diff' }, { name: 'terminal' }]
|
|
26
26
|
}, null, 2) + '\n';
|
|
27
27
|
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface ConfigureResult {
|
|
|
73
73
|
changed: boolean;
|
|
74
74
|
path?: string;
|
|
75
75
|
message: string;
|
|
76
|
+
/** One-line usage advice printed after a successful configure (e.g. which agent mode can run tools). */
|
|
77
|
+
tip?: string;
|
|
76
78
|
/** Restore the exact state captured immediately before this configure call. */
|
|
77
79
|
rollback?: () => Promise<void>;
|
|
78
80
|
}
|