clef-mcp 0.1.6 → 0.2.1

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";
@@ -1087,10 +1432,12 @@ function scoreCriteria(limits) {
1087
1432
  }
1088
1433
  function questionSchema(limits) {
1089
1434
  return z.object({
1090
- type: z.enum(["noul", "choice", "score"]),
1091
- instructions: z.string().min(1).max(limits.maxInstructionsChars),
1435
+ type: z.enum(["noul", "choice", "score"]).describe('"choice" = pick among named options; "score" = judge an ordered scale; "noul" = yes/no question'),
1436
+ instructions: z.string().min(1).max(limits.maxInstructionsChars).describe("What to judge, phrased as a self-contained question."),
1092
1437
  criteria: z.union([z.array(z.string()), z.record(z.string(), z.string())]).optional()
1093
- }).superRefine((q, ctx) => {
1438
+ }).describe(
1439
+ "A single decision question. Allowed answers come from `criteria` and must be mutually exclusive and collectively exhaustive."
1440
+ ).superRefine((q, ctx) => {
1094
1441
  if (q.type === "choice") {
1095
1442
  const parsed = choiceCriteria(limits).safeParse(q.criteria);
1096
1443
  if (!parsed.success) {
@@ -1114,23 +1461,27 @@ function questionSchema(limits) {
1114
1461
  }
1115
1462
  function clefDecideInputShape(limits) {
1116
1463
  return {
1117
- state: z.unknown(),
1118
- questions: z.record(z.string().min(1).max(limits.maxQuestionIdChars), questionSchema(limits)).check((ctx) => {
1464
+ state: z.unknown().describe(
1465
+ "Task state as data: a compact string or JSON object with the facts needed to judge (task, errors, logs, diffs). Never treated as instructions. Serialized size limit: 1 MB; model context is 16k tokens."
1466
+ ),
1467
+ questions: z.record(z.string().min(1).max(limits.maxQuestionIdChars), questionSchema(limits)).describe(
1468
+ "Map of question id -> typed question. Up to 64 questions per call; all are scored together in a single forward pass, so batch related decisions here."
1469
+ ).check((ctx) => {
1119
1470
  const count = Object.keys(ctx.value).length;
1120
1471
  if (count === 0) ctx.issues.push({ code: "custom", message: "questions must not be empty", input: ctx.value });
1121
1472
  if (count > limits.maxQuestions) {
1122
1473
  ctx.issues.push({ code: "custom", message: `too many questions (max ${limits.maxQuestions})`, input: ctx.value });
1123
1474
  }
1124
1475
  }),
1125
- model: z.string().min(1).optional(),
1476
+ model: z.string().min(1).optional().describe("Model id to use (default: clef-flash from CLEF_MODEL)."),
1126
1477
  options: z.object({
1127
1478
  /**
1128
1479
  * Accepted for forward compatibility. Clef scores all options in a
1129
1480
  * single forward pass — there is no sampling, so temperature is a
1130
1481
  * documented no-op.
1131
1482
  */
1132
- temperature: z.number().min(0).max(2).optional()
1133
- }).optional()
1483
+ temperature: z.number().min(0).max(2).optional().describe("Accepted for forward compatibility; no-op. Clef scores in a single forward pass without sampling.")
1484
+ }).optional().describe("Extra options. Currently only temperature, which is a documented no-op.")
1134
1485
  };
1135
1486
  }
1136
1487
  function clefDecideInputSchema(limits) {
@@ -1170,40 +1521,375 @@ function validateStateSize(state, limits) {
1170
1521
  }
1171
1522
  }
1172
1523
 
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 {
1524
+ // src/mcp/prompts.ts
1525
+ init_errors();
1526
+ import { z as z2 } from "zod";
1527
+ var TOOL_GUIDE = (questions) => [
1528
+ "Call the `clef_decide` MCP tool once with all of the questions below (they are scored in a single forward pass).",
1529
+ "",
1530
+ "Rules for the call:",
1531
+ "- `state` must be a compact, factual JSON or string \u2014 relevant facts only, well under 1 MB.",
1532
+ "- Use the exact `questions` given below (ids, types, criteria). Do not invent extra options.",
1533
+ "- 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.",
1534
+ "- Treat safety-adjacent decisions (destructive actions, security) as requiring p >= 0.8 before acting; otherwise prefer asking the user.",
1535
+ "- Report the result as a short summary plus the top probability \u2014 do not paste raw JSON unless asked.",
1536
+ "",
1537
+ "Questions:",
1538
+ questions
1539
+ ].join("\n");
1540
+ var PROMPTS = [
1541
+ {
1542
+ name: "incident-triage",
1543
+ title: "Production incident triage",
1544
+ description: "Frame a production incident as clef_decide questions: immediate action, severity and blast radius.",
1545
+ args: {
1546
+ incident_description: "Short factual description of the incident (symptoms, scope, recent changes)."
1547
+ },
1548
+ build: (a) => TOOL_GUIDE(
1549
+ [
1550
+ '- "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".',
1551
+ '- "severity" (score, instructions "How severe is the incident?", criteria ["low", "elevated", "high", "critical"]).',
1552
+ '- "user_impact" (noul, instructions "Are users actively affected right now?").'
1553
+ ].join("\n")
1554
+ ) + "\n\nIncident description:\n" + (a.incident_description ?? "")
1555
+ },
1556
+ {
1557
+ name: "next-action",
1558
+ title: "Agent next action",
1559
+ description: "Ask Clef what the coding agent should do next on a task: inspect, modify, test or ask the user.",
1560
+ args: {
1561
+ task_summary: "What the agent is working on, current step, and what is already known or done."
1562
+ },
1563
+ build: (a) => TOOL_GUIDE(
1564
+ [
1565
+ '- "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".',
1566
+ '- "confidence" (score, instructions "How confident are you in this decision?", criteria ["very_low", "low", "medium", "high", "very_high"]).'
1567
+ ].join("\n")
1568
+ ) + "\n\nTask summary:\n" + (a.task_summary ?? "")
1569
+ },
1570
+ {
1571
+ name: "ticket-routing",
1572
+ title: "Support ticket routing",
1573
+ description: "Classify an incoming message into the team that should handle it.",
1574
+ args: {
1575
+ ticket_text: "The customer or user message, verbatim if possible."
1576
+ },
1577
+ build: (a) => TOOL_GUIDE(
1578
+ [
1579
+ '- "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".',
1580
+ '- "urgent" (noul, instructions "Does this need a response within the hour?").'
1581
+ ].join("\n")
1582
+ ) + "\n\nTicket text:\n" + (a.ticket_text ?? "")
1583
+ },
1584
+ {
1585
+ name: "security-review",
1586
+ title: "Security sanity check",
1587
+ description: "Ask Clef whether a code snippet, change or setup smells like a security problem.",
1588
+ args: {
1589
+ change_summary: "The code, config or setup to review \u2014 compact but complete enough to judge."
1590
+ },
1591
+ build: (a) => TOOL_GUIDE(
1592
+ [
1593
+ '- "is_vulnerable" (noul, instructions "Does this contain a realistic security vulnerability?").',
1594
+ '- "risk" (score, instructions "How bad is it if this ships as-is?", criteria ["informational", "low", "medium", "high", "critical"]).',
1595
+ '- "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".'
1596
+ ].join("\n") + "\nSafety rule: only act without human review when p(is_vulnerable=false) >= 0.8."
1597
+ ) + "\n\nChange to review:\n" + (a.change_summary ?? "")
1598
+ }
1599
+ ];
1600
+ function registerPrompts(server) {
1601
+ for (const p of PROMPTS) {
1602
+ server.registerPrompt(
1603
+ p.name,
1604
+ {
1605
+ title: p.title,
1606
+ description: p.description,
1607
+ argsSchema: Object.fromEntries(Object.entries(p.args).map(([k, v]) => [k, z2.string().min(1).describe(v)]))
1608
+ },
1609
+ ({ ...args }) => {
1610
+ for (const key of Object.keys(p.args)) {
1611
+ if (typeof args[key] !== "string" || !args[key]) {
1612
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `Prompt argument "${key}" is required`);
1613
+ }
1614
+ }
1615
+ return {
1616
+ messages: [{ role: "user", content: { type: "text", text: p.build(args) } }]
1617
+ };
1618
+ }
1619
+ );
1620
+ }
1621
+ }
1622
+
1623
+ // src/mcp/resources.ts
1624
+ import fsSync3 from "fs";
1625
+ import fsp4 from "fs/promises";
1626
+
1627
+ // src/cli/evals.ts
1628
+ init_errors();
1629
+ import fsp3 from "fs/promises";
1630
+ import fsSync2 from "fs";
1631
+ import { fileURLToPath } from "url";
1632
+ import path6 from "path";
1633
+ function defaultDatasetPath() {
1634
+ let dir = path6.dirname(fileURLToPath(import.meta.url));
1635
+ for (let i = 0; i < 6; i++) {
1636
+ const candidate = path6.join(dir, "tests", "evals", "dataset.jsonl");
1637
+ if (fsSync2.existsSync(candidate)) return candidate;
1638
+ dir = path6.dirname(dir);
1639
+ }
1640
+ return fileURLToPath(new URL("../tests/evals/dataset.jsonl", import.meta.url));
1641
+ }
1642
+ function parseLine(line, lineNo) {
1643
+ const raw = JSON.parse(line);
1644
+ if (typeof raw.id !== "string" || !raw.question?.id || !raw.expected?.kind) {
1645
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `Eval dataset line ${lineNo} is missing id/question/expected`);
1646
+ }
1647
+ return raw;
1648
+ }
1649
+ function evaluate(answer, exp) {
1650
+ const prob = (key) => answer[key] ?? 0;
1651
+ switch (exp.kind) {
1652
+ case "argmax": {
1653
+ const keys = exp.among ?? Object.keys(answer);
1654
+ let best = "";
1655
+ let bestP = -1;
1656
+ for (const k of keys) {
1657
+ if (prob(k) > bestP) {
1658
+ bestP = prob(k);
1659
+ best = k;
1660
+ }
1661
+ }
1662
+ return { pass: best === exp.expect, detail: `argmax=${best || "n/a"} (${bestP.toFixed(3)}), expected=${exp.expect}` };
1663
+ }
1664
+ case "prob_gt":
1665
+ 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)}` };
1666
+ case "min_prob": {
1667
+ const p = prob(exp.option);
1668
+ return { pass: p >= exp.min, detail: `p(${exp.option})=${p.toFixed(3)}, required >= ${exp.min}` };
1669
+ }
1670
+ case "top_mass": {
1671
+ const mass = exp.options.reduce((sum, k) => sum + prob(k), 0);
1672
+ return { pass: mass >= exp.min, detail: `mass(${exp.options.join("+")})=${mass.toFixed(3)}, required >= ${exp.min}` };
1673
+ }
1674
+ }
1675
+ }
1676
+ async function runEvals(opts) {
1677
+ const config = loadConfig();
1678
+ const log = createLogger(config.logLevel);
1679
+ const spec = getModelSpec(config.model);
1680
+ let mlxManifest;
1681
+ if (config.runtime === "mlx") {
1682
+ mlxManifest = await mlxIsInstalled(config.clefHome);
1683
+ if (!mlxManifest) {
1684
+ throw new ClefError(
1685
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1686
+ `Model "${spec.id}" is not installed for the MLX runtime.`,
1687
+ "Run `clef-mcp install --runtime mlx` first."
1688
+ );
1689
+ }
1690
+ }
1691
+ const manifest = config.runtime === "mlx" ? void 0 : await findInstalledManifest(config.clefHome, spec);
1692
+ if (!manifest && !mlxManifest) {
1693
+ throw new ClefError(
1694
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1695
+ `Model "${spec.id}" is not installed; evals need the local model.`,
1696
+ "Run `clef-mcp install` first."
1697
+ );
1698
+ }
1699
+ const datasetPath = opts.dataset ?? defaultDatasetPath();
1700
+ const content = await fsp3.readFile(datasetPath, "utf8");
1701
+ let cases = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l, i) => parseLine(l, i + 1));
1702
+ if (opts.limit && opts.limit > 0) cases = cases.slice(0, opts.limit);
1703
+ if (cases.length === 0) {
1704
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `No eval cases found in ${datasetPath}`);
1705
+ }
1706
+ const binary = resolveLlamaServerBinary(config);
1707
+ 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 });
1708
+ const runtimeLabel = config.runtime === "mlx" ? "MLX" : manifest.quant;
1709
+ console.log(`Running ${cases.length} eval case(s) with ${spec.id} ${runtimeLabel}...
1710
+ `);
1711
+ await runtime.load();
1712
+ let passed = 0;
1713
+ const results = [];
1714
+ try {
1715
+ for (const [i, c] of cases.entries()) {
1716
+ process.stderr.write(`\r case ${i + 1}/${cases.length} \u2014 ${c.id}`);
1717
+ try {
1718
+ const request = toSystemOneRequest({ state: c.state, questions: { [c.question.id]: c.question } }, spec.id);
1719
+ const response = await runtime.decide(request);
1720
+ const output = fromSystemOneResponse(response, { [c.question.id]: c.question }, spec.id);
1721
+ const answer = output.decisions[c.question.id]?.answer ?? {};
1722
+ const verdict = evaluate(answer, c.expected);
1723
+ if (verdict.pass) passed++;
1724
+ results.push({ id: c.id, category: c.category, ok: verdict.pass, detail: verdict.detail });
1725
+ } catch (err) {
1726
+ results.push({
1727
+ id: c.id,
1728
+ category: c.category,
1729
+ ok: false,
1730
+ detail: `error: ${err instanceof Error ? err.message : String(err)}`
1731
+ });
1732
+ }
1733
+ }
1734
+ } finally {
1735
+ process.stderr.write("\n");
1736
+ await runtime.unload();
1737
+ }
1738
+ console.log("");
1739
+ for (const r of results) {
1740
+ console.log(`${r.ok ? "\u2714" : "\u2718"} [${r.category}] ${r.id} \u2014 ${r.detail}`);
1741
+ }
1742
+ const rate = passed / results.length;
1743
+ const minRate = opts.minRate ?? 0.8;
1744
+ console.log(`
1745
+ ${passed}/${results.length} passed (${(rate * 100).toFixed(1)}%), required >= ${(minRate * 100).toFixed(0)}%`);
1746
+ return rate >= minRate;
1747
+ }
1748
+
1749
+ // src/mcp/resources.ts
1750
+ var EVALS_SCHEMA_MD = `# clef-mcp eval dataset format
1751
+
1752
+ JSONL: one JSON object per line.
1753
+
1754
+ {
1755
+ "id": "unique-case-id",
1756
+ "category": "coding | security | classification | routing | yes_no",
1757
+ "state": <string | JSON value>,
1758
+ "question": {
1759
+ "id": "question-id",
1760
+ "type": "choice | score | noul",
1761
+ "instructions": "\u2026",
1762
+ "criteria": { "option": "description" } // choice (or ["a","b"] list)
1763
+ | ["low", "high"] // score, ordered
1764
+ },
1765
+ "expected": {
1766
+ "kind": "argmax | prob_gt | min_prob | top_mass",
1767
+ // argmax: { "expect": "option", "among": ["option", ...] (optional) }
1768
+ // prob_gt: { "a": "opt", "b": "opt" }
1769
+ // min_prob: { "option": "opt", "min": 0.7 }
1770
+ // top_mass: { "options": ["a","b"], "min": 0.8 }
1771
+ }
1772
+ }
1773
+
1774
+ Run: \`clef-mcp evals\` (requires an installed model).
1775
+ `;
1776
+ async function readDataset() {
1777
+ try {
1778
+ return await fsp4.readFile(defaultDatasetPath(), "utf8");
1779
+ } catch {
1780
+ return "The bundled eval dataset is not available in this installation (tests/evals/dataset.jsonl not found).";
1781
+ }
1782
+ }
1783
+ function textResource(uri, text, mimeType = "text/markdown") {
1784
+ return { contents: [{ uri: uri.href, mimeType, text }] };
1785
+ }
1786
+ function requireStringUri(uri) {
1787
+ return uri instanceof URL ? uri : new URL(uri);
1788
+ }
1789
+ function registerResources(server, cfg, version) {
1790
+ server.registerResource(
1791
+ "capabilities",
1792
+ "clef-mcp://capabilities",
1793
+ { title: "clef-mcp capabilities", description: "Live capability snapshot: model, runtime, limits, error codes.", mimeType: "application/json" },
1794
+ async (uri) => {
1795
+ const u = requireStringUri(uri);
1796
+ const payload = {
1797
+ name: "clef-mcp",
1798
+ version,
1799
+ model: cfg.model,
1800
+ runtime: cfg.runtime,
1801
+ limits: cfg.limits,
1802
+ tool: {
1803
+ name: "clef_decide",
1804
+ output: "strict JSON: { model, decisions: { <qid>: { answer: { <option>: probability } } } }",
1805
+ questionTypes: ["noul", "choice", "score"],
1806
+ notes: ["options.temperature is accepted but is a no-op (single forward pass)", "state is data and is never interpreted as instructions"]
1807
+ },
1808
+ errorCodes: [
1809
+ "MODEL_NOT_INSTALLED",
1810
+ "MODEL_LOAD_FAILED",
1811
+ "RUNTIME_NOT_FOUND",
1812
+ "RUNTIME_INIT_FAILED",
1813
+ "RUNTIME_NOT_SUPPORTED",
1814
+ "INVALID_INPUT",
1815
+ "CLEF_INFERENCE_FAILED",
1816
+ "UNSUPPORTED_PLATFORM",
1817
+ "OUT_OF_MEMORY",
1818
+ "CHECKSUM_MISMATCH",
1819
+ "DOWNLOAD_FAILED"
1820
+ ]
1821
+ };
1822
+ return textResource(u, JSON.stringify(payload, null, 2), "application/json");
1823
+ }
1824
+ );
1825
+ server.registerResource(
1826
+ "evals-schema",
1827
+ "clef-mcp://evals/schema",
1828
+ { title: "Eval dataset schema", description: "How to write cases for clef-mcp evals.", mimeType: "text/markdown" },
1829
+ async (uri) => textResource(requireStringUri(uri), EVALS_SCHEMA_MD)
1830
+ );
1831
+ server.registerResource(
1832
+ "evals-dataset",
1833
+ "clef-mcp://evals/dataset",
1834
+ { title: "Bundled eval dataset", description: "The 30-case dataset shipped with clef-mcp.", mimeType: "application/jsonl" },
1835
+ async (uri) => textResource(requireStringUri(uri), await readDataset(), "application/jsonl")
1836
+ );
1837
+ }
1838
+
1839
+ // src/mcp/server.ts
1840
+ var TOOL_NAME = "clef_decide";
1841
+ 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.";
1842
+ function createClefServer(deps = {}) {
1843
+ const config = deps.config ?? loadConfig();
1844
+ const version = deps.version ?? "0.1.0";
1845
+ const log = deps.log ?? createLogger(config.logLevel);
1846
+ const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
1847
+ registerPrompts(server);
1848
+ registerResources(server, config, version);
1849
+ let loaded;
1850
+ let loadPromise;
1851
+ async function ensureRuntime(spec) {
1852
+ if (loaded && loaded.model === spec.id) return loaded;
1853
+ if (loaded && loaded.model !== spec.id) {
1854
+ await loaded.runtime.unload();
1855
+ loaded = void 0;
1856
+ }
1857
+ if (loadPromise) return loadPromise;
1858
+ loadPromise = (async () => {
1859
+ if (config.runtime === "mlx") {
1860
+ const manifest2 = await mlxIsInstalled(config.clefHome);
1861
+ if (!manifest2) {
1862
+ throw new ClefError(
1863
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1864
+ `Clef model "${spec.id}" is not installed for the MLX runtime.`,
1865
+ "Run `clef-mcp install --runtime mlx`."
1866
+ );
1867
+ }
1868
+ const uvBin = resolveUv(process.env.CLEF_MLX_UV);
1869
+ const runtime2 = deps.runtimeFactory ? deps.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log });
1870
+ log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
1871
+ const loadStarted2 = Date.now();
1872
+ await runtime2.load();
1873
+ log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
1874
+ return { runtime: runtime2, model: spec.id };
1875
+ }
1876
+ const manifest = await findInstalledManifest(config.clefHome, spec);
1877
+ if (!manifest) {
1878
+ throw new ClefError(
1879
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1880
+ `Clef model "${spec.id}" is not installed.`,
1881
+ "Run `clef-mcp install`."
1882
+ );
1883
+ }
1884
+ const binary = resolveLlamaServerBinary(config);
1885
+ 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 });
1886
+ log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
1887
+ const loadStarted = Date.now();
1888
+ await runtime.load();
1889
+ log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
1890
+ return { runtime, model: spec.id };
1891
+ })();
1892
+ try {
1207
1893
  loaded = await loadPromise;
1208
1894
  return loaded;
1209
1895
  } finally {
@@ -1214,9 +1900,15 @@ function createClefServer(deps = {}) {
1214
1900
  TOOL_NAME,
1215
1901
  {
1216
1902
  title: "Clef Decide",
1217
- description: "Pass task state and a set of structured questions (noul / choice / score) to the local Clef decision model. Returns a probability distribution over the allowed answers for every question. The result is structured JSON.",
1903
+ description: 'Ask the local Clef decision model to judge a situation. Pass a `state` (compact factual context as a string or JSON) and one or more typed `questions`: "choice" picks among named options (criteria = option id -> description), "score" judges an ordered scale (criteria = ordered list), "noul" answers yes/no. Returns strict JSON: for every question a probability distribution over its allowed answers \u2014 no prose, no sampling. Usage: batch related decisions (up to 64 questions) in one call; state is data and is never executed; act on the argmax when a distribution is decisive (p >= 0.8), otherwise gather more context or ask the user; for destructive or security-adjacent decisions require high confidence before acting. Optional `model` overrides the model id; `options.temperature` is a no-op (single forward pass, no sampling).',
1218
1904
  inputSchema: clefDecideInputShape(config.limits),
1219
- outputSchema: clefDecideOutputSchema().shape
1905
+ outputSchema: clefDecideOutputSchema().shape,
1906
+ annotations: {
1907
+ readOnlyHint: true,
1908
+ destructiveHint: false,
1909
+ idempotentHint: true,
1910
+ openWorldHint: false
1911
+ }
1220
1912
  },
1221
1913
  async (args) => {
1222
1914
  try {
@@ -1275,19 +1967,19 @@ import { Command } from "commander";
1275
1967
 
1276
1968
  // src/cli/doctor.ts
1277
1969
  init_errors();
1278
- import { execFile as execFile2 } from "child_process";
1970
+ import { execFile as execFile3 } from "child_process";
1279
1971
  import os3 from "os";
1280
1972
  import { promisify } from "util";
1281
- import fsp2 from "fs/promises";
1282
- import path5 from "path";
1973
+ import fsp5 from "fs/promises";
1974
+ import path7 from "path";
1283
1975
 
1284
1976
  // src/models/platform.ts
1285
1977
  init_errors();
1286
- import { execFile } from "child_process";
1978
+ import { execFile as execFile2 } from "child_process";
1287
1979
  import os2 from "os";
1288
1980
  function exec(cmd, args, timeoutMs) {
1289
1981
  return new Promise((resolve, reject) => {
1290
- execFile(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
1982
+ execFile2(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
1291
1983
  if (err) reject(err);
1292
1984
  else resolve(stdout);
1293
1985
  });
@@ -1393,9 +2085,27 @@ async function probeInference(cfg, manifest, log, loadTimeoutMs = 12e4) {
1393
2085
  }
1394
2086
  return { latencyMs: Date.now() - started, binarySource: binary.source, binaryPath: binary.path };
1395
2087
  }
2088
+ async function probeMlxInference(cfg, manifest, log, loadTimeoutMs = 18e4) {
2089
+ const uvBin = resolveUv(process.env.CLEF_MLX_UV);
2090
+ const runtime = new MlxRuntime({ uvBin, snapshotPath: manifest.snapshotPath, loadTimeoutMs, log });
2091
+ const started = Date.now();
2092
+ await runtime.load();
2093
+ try {
2094
+ await runtime.decide({
2095
+ model: manifest.model,
2096
+ state: "This is an internal clef-mcp installation probe.",
2097
+ questions: {
2098
+ probe: { type: "noul", instructions: "Is this a clef-mcp installation probe?" }
2099
+ }
2100
+ });
2101
+ } finally {
2102
+ await runtime.unload();
2103
+ }
2104
+ return { latencyMs: Date.now() - started, binarySource: "uv", binaryPath: uvBin };
2105
+ }
1396
2106
 
1397
2107
  // src/cli/doctor.ts
1398
- var execFileP = promisify(execFile2);
2108
+ var execFileP = promisify(execFile3);
1399
2109
  function mark(ok) {
1400
2110
  return ok ? "\u2714" : "\u2718";
1401
2111
  }
@@ -1415,14 +2125,14 @@ async function checkBinaryVersion(binaryPath) {
1415
2125
  async function detectMcpConfigs() {
1416
2126
  const home = os3.homedir();
1417
2127
  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 }
2128
+ { client: "Codex", path: path7.join(home, ".codex", "config.toml"), configured: false },
2129
+ { client: "Claude Code (user)", path: path7.join(home, ".claude.json"), configured: false },
2130
+ { client: "Claude Code (project)", path: path7.resolve(".mcp.json"), configured: false },
2131
+ { client: "Cursor", path: path7.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
1422
2132
  ];
1423
2133
  for (const hint of hints) {
1424
2134
  try {
1425
- const content = await fsp2.readFile(hint.path, "utf8");
2135
+ const content = await fsp5.readFile(hint.path, "utf8");
1426
2136
  hint.configured = content.includes("clef-mcp");
1427
2137
  } catch {
1428
2138
  hint.configured = false;
@@ -1461,33 +2171,75 @@ async function runDoctor(opts) {
1461
2171
  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
2172
  });
1463
2173
  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
- });
2174
+ if (config.runtime === "mlx") {
2175
+ try {
2176
+ binaryPath = resolveUv(process.env.CLEF_MLX_UV);
2177
+ checks.push({
2178
+ name: "Runtime (uv, for the MLX runtime)",
2179
+ ok: true,
2180
+ detail: binaryPath
2181
+ });
2182
+ } catch (err) {
2183
+ checks.push({
2184
+ name: "Runtime (uv, for the MLX runtime)",
2185
+ ok: false,
2186
+ detail: err instanceof Error ? err.message : String(err),
2187
+ hint: "Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh"
2188
+ });
2189
+ }
2190
+ } else {
2191
+ try {
2192
+ const binary = resolveLlamaServerBinary(config);
2193
+ binaryPath = binary.path;
2194
+ const version = await checkBinaryVersion(binary.path);
2195
+ checks.push({
2196
+ name: "Runtime binary",
2197
+ ok: version !== void 0,
2198
+ detail: `${binary.source}: ${binary.path}${version ? ` (${version})` : ""}`,
2199
+ hint: version === void 0 ? "The binary exists but did not answer --version; it may be broken or incompatible." : void 0
2200
+ });
2201
+ } catch (err) {
2202
+ binaryPath = void 0;
2203
+ checks.push({
2204
+ name: "Runtime binary",
2205
+ ok: false,
2206
+ detail: err instanceof Error ? err.message : String(err),
2207
+ hint: err instanceof ClefError ? err.hint : void 0
2208
+ });
2209
+ }
1481
2210
  }
1482
2211
  let spec;
1483
2212
  let manifest;
2213
+ let mlxManifest;
1484
2214
  try {
1485
2215
  spec = getModelSpec(config.model);
1486
- manifest = await findInstalledManifest(config.clefHome, spec);
2216
+ if (config.runtime === "mlx") mlxManifest = await mlxIsInstalled(config.clefHome);
2217
+ else manifest = await findInstalledManifest(config.clefHome, spec);
1487
2218
  } catch {
1488
2219
  checks.push({ name: "Model", ok: false, detail: `unknown model id "${config.model}"` });
1489
2220
  }
1490
- if (spec) {
2221
+ if (config.runtime === "mlx") {
2222
+ if (!mlxManifest) {
2223
+ checks.push({
2224
+ name: "Model",
2225
+ ok: false,
2226
+ detail: `${spec?.id ?? config.model} is not installed for the MLX runtime`,
2227
+ hint: "Run `clef-mcp install --runtime mlx`."
2228
+ });
2229
+ } else {
2230
+ checks.push({
2231
+ name: "Model",
2232
+ ok: true,
2233
+ detail: `MLX snapshot at ${mlxManifest.snapshotPath}`
2234
+ });
2235
+ checks.push({
2236
+ name: "Model license",
2237
+ ok: true,
2238
+ informational: true,
2239
+ detail: `${mlxManifest.licenseId} \u2014 ${mlxManifest.licenseUrl}`
2240
+ });
2241
+ }
2242
+ } else if (spec) {
1491
2243
  if (!manifest) {
1492
2244
  checks.push({
1493
2245
  name: "Model",
@@ -1520,7 +2272,24 @@ async function runDoctor(opts) {
1520
2272
  });
1521
2273
  }
1522
2274
  }
1523
- if (binaryPath && manifest && (await verifyModelFiles(manifest)).ok) {
2275
+ if (config.runtime === "mlx" && binaryPath && mlxManifest) {
2276
+ try {
2277
+ const probe = await probeMlxInference(config, mlxManifest, void 0, 24e4);
2278
+ checks.push({
2279
+ name: "Inference",
2280
+ ok: true,
2281
+ detail: `probe decision returned in ${(probe.latencyMs / 1e3).toFixed(1)}s (incl. model load)`
2282
+ });
2283
+ } catch (err) {
2284
+ const clefErr = err instanceof ClefError ? err : void 0;
2285
+ checks.push({
2286
+ name: "Inference",
2287
+ ok: false,
2288
+ detail: clefErr?.message ?? (err instanceof Error ? err.message : String(err)),
2289
+ hint: clefErr?.hint
2290
+ });
2291
+ }
2292
+ } else if (config.runtime === "llama-cpp" && binaryPath && manifest && (await verifyModelFiles(manifest)).ok) {
1524
2293
  try {
1525
2294
  const probe = await probeInference(config, manifest, void 0, 18e4);
1526
2295
  checks.push({
@@ -1570,127 +2339,20 @@ ${failed.length} critical check(s) failed.`
1570
2339
  return failed.length > 0;
1571
2340
  }
1572
2341
 
1573
- // src/cli/evals.ts
2342
+ // src/cli/install.ts
1574
2343
  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
2344
 
1683
2345
  // src/models/llama-install.ts
1684
2346
  init_errors();
1685
- import { execFile as execFile3 } from "child_process";
1686
- import fsp5 from "fs/promises";
1687
- import path8 from "path";
2347
+ import { execFile as execFile4 } from "child_process";
2348
+ import fsp7 from "fs/promises";
2349
+ import path9 from "path";
1688
2350
 
1689
2351
  // src/models/hf.ts
1690
2352
  init_errors();
1691
2353
  import { createHash as createHash2 } from "crypto";
1692
- import fsp4 from "fs/promises";
1693
- import path7 from "path";
2354
+ import fsp6 from "fs/promises";
2355
+ import path8 from "path";
1694
2356
  var HF_BASE_URL = "https://huggingface.co";
1695
2357
  function resolveUrl(repo, revision, file) {
1696
2358
  return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
@@ -1748,7 +2410,7 @@ async function downloadOnce(url, dest, opts) {
1748
2410
  }
1749
2411
  const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
1750
2412
  const hash = createHash2("sha256");
1751
- const handle = await fsp4.open(dest, "w");
2413
+ const handle = await fsp6.open(dest, "w");
1752
2414
  let bytes = 0;
1753
2415
  try {
1754
2416
  for await (const chunk of res.body) {
@@ -1763,7 +2425,7 @@ async function downloadOnce(url, dest, opts) {
1763
2425
  }
1764
2426
  const sha256 = hash.digest("hex");
1765
2427
  if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
1766
- await fsp4.rm(dest, { force: true });
2428
+ await fsp6.rm(dest, { force: true });
1767
2429
  throw new ClefError(
1768
2430
  ClefErrorCode.CHECKSUM_MISMATCH,
1769
2431
  `Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
@@ -1771,7 +2433,7 @@ async function downloadOnce(url, dest, opts) {
1771
2433
  );
1772
2434
  }
1773
2435
  if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
1774
- await fsp4.rm(dest, { force: true });
2436
+ await fsp6.rm(dest, { force: true });
1775
2437
  throw new ClefError(
1776
2438
  ClefErrorCode.DOWNLOAD_FAILED,
1777
2439
  `Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
@@ -1782,24 +2444,24 @@ async function downloadOnce(url, dest, opts) {
1782
2444
  async function installModel(opts) {
1783
2445
  const { modelSpec, quantSpec } = opts;
1784
2446
  const { modelsDir } = pathsFor(opts.clefHome);
1785
- const manifestPath = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
1786
- const existing = await readModelManifest(manifestPath);
2447
+ const manifestPath2 = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
2448
+ const existing = await readModelManifest(manifestPath2);
1787
2449
  if (existing && (await verifyModelFiles(existing)).ok) {
1788
2450
  opts.log?.debug("model already installed", { model: modelSpec.id, quant: quantSpec.id });
1789
2451
  return { manifest: existing, alreadyInstalled: true };
1790
2452
  }
1791
2453
  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 });
2454
+ const finalPath = path8.join(path8.dirname(manifestPath2), quantSpec.file);
2455
+ await fsp6.mkdir(path8.dirname(finalPath), { recursive: true });
2456
+ const tmpPath = path8.join(path8.dirname(finalPath), `.download-${quantSpec.file}`);
2457
+ await fsp6.rm(tmpPath, { force: true });
1796
2458
  opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
1797
2459
  const result = await downloadFile(info.downloadUrl, tmpPath, {
1798
2460
  expectedSha256: info.sha256,
1799
2461
  expectedSize: info.size ?? quantSpec.bytes,
1800
2462
  onProgress: opts.onProgress
1801
2463
  });
1802
- await fsp4.rename(tmpPath, finalPath);
2464
+ await fsp6.rename(tmpPath, finalPath);
1803
2465
  const manifest = {
1804
2466
  schemaVersion: 1,
1805
2467
  model: modelSpec.id,
@@ -1815,7 +2477,7 @@ async function installModel(opts) {
1815
2477
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1816
2478
  clefMcpVersion: opts.clefMcpVersion
1817
2479
  };
1818
- await writeModelManifest(manifestPath, manifest);
2480
+ await writeModelManifest(manifestPath2, manifest);
1819
2481
  opts.log?.info("model installed", { model: modelSpec.id, quant: quantSpec.id, sha256: result.sha256 });
1820
2482
  return { manifest, alreadyInstalled: false };
1821
2483
  }
@@ -1874,7 +2536,7 @@ function pickAsset(release, platform, arch) {
1874
2536
  }
1875
2537
  function execTar(args, cwd) {
1876
2538
  return new Promise((resolve, reject) => {
1877
- execFile3("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
2539
+ execFile4("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
1878
2540
  if (err) reject(new Error(`tar ${args.join(" ")} failed: ${stderr || err.message}`));
1879
2541
  else resolve();
1880
2542
  });
@@ -1887,12 +2549,12 @@ async function findServerBinary(dir, platform) {
1887
2549
  const current = queue.shift();
1888
2550
  let entries;
1889
2551
  try {
1890
- entries = await fsp5.readdir(current, { withFileTypes: true });
2552
+ entries = await fsp7.readdir(current, { withFileTypes: true });
1891
2553
  } catch {
1892
2554
  continue;
1893
2555
  }
1894
2556
  for (const entry of entries) {
1895
- const full = path8.join(current, entry.name);
2557
+ const full = path9.join(current, entry.name);
1896
2558
  if (entry.isDirectory()) queue.push(full);
1897
2559
  else if (entry.name === target) return full;
1898
2560
  }
@@ -1903,14 +2565,14 @@ async function findServerBinary(dir, platform) {
1903
2565
  );
1904
2566
  }
1905
2567
  async function extractRelease(archivePath, destDir, platform) {
1906
- await fsp5.mkdir(destDir, { recursive: true });
2568
+ await fsp7.mkdir(destDir, { recursive: true });
1907
2569
  const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
1908
2570
  try {
1909
2571
  await execTar(args, destDir);
1910
2572
  } catch (err) {
1911
2573
  throw new ClefError(
1912
2574
  ClefErrorCode.DOWNLOAD_FAILED,
1913
- `Could not extract ${path8.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
2575
+ `Could not extract ${path9.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
1914
2576
  "Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
1915
2577
  );
1916
2578
  }
@@ -1920,7 +2582,7 @@ async function ensureLlamaRuntime(cfg, log) {
1920
2582
  const resolved = resolveLlamaServerBinary(cfg);
1921
2583
  if (resolved.source === "managed") {
1922
2584
  const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
1923
- const manifest = readRuntimeManifest(path8.join(platformDir, "runtime-manifest.json"));
2585
+ const manifest = readRuntimeManifest(path9.join(platformDir, "runtime-manifest.json"));
1924
2586
  return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
1925
2587
  }
1926
2588
  return { ...resolved, releaseTag: resolved.source, freshInstall: false };
@@ -1952,16 +2614,16 @@ async function downloadManagedLlama(cfg, log) {
1952
2614
  }
1953
2615
  const asset = pickAsset(release, platform, arch);
1954
2616
  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 });
2617
+ const tagDir = path9.join(platformDir, release.tag_name);
2618
+ await fsp7.mkdir(tagDir, { recursive: true });
2619
+ const tmpArchive = path9.join(platformDir, `.download-${asset.name}`);
2620
+ await fsp7.rm(tmpArchive, { force: true });
1959
2621
  const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
1960
2622
  await extractRelease(tmpArchive, tagDir, platform);
1961
- await fsp5.rm(tmpArchive, { force: true });
2623
+ await fsp7.rm(tmpArchive, { force: true });
1962
2624
  const binaryPath = await findServerBinary(tagDir, platform);
1963
- if (platform !== "win32") await fsp5.chmod(binaryPath, 493);
1964
- await writeRuntimeManifest(path8.join(platformDir, "runtime-manifest.json"), {
2625
+ if (platform !== "win32") await fsp7.chmod(binaryPath, 493);
2626
+ await writeRuntimeManifest(path9.join(platformDir, "runtime-manifest.json"), {
1965
2627
  schemaVersion: 1,
1966
2628
  kind: "llama-cpp",
1967
2629
  releaseTag: release.tag_name,
@@ -1977,11 +2639,11 @@ async function removeManagedRuntime(cfg) {
1977
2639
  const { runtimeDir } = pathsFor(cfg.clefHome);
1978
2640
  const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
1979
2641
  try {
1980
- await fsp5.stat(dir);
2642
+ await fsp7.stat(dir);
1981
2643
  } catch {
1982
2644
  return false;
1983
2645
  }
1984
- await fsp5.rm(dir, { recursive: true, force: true });
2646
+ await fsp7.rm(dir, { recursive: true, force: true });
1985
2647
  return true;
1986
2648
  }
1987
2649
 
@@ -1989,7 +2651,7 @@ async function removeManagedRuntime(cfg) {
1989
2651
  init_prompt();
1990
2652
 
1991
2653
  // src/version.ts
1992
- var VERSION = "0.1.6";
2654
+ var VERSION = "0.2.1";
1993
2655
 
1994
2656
  // src/cli/install.ts
1995
2657
  var OS_NAMES = {
@@ -2011,6 +2673,10 @@ function printHeader() {
2011
2673
  async function runInstall(opts) {
2012
2674
  const config = loadConfig();
2013
2675
  const log = createLogger("info");
2676
+ const runtime = opts.runtime ?? config.runtime;
2677
+ if (runtime !== "llama-cpp" && runtime !== "mlx") {
2678
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `Unknown runtime "${runtime}".`, "Available: llama-cpp, mlx");
2679
+ }
2014
2680
  printHeader();
2015
2681
  process.stdout.write("Detecting hardware...\n");
2016
2682
  const info = await detectPlatform();
@@ -2023,41 +2689,57 @@ async function runInstall(opts) {
2023
2689
  console.log(` Architecture: ${info.arch}`);
2024
2690
  console.log(` Memory: ${formatBytes(info.totalMemBytes)}`);
2025
2691
  if (gpuLine) console.log(gpuLine);
2692
+ if (runtime === "mlx" && !(info.platform === "darwin" && info.arch === "arm64")) {
2693
+ throw new ClefError(
2694
+ ClefErrorCode.UNSUPPORTED_PLATFORM,
2695
+ "The MLX runtime only works on Apple Silicon (macOS/arm64).",
2696
+ "Use the default llama.cpp runtime (omit --runtime)."
2697
+ );
2698
+ }
2026
2699
  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)}`);
2700
+ console.log(` Model: ${spec.id} (${runtime === "mlx" ? "mlx-community/clef-flash-4bit" : spec.repo})`);
2701
+ console.log(` Runtime: ${runtime}`);
2702
+ if (runtime === "llama-cpp") console.log(` Quantization: ${quant.id}`);
2703
+ console.log(` Download size: ~${runtime === "mlx" ? "6.2 GB" : formatBytes(quant.bytes)}`);
2030
2704
  console.log(`
2031
2705
  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");
2706
+ console.log("The model is downloaded from the official conversion and is not repackaged by clef-mcp.\n");
2033
2707
  const ok = await confirm("Continue?", opts.yes === true);
2034
2708
  if (!ok) {
2035
2709
  console.log("Aborted. Nothing was downloaded.");
2036
2710
  return;
2037
2711
  }
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}`);
2712
+ let llamaManifest;
2713
+ let mlxManifest;
2714
+ if (runtime === "mlx") {
2715
+ console.log("\nDownloading MLX model snapshot (uv-managed; weights ~6 GB)...\n");
2716
+ mlxManifest = await installModelMlx({ clefHome: config.clefHome, clefMcpVersion: VERSION, log });
2717
+ console.log(` Installed: ${mlxManifest.snapshotPath}`);
2718
+ } else {
2719
+ if (!opts.skipRuntime) {
2720
+ process.stdout.write("\nEnsuring llama.cpp runtime...\n");
2721
+ const rt = await ensureLlamaRuntime(config, log);
2722
+ console.log(` Runtime: llama.cpp (${rt.source}, ${rt.releaseTag})`);
2723
+ console.log(` Binary: ${rt.path}`);
2724
+ }
2725
+ console.log("");
2726
+ const installed = await installModel({
2727
+ clefHome: config.clefHome,
2728
+ modelSpec: spec,
2729
+ quantSpec: quant,
2730
+ clefMcpVersion: VERSION,
2731
+ onProgress: (p) => printProgress(p.bytes, p.total),
2732
+ log
2733
+ });
2734
+ process.stderr.write("\n");
2735
+ console.log(installed.alreadyInstalled ? ` Model already installed: ${installed.manifest.modelPath}` : ` Installed: ${installed.manifest.modelPath}`);
2736
+ console.log(` sha256: ${installed.manifest.sha256 ?? "n/a (not advertised upstream)"}`);
2737
+ llamaManifest = installed.manifest;
2044
2738
  }
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
2739
  if (!opts.skipProbe) {
2058
2740
  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)`);
2741
+ const probe = runtime === "mlx" ? await probeMlxInference(config, mlxManifest, log) : await probeInference(config, llamaManifest, log);
2742
+ console.log(` Inference OK (${(probe.latencyMs / 1e3).toFixed(1)}s incl. model load)`);
2061
2743
  }
2062
2744
  if (opts.setup) {
2063
2745
  const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
@@ -2070,7 +2752,10 @@ Model license: ${spec.licenseId} \u2014 ${spec.licenseUrl}`);
2070
2752
  console.log(" Claude Code : claude mcp add clef-mcp -- clef-mcp");
2071
2753
  console.log(" Codex : add [mcp_servers.clef-mcp] to ~/.codex/config.toml (see README)");
2072
2754
  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");
2755
+ console.log(" ZCode : add clef-mcp to ~/.zcode/cli/config.json (see README)");
2756
+ if (runtime === "mlx") {
2757
+ console.log("\nNote: set CLEF_RUNTIME=mlx for the server (and MCP clients) to use the MLX runtime.");
2758
+ }
2074
2759
  }
2075
2760
 
2076
2761
  // src/cli/models-cmd.ts
@@ -2103,7 +2788,7 @@ async function runStatus() {
2103
2788
  spec = getModelSpec(config.model);
2104
2789
  } catch (err) {
2105
2790
  if (err instanceof ClefError && err.code === "INVALID_INPUT") {
2106
- console.log(`Runtime: llama.cpp`);
2791
+ console.log(`Runtime: ${config.runtime}`);
2107
2792
  console.log(`Model: ${config.model} (unknown model id)`);
2108
2793
  console.log("Model status: unknown");
2109
2794
  console.log(`Memory: ${formatBytes(info.totalMemBytes)}`);
@@ -2111,6 +2796,26 @@ async function runStatus() {
2111
2796
  }
2112
2797
  throw err;
2113
2798
  }
2799
+ if (config.runtime === "mlx") {
2800
+ const mlx = await mlxIsInstalled(config.clefHome);
2801
+ let uvLine = "not found (install: curl -LsSf https://astral.sh/uv/install.sh | sh)";
2802
+ try {
2803
+ uvLine = `uv: ${resolveUv(process.env.CLEF_MLX_UV)}`;
2804
+ } catch (err) {
2805
+ uvLine = `not found \u2014 ${err.message.split("\n")[0]}`;
2806
+ }
2807
+ console.log(`Runtime: mlx (${uvLine})`);
2808
+ console.log(`Model: ${spec.id}${mlx ? " (MLX 4-bit)" : ""}`);
2809
+ console.log(`Model status: ${mlx ? "installed" : "not installed (run: clef-mcp install --runtime mlx)"}`);
2810
+ console.log(`Memory: ${formatBytes(info.totalMemBytes)}`);
2811
+ if (mlx) {
2812
+ console.log(`Snapshot: ${mlx.snapshotPath}`);
2813
+ console.log(`Installed: ${mlx.installedAt}`);
2814
+ } else {
2815
+ console.log(`Cache home: ${pathsFor(config.clefHome).root}`);
2816
+ }
2817
+ return;
2818
+ }
2114
2819
  const manifest = await findInstalledManifest(config.clefHome, spec);
2115
2820
  let runtimeLine;
2116
2821
  try {
@@ -2135,13 +2840,27 @@ async function runStatus() {
2135
2840
 
2136
2841
  // src/cli/uninstall.ts
2137
2842
  init_errors();
2138
- import fsp7 from "fs/promises";
2843
+ import fsp9 from "fs/promises";
2139
2844
  init_prompt();
2140
2845
  async function runUninstall(opts) {
2141
2846
  const config = loadConfig();
2142
2847
  const spec = getModelSpec(opts.model ?? config.model);
2143
- const manifest = await findInstalledManifest(config.clefHome, spec);
2144
2848
  const { modelsDir } = pathsFor(config.clefHome);
2849
+ if (config.runtime === "mlx") {
2850
+ const mlx = await mlxIsInstalled(config.clefHome);
2851
+ if (!mlx) {
2852
+ throw new ClefError(ClefErrorCode.MODEL_NOT_INSTALLED, "The MLX model is not installed.", "Nothing to remove.");
2853
+ }
2854
+ const ok2 = await confirm(`Remove the MLX snapshot for ${spec.id}?`, opts.yes === true);
2855
+ if (!ok2) {
2856
+ console.log("Aborted.");
2857
+ return;
2858
+ }
2859
+ await removeMlxModel(config.clefHome);
2860
+ console.log("Removed MLX snapshot.");
2861
+ return;
2862
+ }
2863
+ const manifest = await findInstalledManifest(config.clefHome, spec);
2145
2864
  if (!manifest && !opts.runtime) {
2146
2865
  throw new ClefError(
2147
2866
  ClefErrorCode.MODEL_NOT_INSTALLED,
@@ -2160,7 +2879,7 @@ async function runUninstall(opts) {
2160
2879
  return;
2161
2880
  }
2162
2881
  if (target) {
2163
- await fsp7.rm(target, { recursive: true, force: true });
2882
+ await fsp9.rm(target, { recursive: true, force: true });
2164
2883
  console.log(`Removed: ${target}`);
2165
2884
  }
2166
2885
  if (opts.runtime) {
@@ -2187,7 +2906,7 @@ program.action(async () => {
2187
2906
  handleCliError(err);
2188
2907
  }
2189
2908
  });
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) => {
2909
+ 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
2910
  try {
2192
2911
  await runInstall(opts);
2193
2912
  } catch (err) {