clef-mcp 0.1.6 → 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/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 runtime = env.CLEF_RUNTIME?.trim() || "llama-cpp";
44
- if (runtime !== "llama-cpp") {
45
- throw new Error(`Unsupported CLEF_RUNTIME "${runtime}". Supported: llama-cpp`);
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: "llama-cpp",
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(manifestPath) {
669
+ async function readModelManifest(manifestPath2) {
668
670
  let raw;
669
671
  try {
670
- raw = await fsp.readFile(manifestPath, "utf8");
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(manifestPath, manifest) {
682
- await fsp.mkdir(path3.dirname(manifestPath), { recursive: true });
683
- await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
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(manifestPath, manifest) {
737
- await fsp.mkdir(path3.dirname(manifestPath), { recursive: true });
738
- await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
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(manifestPath) {
742
+ function readRuntimeManifest(manifestPath2) {
741
743
  try {
742
- const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
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 fsp2 from "fs/promises";
839
- import path5 from "path";
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 fsp2.open(dest, "w");
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 fsp2.rm(dest, { force: true });
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 fsp2.rm(dest, { force: true });
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 manifestPath = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
932
- const existing = await readModelManifest(manifestPath);
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 = path5.join(path5.dirname(manifestPath), quantSpec.file);
939
- await fsp2.mkdir(path5.dirname(finalPath), { recursive: true });
940
- const tmpPath = path5.join(path5.dirname(finalPath), `.download-${quantSpec.file}`);
941
- await fsp2.rm(tmpPath, { force: true });
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 fsp2.rename(tmpPath, finalPath);
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(manifestPath, manifest);
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 fsp3 from "fs/promises";
972
- import path6 from "path";
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
- execFile("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
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 fsp3.readdir(current, { withFileTypes: true });
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 = path6.join(current, entry.name);
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 fsp3.mkdir(destDir, { recursive: true });
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 ${path6.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
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(path6.join(platformDir, "runtime-manifest.json"));
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 = path6.join(platformDir, release.tag_name);
1105
- await fsp3.mkdir(tagDir, { recursive: true });
1106
- const tmpArchive = path6.join(platformDir, `.download-${asset.name}`);
1107
- await fsp3.rm(tmpArchive, { force: true });
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 fsp3.rm(tmpArchive, { force: true });
1365
+ await fsp4.rm(tmpArchive, { force: true });
1111
1366
  const binaryPath = await findServerBinary(tagDir, platform);
1112
- if (platform !== "win32") await fsp3.chmod(binaryPath, 493);
1113
- await writeRuntimeManifest(path6.join(platformDir, "runtime-manifest.json"), {
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 fsp3.stat(dir);
1384
+ await fsp4.stat(dir);
1130
1385
  } catch {
1131
1386
  return false;
1132
1387
  }
1133
- await fsp3.rm(dir, { recursive: true, force: true });
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 execFile2 } from "child_process";
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
- execFile2(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
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: deps.version ?? "0.1.0" }, { instructions: SERVER_INSTRUCTIONS });
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.1.6";
1917
+ var VERSION = "0.2.0";
1436
1918
  export {
1437
1919
  CLEF_FLASH,
1438
1920
  ClefError,