scalemax 0.1.0

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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -0
  3. package/dist/clients.d.ts +2 -0
  4. package/dist/clients.js +29 -0
  5. package/dist/commands/configure.d.ts +7 -0
  6. package/dist/commands/configure.js +78 -0
  7. package/dist/commands/doctor.d.ts +1 -0
  8. package/dist/commands/doctor.js +54 -0
  9. package/dist/commands/login.d.ts +1 -0
  10. package/dist/commands/login.js +25 -0
  11. package/dist/commands/logout.d.ts +1 -0
  12. package/dist/commands/logout.js +4 -0
  13. package/dist/commands/models.d.ts +1 -0
  14. package/dist/commands/models.js +19 -0
  15. package/dist/commands/remove.d.ts +1 -0
  16. package/dist/commands/remove.js +20 -0
  17. package/dist/commands/update.d.ts +1 -0
  18. package/dist/commands/update.js +2 -0
  19. package/dist/configure/claude.d.ts +3 -0
  20. package/dist/configure/claude.js +14 -0
  21. package/dist/configure/codex.d.ts +16 -0
  22. package/dist/configure/codex.js +210 -0
  23. package/dist/configure/common.d.ts +4 -0
  24. package/dist/configure/common.js +39 -0
  25. package/dist/configure/continue.d.ts +3 -0
  26. package/dist/configure/continue.js +12 -0
  27. package/dist/configure/cursor.d.ts +3 -0
  28. package/dist/configure/cursor.js +13 -0
  29. package/dist/configure/hermes.d.ts +3 -0
  30. package/dist/configure/hermes.js +14 -0
  31. package/dist/configure/openclaw.d.ts +3 -0
  32. package/dist/configure/openclaw.js +14 -0
  33. package/dist/configure/opencode.d.ts +3 -0
  34. package/dist/configure/opencode.js +14 -0
  35. package/dist/configure/paths.d.ts +6 -0
  36. package/dist/configure/paths.js +35 -0
  37. package/dist/configure/templates.d.ts +3 -0
  38. package/dist/configure/templates.js +31 -0
  39. package/dist/configure/vscode.d.ts +3 -0
  40. package/dist/configure/vscode.js +13 -0
  41. package/dist/configure/windsurf.d.ts +3 -0
  42. package/dist/configure/windsurf.js +13 -0
  43. package/dist/constants.d.ts +4 -0
  44. package/dist/constants.js +4 -0
  45. package/dist/detect/claude.d.ts +2 -0
  46. package/dist/detect/claude.js +2 -0
  47. package/dist/detect/codex.d.ts +2 -0
  48. package/dist/detect/codex.js +2 -0
  49. package/dist/detect/common.d.ts +2 -0
  50. package/dist/detect/common.js +19 -0
  51. package/dist/detect/continue.d.ts +2 -0
  52. package/dist/detect/continue.js +2 -0
  53. package/dist/detect/cursor.d.ts +2 -0
  54. package/dist/detect/cursor.js +2 -0
  55. package/dist/detect/hermes.d.ts +2 -0
  56. package/dist/detect/hermes.js +2 -0
  57. package/dist/detect/openclaw.d.ts +2 -0
  58. package/dist/detect/openclaw.js +2 -0
  59. package/dist/detect/opencode.d.ts +2 -0
  60. package/dist/detect/opencode.js +2 -0
  61. package/dist/detect/vscode.d.ts +2 -0
  62. package/dist/detect/vscode.js +2 -0
  63. package/dist/detect/windsurf.d.ts +2 -0
  64. package/dist/detect/windsurf.js +2 -0
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.js +90 -0
  67. package/dist/types/client.d.ts +32 -0
  68. package/dist/types/client.js +1 -0
  69. package/dist/utils/api.d.ts +15 -0
  70. package/dist/utils/api.js +37 -0
  71. package/dist/utils/exec.d.ts +1 -0
  72. package/dist/utils/exec.js +12 -0
  73. package/dist/utils/fs.d.ts +7 -0
  74. package/dist/utils/fs.js +41 -0
  75. package/dist/utils/installerConfig.d.ts +5 -0
  76. package/dist/utils/installerConfig.js +84 -0
  77. package/dist/utils/keyStore.d.ts +3 -0
  78. package/dist/utils/keyStore.js +17 -0
  79. package/dist/utils/paths.d.ts +7 -0
  80. package/dist/utils/paths.js +15 -0
  81. package/package.json +78 -0
