llm-tuner 0.3.0 → 0.4.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.
- package/README.es.md +19 -3
- package/README.md +19 -3
- package/package.json +1 -1
- package/src/cli/animation.js +85 -0
- package/src/cli/colors.js +12 -0
- package/src/cli/commands/help.js +7 -0
- package/src/cli/commands/load.js +338 -0
- package/src/cli/commands/mcp.js +118 -0
- package/src/cli/commands/presets.js +21 -0
- package/src/cli/commands/settings.js +65 -0
- package/src/cli/index.js +10 -367
- package/src/cli/nav.js +82 -0
- package/src/cli/router.js +243 -0
- package/src/core/index.js +1 -0
- package/src/core/mcp/adapters/json-adapter.js +37 -0
- package/src/core/mcp/adapters/toml-adapter.js +82 -0
- package/src/core/mcp/index.js +82 -0
- package/src/core/mcp/registry.js +121 -0
- package/src/i18n/en.js +46 -4
- package/src/i18n/es.js +46 -4
- package/src/web/public/app.js +172 -1
- package/src/web/public/index.html +33 -0
- package/src/web/public/style.css +29 -0
- package/src/web/server.js +20 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
import { readSettings, writeSettings, THEMES, PROFILES, CURRENT_VERSION } from '../core/index.js';
|
|
3
|
+
import { t as translate, LOCALES, errorText } from '../i18n/index.js';
|
|
4
|
+
import { loadCommand } from './commands/load.js';
|
|
5
|
+
import { mcpCommand } from './commands/mcp.js';
|
|
6
|
+
import { presetsCommand } from './commands/presets.js';
|
|
7
|
+
import { settingsCommand } from './commands/settings.js';
|
|
8
|
+
import { helpCommand } from './commands/help.js';
|
|
9
|
+
import { withNav } from './nav.js';
|
|
10
|
+
|
|
11
|
+
export function parseCliArgs(argv) {
|
|
12
|
+
const flags = new Set(['help', 'version', 'yes', 'dry-run', 'json', 'force', 'presets', 'settings']);
|
|
13
|
+
const tuningKeys = new Set(['engine', 'model', 'ctx', 'profile', 'candidates', 'force', 'dry-run', 'yes', 'json']);
|
|
14
|
+
const args = {};
|
|
15
|
+
let command = null;
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
|
|
20
|
+
// Slash commands or positional aliases
|
|
21
|
+
if (a === '/load' || a === 'load') {
|
|
22
|
+
command = 'load';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (a === '/mcp' || a === 'mcp') {
|
|
26
|
+
command = 'mcp';
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (a === '/presets' || a === 'presets') {
|
|
30
|
+
command = 'presets';
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (a === '/settings' || a === 'settings' || a === '/setting' || a === 'setting') {
|
|
34
|
+
command = 'settings';
|
|
35
|
+
args.settings = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (a === '/help' || a === 'help') {
|
|
39
|
+
command = 'help';
|
|
40
|
+
args.help = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (a === '/exit' || a === 'exit') {
|
|
44
|
+
command = 'exit';
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Short flags
|
|
49
|
+
if (a === '-h') { args.help = true; command = 'help'; continue; }
|
|
50
|
+
if (a === '-v') { args.version = true; command = 'version'; continue; }
|
|
51
|
+
|
|
52
|
+
// Double-dash flags & options
|
|
53
|
+
if (a.startsWith('--')) {
|
|
54
|
+
const k = a.slice(2);
|
|
55
|
+
if (k === 'setting' || k === 'settings') {
|
|
56
|
+
args.settings = true;
|
|
57
|
+
command = 'settings';
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (k === 'presets') {
|
|
61
|
+
args.presets = true;
|
|
62
|
+
command = 'presets';
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (k === 'help') {
|
|
66
|
+
args.help = true;
|
|
67
|
+
command = 'help';
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (k === 'version') {
|
|
71
|
+
args.version = true;
|
|
72
|
+
command = 'version';
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (flags.has(k)) {
|
|
77
|
+
args[k] = true;
|
|
78
|
+
} else {
|
|
79
|
+
args[k] = argv[++i];
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Normalize dry-run / dryRun
|
|
86
|
+
if (args['dry-run']) args.dryRun = true;
|
|
87
|
+
else if (args.dryRun) args['dry-run'] = true;
|
|
88
|
+
|
|
89
|
+
// Route fallback: if command not explicitly set
|
|
90
|
+
if (!command) {
|
|
91
|
+
const hasTuning = Object.keys(args).some((k) => tuningKeys.has(k));
|
|
92
|
+
if (hasTuning) {
|
|
93
|
+
command = 'load';
|
|
94
|
+
} else {
|
|
95
|
+
command = 'menu';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { command, ...args };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function routeCli(argv, env = {}) {
|
|
103
|
+
const parsed = parseCliArgs(argv);
|
|
104
|
+
let { command, ...args } = parsed;
|
|
105
|
+
|
|
106
|
+
let { lang, theme } = await readSettings();
|
|
107
|
+
const t = (key, params) => translate(lang, key, params);
|
|
108
|
+
|
|
109
|
+
if ('lang' in args) {
|
|
110
|
+
if (!LOCALES.includes(args.lang)) {
|
|
111
|
+
console.error(translate('en', 'errors.unknownLocale', { lang: args.lang ?? '', list: LOCALES.join(', ') }));
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
lang = args.lang;
|
|
115
|
+
try { await writeSettings({ lang }); } catch (e) { console.error(`Warning: ${e.message}`); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if ('theme' in args) {
|
|
119
|
+
if (!THEMES.includes(args.theme)) {
|
|
120
|
+
console.error(translate(lang, 'errors.unknownTheme', { theme: args.theme ?? '', list: THEMES.join(', ') }));
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
theme = args.theme;
|
|
124
|
+
try { await writeSettings({ theme }); } catch (e) { console.error(`Warning: ${e.message}`); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if ('profile' in args) {
|
|
128
|
+
if (!PROFILES.includes(args.profile)) {
|
|
129
|
+
console.error(translate(lang, 'errors.unknownProfile', { profile: args.profile ?? '', list: PROFILES.join(', ') }));
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (command === 'version' || args.version) {
|
|
135
|
+
console.log(CURRENT_VERSION);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (command === 'help' || args.help) {
|
|
140
|
+
await helpCommand(args, { lang, theme });
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Interactive menu mode when run bare (persistent loop until /exit)
|
|
145
|
+
if (command === 'menu') {
|
|
146
|
+
const isInteractive = process.stdin.isTTY || env.forceInteractive;
|
|
147
|
+
if (!isInteractive) {
|
|
148
|
+
await loadCommand(args, { lang, theme });
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let currentLang = lang;
|
|
153
|
+
let currentTheme = theme;
|
|
154
|
+
|
|
155
|
+
while (true) {
|
|
156
|
+
const activeT = (k, p) => translate(currentLang, k, p);
|
|
157
|
+
let selectedCommand;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
selectedCommand = await withNav(() => select({
|
|
161
|
+
message: activeT('cli.menuTitle'),
|
|
162
|
+
choices: [
|
|
163
|
+
{ name: `/load ${activeT('cli.cmdLoadDesc')}`, value: 'load' },
|
|
164
|
+
{ name: `/mcp ${activeT('cli.cmdMcpDesc')}`, value: 'mcp' },
|
|
165
|
+
{ name: `/presets ${activeT('cli.cmdPresetsDesc')}`, value: 'presets' },
|
|
166
|
+
{ name: `/settings ${activeT('cli.cmdSettingsDesc')}`, value: 'settings' },
|
|
167
|
+
{ name: `/help ${activeT('cli.cmdHelpDesc')}`, value: 'help' },
|
|
168
|
+
{ name: `/exit ${activeT('cli.cmdExitDesc')}`, value: 'exit' },
|
|
169
|
+
],
|
|
170
|
+
}), { allowBack: false });
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (e?.name === 'ExitPromptError') {
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (selectedCommand === 'exit') {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
switch (selectedCommand) {
|
|
184
|
+
case 'load':
|
|
185
|
+
await loadCommand(args, { lang: currentLang, theme: currentTheme });
|
|
186
|
+
break;
|
|
187
|
+
case 'mcp':
|
|
188
|
+
await mcpCommand(args, { lang: currentLang, theme: currentTheme });
|
|
189
|
+
break;
|
|
190
|
+
case 'presets':
|
|
191
|
+
await presetsCommand(args, { lang: currentLang, theme: currentTheme });
|
|
192
|
+
break;
|
|
193
|
+
case 'settings':
|
|
194
|
+
await settingsCommand(args, { lang: currentLang, theme: currentTheme });
|
|
195
|
+
{
|
|
196
|
+
const s = await readSettings();
|
|
197
|
+
currentLang = s.lang;
|
|
198
|
+
currentTheme = s.theme;
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case 'help':
|
|
202
|
+
await helpCommand(args, { lang: currentLang, theme: currentTheme });
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
if (e?.name === 'ExitPromptError') {
|
|
207
|
+
// Sub-command was cancelled, return to main menu
|
|
208
|
+
} else {
|
|
209
|
+
console.error(`\n${activeT('common.error', { message: errorText(currentLang, e) })}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log('');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
switch (command) {
|
|
219
|
+
case 'load':
|
|
220
|
+
await loadCommand(args, { lang, theme });
|
|
221
|
+
return 0;
|
|
222
|
+
case 'mcp':
|
|
223
|
+
await mcpCommand(args, { lang, theme });
|
|
224
|
+
return 0;
|
|
225
|
+
case 'presets':
|
|
226
|
+
await presetsCommand(args, { lang, theme });
|
|
227
|
+
return 0;
|
|
228
|
+
case 'settings':
|
|
229
|
+
await settingsCommand(args, { lang, theme });
|
|
230
|
+
return 0;
|
|
231
|
+
case 'help':
|
|
232
|
+
await helpCommand(args, { lang, theme });
|
|
233
|
+
return 0;
|
|
234
|
+
default:
|
|
235
|
+
console.error(`Unknown command: ${command}`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
if (e?.name === 'ExitPromptError') return 130;
|
|
240
|
+
console.error(`\n${t('common.error', { message: errorText(lang, e) })}`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
package/src/core/index.js
CHANGED
|
@@ -10,3 +10,4 @@ export { readSettings, writeSettings, settingsPath, THEMES, DEFAULT_THEME, norma
|
|
|
10
10
|
export { TunerError, benchError } from './errors.js';
|
|
11
11
|
export { sampleRam, sampleGpus, startTelemetrySampler } from './telemetry.js';
|
|
12
12
|
export { checkUpdate, isNewerVersion, CURRENT_VERSION } from './updater.js';
|
|
13
|
+
export { detectAgents, installMcpServer, SUPPORTED_AGENTS } from './mcp/index.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readJson, writeJsonWithBackup } from '../../util.js';
|
|
2
|
+
|
|
3
|
+
export async function readJsonMcpServers(filePath, rootKey) {
|
|
4
|
+
const data = (await readJson(filePath, {})) || {};
|
|
5
|
+
return Object.keys(data[rootKey] || {});
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function upsertJsonMcpServer(
|
|
9
|
+
filePath,
|
|
10
|
+
rootKey,
|
|
11
|
+
serverName,
|
|
12
|
+
serverConfig,
|
|
13
|
+
{ isOpenCode = false } = {}
|
|
14
|
+
) {
|
|
15
|
+
const data = (await readJson(filePath, {})) || {};
|
|
16
|
+
|
|
17
|
+
if (!data[rootKey] || typeof data[rootKey] !== 'object') {
|
|
18
|
+
data[rootKey] = {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (isOpenCode) {
|
|
22
|
+
data[rootKey][serverName] = {
|
|
23
|
+
type: 'local',
|
|
24
|
+
command: [serverConfig.command, ...(serverConfig.args || [])],
|
|
25
|
+
environment: serverConfig.env || {}
|
|
26
|
+
};
|
|
27
|
+
} else {
|
|
28
|
+
data[rootKey][serverName] = {
|
|
29
|
+
command: serverConfig.command,
|
|
30
|
+
args: serverConfig.args || [],
|
|
31
|
+
...(serverConfig.env ? { env: serverConfig.env } : {})
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const backup = await writeJsonWithBackup(filePath, data, { indent: 2 });
|
|
36
|
+
return { backup };
|
|
37
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { exists, timestamp } from '../../util.js';
|
|
4
|
+
|
|
5
|
+
export async function readTomlMcpServers(filePath) {
|
|
6
|
+
if (!(await exists(filePath))) return [];
|
|
7
|
+
|
|
8
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
9
|
+
const lines = content.split(/\r?\n/);
|
|
10
|
+
const serverNames = new Set();
|
|
11
|
+
|
|
12
|
+
for (const raw of lines) {
|
|
13
|
+
const line = raw.trim();
|
|
14
|
+
const match = line.match(/^\[mcp_servers\.([A-Za-z0-9_-]+)\]\s*$/);
|
|
15
|
+
if (match) {
|
|
16
|
+
serverNames.add(match[1]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [...serverNames];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function upsertTomlMcpServer(filePath, serverName, serverConfig) {
|
|
24
|
+
let content = '';
|
|
25
|
+
let backup = null;
|
|
26
|
+
|
|
27
|
+
if (await exists(filePath)) {
|
|
28
|
+
const backupPath = `${filePath}.bak-llm-tuner-${timestamp()}`;
|
|
29
|
+
await fs.copyFile(filePath, backupPath);
|
|
30
|
+
backup = backupPath;
|
|
31
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
32
|
+
} else {
|
|
33
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
34
|
+
content = '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lines = content.split(/\r?\n/);
|
|
38
|
+
const newLines = [];
|
|
39
|
+
let skipping = false;
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
|
|
44
|
+
if (trimmed.startsWith('[')) {
|
|
45
|
+
if (
|
|
46
|
+
trimmed === `[mcp_servers.${serverName}]` ||
|
|
47
|
+
trimmed.startsWith(`[mcp_servers.${serverName}.`)
|
|
48
|
+
) {
|
|
49
|
+
skipping = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
skipping = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!skipping) {
|
|
56
|
+
newLines.push(line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let baseContent = newLines.join('\n').trim();
|
|
61
|
+
if (baseContent.length > 0) {
|
|
62
|
+
baseContent += '\n\n';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let tomlBlock = `[mcp_servers.${serverName}]\n`;
|
|
66
|
+
tomlBlock += `command = ${JSON.stringify(serverConfig.command)}\n`;
|
|
67
|
+
tomlBlock += `args = ${JSON.stringify(serverConfig.args || [])}\n`;
|
|
68
|
+
|
|
69
|
+
if (serverConfig.env && typeof serverConfig.env === 'object' && Object.keys(serverConfig.env).length > 0) {
|
|
70
|
+
tomlBlock += `\n[mcp_servers.${serverName}.env]\n`;
|
|
71
|
+
for (const [key, value] of Object.entries(serverConfig.env)) {
|
|
72
|
+
tomlBlock += `${key} = ${JSON.stringify(String(value))}\n`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const finalContent = baseContent + tomlBlock;
|
|
77
|
+
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp-${process.pid}`);
|
|
78
|
+
await fs.writeFile(tempPath, finalContent, 'utf8');
|
|
79
|
+
await fs.rename(tempPath, filePath);
|
|
80
|
+
|
|
81
|
+
return { backup };
|
|
82
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { exists } from '../util.js';
|
|
2
|
+
import { SUPPORTED_AGENTS, resolveConfigPath, buildMcpPayload } from './registry.js';
|
|
3
|
+
import { readJsonMcpServers, upsertJsonMcpServer } from './adapters/json-adapter.js';
|
|
4
|
+
import { readTomlMcpServers, upsertTomlMcpServer } from './adapters/toml-adapter.js';
|
|
5
|
+
|
|
6
|
+
export { SUPPORTED_AGENTS, resolveConfigPath, buildMcpPayload };
|
|
7
|
+
|
|
8
|
+
export async function detectAgents({ platform, env, homedir } = {}) {
|
|
9
|
+
const results = [];
|
|
10
|
+
|
|
11
|
+
for (const agent of SUPPORTED_AGENTS) {
|
|
12
|
+
const configPath = resolveConfigPath(agent.id, { platform, env, homedir });
|
|
13
|
+
const fileExists = await exists(configPath);
|
|
14
|
+
|
|
15
|
+
let configuredServers = [];
|
|
16
|
+
if (fileExists) {
|
|
17
|
+
try {
|
|
18
|
+
if (agent.format === 'toml') {
|
|
19
|
+
configuredServers = await readTomlMcpServers(configPath);
|
|
20
|
+
} else {
|
|
21
|
+
configuredServers = await readJsonMcpServers(configPath, agent.rootKey);
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
configuredServers = [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
results.push({
|
|
29
|
+
id: agent.id,
|
|
30
|
+
name: agent.name,
|
|
31
|
+
format: agent.format,
|
|
32
|
+
configPath,
|
|
33
|
+
detected: fileExists,
|
|
34
|
+
configuredServers
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function installMcpServer(
|
|
42
|
+
{ engineId, customName, agentIds = [], host, apiKey },
|
|
43
|
+
{ platform, env, homedir } = {}
|
|
44
|
+
) {
|
|
45
|
+
if (!engineId || !customName) {
|
|
46
|
+
throw new Error('engineId and customName are required');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const payload = buildMcpPayload(engineId, { customName, host, apiKey });
|
|
50
|
+
const updated = [];
|
|
51
|
+
const errors = [];
|
|
52
|
+
|
|
53
|
+
for (const agentId of agentIds) {
|
|
54
|
+
const agent = SUPPORTED_AGENTS.find((a) => a.id === agentId);
|
|
55
|
+
if (!agent) {
|
|
56
|
+
errors.push({ agentId, error: `Unsupported agent ID: ${agentId}` });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const configPath = resolveConfigPath(agentId, { platform, env, homedir });
|
|
62
|
+
|
|
63
|
+
if (agent.format === 'toml') {
|
|
64
|
+
await upsertTomlMcpServer(configPath, customName, payload);
|
|
65
|
+
} else {
|
|
66
|
+
await upsertJsonMcpServer(
|
|
67
|
+
configPath,
|
|
68
|
+
agent.rootKey,
|
|
69
|
+
customName,
|
|
70
|
+
payload,
|
|
71
|
+
{ isOpenCode: agent.isOpenCode }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
updated.push(agentId);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
errors.push({ agentId, error: err.message });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { success: errors.length === 0, updated, errors };
|
|
82
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const SUPPORTED_AGENTS = [
|
|
5
|
+
{
|
|
6
|
+
id: 'claude-desktop',
|
|
7
|
+
name: 'Claude Desktop',
|
|
8
|
+
format: 'json',
|
|
9
|
+
rootKey: 'mcpServers'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'claude-code',
|
|
13
|
+
name: 'Claude Code',
|
|
14
|
+
format: 'json',
|
|
15
|
+
rootKey: 'mcpServers'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'opencode',
|
|
19
|
+
name: 'OpenCode',
|
|
20
|
+
format: 'json',
|
|
21
|
+
rootKey: 'mcp',
|
|
22
|
+
isOpenCode: true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'antigravity',
|
|
26
|
+
name: 'Antigravity',
|
|
27
|
+
format: 'json',
|
|
28
|
+
rootKey: 'mcpServers'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'vscode',
|
|
32
|
+
name: 'VS Code',
|
|
33
|
+
format: 'json',
|
|
34
|
+
rootKey: 'servers'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'chatgpt',
|
|
38
|
+
name: 'ChatGPT / Codex',
|
|
39
|
+
format: 'toml'
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export function resolveConfigPath(agentId, { platform = process.platform, env = process.env, homedir = os.homedir() } = {}) {
|
|
44
|
+
const p = platform === 'win32' ? path.win32 : path.posix;
|
|
45
|
+
|
|
46
|
+
switch (agentId) {
|
|
47
|
+
case 'claude-desktop': {
|
|
48
|
+
if (platform === 'darwin') {
|
|
49
|
+
return p.join(homedir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
50
|
+
}
|
|
51
|
+
if (platform === 'win32') {
|
|
52
|
+
const appData = env.APPDATA || p.join(homedir, 'AppData', 'Roaming');
|
|
53
|
+
return p.join(appData, 'Claude', 'claude_desktop_config.json');
|
|
54
|
+
}
|
|
55
|
+
const configDir = env.XDG_CONFIG_HOME || p.join(homedir, '.config');
|
|
56
|
+
return p.join(configDir, 'Claude', 'claude_desktop_config.json');
|
|
57
|
+
}
|
|
58
|
+
case 'claude-code': {
|
|
59
|
+
const userProfile = (platform === 'win32' && env.USERPROFILE) ? env.USERPROFILE : homedir;
|
|
60
|
+
return p.join(userProfile, '.claude.json');
|
|
61
|
+
}
|
|
62
|
+
case 'opencode': {
|
|
63
|
+
if (platform === 'darwin') {
|
|
64
|
+
return p.join(homedir, 'Library', 'Application Support', 'opencode', 'opencode.json');
|
|
65
|
+
}
|
|
66
|
+
if (platform === 'win32') {
|
|
67
|
+
const appData = env.APPDATA || p.join(homedir, 'AppData', 'Roaming');
|
|
68
|
+
return p.join(appData, 'opencode', 'opencode.json');
|
|
69
|
+
}
|
|
70
|
+
const configDir = env.XDG_CONFIG_HOME || p.join(homedir, '.config');
|
|
71
|
+
return p.join(configDir, 'opencode', 'opencode.json');
|
|
72
|
+
}
|
|
73
|
+
case 'antigravity': {
|
|
74
|
+
const userProfile = (platform === 'win32' && env.USERPROFILE) ? env.USERPROFILE : homedir;
|
|
75
|
+
return p.join(userProfile, '.gemini', 'config', 'mcp_config.json');
|
|
76
|
+
}
|
|
77
|
+
case 'vscode': {
|
|
78
|
+
if (platform === 'darwin') {
|
|
79
|
+
return p.join(homedir, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
|
|
80
|
+
}
|
|
81
|
+
if (platform === 'win32') {
|
|
82
|
+
const appData = env.APPDATA || p.join(homedir, 'AppData', 'Roaming');
|
|
83
|
+
return p.join(appData, 'Code', 'User', 'mcp.json');
|
|
84
|
+
}
|
|
85
|
+
const configDir = env.XDG_CONFIG_HOME || p.join(homedir, '.config');
|
|
86
|
+
return p.join(configDir, 'Code', 'User', 'mcp.json');
|
|
87
|
+
}
|
|
88
|
+
case 'chatgpt': {
|
|
89
|
+
const userProfile = (platform === 'win32' && env.USERPROFILE) ? env.USERPROFILE : homedir;
|
|
90
|
+
return p.join(userProfile, '.codex', 'config.toml');
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
throw new Error(`Unsupported agent ID: ${agentId}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildMcpPayload(engineId, { customName, host, apiKey } = {}) {
|
|
98
|
+
switch (engineId) {
|
|
99
|
+
case 'lmstudio': {
|
|
100
|
+
return {
|
|
101
|
+
command: 'npx',
|
|
102
|
+
args: ['-y', '@mzxrai/mcp-openai@latest'],
|
|
103
|
+
env: {
|
|
104
|
+
OPENAI_BASE_URL: host || 'http://127.0.0.1:1234/v1',
|
|
105
|
+
OPENAI_API_KEY: apiKey || 'sk-lm-local'
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case 'ollama': {
|
|
110
|
+
return {
|
|
111
|
+
command: 'npx',
|
|
112
|
+
args: ['-y', 'ollama-mcp-server@latest'],
|
|
113
|
+
env: {
|
|
114
|
+
OLLAMA_HOST: host || 'http://127.0.0.1:11434'
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
default:
|
|
119
|
+
throw new Error(`Unsupported engine ID: ${engineId}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/i18n/en.js
CHANGED
|
@@ -95,24 +95,34 @@ export default {
|
|
|
95
95
|
'common.longPrompt': 'long prompt (~{tokens} tokens, 50% context)…',
|
|
96
96
|
'common.presetSaved': 'Preset saved: {file}',
|
|
97
97
|
'common.backups': 'Backups: {list}',
|
|
98
|
+
'common.back': 'Back',
|
|
98
99
|
|
|
99
100
|
// CLI
|
|
100
101
|
'cli.help': `llm-tuner — load your local LLM with the fastest configuration for the context you need
|
|
101
102
|
|
|
102
|
-
Usage: llm-tuner [options]
|
|
103
|
+
Usage: llm-tuner [command] [options]
|
|
104
|
+
|
|
105
|
+
Commands:
|
|
106
|
+
/load Tune and load a model with optimal hardware settings
|
|
107
|
+
/mcp Connect local engine to AI coding agents as MCP server
|
|
108
|
+
/presets List saved presets
|
|
109
|
+
/settings Interactive settings menu (language and theme)
|
|
110
|
+
/help Show this help message
|
|
111
|
+
/exit Exit the interactive menu
|
|
112
|
+
|
|
113
|
+
Options:
|
|
103
114
|
--engine <lmstudio|ollama> Engine
|
|
104
115
|
--model <key> Model (LM Studio key or Ollama name)
|
|
105
116
|
--ctx <tokens> Context (e.g. 16384)
|
|
106
|
-
--profile <speed|balanced|quality> What to optimize for (
|
|
117
|
+
--profile <speed|balanced|quality> What to optimize for (speed, balanced, quality)
|
|
107
118
|
--force Measure again even if a preset exists
|
|
108
119
|
--candidates <n> Configurations to test when measuring (3)
|
|
109
120
|
--yes Do not ask for confirmation
|
|
110
121
|
--dry-run Measure and show the changes without applying anything or saving a preset
|
|
111
|
-
--presets List saved presets
|
|
122
|
+
--presets List saved presets (same as /presets)
|
|
112
123
|
--json Final output as JSON
|
|
113
124
|
--lang <en|es> Interface language (saved for next time)
|
|
114
125
|
--theme <system|light|dark> Interface theme (saved for next time)
|
|
115
|
-
/settings Interactive settings menu
|
|
116
126
|
|
|
117
127
|
Presets are saved in {presetsDir}`,
|
|
118
128
|
'cli.desktopStartFailed': 'Could not start the desktop app: {error}\nThe desktop runtime (Electron) is downloaded the first time you run llm-tuner-desktop, so that first run needs a network connection.\nThe terminal interface works without it: run llm-tuner.',
|
|
@@ -249,5 +259,37 @@ Presets are saved in {presetsDir}`,
|
|
|
249
259
|
'web.updateAvailable': 'Update available: v{latest}',
|
|
250
260
|
'web.updateLink': 'View release notes on GitHub',
|
|
251
261
|
'web.version': 'Version {version}',
|
|
262
|
+
'errors.missingParameter': 'Missing required parameter: {param}',
|
|
263
|
+
'web.addAsMcp': 'Add as MCP',
|
|
264
|
+
'web.addAsMcpTooltip': 'Configure this engine as an MCP server for your AI agents (Claude, OpenCode, VS Code, ChatGPT)',
|
|
265
|
+
'web.mcpModalTitle': 'Add {engine} as MCP Server',
|
|
266
|
+
'web.mcpServerName': 'MCP Server Name',
|
|
267
|
+
'web.mcpAdvancedOptions': 'Advanced connection options',
|
|
268
|
+
'web.mcpBaseUrl': 'Base URL / Host',
|
|
269
|
+
'web.mcpApiKey': 'API Key',
|
|
270
|
+
'web.mcpSelectAgents': 'Select AI agents to configure:',
|
|
271
|
+
'web.mcpDetected': 'Detected',
|
|
272
|
+
'web.mcpNotDetected': 'Available',
|
|
273
|
+
'web.mcpAlreadyConfigured': 'Configured',
|
|
274
|
+
'web.mcpSuccess': 'Successfully configured MCP in: {agents}',
|
|
275
|
+
'web.mcpSaveBtn': 'Save & Configure',
|
|
276
|
+
|
|
277
|
+
// CLI Slash Commands
|
|
278
|
+
'cli.menuTitle': 'Select a command:',
|
|
279
|
+
'cli.cmdLoadDesc': 'Tune and load a model with optimal hardware settings',
|
|
280
|
+
'cli.cmdMcpDesc': 'Connect local engine to AI coding agents as MCP server',
|
|
281
|
+
'cli.cmdPresetsDesc': 'View saved tuning presets',
|
|
282
|
+
'cli.cmdSettingsDesc': 'Configure interface language and visual theme',
|
|
283
|
+
'cli.cmdHelpDesc': 'View usage help and CLI flags',
|
|
284
|
+
'cli.cmdExitDesc': 'Exit',
|
|
285
|
+
'cli.mcpSelectEngine': 'Which engine do you want to configure as an MCP server?',
|
|
286
|
+
'cli.mcpSelectAgents': 'Select AI agents to configure (space to toggle, enter to confirm):',
|
|
287
|
+
'cli.mcpServerNamePrompt': 'MCP server name:',
|
|
288
|
+
'cli.mcpNoAgentsSelected': 'No agents selected. Exiting.',
|
|
289
|
+
'cli.mcpSuccess': "Configured MCP server '{server}' in: {agents}",
|
|
290
|
+
'cli.mcpFailed': 'Failed to configure MCP in: {agents}',
|
|
291
|
+
'cli.mcpStatusInstalled': 'Installed',
|
|
292
|
+
'cli.mcpStatusDetected': 'Detected',
|
|
293
|
+
'cli.mcpStatusAvailable': 'Available',
|
|
252
294
|
};
|
|
253
295
|
|