@kernel.chat/kbot 2.25.0 → 2.27.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/dist/mcp-plugins.js +1 -1
- package/dist/mcp-plugins.js.map +1 -1
- package/dist/tools/containers.js +7 -7
- package/dist/tools/containers.js.map +1 -1
- package/dist/tools/database.js +6 -6
- package/dist/tools/database.js.map +1 -1
- package/dist/tools/deploy.js +3 -3
- package/dist/tools/deploy.js.map +1 -1
- package/dist/tools/e2b-sandbox.js +5 -5
- package/dist/tools/e2b-sandbox.js.map +1 -1
- package/dist/tools/index.test.js +1 -1
- package/dist/tools/index.test.js.map +1 -1
- package/dist/tools/research.js +1 -1
- package/dist/tools/research.js.map +1 -1
- package/dist/tools/training.d.ts.map +1 -1
- package/dist/tools/training.js +142 -79
- package/dist/tools/training.js.map +1 -1
- package/dist/tools/vfx.js +2 -2
- package/dist/tools/vfx.js.map +1 -1
- package/dist/workflows.js +3 -3
- package/dist/workflows.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/training.js
CHANGED
|
@@ -13,11 +13,56 @@
|
|
|
13
13
|
// train_deploy — Deploy to Ollama, HuggingFace, or K:BOT local
|
|
14
14
|
// train_cost — Estimate training cost, time, and VRAM
|
|
15
15
|
import { execSync, spawn } from 'node:child_process';
|
|
16
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
17
17
|
import { resolve, join, basename, extname, dirname } from 'node:path';
|
|
18
18
|
import { homedir, cpus } from 'node:os';
|
|
19
19
|
import { registerTool } from './index.js';
|
|
20
20
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
21
|
+
/** Shell-escape a string for safe interpolation into shell commands */
|
|
22
|
+
function esc(s) {
|
|
23
|
+
// Single-quote wrapping with internal single-quote escaping
|
|
24
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
25
|
+
}
|
|
26
|
+
/** Validate a string is safe for use as a model name/identifier (no shell metacharacters) */
|
|
27
|
+
function validateIdentifier(value, label) {
|
|
28
|
+
if (/[;&|`$(){}[\]!#~<>"\n\r\\]/.test(value)) {
|
|
29
|
+
throw new Error(`${label} contains unsafe characters. Only alphanumeric, hyphens, underscores, dots, colons, and slashes are allowed.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Validate a path doesn't escape the allowed directories */
|
|
33
|
+
function validatePath(p, label) {
|
|
34
|
+
const resolved = resolve(p);
|
|
35
|
+
const home = homedir();
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
// Allow paths under home directory, cwd, or /tmp
|
|
38
|
+
if (!resolved.startsWith(home) && !resolved.startsWith(cwd) && !resolved.startsWith('/tmp')) {
|
|
39
|
+
throw new Error(`${label} path must be under your home directory, current directory, or /tmp. Got: ${resolved}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Execute a command with argument array (no shell interpolation) */
|
|
43
|
+
function execSafe(cmd, args, opts) {
|
|
44
|
+
return execSync([cmd, ...args].map(a => esc(a)).join(' '), {
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
timeout: opts?.timeout ?? 60_000,
|
|
47
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
48
|
+
cwd: opts?.cwd ?? process.cwd(),
|
|
49
|
+
env: opts?.env ?? process.env,
|
|
50
|
+
shell: '/bin/sh',
|
|
51
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
+
}).trim();
|
|
53
|
+
}
|
|
54
|
+
function execSafeResult(cmd, args, opts) {
|
|
55
|
+
try {
|
|
56
|
+
const output = execSafe(cmd, args, opts);
|
|
57
|
+
return { ok: true, output };
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const e = err;
|
|
61
|
+
const output = [e.stdout, e.stderr].filter(Boolean).join('\n').trim();
|
|
62
|
+
return { ok: false, output: output || e.message || 'Command failed' };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Legacy shell function — ONLY for hardcoded commands with no user input */
|
|
21
66
|
function shell(cmd, opts) {
|
|
22
67
|
return execSync(cmd, {
|
|
23
68
|
encoding: 'utf-8',
|
|
@@ -40,14 +85,21 @@ function shellSafe(cmd, opts) {
|
|
|
40
85
|
}
|
|
41
86
|
}
|
|
42
87
|
function isCommandAvailable(cmd) {
|
|
88
|
+
// Only hardcoded command names — never user input
|
|
89
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(cmd))
|
|
90
|
+
return false;
|
|
43
91
|
try {
|
|
44
|
-
execSync(`which ${cmd}`, { encoding: 'utf-8', timeout: 5_000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
92
|
+
execSync(`which ${esc(cmd)}`, { encoding: 'utf-8', timeout: 5_000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
45
93
|
return true;
|
|
46
94
|
}
|
|
47
95
|
catch {
|
|
48
96
|
return false;
|
|
49
97
|
}
|
|
50
98
|
}
|
|
99
|
+
/** Escape a string for safe embedding in a Python string literal (double-quoted) */
|
|
100
|
+
function pyEsc(s) {
|
|
101
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
102
|
+
}
|
|
51
103
|
function estimateTokens(text) {
|
|
52
104
|
// Rough estimate: ~4 chars per token for English text
|
|
53
105
|
return Math.ceil(text.length / 4);
|
|
@@ -393,7 +445,7 @@ export function registerTrainingTools() {
|
|
|
393
445
|
description: 'System prompt to prepend to each example (optional)',
|
|
394
446
|
},
|
|
395
447
|
},
|
|
396
|
-
tier: '
|
|
448
|
+
tier: 'free',
|
|
397
449
|
timeout: 300_000,
|
|
398
450
|
async execute(args) {
|
|
399
451
|
try {
|
|
@@ -527,7 +579,7 @@ export function registerTrainingTools() {
|
|
|
527
579
|
description: 'Dataset format: jsonl, alpaca, sharegpt (default: jsonl)',
|
|
528
580
|
},
|
|
529
581
|
},
|
|
530
|
-
tier: '
|
|
582
|
+
tier: 'free',
|
|
531
583
|
timeout: 120_000,
|
|
532
584
|
async execute(args) {
|
|
533
585
|
try {
|
|
@@ -814,7 +866,7 @@ export function registerTrainingTools() {
|
|
|
814
866
|
description: 'API key for cloud backends (optional — reads from env or ~/.kbot/config.json)',
|
|
815
867
|
},
|
|
816
868
|
},
|
|
817
|
-
tier: '
|
|
869
|
+
tier: 'free',
|
|
818
870
|
timeout: 600_000,
|
|
819
871
|
async execute(args) {
|
|
820
872
|
try {
|
|
@@ -829,6 +881,9 @@ export function registerTrainingTools() {
|
|
|
829
881
|
if (!existsSync(datasetPath)) {
|
|
830
882
|
return `Error: Dataset not found: ${datasetPath}`;
|
|
831
883
|
}
|
|
884
|
+
// Input validation
|
|
885
|
+
validatePath(datasetPath, 'Dataset');
|
|
886
|
+
validateIdentifier(baseModel, 'Base model');
|
|
832
887
|
const validBackends = ['openai', 'together', 'mistral', 'mlx', 'unsloth', 'llama-cpp'];
|
|
833
888
|
if (!validBackends.includes(backend)) {
|
|
834
889
|
return `Error: Invalid backend "${backend}". Supported: ${validBackends.join(', ')}`;
|
|
@@ -1022,27 +1077,27 @@ export function registerTrainingTools() {
|
|
|
1022
1077
|
const iters = epochs * 100; // Rough: iters = epochs * (dataset_size / batch_size)
|
|
1023
1078
|
const adapterPath = join(outputDir, 'adapters');
|
|
1024
1079
|
mkdirSync(adapterPath, { recursive: true });
|
|
1025
|
-
const
|
|
1026
|
-
'
|
|
1027
|
-
|
|
1028
|
-
|
|
1080
|
+
const cmdArgs = [
|
|
1081
|
+
'-m', 'mlx_lm.lora',
|
|
1082
|
+
'--model', baseModel,
|
|
1083
|
+
'--data', datasetPath,
|
|
1029
1084
|
'--train',
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
]
|
|
1085
|
+
'--iters', String(iters),
|
|
1086
|
+
'--batch-size', String(batchSize),
|
|
1087
|
+
'--lora-layers', String(loraRank),
|
|
1088
|
+
'--adapter-path', adapterPath,
|
|
1089
|
+
];
|
|
1035
1090
|
// Write the command to a script file for background execution
|
|
1036
1091
|
const scriptPath = join(outputDir, 'train.sh');
|
|
1037
1092
|
const logPath = join(outputDir, 'train.log');
|
|
1093
|
+
const escapedCmd = ['python3', ...cmdArgs].map(a => esc(a)).join(' ');
|
|
1038
1094
|
writeFileSync(scriptPath, [
|
|
1039
1095
|
'#!/bin/bash',
|
|
1040
|
-
`echo "Training started at $(date)" > ${logPath}`,
|
|
1041
|
-
`echo "Command: ${
|
|
1042
|
-
`${
|
|
1043
|
-
`echo "Training finished at $(date)" >> ${logPath}`,
|
|
1044
|
-
].join('\n'), 'utf-8');
|
|
1045
|
-
shell(`chmod +x ${scriptPath}`);
|
|
1096
|
+
`echo "Training started at $(date)" > ${esc(logPath)}`,
|
|
1097
|
+
`echo "Command: ${escapedCmd}" >> ${esc(logPath)}`,
|
|
1098
|
+
`${escapedCmd} 2>&1 | tee -a ${esc(logPath)}`,
|
|
1099
|
+
`echo "Training finished at $(date)" >> ${esc(logPath)}`,
|
|
1100
|
+
].join('\n'), { encoding: 'utf-8', mode: 0o700 });
|
|
1046
1101
|
// Launch in background
|
|
1047
1102
|
const child = spawn('bash', [scriptPath], {
|
|
1048
1103
|
detached: true,
|
|
@@ -1091,9 +1146,9 @@ from transformers import TrainingArguments
|
|
|
1091
1146
|
from datasets import load_dataset
|
|
1092
1147
|
|
|
1093
1148
|
# Configuration
|
|
1094
|
-
BASE_MODEL = "${baseModel}"
|
|
1095
|
-
DATASET_PATH = "${datasetPath}"
|
|
1096
|
-
OUTPUT_DIR = "${outputDir}"
|
|
1149
|
+
BASE_MODEL = "${pyEsc(baseModel)}"
|
|
1150
|
+
DATASET_PATH = "${pyEsc(datasetPath)}"
|
|
1151
|
+
OUTPUT_DIR = "${pyEsc(outputDir)}"
|
|
1097
1152
|
EPOCHS = ${epochs}
|
|
1098
1153
|
BATCH_SIZE = ${batchSize}
|
|
1099
1154
|
LEARNING_RATE = ${learningRate}
|
|
@@ -1184,11 +1239,10 @@ print("Done!")
|
|
|
1184
1239
|
const launchScript = join(outputDir, 'train.sh');
|
|
1185
1240
|
writeFileSync(launchScript, [
|
|
1186
1241
|
'#!/bin/bash',
|
|
1187
|
-
`echo "Training started at $(date)" > ${logPath}`,
|
|
1188
|
-
`python3 ${scriptPath} 2>&1 | tee -a ${logPath}`,
|
|
1189
|
-
`echo "Training finished at $(date)" >> ${logPath}`,
|
|
1190
|
-
].join('\n'), 'utf-8');
|
|
1191
|
-
shell(`chmod +x ${launchScript}`);
|
|
1242
|
+
`echo "Training started at $(date)" > ${esc(logPath)}`,
|
|
1243
|
+
`python3 ${esc(scriptPath)} 2>&1 | tee -a ${esc(logPath)}`,
|
|
1244
|
+
`echo "Training finished at $(date)" >> ${esc(logPath)}`,
|
|
1245
|
+
].join('\n'), { encoding: 'utf-8', mode: 0o700 });
|
|
1192
1246
|
const child = spawn('bash', [launchScript], {
|
|
1193
1247
|
detached: true,
|
|
1194
1248
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
@@ -1230,26 +1284,26 @@ print("Done!")
|
|
|
1230
1284
|
const threads = Math.max(1, cpus().length - 2);
|
|
1231
1285
|
const loraOutPath = join(outputDir, 'lora-adapter.bin');
|
|
1232
1286
|
const logPath = join(outputDir, 'train.log');
|
|
1233
|
-
const
|
|
1287
|
+
const cmdArgs = [
|
|
1234
1288
|
'llama-finetune',
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
]
|
|
1289
|
+
'--model-base', baseModel,
|
|
1290
|
+
'--lora-out', loraOutPath,
|
|
1291
|
+
'--train-data', datasetPath,
|
|
1292
|
+
'--threads', String(threads),
|
|
1293
|
+
'--epochs', String(epochs),
|
|
1294
|
+
'--batch', String(batchSize),
|
|
1295
|
+
'--lora-r', String(loraRank),
|
|
1296
|
+
'--lora-alpha', String(loraAlpha),
|
|
1297
|
+
];
|
|
1244
1298
|
const scriptPath = join(outputDir, 'train.sh');
|
|
1299
|
+
const escapedLlamaCmd = cmdArgs.map(a => esc(a)).join(' ');
|
|
1245
1300
|
writeFileSync(scriptPath, [
|
|
1246
1301
|
'#!/bin/bash',
|
|
1247
|
-
`echo "Training started at $(date)" > ${logPath}`,
|
|
1248
|
-
`echo "Command: ${
|
|
1249
|
-
`${
|
|
1250
|
-
`echo "Training finished at $(date)" >> ${logPath}`,
|
|
1251
|
-
].join('\n'), 'utf-8');
|
|
1252
|
-
shell(`chmod +x ${scriptPath}`);
|
|
1302
|
+
`echo "Training started at $(date)" > ${esc(logPath)}`,
|
|
1303
|
+
`echo "Command: ${escapedLlamaCmd}" >> ${esc(logPath)}`,
|
|
1304
|
+
`${escapedLlamaCmd} 2>&1 | tee -a ${esc(logPath)}`,
|
|
1305
|
+
`echo "Training finished at $(date)" >> ${esc(logPath)}`,
|
|
1306
|
+
].join('\n'), { encoding: 'utf-8', mode: 0o700 });
|
|
1253
1307
|
const child = spawn('bash', [scriptPath], {
|
|
1254
1308
|
detached: true,
|
|
1255
1309
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
@@ -1303,7 +1357,7 @@ print("Done!")
|
|
|
1303
1357
|
description: 'API key for cloud backends (optional)',
|
|
1304
1358
|
},
|
|
1305
1359
|
},
|
|
1306
|
-
tier: '
|
|
1360
|
+
tier: 'free',
|
|
1307
1361
|
timeout: 30_000,
|
|
1308
1362
|
async execute(args) {
|
|
1309
1363
|
try {
|
|
@@ -1516,7 +1570,7 @@ print("Done!")
|
|
|
1516
1570
|
description: 'API key for cloud backends (optional)',
|
|
1517
1571
|
},
|
|
1518
1572
|
},
|
|
1519
|
-
tier: '
|
|
1573
|
+
tier: 'free',
|
|
1520
1574
|
timeout: 600_000,
|
|
1521
1575
|
async execute(args) {
|
|
1522
1576
|
try {
|
|
@@ -1603,7 +1657,7 @@ print("Done!")
|
|
|
1603
1657
|
const prompt = tc.system
|
|
1604
1658
|
? `System: ${tc.system}\n\nUser: ${tc.prompt}\n\nAssistant:`
|
|
1605
1659
|
: `User: ${tc.prompt}\n\nAssistant:`;
|
|
1606
|
-
const result =
|
|
1660
|
+
const result = execSafeResult('ollama', ['run', model, prompt], { timeout: 60_000 });
|
|
1607
1661
|
actual = result.ok ? result.output : '';
|
|
1608
1662
|
}
|
|
1609
1663
|
else if (backend === 'llama-cpp') {
|
|
@@ -1611,7 +1665,7 @@ print("Done!")
|
|
|
1611
1665
|
return 'Error: llama-cli is not installed. Build from https://github.com/ggerganov/llama.cpp';
|
|
1612
1666
|
}
|
|
1613
1667
|
const prompt = tc.prompt;
|
|
1614
|
-
const result =
|
|
1668
|
+
const result = execSafeResult('llama-cli', ['-m', model, '-p', prompt, '-n', '512', '--temp', '0.1'], { timeout: 120_000 });
|
|
1615
1669
|
actual = result.ok ? result.output : '';
|
|
1616
1670
|
}
|
|
1617
1671
|
else if (backend === 'openai' || backend === 'together' || backend === 'mistral') {
|
|
@@ -1730,7 +1784,7 @@ print("Done!")
|
|
|
1730
1784
|
description: 'Quantization type for to_gguf and quantize: q4_K_M, q5_K_M, q8_0, f16 (default: q4_K_M)',
|
|
1731
1785
|
},
|
|
1732
1786
|
},
|
|
1733
|
-
tier: '
|
|
1787
|
+
tier: 'free',
|
|
1734
1788
|
timeout: 600_000,
|
|
1735
1789
|
async execute(args) {
|
|
1736
1790
|
try {
|
|
@@ -1754,8 +1808,7 @@ print("Done!")
|
|
|
1754
1808
|
// Try MLX merge first (Apple Silicon)
|
|
1755
1809
|
const hasMlx = shellSafe('python3 -c "import mlx_lm"');
|
|
1756
1810
|
if (hasMlx.ok) {
|
|
1757
|
-
const
|
|
1758
|
-
const result = shellSafe(cmd, { timeout: 300_000 });
|
|
1811
|
+
const result = execSafeResult('python3', ['-m', 'mlx_lm.fuse', '--model', baseModel, '--adapter-path', modelPath, '--save-path', outputPath], { timeout: 300_000 });
|
|
1759
1812
|
if (result.ok) {
|
|
1760
1813
|
return [
|
|
1761
1814
|
`LoRA merge completed (MLX).`,
|
|
@@ -1775,27 +1828,27 @@ import torch
|
|
|
1775
1828
|
from peft import PeftModel
|
|
1776
1829
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
1777
1830
|
|
|
1778
|
-
print("Loading base model: ${baseModel}")
|
|
1779
|
-
model = AutoModelForCausalLM.from_pretrained("${baseModel}", torch_dtype=torch.float16, device_map="auto")
|
|
1780
|
-
tokenizer = AutoTokenizer.from_pretrained("${baseModel}")
|
|
1831
|
+
print("Loading base model: ${pyEsc(baseModel)}")
|
|
1832
|
+
model = AutoModelForCausalLM.from_pretrained("${pyEsc(baseModel)}", torch_dtype=torch.float16, device_map="auto")
|
|
1833
|
+
tokenizer = AutoTokenizer.from_pretrained("${pyEsc(baseModel)}")
|
|
1781
1834
|
|
|
1782
|
-
print("Loading LoRA adapter: ${modelPath}")
|
|
1783
|
-
model = PeftModel.from_pretrained(model, "${modelPath}")
|
|
1835
|
+
print("Loading LoRA adapter: ${pyEsc(modelPath)}")
|
|
1836
|
+
model = PeftModel.from_pretrained(model, "${pyEsc(modelPath)}")
|
|
1784
1837
|
|
|
1785
1838
|
print("Merging LoRA weights into base model...")
|
|
1786
1839
|
model = model.merge_and_unload()
|
|
1787
1840
|
|
|
1788
|
-
print("Saving merged model to: ${outputPath}")
|
|
1789
|
-
model.save_pretrained("${outputPath}")
|
|
1790
|
-
tokenizer.save_pretrained("${outputPath}")
|
|
1841
|
+
print("Saving merged model to: ${pyEsc(outputPath)}")
|
|
1842
|
+
model.save_pretrained("${pyEsc(outputPath)}")
|
|
1843
|
+
tokenizer.save_pretrained("${pyEsc(outputPath)}")
|
|
1791
1844
|
print("Done!")
|
|
1792
1845
|
`;
|
|
1793
1846
|
const scriptPath = join(dirname(modelPath), '_merge_lora.py');
|
|
1794
|
-
writeFileSync(scriptPath, mergeScript, 'utf-8');
|
|
1795
|
-
const result =
|
|
1847
|
+
writeFileSync(scriptPath, mergeScript, { encoding: 'utf-8', mode: 0o700 });
|
|
1848
|
+
const result = execSafeResult('python3', [scriptPath], { timeout: 600_000 });
|
|
1796
1849
|
// Clean up script
|
|
1797
1850
|
try {
|
|
1798
|
-
|
|
1851
|
+
unlinkSync(scriptPath);
|
|
1799
1852
|
}
|
|
1800
1853
|
catch { /* ignore */ }
|
|
1801
1854
|
if (!result.ok) {
|
|
@@ -1816,13 +1869,13 @@ print("Done!")
|
|
|
1816
1869
|
? resolve(String(args.output))
|
|
1817
1870
|
: resolve(dirname(modelPath), `${basename(modelPath)}.${quantType}.gguf`);
|
|
1818
1871
|
// Try llama.cpp's convert script
|
|
1819
|
-
|
|
1820
|
-
let
|
|
1821
|
-
|
|
1822
|
-
|
|
1872
|
+
// Find conversion script
|
|
1873
|
+
let convertScriptPath = '';
|
|
1874
|
+
const whichResult = shellSafe('which convert_hf_to_gguf.py || which convert-hf-to-gguf.py');
|
|
1875
|
+
if (whichResult.ok && whichResult.output) {
|
|
1876
|
+
convertScriptPath = whichResult.output;
|
|
1823
1877
|
}
|
|
1824
1878
|
else {
|
|
1825
|
-
// Try finding it in common locations
|
|
1826
1879
|
const commonPaths = [
|
|
1827
1880
|
join(homedir(), 'llama.cpp/convert_hf_to_gguf.py'),
|
|
1828
1881
|
join(homedir(), 'llama.cpp/convert-hf-to-gguf.py'),
|
|
@@ -1839,12 +1892,12 @@ print("Done!")
|
|
|
1839
1892
|
' pip install -r requirements.txt',
|
|
1840
1893
|
'',
|
|
1841
1894
|
'Then run:',
|
|
1842
|
-
` python3 convert_hf_to_gguf.py
|
|
1895
|
+
` python3 convert_hf_to_gguf.py <model_path> --outfile <output> --outtype ${quantType}`,
|
|
1843
1896
|
].join('\n');
|
|
1844
1897
|
}
|
|
1845
|
-
|
|
1898
|
+
convertScriptPath = found;
|
|
1846
1899
|
}
|
|
1847
|
-
const result =
|
|
1900
|
+
const result = execSafeResult('python3', [convertScriptPath, modelPath, '--outfile', outputPath, '--outtype', quantType], { timeout: 600_000 });
|
|
1848
1901
|
if (!result.ok) {
|
|
1849
1902
|
return `Error converting to GGUF:\n${result.output}`;
|
|
1850
1903
|
}
|
|
@@ -1873,7 +1926,7 @@ print("Done!")
|
|
|
1873
1926
|
const outputPath = args.output
|
|
1874
1927
|
? resolve(String(args.output))
|
|
1875
1928
|
: modelPath.replace(/\.gguf$/, '') + `.${quantType}.gguf`;
|
|
1876
|
-
const result =
|
|
1929
|
+
const result = execSafeResult('llama-quantize', [modelPath, outputPath, quantType], { timeout: 600_000 });
|
|
1877
1930
|
if (!result.ok) {
|
|
1878
1931
|
return `Error quantizing model:\n${result.output}`;
|
|
1879
1932
|
}
|
|
@@ -1925,7 +1978,7 @@ print("Done!")
|
|
|
1925
1978
|
description: 'Model description (optional)',
|
|
1926
1979
|
},
|
|
1927
1980
|
},
|
|
1928
|
-
tier: '
|
|
1981
|
+
tier: 'free',
|
|
1929
1982
|
timeout: 600_000,
|
|
1930
1983
|
async execute(args) {
|
|
1931
1984
|
try {
|
|
@@ -1945,7 +1998,13 @@ print("Done!")
|
|
|
1945
1998
|
}
|
|
1946
1999
|
// Determine if model is GGUF file or directory
|
|
1947
2000
|
const isGguf = modelPath.endsWith('.gguf');
|
|
1948
|
-
|
|
2001
|
+
// Validate name for Ollama (alphanumeric, hyphens, underscores, colons)
|
|
2002
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_:.-]*$/.test(name)) {
|
|
2003
|
+
return 'Error: Ollama model name must be alphanumeric (hyphens, underscores, colons, dots allowed).';
|
|
2004
|
+
}
|
|
2005
|
+
const fromLine = `FROM ${modelPath}`;
|
|
2006
|
+
// Escape description for Ollama Modelfile (prevent """ breakout)
|
|
2007
|
+
const safeDescription = description.replace(/"""/g, '').replace(/\\/g, '\\\\');
|
|
1949
2008
|
// Create a Modelfile
|
|
1950
2009
|
const modelfileContent = [
|
|
1951
2010
|
fromLine,
|
|
@@ -1954,12 +2013,12 @@ print("Done!")
|
|
|
1954
2013
|
`PARAMETER top_p 0.9`,
|
|
1955
2014
|
`PARAMETER top_k 40`,
|
|
1956
2015
|
'',
|
|
1957
|
-
`SYSTEM """${
|
|
2016
|
+
`SYSTEM """${safeDescription}"""`,
|
|
1958
2017
|
].join('\n');
|
|
1959
2018
|
const modelfilePath = join(dirname(modelPath), 'Modelfile');
|
|
1960
2019
|
writeFileSync(modelfilePath, modelfileContent, 'utf-8');
|
|
1961
2020
|
// Create the Ollama model
|
|
1962
|
-
const result =
|
|
2021
|
+
const result = execSafeResult('ollama', ['create', name, '-f', modelfilePath], { timeout: 300_000 });
|
|
1963
2022
|
if (!result.ok) {
|
|
1964
2023
|
return `Error creating Ollama model:\n${result.output}\n\nModelfile written to: ${modelfilePath}`;
|
|
1965
2024
|
}
|
|
@@ -1990,12 +2049,12 @@ print("Done!")
|
|
|
1990
2049
|
// Check if HF_TOKEN is available
|
|
1991
2050
|
const hasToken = process.env.HF_TOKEN || process.env.HUGGING_FACE_HUB_TOKEN;
|
|
1992
2051
|
if (!hasToken) {
|
|
1993
|
-
const loginCheck =
|
|
2052
|
+
const loginCheck = execSafeResult('huggingface-cli', ['whoami']);
|
|
1994
2053
|
if (!loginCheck.ok) {
|
|
1995
2054
|
return 'Error: Not authenticated with HuggingFace. Run: huggingface-cli login\nOr set HF_TOKEN environment variable.';
|
|
1996
2055
|
}
|
|
1997
2056
|
}
|
|
1998
|
-
const result =
|
|
2057
|
+
const result = execSafeResult('huggingface-cli', ['upload', name, modelPath], { timeout: 600_000 });
|
|
1999
2058
|
if (!result.ok) {
|
|
2000
2059
|
return `Error uploading to HuggingFace:\n${result.output}`;
|
|
2001
2060
|
}
|
|
@@ -2018,15 +2077,19 @@ print("Done!")
|
|
|
2018
2077
|
// Copy model file(s)
|
|
2019
2078
|
const stat = statSync(modelPath);
|
|
2020
2079
|
if (stat.isFile()) {
|
|
2021
|
-
const result =
|
|
2080
|
+
const result = execSafeResult('cp', [modelPath, destPath], { timeout: 120_000 });
|
|
2022
2081
|
if (!result.ok) {
|
|
2023
2082
|
return `Error copying model: ${result.output}`;
|
|
2024
2083
|
}
|
|
2025
2084
|
}
|
|
2026
2085
|
else if (stat.isDirectory()) {
|
|
2086
|
+
// Validate name for path safety
|
|
2087
|
+
if (/[\/\\]/.test(name) || name.includes('..')) {
|
|
2088
|
+
return 'Error: Model name must not contain path separators or ".."';
|
|
2089
|
+
}
|
|
2027
2090
|
const destDir = join(modelsDir, name);
|
|
2028
2091
|
mkdirSync(destDir, { recursive: true });
|
|
2029
|
-
const result =
|
|
2092
|
+
const result = execSafeResult('cp', ['-r', modelPath + '/.', destDir + '/'], { timeout: 300_000 });
|
|
2030
2093
|
if (!result.ok) {
|
|
2031
2094
|
return `Error copying model directory: ${result.output}`;
|
|
2032
2095
|
}
|