@@ -0,0 +1,210 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { execa } from 'execa';
4
+ import { codexAuthPath, codexConfigPath } from './paths.js';
5
+ import { atomicWrite, backupFile, listBackups } from '../utils/fs.js';
6
+ import { GENERATED_HEADER } from '../constants.js';
7
+ import { commandExists } from '../utils/exec.js';
8
+ const id = 'codex';
9
+ const name = 'Codex';
10
+ const provider = 'scalemax';
11
+ const model = 'gpt-5.5';
12
+ function codexConfig(baseUrl) {
13
+ return `# ${GENERATED_HEADER}\nmodel = "${model}"\nmodel_provider = "${provider}"\ndisable_response_storage = true\n\n[model_providers.${provider}]\nname = "ScaleMax"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
14
+ }
15
+ function codexAuth(apiKey) {
16
+ return JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2) + '\n';
17
+ }
18
+ function legacyCodexConfigPath(home) {
19
+ return path.join(home, '.codex', 'config.json');
20
+ }
21
+ function parseSimpleTomlForCodex(text) {
22
+ const values = {};
23
+ let section = '';
24
+ for (const raw of text.split(/\r?\n/)) {
25
+ const line = raw.trim();
26
+ if (!line || line.startsWith('#'))
27
+ continue;
28
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/);
29
+ if (sectionMatch) {
30
+ section = sectionMatch[1];
31
+ continue;
32
+ }
33
+ const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*"([^"]*)"\s*$/);
34
+ if (match) {
35
+ values[section ? `${section}.${match[1]}` : match[1]] = match[2];
36
+ continue;
37
+ }
38
+ const boolMatch = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(true|false)\s*$/);
39
+ if (boolMatch)
40
+ values[section ? `${section}.${boolMatch[1]}` : boolMatch[1]] = boolMatch[2];
41
+ }
42
+ return values;
43
+ }
44
+ async function modeIsPrivate(file, platform) {
45
+ if (platform === 'win32')
46
+ return true;
47
+ const stat = await fs.stat(file);
48
+ return (stat.mode & 0o077) === 0;
49
+ }
50
+ async function latestRestorableBackup(file, home) {
51
+ for (const candidate of await listBackups(file, home)) {
52
+ const text = await fs.readFile(candidate, 'utf8').catch(() => '');
53
+ if (!text.includes(GENERATED_HEADER) && !text.includes('"scalemax.managedBy"'))
54
+ return candidate;
55
+ }
56
+ return undefined;
57
+ }
58
+ async function restoreOrRemove(file, home) {
59
+ const restore = await latestRestorableBackup(file, home);
60
+ if (restore) {
61
+ await fs.ensureDir(path.dirname(file));
62
+ await fs.copy(restore, file, { overwrite: true });
63
+ await fs.chmod(file, 0o600).catch(() => undefined);
64
+ return `restored backup ${restore}`;
65
+ }
66
+ if (await fs.pathExists(file)) {
67
+ await fs.remove(file);
68
+ return 'removed ScaleMax configuration';
69
+ }
70
+ return 'no ScaleMax configuration found';
71
+ }
72
+ export async function verifyCodexConfiguration(ctx) {
73
+ const configPath = codexConfigPath(ctx.home);
74
+ const authPath = codexAuthPath(ctx.home);
75
+ const errors = [];
76
+ const warnings = [];
77
+ if (!(await fs.pathExists(configPath)))
78
+ errors.push(`${configPath} does not exist`);
79
+ if (!(await fs.pathExists(authPath)))
80
+ errors.push(`${authPath} does not exist`);
81
+ if (errors.length)
82
+ return { ok: false, warnings, errors, configPath, authPath };
83
+ let configText = '';
84
+ let auth = {};
85
+ try {
86
+ configText = await fs.readFile(configPath, 'utf8');
87
+ }
88
+ catch (err) {
89
+ errors.push(`Cannot read ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
90
+ }
91
+ try {
92
+ auth = await fs.readJson(authPath);
93
+ }
94
+ catch (err) {
95
+ errors.push(`${authPath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
96
+ }
97
+ const toml = parseSimpleTomlForCodex(configText);
98
+ if (toml.model !== model)
99
+ errors.push(`Codex model mismatch: expected ${model}`);
100
+ if (toml.model_provider !== provider)
101
+ errors.push(`Codex model_provider mismatch: expected ${provider}`);
102
+ if (toml[`model_providers.${provider}.base_url`] !== ctx.baseUrl)
103
+ errors.push(`Codex base_url mismatch: expected ${ctx.baseUrl}`);
104
+ if (toml[`model_providers.${provider}.wire_api`] !== 'responses')
105
+ errors.push('Codex wire_api mismatch: expected responses');
106
+ if (toml[`model_providers.${provider}.requires_openai_auth`] !== 'true')
107
+ errors.push('Codex requires_openai_auth mismatch: expected true');
108
+ if (!configText.includes(`[model_providers.${provider}]`))
109
+ errors.push('Codex provider section is missing');
110
+ if (auth.auth_mode !== 'apikey')
111
+ errors.push('Codex auth.json missing auth_mode="apikey"');
112
+ if (auth.OPENAI_API_KEY !== ctx.apiKey)
113
+ errors.push('Codex auth.json missing the selected ScaleMax API key');
114
+ for (const file of [configPath, authPath]) {
115
+ try {
116
+ if (!(await modeIsPrivate(file, ctx.platform)))
117
+ errors.push(`${file} permissions are too broad; expected 0600-style private file`);
118
+ }
119
+ catch (err) {
120
+ errors.push(`Cannot stat ${file}: ${err instanceof Error ? err.message : String(err)}`);
121
+ }
122
+ }
123
+ if (await commandExists('codex')) {
124
+ try {
125
+ const result = await execa('codex', ['doctor', '--json'], {
126
+ env: { ...process.env, CODEX_HOME: path.join(ctx.home, '.codex') },
127
+ reject: false,
128
+ timeout: 20000
129
+ });
130
+ const output = result.stdout || result.stderr;
131
+ const report = JSON.parse(output);
132
+ const config = report.checks?.['config.load'];
133
+ const authCheck = report.checks?.['auth.credentials'];
134
+ const reachability = report.checks?.['network.provider_reachability'];
135
+ if (config?.status === 'fail')
136
+ errors.push(`Codex rejected config.toml: ${config.summary || 'config.load failed'}`);
137
+ if (authCheck?.status === 'fail')
138
+ errors.push(`Codex rejected auth.json: ${authCheck.summary || 'auth.credentials failed'}`);
139
+ if (reachability?.status === 'fail')
140
+ warnings.push(`Codex provider reachability check failed: ${reachability.summary || 'network.provider_reachability failed'}`);
141
+ }
142
+ catch (err) {
143
+ warnings.push(`Could not run codex doctor for deep verification: ${err instanceof Error ? err.message : String(err)}`);
144
+ }
145
+ }
146
+ else {
147
+ warnings.push('Codex executable was not found on PATH, so launch verification was skipped.');
148
+ }
149
+ try {
150
+ const res = await fetch(`${ctx.baseUrl}/responses`, {
151
+ method: 'POST',
152
+ headers: { Authorization: `Bearer ${ctx.apiKey}`, 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({ model, input: 'Say OK', stream: false })
154
+ });
155
+ if (res.status === 404)
156
+ warnings.push('ScaleMax /v1/responses is not available yet. Current Codex requires the Responses API, so it may launch but cannot complete requests until the gateway supports /v1/responses.');
157
+ else if (res.status === 401 || res.status === 403)
158
+ errors.push(`ScaleMax rejected the API key on /v1/responses with HTTP ${res.status}`);
159
+ else if (!res.ok)
160
+ warnings.push(`ScaleMax /v1/responses probe returned HTTP ${res.status}`);
161
+ }
162
+ catch (err) {
163
+ warnings.push(`Could not probe ScaleMax /v1/responses: ${err instanceof Error ? err.message : String(err)}`);
164
+ }
165
+ return { ok: errors.length === 0, warnings, errors, configPath, authPath };
166
+ }
167
+ export async function configure(ctx) {
168
+ const configPath = codexConfigPath(ctx.home);
169
+ const authPath = codexAuthPath(ctx.home);
170
+ const legacyConfig = legacyCodexConfigPath(ctx.home);
171
+ const backups = [];
172
+ await fs.ensureDir(path.dirname(configPath));
173
+ for (const file of [configPath, authPath, legacyConfig]) {
174
+ const backup = await backupFile(file, ctx.home);
175
+ if (backup)
176
+ backups.push(backup);
177
+ }
178
+ await atomicWrite(configPath, codexConfig(ctx.baseUrl), 0o600);
179
+ await atomicWrite(authPath, codexAuth(ctx.apiKey), 0o600);
180
+ if (legacyConfig.endsWith('config.json') && await fs.pathExists(legacyConfig))
181
+ await fs.remove(legacyConfig);
182
+ const verification = await verifyCodexConfiguration(ctx);
183
+ if (!verification.ok)
184
+ throw new Error(`Codex configuration verification failed:\n${verification.errors.map(e => `- ${e}`).join('\n')}`);
185
+ const warningText = verification.warnings.length ? ` Warnings: ${verification.warnings.join(' ')}` : '';
186
+ const backupText = backups.length ? ` (backups: ${backups.join(', ')})` : '';
187
+ return { id, name, changed: true, path: configPath, message: `configured and verified${backupText}.${warningText}` };
188
+ }
189
+ export async function remove(ctx) {
190
+ const messages = [];
191
+ messages.push(`config.toml: ${await restoreOrRemove(codexConfigPath(ctx.home), ctx.home)}`);
192
+ messages.push(`auth.json: ${await restoreOrRemove(codexAuthPath(ctx.home), ctx.home)}`);
193
+ const legacyConfig = legacyCodexConfigPath(ctx.home);
194
+ if (legacyConfig.endsWith('config.json'))
195
+ messages.push(`legacy config.json: ${await restoreOrRemove(legacyConfig, ctx.home)}`);
196
+ return { id, name, changed: true, path: codexConfigPath(ctx.home), message: messages.join('; ') };
197
+ }
198
+ export async function launchCodex() {
199
+ if (!(await commandExists('codex')))
200
+ return { ok: false, message: 'Codex executable was not found on PATH.' };
201
+ try {
202
+ const result = await execa('codex', [], { stdio: 'inherit', reject: false });
203
+ if (result.exitCode === 0)
204
+ return { ok: true, message: 'Codex launched successfully.' };
205
+ return { ok: false, message: `Codex exited with code ${result.exitCode}.`, output: result.stderr || result.stdout };
206
+ }
207
+ catch (err) {
208
+ return { ok: false, message: `Failed to launch Codex: ${err instanceof Error ? err.message : String(err)}` };
209
+ }
210
+ }
@@ -0,0 +1,4 @@
1
+ import { ConfigureContext, ConfigureResult, ClientId } from '../types/client.js';
2
+ export declare function writeManagedConfig(ctx: ConfigureContext, id: ClientId, name: string, file: string, content: string): Promise<ConfigureResult>;
3
+ export declare function writeMergedJsonConfig(ctx: ConfigureContext, id: ClientId, name: string, file: string, build: (existing: Record<string, unknown>) => string): Promise<ConfigureResult>;
4
+ export declare function removeManagedConfig(ctx: ConfigureContext, id: ClientId, name: string, file: string): Promise<ConfigureResult>;
@@ -0,0 +1,39 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { atomicWrite, backupFile, listBackups, readJson } from '../utils/fs.js';
4
+ import { GENERATED_HEADER } from '../constants.js';
5
+ export async function writeManagedConfig(ctx, id, name, file, content) {
6
+ await fs.ensureDir(path.dirname(file));
7
+ const backup = await backupFile(file, ctx.home);
8
+ await atomicWrite(file, content, 0o600);
9
+ return { id, name, changed: true, path: file, message: backup ? `configured (backup: ${backup})` : 'configured' };
10
+ }
11
+ export async function writeMergedJsonConfig(ctx, id, name, file, build) {
12
+ await fs.ensureDir(path.dirname(file));
13
+ const existing = await readJson(file, {});
14
+ const backup = await backupFile(file, ctx.home);
15
+ await atomicWrite(file, build(existing), 0o600);
16
+ return { id, name, changed: true, path: file, message: backup ? `configured (backup: ${backup})` : 'configured' };
17
+ }
18
+ async function latestRestorableBackup(file, home) {
19
+ for (const candidate of await listBackups(file, home)) {
20
+ const text = await fs.readFile(candidate, 'utf8').catch(() => '');
21
+ if (!text.includes(GENERATED_HEADER) && !text.includes('"scalemax.managedBy"'))
22
+ return candidate;
23
+ }
24
+ return undefined;
25
+ }
26
+ export async function removeManagedConfig(ctx, id, name, file) {
27
+ const restore = await latestRestorableBackup(file, ctx.home);
28
+ if (restore) {
29
+ await fs.ensureDir(path.dirname(file));
30
+ await fs.copy(restore, file, { overwrite: true });
31
+ await fs.chmod(file, 0o600).catch(() => undefined);
32
+ return { id, name, changed: true, path: file, message: `restored backup ${restore}` };
33
+ }
34
+ if (await fs.pathExists(file)) {
35
+ await fs.remove(file);
36
+ return { id, name, changed: true, path: file, message: 'removed ScaleMax configuration' };
37
+ }
38
+ return { id, name, changed: false, path: file, message: 'no ScaleMax configuration found' };
39
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigureContext, ConfigureResult } from '../types/client.js';
2
+ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
3
+ export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
@@ -0,0 +1,12 @@
1
+ import { clientConfigPath } from './paths.js';
2
+ import { writeManagedConfig, removeManagedConfig } from './common.js';
3
+ import { continueConfig } from './templates.js';
4
+ const id = 'continue';
5
+ const name = 'Continue';
6
+ export async function configure(ctx) {
7
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
8
+ return writeManagedConfig(ctx, id, name, file, continueConfig(ctx.apiKey, ctx.baseUrl));
9
+ }
10
+ export async function remove(ctx) {
11
+ return removeManagedConfig(ctx, id, name, clientConfigPath(id, ctx.home, ctx.platform));
12
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigureContext, ConfigureResult } from '../types/client.js';
2
+ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
3
+ export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
@@ -0,0 +1,13 @@
1
+ import { clientConfigPath } from './paths.js';
2
+ import { removeManagedConfig, writeMergedJsonConfig } from './common.js';
3
+ import { editorSettings } from './templates.js';
4
+ const id = 'cursor';
5
+ const name = 'Cursor';
6
+ export async function configure(ctx) {
7
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
8
+ return writeMergedJsonConfig(ctx, id, name, file, existing => editorSettings(name, ctx.apiKey, ctx.baseUrl, existing));
9
+ }
10
+ export async function remove(ctx) {
11
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
12
+ return removeManagedConfig(ctx, id, name, file);
13
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigureContext, ConfigureResult } from '../types/client.js';
2
+ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
3
+ export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
@@ -0,0 +1,14 @@
1
+ import { clientConfigPath } from './paths.js';
2
+ import { writeManagedConfig, removeManagedConfig } from './common.js';
3
+ import { jsonConfig } from './templates.js';
4
+ const id = 'hermes';
5
+ const name = 'Hermes';
6
+ export async function configure(ctx) {
7
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
8
+ const content = jsonConfig(name, ctx.apiKey, ctx.baseUrl);
9
+ return writeManagedConfig(ctx, id, name, file, content);
10
+ }
11
+ export async function remove(ctx) {
12
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
13
+ return removeManagedConfig(ctx, id, name, file);
14
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigureContext, ConfigureResult } from '../types/client.js';
2
+ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
3
+ export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
@@ -0,0 +1,14 @@
1
+ import { clientConfigPath } from './paths.js';
2
+ import { writeManagedConfig, removeManagedConfig } from './common.js';
3
+ import { jsonConfig } from './templates.js';
4
+ const id = 'openclaw';
5
+ const name = 'OpenClaw';
6
+ export async function configure(ctx) {
7
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
8
+ const content = jsonConfig(name, ctx.apiKey, ctx.baseUrl);
9
+ return writeManagedConfig(ctx, id, name, file, content);
10
+ }
11
+ export async function remove(ctx) {
12
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
13
+ return removeManagedConfig(ctx, id, name, file);
14
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigureContext, ConfigureResult } from '../types/client.js';
2
+ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
3
+ export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
@@ -0,0 +1,14 @@
1
+ import { clientConfigPath } from './paths.js';
2
+ import { writeManagedConfig, removeManagedConfig } from './common.js';
3
+ import { jsonConfig } from './templates.js';
4
+ const id = 'opencode';
5
+ const name = 'OpenCode';
6
+ export async function configure(ctx) {
7
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
8
+ const content = jsonConfig(name, ctx.apiKey, ctx.baseUrl);
9
+ return writeManagedConfig(ctx, id, name, file, content);
10
+ }
11
+ export async function remove(ctx) {
12
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
13
+ return removeManagedConfig(ctx, id, name, file);
14
+ }
@@ -0,0 +1,6 @@
1
+ import { ClientId } from '../types/client.js';
2
+ export declare function codexDir(home: string): string;
3
+ export declare function codexConfigPath(home: string): string;
4
+ export declare function codexAuthPath(home: string): string;
5
+ export declare function clientConfigPath(id: ClientId, home: string, platform: NodeJS.Platform, env?: NodeJS.ProcessEnv): string;
6
+ export declare function installHints(id: ClientId, home: string, platform: NodeJS.Platform, env?: NodeJS.ProcessEnv): string[];
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import { configRoot } from '../utils/paths.js';
3
+ export function codexDir(home) { return path.join(home, '.codex'); }
4
+ export function codexConfigPath(home) { return path.join(codexDir(home), 'config.toml'); }
5
+ export function codexAuthPath(home) { return path.join(codexDir(home), 'auth.json'); }
6
+ export function clientConfigPath(id, home, platform, env = process.env) {
7
+ const app = configRoot(home, platform, env);
8
+ const linux = env.XDG_CONFIG_HOME || path.join(home, '.config');
9
+ switch (id) {
10
+ case 'codex': return codexConfigPath(home);
11
+ case 'cursor': return platform === 'win32' ? path.join(app, 'Cursor', 'User', 'settings.json') : platform === 'darwin' ? path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json') : path.join(linux, 'Cursor', 'User', 'settings.json');
12
+ case 'claude': return path.join(home, '.claude', 'settings.json');
13
+ case 'opencode': return path.join(linux, 'opencode', 'config.json');
14
+ case 'openclaw': return path.join(home, '.openclaw', 'config.json');
15
+ case 'hermes': return path.join(home, '.hermes', 'config', 'scalemax.json');
16
+ case 'continue': return path.join(home, '.continue', 'config.json');
17
+ case 'vscode': return platform === 'win32' ? path.join(app, 'Code', 'User', 'settings.json') : platform === 'darwin' ? path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json') : path.join(linux, 'Code', 'User', 'settings.json');
18
+ case 'windsurf': return platform === 'win32' ? path.join(app, 'Windsurf', 'User', 'settings.json') : platform === 'darwin' ? path.join(home, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json') : path.join(linux, 'Windsurf', 'User', 'settings.json');
19
+ }
20
+ }
21
+ export function installHints(id, home, platform, env = process.env) {
22
+ const app = configRoot(home, platform, env);
23
+ const linux = env.XDG_CONFIG_HOME || path.join(home, '.config');
24
+ switch (id) {
25
+ case 'codex': return [codexDir(home), path.join(linux, 'codex')];
26
+ case 'cursor': return [platform === 'darwin' ? '/Applications/Cursor.app' : path.join(app, 'Cursor'), path.join(linux, 'Cursor')];
27
+ case 'claude': return [path.join(home, '.claude')];
28
+ case 'opencode': return [path.join(linux, 'opencode')];
29
+ case 'openclaw': return [path.join(home, '.openclaw'), path.join(linux, 'openclaw')];
30
+ case 'hermes': return [path.join(home, '.hermes')];
31
+ case 'continue': return [path.join(home, '.continue')];
32
+ case 'vscode': return [platform === 'darwin' ? '/Applications/Visual Studio Code.app' : path.join(app, 'Code'), path.join(linux, 'Code')];
33
+ case 'windsurf': return [platform === 'darwin' ? '/Applications/Windsurf.app' : path.join(app, 'Windsurf'), path.join(linux, 'Windsurf')];
34
+ }
35
+ }
@@ -0,0 +1,3 @@
1
+ export declare function jsonConfig(client: string, apiKey: string, baseUrl: string, extra?: Record<string, unknown>): string;
2
+ export declare function continueConfig(apiKey: string, baseUrl: string): string;
3
+ export declare function editorSettings(client: string, apiKey: string, baseUrl: string, existing?: Record<string, unknown>): string;
@@ -0,0 +1,31 @@
1
+ import { GENERATED_HEADER } from '../constants.js';
2
+ export function jsonConfig(client, apiKey, baseUrl, extra = {}) {
3
+ return JSON.stringify({
4
+ $schema: 'https://scalemax.pro/schemas/client-config.json',
5
+ managedBy: GENERATED_HEADER,
6
+ client,
7
+ provider: 'openai-compatible',
8
+ baseURL: baseUrl,
9
+ apiKey,
10
+ ...extra
11
+ }, null, 2) + '\n';
12
+ }
13
+ export function continueConfig(apiKey, baseUrl) {
14
+ return JSON.stringify({
15
+ models: [{ title: 'ScaleMax', provider: 'openai', model: 'gpt-5.5', apiBase: baseUrl, apiKey }],
16
+ tabAutocompleteModel: { title: 'ScaleMax', provider: 'openai', model: 'gpt-5.5', apiBase: baseUrl, apiKey },
17
+ contextProviders: [{ name: 'code' }, { name: 'docs' }, { name: 'diff' }, { name: 'terminal' }]
18
+ }, null, 2) + '\n';
19
+ }
20
+ export function editorSettings(client, apiKey, baseUrl, existing = {}) {
21
+ return JSON.stringify({
22
+ ...existing,
23
+ 'scalemax.managedBy': GENERATED_HEADER,
24
+ 'scalemax.baseURL': baseUrl,
25
+ 'scalemax.apiKey': apiKey,
26
+ 'scalemax.provider': 'openai-compatible',
27
+ 'scalemax.client': client,
28
+ 'openai.baseURL': baseUrl,
29
+ 'openai.apiKey': apiKey
30
+ }, null, 2) + '\n';
31
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigureContext, ConfigureResult } from '../types/client.js';
2
+ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
3
+ export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
@@ -0,0 +1,13 @@
1
+ import { clientConfigPath } from './paths.js';
2
+ import { removeManagedConfig, writeMergedJsonConfig } from './common.js';
3
+ import { editorSettings } from './templates.js';
4
+ const id = 'vscode';
5
+ const name = 'VS Code';
6
+ export async function configure(ctx) {
7
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
8
+ return writeMergedJsonConfig(ctx, id, name, file, existing => editorSettings(name, ctx.apiKey, ctx.baseUrl, existing));
9
+ }
10
+ export async function remove(ctx) {
11
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
12
+ return removeManagedConfig(ctx, id, name, file);
13
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigureContext, ConfigureResult } from '../types/client.js';
2
+ export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
3
+ export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
@@ -0,0 +1,13 @@
1
+ import { clientConfigPath } from './paths.js';
2
+ import { removeManagedConfig, writeMergedJsonConfig } from './common.js';
3
+ import { editorSettings } from './templates.js';
4
+ const id = 'windsurf';
5
+ const name = 'Windsurf';
6
+ export async function configure(ctx) {
7
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
8
+ return writeMergedJsonConfig(ctx, id, name, file, existing => editorSettings(name, ctx.apiKey, ctx.baseUrl, existing));
9
+ }
10
+ export async function remove(ctx) {
11
+ const file = clientConfigPath(id, ctx.home, ctx.platform);
12
+ return removeManagedConfig(ctx, id, name, file);
13
+ }
@@ -0,0 +1,4 @@
1
+ export declare const SCALEMAX_BASE_URL = "https://api.scalemax.pro/v1";
2
+ export declare const APP_NAME = "ScaleMax";
3
+ export declare const GENERATED_HEADER = "ScaleMax managed configuration";
4
+ export declare const KEY_ENV = "SCALEMAX_API_KEY";
@@ -0,0 +1,4 @@
1
+ export const SCALEMAX_BASE_URL = 'https://api.scalemax.pro/v1';
2
+ export const APP_NAME = 'ScaleMax';
3
+ export const GENERATED_HEADER = 'ScaleMax managed configuration';
4
+ export const KEY_ENV = 'SCALEMAX_API_KEY';
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'claude', 'Claude Code'); }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'codex', 'Codex'); }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult, ClientId } from '../types/client.js';
2
+ export declare function detectClient(ctx: DetectContext, id: ClientId, name: string): Promise<DetectionResult>;
@@ -0,0 +1,19 @@
1
+ import fs from 'fs-extra';
2
+ import { commandExists } from '../utils/exec.js';
3
+ import { clientConfigPath, installHints } from '../configure/paths.js';
4
+ const commands = {
5
+ codex: ['codex'], cursor: ['cursor'], claude: ['claude'], opencode: ['opencode'], openclaw: ['openclaw'], hermes: ['hermes'], continue: ['continue'], vscode: ['code'], windsurf: ['windsurf']
6
+ };
7
+ export async function detectClient(ctx, id, name) {
8
+ const paths = [clientConfigPath(id, ctx.home, ctx.platform, ctx.env), ...installHints(id, ctx.home, ctx.platform, ctx.env)];
9
+ const pathHits = [];
10
+ for (const p of paths)
11
+ if (await fs.pathExists(p))
12
+ pathHits.push(p);
13
+ const cmdHits = [];
14
+ for (const cmd of commands[id])
15
+ if (await commandExists(cmd))
16
+ cmdHits.push(`command:${cmd}`);
17
+ const hits = [...cmdHits, ...pathHits];
18
+ return { id, name, installed: hits.length > 0, reason: hits.length ? undefined : 'not detected', paths: hits };
19
+ }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'continue', 'Continue'); }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'cursor', 'Cursor'); }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'hermes', 'Hermes'); }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'openclaw', 'OpenClaw'); }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'opencode', 'OpenCode'); }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'vscode', 'VS Code'); }
@@ -0,0 +1,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -0,0 +1,2 @@
1
+ import { detectClient } from './common.js';
2
+ export function detect(ctx) { return detectClient(ctx, 'windsurf', 'Windsurf'); }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};