scalemax 0.1.10 → 0.1.11
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 +17 -1
- package/dist/clients.js +1 -1
- package/dist/commands/configure.js +22 -1
- package/dist/configure/codex.d.ts +1 -0
- package/dist/configure/codex.js +32 -4
- package/dist/configure/cursor-bundle.d.ts +36 -0
- package/dist/configure/cursor-bundle.js +100 -0
- package/dist/configure/cursor-state.d.ts +19 -0
- package/dist/configure/cursor-state.js +127 -0
- package/dist/configure/cursor.d.ts +2 -0
- package/dist/configure/cursor.js +87 -9
- package/dist/detect/cursor.js +18 -2
- package/dist/types/client.d.ts +8 -0
- package/dist/utils/api.js +4 -4
- package/dist/utils/exec.d.ts +1 -1
- package/dist/utils/exec.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ SCALEMAX_API_KEY=sm_live_xxx scalemax models
|
|
|
98
98
|
|
|
99
99
|
## Supported clients
|
|
100
100
|
|
|
101
|
-
`npx scalemax`
|
|
101
|
+
`npx scalemax` supports this client set:
|
|
102
102
|
|
|
103
103
|
- Codex
|
|
104
104
|
- Cursor
|
|
@@ -112,6 +112,22 @@ SCALEMAX_API_KEY=sm_live_xxx scalemax models
|
|
|
112
112
|
|
|
113
113
|
For clients without a stable official config file, the installer writes a ScaleMax setup guide under `~/.scalemax/` instead of modifying private app storage.
|
|
114
114
|
|
|
115
|
+
### Cursor BYOK routing on macOS
|
|
116
|
+
|
|
117
|
+
Cursor can send named models through its own plan-gated router unless its local BYOK runtime is active. When you select Cursor, `npx scalemax`:
|
|
118
|
+
|
|
119
|
+
1. Fetches the complete authenticated model list from `https://api.scalemax.pro/v1/models`.
|
|
120
|
+
2. Asks before closing Cursor and modifying the application.
|
|
121
|
+
3. Verifies one known semantic patch anchor in each of three Cursor bundles before writing anything.
|
|
122
|
+
4. Stores backups outside `Cursor.app`, records SHA-256 hashes, and rolls back all files if any step fails.
|
|
123
|
+
5. Enables Cursor's local BYOK runtime without hard-coding GPT-5.5.
|
|
124
|
+
6. Stores the ScaleMax key in Cursor's native macOS protected-secret format and sets `useOpenAIKey=true` with `openAIBaseUrl=https://api.scalemax.pro/v1`.
|
|
125
|
+
7. Synchronizes every live ScaleMax model into Cursor's local-provider catalog while preserving the user's currently selected model.
|
|
126
|
+
|
|
127
|
+
Automatic Cursor patching is currently verified on macOS only. Unknown, partial, or changed Cursor bundle signatures fail closed without modifying the app. Cursor updates can replace the patched bundles; rerun `npx scalemax@latest` after an update. `scalemax remove` restores only bundle versions and protected-state values managed by ScaleMax, and refuses to overwrite files changed afterward.
|
|
128
|
+
|
|
129
|
+
For explicitly approved non-interactive automation, set `SCALEMAX_CURSOR_PATCH_CONSENT=1`. Without it, non-interactive Cursor modification is skipped safely.
|
|
130
|
+
|
|
115
131
|
## Examples
|
|
116
132
|
|
|
117
133
|
List models:
|
package/dist/clients.js
CHANGED
|
@@ -18,7 +18,7 @@ import * as apiCodeDetect from './detect/api-code.js';
|
|
|
18
18
|
import * as apiCodeConfig from './configure/api-code.js';
|
|
19
19
|
export const clients = [
|
|
20
20
|
{ id: 'codex', name: 'Codex', detect: codexDetect.detect, install: codexConfig.install, configure: codexConfig.configure, verify: codexConfig.verify, launch: codexConfig.launch, remove: codexConfig.remove },
|
|
21
|
-
{ id: 'cursor', name: 'Cursor', detect: cursorDetect.detect, configure: cursorConfig.configure, remove: cursorConfig.remove },
|
|
21
|
+
{ id: 'cursor', name: 'Cursor', detect: cursorDetect.detect, configure: cursorConfig.configure, verify: cursorConfig.verify, remove: cursorConfig.remove },
|
|
22
22
|
{ id: 'trae-solo', name: 'TRAE SOLO', detect: traeSoloDetect.detect, configure: traeSoloConfig.configure, remove: traeSoloConfig.remove },
|
|
23
23
|
{ id: 'claude', name: 'Claude Code', detect: claudeDetect.detect, configure: claudeConfig.configure, remove: claudeConfig.remove },
|
|
24
24
|
{ id: 'opencode', name: 'OpenCode', detect: opencodeDetect.detect, configure: opencodeConfig.configure, remove: opencodeConfig.remove },
|
|
@@ -57,6 +57,10 @@ export async function configureCommand(opts = {}) {
|
|
|
57
57
|
if (!apiKey)
|
|
58
58
|
throw new Error('No ScaleMax API key found. Run `scalemax login` first.');
|
|
59
59
|
const keyInfo = await validateApiKey(apiKey);
|
|
60
|
+
if (!keyInfo.ok)
|
|
61
|
+
throw new Error(keyInfo.error || 'ScaleMax API key validation failed.');
|
|
62
|
+
if (!keyInfo.models.length)
|
|
63
|
+
throw new Error(keyInfo.error || 'ScaleMax returned no models for this API key. No client configuration was changed.');
|
|
60
64
|
const providerScope = keyInfo.key?.provider_scope || 'both';
|
|
61
65
|
const availableClients = clients.filter(c => clientAllowedForScope(c.id, providerScope));
|
|
62
66
|
console.log(chalk.dim(`API key scope: ${scopeHint(providerScope)}. Incompatible clients are hidden.`));
|
|
@@ -82,6 +86,23 @@ export async function configureCommand(opts = {}) {
|
|
|
82
86
|
}
|
|
83
87
|
const successful = new Set();
|
|
84
88
|
for (const client of availableClients.filter(c => chosen.includes(c.id))) {
|
|
89
|
+
let allowAppModification = false;
|
|
90
|
+
if (client.id === 'cursor') {
|
|
91
|
+
if (process.stdin.isTTY) {
|
|
92
|
+
const approved = await confirm({
|
|
93
|
+
message: 'Cursor BYOK requires closing Cursor and modifying 3 application bundles. ScaleMax will back up every file and can restore managed changes. Continue?',
|
|
94
|
+
initialValue: false
|
|
95
|
+
});
|
|
96
|
+
if (isCancel(approved) || approved === false) {
|
|
97
|
+
console.log(chalk.yellow('Cursor setup skipped.'));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
allowAppModification = true;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
allowAppModification = process.env.SCALEMAX_CURSOR_PATCH_CONSENT === '1';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
85
106
|
const baseCtx = { home, platform: process.platform, env: process.env };
|
|
86
107
|
if (client.install) {
|
|
87
108
|
const spinner = ora(`Checking ${client.name}`).start();
|
|
@@ -96,7 +117,7 @@ export async function configureCommand(opts = {}) {
|
|
|
96
117
|
}
|
|
97
118
|
const spinner = ora(`Configuring ${client.name}`).start();
|
|
98
119
|
try {
|
|
99
|
-
const ctx = { ...baseCtx, apiKey, baseUrl: SCALEMAX_BASE_URL, providerScope };
|
|
120
|
+
const ctx = { ...baseCtx, apiKey, baseUrl: SCALEMAX_BASE_URL, providerScope, models: keyInfo.models, interactive: Boolean(process.stdin.isTTY), allowAppModification };
|
|
100
121
|
const res = await client.configure(ctx);
|
|
101
122
|
spinner.succeed(`${client.name} ${res.message}`);
|
|
102
123
|
if (client.verify) {
|
|
@@ -6,6 +6,7 @@ export interface CodexVerificationResult {
|
|
|
6
6
|
configPath: string;
|
|
7
7
|
authPath: string;
|
|
8
8
|
}
|
|
9
|
+
export declare function mergeCodexConfig(existing: string, baseUrl: string): string;
|
|
9
10
|
export declare function verifyCodexConfiguration(ctx: ConfigureContext): Promise<CodexVerificationResult>;
|
|
10
11
|
export declare function install(ctx: InstallContext): Promise<ConfigureResult>;
|
|
11
12
|
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
package/dist/configure/codex.js
CHANGED
|
@@ -9,8 +9,33 @@ const id = 'codex';
|
|
|
9
9
|
const name = 'Codex';
|
|
10
10
|
const provider = 'scalemax';
|
|
11
11
|
const model = 'gpt-5.5';
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const reasoningEffort = 'medium';
|
|
13
|
+
function scalemaxCodexBlock(baseUrl) {
|
|
14
|
+
return `# ${GENERATED_HEADER}\nmodel = "${model}"\nmodel_provider = "${provider}"\nmodel_reasoning_effort = "${reasoningEffort}"\n# Recommended ScaleMax Codex models: gpt-5.5, gpt-5.6-terra, gpt-5.6-sol, gpt-5.6-luna, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, fable-5, fable-5-ultra.\n# Provider-internal gpt-5.3-codex-spark is routed behind the public gpt-5.3-codex Codex slug.\n\n[model_providers.${provider}]\nname = "ScaleMax"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
|
|
15
|
+
}
|
|
16
|
+
export function mergeCodexConfig(existing, baseUrl) {
|
|
17
|
+
const preserved = [];
|
|
18
|
+
let section = '';
|
|
19
|
+
let skipScaleMaxProvider = false;
|
|
20
|
+
for (const raw of existing.split(/\r?\n/)) {
|
|
21
|
+
const line = raw.trim();
|
|
22
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
23
|
+
if (sectionMatch) {
|
|
24
|
+
section = sectionMatch[1];
|
|
25
|
+
skipScaleMaxProvider = section === `model_providers.${provider}`;
|
|
26
|
+
if (skipScaleMaxProvider)
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (skipScaleMaxProvider)
|
|
30
|
+
continue;
|
|
31
|
+
if (!section && /^(model|model_provider|model_reasoning_effort|disable_response_storage)\s*=/.test(line))
|
|
32
|
+
continue;
|
|
33
|
+
if (line === `# ${GENERATED_HEADER}`)
|
|
34
|
+
continue;
|
|
35
|
+
preserved.push(raw);
|
|
36
|
+
}
|
|
37
|
+
const tail = preserved.join('\n').trim();
|
|
38
|
+
return `${scalemaxCodexBlock(baseUrl)}${tail ? `\n${tail}\n` : ''}`;
|
|
14
39
|
}
|
|
15
40
|
function codexAuth(apiKey) {
|
|
16
41
|
return JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2) + '\n';
|
|
@@ -99,6 +124,8 @@ export async function verifyCodexConfiguration(ctx) {
|
|
|
99
124
|
errors.push(`Codex model mismatch: expected ${model}`);
|
|
100
125
|
if (toml.model_provider !== provider)
|
|
101
126
|
errors.push(`Codex model_provider mismatch: expected ${provider}`);
|
|
127
|
+
if (toml.model_reasoning_effort !== reasoningEffort)
|
|
128
|
+
errors.push(`Codex model_reasoning_effort mismatch: expected ${reasoningEffort}`);
|
|
102
129
|
if (toml[`model_providers.${provider}.base_url`] !== ctx.baseUrl)
|
|
103
130
|
errors.push(`Codex base_url mismatch: expected ${ctx.baseUrl}`);
|
|
104
131
|
if (toml[`model_providers.${provider}.wire_api`] !== 'responses')
|
|
@@ -120,7 +147,7 @@ export async function verifyCodexConfiguration(ctx) {
|
|
|
120
147
|
errors.push(`Cannot stat ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
121
148
|
}
|
|
122
149
|
}
|
|
123
|
-
if (await commandExists('codex')) {
|
|
150
|
+
if (await commandExists('codex', ctx.env)) {
|
|
124
151
|
try {
|
|
125
152
|
const result = await execa('codex', ['doctor', '--json'], {
|
|
126
153
|
env: { ...process.env, CODEX_HOME: path.join(ctx.home, '.codex') },
|
|
@@ -189,7 +216,8 @@ export async function configure(ctx) {
|
|
|
189
216
|
if (backup)
|
|
190
217
|
backups.push(backup);
|
|
191
218
|
}
|
|
192
|
-
await
|
|
219
|
+
const existingConfig = await fs.readFile(configPath, 'utf8').catch(() => '');
|
|
220
|
+
await atomicWrite(configPath, mergeCodexConfig(existingConfig, ctx.baseUrl), 0o600);
|
|
193
221
|
await atomicWrite(authPath, codexAuth(ctx.apiKey), 0o600);
|
|
194
222
|
if (legacyConfig.endsWith('config.json') && await fs.pathExists(legacyConfig))
|
|
195
223
|
await fs.remove(legacyConfig);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const CURSOR_BUNDLE_RELATIVE_PATHS: readonly ["Contents/Resources/app/out/main.js", "Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js", "Contents/Resources/app/out/vs/workbench/workbench.glass.main.js"];
|
|
2
|
+
export type CursorBundleStatus = 'unpatched' | 'patched' | 'unsupported' | 'incomplete';
|
|
3
|
+
export interface CursorBundleInspection {
|
|
4
|
+
path: string;
|
|
5
|
+
status: 'unpatched' | 'patched' | 'unsupported';
|
|
6
|
+
originalMatches: number;
|
|
7
|
+
patchedMatches: number;
|
|
8
|
+
sha256: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CursorBundleRecord {
|
|
11
|
+
path: string;
|
|
12
|
+
relativePath: string;
|
|
13
|
+
originalSha256: string;
|
|
14
|
+
patchedSha256: string;
|
|
15
|
+
backupPath?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CursorBundlePatchResult {
|
|
18
|
+
status: 'patched' | 'already-patched';
|
|
19
|
+
managed: boolean;
|
|
20
|
+
records: CursorBundleRecord[];
|
|
21
|
+
backupRoot?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function cursorAppPath(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string;
|
|
24
|
+
export declare function cursorBundlePaths(appPath: string): string[];
|
|
25
|
+
export declare function sha256Buffer(value: Buffer | string): string;
|
|
26
|
+
export declare function inspectCursorBundle(file: string): Promise<CursorBundleInspection>;
|
|
27
|
+
export declare function inspectCursorBundles(appPath: string): Promise<{
|
|
28
|
+
status: CursorBundleStatus;
|
|
29
|
+
bundles: CursorBundleInspection[];
|
|
30
|
+
}>;
|
|
31
|
+
export declare function patchCursorBundles(appPath: string, home: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): Promise<CursorBundlePatchResult>;
|
|
32
|
+
export declare function restoreCursorBundles(records: CursorBundleRecord[]): Promise<boolean>;
|
|
33
|
+
export declare const cursorPatchSignatures: {
|
|
34
|
+
original: string;
|
|
35
|
+
patched: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { atomicWrite } from '../utils/fs.js';
|
|
5
|
+
import { scaleMaxDir, timestamp } from '../utils/paths.js';
|
|
6
|
+
export const CURSOR_BUNDLE_RELATIVE_PATHS = [
|
|
7
|
+
'Contents/Resources/app/out/main.js',
|
|
8
|
+
'Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js',
|
|
9
|
+
'Contents/Resources/app/out/vs/workbench/workbench.glass.main.js'
|
|
10
|
+
];
|
|
11
|
+
const FLAGS_PREFIX = 'enableEmbeddingsModelToggle:!1,enableCPPControlTokenToggle:!1,cursorPredictionOptions:!1,localMode:';
|
|
12
|
+
const ORIGINAL = `${FLAGS_PREFIX}!1`;
|
|
13
|
+
const PATCHED = `${FLAGS_PREFIX}!0`;
|
|
14
|
+
export function cursorAppPath(platform, env) {
|
|
15
|
+
if (env.SCALEMAX_CURSOR_APP_PATH)
|
|
16
|
+
return path.resolve(env.SCALEMAX_CURSOR_APP_PATH);
|
|
17
|
+
if (platform === 'darwin')
|
|
18
|
+
return '/Applications/Cursor.app';
|
|
19
|
+
throw new Error(`Automatic Cursor BYOK setup is currently verified on macOS only (${platform} is not modified).`);
|
|
20
|
+
}
|
|
21
|
+
export function cursorBundlePaths(appPath) { return CURSOR_BUNDLE_RELATIVE_PATHS.map(p => path.join(appPath, p)); }
|
|
22
|
+
export function sha256Buffer(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
|
|
23
|
+
function count(text, needle) { let n = 0, from = 0; while ((from = text.indexOf(needle, from)) !== -1) {
|
|
24
|
+
n++;
|
|
25
|
+
from += needle.length;
|
|
26
|
+
} return n; }
|
|
27
|
+
export async function inspectCursorBundle(file) {
|
|
28
|
+
const text = await fs.readFile(file, 'utf8');
|
|
29
|
+
const originalMatches = count(text, ORIGINAL), patchedMatches = count(text, PATCHED);
|
|
30
|
+
const status = originalMatches === 1 && patchedMatches === 0 ? 'unpatched' : patchedMatches === 1 && originalMatches === 0 ? 'patched' : 'unsupported';
|
|
31
|
+
return { path: file, status, originalMatches, patchedMatches, sha256: sha256Buffer(text) };
|
|
32
|
+
}
|
|
33
|
+
export async function inspectCursorBundles(appPath) {
|
|
34
|
+
const files = cursorBundlePaths(appPath);
|
|
35
|
+
for (const file of files)
|
|
36
|
+
if (!(await fs.pathExists(file)))
|
|
37
|
+
throw new Error(`Cursor bundle is missing: ${file}`);
|
|
38
|
+
const bundles = await Promise.all(files.map(inspectCursorBundle));
|
|
39
|
+
if (bundles.every(b => b.status === 'unpatched'))
|
|
40
|
+
return { status: 'unpatched', bundles };
|
|
41
|
+
if (bundles.every(b => b.status === 'patched'))
|
|
42
|
+
return { status: 'patched', bundles };
|
|
43
|
+
if (bundles.some(b => b.status === 'unsupported'))
|
|
44
|
+
return { status: 'unsupported', bundles };
|
|
45
|
+
return { status: 'incomplete', bundles };
|
|
46
|
+
}
|
|
47
|
+
export async function patchCursorBundles(appPath, home, platform, env) {
|
|
48
|
+
const before = await inspectCursorBundles(appPath);
|
|
49
|
+
if (before.status === 'patched')
|
|
50
|
+
return { status: 'already-patched', managed: false, records: before.bundles.map((b, i) => ({ path: b.path, relativePath: CURSOR_BUNDLE_RELATIVE_PATHS[i], originalSha256: '', patchedSha256: b.sha256 })) };
|
|
51
|
+
if (before.status !== 'unpatched')
|
|
52
|
+
throw new Error(`Unsupported or partial Cursor bundle state (${before.status}). No files were changed.`);
|
|
53
|
+
const backupRoot = path.join(scaleMaxDir(home, platform, env), 'cursor', 'backups', timestamp());
|
|
54
|
+
const records = [];
|
|
55
|
+
await fs.ensureDir(backupRoot, { mode: 0o700 });
|
|
56
|
+
try {
|
|
57
|
+
for (let i = 0; i < before.bundles.length; i++) {
|
|
58
|
+
const b = before.bundles[i], relativePath = CURSOR_BUNDLE_RELATIVE_PATHS[i], backupPath = path.join(backupRoot, relativePath);
|
|
59
|
+
await fs.ensureDir(path.dirname(backupPath), { mode: 0o700 });
|
|
60
|
+
await fs.copy(b.path, backupPath, { overwrite: false, errorOnExist: true, preserveTimestamps: true });
|
|
61
|
+
const mode = (await fs.stat(b.path)).mode & 0o777;
|
|
62
|
+
const original = await fs.readFile(b.path, 'utf8'), patched = original.replace(ORIGINAL, PATCHED);
|
|
63
|
+
if (patched === original)
|
|
64
|
+
throw new Error(`Cursor patch anchor was not changed in ${b.path}`);
|
|
65
|
+
await atomicWrite(b.path, patched, mode);
|
|
66
|
+
records.push({ path: b.path, relativePath, originalSha256: b.sha256, patchedSha256: sha256Buffer(patched), backupPath });
|
|
67
|
+
}
|
|
68
|
+
const after = await inspectCursorBundles(appPath);
|
|
69
|
+
if (after.status !== 'patched' || records.some((r, i) => r.patchedSha256 !== after.bundles[i].sha256))
|
|
70
|
+
throw new Error('Cursor bundle postcondition failed.');
|
|
71
|
+
return { status: 'patched', managed: true, records, backupRoot };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
for (const r of records.slice().reverse())
|
|
75
|
+
if (r.backupPath && await fs.pathExists(r.backupPath))
|
|
76
|
+
await fs.copy(r.backupPath, r.path, { overwrite: true, preserveTimestamps: true }).catch(() => undefined);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function restoreCursorBundles(records) {
|
|
81
|
+
if (!records.length || records.some(r => !r.backupPath))
|
|
82
|
+
return false;
|
|
83
|
+
for (const r of records) {
|
|
84
|
+
if (!(await fs.pathExists(r.path)) || !(await fs.pathExists(r.backupPath)))
|
|
85
|
+
throw new Error(`Cursor restore file is missing: ${r.path}`);
|
|
86
|
+
const current = sha256Buffer(await fs.readFile(r.path));
|
|
87
|
+
const backup = sha256Buffer(await fs.readFile(r.backupPath));
|
|
88
|
+
if (current !== r.patchedSha256)
|
|
89
|
+
throw new Error(`Cursor changed after ScaleMax setup; refusing to overwrite ${r.path}.`);
|
|
90
|
+
if (backup !== r.originalSha256)
|
|
91
|
+
throw new Error(`Cursor backup hash mismatch: ${r.backupPath}`);
|
|
92
|
+
}
|
|
93
|
+
for (const r of records)
|
|
94
|
+
await fs.copy(r.backupPath, r.path, { overwrite: true, preserveTimestamps: true });
|
|
95
|
+
const restored = await Promise.all(records.map(async (r) => sha256Buffer(await fs.readFile(r.path)) === r.originalSha256));
|
|
96
|
+
if (restored.some(v => !v))
|
|
97
|
+
throw new Error('Cursor restore verification failed.');
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
export const cursorPatchSignatures = { original: ORIGINAL, patched: PATCHED };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ModelInfo } from '../utils/api.js';
|
|
2
|
+
export declare const CURSOR_STATE_KEY = "src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser";
|
|
3
|
+
export declare const CURSOR_SECRET_KEY = "secret://cursorAuth/openAIKey";
|
|
4
|
+
export interface CursorStateRecord {
|
|
5
|
+
dbPath: string;
|
|
6
|
+
backupPath: string;
|
|
7
|
+
originalState: string;
|
|
8
|
+
configuredState: string;
|
|
9
|
+
originalSecret?: string;
|
|
10
|
+
configuredSecret: string;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
modelIds: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function cursorStateDbPath(home: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string;
|
|
15
|
+
export declare function normalizeCursorBaseUrl(value: string): string;
|
|
16
|
+
export declare function mergeCursorState(current: Record<string, any>, models: ModelInfo[], baseUrl: string): Record<string, any>;
|
|
17
|
+
export declare function encryptCursorSecret(password: string, apiKey: string): string;
|
|
18
|
+
export declare function configureCursorState(home: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv, apiKey: string, baseUrl: string, models: ModelInfo[], backupRoot: string): Promise<CursorStateRecord>;
|
|
19
|
+
export declare function restoreCursorState(record: CursorStateRecord): Promise<boolean>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
export const CURSOR_STATE_KEY = 'src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser';
|
|
6
|
+
export const CURSOR_SECRET_KEY = 'secret://cursorAuth/openAIKey';
|
|
7
|
+
const KEYCHAIN_SERVICE = 'Cursor Safe Storage';
|
|
8
|
+
export function cursorStateDbPath(home, platform, env) {
|
|
9
|
+
if (env.SCALEMAX_CURSOR_STATE_DB)
|
|
10
|
+
return path.resolve(env.SCALEMAX_CURSOR_STATE_DB);
|
|
11
|
+
if (platform !== 'darwin')
|
|
12
|
+
throw new Error(`Cursor protected-state setup is currently verified on macOS only (${platform} is not modified).`);
|
|
13
|
+
return path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
14
|
+
}
|
|
15
|
+
export function normalizeCursorBaseUrl(value) {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash)
|
|
18
|
+
throw new Error('Cursor ScaleMax URL must be a clean HTTPS URL.');
|
|
19
|
+
url.pathname = url.pathname.replace(/\/+$/, '');
|
|
20
|
+
if (!url.pathname.endsWith('/v1'))
|
|
21
|
+
url.pathname = `${url.pathname}/v1`.replace(/\/+/g, '/');
|
|
22
|
+
return url.toString().replace(/\/$/, '');
|
|
23
|
+
}
|
|
24
|
+
function genericModelEntry(id) { return { name: id, defaultOn: true, parameterDefinitions: [], variants: [], legacySlugs: [], idAliases: [], cloudAgentEffortModes: [], modelPickerBadges: [], supportsAgent: true, tooltipData: { primaryText: '', secondaryText: '', secondaryWarningText: false, icon: '', tertiaryText: '', tertiaryTextUrl: '', markdownContent: 'Discovered from your local provider.' }, supportsThinking: false, supportsImages: false, supportsNonMaxMode: true, clientDisplayName: id, serverModelName: id, supportsPlanMode: true, supportsSandboxing: true, inputboxShortModelName: id, supportsCmdK: false, namedModelSectionIndex: 1, tagline: 'Local provider model' }; }
|
|
25
|
+
function isLocalProviderEntry(value) { return value?.tagline === 'Local provider model' || value?.tooltipData?.markdownContent === 'Discovered from your local provider.'; }
|
|
26
|
+
export function mergeCursorState(current, models, baseUrl) {
|
|
27
|
+
const ids = [...new Set(models.map(m => String(m?.id || '').trim()).filter(Boolean))];
|
|
28
|
+
if (!ids.length)
|
|
29
|
+
throw new Error('ScaleMax returned no models; Cursor was not changed.');
|
|
30
|
+
const existing = Array.isArray(current.availableDefaultModels2) ? current.availableDefaultModels2 : [];
|
|
31
|
+
const byId = new Map(existing.map((m) => [String(m?.serverModelName || m?.name || ''), m]));
|
|
32
|
+
const preserved = existing.filter((m) => !isLocalProviderEntry(m));
|
|
33
|
+
const local = ids.map(id => byId.get(id) || genericModelEntry(id));
|
|
34
|
+
return { ...current, useOpenAIKey: true, openAIBaseUrl: normalizeCursorBaseUrl(baseUrl), localProviderModelIds: ids, availableDefaultModels2: [...preserved, ...local] };
|
|
35
|
+
}
|
|
36
|
+
export function encryptCursorSecret(password, apiKey) {
|
|
37
|
+
const key = crypto.pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1'), iv = Buffer.alloc(16, 0x20), cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
|
|
38
|
+
const encrypted = Buffer.concat([Buffer.from('v10'), cipher.update(apiKey, 'utf8'), cipher.final()]);
|
|
39
|
+
return JSON.stringify(encrypted);
|
|
40
|
+
}
|
|
41
|
+
async function sqliteValue(db, key) {
|
|
42
|
+
const sql = `SELECT hex(value) FROM ItemTable WHERE key='${key.replaceAll("'", "''")}';`;
|
|
43
|
+
const result = await execa('/usr/bin/sqlite3', [db, sql], { reject: false });
|
|
44
|
+
if (result.exitCode !== 0)
|
|
45
|
+
throw new Error('Unable to read Cursor protected state. Close Cursor and retry.');
|
|
46
|
+
const hex = result.stdout.trim();
|
|
47
|
+
return hex ? Buffer.from(hex, 'hex').toString('utf8') : undefined;
|
|
48
|
+
}
|
|
49
|
+
function sqlText(value) { return `CAST(X'${Buffer.from(value, 'utf8').toString('hex')}' AS TEXT)`; }
|
|
50
|
+
async function writeRows(db, state, secret) {
|
|
51
|
+
const secretSql = secret === undefined ? `DELETE FROM ItemTable WHERE key='${CURSOR_SECRET_KEY}';` : `INSERT INTO ItemTable(key,value) VALUES('${CURSOR_SECRET_KEY}',${sqlText(secret)}) ON CONFLICT(key) DO UPDATE SET value=excluded.value;`;
|
|
52
|
+
const sql = `BEGIN IMMEDIATE; INSERT INTO ItemTable(key,value) VALUES('${CURSOR_STATE_KEY}',${sqlText(state)}) ON CONFLICT(key) DO UPDATE SET value=excluded.value; ${secretSql} COMMIT;`;
|
|
53
|
+
const result = await execa('/usr/bin/sqlite3', [db], { input: sql, reject: false });
|
|
54
|
+
if (result.exitCode !== 0)
|
|
55
|
+
throw new Error('Unable to update Cursor protected state. No API key was printed.');
|
|
56
|
+
}
|
|
57
|
+
async function keychainPassword() {
|
|
58
|
+
const found = await execa('/usr/bin/security', ['find-generic-password', '-w', '-s', KEYCHAIN_SERVICE], { reject: false });
|
|
59
|
+
if (found.exitCode === 0 && found.stdout)
|
|
60
|
+
return found.stdout.trimEnd();
|
|
61
|
+
const password = crypto.randomBytes(32).toString('base64url');
|
|
62
|
+
const add = await execa('/usr/bin/security', ['add-generic-password', '-U', '-a', 'Cursor', '-s', KEYCHAIN_SERVICE, '-w', password], { reject: false });
|
|
63
|
+
if (add.exitCode !== 0)
|
|
64
|
+
throw new Error('Cursor Safe Storage is unavailable. Launch Cursor once, close it, and retry.');
|
|
65
|
+
return password;
|
|
66
|
+
}
|
|
67
|
+
export async function configureCursorState(home, platform, env, apiKey, baseUrl, models, backupRoot) {
|
|
68
|
+
const dbPath = cursorStateDbPath(home, platform, env);
|
|
69
|
+
if (!(await fs.pathExists(dbPath)))
|
|
70
|
+
throw new Error('Cursor state database was not found. Launch Cursor once, close it, and retry.');
|
|
71
|
+
const originalState = await sqliteValue(dbPath, CURSOR_STATE_KEY);
|
|
72
|
+
if (!originalState)
|
|
73
|
+
throw new Error('Cursor application state was not initialized. Launch Cursor once, close it, and retry.');
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(originalState);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error('Cursor application state is not valid JSON. No changes were made.');
|
|
80
|
+
}
|
|
81
|
+
const configured = mergeCursorState(parsed, models, baseUrl), configuredState = JSON.stringify(configured);
|
|
82
|
+
const originalSecret = await sqliteValue(dbPath, CURSOR_SECRET_KEY), configuredSecret = encryptCursorSecret(await keychainPassword(), apiKey);
|
|
83
|
+
const backupPath = path.join(backupRoot, 'state.vscdb');
|
|
84
|
+
await fs.ensureDir(path.dirname(backupPath), { mode: 0o700 });
|
|
85
|
+
await fs.copy(dbPath, backupPath, { overwrite: false, errorOnExist: true });
|
|
86
|
+
await fs.chmod(backupPath, 0o600).catch(() => undefined);
|
|
87
|
+
try {
|
|
88
|
+
await writeRows(dbPath, configuredState, configuredSecret);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
await fs.copy(backupPath, dbPath, { overwrite: true });
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
const verifyState = await sqliteValue(dbPath, CURSOR_STATE_KEY), verifySecret = await sqliteValue(dbPath, CURSOR_SECRET_KEY);
|
|
95
|
+
if (verifyState !== configuredState || verifySecret !== configuredSecret) {
|
|
96
|
+
await fs.copy(backupPath, dbPath, { overwrite: true });
|
|
97
|
+
throw new Error('Cursor protected-state verification failed; original database restored.');
|
|
98
|
+
}
|
|
99
|
+
return { dbPath, backupPath, originalState, configuredState, originalSecret, configuredSecret, baseUrl: normalizeCursorBaseUrl(baseUrl), modelIds: configured.localProviderModelIds };
|
|
100
|
+
}
|
|
101
|
+
export async function restoreCursorState(record) {
|
|
102
|
+
if (!(await fs.pathExists(record.dbPath)))
|
|
103
|
+
return false;
|
|
104
|
+
const currentStateText = await sqliteValue(record.dbPath, CURSOR_STATE_KEY);
|
|
105
|
+
if (!currentStateText)
|
|
106
|
+
return false;
|
|
107
|
+
let current, original, configured;
|
|
108
|
+
try {
|
|
109
|
+
current = JSON.parse(currentStateText);
|
|
110
|
+
original = JSON.parse(record.originalState);
|
|
111
|
+
configured = JSON.parse(record.configuredState);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const restoreField = (key) => { if (JSON.stringify(current[key]) === JSON.stringify(configured[key])) {
|
|
117
|
+
if (Object.prototype.hasOwnProperty.call(original, key))
|
|
118
|
+
current[key] = original[key];
|
|
119
|
+
else
|
|
120
|
+
delete current[key];
|
|
121
|
+
} };
|
|
122
|
+
for (const key of ['useOpenAIKey', 'openAIBaseUrl', 'localProviderModelIds', 'availableDefaultModels2'])
|
|
123
|
+
restoreField(key);
|
|
124
|
+
const currentSecret = await sqliteValue(record.dbPath, CURSOR_SECRET_KEY), secret = currentSecret === record.configuredSecret ? record.originalSecret : currentSecret;
|
|
125
|
+
await writeRows(record.dbPath, JSON.stringify(current), secret);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { ConfigureContext, ConfigureResult } from '../types/client.js';
|
|
2
|
+
export declare function isCursorRunning(appPath: string): Promise<boolean>;
|
|
2
3
|
export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
4
|
+
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
3
5
|
export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
|
package/dist/configure/cursor.js
CHANGED
|
@@ -1,13 +1,91 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { scaleMaxDir, timestamp } from '../utils/paths.js';
|
|
5
|
+
import { writeJsonAtomic, readJson } from '../utils/fs.js';
|
|
6
|
+
import { cursorAppPath, inspectCursorBundles, patchCursorBundles, restoreCursorBundles } from './cursor-bundle.js';
|
|
7
|
+
import { configureCursorState, restoreCursorState } from './cursor-state.js';
|
|
8
|
+
const id = 'cursor', name = 'Cursor';
|
|
9
|
+
function manifestPath(ctx) { return path.join(scaleMaxDir(ctx.home, ctx.platform, ctx.env), 'cursor', 'manifest.json'); }
|
|
10
|
+
async function cursorVersion(appPath) { try {
|
|
11
|
+
return (await fs.readJson(path.join(appPath, 'Contents', 'Resources', 'app', 'package.json')))?.version;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
} }
|
|
16
|
+
export async function isCursorRunning(appPath) { const r = await execa('/usr/bin/pgrep', ['-f', `${appPath}/Contents/MacOS/Cursor`], { reject: false }); return r.exitCode === 0 && Boolean(r.stdout.trim()); }
|
|
17
|
+
async function stopCursor(appPath) { await execa('/usr/bin/osascript', ['-e', 'tell application "Cursor" to quit'], { reject: false }); for (let i = 0; i < 30; i++) {
|
|
18
|
+
if (!(await isCursorRunning(appPath)))
|
|
19
|
+
return;
|
|
20
|
+
await new Promise(r => setTimeout(r, 500));
|
|
21
|
+
} throw new Error('Cursor is still running. Close Cursor completely and rerun `npx scalemax`.'); }
|
|
22
|
+
async function launchCursor(appPath) { await execa('/usr/bin/open', ['-a', appPath], { reject: false }); }
|
|
6
23
|
export async function configure(ctx) {
|
|
7
|
-
|
|
8
|
-
|
|
24
|
+
if (ctx.platform !== 'darwin')
|
|
25
|
+
return { id, name, changed: false, message: `automatic Cursor BYOK setup is currently verified on macOS only; ${ctx.platform} was not modified` };
|
|
26
|
+
const appPath = cursorAppPath(ctx.platform, ctx.env);
|
|
27
|
+
if (!(await fs.pathExists(appPath)))
|
|
28
|
+
return { id, name, changed: false, path: appPath, message: 'Cursor.app was not found' };
|
|
29
|
+
const inspection = await inspectCursorBundles(appPath);
|
|
30
|
+
if (inspection.status === 'unsupported' || inspection.status === 'incomplete')
|
|
31
|
+
throw new Error(`Unsupported or partial Cursor bundle state (${inspection.status}). No files were changed.`);
|
|
32
|
+
if (inspection.status === 'unpatched' && !ctx.allowAppModification)
|
|
33
|
+
throw new Error('Cursor BYOK requires a reviewed three-bundle local-mode patch. Rerun interactively and approve the Cursor modification prompt.');
|
|
34
|
+
const wasRunning = await isCursorRunning(appPath);
|
|
35
|
+
if (wasRunning) {
|
|
36
|
+
if (!ctx.allowAppModification)
|
|
37
|
+
throw new Error('Close Cursor completely and rerun `npx scalemax`.');
|
|
38
|
+
await stopCursor(appPath);
|
|
39
|
+
}
|
|
40
|
+
const prior = await readJson(manifestPath(ctx), undefined);
|
|
41
|
+
let patchResult, state;
|
|
42
|
+
const stateBackupRoot = path.join(scaleMaxDir(ctx.home, ctx.platform, ctx.env), 'cursor', 'backups', timestamp());
|
|
43
|
+
try {
|
|
44
|
+
patchResult = await patchCursorBundles(appPath, ctx.home, ctx.platform, ctx.env);
|
|
45
|
+
const backupRoot = patchResult.backupRoot || stateBackupRoot;
|
|
46
|
+
await fs.ensureDir(backupRoot, { mode: 0o700 });
|
|
47
|
+
state = await configureCursorState(ctx.home, ctx.platform, ctx.env, ctx.apiKey, ctx.baseUrl, ctx.models || [], backupRoot);
|
|
48
|
+
if (prior?.state) {
|
|
49
|
+
state.originalState = prior.state.originalState;
|
|
50
|
+
state.originalSecret = prior.state.originalSecret;
|
|
51
|
+
}
|
|
52
|
+
const bundles = patchResult.managed ? patchResult.records : (prior?.bundleManaged ? prior.bundles : patchResult.records);
|
|
53
|
+
const manifest = { schemaVersion: 1, createdAt: new Date().toISOString(), appPath, version: await cursorVersion(appPath), bundleManaged: Boolean(patchResult.managed || prior?.bundleManaged), bundles, state };
|
|
54
|
+
await writeJsonAtomic(manifestPath(ctx), manifest, 0o600);
|
|
55
|
+
if (wasRunning)
|
|
56
|
+
await launchCursor(appPath);
|
|
57
|
+
return { id, name, changed: true, path: manifestPath(ctx), message: `configured protected BYOK routing with ${state.modelIds.length} live ScaleMax models${patchResult.status === 'already-patched' ? ' (Cursor patch already active)' : ' (Cursor patch backed up)'}` };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (state)
|
|
61
|
+
await restoreCursorState(state).catch(() => undefined);
|
|
62
|
+
if (patchResult?.managed)
|
|
63
|
+
await restoreCursorBundles(patchResult.records).catch(() => undefined);
|
|
64
|
+
if (wasRunning)
|
|
65
|
+
await launchCursor(appPath).catch(() => undefined);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function verify(ctx) {
|
|
70
|
+
if (ctx.platform !== 'darwin')
|
|
71
|
+
return { id, name, changed: false, message: 'verification is currently available on macOS only' };
|
|
72
|
+
const appPath = cursorAppPath(ctx.platform, ctx.env), inspection = await inspectCursorBundles(appPath), manifest = await readJson(manifestPath(ctx), undefined);
|
|
73
|
+
if (inspection.status !== 'patched' || !manifest?.state?.modelIds?.length)
|
|
74
|
+
throw new Error('Cursor local BYOK patch or protected model state is incomplete.');
|
|
75
|
+
const live = new Set((ctx.models || []).map(m => m.id)), configured = new Set(manifest.state.modelIds), missing = [...live].filter(x => !configured.has(x));
|
|
76
|
+
if (missing.length)
|
|
77
|
+
throw new Error(`Cursor is missing ${missing.length} live ScaleMax model(s). Rerun update.`);
|
|
78
|
+
return { id, name, changed: false, path: manifestPath(ctx), message: `verified local BYOK patch and ${configured.size} configured model IDs` };
|
|
9
79
|
}
|
|
10
80
|
export async function remove(ctx) {
|
|
11
|
-
|
|
12
|
-
|
|
81
|
+
if (ctx.platform !== 'darwin')
|
|
82
|
+
return { id, name, changed: false, message: `no managed macOS Cursor configuration on ${ctx.platform}` };
|
|
83
|
+
const file = manifestPath(ctx), manifest = await readJson(file, undefined);
|
|
84
|
+
if (!manifest)
|
|
85
|
+
return { id, name, changed: false, path: file, message: 'no ScaleMax-managed Cursor manifest found' };
|
|
86
|
+
if (await isCursorRunning(manifest.appPath))
|
|
87
|
+
throw new Error('Close Cursor completely before removing its ScaleMax configuration.');
|
|
88
|
+
const stateRestored = await restoreCursorState(manifest.state), bundlesRestored = manifest.bundleManaged ? await restoreCursorBundles(manifest.bundles) : false;
|
|
89
|
+
await fs.remove(file);
|
|
90
|
+
return { id, name, changed: stateRestored || bundlesRestored, path: file, message: `removed protected BYOK state${bundlesRestored ? ' and restored original Cursor bundles' : ' (pre-existing external Cursor patch left unchanged)'}` };
|
|
13
91
|
}
|
package/dist/detect/cursor.js
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { scaleMaxDir } from '../utils/paths.js';
|
|
4
|
+
import { cursorAppPath, inspectCursorBundles } from '../configure/cursor-bundle.js';
|
|
5
|
+
export async function detect(ctx) {
|
|
6
|
+
if (ctx.platform !== 'darwin')
|
|
7
|
+
return { id: 'cursor', name: 'Cursor', installed: false, configured: false, paths: [], reason: `automatic BYOK setup currently supports macOS only (${ctx.platform})` };
|
|
8
|
+
const appPath = cursorAppPath(ctx.platform, ctx.env), manifest = path.join(scaleMaxDir(ctx.home, ctx.platform, ctx.env), 'cursor', 'manifest.json');
|
|
9
|
+
if (!(await fs.pathExists(appPath)))
|
|
10
|
+
return { id: 'cursor', name: 'Cursor', installed: false, configured: false, paths: [appPath], reason: 'Cursor.app not found' };
|
|
11
|
+
try {
|
|
12
|
+
const inspection = await inspectCursorBundles(appPath), configured = inspection.status === 'patched' && await fs.pathExists(manifest);
|
|
13
|
+
return { id: 'cursor', name: 'Cursor', installed: true, configured, paths: [appPath, manifest], reason: `bundle status: ${inspection.status}${configured ? '; ScaleMax protected state configured' : ''}` };
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return { id: 'cursor', name: 'Cursor', installed: true, configured: false, paths: [appPath], reason: error instanceof Error ? error.message : String(error) };
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -32,6 +32,14 @@ export interface ConfigureContext extends DetectContext {
|
|
|
32
32
|
apiKey: string;
|
|
33
33
|
baseUrl: string;
|
|
34
34
|
dryRun?: boolean;
|
|
35
|
+
interactive?: boolean;
|
|
36
|
+
allowAppModification?: boolean;
|
|
37
|
+
models?: Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
object?: string;
|
|
40
|
+
owned_by?: string;
|
|
41
|
+
display_name?: string;
|
|
42
|
+
}>;
|
|
35
43
|
}
|
|
36
44
|
export interface ConfigureResult {
|
|
37
45
|
id: ClientId;
|
package/dist/utils/api.js
CHANGED
|
@@ -36,10 +36,10 @@ export async function validateApiKey(apiKey) {
|
|
|
36
36
|
return { ok: false, models: [], error: await friendlyError(authCheck) };
|
|
37
37
|
const usage = await authCheck.json().catch(() => ({}));
|
|
38
38
|
const keyMeta = { provider_scope: usage?.key?.provider_scope || 'both', mode: usage?.key?.mode, prefix: usage?.key?.prefix, label: usage?.key?.label };
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
return { ok: true, models: [], key: keyMeta, error: await friendlyError(
|
|
42
|
-
const data = await
|
|
39
|
+
const modelsRes = await request('/models', apiKey);
|
|
40
|
+
if (!modelsRes.ok)
|
|
41
|
+
return { ok: true, models: [], key: keyMeta, error: await friendlyError(modelsRes) };
|
|
42
|
+
const data = await modelsRes.json();
|
|
43
43
|
return { ok: true, models: Array.isArray(data.data) ? data.data : [], key: keyMeta };
|
|
44
44
|
}
|
|
45
45
|
catch (err) {
|
package/dist/utils/exec.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function commandExists(command: string): Promise<boolean>;
|
|
1
|
+
export declare function commandExists(command: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
package/dist/utils/exec.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
|
-
export async function commandExists(command) {
|
|
2
|
+
export async function commandExists(command, env = process.env) {
|
|
3
3
|
const checker = process.platform === 'win32' ? 'where' : 'command';
|
|
4
4
|
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
5
5
|
try {
|
|
6
|
-
await execa(checker, args, { shell: process.platform !== 'win32' });
|
|
6
|
+
await execa(checker, args, { shell: process.platform !== 'win32', env });
|
|
7
7
|
return true;
|
|
8
8
|
}
|
|
9
9
|
catch {
|