clef-mcp 0.1.5 → 0.2.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.md +171 -142
- package/assets/demo.svg +70 -0
- package/dist/cli.js +987 -280
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +534 -52
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ function limitsFromEnv(env = process.env) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// src/config/env.ts
|
|
32
|
+
var SUPPORTED_RUNTIMES = ["llama-cpp", "mlx"];
|
|
32
33
|
var SUPPORTED_LOG_LEVELS = ["error", "warn", "info", "debug"];
|
|
33
34
|
function parseLogLevel(value) {
|
|
34
35
|
if (!value) return void 0;
|
|
@@ -40,19 +41,20 @@ function defaultClefHome() {
|
|
|
40
41
|
}
|
|
41
42
|
function loadConfig(env = process.env) {
|
|
42
43
|
const logLevel = parseLogLevel(env.CLEF_LOG_LEVEL) ?? "error";
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
throw new Error(`Unsupported CLEF_RUNTIME "${
|
|
44
|
+
const runtimeRaw = (env.CLEF_RUNTIME?.trim() || "llama-cpp").toLowerCase();
|
|
45
|
+
if (!SUPPORTED_RUNTIMES.includes(runtimeRaw)) {
|
|
46
|
+
throw new Error(`Unsupported CLEF_RUNTIME "${runtimeRaw}". Supported: ${SUPPORTED_RUNTIMES.join(", ")}`);
|
|
46
47
|
}
|
|
48
|
+
const runtime = runtimeRaw;
|
|
47
49
|
const llamaBin = env.CLEF_LLAMA_BIN?.trim() || void 0;
|
|
48
50
|
const llamaReleaseTag = env.CLEF_LLAMA_RELEASE_TAG?.trim() || void 0;
|
|
49
51
|
return {
|
|
50
52
|
model: env.CLEF_MODEL?.trim() || "clef-flash",
|
|
51
53
|
clefHome: env.CLEF_HOME?.trim() || defaultClefHome(),
|
|
52
|
-
runtime
|
|
54
|
+
runtime,
|
|
53
55
|
logLevel,
|
|
54
|
-
llamaBin,
|
|
55
|
-
llamaReleaseTag,
|
|
56
|
+
llamaBin: runtime === "llama-cpp" ? llamaBin : void 0,
|
|
57
|
+
llamaReleaseTag: runtime === "llama-cpp" ? llamaReleaseTag : void 0,
|
|
56
58
|
limits: limitsFromEnv(env)
|
|
57
59
|
};
|
|
58
60
|
}
|
|
@@ -664,10 +666,10 @@ function isModelManifest(v) {
|
|
|
664
666
|
const m = v;
|
|
665
667
|
return m.schemaVersion === 1 && typeof m.model === "string" && typeof m.quant === "string" && typeof m.file === "string" && typeof m.modelPath === "string" && typeof m.bytes === "number";
|
|
666
668
|
}
|
|
667
|
-
async function readModelManifest(
|
|
669
|
+
async function readModelManifest(manifestPath2) {
|
|
668
670
|
let raw;
|
|
669
671
|
try {
|
|
670
|
-
raw = await fsp.readFile(
|
|
672
|
+
raw = await fsp.readFile(manifestPath2, "utf8");
|
|
671
673
|
} catch {
|
|
672
674
|
return void 0;
|
|
673
675
|
}
|
|
@@ -678,9 +680,9 @@ async function readModelManifest(manifestPath) {
|
|
|
678
680
|
return void 0;
|
|
679
681
|
}
|
|
680
682
|
}
|
|
681
|
-
async function writeModelManifest(
|
|
682
|
-
await fsp.mkdir(path3.dirname(
|
|
683
|
-
await fsp.writeFile(
|
|
683
|
+
async function writeModelManifest(manifestPath2, manifest) {
|
|
684
|
+
await fsp.mkdir(path3.dirname(manifestPath2), { recursive: true });
|
|
685
|
+
await fsp.writeFile(manifestPath2, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
684
686
|
}
|
|
685
687
|
async function findInstalledQuant(modelsDir, model, specs) {
|
|
686
688
|
const installed = [];
|
|
@@ -733,13 +735,13 @@ function isRuntimeManifest(v) {
|
|
|
733
735
|
const m = v;
|
|
734
736
|
return m.schemaVersion === 1 && m.kind === "llama-cpp" && typeof m.releaseTag === "string" && typeof m.asset === "string" && typeof m.binaryPath === "string";
|
|
735
737
|
}
|
|
736
|
-
async function writeRuntimeManifest(
|
|
737
|
-
await fsp.mkdir(path3.dirname(
|
|
738
|
-
await fsp.writeFile(
|
|
738
|
+
async function writeRuntimeManifest(manifestPath2, manifest) {
|
|
739
|
+
await fsp.mkdir(path3.dirname(manifestPath2), { recursive: true });
|
|
740
|
+
await fsp.writeFile(manifestPath2, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
739
741
|
}
|
|
740
|
-
function readRuntimeManifest(
|
|
742
|
+
function readRuntimeManifest(manifestPath2) {
|
|
741
743
|
try {
|
|
742
|
-
const parsed = JSON.parse(fs.readFileSync(
|
|
744
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath2, "utf8"));
|
|
743
745
|
return isRuntimeManifest(parsed) ? parsed : void 0;
|
|
744
746
|
} catch {
|
|
745
747
|
return void 0;
|
|
@@ -801,6 +803,259 @@ function resolveLlamaServerBinary(cfg, deps = {}) {
|
|
|
801
803
|
|
|
802
804
|
// src/runtime/probe.ts
|
|
803
805
|
import fs3 from "fs/promises";
|
|
806
|
+
|
|
807
|
+
// src/models/mlx-install.ts
|
|
808
|
+
import { execFile } from "child_process";
|
|
809
|
+
import fsSync from "fs";
|
|
810
|
+
import fsp2 from "fs/promises";
|
|
811
|
+
import path5 from "path";
|
|
812
|
+
function manifestPath(clefHome) {
|
|
813
|
+
return path5.join(pathsFor(clefHome).modelsDir, "clef-flash", "mlx", "manifest.json");
|
|
814
|
+
}
|
|
815
|
+
async function readMlxManifest(clefHome) {
|
|
816
|
+
let raw;
|
|
817
|
+
try {
|
|
818
|
+
raw = await fsp2.readFile(manifestPath(clefHome), "utf8");
|
|
819
|
+
} catch {
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const parsed = JSON.parse(raw);
|
|
824
|
+
if (parsed.kind === "mlx" && typeof parsed.snapshotPath === "string") return parsed;
|
|
825
|
+
return void 0;
|
|
826
|
+
} catch {
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function mlxIsInstalled(clefHome) {
|
|
831
|
+
const manifest = await readMlxManifest(clefHome);
|
|
832
|
+
if (!manifest) return void 0;
|
|
833
|
+
try {
|
|
834
|
+
await fsp2.access(path5.join(manifest.snapshotPath, "clef_mlx.py"));
|
|
835
|
+
return manifest;
|
|
836
|
+
} catch {
|
|
837
|
+
return void 0;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function resolveUv(envUv) {
|
|
841
|
+
if (envUv?.trim()) return envUv.trim();
|
|
842
|
+
const dirs = (process.env.PATH ?? "").split(path5.delimiter).filter(Boolean);
|
|
843
|
+
for (const dir of dirs) {
|
|
844
|
+
const candidate = path5.join(dir, "uv");
|
|
845
|
+
try {
|
|
846
|
+
fsSync.accessSync(candidate, fsSync.constants.X_OK);
|
|
847
|
+
return candidate;
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
throw new ClefError(
|
|
852
|
+
ClefErrorCode.RUNTIME_NOT_FOUND,
|
|
853
|
+
"uv is required for the MLX runtime but was not found on PATH.",
|
|
854
|
+
"Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/runtime/mlx.ts
|
|
859
|
+
import { spawn as spawn2 } from "child_process";
|
|
860
|
+
import net2 from "net";
|
|
861
|
+
var STDERR_TAIL_LIMIT2 = 4e3;
|
|
862
|
+
function freePort2() {
|
|
863
|
+
return new Promise((resolve, reject) => {
|
|
864
|
+
const srv = net2.createServer();
|
|
865
|
+
srv.unref();
|
|
866
|
+
srv.on("error", reject);
|
|
867
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
868
|
+
const addr = srv.address();
|
|
869
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
870
|
+
srv.close(() => port > 0 ? resolve(port) : reject(new Error("could not acquire a free port")));
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
async function fetchWithTimeout2(url, init, timeoutMs) {
|
|
875
|
+
const controller = new AbortController();
|
|
876
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
877
|
+
try {
|
|
878
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
879
|
+
} finally {
|
|
880
|
+
clearTimeout(timer);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
var OOM_PATTERNS2 = ["out of memory", "failed to allocate", "unable to allocate", "not enough memory"];
|
|
884
|
+
var MlxRuntime = class {
|
|
885
|
+
constructor(opts) {
|
|
886
|
+
this.opts = opts;
|
|
887
|
+
this.host = opts.host ?? "127.0.0.1";
|
|
888
|
+
this.loadTimeoutMs = opts.loadTimeoutMs ?? 18e4;
|
|
889
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? 12e4;
|
|
890
|
+
}
|
|
891
|
+
opts;
|
|
892
|
+
proc;
|
|
893
|
+
resolvedPort;
|
|
894
|
+
host;
|
|
895
|
+
loadTimeoutMs;
|
|
896
|
+
requestTimeoutMs;
|
|
897
|
+
queue = Promise.resolve();
|
|
898
|
+
get endpoint() {
|
|
899
|
+
if (!this.resolvedPort) throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "Runtime is not started");
|
|
900
|
+
return `http://${this.host}:${this.resolvedPort}`;
|
|
901
|
+
}
|
|
902
|
+
get running() {
|
|
903
|
+
return this.proc !== void 0 && this.proc.exitCode === null;
|
|
904
|
+
}
|
|
905
|
+
async load() {
|
|
906
|
+
if (this.running) return;
|
|
907
|
+
const port = this.opts.port ?? await freePort2();
|
|
908
|
+
this.resolvedPort = port;
|
|
909
|
+
const args = [
|
|
910
|
+
"run",
|
|
911
|
+
"--directory",
|
|
912
|
+
this.opts.snapshotPath,
|
|
913
|
+
"--with",
|
|
914
|
+
"mlx-vlm>=0.7.4,<0.8",
|
|
915
|
+
"--with",
|
|
916
|
+
"huggingface_hub",
|
|
917
|
+
"python",
|
|
918
|
+
"clef_mlx.py",
|
|
919
|
+
"serve",
|
|
920
|
+
"--host",
|
|
921
|
+
this.host,
|
|
922
|
+
"--port",
|
|
923
|
+
String(port)
|
|
924
|
+
];
|
|
925
|
+
this.opts.log?.debug("spawning clef_mlx.py serve", { uv: this.opts.uvBin, snapshot: this.opts.snapshotPath });
|
|
926
|
+
let proc;
|
|
927
|
+
try {
|
|
928
|
+
proc = spawn2(this.opts.uvBin, args, { stdio: ["ignore", "pipe", "pipe"], cwd: this.opts.snapshotPath });
|
|
929
|
+
} catch (err) {
|
|
930
|
+
throw new ClefError(
|
|
931
|
+
ClefErrorCode.RUNTIME_INIT_FAILED,
|
|
932
|
+
`Failed to start the MLX server: ${err instanceof Error ? err.message : String(err)}`,
|
|
933
|
+
"Run `clef-mcp doctor` for a full diagnosis."
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
this.proc = proc;
|
|
937
|
+
let stderrTail = "";
|
|
938
|
+
proc.stderr?.on("data", (chunk) => {
|
|
939
|
+
const text = chunk.toString();
|
|
940
|
+
stderrTail = (stderrTail + text).slice(-STDERR_TAIL_LIMIT2);
|
|
941
|
+
this.opts.log?.debug(text.trimEnd());
|
|
942
|
+
});
|
|
943
|
+
proc.stdout?.on("data", (chunk) => {
|
|
944
|
+
this.opts.log?.debug(chunk.toString().trimEnd());
|
|
945
|
+
});
|
|
946
|
+
let rejectOnExit;
|
|
947
|
+
const exitedBeforeReady = new Promise((_resolve, reject) => {
|
|
948
|
+
rejectOnExit = reject;
|
|
949
|
+
});
|
|
950
|
+
const onEarlyExit = (code, signal) => {
|
|
951
|
+
const oom = OOM_PATTERNS2.some((p) => stderrTail.toLowerCase().includes(p));
|
|
952
|
+
const message = `clef_mlx.py exited before becoming ready (code=${code ?? "null"}, signal=${signal ?? "null"})`;
|
|
953
|
+
const tail = stderrTail.trim();
|
|
954
|
+
rejectOnExit?.(
|
|
955
|
+
oom ? new ClefError(ClefErrorCode.OUT_OF_MEMORY, message, "Not enough free memory for this model. Try the llama.cpp runtime with Q4_K_M instead.") : new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, message + (tail ? `
|
|
956
|
+
stderr tail:
|
|
957
|
+
${tail}` : ""), "Run `clef-mcp doctor` for a full diagnosis.")
|
|
958
|
+
);
|
|
959
|
+
};
|
|
960
|
+
proc.once("exit", onEarlyExit);
|
|
961
|
+
try {
|
|
962
|
+
await Promise.race([this.waitUntilReady(), exitedBeforeReady]);
|
|
963
|
+
} finally {
|
|
964
|
+
proc.removeListener("exit", onEarlyExit);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async waitUntilReady() {
|
|
968
|
+
const deadline = Date.now() + this.loadTimeoutMs;
|
|
969
|
+
let lastError = "";
|
|
970
|
+
while (Date.now() < deadline) {
|
|
971
|
+
if (!this.running) throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "MLX server is not running");
|
|
972
|
+
try {
|
|
973
|
+
const res = await fetchWithTimeout2(`${this.endpoint}/health`, { method: "GET" }, 2e3);
|
|
974
|
+
if (res.status === 200) return;
|
|
975
|
+
lastError = `health status ${res.status}`;
|
|
976
|
+
} catch (err) {
|
|
977
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
978
|
+
}
|
|
979
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
980
|
+
}
|
|
981
|
+
throw new ClefError(
|
|
982
|
+
ClefErrorCode.MODEL_LOAD_FAILED,
|
|
983
|
+
`MLX server did not become ready within ${Math.round(this.loadTimeoutMs / 1e3)}s (last health check: ${lastError})`,
|
|
984
|
+
"The first load can take a while; subsequent loads reuse the snapshot cache."
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
async decide(input) {
|
|
988
|
+
const run = this.queue.then(() => this.decideOnce(input));
|
|
989
|
+
this.queue = run.catch(() => void 0);
|
|
990
|
+
return run;
|
|
991
|
+
}
|
|
992
|
+
async decideOnce(input) {
|
|
993
|
+
if (!this.running) {
|
|
994
|
+
throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "Runtime is not loaded; call load() first");
|
|
995
|
+
}
|
|
996
|
+
let res;
|
|
997
|
+
try {
|
|
998
|
+
res = await fetchWithTimeout2(
|
|
999
|
+
`${this.endpoint}/v1/systemone`,
|
|
1000
|
+
{ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(input) },
|
|
1001
|
+
this.requestTimeoutMs
|
|
1002
|
+
);
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1005
|
+
throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Inference request failed: ${msg}`);
|
|
1006
|
+
}
|
|
1007
|
+
const bodyText = await res.text();
|
|
1008
|
+
let body = void 0;
|
|
1009
|
+
try {
|
|
1010
|
+
body = bodyText ? JSON.parse(bodyText) : void 0;
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
if (!res.ok) {
|
|
1014
|
+
const apiMessage = body && typeof body === "object" && "error" in body ? String(body.error?.message ?? bodyText) : bodyText;
|
|
1015
|
+
throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Inference failed with HTTP ${res.status}: ${apiMessage.slice(0, 2e3)}`);
|
|
1016
|
+
}
|
|
1017
|
+
if (!body || typeof body !== "object") {
|
|
1018
|
+
throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Runtime returned a non-JSON response (HTTP ${res.status})`);
|
|
1019
|
+
}
|
|
1020
|
+
return body;
|
|
1021
|
+
}
|
|
1022
|
+
async health() {
|
|
1023
|
+
if (!this.running) return { status: "unavailable", detail: "not started" };
|
|
1024
|
+
try {
|
|
1025
|
+
const res = await fetchWithTimeout2(`${this.endpoint}/health`, { method: "GET" }, 1500);
|
|
1026
|
+
if (res.status === 200) return { status: "ready" };
|
|
1027
|
+
return { status: "loading", detail: `health status ${res.status}` };
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
return { status: "unavailable", detail: err instanceof Error ? err.message : String(err) };
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async unload() {
|
|
1033
|
+
const proc = this.proc;
|
|
1034
|
+
if (!proc) return;
|
|
1035
|
+
this.proc = void 0;
|
|
1036
|
+
if (proc.exitCode !== null || proc.signalCode) return;
|
|
1037
|
+
await new Promise((resolve) => {
|
|
1038
|
+
const killTimer = setTimeout(() => {
|
|
1039
|
+
try {
|
|
1040
|
+
proc.kill("SIGKILL");
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
}, 3e3);
|
|
1044
|
+
proc.once("exit", () => {
|
|
1045
|
+
clearTimeout(killTimer);
|
|
1046
|
+
resolve();
|
|
1047
|
+
});
|
|
1048
|
+
try {
|
|
1049
|
+
proc.kill("SIGTERM");
|
|
1050
|
+
} catch {
|
|
1051
|
+
clearTimeout(killTimer);
|
|
1052
|
+
resolve();
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// src/runtime/probe.ts
|
|
804
1059
|
async function probeInference(cfg, manifest, log, loadTimeoutMs = 12e4) {
|
|
805
1060
|
await fs3.access(manifest.modelPath).catch(() => {
|
|
806
1061
|
throw new ClefError(
|
|
@@ -835,8 +1090,8 @@ async function probeInference(cfg, manifest, log, loadTimeoutMs = 12e4) {
|
|
|
835
1090
|
|
|
836
1091
|
// src/models/hf.ts
|
|
837
1092
|
import { createHash as createHash2 } from "crypto";
|
|
838
|
-
import
|
|
839
|
-
import
|
|
1093
|
+
import fsp3 from "fs/promises";
|
|
1094
|
+
import path6 from "path";
|
|
840
1095
|
var HF_BASE_URL = "https://huggingface.co";
|
|
841
1096
|
function resolveUrl(repo, revision, file) {
|
|
842
1097
|
return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
|
|
@@ -894,7 +1149,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
894
1149
|
}
|
|
895
1150
|
const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
|
|
896
1151
|
const hash = createHash2("sha256");
|
|
897
|
-
const handle = await
|
|
1152
|
+
const handle = await fsp3.open(dest, "w");
|
|
898
1153
|
let bytes = 0;
|
|
899
1154
|
try {
|
|
900
1155
|
for await (const chunk of res.body) {
|
|
@@ -909,7 +1164,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
909
1164
|
}
|
|
910
1165
|
const sha256 = hash.digest("hex");
|
|
911
1166
|
if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
|
|
912
|
-
await
|
|
1167
|
+
await fsp3.rm(dest, { force: true });
|
|
913
1168
|
throw new ClefError(
|
|
914
1169
|
ClefErrorCode.CHECKSUM_MISMATCH,
|
|
915
1170
|
`Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
|
|
@@ -917,7 +1172,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
917
1172
|
);
|
|
918
1173
|
}
|
|
919
1174
|
if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
|
|
920
|
-
await
|
|
1175
|
+
await fsp3.rm(dest, { force: true });
|
|
921
1176
|
throw new ClefError(
|
|
922
1177
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
923
1178
|
`Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
|
|
@@ -928,24 +1183,24 @@ async function downloadOnce(url, dest, opts) {
|
|
|
928
1183
|
async function installModel(opts) {
|
|
929
1184
|
const { modelSpec, quantSpec } = opts;
|
|
930
1185
|
const { modelsDir } = pathsFor(opts.clefHome);
|
|
931
|
-
const
|
|
932
|
-
const existing = await readModelManifest(
|
|
1186
|
+
const manifestPath2 = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
|
|
1187
|
+
const existing = await readModelManifest(manifestPath2);
|
|
933
1188
|
if (existing && (await verifyModelFiles(existing)).ok) {
|
|
934
1189
|
opts.log?.debug("model already installed", { model: modelSpec.id, quant: quantSpec.id });
|
|
935
1190
|
return { manifest: existing, alreadyInstalled: true };
|
|
936
1191
|
}
|
|
937
1192
|
const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
|
|
938
|
-
const finalPath =
|
|
939
|
-
await
|
|
940
|
-
const tmpPath =
|
|
941
|
-
await
|
|
1193
|
+
const finalPath = path6.join(path6.dirname(manifestPath2), quantSpec.file);
|
|
1194
|
+
await fsp3.mkdir(path6.dirname(finalPath), { recursive: true });
|
|
1195
|
+
const tmpPath = path6.join(path6.dirname(finalPath), `.download-${quantSpec.file}`);
|
|
1196
|
+
await fsp3.rm(tmpPath, { force: true });
|
|
942
1197
|
opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
|
|
943
1198
|
const result = await downloadFile(info.downloadUrl, tmpPath, {
|
|
944
1199
|
expectedSha256: info.sha256,
|
|
945
1200
|
expectedSize: info.size ?? quantSpec.bytes,
|
|
946
1201
|
onProgress: opts.onProgress
|
|
947
1202
|
});
|
|
948
|
-
await
|
|
1203
|
+
await fsp3.rename(tmpPath, finalPath);
|
|
949
1204
|
const manifest = {
|
|
950
1205
|
schemaVersion: 1,
|
|
951
1206
|
model: modelSpec.id,
|
|
@@ -961,15 +1216,15 @@ async function installModel(opts) {
|
|
|
961
1216
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
962
1217
|
clefMcpVersion: opts.clefMcpVersion
|
|
963
1218
|
};
|
|
964
|
-
await writeModelManifest(
|
|
1219
|
+
await writeModelManifest(manifestPath2, manifest);
|
|
965
1220
|
opts.log?.info("model installed", { model: modelSpec.id, quant: quantSpec.id, sha256: result.sha256 });
|
|
966
1221
|
return { manifest, alreadyInstalled: false };
|
|
967
1222
|
}
|
|
968
1223
|
|
|
969
1224
|
// src/models/llama-install.ts
|
|
970
|
-
import { execFile } from "child_process";
|
|
971
|
-
import
|
|
972
|
-
import
|
|
1225
|
+
import { execFile as execFile2 } from "child_process";
|
|
1226
|
+
import fsp4 from "fs/promises";
|
|
1227
|
+
import path7 from "path";
|
|
973
1228
|
var GITHUB_API = "https://api.github.com/repos/ggml-org/llama.cpp";
|
|
974
1229
|
async function ghJson(url) {
|
|
975
1230
|
let res;
|
|
@@ -1023,7 +1278,7 @@ function pickAsset(release, platform, arch) {
|
|
|
1023
1278
|
}
|
|
1024
1279
|
function execTar(args, cwd) {
|
|
1025
1280
|
return new Promise((resolve, reject) => {
|
|
1026
|
-
|
|
1281
|
+
execFile2("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
|
|
1027
1282
|
if (err) reject(new Error(`tar ${args.join(" ")} failed: ${stderr || err.message}`));
|
|
1028
1283
|
else resolve();
|
|
1029
1284
|
});
|
|
@@ -1036,12 +1291,12 @@ async function findServerBinary(dir, platform) {
|
|
|
1036
1291
|
const current = queue.shift();
|
|
1037
1292
|
let entries;
|
|
1038
1293
|
try {
|
|
1039
|
-
entries = await
|
|
1294
|
+
entries = await fsp4.readdir(current, { withFileTypes: true });
|
|
1040
1295
|
} catch {
|
|
1041
1296
|
continue;
|
|
1042
1297
|
}
|
|
1043
1298
|
for (const entry of entries) {
|
|
1044
|
-
const full =
|
|
1299
|
+
const full = path7.join(current, entry.name);
|
|
1045
1300
|
if (entry.isDirectory()) queue.push(full);
|
|
1046
1301
|
else if (entry.name === target) return full;
|
|
1047
1302
|
}
|
|
@@ -1052,14 +1307,14 @@ async function findServerBinary(dir, platform) {
|
|
|
1052
1307
|
);
|
|
1053
1308
|
}
|
|
1054
1309
|
async function extractRelease(archivePath, destDir, platform) {
|
|
1055
|
-
await
|
|
1310
|
+
await fsp4.mkdir(destDir, { recursive: true });
|
|
1056
1311
|
const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
|
|
1057
1312
|
try {
|
|
1058
1313
|
await execTar(args, destDir);
|
|
1059
1314
|
} catch (err) {
|
|
1060
1315
|
throw new ClefError(
|
|
1061
1316
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
1062
|
-
`Could not extract ${
|
|
1317
|
+
`Could not extract ${path7.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1063
1318
|
"Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
|
|
1064
1319
|
);
|
|
1065
1320
|
}
|
|
@@ -1069,7 +1324,7 @@ async function ensureLlamaRuntime(cfg, log) {
|
|
|
1069
1324
|
const resolved = resolveLlamaServerBinary(cfg);
|
|
1070
1325
|
if (resolved.source === "managed") {
|
|
1071
1326
|
const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
|
|
1072
|
-
const manifest = readRuntimeManifest(
|
|
1327
|
+
const manifest = readRuntimeManifest(path7.join(platformDir, "runtime-manifest.json"));
|
|
1073
1328
|
return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
|
|
1074
1329
|
}
|
|
1075
1330
|
return { ...resolved, releaseTag: resolved.source, freshInstall: false };
|
|
@@ -1101,16 +1356,16 @@ async function downloadManagedLlama(cfg, log) {
|
|
|
1101
1356
|
}
|
|
1102
1357
|
const asset = pickAsset(release, platform, arch);
|
|
1103
1358
|
log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
|
|
1104
|
-
const tagDir =
|
|
1105
|
-
await
|
|
1106
|
-
const tmpArchive =
|
|
1107
|
-
await
|
|
1359
|
+
const tagDir = path7.join(platformDir, release.tag_name);
|
|
1360
|
+
await fsp4.mkdir(tagDir, { recursive: true });
|
|
1361
|
+
const tmpArchive = path7.join(platformDir, `.download-${asset.name}`);
|
|
1362
|
+
await fsp4.rm(tmpArchive, { force: true });
|
|
1108
1363
|
const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
|
|
1109
1364
|
await extractRelease(tmpArchive, tagDir, platform);
|
|
1110
|
-
await
|
|
1365
|
+
await fsp4.rm(tmpArchive, { force: true });
|
|
1111
1366
|
const binaryPath = await findServerBinary(tagDir, platform);
|
|
1112
|
-
if (platform !== "win32") await
|
|
1113
|
-
await writeRuntimeManifest(
|
|
1367
|
+
if (platform !== "win32") await fsp4.chmod(binaryPath, 493);
|
|
1368
|
+
await writeRuntimeManifest(path7.join(platformDir, "runtime-manifest.json"), {
|
|
1114
1369
|
schemaVersion: 1,
|
|
1115
1370
|
kind: "llama-cpp",
|
|
1116
1371
|
releaseTag: release.tag_name,
|
|
@@ -1126,20 +1381,20 @@ async function removeManagedRuntime(cfg) {
|
|
|
1126
1381
|
const { runtimeDir } = pathsFor(cfg.clefHome);
|
|
1127
1382
|
const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
|
|
1128
1383
|
try {
|
|
1129
|
-
await
|
|
1384
|
+
await fsp4.stat(dir);
|
|
1130
1385
|
} catch {
|
|
1131
1386
|
return false;
|
|
1132
1387
|
}
|
|
1133
|
-
await
|
|
1388
|
+
await fsp4.rm(dir, { recursive: true, force: true });
|
|
1134
1389
|
return true;
|
|
1135
1390
|
}
|
|
1136
1391
|
|
|
1137
1392
|
// src/models/platform.ts
|
|
1138
|
-
import { execFile as
|
|
1393
|
+
import { execFile as execFile3 } from "child_process";
|
|
1139
1394
|
import os2 from "os";
|
|
1140
1395
|
function exec(cmd, args, timeoutMs) {
|
|
1141
1396
|
return new Promise((resolve, reject) => {
|
|
1142
|
-
|
|
1397
|
+
execFile3(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
|
|
1143
1398
|
if (err) reject(err);
|
|
1144
1399
|
else resolve(stdout);
|
|
1145
1400
|
});
|
|
@@ -1331,13 +1586,223 @@ function validateStateSize(state, limits) {
|
|
|
1331
1586
|
}
|
|
1332
1587
|
}
|
|
1333
1588
|
|
|
1589
|
+
// src/mcp/prompts.ts
|
|
1590
|
+
import { z as z2 } from "zod";
|
|
1591
|
+
var TOOL_GUIDE = (questions) => [
|
|
1592
|
+
"Call the `clef_decide` MCP tool once with all of the questions below (they are scored in a single forward pass).",
|
|
1593
|
+
"",
|
|
1594
|
+
"Rules for the call:",
|
|
1595
|
+
"- `state` must be a compact, factual JSON or string \u2014 relevant facts only, well under 1 MB.",
|
|
1596
|
+
"- Use the exact `questions` given below (ids, types, criteria). Do not invent extra options.",
|
|
1597
|
+
"- If the answer distribution is flat (no option above ~0.6), gather more context and re-ask once before falling back to asking the user.",
|
|
1598
|
+
"- Treat safety-adjacent decisions (destructive actions, security) as requiring p >= 0.8 before acting; otherwise prefer asking the user.",
|
|
1599
|
+
"- Report the result as a short summary plus the top probability \u2014 do not paste raw JSON unless asked.",
|
|
1600
|
+
"",
|
|
1601
|
+
"Questions:",
|
|
1602
|
+
questions
|
|
1603
|
+
].join("\n");
|
|
1604
|
+
var PROMPTS = [
|
|
1605
|
+
{
|
|
1606
|
+
name: "incident-triage",
|
|
1607
|
+
title: "Production incident triage",
|
|
1608
|
+
description: "Frame a production incident as clef_decide questions: immediate action, severity and blast radius.",
|
|
1609
|
+
args: {
|
|
1610
|
+
incident_description: "Short factual description of the incident (symptoms, scope, recent changes)."
|
|
1611
|
+
},
|
|
1612
|
+
build: (a) => TOOL_GUIDE(
|
|
1613
|
+
[
|
|
1614
|
+
'- "action" (choice, instructions "What should the responder do first?"): rollback "Revert the latest deploy", hotfix "Ship a targeted fix now", investigate "Gather diagnostics before acting", wait "Observe; impact is not growing".',
|
|
1615
|
+
'- "severity" (score, instructions "How severe is the incident?", criteria ["low", "elevated", "high", "critical"]).',
|
|
1616
|
+
'- "user_impact" (noul, instructions "Are users actively affected right now?").'
|
|
1617
|
+
].join("\n")
|
|
1618
|
+
) + "\n\nIncident description:\n" + (a.incident_description ?? "")
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
name: "next-action",
|
|
1622
|
+
title: "Agent next action",
|
|
1623
|
+
description: "Ask Clef what the coding agent should do next on a task: inspect, modify, test or ask the user.",
|
|
1624
|
+
args: {
|
|
1625
|
+
task_summary: "What the agent is working on, current step, and what is already known or done."
|
|
1626
|
+
},
|
|
1627
|
+
build: (a) => TOOL_GUIDE(
|
|
1628
|
+
[
|
|
1629
|
+
'- "next_action" (choice, instructions "What should the coding agent do next?"): inspect "Inspect the code and gather more information", modify "Modify the code", test "Run additional tests", ask_user "Ask the user for clarification".',
|
|
1630
|
+
'- "confidence" (score, instructions "How confident are you in this decision?", criteria ["very_low", "low", "medium", "high", "very_high"]).'
|
|
1631
|
+
].join("\n")
|
|
1632
|
+
) + "\n\nTask summary:\n" + (a.task_summary ?? "")
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
name: "ticket-routing",
|
|
1636
|
+
title: "Support ticket routing",
|
|
1637
|
+
description: "Classify an incoming message into the team that should handle it.",
|
|
1638
|
+
args: {
|
|
1639
|
+
ticket_text: "The customer or user message, verbatim if possible."
|
|
1640
|
+
},
|
|
1641
|
+
build: (a) => TOOL_GUIDE(
|
|
1642
|
+
[
|
|
1643
|
+
'- "department" (choice, instructions "Which team should handle the message?"): billing "Payments, invoices and refunds", technical "Bugs, errors and outages", sales "Pricing and new purchases", other "Anything else".',
|
|
1644
|
+
'- "urgent" (noul, instructions "Does this need a response within the hour?").'
|
|
1645
|
+
].join("\n")
|
|
1646
|
+
) + "\n\nTicket text:\n" + (a.ticket_text ?? "")
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
name: "security-review",
|
|
1650
|
+
title: "Security sanity check",
|
|
1651
|
+
description: "Ask Clef whether a code snippet, change or setup smells like a security problem.",
|
|
1652
|
+
args: {
|
|
1653
|
+
change_summary: "The code, config or setup to review \u2014 compact but complete enough to judge."
|
|
1654
|
+
},
|
|
1655
|
+
build: (a) => TOOL_GUIDE(
|
|
1656
|
+
[
|
|
1657
|
+
'- "is_vulnerable" (noul, instructions "Does this contain a realistic security vulnerability?").',
|
|
1658
|
+
'- "risk" (score, instructions "How bad is it if this ships as-is?", criteria ["informational", "low", "medium", "high", "critical"]).',
|
|
1659
|
+
'- "first_mitigation" (choice, instructions "What is the first mitigation?"): fix_code "Fix the code before merging", restrict "Restrict access or add guardrails", monitor "Add detection and monitoring", none "No mitigation needed".'
|
|
1660
|
+
].join("\n") + "\nSafety rule: only act without human review when p(is_vulnerable=false) >= 0.8."
|
|
1661
|
+
) + "\n\nChange to review:\n" + (a.change_summary ?? "")
|
|
1662
|
+
}
|
|
1663
|
+
];
|
|
1664
|
+
function registerPrompts(server) {
|
|
1665
|
+
for (const p of PROMPTS) {
|
|
1666
|
+
server.registerPrompt(
|
|
1667
|
+
p.name,
|
|
1668
|
+
{
|
|
1669
|
+
title: p.title,
|
|
1670
|
+
description: p.description,
|
|
1671
|
+
argsSchema: Object.fromEntries(Object.entries(p.args).map(([k, v]) => [k, z2.string().min(1).describe(v)]))
|
|
1672
|
+
},
|
|
1673
|
+
({ ...args }) => {
|
|
1674
|
+
for (const key of Object.keys(p.args)) {
|
|
1675
|
+
if (typeof args[key] !== "string" || !args[key]) {
|
|
1676
|
+
throw new ClefError(ClefErrorCode.INVALID_INPUT, `Prompt argument "${key}" is required`);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return {
|
|
1680
|
+
messages: [{ role: "user", content: { type: "text", text: p.build(args) } }]
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// src/mcp/resources.ts
|
|
1688
|
+
import fsSync3 from "fs";
|
|
1689
|
+
import fsp6 from "fs/promises";
|
|
1690
|
+
|
|
1691
|
+
// src/cli/evals.ts
|
|
1692
|
+
import fsp5 from "fs/promises";
|
|
1693
|
+
import fsSync2 from "fs";
|
|
1694
|
+
import { fileURLToPath } from "url";
|
|
1695
|
+
import path8 from "path";
|
|
1696
|
+
function defaultDatasetPath() {
|
|
1697
|
+
let dir = path8.dirname(fileURLToPath(import.meta.url));
|
|
1698
|
+
for (let i = 0; i < 6; i++) {
|
|
1699
|
+
const candidate = path8.join(dir, "tests", "evals", "dataset.jsonl");
|
|
1700
|
+
if (fsSync2.existsSync(candidate)) return candidate;
|
|
1701
|
+
dir = path8.dirname(dir);
|
|
1702
|
+
}
|
|
1703
|
+
return fileURLToPath(new URL("../tests/evals/dataset.jsonl", import.meta.url));
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/mcp/resources.ts
|
|
1707
|
+
var EVALS_SCHEMA_MD = `# clef-mcp eval dataset format
|
|
1708
|
+
|
|
1709
|
+
JSONL: one JSON object per line.
|
|
1710
|
+
|
|
1711
|
+
{
|
|
1712
|
+
"id": "unique-case-id",
|
|
1713
|
+
"category": "coding | security | classification | routing | yes_no",
|
|
1714
|
+
"state": <string | JSON value>,
|
|
1715
|
+
"question": {
|
|
1716
|
+
"id": "question-id",
|
|
1717
|
+
"type": "choice | score | noul",
|
|
1718
|
+
"instructions": "\u2026",
|
|
1719
|
+
"criteria": { "option": "description" } // choice (or ["a","b"] list)
|
|
1720
|
+
| ["low", "high"] // score, ordered
|
|
1721
|
+
},
|
|
1722
|
+
"expected": {
|
|
1723
|
+
"kind": "argmax | prob_gt | min_prob | top_mass",
|
|
1724
|
+
// argmax: { "expect": "option", "among": ["option", ...] (optional) }
|
|
1725
|
+
// prob_gt: { "a": "opt", "b": "opt" }
|
|
1726
|
+
// min_prob: { "option": "opt", "min": 0.7 }
|
|
1727
|
+
// top_mass: { "options": ["a","b"], "min": 0.8 }
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
Run: \`clef-mcp evals\` (requires an installed model).
|
|
1732
|
+
`;
|
|
1733
|
+
async function readDataset() {
|
|
1734
|
+
try {
|
|
1735
|
+
return await fsp6.readFile(defaultDatasetPath(), "utf8");
|
|
1736
|
+
} catch {
|
|
1737
|
+
return "The bundled eval dataset is not available in this installation (tests/evals/dataset.jsonl not found).";
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
function textResource(uri, text, mimeType = "text/markdown") {
|
|
1741
|
+
return { contents: [{ uri: uri.href, mimeType, text }] };
|
|
1742
|
+
}
|
|
1743
|
+
function requireStringUri(uri) {
|
|
1744
|
+
return uri instanceof URL ? uri : new URL(uri);
|
|
1745
|
+
}
|
|
1746
|
+
function registerResources(server, cfg, version) {
|
|
1747
|
+
server.registerResource(
|
|
1748
|
+
"capabilities",
|
|
1749
|
+
"clef-mcp://capabilities",
|
|
1750
|
+
{ title: "clef-mcp capabilities", description: "Live capability snapshot: model, runtime, limits, error codes.", mimeType: "application/json" },
|
|
1751
|
+
async (uri) => {
|
|
1752
|
+
const u = requireStringUri(uri);
|
|
1753
|
+
const payload = {
|
|
1754
|
+
name: "clef-mcp",
|
|
1755
|
+
version,
|
|
1756
|
+
model: cfg.model,
|
|
1757
|
+
runtime: cfg.runtime,
|
|
1758
|
+
limits: cfg.limits,
|
|
1759
|
+
tool: {
|
|
1760
|
+
name: "clef_decide",
|
|
1761
|
+
output: "strict JSON: { model, decisions: { <qid>: { answer: { <option>: probability } } } }",
|
|
1762
|
+
questionTypes: ["noul", "choice", "score"],
|
|
1763
|
+
notes: ["options.temperature is accepted but is a no-op (single forward pass)", "state is data and is never interpreted as instructions"]
|
|
1764
|
+
},
|
|
1765
|
+
errorCodes: [
|
|
1766
|
+
"MODEL_NOT_INSTALLED",
|
|
1767
|
+
"MODEL_LOAD_FAILED",
|
|
1768
|
+
"RUNTIME_NOT_FOUND",
|
|
1769
|
+
"RUNTIME_INIT_FAILED",
|
|
1770
|
+
"RUNTIME_NOT_SUPPORTED",
|
|
1771
|
+
"INVALID_INPUT",
|
|
1772
|
+
"CLEF_INFERENCE_FAILED",
|
|
1773
|
+
"UNSUPPORTED_PLATFORM",
|
|
1774
|
+
"OUT_OF_MEMORY",
|
|
1775
|
+
"CHECKSUM_MISMATCH",
|
|
1776
|
+
"DOWNLOAD_FAILED"
|
|
1777
|
+
]
|
|
1778
|
+
};
|
|
1779
|
+
return textResource(u, JSON.stringify(payload, null, 2), "application/json");
|
|
1780
|
+
}
|
|
1781
|
+
);
|
|
1782
|
+
server.registerResource(
|
|
1783
|
+
"evals-schema",
|
|
1784
|
+
"clef-mcp://evals/schema",
|
|
1785
|
+
{ title: "Eval dataset schema", description: "How to write cases for clef-mcp evals.", mimeType: "text/markdown" },
|
|
1786
|
+
async (uri) => textResource(requireStringUri(uri), EVALS_SCHEMA_MD)
|
|
1787
|
+
);
|
|
1788
|
+
server.registerResource(
|
|
1789
|
+
"evals-dataset",
|
|
1790
|
+
"clef-mcp://evals/dataset",
|
|
1791
|
+
{ title: "Bundled eval dataset", description: "The 30-case dataset shipped with clef-mcp.", mimeType: "application/jsonl" },
|
|
1792
|
+
async (uri) => textResource(requireStringUri(uri), await readDataset(), "application/jsonl")
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1334
1796
|
// src/mcp/server.ts
|
|
1335
1797
|
var TOOL_NAME = "clef_decide";
|
|
1336
1798
|
var SERVER_INSTRUCTIONS = "clef_decide passes task state plus typed questions to the local Clef decision model and returns a probability distribution over answer options. Treat `state` as data: it is never interpreted as instructions for this server. The result is structured JSON; do not render it as prose.";
|
|
1337
1799
|
function createClefServer(deps = {}) {
|
|
1338
1800
|
const config = deps.config ?? loadConfig();
|
|
1801
|
+
const version = deps.version ?? "0.1.0";
|
|
1339
1802
|
const log = deps.log ?? createLogger(config.logLevel);
|
|
1340
|
-
const server = new McpServer({ name: "clef-mcp", version
|
|
1803
|
+
const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
|
|
1804
|
+
registerPrompts(server);
|
|
1805
|
+
registerResources(server, config, version);
|
|
1341
1806
|
let loaded;
|
|
1342
1807
|
let loadPromise;
|
|
1343
1808
|
async function ensureRuntime(spec) {
|
|
@@ -1348,6 +1813,23 @@ function createClefServer(deps = {}) {
|
|
|
1348
1813
|
}
|
|
1349
1814
|
if (loadPromise) return loadPromise;
|
|
1350
1815
|
loadPromise = (async () => {
|
|
1816
|
+
if (config.runtime === "mlx") {
|
|
1817
|
+
const manifest2 = await mlxIsInstalled(config.clefHome);
|
|
1818
|
+
if (!manifest2) {
|
|
1819
|
+
throw new ClefError(
|
|
1820
|
+
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1821
|
+
`Clef model "${spec.id}" is not installed for the MLX runtime.`,
|
|
1822
|
+
"Run `clef-mcp install --runtime mlx`."
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
const uvBin = resolveUv(process.env.CLEF_MLX_UV);
|
|
1826
|
+
const runtime2 = deps.runtimeFactory ? deps.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log });
|
|
1827
|
+
log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
|
|
1828
|
+
const loadStarted2 = Date.now();
|
|
1829
|
+
await runtime2.load();
|
|
1830
|
+
log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
|
|
1831
|
+
return { runtime: runtime2, model: spec.id };
|
|
1832
|
+
}
|
|
1351
1833
|
const manifest = await findInstalledManifest(config.clefHome, spec);
|
|
1352
1834
|
if (!manifest) {
|
|
1353
1835
|
throw new ClefError(
|
|
@@ -1357,7 +1839,7 @@ function createClefServer(deps = {}) {
|
|
|
1357
1839
|
);
|
|
1358
1840
|
}
|
|
1359
1841
|
const binary = resolveLlamaServerBinary(config);
|
|
1360
|
-
const runtime = deps.runtimeFactory ? deps.runtimeFactory({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
|
|
1842
|
+
const runtime = deps.runtimeFactory ? deps.runtimeFactory({ kind: "llama-cpp", serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
|
|
1361
1843
|
log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
|
|
1362
1844
|
const loadStarted = Date.now();
|
|
1363
1845
|
await runtime.load();
|
|
@@ -1432,7 +1914,7 @@ function createClefServer(deps = {}) {
|
|
|
1432
1914
|
}
|
|
1433
1915
|
|
|
1434
1916
|
// src/version.ts
|
|
1435
|
-
var VERSION = "0.
|
|
1917
|
+
var VERSION = "0.2.0";
|
|
1436
1918
|
export {
|
|
1437
1919
|
CLEF_FLASH,
|
|
1438
1920
|
ClefError,
|