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/README.md +168 -143
- package/assets/demo.svg +70 -0
- package/dist/cli.js +1009 -290
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +556 -62
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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
|
|
106
|
-
import
|
|
107
|
-
import
|
|
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
|
|
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
|
|
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
|
|
135
|
-
await
|
|
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 =
|
|
139
|
+
const file = path10.join(HOME(), ".codex", "config.toml");
|
|
140
140
|
let content = "";
|
|
141
141
|
try {
|
|
142
|
-
content = await
|
|
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
|
|
151
|
-
await
|
|
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 =
|
|
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 =
|
|
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(
|
|
165
|
+
const dirs = (process.env.PATH ?? "").split(path10.delimiter).filter(Boolean);
|
|
166
166
|
for (const dir of dirs) {
|
|
167
|
-
const candidate =
|
|
167
|
+
const candidate = path10.join(dir, bin);
|
|
168
168
|
try {
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
204
|
-
const dest =
|
|
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
|
|
207
|
+
content = await fsp8.readFile(dest, "utf8");
|
|
208
208
|
} catch {
|
|
209
209
|
}
|
|
210
|
-
const incoming = await
|
|
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
|
|
215
|
-
await
|
|
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
|
|
508
|
-
if (
|
|
509
|
-
throw new Error(`Unsupported CLEF_RUNTIME "${
|
|
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
|
|
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(
|
|
676
|
+
async function readModelManifest(manifestPath2) {
|
|
675
677
|
let raw;
|
|
676
678
|
try {
|
|
677
|
-
raw = await fsp.readFile(
|
|
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(
|
|
689
|
-
await fsp.mkdir(path3.dirname(
|
|
690
|
-
await fsp.writeFile(
|
|
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(
|
|
749
|
-
await fsp.mkdir(path3.dirname(
|
|
750
|
-
await fsp.writeFile(
|
|
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(
|
|
754
|
+
function readRuntimeManifest(manifestPath2) {
|
|
753
755
|
try {
|
|
754
|
-
const parsed = JSON.parse(fs.readFileSync(
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
930
|
+
const manifest = readRuntimeManifest(path5.join(platformDir, "runtime-manifest.json"));
|
|
787
931
|
if (manifest && exists(manifest.binaryPath)) return manifest.binaryPath;
|
|
788
|
-
const legacy =
|
|
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
|
-
}).
|
|
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
|
-
|
|
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/
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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:
|
|
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
|
|
1970
|
+
import { execFile as execFile3 } from "child_process";
|
|
1279
1971
|
import os3 from "os";
|
|
1280
1972
|
import { promisify } from "util";
|
|
1281
|
-
import
|
|
1282
|
-
import
|
|
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
|
-
|
|
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(
|
|
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:
|
|
1419
|
-
{ client: "Claude Code (user)", path:
|
|
1420
|
-
{ client: "Claude Code (project)", path:
|
|
1421
|
-
{ client: "Cursor", path:
|
|
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
|
|
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
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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/
|
|
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
|
|
1686
|
-
import
|
|
1687
|
-
import
|
|
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
|
|
1693
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1786
|
-
const existing = await readModelManifest(
|
|
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 =
|
|
1793
|
-
await
|
|
1794
|
-
const tmpPath =
|
|
1795
|
-
await
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 ${
|
|
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(
|
|
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 =
|
|
1956
|
-
await
|
|
1957
|
-
const tmpArchive =
|
|
1958
|
-
await
|
|
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
|
|
2623
|
+
await fsp7.rm(tmpArchive, { force: true });
|
|
1962
2624
|
const binaryPath = await findServerBinary(tagDir, platform);
|
|
1963
|
-
if (platform !== "win32") await
|
|
1964
|
-
await writeRuntimeManifest(
|
|
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
|
|
2642
|
+
await fsp7.stat(dir);
|
|
1981
2643
|
} catch {
|
|
1982
2644
|
return false;
|
|
1983
2645
|
}
|
|
1984
|
-
await
|
|
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
|
|
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(`
|
|
2029
|
-
console.log(`
|
|
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
|
|
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
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
console.log(`
|
|
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,
|
|
2060
|
-
console.log(` Inference OK (${(probe.latencyMs / 1e3).toFixed(1)}s incl. model load
|
|
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)
|
|
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:
|
|
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
|
|
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
|
|
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) {
|