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/dist/cli.js CHANGED
@@ -102,11 +102,11 @@ __export(setup_exports, {
102
102
  installSkill: () => installSkill,
103
103
  runSetup: () => runSetup
104
104
  });
105
- import { execFile as execFile4 } from "child_process";
106
- import fsSync2 from "fs";
107
- import fsp6 from "fs/promises";
105
+ import { execFile as execFile5 } from "child_process";
106
+ import fsSync4 from "fs";
107
+ import fsp8 from "fs/promises";
108
108
  import os4 from "os";
109
- import path9 from "path";
109
+ import path10 from "path";
110
110
  import { fileURLToPath as fileURLToPath2 } from "url";
111
111
  function bundledSkillPath() {
112
112
  return fileURLToPath2(new URL("../skills/clef-decisions/SKILL.md", import.meta.url));
@@ -114,7 +114,7 @@ function bundledSkillPath() {
114
114
  async function mergeJsonServerEntry(file, serverKey, container) {
115
115
  let doc = {};
116
116
  try {
117
- doc = JSON.parse(await fsp6.readFile(file, "utf8"));
117
+ doc = JSON.parse(await fsp8.readFile(file, "utf8"));
118
118
  } catch {
119
119
  doc = {};
120
120
  }
@@ -131,15 +131,15 @@ async function mergeJsonServerEntry(file, serverKey, container) {
131
131
  servers[serverKey] = { command: SERVER_ENTRY.command, args: [...SERVER_ENTRY.args] };
132
132
  doc.mcpServers = servers;
133
133
  }
134
- await fsp6.mkdir(path9.dirname(file), { recursive: true });
135
- await fsp6.writeFile(file, JSON.stringify(doc, null, 2) + "\n", "utf8");
134
+ await fsp8.mkdir(path10.dirname(file), { recursive: true });
135
+ await fsp8.writeFile(file, JSON.stringify(doc, null, 2) + "\n", "utf8");
136
136
  return "configured";
137
137
  }
138
138
  async function setupCodex() {
139
- const file = path9.join(HOME(), ".codex", "config.toml");
139
+ const file = path10.join(HOME(), ".codex", "config.toml");
140
140
  let content = "";
141
141
  try {
142
- content = await fsp6.readFile(file, "utf8");
142
+ content = await fsp8.readFile(file, "utf8");
143
143
  } catch {
144
144
  content = "";
145
145
  }
@@ -147,26 +147,26 @@ async function setupCodex() {
147
147
  return { client: "codex", status: "already-configured", detail: file };
148
148
  }
149
149
  const next = (content && !content.endsWith("\n") ? content + "\n" : content) + "\n" + CODEX_SECTION;
150
- await fsp6.mkdir(path9.dirname(file), { recursive: true });
151
- await fsp6.writeFile(file, next, "utf8");
150
+ await fsp8.mkdir(path10.dirname(file), { recursive: true });
151
+ await fsp8.writeFile(file, next, "utf8");
152
152
  return { client: "codex", status: "configured", detail: file };
153
153
  }
154
154
  async function setupZcode() {
155
- const file = path9.join(HOME(), ".zcode", "cli", "config.json");
155
+ const file = path10.join(HOME(), ".zcode", "cli", "config.json");
156
156
  const status = await mergeJsonServerEntry(file, "clef-mcp", "mcp.servers");
157
157
  return { client: "zcode", status, detail: file };
158
158
  }
159
159
  async function setupCursor() {
160
- const file = path9.join(HOME(), ".cursor", "mcp.json");
160
+ const file = path10.join(HOME(), ".cursor", "mcp.json");
161
161
  const status = await mergeJsonServerEntry(file, "clef-mcp", "mcpServers");
162
162
  return { client: "cursor", status, detail: file };
163
163
  }
164
164
  function whichCli(bin) {
165
- const dirs = (process.env.PATH ?? "").split(path9.delimiter).filter(Boolean);
165
+ const dirs = (process.env.PATH ?? "").split(path10.delimiter).filter(Boolean);
166
166
  for (const dir of dirs) {
167
- const candidate = path9.join(dir, bin);
167
+ const candidate = path10.join(dir, bin);
168
168
  try {
169
- fsSync2.accessSync(candidate, fsSync2.constants.X_OK);
169
+ fsSync4.accessSync(candidate, fsSync4.constants.X_OK);
170
170
  return candidate;
171
171
  } catch {
172
172
  }
@@ -183,13 +183,13 @@ async function setupClaudeCode() {
183
183
  };
184
184
  }
185
185
  const existing = await new Promise((resolve) => {
186
- execFile4(claude, ["mcp", "list"], { timeout: 3e4, encoding: "utf8" }, (_err, stdout) => resolve(String(stdout)));
186
+ execFile5(claude, ["mcp", "list"], { timeout: 3e4, encoding: "utf8" }, (_err, stdout) => resolve(String(stdout)));
187
187
  });
188
188
  if (existing.includes("clef-mcp")) {
189
189
  return { client: "claude-code", status: "already-configured", detail: "claude mcp list" };
190
190
  }
191
191
  await new Promise((resolve, reject) => {
192
- execFile4(
192
+ execFile5(
193
193
  claude,
194
194
  ["mcp", "add", "--scope", "user", "clef-mcp", "--", "clef-mcp"],
195
195
  { timeout: 6e4, encoding: "utf8" },
@@ -200,19 +200,19 @@ async function setupClaudeCode() {
200
200
  }
201
201
  async function installSkill() {
202
202
  const src = bundledSkillPath();
203
- const destDir = path9.join(HOME(), ".agents", "skills", "clef-decisions");
204
- const dest = path9.join(destDir, "SKILL.md");
203
+ const destDir = path10.join(HOME(), ".agents", "skills", "clef-decisions");
204
+ const dest = path10.join(destDir, "SKILL.md");
205
205
  let content = "";
206
206
  try {
207
- content = await fsp6.readFile(dest, "utf8");
207
+ content = await fsp8.readFile(dest, "utf8");
208
208
  } catch {
209
209
  }
210
- const incoming = await fsp6.readFile(src, "utf8");
210
+ const incoming = await fsp8.readFile(src, "utf8");
211
211
  if (content === incoming) {
212
212
  return { client: "skill", status: "already-configured", detail: dest };
213
213
  }
214
- await fsp6.mkdir(destDir, { recursive: true });
215
- await fsp6.writeFile(dest, incoming, "utf8");
214
+ await fsp8.mkdir(destDir, { recursive: true });
215
+ await fsp8.writeFile(dest, incoming, "utf8");
216
216
  return { client: "skill", status: "configured", detail: dest };
217
217
  }
218
218
  function parseClients(raw) {
@@ -493,6 +493,7 @@ function limitsFromEnv(env = process.env) {
493
493
  }
494
494
 
495
495
  // src/config/env.ts
496
+ var SUPPORTED_RUNTIMES = ["llama-cpp", "mlx"];
496
497
  var SUPPORTED_LOG_LEVELS = ["error", "warn", "info", "debug"];
497
498
  function parseLogLevel(value) {
498
499
  if (!value) return void 0;
@@ -504,19 +505,20 @@ function defaultClefHome() {
504
505
  }
505
506
  function loadConfig(env = process.env) {
506
507
  const logLevel = parseLogLevel(env.CLEF_LOG_LEVEL) ?? "error";
507
- const runtime = env.CLEF_RUNTIME?.trim() || "llama-cpp";
508
- if (runtime !== "llama-cpp") {
509
- throw new Error(`Unsupported CLEF_RUNTIME "${runtime}". Supported: llama-cpp`);
508
+ const runtimeRaw = (env.CLEF_RUNTIME?.trim() || "llama-cpp").toLowerCase();
509
+ if (!SUPPORTED_RUNTIMES.includes(runtimeRaw)) {
510
+ throw new Error(`Unsupported CLEF_RUNTIME "${runtimeRaw}". Supported: ${SUPPORTED_RUNTIMES.join(", ")}`);
510
511
  }
512
+ const runtime = runtimeRaw;
511
513
  const llamaBin = env.CLEF_LLAMA_BIN?.trim() || void 0;
512
514
  const llamaReleaseTag = env.CLEF_LLAMA_RELEASE_TAG?.trim() || void 0;
513
515
  return {
514
516
  model: env.CLEF_MODEL?.trim() || "clef-flash",
515
517
  clefHome: env.CLEF_HOME?.trim() || defaultClefHome(),
516
- runtime: "llama-cpp",
518
+ runtime,
517
519
  logLevel,
518
- llamaBin,
519
- llamaReleaseTag,
520
+ llamaBin: runtime === "llama-cpp" ? llamaBin : void 0,
521
+ llamaReleaseTag: runtime === "llama-cpp" ? llamaReleaseTag : void 0,
520
522
  limits: limitsFromEnv(env)
521
523
  };
522
524
  }
@@ -671,10 +673,10 @@ function isModelManifest(v) {
671
673
  const m = v;
672
674
  return m.schemaVersion === 1 && typeof m.model === "string" && typeof m.quant === "string" && typeof m.file === "string" && typeof m.modelPath === "string" && typeof m.bytes === "number";
673
675
  }
674
- async function readModelManifest(manifestPath) {
676
+ async function readModelManifest(manifestPath2) {
675
677
  let raw;
676
678
  try {
677
- raw = await fsp.readFile(manifestPath, "utf8");
679
+ raw = await fsp.readFile(manifestPath2, "utf8");
678
680
  } catch {
679
681
  return void 0;
680
682
  }
@@ -685,9 +687,9 @@ async function readModelManifest(manifestPath) {
685
687
  return void 0;
686
688
  }
687
689
  }
688
- async function writeModelManifest(manifestPath, manifest) {
689
- await fsp.mkdir(path3.dirname(manifestPath), { recursive: true });
690
- await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
690
+ async function writeModelManifest(manifestPath2, manifest) {
691
+ await fsp.mkdir(path3.dirname(manifestPath2), { recursive: true });
692
+ await fsp.writeFile(manifestPath2, JSON.stringify(manifest, null, 2) + "\n", "utf8");
691
693
  }
692
694
  async function findInstalledQuant(modelsDir, model, specs) {
693
695
  const installed = [];
@@ -745,23 +747,165 @@ function isRuntimeManifest(v) {
745
747
  const m = v;
746
748
  return m.schemaVersion === 1 && m.kind === "llama-cpp" && typeof m.releaseTag === "string" && typeof m.asset === "string" && typeof m.binaryPath === "string";
747
749
  }
748
- async function writeRuntimeManifest(manifestPath, manifest) {
749
- await fsp.mkdir(path3.dirname(manifestPath), { recursive: true });
750
- await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
750
+ async function writeRuntimeManifest(manifestPath2, manifest) {
751
+ await fsp.mkdir(path3.dirname(manifestPath2), { recursive: true });
752
+ await fsp.writeFile(manifestPath2, JSON.stringify(manifest, null, 2) + "\n", "utf8");
751
753
  }
752
- function readRuntimeManifest(manifestPath) {
754
+ function readRuntimeManifest(manifestPath2) {
753
755
  try {
754
- const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
756
+ const parsed = JSON.parse(fs.readFileSync(manifestPath2, "utf8"));
755
757
  return isRuntimeManifest(parsed) ? parsed : void 0;
756
758
  } catch {
757
759
  return void 0;
758
760
  }
759
761
  }
760
762
 
763
+ // src/models/mlx-install.ts
764
+ init_errors();
765
+ import { execFile } from "child_process";
766
+ import fsSync from "fs";
767
+ import fsp2 from "fs/promises";
768
+ import path4 from "path";
769
+ var MLX_MODEL_REPO = "mlx-community/clef-flash-4bit";
770
+ var MLX_MODEL_REVISION = "140bf7e037f5fa95a96535feca112f46c15927cf";
771
+ function manifestPath(clefHome) {
772
+ return path4.join(pathsFor(clefHome).modelsDir, "clef-flash", "mlx", "manifest.json");
773
+ }
774
+ async function readMlxManifest(clefHome) {
775
+ let raw;
776
+ try {
777
+ raw = await fsp2.readFile(manifestPath(clefHome), "utf8");
778
+ } catch {
779
+ return void 0;
780
+ }
781
+ try {
782
+ const parsed = JSON.parse(raw);
783
+ if (parsed.kind === "mlx" && typeof parsed.snapshotPath === "string") return parsed;
784
+ return void 0;
785
+ } catch {
786
+ return void 0;
787
+ }
788
+ }
789
+ async function writeMlxManifest(clefHome, manifest) {
790
+ const file = manifestPath(clefHome);
791
+ await fsp2.mkdir(path4.dirname(file), { recursive: true });
792
+ await fsp2.writeFile(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
793
+ }
794
+ async function mlxIsInstalled(clefHome) {
795
+ const manifest = await readMlxManifest(clefHome);
796
+ if (!manifest) return void 0;
797
+ try {
798
+ await fsp2.access(path4.join(manifest.snapshotPath, "clef_mlx.py"));
799
+ return manifest;
800
+ } catch {
801
+ return void 0;
802
+ }
803
+ }
804
+ function resolveUv(envUv) {
805
+ if (envUv?.trim()) return envUv.trim();
806
+ const dirs = (process.env.PATH ?? "").split(path4.delimiter).filter(Boolean);
807
+ for (const dir of dirs) {
808
+ const candidate = path4.join(dir, "uv");
809
+ try {
810
+ fsSync.accessSync(candidate, fsSync.constants.X_OK);
811
+ return candidate;
812
+ } catch {
813
+ }
814
+ }
815
+ throw new ClefError(
816
+ ClefErrorCode.RUNTIME_NOT_FOUND,
817
+ "uv is required for the MLX runtime but was not found on PATH.",
818
+ "Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh"
819
+ );
820
+ }
821
+ async function runUv(uvBin, args, opts = {}) {
822
+ return new Promise((resolve, reject) => {
823
+ const child = execFile(
824
+ uvBin,
825
+ args,
826
+ { timeout: opts.timeoutMs ?? 6e5, maxBuffer: 16 * 1024 * 1024, encoding: "utf8" },
827
+ (err, stdout, stderr) => {
828
+ if (err) {
829
+ reject(
830
+ new ClefError(
831
+ ClefErrorCode.DOWNLOAD_FAILED,
832
+ `uv command failed: ${err.message.split("\n")[0]}${stderr ? `
833
+ ${stderr.trim().slice(0, 1e3)}` : ""}`,
834
+ "Make sure uv is up to date: uv self update"
835
+ )
836
+ );
837
+ } else {
838
+ resolve(stdout.trim());
839
+ }
840
+ }
841
+ );
842
+ if (opts.onStdoutLine) {
843
+ let buf = "";
844
+ child.stdout?.on("data", (chunk) => {
845
+ buf += chunk.toString();
846
+ const lines = buf.split("\n");
847
+ buf = lines.pop() ?? "";
848
+ for (const line of lines) opts.onStdoutLine(line);
849
+ });
850
+ }
851
+ });
852
+ }
853
+ async function installModelMlx(opts) {
854
+ const existing = await mlxIsInstalled(opts.clefHome);
855
+ if (existing) return existing;
856
+ const uvBin = resolveUv(opts.uvBin ?? process.env.CLEF_MLX_UV);
857
+ const hfCache = path4.join(pathsFor(opts.clefHome).root, "hf-cache");
858
+ await fsp2.mkdir(hfCache, { recursive: true });
859
+ opts.log?.info("downloading MLX model snapshot", { repo: MLX_MODEL_REPO, revision: MLX_MODEL_REVISION });
860
+ const py = [
861
+ "from huggingface_hub import snapshot_download",
862
+ `p = snapshot_download("${MLX_MODEL_REPO}", revision="${MLX_MODEL_REVISION}", cache_dir=r"${hfCache}")`,
863
+ "print(p)"
864
+ ].join("\n");
865
+ const stdout = await runUv(
866
+ uvBin,
867
+ ["run", "--with", "huggingface_hub", "python", "-c", py],
868
+ {
869
+ timeoutMs: 18e5,
870
+ onStdoutLine: (line) => {
871
+ if (line.trim()) opts.log?.debug(line.trimEnd());
872
+ }
873
+ }
874
+ );
875
+ const lines = stdout.split("\n").filter((l) => l.trim().startsWith("/"));
876
+ const snapshotPath = lines.at(-1)?.trim();
877
+ if (!snapshotPath) {
878
+ throw new ClefError(ClefErrorCode.DOWNLOAD_FAILED, "snapshot_download did not report a snapshot path");
879
+ }
880
+ await fsp2.access(path4.join(snapshotPath, "clef_mlx.py"));
881
+ const manifest = {
882
+ schemaVersion: 1,
883
+ kind: "mlx",
884
+ model: "clef-flash",
885
+ repo: MLX_MODEL_REPO,
886
+ revision: MLX_MODEL_REVISION,
887
+ snapshotPath,
888
+ licenseId: "Apache-2.0",
889
+ licenseUrl: "https://huggingface.co/Cloudflare/clef-flash",
890
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
891
+ clefMcpVersion: opts.clefMcpVersion
892
+ };
893
+ await writeMlxManifest(opts.clefHome, manifest);
894
+ opts.log?.info("MLX model installed", { snapshotPath });
895
+ return manifest;
896
+ }
897
+ async function removeMlxModel(clefHome) {
898
+ const manifest = await readMlxManifest(clefHome);
899
+ if (!manifest) return false;
900
+ await fsp2.rm(manifest.snapshotPath, { recursive: true, force: true }).catch(() => void 0);
901
+ await fsp2.rm(path4.dirname(manifestPath(clefHome)), { recursive: true, force: true });
902
+ return true;
903
+ }
904
+
761
905
  // src/runtime/binary.ts
762
906
  init_errors();
763
907
  import fs2 from "fs";
764
- import path4 from "path";
908
+ import path5 from "path";
765
909
  function existsExecutable(p) {
766
910
  try {
767
911
  fs2.accessSync(p, fs2.constants.X_OK);
@@ -771,11 +915,11 @@ function existsExecutable(p) {
771
915
  }
772
916
  }
773
917
  function whichBin(name, platform) {
774
- const dirs = (process.env.PATH ?? "").split(path4.delimiter).filter(Boolean);
918
+ const dirs = (process.env.PATH ?? "").split(path5.delimiter).filter(Boolean);
775
919
  const exts = platform === "win32" ? (process.env.PATHEXT ?? ".exe").split(";") : [""];
776
920
  for (const dir of dirs) {
777
921
  for (const ext of exts) {
778
- const candidate = path4.join(dir, name + ext.toLowerCase());
922
+ const candidate = path5.join(dir, name + ext.toLowerCase());
779
923
  if (existsExecutable(candidate)) return candidate;
780
924
  }
781
925
  }
@@ -783,9 +927,9 @@ function whichBin(name, platform) {
783
927
  }
784
928
  function managedBinary(clefHome, platform, arch, exists) {
785
929
  const platformDir = llamaRuntimeDir(pathsFor(clefHome).runtimeDir, platform, arch);
786
- const manifest = readRuntimeManifest(path4.join(platformDir, "runtime-manifest.json"));
930
+ const manifest = readRuntimeManifest(path5.join(platformDir, "runtime-manifest.json"));
787
931
  if (manifest && exists(manifest.binaryPath)) return manifest.binaryPath;
788
- const legacy = path4.join(platformDir, llamaServerName(platform));
932
+ const legacy = path5.join(platformDir, llamaServerName(platform));
789
933
  if (exists(legacy)) return legacy;
790
934
  return void 0;
791
935
  }
@@ -1053,6 +1197,207 @@ ${tail}` : ""),
1053
1197
  }
1054
1198
  };
1055
1199
 
1200
+ // src/runtime/mlx.ts
1201
+ init_errors();
1202
+ import { spawn as spawn2 } from "child_process";
1203
+ import net2 from "net";
1204
+ var STDERR_TAIL_LIMIT2 = 4e3;
1205
+ function freePort2() {
1206
+ return new Promise((resolve, reject) => {
1207
+ const srv = net2.createServer();
1208
+ srv.unref();
1209
+ srv.on("error", reject);
1210
+ srv.listen(0, "127.0.0.1", () => {
1211
+ const addr = srv.address();
1212
+ const port = typeof addr === "object" && addr ? addr.port : 0;
1213
+ srv.close(() => port > 0 ? resolve(port) : reject(new Error("could not acquire a free port")));
1214
+ });
1215
+ });
1216
+ }
1217
+ async function fetchWithTimeout2(url, init, timeoutMs) {
1218
+ const controller = new AbortController();
1219
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1220
+ try {
1221
+ return await fetch(url, { ...init, signal: controller.signal });
1222
+ } finally {
1223
+ clearTimeout(timer);
1224
+ }
1225
+ }
1226
+ var OOM_PATTERNS2 = ["out of memory", "failed to allocate", "unable to allocate", "not enough memory"];
1227
+ var MlxRuntime = class {
1228
+ constructor(opts) {
1229
+ this.opts = opts;
1230
+ this.host = opts.host ?? "127.0.0.1";
1231
+ this.loadTimeoutMs = opts.loadTimeoutMs ?? 18e4;
1232
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 12e4;
1233
+ }
1234
+ opts;
1235
+ proc;
1236
+ resolvedPort;
1237
+ host;
1238
+ loadTimeoutMs;
1239
+ requestTimeoutMs;
1240
+ queue = Promise.resolve();
1241
+ get endpoint() {
1242
+ if (!this.resolvedPort) throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "Runtime is not started");
1243
+ return `http://${this.host}:${this.resolvedPort}`;
1244
+ }
1245
+ get running() {
1246
+ return this.proc !== void 0 && this.proc.exitCode === null;
1247
+ }
1248
+ async load() {
1249
+ if (this.running) return;
1250
+ const port = this.opts.port ?? await freePort2();
1251
+ this.resolvedPort = port;
1252
+ const args = [
1253
+ "run",
1254
+ "--directory",
1255
+ this.opts.snapshotPath,
1256
+ "--with",
1257
+ "mlx-vlm>=0.7.4,<0.8",
1258
+ "--with",
1259
+ "huggingface_hub",
1260
+ "python",
1261
+ "clef_mlx.py",
1262
+ "serve",
1263
+ "--host",
1264
+ this.host,
1265
+ "--port",
1266
+ String(port)
1267
+ ];
1268
+ this.opts.log?.debug("spawning clef_mlx.py serve", { uv: this.opts.uvBin, snapshot: this.opts.snapshotPath });
1269
+ let proc;
1270
+ try {
1271
+ proc = spawn2(this.opts.uvBin, args, { stdio: ["ignore", "pipe", "pipe"], cwd: this.opts.snapshotPath });
1272
+ } catch (err) {
1273
+ throw new ClefError(
1274
+ ClefErrorCode.RUNTIME_INIT_FAILED,
1275
+ `Failed to start the MLX server: ${err instanceof Error ? err.message : String(err)}`,
1276
+ "Run `clef-mcp doctor` for a full diagnosis."
1277
+ );
1278
+ }
1279
+ this.proc = proc;
1280
+ let stderrTail = "";
1281
+ proc.stderr?.on("data", (chunk) => {
1282
+ const text = chunk.toString();
1283
+ stderrTail = (stderrTail + text).slice(-STDERR_TAIL_LIMIT2);
1284
+ this.opts.log?.debug(text.trimEnd());
1285
+ });
1286
+ proc.stdout?.on("data", (chunk) => {
1287
+ this.opts.log?.debug(chunk.toString().trimEnd());
1288
+ });
1289
+ let rejectOnExit;
1290
+ const exitedBeforeReady = new Promise((_resolve, reject) => {
1291
+ rejectOnExit = reject;
1292
+ });
1293
+ const onEarlyExit = (code, signal) => {
1294
+ const oom = OOM_PATTERNS2.some((p) => stderrTail.toLowerCase().includes(p));
1295
+ const message = `clef_mlx.py exited before becoming ready (code=${code ?? "null"}, signal=${signal ?? "null"})`;
1296
+ const tail = stderrTail.trim();
1297
+ rejectOnExit?.(
1298
+ 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 ? `
1299
+ stderr tail:
1300
+ ${tail}` : ""), "Run `clef-mcp doctor` for a full diagnosis.")
1301
+ );
1302
+ };
1303
+ proc.once("exit", onEarlyExit);
1304
+ try {
1305
+ await Promise.race([this.waitUntilReady(), exitedBeforeReady]);
1306
+ } finally {
1307
+ proc.removeListener("exit", onEarlyExit);
1308
+ }
1309
+ }
1310
+ async waitUntilReady() {
1311
+ const deadline = Date.now() + this.loadTimeoutMs;
1312
+ let lastError = "";
1313
+ while (Date.now() < deadline) {
1314
+ if (!this.running) throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "MLX server is not running");
1315
+ try {
1316
+ const res = await fetchWithTimeout2(`${this.endpoint}/health`, { method: "GET" }, 2e3);
1317
+ if (res.status === 200) return;
1318
+ lastError = `health status ${res.status}`;
1319
+ } catch (err) {
1320
+ lastError = err instanceof Error ? err.message : String(err);
1321
+ }
1322
+ await new Promise((r) => setTimeout(r, 250));
1323
+ }
1324
+ throw new ClefError(
1325
+ ClefErrorCode.MODEL_LOAD_FAILED,
1326
+ `MLX server did not become ready within ${Math.round(this.loadTimeoutMs / 1e3)}s (last health check: ${lastError})`,
1327
+ "The first load can take a while; subsequent loads reuse the snapshot cache."
1328
+ );
1329
+ }
1330
+ async decide(input) {
1331
+ const run = this.queue.then(() => this.decideOnce(input));
1332
+ this.queue = run.catch(() => void 0);
1333
+ return run;
1334
+ }
1335
+ async decideOnce(input) {
1336
+ if (!this.running) {
1337
+ throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "Runtime is not loaded; call load() first");
1338
+ }
1339
+ let res;
1340
+ try {
1341
+ res = await fetchWithTimeout2(
1342
+ `${this.endpoint}/v1/systemone`,
1343
+ { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(input) },
1344
+ this.requestTimeoutMs
1345
+ );
1346
+ } catch (err) {
1347
+ const msg = err instanceof Error ? err.message : String(err);
1348
+ throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Inference request failed: ${msg}`);
1349
+ }
1350
+ const bodyText = await res.text();
1351
+ let body = void 0;
1352
+ try {
1353
+ body = bodyText ? JSON.parse(bodyText) : void 0;
1354
+ } catch {
1355
+ }
1356
+ if (!res.ok) {
1357
+ const apiMessage = body && typeof body === "object" && "error" in body ? String(body.error?.message ?? bodyText) : bodyText;
1358
+ throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Inference failed with HTTP ${res.status}: ${apiMessage.slice(0, 2e3)}`);
1359
+ }
1360
+ if (!body || typeof body !== "object") {
1361
+ throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Runtime returned a non-JSON response (HTTP ${res.status})`);
1362
+ }
1363
+ return body;
1364
+ }
1365
+ async health() {
1366
+ if (!this.running) return { status: "unavailable", detail: "not started" };
1367
+ try {
1368
+ const res = await fetchWithTimeout2(`${this.endpoint}/health`, { method: "GET" }, 1500);
1369
+ if (res.status === 200) return { status: "ready" };
1370
+ return { status: "loading", detail: `health status ${res.status}` };
1371
+ } catch (err) {
1372
+ return { status: "unavailable", detail: err instanceof Error ? err.message : String(err) };
1373
+ }
1374
+ }
1375
+ async unload() {
1376
+ const proc = this.proc;
1377
+ if (!proc) return;
1378
+ this.proc = void 0;
1379
+ if (proc.exitCode !== null || proc.signalCode) return;
1380
+ await new Promise((resolve) => {
1381
+ const killTimer = setTimeout(() => {
1382
+ try {
1383
+ proc.kill("SIGKILL");
1384
+ } catch {
1385
+ }
1386
+ }, 3e3);
1387
+ proc.once("exit", () => {
1388
+ clearTimeout(killTimer);
1389
+ resolve();
1390
+ });
1391
+ try {
1392
+ proc.kill("SIGTERM");
1393
+ } catch {
1394
+ clearTimeout(killTimer);
1395
+ resolve();
1396
+ }
1397
+ });
1398
+ }
1399
+ };
1400
+
1056
1401
  // src/mcp/schema.ts
1057
1402
  init_errors();
1058
1403
  import { z } from "zod";
@@ -1170,40 +1515,375 @@ function validateStateSize(state, limits) {
1170
1515
  }
1171
1516
  }
1172
1517
 
1173
- // src/mcp/server.ts
1174
- var TOOL_NAME = "clef_decide";
1175
- 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.";
1176
- function createClefServer(deps = {}) {
1177
- const config = deps.config ?? loadConfig();
1178
- const log = deps.log ?? createLogger(config.logLevel);
1179
- const server = new McpServer({ name: "clef-mcp", version: deps.version ?? "0.1.0" }, { instructions: SERVER_INSTRUCTIONS });
1180
- let loaded;
1181
- let loadPromise;
1182
- async function ensureRuntime(spec) {
1183
- if (loaded && loaded.model === spec.id) return loaded;
1184
- if (loaded && loaded.model !== spec.id) {
1185
- await loaded.runtime.unload();
1186
- loaded = void 0;
1187
- }
1188
- if (loadPromise) return loadPromise;
1189
- loadPromise = (async () => {
1190
- const manifest = await findInstalledManifest(config.clefHome, spec);
1191
- if (!manifest) {
1192
- throw new ClefError(
1193
- ClefErrorCode.MODEL_NOT_INSTALLED,
1194
- `Clef model "${spec.id}" is not installed.`,
1195
- "Run `clef-mcp install`."
1196
- );
1197
- }
1198
- const binary = resolveLlamaServerBinary(config);
1199
- 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 });
1200
- log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
1201
- const loadStarted = Date.now();
1202
- await runtime.load();
1203
- log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
1204
- return { runtime, model: spec.id };
1205
- })();
1206
- try {
1518
+ // src/mcp/prompts.ts
1519
+ init_errors();
1520
+ import { z as z2 } from "zod";
1521
+ var TOOL_GUIDE = (questions) => [
1522
+ "Call the `clef_decide` MCP tool once with all of the questions below (they are scored in a single forward pass).",
1523
+ "",
1524
+ "Rules for the call:",
1525
+ "- `state` must be a compact, factual JSON or string \u2014 relevant facts only, well under 1 MB.",
1526
+ "- Use the exact `questions` given below (ids, types, criteria). Do not invent extra options.",
1527
+ "- 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.",
1528
+ "- Treat safety-adjacent decisions (destructive actions, security) as requiring p >= 0.8 before acting; otherwise prefer asking the user.",
1529
+ "- Report the result as a short summary plus the top probability \u2014 do not paste raw JSON unless asked.",
1530
+ "",
1531
+ "Questions:",
1532
+ questions
1533
+ ].join("\n");
1534
+ var PROMPTS = [
1535
+ {
1536
+ name: "incident-triage",
1537
+ title: "Production incident triage",
1538
+ description: "Frame a production incident as clef_decide questions: immediate action, severity and blast radius.",
1539
+ args: {
1540
+ incident_description: "Short factual description of the incident (symptoms, scope, recent changes)."
1541
+ },
1542
+ build: (a) => TOOL_GUIDE(
1543
+ [
1544
+ '- "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".',
1545
+ '- "severity" (score, instructions "How severe is the incident?", criteria ["low", "elevated", "high", "critical"]).',
1546
+ '- "user_impact" (noul, instructions "Are users actively affected right now?").'
1547
+ ].join("\n")
1548
+ ) + "\n\nIncident description:\n" + (a.incident_description ?? "")
1549
+ },
1550
+ {
1551
+ name: "next-action",
1552
+ title: "Agent next action",
1553
+ description: "Ask Clef what the coding agent should do next on a task: inspect, modify, test or ask the user.",
1554
+ args: {
1555
+ task_summary: "What the agent is working on, current step, and what is already known or done."
1556
+ },
1557
+ build: (a) => TOOL_GUIDE(
1558
+ [
1559
+ '- "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".',
1560
+ '- "confidence" (score, instructions "How confident are you in this decision?", criteria ["very_low", "low", "medium", "high", "very_high"]).'
1561
+ ].join("\n")
1562
+ ) + "\n\nTask summary:\n" + (a.task_summary ?? "")
1563
+ },
1564
+ {
1565
+ name: "ticket-routing",
1566
+ title: "Support ticket routing",
1567
+ description: "Classify an incoming message into the team that should handle it.",
1568
+ args: {
1569
+ ticket_text: "The customer or user message, verbatim if possible."
1570
+ },
1571
+ build: (a) => TOOL_GUIDE(
1572
+ [
1573
+ '- "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".',
1574
+ '- "urgent" (noul, instructions "Does this need a response within the hour?").'
1575
+ ].join("\n")
1576
+ ) + "\n\nTicket text:\n" + (a.ticket_text ?? "")
1577
+ },
1578
+ {
1579
+ name: "security-review",
1580
+ title: "Security sanity check",
1581
+ description: "Ask Clef whether a code snippet, change or setup smells like a security problem.",
1582
+ args: {
1583
+ change_summary: "The code, config or setup to review \u2014 compact but complete enough to judge."
1584
+ },
1585
+ build: (a) => TOOL_GUIDE(
1586
+ [
1587
+ '- "is_vulnerable" (noul, instructions "Does this contain a realistic security vulnerability?").',
1588
+ '- "risk" (score, instructions "How bad is it if this ships as-is?", criteria ["informational", "low", "medium", "high", "critical"]).',
1589
+ '- "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".'
1590
+ ].join("\n") + "\nSafety rule: only act without human review when p(is_vulnerable=false) >= 0.8."
1591
+ ) + "\n\nChange to review:\n" + (a.change_summary ?? "")
1592
+ }
1593
+ ];
1594
+ function registerPrompts(server) {
1595
+ for (const p of PROMPTS) {
1596
+ server.registerPrompt(
1597
+ p.name,
1598
+ {
1599
+ title: p.title,
1600
+ description: p.description,
1601
+ argsSchema: Object.fromEntries(Object.entries(p.args).map(([k, v]) => [k, z2.string().min(1).describe(v)]))
1602
+ },
1603
+ ({ ...args }) => {
1604
+ for (const key of Object.keys(p.args)) {
1605
+ if (typeof args[key] !== "string" || !args[key]) {
1606
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `Prompt argument "${key}" is required`);
1607
+ }
1608
+ }
1609
+ return {
1610
+ messages: [{ role: "user", content: { type: "text", text: p.build(args) } }]
1611
+ };
1612
+ }
1613
+ );
1614
+ }
1615
+ }
1616
+
1617
+ // src/mcp/resources.ts
1618
+ import fsSync3 from "fs";
1619
+ import fsp4 from "fs/promises";
1620
+
1621
+ // src/cli/evals.ts
1622
+ init_errors();
1623
+ import fsp3 from "fs/promises";
1624
+ import fsSync2 from "fs";
1625
+ import { fileURLToPath } from "url";
1626
+ import path6 from "path";
1627
+ function defaultDatasetPath() {
1628
+ let dir = path6.dirname(fileURLToPath(import.meta.url));
1629
+ for (let i = 0; i < 6; i++) {
1630
+ const candidate = path6.join(dir, "tests", "evals", "dataset.jsonl");
1631
+ if (fsSync2.existsSync(candidate)) return candidate;
1632
+ dir = path6.dirname(dir);
1633
+ }
1634
+ return fileURLToPath(new URL("../tests/evals/dataset.jsonl", import.meta.url));
1635
+ }
1636
+ function parseLine(line, lineNo) {
1637
+ const raw = JSON.parse(line);
1638
+ if (typeof raw.id !== "string" || !raw.question?.id || !raw.expected?.kind) {
1639
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `Eval dataset line ${lineNo} is missing id/question/expected`);
1640
+ }
1641
+ return raw;
1642
+ }
1643
+ function evaluate(answer, exp) {
1644
+ const prob = (key) => answer[key] ?? 0;
1645
+ switch (exp.kind) {
1646
+ case "argmax": {
1647
+ const keys = exp.among ?? Object.keys(answer);
1648
+ let best = "";
1649
+ let bestP = -1;
1650
+ for (const k of keys) {
1651
+ if (prob(k) > bestP) {
1652
+ bestP = prob(k);
1653
+ best = k;
1654
+ }
1655
+ }
1656
+ return { pass: best === exp.expect, detail: `argmax=${best || "n/a"} (${bestP.toFixed(3)}), expected=${exp.expect}` };
1657
+ }
1658
+ case "prob_gt":
1659
+ return { pass: prob(exp.a) > prob(exp.b), detail: `p(${exp.a})=${prob(exp.a).toFixed(3)} vs p(${exp.b})=${prob(exp.b).toFixed(3)}` };
1660
+ case "min_prob": {
1661
+ const p = prob(exp.option);
1662
+ return { pass: p >= exp.min, detail: `p(${exp.option})=${p.toFixed(3)}, required >= ${exp.min}` };
1663
+ }
1664
+ case "top_mass": {
1665
+ const mass = exp.options.reduce((sum, k) => sum + prob(k), 0);
1666
+ return { pass: mass >= exp.min, detail: `mass(${exp.options.join("+")})=${mass.toFixed(3)}, required >= ${exp.min}` };
1667
+ }
1668
+ }
1669
+ }
1670
+ async function runEvals(opts) {
1671
+ const config = loadConfig();
1672
+ const log = createLogger(config.logLevel);
1673
+ const spec = getModelSpec(config.model);
1674
+ let mlxManifest;
1675
+ if (config.runtime === "mlx") {
1676
+ mlxManifest = await mlxIsInstalled(config.clefHome);
1677
+ if (!mlxManifest) {
1678
+ throw new ClefError(
1679
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1680
+ `Model "${spec.id}" is not installed for the MLX runtime.`,
1681
+ "Run `clef-mcp install --runtime mlx` first."
1682
+ );
1683
+ }
1684
+ }
1685
+ const manifest = config.runtime === "mlx" ? void 0 : await findInstalledManifest(config.clefHome, spec);
1686
+ if (!manifest && !mlxManifest) {
1687
+ throw new ClefError(
1688
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1689
+ `Model "${spec.id}" is not installed; evals need the local model.`,
1690
+ "Run `clef-mcp install` first."
1691
+ );
1692
+ }
1693
+ const datasetPath = opts.dataset ?? defaultDatasetPath();
1694
+ const content = await fsp3.readFile(datasetPath, "utf8");
1695
+ let cases = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l, i) => parseLine(l, i + 1));
1696
+ if (opts.limit && opts.limit > 0) cases = cases.slice(0, opts.limit);
1697
+ if (cases.length === 0) {
1698
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `No eval cases found in ${datasetPath}`);
1699
+ }
1700
+ const binary = resolveLlamaServerBinary(config);
1701
+ const runtime = config.runtime === "mlx" ? new MlxRuntime({ uvBin: resolveUv(process.env.CLEF_MLX_UV), snapshotPath: mlxManifest.snapshotPath, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
1702
+ const runtimeLabel = config.runtime === "mlx" ? "MLX" : manifest.quant;
1703
+ console.log(`Running ${cases.length} eval case(s) with ${spec.id} ${runtimeLabel}...
1704
+ `);
1705
+ await runtime.load();
1706
+ let passed = 0;
1707
+ const results = [];
1708
+ try {
1709
+ for (const [i, c] of cases.entries()) {
1710
+ process.stderr.write(`\r case ${i + 1}/${cases.length} \u2014 ${c.id}`);
1711
+ try {
1712
+ const request = toSystemOneRequest({ state: c.state, questions: { [c.question.id]: c.question } }, spec.id);
1713
+ const response = await runtime.decide(request);
1714
+ const output = fromSystemOneResponse(response, { [c.question.id]: c.question }, spec.id);
1715
+ const answer = output.decisions[c.question.id]?.answer ?? {};
1716
+ const verdict = evaluate(answer, c.expected);
1717
+ if (verdict.pass) passed++;
1718
+ results.push({ id: c.id, category: c.category, ok: verdict.pass, detail: verdict.detail });
1719
+ } catch (err) {
1720
+ results.push({
1721
+ id: c.id,
1722
+ category: c.category,
1723
+ ok: false,
1724
+ detail: `error: ${err instanceof Error ? err.message : String(err)}`
1725
+ });
1726
+ }
1727
+ }
1728
+ } finally {
1729
+ process.stderr.write("\n");
1730
+ await runtime.unload();
1731
+ }
1732
+ console.log("");
1733
+ for (const r of results) {
1734
+ console.log(`${r.ok ? "\u2714" : "\u2718"} [${r.category}] ${r.id} \u2014 ${r.detail}`);
1735
+ }
1736
+ const rate = passed / results.length;
1737
+ const minRate = opts.minRate ?? 0.8;
1738
+ console.log(`
1739
+ ${passed}/${results.length} passed (${(rate * 100).toFixed(1)}%), required >= ${(minRate * 100).toFixed(0)}%`);
1740
+ return rate >= minRate;
1741
+ }
1742
+
1743
+ // src/mcp/resources.ts
1744
+ var EVALS_SCHEMA_MD = `# clef-mcp eval dataset format
1745
+
1746
+ JSONL: one JSON object per line.
1747
+
1748
+ {
1749
+ "id": "unique-case-id",
1750
+ "category": "coding | security | classification | routing | yes_no",
1751
+ "state": <string | JSON value>,
1752
+ "question": {
1753
+ "id": "question-id",
1754
+ "type": "choice | score | noul",
1755
+ "instructions": "\u2026",
1756
+ "criteria": { "option": "description" } // choice (or ["a","b"] list)
1757
+ | ["low", "high"] // score, ordered
1758
+ },
1759
+ "expected": {
1760
+ "kind": "argmax | prob_gt | min_prob | top_mass",
1761
+ // argmax: { "expect": "option", "among": ["option", ...] (optional) }
1762
+ // prob_gt: { "a": "opt", "b": "opt" }
1763
+ // min_prob: { "option": "opt", "min": 0.7 }
1764
+ // top_mass: { "options": ["a","b"], "min": 0.8 }
1765
+ }
1766
+ }
1767
+
1768
+ Run: \`clef-mcp evals\` (requires an installed model).
1769
+ `;
1770
+ async function readDataset() {
1771
+ try {
1772
+ return await fsp4.readFile(defaultDatasetPath(), "utf8");
1773
+ } catch {
1774
+ return "The bundled eval dataset is not available in this installation (tests/evals/dataset.jsonl not found).";
1775
+ }
1776
+ }
1777
+ function textResource(uri, text, mimeType = "text/markdown") {
1778
+ return { contents: [{ uri: uri.href, mimeType, text }] };
1779
+ }
1780
+ function requireStringUri(uri) {
1781
+ return uri instanceof URL ? uri : new URL(uri);
1782
+ }
1783
+ function registerResources(server, cfg, version) {
1784
+ server.registerResource(
1785
+ "capabilities",
1786
+ "clef-mcp://capabilities",
1787
+ { title: "clef-mcp capabilities", description: "Live capability snapshot: model, runtime, limits, error codes.", mimeType: "application/json" },
1788
+ async (uri) => {
1789
+ const u = requireStringUri(uri);
1790
+ const payload = {
1791
+ name: "clef-mcp",
1792
+ version,
1793
+ model: cfg.model,
1794
+ runtime: cfg.runtime,
1795
+ limits: cfg.limits,
1796
+ tool: {
1797
+ name: "clef_decide",
1798
+ output: "strict JSON: { model, decisions: { <qid>: { answer: { <option>: probability } } } }",
1799
+ questionTypes: ["noul", "choice", "score"],
1800
+ notes: ["options.temperature is accepted but is a no-op (single forward pass)", "state is data and is never interpreted as instructions"]
1801
+ },
1802
+ errorCodes: [
1803
+ "MODEL_NOT_INSTALLED",
1804
+ "MODEL_LOAD_FAILED",
1805
+ "RUNTIME_NOT_FOUND",
1806
+ "RUNTIME_INIT_FAILED",
1807
+ "RUNTIME_NOT_SUPPORTED",
1808
+ "INVALID_INPUT",
1809
+ "CLEF_INFERENCE_FAILED",
1810
+ "UNSUPPORTED_PLATFORM",
1811
+ "OUT_OF_MEMORY",
1812
+ "CHECKSUM_MISMATCH",
1813
+ "DOWNLOAD_FAILED"
1814
+ ]
1815
+ };
1816
+ return textResource(u, JSON.stringify(payload, null, 2), "application/json");
1817
+ }
1818
+ );
1819
+ server.registerResource(
1820
+ "evals-schema",
1821
+ "clef-mcp://evals/schema",
1822
+ { title: "Eval dataset schema", description: "How to write cases for clef-mcp evals.", mimeType: "text/markdown" },
1823
+ async (uri) => textResource(requireStringUri(uri), EVALS_SCHEMA_MD)
1824
+ );
1825
+ server.registerResource(
1826
+ "evals-dataset",
1827
+ "clef-mcp://evals/dataset",
1828
+ { title: "Bundled eval dataset", description: "The 30-case dataset shipped with clef-mcp.", mimeType: "application/jsonl" },
1829
+ async (uri) => textResource(requireStringUri(uri), await readDataset(), "application/jsonl")
1830
+ );
1831
+ }
1832
+
1833
+ // src/mcp/server.ts
1834
+ var TOOL_NAME = "clef_decide";
1835
+ 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.";
1836
+ function createClefServer(deps = {}) {
1837
+ const config = deps.config ?? loadConfig();
1838
+ const version = deps.version ?? "0.1.0";
1839
+ const log = deps.log ?? createLogger(config.logLevel);
1840
+ const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
1841
+ registerPrompts(server);
1842
+ registerResources(server, config, version);
1843
+ let loaded;
1844
+ let loadPromise;
1845
+ async function ensureRuntime(spec) {
1846
+ if (loaded && loaded.model === spec.id) return loaded;
1847
+ if (loaded && loaded.model !== spec.id) {
1848
+ await loaded.runtime.unload();
1849
+ loaded = void 0;
1850
+ }
1851
+ if (loadPromise) return loadPromise;
1852
+ loadPromise = (async () => {
1853
+ if (config.runtime === "mlx") {
1854
+ const manifest2 = await mlxIsInstalled(config.clefHome);
1855
+ if (!manifest2) {
1856
+ throw new ClefError(
1857
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1858
+ `Clef model "${spec.id}" is not installed for the MLX runtime.`,
1859
+ "Run `clef-mcp install --runtime mlx`."
1860
+ );
1861
+ }
1862
+ const uvBin = resolveUv(process.env.CLEF_MLX_UV);
1863
+ const runtime2 = deps.runtimeFactory ? deps.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log });
1864
+ log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
1865
+ const loadStarted2 = Date.now();
1866
+ await runtime2.load();
1867
+ log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
1868
+ return { runtime: runtime2, model: spec.id };
1869
+ }
1870
+ const manifest = await findInstalledManifest(config.clefHome, spec);
1871
+ if (!manifest) {
1872
+ throw new ClefError(
1873
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1874
+ `Clef model "${spec.id}" is not installed.`,
1875
+ "Run `clef-mcp install`."
1876
+ );
1877
+ }
1878
+ const binary = resolveLlamaServerBinary(config);
1879
+ 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 });
1880
+ log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
1881
+ const loadStarted = Date.now();
1882
+ await runtime.load();
1883
+ log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
1884
+ return { runtime, model: spec.id };
1885
+ })();
1886
+ try {
1207
1887
  loaded = await loadPromise;
1208
1888
  return loaded;
1209
1889
  } finally {
@@ -1275,19 +1955,19 @@ import { Command } from "commander";
1275
1955
 
1276
1956
  // src/cli/doctor.ts
1277
1957
  init_errors();
1278
- import { execFile as execFile2 } from "child_process";
1958
+ import { execFile as execFile3 } from "child_process";
1279
1959
  import os3 from "os";
1280
1960
  import { promisify } from "util";
1281
- import fsp2 from "fs/promises";
1282
- import path5 from "path";
1961
+ import fsp5 from "fs/promises";
1962
+ import path7 from "path";
1283
1963
 
1284
1964
  // src/models/platform.ts
1285
1965
  init_errors();
1286
- import { execFile } from "child_process";
1966
+ import { execFile as execFile2 } from "child_process";
1287
1967
  import os2 from "os";
1288
1968
  function exec(cmd, args, timeoutMs) {
1289
1969
  return new Promise((resolve, reject) => {
1290
- execFile(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
1970
+ execFile2(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
1291
1971
  if (err) reject(err);
1292
1972
  else resolve(stdout);
1293
1973
  });
@@ -1393,9 +2073,27 @@ async function probeInference(cfg, manifest, log, loadTimeoutMs = 12e4) {
1393
2073
  }
1394
2074
  return { latencyMs: Date.now() - started, binarySource: binary.source, binaryPath: binary.path };
1395
2075
  }
2076
+ async function probeMlxInference(cfg, manifest, log, loadTimeoutMs = 18e4) {
2077
+ const uvBin = resolveUv(process.env.CLEF_MLX_UV);
2078
+ const runtime = new MlxRuntime({ uvBin, snapshotPath: manifest.snapshotPath, loadTimeoutMs, log });
2079
+ const started = Date.now();
2080
+ await runtime.load();
2081
+ try {
2082
+ await runtime.decide({
2083
+ model: manifest.model,
2084
+ state: "This is an internal clef-mcp installation probe.",
2085
+ questions: {
2086
+ probe: { type: "noul", instructions: "Is this a clef-mcp installation probe?" }
2087
+ }
2088
+ });
2089
+ } finally {
2090
+ await runtime.unload();
2091
+ }
2092
+ return { latencyMs: Date.now() - started, binarySource: "uv", binaryPath: uvBin };
2093
+ }
1396
2094
 
1397
2095
  // src/cli/doctor.ts
1398
- var execFileP = promisify(execFile2);
2096
+ var execFileP = promisify(execFile3);
1399
2097
  function mark(ok) {
1400
2098
  return ok ? "\u2714" : "\u2718";
1401
2099
  }
@@ -1415,14 +2113,14 @@ async function checkBinaryVersion(binaryPath) {
1415
2113
  async function detectMcpConfigs() {
1416
2114
  const home = os3.homedir();
1417
2115
  const hints = [
1418
- { client: "Codex", path: path5.join(home, ".codex", "config.toml"), configured: false },
1419
- { client: "Claude Code (user)", path: path5.join(home, ".claude.json"), configured: false },
1420
- { client: "Claude Code (project)", path: path5.resolve(".mcp.json"), configured: false },
1421
- { client: "Cursor", path: path5.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
2116
+ { client: "Codex", path: path7.join(home, ".codex", "config.toml"), configured: false },
2117
+ { client: "Claude Code (user)", path: path7.join(home, ".claude.json"), configured: false },
2118
+ { client: "Claude Code (project)", path: path7.resolve(".mcp.json"), configured: false },
2119
+ { client: "Cursor", path: path7.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
1422
2120
  ];
1423
2121
  for (const hint of hints) {
1424
2122
  try {
1425
- const content = await fsp2.readFile(hint.path, "utf8");
2123
+ const content = await fsp5.readFile(hint.path, "utf8");
1426
2124
  hint.configured = content.includes("clef-mcp");
1427
2125
  } catch {
1428
2126
  hint.configured = false;
@@ -1461,33 +2159,75 @@ async function runDoctor(opts) {
1461
2159
  detail: info.gpu?.name ? `${info.gpu.name}${info.gpu.vramBytes ? ` (${formatBytes(info.gpu.vramBytes)})` : ""}` : "not detected (llama.cpp falls back to CPU; on Apple Silicon Metal is built in)"
1462
2160
  });
1463
2161
  let binaryPath;
1464
- try {
1465
- const binary = resolveLlamaServerBinary(config);
1466
- binaryPath = binary.path;
1467
- const version = await checkBinaryVersion(binary.path);
1468
- checks.push({
1469
- name: "Runtime binary",
1470
- ok: version !== void 0,
1471
- detail: `${binary.source}: ${binary.path}${version ? ` (${version})` : ""}`,
1472
- hint: version === void 0 ? "The binary exists but did not answer --version; it may be broken or incompatible." : void 0
1473
- });
1474
- } catch (err) {
1475
- checks.push({
1476
- name: "Runtime binary",
1477
- ok: false,
1478
- detail: err instanceof Error ? err.message : String(err),
1479
- hint: err instanceof ClefError ? err.hint : void 0
1480
- });
2162
+ if (config.runtime === "mlx") {
2163
+ try {
2164
+ binaryPath = resolveUv(process.env.CLEF_MLX_UV);
2165
+ checks.push({
2166
+ name: "Runtime (uv, for the MLX runtime)",
2167
+ ok: true,
2168
+ detail: binaryPath
2169
+ });
2170
+ } catch (err) {
2171
+ checks.push({
2172
+ name: "Runtime (uv, for the MLX runtime)",
2173
+ ok: false,
2174
+ detail: err instanceof Error ? err.message : String(err),
2175
+ hint: "Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh"
2176
+ });
2177
+ }
2178
+ } else {
2179
+ try {
2180
+ const binary = resolveLlamaServerBinary(config);
2181
+ binaryPath = binary.path;
2182
+ const version = await checkBinaryVersion(binary.path);
2183
+ checks.push({
2184
+ name: "Runtime binary",
2185
+ ok: version !== void 0,
2186
+ detail: `${binary.source}: ${binary.path}${version ? ` (${version})` : ""}`,
2187
+ hint: version === void 0 ? "The binary exists but did not answer --version; it may be broken or incompatible." : void 0
2188
+ });
2189
+ } catch (err) {
2190
+ binaryPath = void 0;
2191
+ checks.push({
2192
+ name: "Runtime binary",
2193
+ ok: false,
2194
+ detail: err instanceof Error ? err.message : String(err),
2195
+ hint: err instanceof ClefError ? err.hint : void 0
2196
+ });
2197
+ }
1481
2198
  }
1482
2199
  let spec;
1483
2200
  let manifest;
2201
+ let mlxManifest;
1484
2202
  try {
1485
2203
  spec = getModelSpec(config.model);
1486
- manifest = await findInstalledManifest(config.clefHome, spec);
2204
+ if (config.runtime === "mlx") mlxManifest = await mlxIsInstalled(config.clefHome);
2205
+ else manifest = await findInstalledManifest(config.clefHome, spec);
1487
2206
  } catch {
1488
2207
  checks.push({ name: "Model", ok: false, detail: `unknown model id "${config.model}"` });
1489
2208
  }
1490
- if (spec) {
2209
+ if (config.runtime === "mlx") {
2210
+ if (!mlxManifest) {
2211
+ checks.push({
2212
+ name: "Model",
2213
+ ok: false,
2214
+ detail: `${spec?.id ?? config.model} is not installed for the MLX runtime`,
2215
+ hint: "Run `clef-mcp install --runtime mlx`."
2216
+ });
2217
+ } else {
2218
+ checks.push({
2219
+ name: "Model",
2220
+ ok: true,
2221
+ detail: `MLX snapshot at ${mlxManifest.snapshotPath}`
2222
+ });
2223
+ checks.push({
2224
+ name: "Model license",
2225
+ ok: true,
2226
+ informational: true,
2227
+ detail: `${mlxManifest.licenseId} \u2014 ${mlxManifest.licenseUrl}`
2228
+ });
2229
+ }
2230
+ } else if (spec) {
1491
2231
  if (!manifest) {
1492
2232
  checks.push({
1493
2233
  name: "Model",
@@ -1520,7 +2260,24 @@ async function runDoctor(opts) {
1520
2260
  });
1521
2261
  }
1522
2262
  }
1523
- if (binaryPath && manifest && (await verifyModelFiles(manifest)).ok) {
2263
+ if (config.runtime === "mlx" && binaryPath && mlxManifest) {
2264
+ try {
2265
+ const probe = await probeMlxInference(config, mlxManifest, void 0, 24e4);
2266
+ checks.push({
2267
+ name: "Inference",
2268
+ ok: true,
2269
+ detail: `probe decision returned in ${(probe.latencyMs / 1e3).toFixed(1)}s (incl. model load)`
2270
+ });
2271
+ } catch (err) {
2272
+ const clefErr = err instanceof ClefError ? err : void 0;
2273
+ checks.push({
2274
+ name: "Inference",
2275
+ ok: false,
2276
+ detail: clefErr?.message ?? (err instanceof Error ? err.message : String(err)),
2277
+ hint: clefErr?.hint
2278
+ });
2279
+ }
2280
+ } else if (config.runtime === "llama-cpp" && binaryPath && manifest && (await verifyModelFiles(manifest)).ok) {
1524
2281
  try {
1525
2282
  const probe = await probeInference(config, manifest, void 0, 18e4);
1526
2283
  checks.push({
@@ -1570,127 +2327,20 @@ ${failed.length} critical check(s) failed.`
1570
2327
  return failed.length > 0;
1571
2328
  }
1572
2329
 
1573
- // src/cli/evals.ts
2330
+ // src/cli/install.ts
1574
2331
  init_errors();
1575
- import fsp3 from "fs/promises";
1576
- import fsSync from "fs";
1577
- import { fileURLToPath } from "url";
1578
- import path6 from "path";
1579
- function defaultDatasetPath() {
1580
- let dir = path6.dirname(fileURLToPath(import.meta.url));
1581
- for (let i = 0; i < 6; i++) {
1582
- const candidate = path6.join(dir, "tests", "evals", "dataset.jsonl");
1583
- if (fsSync.existsSync(candidate)) return candidate;
1584
- dir = path6.dirname(dir);
1585
- }
1586
- return fileURLToPath(new URL("../tests/evals/dataset.jsonl", import.meta.url));
1587
- }
1588
- function parseLine(line, lineNo) {
1589
- const raw = JSON.parse(line);
1590
- if (typeof raw.id !== "string" || !raw.question?.id || !raw.expected?.kind) {
1591
- throw new ClefError(ClefErrorCode.INVALID_INPUT, `Eval dataset line ${lineNo} is missing id/question/expected`);
1592
- }
1593
- return raw;
1594
- }
1595
- function evaluate(answer, exp) {
1596
- const prob = (key) => answer[key] ?? 0;
1597
- switch (exp.kind) {
1598
- case "argmax": {
1599
- const keys = exp.among ?? Object.keys(answer);
1600
- let best = "";
1601
- let bestP = -1;
1602
- for (const k of keys) {
1603
- if (prob(k) > bestP) {
1604
- bestP = prob(k);
1605
- best = k;
1606
- }
1607
- }
1608
- return { pass: best === exp.expect, detail: `argmax=${best || "n/a"} (${bestP.toFixed(3)}), expected=${exp.expect}` };
1609
- }
1610
- case "prob_gt":
1611
- return { pass: prob(exp.a) > prob(exp.b), detail: `p(${exp.a})=${prob(exp.a).toFixed(3)} vs p(${exp.b})=${prob(exp.b).toFixed(3)}` };
1612
- case "min_prob": {
1613
- const p = prob(exp.option);
1614
- return { pass: p >= exp.min, detail: `p(${exp.option})=${p.toFixed(3)}, required >= ${exp.min}` };
1615
- }
1616
- case "top_mass": {
1617
- const mass = exp.options.reduce((sum, k) => sum + prob(k), 0);
1618
- return { pass: mass >= exp.min, detail: `mass(${exp.options.join("+")})=${mass.toFixed(3)}, required >= ${exp.min}` };
1619
- }
1620
- }
1621
- }
1622
- async function runEvals(opts) {
1623
- const config = loadConfig();
1624
- const log = createLogger(config.logLevel);
1625
- const spec = getModelSpec(config.model);
1626
- const manifest = await findInstalledManifest(config.clefHome, spec);
1627
- if (!manifest) {
1628
- throw new ClefError(
1629
- ClefErrorCode.MODEL_NOT_INSTALLED,
1630
- `Model "${spec.id}" is not installed; evals need the local model.`,
1631
- "Run `clef-mcp install` first."
1632
- );
1633
- }
1634
- const datasetPath = opts.dataset ?? defaultDatasetPath();
1635
- const content = await fsp3.readFile(datasetPath, "utf8");
1636
- let cases = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l, i) => parseLine(l, i + 1));
1637
- if (opts.limit && opts.limit > 0) cases = cases.slice(0, opts.limit);
1638
- if (cases.length === 0) {
1639
- throw new ClefError(ClefErrorCode.INVALID_INPUT, `No eval cases found in ${datasetPath}`);
1640
- }
1641
- const binary = resolveLlamaServerBinary(config);
1642
- const runtime = new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
1643
- console.log(`Running ${cases.length} eval case(s) with ${spec.id} ${manifest.quant}...
1644
- `);
1645
- await runtime.load();
1646
- let passed = 0;
1647
- const results = [];
1648
- try {
1649
- for (const [i, c] of cases.entries()) {
1650
- process.stderr.write(`\r case ${i + 1}/${cases.length} \u2014 ${c.id}`);
1651
- try {
1652
- const request = toSystemOneRequest({ state: c.state, questions: { [c.question.id]: c.question } }, spec.id);
1653
- const response = await runtime.decide(request);
1654
- const output = fromSystemOneResponse(response, { [c.question.id]: c.question }, spec.id);
1655
- const answer = output.decisions[c.question.id]?.answer ?? {};
1656
- const verdict = evaluate(answer, c.expected);
1657
- if (verdict.pass) passed++;
1658
- results.push({ id: c.id, category: c.category, ok: verdict.pass, detail: verdict.detail });
1659
- } catch (err) {
1660
- results.push({
1661
- id: c.id,
1662
- category: c.category,
1663
- ok: false,
1664
- detail: `error: ${err instanceof Error ? err.message : String(err)}`
1665
- });
1666
- }
1667
- }
1668
- } finally {
1669
- process.stderr.write("\n");
1670
- await runtime.unload();
1671
- }
1672
- console.log("");
1673
- for (const r of results) {
1674
- console.log(`${r.ok ? "\u2714" : "\u2718"} [${r.category}] ${r.id} \u2014 ${r.detail}`);
1675
- }
1676
- const rate = passed / results.length;
1677
- const minRate = opts.minRate ?? 0.8;
1678
- console.log(`
1679
- ${passed}/${results.length} passed (${(rate * 100).toFixed(1)}%), required >= ${(minRate * 100).toFixed(0)}%`);
1680
- return rate >= minRate;
1681
- }
1682
2332
 
1683
2333
  // src/models/llama-install.ts
1684
2334
  init_errors();
1685
- import { execFile as execFile3 } from "child_process";
1686
- import fsp5 from "fs/promises";
1687
- import path8 from "path";
2335
+ import { execFile as execFile4 } from "child_process";
2336
+ import fsp7 from "fs/promises";
2337
+ import path9 from "path";
1688
2338
 
1689
2339
  // src/models/hf.ts
1690
2340
  init_errors();
1691
2341
  import { createHash as createHash2 } from "crypto";
1692
- import fsp4 from "fs/promises";
1693
- import path7 from "path";
2342
+ import fsp6 from "fs/promises";
2343
+ import path8 from "path";
1694
2344
  var HF_BASE_URL = "https://huggingface.co";
1695
2345
  function resolveUrl(repo, revision, file) {
1696
2346
  return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
@@ -1748,7 +2398,7 @@ async function downloadOnce(url, dest, opts) {
1748
2398
  }
1749
2399
  const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
1750
2400
  const hash = createHash2("sha256");
1751
- const handle = await fsp4.open(dest, "w");
2401
+ const handle = await fsp6.open(dest, "w");
1752
2402
  let bytes = 0;
1753
2403
  try {
1754
2404
  for await (const chunk of res.body) {
@@ -1763,7 +2413,7 @@ async function downloadOnce(url, dest, opts) {
1763
2413
  }
1764
2414
  const sha256 = hash.digest("hex");
1765
2415
  if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
1766
- await fsp4.rm(dest, { force: true });
2416
+ await fsp6.rm(dest, { force: true });
1767
2417
  throw new ClefError(
1768
2418
  ClefErrorCode.CHECKSUM_MISMATCH,
1769
2419
  `Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
@@ -1771,7 +2421,7 @@ async function downloadOnce(url, dest, opts) {
1771
2421
  );
1772
2422
  }
1773
2423
  if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
1774
- await fsp4.rm(dest, { force: true });
2424
+ await fsp6.rm(dest, { force: true });
1775
2425
  throw new ClefError(
1776
2426
  ClefErrorCode.DOWNLOAD_FAILED,
1777
2427
  `Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
@@ -1782,24 +2432,24 @@ async function downloadOnce(url, dest, opts) {
1782
2432
  async function installModel(opts) {
1783
2433
  const { modelSpec, quantSpec } = opts;
1784
2434
  const { modelsDir } = pathsFor(opts.clefHome);
1785
- const manifestPath = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
1786
- const existing = await readModelManifest(manifestPath);
2435
+ const manifestPath2 = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
2436
+ const existing = await readModelManifest(manifestPath2);
1787
2437
  if (existing && (await verifyModelFiles(existing)).ok) {
1788
2438
  opts.log?.debug("model already installed", { model: modelSpec.id, quant: quantSpec.id });
1789
2439
  return { manifest: existing, alreadyInstalled: true };
1790
2440
  }
1791
2441
  const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
1792
- const finalPath = path7.join(path7.dirname(manifestPath), quantSpec.file);
1793
- await fsp4.mkdir(path7.dirname(finalPath), { recursive: true });
1794
- const tmpPath = path7.join(path7.dirname(finalPath), `.download-${quantSpec.file}`);
1795
- await fsp4.rm(tmpPath, { force: true });
2442
+ const finalPath = path8.join(path8.dirname(manifestPath2), quantSpec.file);
2443
+ await fsp6.mkdir(path8.dirname(finalPath), { recursive: true });
2444
+ const tmpPath = path8.join(path8.dirname(finalPath), `.download-${quantSpec.file}`);
2445
+ await fsp6.rm(tmpPath, { force: true });
1796
2446
  opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
1797
2447
  const result = await downloadFile(info.downloadUrl, tmpPath, {
1798
2448
  expectedSha256: info.sha256,
1799
2449
  expectedSize: info.size ?? quantSpec.bytes,
1800
2450
  onProgress: opts.onProgress
1801
2451
  });
1802
- await fsp4.rename(tmpPath, finalPath);
2452
+ await fsp6.rename(tmpPath, finalPath);
1803
2453
  const manifest = {
1804
2454
  schemaVersion: 1,
1805
2455
  model: modelSpec.id,
@@ -1815,7 +2465,7 @@ async function installModel(opts) {
1815
2465
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1816
2466
  clefMcpVersion: opts.clefMcpVersion
1817
2467
  };
1818
- await writeModelManifest(manifestPath, manifest);
2468
+ await writeModelManifest(manifestPath2, manifest);
1819
2469
  opts.log?.info("model installed", { model: modelSpec.id, quant: quantSpec.id, sha256: result.sha256 });
1820
2470
  return { manifest, alreadyInstalled: false };
1821
2471
  }
@@ -1874,7 +2524,7 @@ function pickAsset(release, platform, arch) {
1874
2524
  }
1875
2525
  function execTar(args, cwd) {
1876
2526
  return new Promise((resolve, reject) => {
1877
- execFile3("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
2527
+ execFile4("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
1878
2528
  if (err) reject(new Error(`tar ${args.join(" ")} failed: ${stderr || err.message}`));
1879
2529
  else resolve();
1880
2530
  });
@@ -1887,12 +2537,12 @@ async function findServerBinary(dir, platform) {
1887
2537
  const current = queue.shift();
1888
2538
  let entries;
1889
2539
  try {
1890
- entries = await fsp5.readdir(current, { withFileTypes: true });
2540
+ entries = await fsp7.readdir(current, { withFileTypes: true });
1891
2541
  } catch {
1892
2542
  continue;
1893
2543
  }
1894
2544
  for (const entry of entries) {
1895
- const full = path8.join(current, entry.name);
2545
+ const full = path9.join(current, entry.name);
1896
2546
  if (entry.isDirectory()) queue.push(full);
1897
2547
  else if (entry.name === target) return full;
1898
2548
  }
@@ -1903,14 +2553,14 @@ async function findServerBinary(dir, platform) {
1903
2553
  );
1904
2554
  }
1905
2555
  async function extractRelease(archivePath, destDir, platform) {
1906
- await fsp5.mkdir(destDir, { recursive: true });
2556
+ await fsp7.mkdir(destDir, { recursive: true });
1907
2557
  const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
1908
2558
  try {
1909
2559
  await execTar(args, destDir);
1910
2560
  } catch (err) {
1911
2561
  throw new ClefError(
1912
2562
  ClefErrorCode.DOWNLOAD_FAILED,
1913
- `Could not extract ${path8.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
2563
+ `Could not extract ${path9.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
1914
2564
  "Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
1915
2565
  );
1916
2566
  }
@@ -1920,7 +2570,7 @@ async function ensureLlamaRuntime(cfg, log) {
1920
2570
  const resolved = resolveLlamaServerBinary(cfg);
1921
2571
  if (resolved.source === "managed") {
1922
2572
  const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
1923
- const manifest = readRuntimeManifest(path8.join(platformDir, "runtime-manifest.json"));
2573
+ const manifest = readRuntimeManifest(path9.join(platformDir, "runtime-manifest.json"));
1924
2574
  return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
1925
2575
  }
1926
2576
  return { ...resolved, releaseTag: resolved.source, freshInstall: false };
@@ -1952,16 +2602,16 @@ async function downloadManagedLlama(cfg, log) {
1952
2602
  }
1953
2603
  const asset = pickAsset(release, platform, arch);
1954
2604
  log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
1955
- const tagDir = path8.join(platformDir, release.tag_name);
1956
- await fsp5.mkdir(tagDir, { recursive: true });
1957
- const tmpArchive = path8.join(platformDir, `.download-${asset.name}`);
1958
- await fsp5.rm(tmpArchive, { force: true });
2605
+ const tagDir = path9.join(platformDir, release.tag_name);
2606
+ await fsp7.mkdir(tagDir, { recursive: true });
2607
+ const tmpArchive = path9.join(platformDir, `.download-${asset.name}`);
2608
+ await fsp7.rm(tmpArchive, { force: true });
1959
2609
  const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
1960
2610
  await extractRelease(tmpArchive, tagDir, platform);
1961
- await fsp5.rm(tmpArchive, { force: true });
2611
+ await fsp7.rm(tmpArchive, { force: true });
1962
2612
  const binaryPath = await findServerBinary(tagDir, platform);
1963
- if (platform !== "win32") await fsp5.chmod(binaryPath, 493);
1964
- await writeRuntimeManifest(path8.join(platformDir, "runtime-manifest.json"), {
2613
+ if (platform !== "win32") await fsp7.chmod(binaryPath, 493);
2614
+ await writeRuntimeManifest(path9.join(platformDir, "runtime-manifest.json"), {
1965
2615
  schemaVersion: 1,
1966
2616
  kind: "llama-cpp",
1967
2617
  releaseTag: release.tag_name,
@@ -1977,11 +2627,11 @@ async function removeManagedRuntime(cfg) {
1977
2627
  const { runtimeDir } = pathsFor(cfg.clefHome);
1978
2628
  const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
1979
2629
  try {
1980
- await fsp5.stat(dir);
2630
+ await fsp7.stat(dir);
1981
2631
  } catch {
1982
2632
  return false;
1983
2633
  }
1984
- await fsp5.rm(dir, { recursive: true, force: true });
2634
+ await fsp7.rm(dir, { recursive: true, force: true });
1985
2635
  return true;
1986
2636
  }
1987
2637
 
@@ -1989,7 +2639,7 @@ async function removeManagedRuntime(cfg) {
1989
2639
  init_prompt();
1990
2640
 
1991
2641
  // src/version.ts
1992
- var VERSION = "0.1.5";
2642
+ var VERSION = "0.2.0";
1993
2643
 
1994
2644
  // src/cli/install.ts
1995
2645
  var OS_NAMES = {
@@ -2011,6 +2661,10 @@ function printHeader() {
2011
2661
  async function runInstall(opts) {
2012
2662
  const config = loadConfig();
2013
2663
  const log = createLogger("info");
2664
+ const runtime = opts.runtime ?? config.runtime;
2665
+ if (runtime !== "llama-cpp" && runtime !== "mlx") {
2666
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `Unknown runtime "${runtime}".`, "Available: llama-cpp, mlx");
2667
+ }
2014
2668
  printHeader();
2015
2669
  process.stdout.write("Detecting hardware...\n");
2016
2670
  const info = await detectPlatform();
@@ -2023,41 +2677,57 @@ async function runInstall(opts) {
2023
2677
  console.log(` Architecture: ${info.arch}`);
2024
2678
  console.log(` Memory: ${formatBytes(info.totalMemBytes)}`);
2025
2679
  if (gpuLine) console.log(gpuLine);
2680
+ if (runtime === "mlx" && !(info.platform === "darwin" && info.arch === "arm64")) {
2681
+ throw new ClefError(
2682
+ ClefErrorCode.UNSUPPORTED_PLATFORM,
2683
+ "The MLX runtime only works on Apple Silicon (macOS/arm64).",
2684
+ "Use the default llama.cpp runtime (omit --runtime)."
2685
+ );
2686
+ }
2026
2687
  console.log("\nRecommended:");
2027
- console.log(` Model: ${spec.id} (${spec.repo})`);
2028
- console.log(` Quantization: ${quant.id}`);
2029
- console.log(` Download size: ~${formatBytes(quant.bytes)}`);
2688
+ console.log(` Model: ${spec.id} (${runtime === "mlx" ? "mlx-community/clef-flash-4bit" : spec.repo})`);
2689
+ console.log(` Runtime: ${runtime}`);
2690
+ if (runtime === "llama-cpp") console.log(` Quantization: ${quant.id}`);
2691
+ console.log(` Download size: ~${runtime === "mlx" ? "6.2 GB" : formatBytes(quant.bytes)}`);
2030
2692
  console.log(`
2031
2693
  Model license: ${spec.licenseId} \u2014 ${spec.licenseUrl}`);
2032
- console.log("The model is downloaded from the official GGUF conversion and is not repackaged by clef-mcp.\n");
2694
+ console.log("The model is downloaded from the official conversion and is not repackaged by clef-mcp.\n");
2033
2695
  const ok = await confirm("Continue?", opts.yes === true);
2034
2696
  if (!ok) {
2035
2697
  console.log("Aborted. Nothing was downloaded.");
2036
2698
  return;
2037
2699
  }
2038
- let runtime;
2039
- if (!opts.skipRuntime) {
2040
- process.stdout.write("\nEnsuring llama.cpp runtime...\n");
2041
- runtime = await ensureLlamaRuntime(config, log);
2042
- console.log(` Runtime: llama.cpp (${runtime.source}, ${runtime.releaseTag})`);
2043
- console.log(` Binary: ${runtime.path}`);
2700
+ let llamaManifest;
2701
+ let mlxManifest;
2702
+ if (runtime === "mlx") {
2703
+ console.log("\nDownloading MLX model snapshot (uv-managed; weights ~6 GB)...\n");
2704
+ mlxManifest = await installModelMlx({ clefHome: config.clefHome, clefMcpVersion: VERSION, log });
2705
+ console.log(` Installed: ${mlxManifest.snapshotPath}`);
2706
+ } else {
2707
+ if (!opts.skipRuntime) {
2708
+ process.stdout.write("\nEnsuring llama.cpp runtime...\n");
2709
+ const rt = await ensureLlamaRuntime(config, log);
2710
+ console.log(` Runtime: llama.cpp (${rt.source}, ${rt.releaseTag})`);
2711
+ console.log(` Binary: ${rt.path}`);
2712
+ }
2713
+ console.log("");
2714
+ const installed = await installModel({
2715
+ clefHome: config.clefHome,
2716
+ modelSpec: spec,
2717
+ quantSpec: quant,
2718
+ clefMcpVersion: VERSION,
2719
+ onProgress: (p) => printProgress(p.bytes, p.total),
2720
+ log
2721
+ });
2722
+ process.stderr.write("\n");
2723
+ console.log(installed.alreadyInstalled ? ` Model already installed: ${installed.manifest.modelPath}` : ` Installed: ${installed.manifest.modelPath}`);
2724
+ console.log(` sha256: ${installed.manifest.sha256 ?? "n/a (not advertised upstream)"}`);
2725
+ llamaManifest = installed.manifest;
2044
2726
  }
2045
- console.log("");
2046
- const { manifest, alreadyInstalled } = await installModel({
2047
- clefHome: config.clefHome,
2048
- modelSpec: spec,
2049
- quantSpec: quant,
2050
- clefMcpVersion: VERSION,
2051
- onProgress: (p) => printProgress(p.bytes, p.total),
2052
- log
2053
- });
2054
- process.stderr.write("\n");
2055
- console.log(alreadyInstalled ? ` Model already installed: ${manifest.modelPath}` : ` Installed: ${manifest.modelPath}`);
2056
- console.log(` sha256: ${manifest.sha256 ?? "n/a (not advertised upstream)"}`);
2057
2727
  if (!opts.skipProbe) {
2058
2728
  process.stdout.write("\nVerifying inference (loads the model; first run can take a few minutes)...\n");
2059
- const probe = await probeInference(config, manifest, log);
2060
- console.log(` Inference OK (${(probe.latencyMs / 1e3).toFixed(1)}s incl. model load, via ${probe.binarySource} binary)`);
2729
+ const probe = runtime === "mlx" ? await probeMlxInference(config, mlxManifest, log) : await probeInference(config, llamaManifest, log);
2730
+ console.log(` Inference OK (${(probe.latencyMs / 1e3).toFixed(1)}s incl. model load)`);
2061
2731
  }
2062
2732
  if (opts.setup) {
2063
2733
  const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
@@ -2070,7 +2740,10 @@ Model license: ${spec.licenseId} \u2014 ${spec.licenseUrl}`);
2070
2740
  console.log(" Claude Code : claude mcp add clef-mcp -- clef-mcp");
2071
2741
  console.log(" Codex : add [mcp_servers.clef-mcp] to ~/.codex/config.toml (see README)");
2072
2742
  console.log(" Cursor : add clef-mcp to .cursor/mcp.json (see README)");
2073
- console.log(" ZCode : add clef-mcp to ~/.zcode/cli/config.json (see README)\n");
2743
+ console.log(" ZCode : add clef-mcp to ~/.zcode/cli/config.json (see README)");
2744
+ if (runtime === "mlx") {
2745
+ console.log("\nNote: set CLEF_RUNTIME=mlx for the server (and MCP clients) to use the MLX runtime.");
2746
+ }
2074
2747
  }
2075
2748
 
2076
2749
  // src/cli/models-cmd.ts
@@ -2103,7 +2776,7 @@ async function runStatus() {
2103
2776
  spec = getModelSpec(config.model);
2104
2777
  } catch (err) {
2105
2778
  if (err instanceof ClefError && err.code === "INVALID_INPUT") {
2106
- console.log(`Runtime: llama.cpp`);
2779
+ console.log(`Runtime: ${config.runtime}`);
2107
2780
  console.log(`Model: ${config.model} (unknown model id)`);
2108
2781
  console.log("Model status: unknown");
2109
2782
  console.log(`Memory: ${formatBytes(info.totalMemBytes)}`);
@@ -2111,6 +2784,26 @@ async function runStatus() {
2111
2784
  }
2112
2785
  throw err;
2113
2786
  }
2787
+ if (config.runtime === "mlx") {
2788
+ const mlx = await mlxIsInstalled(config.clefHome);
2789
+ let uvLine = "not found (install: curl -LsSf https://astral.sh/uv/install.sh | sh)";
2790
+ try {
2791
+ uvLine = `uv: ${resolveUv(process.env.CLEF_MLX_UV)}`;
2792
+ } catch (err) {
2793
+ uvLine = `not found \u2014 ${err.message.split("\n")[0]}`;
2794
+ }
2795
+ console.log(`Runtime: mlx (${uvLine})`);
2796
+ console.log(`Model: ${spec.id}${mlx ? " (MLX 4-bit)" : ""}`);
2797
+ console.log(`Model status: ${mlx ? "installed" : "not installed (run: clef-mcp install --runtime mlx)"}`);
2798
+ console.log(`Memory: ${formatBytes(info.totalMemBytes)}`);
2799
+ if (mlx) {
2800
+ console.log(`Snapshot: ${mlx.snapshotPath}`);
2801
+ console.log(`Installed: ${mlx.installedAt}`);
2802
+ } else {
2803
+ console.log(`Cache home: ${pathsFor(config.clefHome).root}`);
2804
+ }
2805
+ return;
2806
+ }
2114
2807
  const manifest = await findInstalledManifest(config.clefHome, spec);
2115
2808
  let runtimeLine;
2116
2809
  try {
@@ -2135,13 +2828,27 @@ async function runStatus() {
2135
2828
 
2136
2829
  // src/cli/uninstall.ts
2137
2830
  init_errors();
2138
- import fsp7 from "fs/promises";
2831
+ import fsp9 from "fs/promises";
2139
2832
  init_prompt();
2140
2833
  async function runUninstall(opts) {
2141
2834
  const config = loadConfig();
2142
2835
  const spec = getModelSpec(opts.model ?? config.model);
2143
- const manifest = await findInstalledManifest(config.clefHome, spec);
2144
2836
  const { modelsDir } = pathsFor(config.clefHome);
2837
+ if (config.runtime === "mlx") {
2838
+ const mlx = await mlxIsInstalled(config.clefHome);
2839
+ if (!mlx) {
2840
+ throw new ClefError(ClefErrorCode.MODEL_NOT_INSTALLED, "The MLX model is not installed.", "Nothing to remove.");
2841
+ }
2842
+ const ok2 = await confirm(`Remove the MLX snapshot for ${spec.id}?`, opts.yes === true);
2843
+ if (!ok2) {
2844
+ console.log("Aborted.");
2845
+ return;
2846
+ }
2847
+ await removeMlxModel(config.clefHome);
2848
+ console.log("Removed MLX snapshot.");
2849
+ return;
2850
+ }
2851
+ const manifest = await findInstalledManifest(config.clefHome, spec);
2145
2852
  if (!manifest && !opts.runtime) {
2146
2853
  throw new ClefError(
2147
2854
  ClefErrorCode.MODEL_NOT_INSTALLED,
@@ -2160,7 +2867,7 @@ async function runUninstall(opts) {
2160
2867
  return;
2161
2868
  }
2162
2869
  if (target) {
2163
- await fsp7.rm(target, { recursive: true, force: true });
2870
+ await fsp9.rm(target, { recursive: true, force: true });
2164
2871
  console.log(`Removed: ${target}`);
2165
2872
  }
2166
2873
  if (opts.runtime) {
@@ -2187,7 +2894,7 @@ program.action(async () => {
2187
2894
  handleCliError(err);
2188
2895
  }
2189
2896
  });
2190
- program.command("install").description("Detect the machine, download a Clef model + runtime, verify checksum and inference.").option("--model <id>", "model to install (default: clef-flash)").option("--quant <id>", "quantization override: Q4_K_M | Q8_0 | BF16").option("-y, --yes", "assume yes for all prompts (non-interactive)").option("--skip-runtime", "do not download/verify the llama.cpp runtime").option("--skip-probe", "skip the post-install inference verification").option("--setup", "also register the MCP server + agent skill in detected clients (same as running `clef-mcp setup`)").action(async (opts) => {
2897
+ program.command("install").description("Detect the machine, download a Clef model + runtime, verify checksum and inference.").option("--model <id>", "model to install (default: clef-flash)").option("--quant <id>", "quantization override: Q4_K_M | Q8_0 | BF16").option("-y, --yes", "assume yes for all prompts (non-interactive)").option("--skip-runtime", "do not download/verify the llama.cpp runtime").option("--skip-probe", "skip the post-install inference verification").option("--runtime <id>", "inference runtime: llama-cpp (default) | mlx (Apple Silicon)").option("--setup", "also register the MCP server + agent skill in detected clients (same as running `clef-mcp setup`)").action(async (opts) => {
2191
2898
  try {
2192
2899
  await runInstall(opts);
2193
2900
  } catch (err) {