clef-mcp 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -142
- package/assets/demo.svg +70 -0
- package/dist/cli.js +987 -280
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +534 -52
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/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";
|
|
@@ -1170,40 +1515,375 @@ function validateStateSize(state, limits) {
|
|
|
1170
1515
|
}
|
|
1171
1516
|
}
|
|
1172
1517
|
|
|
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
|
-
|
|
1518
|
+
// src/mcp/prompts.ts
|
|
1519
|
+
init_errors();
|
|
1520
|
+
import { z as z2 } from "zod";
|
|
1521
|
+
var TOOL_GUIDE = (questions) => [
|
|
1522
|
+
"Call the `clef_decide` MCP tool once with all of the questions below (they are scored in a single forward pass).",
|
|
1523
|
+
"",
|
|
1524
|
+
"Rules for the call:",
|
|
1525
|
+
"- `state` must be a compact, factual JSON or string \u2014 relevant facts only, well under 1 MB.",
|
|
1526
|
+
"- Use the exact `questions` given below (ids, types, criteria). Do not invent extra options.",
|
|
1527
|
+
"- If the answer distribution is flat (no option above ~0.6), gather more context and re-ask once before falling back to asking the user.",
|
|
1528
|
+
"- Treat safety-adjacent decisions (destructive actions, security) as requiring p >= 0.8 before acting; otherwise prefer asking the user.",
|
|
1529
|
+
"- Report the result as a short summary plus the top probability \u2014 do not paste raw JSON unless asked.",
|
|
1530
|
+
"",
|
|
1531
|
+
"Questions:",
|
|
1532
|
+
questions
|
|
1533
|
+
].join("\n");
|
|
1534
|
+
var PROMPTS = [
|
|
1535
|
+
{
|
|
1536
|
+
name: "incident-triage",
|
|
1537
|
+
title: "Production incident triage",
|
|
1538
|
+
description: "Frame a production incident as clef_decide questions: immediate action, severity and blast radius.",
|
|
1539
|
+
args: {
|
|
1540
|
+
incident_description: "Short factual description of the incident (symptoms, scope, recent changes)."
|
|
1541
|
+
},
|
|
1542
|
+
build: (a) => TOOL_GUIDE(
|
|
1543
|
+
[
|
|
1544
|
+
'- "action" (choice, instructions "What should the responder do first?"): rollback "Revert the latest deploy", hotfix "Ship a targeted fix now", investigate "Gather diagnostics before acting", wait "Observe; impact is not growing".',
|
|
1545
|
+
'- "severity" (score, instructions "How severe is the incident?", criteria ["low", "elevated", "high", "critical"]).',
|
|
1546
|
+
'- "user_impact" (noul, instructions "Are users actively affected right now?").'
|
|
1547
|
+
].join("\n")
|
|
1548
|
+
) + "\n\nIncident description:\n" + (a.incident_description ?? "")
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
name: "next-action",
|
|
1552
|
+
title: "Agent next action",
|
|
1553
|
+
description: "Ask Clef what the coding agent should do next on a task: inspect, modify, test or ask the user.",
|
|
1554
|
+
args: {
|
|
1555
|
+
task_summary: "What the agent is working on, current step, and what is already known or done."
|
|
1556
|
+
},
|
|
1557
|
+
build: (a) => TOOL_GUIDE(
|
|
1558
|
+
[
|
|
1559
|
+
'- "next_action" (choice, instructions "What should the coding agent do next?"): inspect "Inspect the code and gather more information", modify "Modify the code", test "Run additional tests", ask_user "Ask the user for clarification".',
|
|
1560
|
+
'- "confidence" (score, instructions "How confident are you in this decision?", criteria ["very_low", "low", "medium", "high", "very_high"]).'
|
|
1561
|
+
].join("\n")
|
|
1562
|
+
) + "\n\nTask summary:\n" + (a.task_summary ?? "")
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
name: "ticket-routing",
|
|
1566
|
+
title: "Support ticket routing",
|
|
1567
|
+
description: "Classify an incoming message into the team that should handle it.",
|
|
1568
|
+
args: {
|
|
1569
|
+
ticket_text: "The customer or user message, verbatim if possible."
|
|
1570
|
+
},
|
|
1571
|
+
build: (a) => TOOL_GUIDE(
|
|
1572
|
+
[
|
|
1573
|
+
'- "department" (choice, instructions "Which team should handle the message?"): billing "Payments, invoices and refunds", technical "Bugs, errors and outages", sales "Pricing and new purchases", other "Anything else".',
|
|
1574
|
+
'- "urgent" (noul, instructions "Does this need a response within the hour?").'
|
|
1575
|
+
].join("\n")
|
|
1576
|
+
) + "\n\nTicket text:\n" + (a.ticket_text ?? "")
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
name: "security-review",
|
|
1580
|
+
title: "Security sanity check",
|
|
1581
|
+
description: "Ask Clef whether a code snippet, change or setup smells like a security problem.",
|
|
1582
|
+
args: {
|
|
1583
|
+
change_summary: "The code, config or setup to review \u2014 compact but complete enough to judge."
|
|
1584
|
+
},
|
|
1585
|
+
build: (a) => TOOL_GUIDE(
|
|
1586
|
+
[
|
|
1587
|
+
'- "is_vulnerable" (noul, instructions "Does this contain a realistic security vulnerability?").',
|
|
1588
|
+
'- "risk" (score, instructions "How bad is it if this ships as-is?", criteria ["informational", "low", "medium", "high", "critical"]).',
|
|
1589
|
+
'- "first_mitigation" (choice, instructions "What is the first mitigation?"): fix_code "Fix the code before merging", restrict "Restrict access or add guardrails", monitor "Add detection and monitoring", none "No mitigation needed".'
|
|
1590
|
+
].join("\n") + "\nSafety rule: only act without human review when p(is_vulnerable=false) >= 0.8."
|
|
1591
|
+
) + "\n\nChange to review:\n" + (a.change_summary ?? "")
|
|
1592
|
+
}
|
|
1593
|
+
];
|
|
1594
|
+
function registerPrompts(server) {
|
|
1595
|
+
for (const p of PROMPTS) {
|
|
1596
|
+
server.registerPrompt(
|
|
1597
|
+
p.name,
|
|
1598
|
+
{
|
|
1599
|
+
title: p.title,
|
|
1600
|
+
description: p.description,
|
|
1601
|
+
argsSchema: Object.fromEntries(Object.entries(p.args).map(([k, v]) => [k, z2.string().min(1).describe(v)]))
|
|
1602
|
+
},
|
|
1603
|
+
({ ...args }) => {
|
|
1604
|
+
for (const key of Object.keys(p.args)) {
|
|
1605
|
+
if (typeof args[key] !== "string" || !args[key]) {
|
|
1606
|
+
throw new ClefError(ClefErrorCode.INVALID_INPUT, `Prompt argument "${key}" is required`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return {
|
|
1610
|
+
messages: [{ role: "user", content: { type: "text", text: p.build(args) } }]
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/mcp/resources.ts
|
|
1618
|
+
import fsSync3 from "fs";
|
|
1619
|
+
import fsp4 from "fs/promises";
|
|
1620
|
+
|
|
1621
|
+
// src/cli/evals.ts
|
|
1622
|
+
init_errors();
|
|
1623
|
+
import fsp3 from "fs/promises";
|
|
1624
|
+
import fsSync2 from "fs";
|
|
1625
|
+
import { fileURLToPath } from "url";
|
|
1626
|
+
import path6 from "path";
|
|
1627
|
+
function defaultDatasetPath() {
|
|
1628
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
1629
|
+
for (let i = 0; i < 6; i++) {
|
|
1630
|
+
const candidate = path6.join(dir, "tests", "evals", "dataset.jsonl");
|
|
1631
|
+
if (fsSync2.existsSync(candidate)) return candidate;
|
|
1632
|
+
dir = path6.dirname(dir);
|
|
1633
|
+
}
|
|
1634
|
+
return fileURLToPath(new URL("../tests/evals/dataset.jsonl", import.meta.url));
|
|
1635
|
+
}
|
|
1636
|
+
function parseLine(line, lineNo) {
|
|
1637
|
+
const raw = JSON.parse(line);
|
|
1638
|
+
if (typeof raw.id !== "string" || !raw.question?.id || !raw.expected?.kind) {
|
|
1639
|
+
throw new ClefError(ClefErrorCode.INVALID_INPUT, `Eval dataset line ${lineNo} is missing id/question/expected`);
|
|
1640
|
+
}
|
|
1641
|
+
return raw;
|
|
1642
|
+
}
|
|
1643
|
+
function evaluate(answer, exp) {
|
|
1644
|
+
const prob = (key) => answer[key] ?? 0;
|
|
1645
|
+
switch (exp.kind) {
|
|
1646
|
+
case "argmax": {
|
|
1647
|
+
const keys = exp.among ?? Object.keys(answer);
|
|
1648
|
+
let best = "";
|
|
1649
|
+
let bestP = -1;
|
|
1650
|
+
for (const k of keys) {
|
|
1651
|
+
if (prob(k) > bestP) {
|
|
1652
|
+
bestP = prob(k);
|
|
1653
|
+
best = k;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return { pass: best === exp.expect, detail: `argmax=${best || "n/a"} (${bestP.toFixed(3)}), expected=${exp.expect}` };
|
|
1657
|
+
}
|
|
1658
|
+
case "prob_gt":
|
|
1659
|
+
return { pass: prob(exp.a) > prob(exp.b), detail: `p(${exp.a})=${prob(exp.a).toFixed(3)} vs p(${exp.b})=${prob(exp.b).toFixed(3)}` };
|
|
1660
|
+
case "min_prob": {
|
|
1661
|
+
const p = prob(exp.option);
|
|
1662
|
+
return { pass: p >= exp.min, detail: `p(${exp.option})=${p.toFixed(3)}, required >= ${exp.min}` };
|
|
1663
|
+
}
|
|
1664
|
+
case "top_mass": {
|
|
1665
|
+
const mass = exp.options.reduce((sum, k) => sum + prob(k), 0);
|
|
1666
|
+
return { pass: mass >= exp.min, detail: `mass(${exp.options.join("+")})=${mass.toFixed(3)}, required >= ${exp.min}` };
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
async function runEvals(opts) {
|
|
1671
|
+
const config = loadConfig();
|
|
1672
|
+
const log = createLogger(config.logLevel);
|
|
1673
|
+
const spec = getModelSpec(config.model);
|
|
1674
|
+
let mlxManifest;
|
|
1675
|
+
if (config.runtime === "mlx") {
|
|
1676
|
+
mlxManifest = await mlxIsInstalled(config.clefHome);
|
|
1677
|
+
if (!mlxManifest) {
|
|
1678
|
+
throw new ClefError(
|
|
1679
|
+
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1680
|
+
`Model "${spec.id}" is not installed for the MLX runtime.`,
|
|
1681
|
+
"Run `clef-mcp install --runtime mlx` first."
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const manifest = config.runtime === "mlx" ? void 0 : await findInstalledManifest(config.clefHome, spec);
|
|
1686
|
+
if (!manifest && !mlxManifest) {
|
|
1687
|
+
throw new ClefError(
|
|
1688
|
+
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1689
|
+
`Model "${spec.id}" is not installed; evals need the local model.`,
|
|
1690
|
+
"Run `clef-mcp install` first."
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
const datasetPath = opts.dataset ?? defaultDatasetPath();
|
|
1694
|
+
const content = await fsp3.readFile(datasetPath, "utf8");
|
|
1695
|
+
let cases = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l, i) => parseLine(l, i + 1));
|
|
1696
|
+
if (opts.limit && opts.limit > 0) cases = cases.slice(0, opts.limit);
|
|
1697
|
+
if (cases.length === 0) {
|
|
1698
|
+
throw new ClefError(ClefErrorCode.INVALID_INPUT, `No eval cases found in ${datasetPath}`);
|
|
1699
|
+
}
|
|
1700
|
+
const binary = resolveLlamaServerBinary(config);
|
|
1701
|
+
const runtime = config.runtime === "mlx" ? new MlxRuntime({ uvBin: resolveUv(process.env.CLEF_MLX_UV), snapshotPath: mlxManifest.snapshotPath, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
|
|
1702
|
+
const runtimeLabel = config.runtime === "mlx" ? "MLX" : manifest.quant;
|
|
1703
|
+
console.log(`Running ${cases.length} eval case(s) with ${spec.id} ${runtimeLabel}...
|
|
1704
|
+
`);
|
|
1705
|
+
await runtime.load();
|
|
1706
|
+
let passed = 0;
|
|
1707
|
+
const results = [];
|
|
1708
|
+
try {
|
|
1709
|
+
for (const [i, c] of cases.entries()) {
|
|
1710
|
+
process.stderr.write(`\r case ${i + 1}/${cases.length} \u2014 ${c.id}`);
|
|
1711
|
+
try {
|
|
1712
|
+
const request = toSystemOneRequest({ state: c.state, questions: { [c.question.id]: c.question } }, spec.id);
|
|
1713
|
+
const response = await runtime.decide(request);
|
|
1714
|
+
const output = fromSystemOneResponse(response, { [c.question.id]: c.question }, spec.id);
|
|
1715
|
+
const answer = output.decisions[c.question.id]?.answer ?? {};
|
|
1716
|
+
const verdict = evaluate(answer, c.expected);
|
|
1717
|
+
if (verdict.pass) passed++;
|
|
1718
|
+
results.push({ id: c.id, category: c.category, ok: verdict.pass, detail: verdict.detail });
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
results.push({
|
|
1721
|
+
id: c.id,
|
|
1722
|
+
category: c.category,
|
|
1723
|
+
ok: false,
|
|
1724
|
+
detail: `error: ${err instanceof Error ? err.message : String(err)}`
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
} finally {
|
|
1729
|
+
process.stderr.write("\n");
|
|
1730
|
+
await runtime.unload();
|
|
1731
|
+
}
|
|
1732
|
+
console.log("");
|
|
1733
|
+
for (const r of results) {
|
|
1734
|
+
console.log(`${r.ok ? "\u2714" : "\u2718"} [${r.category}] ${r.id} \u2014 ${r.detail}`);
|
|
1735
|
+
}
|
|
1736
|
+
const rate = passed / results.length;
|
|
1737
|
+
const minRate = opts.minRate ?? 0.8;
|
|
1738
|
+
console.log(`
|
|
1739
|
+
${passed}/${results.length} passed (${(rate * 100).toFixed(1)}%), required >= ${(minRate * 100).toFixed(0)}%`);
|
|
1740
|
+
return rate >= minRate;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// src/mcp/resources.ts
|
|
1744
|
+
var EVALS_SCHEMA_MD = `# clef-mcp eval dataset format
|
|
1745
|
+
|
|
1746
|
+
JSONL: one JSON object per line.
|
|
1747
|
+
|
|
1748
|
+
{
|
|
1749
|
+
"id": "unique-case-id",
|
|
1750
|
+
"category": "coding | security | classification | routing | yes_no",
|
|
1751
|
+
"state": <string | JSON value>,
|
|
1752
|
+
"question": {
|
|
1753
|
+
"id": "question-id",
|
|
1754
|
+
"type": "choice | score | noul",
|
|
1755
|
+
"instructions": "\u2026",
|
|
1756
|
+
"criteria": { "option": "description" } // choice (or ["a","b"] list)
|
|
1757
|
+
| ["low", "high"] // score, ordered
|
|
1758
|
+
},
|
|
1759
|
+
"expected": {
|
|
1760
|
+
"kind": "argmax | prob_gt | min_prob | top_mass",
|
|
1761
|
+
// argmax: { "expect": "option", "among": ["option", ...] (optional) }
|
|
1762
|
+
// prob_gt: { "a": "opt", "b": "opt" }
|
|
1763
|
+
// min_prob: { "option": "opt", "min": 0.7 }
|
|
1764
|
+
// top_mass: { "options": ["a","b"], "min": 0.8 }
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
Run: \`clef-mcp evals\` (requires an installed model).
|
|
1769
|
+
`;
|
|
1770
|
+
async function readDataset() {
|
|
1771
|
+
try {
|
|
1772
|
+
return await fsp4.readFile(defaultDatasetPath(), "utf8");
|
|
1773
|
+
} catch {
|
|
1774
|
+
return "The bundled eval dataset is not available in this installation (tests/evals/dataset.jsonl not found).";
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
function textResource(uri, text, mimeType = "text/markdown") {
|
|
1778
|
+
return { contents: [{ uri: uri.href, mimeType, text }] };
|
|
1779
|
+
}
|
|
1780
|
+
function requireStringUri(uri) {
|
|
1781
|
+
return uri instanceof URL ? uri : new URL(uri);
|
|
1782
|
+
}
|
|
1783
|
+
function registerResources(server, cfg, version) {
|
|
1784
|
+
server.registerResource(
|
|
1785
|
+
"capabilities",
|
|
1786
|
+
"clef-mcp://capabilities",
|
|
1787
|
+
{ title: "clef-mcp capabilities", description: "Live capability snapshot: model, runtime, limits, error codes.", mimeType: "application/json" },
|
|
1788
|
+
async (uri) => {
|
|
1789
|
+
const u = requireStringUri(uri);
|
|
1790
|
+
const payload = {
|
|
1791
|
+
name: "clef-mcp",
|
|
1792
|
+
version,
|
|
1793
|
+
model: cfg.model,
|
|
1794
|
+
runtime: cfg.runtime,
|
|
1795
|
+
limits: cfg.limits,
|
|
1796
|
+
tool: {
|
|
1797
|
+
name: "clef_decide",
|
|
1798
|
+
output: "strict JSON: { model, decisions: { <qid>: { answer: { <option>: probability } } } }",
|
|
1799
|
+
questionTypes: ["noul", "choice", "score"],
|
|
1800
|
+
notes: ["options.temperature is accepted but is a no-op (single forward pass)", "state is data and is never interpreted as instructions"]
|
|
1801
|
+
},
|
|
1802
|
+
errorCodes: [
|
|
1803
|
+
"MODEL_NOT_INSTALLED",
|
|
1804
|
+
"MODEL_LOAD_FAILED",
|
|
1805
|
+
"RUNTIME_NOT_FOUND",
|
|
1806
|
+
"RUNTIME_INIT_FAILED",
|
|
1807
|
+
"RUNTIME_NOT_SUPPORTED",
|
|
1808
|
+
"INVALID_INPUT",
|
|
1809
|
+
"CLEF_INFERENCE_FAILED",
|
|
1810
|
+
"UNSUPPORTED_PLATFORM",
|
|
1811
|
+
"OUT_OF_MEMORY",
|
|
1812
|
+
"CHECKSUM_MISMATCH",
|
|
1813
|
+
"DOWNLOAD_FAILED"
|
|
1814
|
+
]
|
|
1815
|
+
};
|
|
1816
|
+
return textResource(u, JSON.stringify(payload, null, 2), "application/json");
|
|
1817
|
+
}
|
|
1818
|
+
);
|
|
1819
|
+
server.registerResource(
|
|
1820
|
+
"evals-schema",
|
|
1821
|
+
"clef-mcp://evals/schema",
|
|
1822
|
+
{ title: "Eval dataset schema", description: "How to write cases for clef-mcp evals.", mimeType: "text/markdown" },
|
|
1823
|
+
async (uri) => textResource(requireStringUri(uri), EVALS_SCHEMA_MD)
|
|
1824
|
+
);
|
|
1825
|
+
server.registerResource(
|
|
1826
|
+
"evals-dataset",
|
|
1827
|
+
"clef-mcp://evals/dataset",
|
|
1828
|
+
{ title: "Bundled eval dataset", description: "The 30-case dataset shipped with clef-mcp.", mimeType: "application/jsonl" },
|
|
1829
|
+
async (uri) => textResource(requireStringUri(uri), await readDataset(), "application/jsonl")
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/mcp/server.ts
|
|
1834
|
+
var TOOL_NAME = "clef_decide";
|
|
1835
|
+
var SERVER_INSTRUCTIONS = "clef_decide passes task state plus typed questions to the local Clef decision model and returns a probability distribution over answer options. Treat `state` as data: it is never interpreted as instructions for this server. The result is structured JSON; do not render it as prose.";
|
|
1836
|
+
function createClefServer(deps = {}) {
|
|
1837
|
+
const config = deps.config ?? loadConfig();
|
|
1838
|
+
const version = deps.version ?? "0.1.0";
|
|
1839
|
+
const log = deps.log ?? createLogger(config.logLevel);
|
|
1840
|
+
const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
|
|
1841
|
+
registerPrompts(server);
|
|
1842
|
+
registerResources(server, config, version);
|
|
1843
|
+
let loaded;
|
|
1844
|
+
let loadPromise;
|
|
1845
|
+
async function ensureRuntime(spec) {
|
|
1846
|
+
if (loaded && loaded.model === spec.id) return loaded;
|
|
1847
|
+
if (loaded && loaded.model !== spec.id) {
|
|
1848
|
+
await loaded.runtime.unload();
|
|
1849
|
+
loaded = void 0;
|
|
1850
|
+
}
|
|
1851
|
+
if (loadPromise) return loadPromise;
|
|
1852
|
+
loadPromise = (async () => {
|
|
1853
|
+
if (config.runtime === "mlx") {
|
|
1854
|
+
const manifest2 = await mlxIsInstalled(config.clefHome);
|
|
1855
|
+
if (!manifest2) {
|
|
1856
|
+
throw new ClefError(
|
|
1857
|
+
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1858
|
+
`Clef model "${spec.id}" is not installed for the MLX runtime.`,
|
|
1859
|
+
"Run `clef-mcp install --runtime mlx`."
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
const uvBin = resolveUv(process.env.CLEF_MLX_UV);
|
|
1863
|
+
const runtime2 = deps.runtimeFactory ? deps.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log });
|
|
1864
|
+
log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
|
|
1865
|
+
const loadStarted2 = Date.now();
|
|
1866
|
+
await runtime2.load();
|
|
1867
|
+
log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
|
|
1868
|
+
return { runtime: runtime2, model: spec.id };
|
|
1869
|
+
}
|
|
1870
|
+
const manifest = await findInstalledManifest(config.clefHome, spec);
|
|
1871
|
+
if (!manifest) {
|
|
1872
|
+
throw new ClefError(
|
|
1873
|
+
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1874
|
+
`Clef model "${spec.id}" is not installed.`,
|
|
1875
|
+
"Run `clef-mcp install`."
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
const binary = resolveLlamaServerBinary(config);
|
|
1879
|
+
const runtime = deps.runtimeFactory ? deps.runtimeFactory({ kind: "llama-cpp", serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
|
|
1880
|
+
log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
|
|
1881
|
+
const loadStarted = Date.now();
|
|
1882
|
+
await runtime.load();
|
|
1883
|
+
log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
|
|
1884
|
+
return { runtime, model: spec.id };
|
|
1885
|
+
})();
|
|
1886
|
+
try {
|
|
1207
1887
|
loaded = await loadPromise;
|
|
1208
1888
|
return loaded;
|
|
1209
1889
|
} finally {
|
|
@@ -1275,19 +1955,19 @@ import { Command } from "commander";
|
|
|
1275
1955
|
|
|
1276
1956
|
// src/cli/doctor.ts
|
|
1277
1957
|
init_errors();
|
|
1278
|
-
import { execFile as
|
|
1958
|
+
import { execFile as execFile3 } from "child_process";
|
|
1279
1959
|
import os3 from "os";
|
|
1280
1960
|
import { promisify } from "util";
|
|
1281
|
-
import
|
|
1282
|
-
import
|
|
1961
|
+
import fsp5 from "fs/promises";
|
|
1962
|
+
import path7 from "path";
|
|
1283
1963
|
|
|
1284
1964
|
// src/models/platform.ts
|
|
1285
1965
|
init_errors();
|
|
1286
|
-
import { execFile } from "child_process";
|
|
1966
|
+
import { execFile as execFile2 } from "child_process";
|
|
1287
1967
|
import os2 from "os";
|
|
1288
1968
|
function exec(cmd, args, timeoutMs) {
|
|
1289
1969
|
return new Promise((resolve, reject) => {
|
|
1290
|
-
|
|
1970
|
+
execFile2(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
|
|
1291
1971
|
if (err) reject(err);
|
|
1292
1972
|
else resolve(stdout);
|
|
1293
1973
|
});
|
|
@@ -1393,9 +2073,27 @@ async function probeInference(cfg, manifest, log, loadTimeoutMs = 12e4) {
|
|
|
1393
2073
|
}
|
|
1394
2074
|
return { latencyMs: Date.now() - started, binarySource: binary.source, binaryPath: binary.path };
|
|
1395
2075
|
}
|
|
2076
|
+
async function probeMlxInference(cfg, manifest, log, loadTimeoutMs = 18e4) {
|
|
2077
|
+
const uvBin = resolveUv(process.env.CLEF_MLX_UV);
|
|
2078
|
+
const runtime = new MlxRuntime({ uvBin, snapshotPath: manifest.snapshotPath, loadTimeoutMs, log });
|
|
2079
|
+
const started = Date.now();
|
|
2080
|
+
await runtime.load();
|
|
2081
|
+
try {
|
|
2082
|
+
await runtime.decide({
|
|
2083
|
+
model: manifest.model,
|
|
2084
|
+
state: "This is an internal clef-mcp installation probe.",
|
|
2085
|
+
questions: {
|
|
2086
|
+
probe: { type: "noul", instructions: "Is this a clef-mcp installation probe?" }
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
} finally {
|
|
2090
|
+
await runtime.unload();
|
|
2091
|
+
}
|
|
2092
|
+
return { latencyMs: Date.now() - started, binarySource: "uv", binaryPath: uvBin };
|
|
2093
|
+
}
|
|
1396
2094
|
|
|
1397
2095
|
// src/cli/doctor.ts
|
|
1398
|
-
var execFileP = promisify(
|
|
2096
|
+
var execFileP = promisify(execFile3);
|
|
1399
2097
|
function mark(ok) {
|
|
1400
2098
|
return ok ? "\u2714" : "\u2718";
|
|
1401
2099
|
}
|
|
@@ -1415,14 +2113,14 @@ async function checkBinaryVersion(binaryPath) {
|
|
|
1415
2113
|
async function detectMcpConfigs() {
|
|
1416
2114
|
const home = os3.homedir();
|
|
1417
2115
|
const hints = [
|
|
1418
|
-
{ client: "Codex", path:
|
|
1419
|
-
{ client: "Claude Code (user)", path:
|
|
1420
|
-
{ client: "Claude Code (project)", path:
|
|
1421
|
-
{ client: "Cursor", path:
|
|
2116
|
+
{ client: "Codex", path: path7.join(home, ".codex", "config.toml"), configured: false },
|
|
2117
|
+
{ client: "Claude Code (user)", path: path7.join(home, ".claude.json"), configured: false },
|
|
2118
|
+
{ client: "Claude Code (project)", path: path7.resolve(".mcp.json"), configured: false },
|
|
2119
|
+
{ client: "Cursor", path: path7.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
|
|
1422
2120
|
];
|
|
1423
2121
|
for (const hint of hints) {
|
|
1424
2122
|
try {
|
|
1425
|
-
const content = await
|
|
2123
|
+
const content = await fsp5.readFile(hint.path, "utf8");
|
|
1426
2124
|
hint.configured = content.includes("clef-mcp");
|
|
1427
2125
|
} catch {
|
|
1428
2126
|
hint.configured = false;
|
|
@@ -1461,33 +2159,75 @@ async function runDoctor(opts) {
|
|
|
1461
2159
|
detail: info.gpu?.name ? `${info.gpu.name}${info.gpu.vramBytes ? ` (${formatBytes(info.gpu.vramBytes)})` : ""}` : "not detected (llama.cpp falls back to CPU; on Apple Silicon Metal is built in)"
|
|
1462
2160
|
});
|
|
1463
2161
|
let binaryPath;
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
2162
|
+
if (config.runtime === "mlx") {
|
|
2163
|
+
try {
|
|
2164
|
+
binaryPath = resolveUv(process.env.CLEF_MLX_UV);
|
|
2165
|
+
checks.push({
|
|
2166
|
+
name: "Runtime (uv, for the MLX runtime)",
|
|
2167
|
+
ok: true,
|
|
2168
|
+
detail: binaryPath
|
|
2169
|
+
});
|
|
2170
|
+
} catch (err) {
|
|
2171
|
+
checks.push({
|
|
2172
|
+
name: "Runtime (uv, for the MLX runtime)",
|
|
2173
|
+
ok: false,
|
|
2174
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
2175
|
+
hint: "Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
} else {
|
|
2179
|
+
try {
|
|
2180
|
+
const binary = resolveLlamaServerBinary(config);
|
|
2181
|
+
binaryPath = binary.path;
|
|
2182
|
+
const version = await checkBinaryVersion(binary.path);
|
|
2183
|
+
checks.push({
|
|
2184
|
+
name: "Runtime binary",
|
|
2185
|
+
ok: version !== void 0,
|
|
2186
|
+
detail: `${binary.source}: ${binary.path}${version ? ` (${version})` : ""}`,
|
|
2187
|
+
hint: version === void 0 ? "The binary exists but did not answer --version; it may be broken or incompatible." : void 0
|
|
2188
|
+
});
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
binaryPath = void 0;
|
|
2191
|
+
checks.push({
|
|
2192
|
+
name: "Runtime binary",
|
|
2193
|
+
ok: false,
|
|
2194
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
2195
|
+
hint: err instanceof ClefError ? err.hint : void 0
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
1481
2198
|
}
|
|
1482
2199
|
let spec;
|
|
1483
2200
|
let manifest;
|
|
2201
|
+
let mlxManifest;
|
|
1484
2202
|
try {
|
|
1485
2203
|
spec = getModelSpec(config.model);
|
|
1486
|
-
|
|
2204
|
+
if (config.runtime === "mlx") mlxManifest = await mlxIsInstalled(config.clefHome);
|
|
2205
|
+
else manifest = await findInstalledManifest(config.clefHome, spec);
|
|
1487
2206
|
} catch {
|
|
1488
2207
|
checks.push({ name: "Model", ok: false, detail: `unknown model id "${config.model}"` });
|
|
1489
2208
|
}
|
|
1490
|
-
if (
|
|
2209
|
+
if (config.runtime === "mlx") {
|
|
2210
|
+
if (!mlxManifest) {
|
|
2211
|
+
checks.push({
|
|
2212
|
+
name: "Model",
|
|
2213
|
+
ok: false,
|
|
2214
|
+
detail: `${spec?.id ?? config.model} is not installed for the MLX runtime`,
|
|
2215
|
+
hint: "Run `clef-mcp install --runtime mlx`."
|
|
2216
|
+
});
|
|
2217
|
+
} else {
|
|
2218
|
+
checks.push({
|
|
2219
|
+
name: "Model",
|
|
2220
|
+
ok: true,
|
|
2221
|
+
detail: `MLX snapshot at ${mlxManifest.snapshotPath}`
|
|
2222
|
+
});
|
|
2223
|
+
checks.push({
|
|
2224
|
+
name: "Model license",
|
|
2225
|
+
ok: true,
|
|
2226
|
+
informational: true,
|
|
2227
|
+
detail: `${mlxManifest.licenseId} \u2014 ${mlxManifest.licenseUrl}`
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
} else if (spec) {
|
|
1491
2231
|
if (!manifest) {
|
|
1492
2232
|
checks.push({
|
|
1493
2233
|
name: "Model",
|
|
@@ -1520,7 +2260,24 @@ async function runDoctor(opts) {
|
|
|
1520
2260
|
});
|
|
1521
2261
|
}
|
|
1522
2262
|
}
|
|
1523
|
-
if (
|
|
2263
|
+
if (config.runtime === "mlx" && binaryPath && mlxManifest) {
|
|
2264
|
+
try {
|
|
2265
|
+
const probe = await probeMlxInference(config, mlxManifest, void 0, 24e4);
|
|
2266
|
+
checks.push({
|
|
2267
|
+
name: "Inference",
|
|
2268
|
+
ok: true,
|
|
2269
|
+
detail: `probe decision returned in ${(probe.latencyMs / 1e3).toFixed(1)}s (incl. model load)`
|
|
2270
|
+
});
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
const clefErr = err instanceof ClefError ? err : void 0;
|
|
2273
|
+
checks.push({
|
|
2274
|
+
name: "Inference",
|
|
2275
|
+
ok: false,
|
|
2276
|
+
detail: clefErr?.message ?? (err instanceof Error ? err.message : String(err)),
|
|
2277
|
+
hint: clefErr?.hint
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
} else if (config.runtime === "llama-cpp" && binaryPath && manifest && (await verifyModelFiles(manifest)).ok) {
|
|
1524
2281
|
try {
|
|
1525
2282
|
const probe = await probeInference(config, manifest, void 0, 18e4);
|
|
1526
2283
|
checks.push({
|
|
@@ -1570,127 +2327,20 @@ ${failed.length} critical check(s) failed.`
|
|
|
1570
2327
|
return failed.length > 0;
|
|
1571
2328
|
}
|
|
1572
2329
|
|
|
1573
|
-
// src/cli/
|
|
2330
|
+
// src/cli/install.ts
|
|
1574
2331
|
init_errors();
|
|
1575
|
-
import fsp3 from "fs/promises";
|
|
1576
|
-
import fsSync from "fs";
|
|
1577
|
-
import { fileURLToPath } from "url";
|
|
1578
|
-
import path6 from "path";
|
|
1579
|
-
function defaultDatasetPath() {
|
|
1580
|
-
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
1581
|
-
for (let i = 0; i < 6; i++) {
|
|
1582
|
-
const candidate = path6.join(dir, "tests", "evals", "dataset.jsonl");
|
|
1583
|
-
if (fsSync.existsSync(candidate)) return candidate;
|
|
1584
|
-
dir = path6.dirname(dir);
|
|
1585
|
-
}
|
|
1586
|
-
return fileURLToPath(new URL("../tests/evals/dataset.jsonl", import.meta.url));
|
|
1587
|
-
}
|
|
1588
|
-
function parseLine(line, lineNo) {
|
|
1589
|
-
const raw = JSON.parse(line);
|
|
1590
|
-
if (typeof raw.id !== "string" || !raw.question?.id || !raw.expected?.kind) {
|
|
1591
|
-
throw new ClefError(ClefErrorCode.INVALID_INPUT, `Eval dataset line ${lineNo} is missing id/question/expected`);
|
|
1592
|
-
}
|
|
1593
|
-
return raw;
|
|
1594
|
-
}
|
|
1595
|
-
function evaluate(answer, exp) {
|
|
1596
|
-
const prob = (key) => answer[key] ?? 0;
|
|
1597
|
-
switch (exp.kind) {
|
|
1598
|
-
case "argmax": {
|
|
1599
|
-
const keys = exp.among ?? Object.keys(answer);
|
|
1600
|
-
let best = "";
|
|
1601
|
-
let bestP = -1;
|
|
1602
|
-
for (const k of keys) {
|
|
1603
|
-
if (prob(k) > bestP) {
|
|
1604
|
-
bestP = prob(k);
|
|
1605
|
-
best = k;
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
return { pass: best === exp.expect, detail: `argmax=${best || "n/a"} (${bestP.toFixed(3)}), expected=${exp.expect}` };
|
|
1609
|
-
}
|
|
1610
|
-
case "prob_gt":
|
|
1611
|
-
return { pass: prob(exp.a) > prob(exp.b), detail: `p(${exp.a})=${prob(exp.a).toFixed(3)} vs p(${exp.b})=${prob(exp.b).toFixed(3)}` };
|
|
1612
|
-
case "min_prob": {
|
|
1613
|
-
const p = prob(exp.option);
|
|
1614
|
-
return { pass: p >= exp.min, detail: `p(${exp.option})=${p.toFixed(3)}, required >= ${exp.min}` };
|
|
1615
|
-
}
|
|
1616
|
-
case "top_mass": {
|
|
1617
|
-
const mass = exp.options.reduce((sum, k) => sum + prob(k), 0);
|
|
1618
|
-
return { pass: mass >= exp.min, detail: `mass(${exp.options.join("+")})=${mass.toFixed(3)}, required >= ${exp.min}` };
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
async function runEvals(opts) {
|
|
1623
|
-
const config = loadConfig();
|
|
1624
|
-
const log = createLogger(config.logLevel);
|
|
1625
|
-
const spec = getModelSpec(config.model);
|
|
1626
|
-
const manifest = await findInstalledManifest(config.clefHome, spec);
|
|
1627
|
-
if (!manifest) {
|
|
1628
|
-
throw new ClefError(
|
|
1629
|
-
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1630
|
-
`Model "${spec.id}" is not installed; evals need the local model.`,
|
|
1631
|
-
"Run `clef-mcp install` first."
|
|
1632
|
-
);
|
|
1633
|
-
}
|
|
1634
|
-
const datasetPath = opts.dataset ?? defaultDatasetPath();
|
|
1635
|
-
const content = await fsp3.readFile(datasetPath, "utf8");
|
|
1636
|
-
let cases = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l, i) => parseLine(l, i + 1));
|
|
1637
|
-
if (opts.limit && opts.limit > 0) cases = cases.slice(0, opts.limit);
|
|
1638
|
-
if (cases.length === 0) {
|
|
1639
|
-
throw new ClefError(ClefErrorCode.INVALID_INPUT, `No eval cases found in ${datasetPath}`);
|
|
1640
|
-
}
|
|
1641
|
-
const binary = resolveLlamaServerBinary(config);
|
|
1642
|
-
const runtime = new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
|
|
1643
|
-
console.log(`Running ${cases.length} eval case(s) with ${spec.id} ${manifest.quant}...
|
|
1644
|
-
`);
|
|
1645
|
-
await runtime.load();
|
|
1646
|
-
let passed = 0;
|
|
1647
|
-
const results = [];
|
|
1648
|
-
try {
|
|
1649
|
-
for (const [i, c] of cases.entries()) {
|
|
1650
|
-
process.stderr.write(`\r case ${i + 1}/${cases.length} \u2014 ${c.id}`);
|
|
1651
|
-
try {
|
|
1652
|
-
const request = toSystemOneRequest({ state: c.state, questions: { [c.question.id]: c.question } }, spec.id);
|
|
1653
|
-
const response = await runtime.decide(request);
|
|
1654
|
-
const output = fromSystemOneResponse(response, { [c.question.id]: c.question }, spec.id);
|
|
1655
|
-
const answer = output.decisions[c.question.id]?.answer ?? {};
|
|
1656
|
-
const verdict = evaluate(answer, c.expected);
|
|
1657
|
-
if (verdict.pass) passed++;
|
|
1658
|
-
results.push({ id: c.id, category: c.category, ok: verdict.pass, detail: verdict.detail });
|
|
1659
|
-
} catch (err) {
|
|
1660
|
-
results.push({
|
|
1661
|
-
id: c.id,
|
|
1662
|
-
category: c.category,
|
|
1663
|
-
ok: false,
|
|
1664
|
-
detail: `error: ${err instanceof Error ? err.message : String(err)}`
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
} finally {
|
|
1669
|
-
process.stderr.write("\n");
|
|
1670
|
-
await runtime.unload();
|
|
1671
|
-
}
|
|
1672
|
-
console.log("");
|
|
1673
|
-
for (const r of results) {
|
|
1674
|
-
console.log(`${r.ok ? "\u2714" : "\u2718"} [${r.category}] ${r.id} \u2014 ${r.detail}`);
|
|
1675
|
-
}
|
|
1676
|
-
const rate = passed / results.length;
|
|
1677
|
-
const minRate = opts.minRate ?? 0.8;
|
|
1678
|
-
console.log(`
|
|
1679
|
-
${passed}/${results.length} passed (${(rate * 100).toFixed(1)}%), required >= ${(minRate * 100).toFixed(0)}%`);
|
|
1680
|
-
return rate >= minRate;
|
|
1681
|
-
}
|
|
1682
2332
|
|
|
1683
2333
|
// src/models/llama-install.ts
|
|
1684
2334
|
init_errors();
|
|
1685
|
-
import { execFile as
|
|
1686
|
-
import
|
|
1687
|
-
import
|
|
2335
|
+
import { execFile as execFile4 } from "child_process";
|
|
2336
|
+
import fsp7 from "fs/promises";
|
|
2337
|
+
import path9 from "path";
|
|
1688
2338
|
|
|
1689
2339
|
// src/models/hf.ts
|
|
1690
2340
|
init_errors();
|
|
1691
2341
|
import { createHash as createHash2 } from "crypto";
|
|
1692
|
-
import
|
|
1693
|
-
import
|
|
2342
|
+
import fsp6 from "fs/promises";
|
|
2343
|
+
import path8 from "path";
|
|
1694
2344
|
var HF_BASE_URL = "https://huggingface.co";
|
|
1695
2345
|
function resolveUrl(repo, revision, file) {
|
|
1696
2346
|
return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
|
|
@@ -1748,7 +2398,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
1748
2398
|
}
|
|
1749
2399
|
const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
|
|
1750
2400
|
const hash = createHash2("sha256");
|
|
1751
|
-
const handle = await
|
|
2401
|
+
const handle = await fsp6.open(dest, "w");
|
|
1752
2402
|
let bytes = 0;
|
|
1753
2403
|
try {
|
|
1754
2404
|
for await (const chunk of res.body) {
|
|
@@ -1763,7 +2413,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
1763
2413
|
}
|
|
1764
2414
|
const sha256 = hash.digest("hex");
|
|
1765
2415
|
if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
|
|
1766
|
-
await
|
|
2416
|
+
await fsp6.rm(dest, { force: true });
|
|
1767
2417
|
throw new ClefError(
|
|
1768
2418
|
ClefErrorCode.CHECKSUM_MISMATCH,
|
|
1769
2419
|
`Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
|
|
@@ -1771,7 +2421,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
1771
2421
|
);
|
|
1772
2422
|
}
|
|
1773
2423
|
if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
|
|
1774
|
-
await
|
|
2424
|
+
await fsp6.rm(dest, { force: true });
|
|
1775
2425
|
throw new ClefError(
|
|
1776
2426
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
1777
2427
|
`Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
|
|
@@ -1782,24 +2432,24 @@ async function downloadOnce(url, dest, opts) {
|
|
|
1782
2432
|
async function installModel(opts) {
|
|
1783
2433
|
const { modelSpec, quantSpec } = opts;
|
|
1784
2434
|
const { modelsDir } = pathsFor(opts.clefHome);
|
|
1785
|
-
const
|
|
1786
|
-
const existing = await readModelManifest(
|
|
2435
|
+
const manifestPath2 = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
|
|
2436
|
+
const existing = await readModelManifest(manifestPath2);
|
|
1787
2437
|
if (existing && (await verifyModelFiles(existing)).ok) {
|
|
1788
2438
|
opts.log?.debug("model already installed", { model: modelSpec.id, quant: quantSpec.id });
|
|
1789
2439
|
return { manifest: existing, alreadyInstalled: true };
|
|
1790
2440
|
}
|
|
1791
2441
|
const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
|
|
1792
|
-
const finalPath =
|
|
1793
|
-
await
|
|
1794
|
-
const tmpPath =
|
|
1795
|
-
await
|
|
2442
|
+
const finalPath = path8.join(path8.dirname(manifestPath2), quantSpec.file);
|
|
2443
|
+
await fsp6.mkdir(path8.dirname(finalPath), { recursive: true });
|
|
2444
|
+
const tmpPath = path8.join(path8.dirname(finalPath), `.download-${quantSpec.file}`);
|
|
2445
|
+
await fsp6.rm(tmpPath, { force: true });
|
|
1796
2446
|
opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
|
|
1797
2447
|
const result = await downloadFile(info.downloadUrl, tmpPath, {
|
|
1798
2448
|
expectedSha256: info.sha256,
|
|
1799
2449
|
expectedSize: info.size ?? quantSpec.bytes,
|
|
1800
2450
|
onProgress: opts.onProgress
|
|
1801
2451
|
});
|
|
1802
|
-
await
|
|
2452
|
+
await fsp6.rename(tmpPath, finalPath);
|
|
1803
2453
|
const manifest = {
|
|
1804
2454
|
schemaVersion: 1,
|
|
1805
2455
|
model: modelSpec.id,
|
|
@@ -1815,7 +2465,7 @@ async function installModel(opts) {
|
|
|
1815
2465
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1816
2466
|
clefMcpVersion: opts.clefMcpVersion
|
|
1817
2467
|
};
|
|
1818
|
-
await writeModelManifest(
|
|
2468
|
+
await writeModelManifest(manifestPath2, manifest);
|
|
1819
2469
|
opts.log?.info("model installed", { model: modelSpec.id, quant: quantSpec.id, sha256: result.sha256 });
|
|
1820
2470
|
return { manifest, alreadyInstalled: false };
|
|
1821
2471
|
}
|
|
@@ -1874,7 +2524,7 @@ function pickAsset(release, platform, arch) {
|
|
|
1874
2524
|
}
|
|
1875
2525
|
function execTar(args, cwd) {
|
|
1876
2526
|
return new Promise((resolve, reject) => {
|
|
1877
|
-
|
|
2527
|
+
execFile4("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
|
|
1878
2528
|
if (err) reject(new Error(`tar ${args.join(" ")} failed: ${stderr || err.message}`));
|
|
1879
2529
|
else resolve();
|
|
1880
2530
|
});
|
|
@@ -1887,12 +2537,12 @@ async function findServerBinary(dir, platform) {
|
|
|
1887
2537
|
const current = queue.shift();
|
|
1888
2538
|
let entries;
|
|
1889
2539
|
try {
|
|
1890
|
-
entries = await
|
|
2540
|
+
entries = await fsp7.readdir(current, { withFileTypes: true });
|
|
1891
2541
|
} catch {
|
|
1892
2542
|
continue;
|
|
1893
2543
|
}
|
|
1894
2544
|
for (const entry of entries) {
|
|
1895
|
-
const full =
|
|
2545
|
+
const full = path9.join(current, entry.name);
|
|
1896
2546
|
if (entry.isDirectory()) queue.push(full);
|
|
1897
2547
|
else if (entry.name === target) return full;
|
|
1898
2548
|
}
|
|
@@ -1903,14 +2553,14 @@ async function findServerBinary(dir, platform) {
|
|
|
1903
2553
|
);
|
|
1904
2554
|
}
|
|
1905
2555
|
async function extractRelease(archivePath, destDir, platform) {
|
|
1906
|
-
await
|
|
2556
|
+
await fsp7.mkdir(destDir, { recursive: true });
|
|
1907
2557
|
const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
|
|
1908
2558
|
try {
|
|
1909
2559
|
await execTar(args, destDir);
|
|
1910
2560
|
} catch (err) {
|
|
1911
2561
|
throw new ClefError(
|
|
1912
2562
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
1913
|
-
`Could not extract ${
|
|
2563
|
+
`Could not extract ${path9.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1914
2564
|
"Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
|
|
1915
2565
|
);
|
|
1916
2566
|
}
|
|
@@ -1920,7 +2570,7 @@ async function ensureLlamaRuntime(cfg, log) {
|
|
|
1920
2570
|
const resolved = resolveLlamaServerBinary(cfg);
|
|
1921
2571
|
if (resolved.source === "managed") {
|
|
1922
2572
|
const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
|
|
1923
|
-
const manifest = readRuntimeManifest(
|
|
2573
|
+
const manifest = readRuntimeManifest(path9.join(platformDir, "runtime-manifest.json"));
|
|
1924
2574
|
return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
|
|
1925
2575
|
}
|
|
1926
2576
|
return { ...resolved, releaseTag: resolved.source, freshInstall: false };
|
|
@@ -1952,16 +2602,16 @@ async function downloadManagedLlama(cfg, log) {
|
|
|
1952
2602
|
}
|
|
1953
2603
|
const asset = pickAsset(release, platform, arch);
|
|
1954
2604
|
log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
|
|
1955
|
-
const tagDir =
|
|
1956
|
-
await
|
|
1957
|
-
const tmpArchive =
|
|
1958
|
-
await
|
|
2605
|
+
const tagDir = path9.join(platformDir, release.tag_name);
|
|
2606
|
+
await fsp7.mkdir(tagDir, { recursive: true });
|
|
2607
|
+
const tmpArchive = path9.join(platformDir, `.download-${asset.name}`);
|
|
2608
|
+
await fsp7.rm(tmpArchive, { force: true });
|
|
1959
2609
|
const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
|
|
1960
2610
|
await extractRelease(tmpArchive, tagDir, platform);
|
|
1961
|
-
await
|
|
2611
|
+
await fsp7.rm(tmpArchive, { force: true });
|
|
1962
2612
|
const binaryPath = await findServerBinary(tagDir, platform);
|
|
1963
|
-
if (platform !== "win32") await
|
|
1964
|
-
await writeRuntimeManifest(
|
|
2613
|
+
if (platform !== "win32") await fsp7.chmod(binaryPath, 493);
|
|
2614
|
+
await writeRuntimeManifest(path9.join(platformDir, "runtime-manifest.json"), {
|
|
1965
2615
|
schemaVersion: 1,
|
|
1966
2616
|
kind: "llama-cpp",
|
|
1967
2617
|
releaseTag: release.tag_name,
|
|
@@ -1977,11 +2627,11 @@ async function removeManagedRuntime(cfg) {
|
|
|
1977
2627
|
const { runtimeDir } = pathsFor(cfg.clefHome);
|
|
1978
2628
|
const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
|
|
1979
2629
|
try {
|
|
1980
|
-
await
|
|
2630
|
+
await fsp7.stat(dir);
|
|
1981
2631
|
} catch {
|
|
1982
2632
|
return false;
|
|
1983
2633
|
}
|
|
1984
|
-
await
|
|
2634
|
+
await fsp7.rm(dir, { recursive: true, force: true });
|
|
1985
2635
|
return true;
|
|
1986
2636
|
}
|
|
1987
2637
|
|
|
@@ -1989,7 +2639,7 @@ async function removeManagedRuntime(cfg) {
|
|
|
1989
2639
|
init_prompt();
|
|
1990
2640
|
|
|
1991
2641
|
// src/version.ts
|
|
1992
|
-
var VERSION = "0.
|
|
2642
|
+
var VERSION = "0.2.0";
|
|
1993
2643
|
|
|
1994
2644
|
// src/cli/install.ts
|
|
1995
2645
|
var OS_NAMES = {
|
|
@@ -2011,6 +2661,10 @@ function printHeader() {
|
|
|
2011
2661
|
async function runInstall(opts) {
|
|
2012
2662
|
const config = loadConfig();
|
|
2013
2663
|
const log = createLogger("info");
|
|
2664
|
+
const runtime = opts.runtime ?? config.runtime;
|
|
2665
|
+
if (runtime !== "llama-cpp" && runtime !== "mlx") {
|
|
2666
|
+
throw new ClefError(ClefErrorCode.INVALID_INPUT, `Unknown runtime "${runtime}".`, "Available: llama-cpp, mlx");
|
|
2667
|
+
}
|
|
2014
2668
|
printHeader();
|
|
2015
2669
|
process.stdout.write("Detecting hardware...\n");
|
|
2016
2670
|
const info = await detectPlatform();
|
|
@@ -2023,41 +2677,57 @@ async function runInstall(opts) {
|
|
|
2023
2677
|
console.log(` Architecture: ${info.arch}`);
|
|
2024
2678
|
console.log(` Memory: ${formatBytes(info.totalMemBytes)}`);
|
|
2025
2679
|
if (gpuLine) console.log(gpuLine);
|
|
2680
|
+
if (runtime === "mlx" && !(info.platform === "darwin" && info.arch === "arm64")) {
|
|
2681
|
+
throw new ClefError(
|
|
2682
|
+
ClefErrorCode.UNSUPPORTED_PLATFORM,
|
|
2683
|
+
"The MLX runtime only works on Apple Silicon (macOS/arm64).",
|
|
2684
|
+
"Use the default llama.cpp runtime (omit --runtime)."
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2026
2687
|
console.log("\nRecommended:");
|
|
2027
|
-
console.log(` Model: ${spec.id} (${spec.repo})`);
|
|
2028
|
-
console.log(`
|
|
2029
|
-
console.log(`
|
|
2688
|
+
console.log(` Model: ${spec.id} (${runtime === "mlx" ? "mlx-community/clef-flash-4bit" : spec.repo})`);
|
|
2689
|
+
console.log(` Runtime: ${runtime}`);
|
|
2690
|
+
if (runtime === "llama-cpp") console.log(` Quantization: ${quant.id}`);
|
|
2691
|
+
console.log(` Download size: ~${runtime === "mlx" ? "6.2 GB" : formatBytes(quant.bytes)}`);
|
|
2030
2692
|
console.log(`
|
|
2031
2693
|
Model license: ${spec.licenseId} \u2014 ${spec.licenseUrl}`);
|
|
2032
|
-
console.log("The model is downloaded from the official
|
|
2694
|
+
console.log("The model is downloaded from the official conversion and is not repackaged by clef-mcp.\n");
|
|
2033
2695
|
const ok = await confirm("Continue?", opts.yes === true);
|
|
2034
2696
|
if (!ok) {
|
|
2035
2697
|
console.log("Aborted. Nothing was downloaded.");
|
|
2036
2698
|
return;
|
|
2037
2699
|
}
|
|
2038
|
-
let
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
console.log(`
|
|
2700
|
+
let llamaManifest;
|
|
2701
|
+
let mlxManifest;
|
|
2702
|
+
if (runtime === "mlx") {
|
|
2703
|
+
console.log("\nDownloading MLX model snapshot (uv-managed; weights ~6 GB)...\n");
|
|
2704
|
+
mlxManifest = await installModelMlx({ clefHome: config.clefHome, clefMcpVersion: VERSION, log });
|
|
2705
|
+
console.log(` Installed: ${mlxManifest.snapshotPath}`);
|
|
2706
|
+
} else {
|
|
2707
|
+
if (!opts.skipRuntime) {
|
|
2708
|
+
process.stdout.write("\nEnsuring llama.cpp runtime...\n");
|
|
2709
|
+
const rt = await ensureLlamaRuntime(config, log);
|
|
2710
|
+
console.log(` Runtime: llama.cpp (${rt.source}, ${rt.releaseTag})`);
|
|
2711
|
+
console.log(` Binary: ${rt.path}`);
|
|
2712
|
+
}
|
|
2713
|
+
console.log("");
|
|
2714
|
+
const installed = await installModel({
|
|
2715
|
+
clefHome: config.clefHome,
|
|
2716
|
+
modelSpec: spec,
|
|
2717
|
+
quantSpec: quant,
|
|
2718
|
+
clefMcpVersion: VERSION,
|
|
2719
|
+
onProgress: (p) => printProgress(p.bytes, p.total),
|
|
2720
|
+
log
|
|
2721
|
+
});
|
|
2722
|
+
process.stderr.write("\n");
|
|
2723
|
+
console.log(installed.alreadyInstalled ? ` Model already installed: ${installed.manifest.modelPath}` : ` Installed: ${installed.manifest.modelPath}`);
|
|
2724
|
+
console.log(` sha256: ${installed.manifest.sha256 ?? "n/a (not advertised upstream)"}`);
|
|
2725
|
+
llamaManifest = installed.manifest;
|
|
2044
2726
|
}
|
|
2045
|
-
console.log("");
|
|
2046
|
-
const { manifest, alreadyInstalled } = await installModel({
|
|
2047
|
-
clefHome: config.clefHome,
|
|
2048
|
-
modelSpec: spec,
|
|
2049
|
-
quantSpec: quant,
|
|
2050
|
-
clefMcpVersion: VERSION,
|
|
2051
|
-
onProgress: (p) => printProgress(p.bytes, p.total),
|
|
2052
|
-
log
|
|
2053
|
-
});
|
|
2054
|
-
process.stderr.write("\n");
|
|
2055
|
-
console.log(alreadyInstalled ? ` Model already installed: ${manifest.modelPath}` : ` Installed: ${manifest.modelPath}`);
|
|
2056
|
-
console.log(` sha256: ${manifest.sha256 ?? "n/a (not advertised upstream)"}`);
|
|
2057
2727
|
if (!opts.skipProbe) {
|
|
2058
2728
|
process.stdout.write("\nVerifying inference (loads the model; first run can take a few minutes)...\n");
|
|
2059
|
-
const probe = await probeInference(config,
|
|
2060
|
-
console.log(` Inference OK (${(probe.latencyMs / 1e3).toFixed(1)}s incl. model load
|
|
2729
|
+
const probe = runtime === "mlx" ? await probeMlxInference(config, mlxManifest, log) : await probeInference(config, llamaManifest, log);
|
|
2730
|
+
console.log(` Inference OK (${(probe.latencyMs / 1e3).toFixed(1)}s incl. model load)`);
|
|
2061
2731
|
}
|
|
2062
2732
|
if (opts.setup) {
|
|
2063
2733
|
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
@@ -2070,7 +2740,10 @@ Model license: ${spec.licenseId} \u2014 ${spec.licenseUrl}`);
|
|
|
2070
2740
|
console.log(" Claude Code : claude mcp add clef-mcp -- clef-mcp");
|
|
2071
2741
|
console.log(" Codex : add [mcp_servers.clef-mcp] to ~/.codex/config.toml (see README)");
|
|
2072
2742
|
console.log(" Cursor : add clef-mcp to .cursor/mcp.json (see README)");
|
|
2073
|
-
console.log(" ZCode : add clef-mcp to ~/.zcode/cli/config.json (see README)
|
|
2743
|
+
console.log(" ZCode : add clef-mcp to ~/.zcode/cli/config.json (see README)");
|
|
2744
|
+
if (runtime === "mlx") {
|
|
2745
|
+
console.log("\nNote: set CLEF_RUNTIME=mlx for the server (and MCP clients) to use the MLX runtime.");
|
|
2746
|
+
}
|
|
2074
2747
|
}
|
|
2075
2748
|
|
|
2076
2749
|
// src/cli/models-cmd.ts
|
|
@@ -2103,7 +2776,7 @@ async function runStatus() {
|
|
|
2103
2776
|
spec = getModelSpec(config.model);
|
|
2104
2777
|
} catch (err) {
|
|
2105
2778
|
if (err instanceof ClefError && err.code === "INVALID_INPUT") {
|
|
2106
|
-
console.log(`Runtime:
|
|
2779
|
+
console.log(`Runtime: ${config.runtime}`);
|
|
2107
2780
|
console.log(`Model: ${config.model} (unknown model id)`);
|
|
2108
2781
|
console.log("Model status: unknown");
|
|
2109
2782
|
console.log(`Memory: ${formatBytes(info.totalMemBytes)}`);
|
|
@@ -2111,6 +2784,26 @@ async function runStatus() {
|
|
|
2111
2784
|
}
|
|
2112
2785
|
throw err;
|
|
2113
2786
|
}
|
|
2787
|
+
if (config.runtime === "mlx") {
|
|
2788
|
+
const mlx = await mlxIsInstalled(config.clefHome);
|
|
2789
|
+
let uvLine = "not found (install: curl -LsSf https://astral.sh/uv/install.sh | sh)";
|
|
2790
|
+
try {
|
|
2791
|
+
uvLine = `uv: ${resolveUv(process.env.CLEF_MLX_UV)}`;
|
|
2792
|
+
} catch (err) {
|
|
2793
|
+
uvLine = `not found \u2014 ${err.message.split("\n")[0]}`;
|
|
2794
|
+
}
|
|
2795
|
+
console.log(`Runtime: mlx (${uvLine})`);
|
|
2796
|
+
console.log(`Model: ${spec.id}${mlx ? " (MLX 4-bit)" : ""}`);
|
|
2797
|
+
console.log(`Model status: ${mlx ? "installed" : "not installed (run: clef-mcp install --runtime mlx)"}`);
|
|
2798
|
+
console.log(`Memory: ${formatBytes(info.totalMemBytes)}`);
|
|
2799
|
+
if (mlx) {
|
|
2800
|
+
console.log(`Snapshot: ${mlx.snapshotPath}`);
|
|
2801
|
+
console.log(`Installed: ${mlx.installedAt}`);
|
|
2802
|
+
} else {
|
|
2803
|
+
console.log(`Cache home: ${pathsFor(config.clefHome).root}`);
|
|
2804
|
+
}
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2114
2807
|
const manifest = await findInstalledManifest(config.clefHome, spec);
|
|
2115
2808
|
let runtimeLine;
|
|
2116
2809
|
try {
|
|
@@ -2135,13 +2828,27 @@ async function runStatus() {
|
|
|
2135
2828
|
|
|
2136
2829
|
// src/cli/uninstall.ts
|
|
2137
2830
|
init_errors();
|
|
2138
|
-
import
|
|
2831
|
+
import fsp9 from "fs/promises";
|
|
2139
2832
|
init_prompt();
|
|
2140
2833
|
async function runUninstall(opts) {
|
|
2141
2834
|
const config = loadConfig();
|
|
2142
2835
|
const spec = getModelSpec(opts.model ?? config.model);
|
|
2143
|
-
const manifest = await findInstalledManifest(config.clefHome, spec);
|
|
2144
2836
|
const { modelsDir } = pathsFor(config.clefHome);
|
|
2837
|
+
if (config.runtime === "mlx") {
|
|
2838
|
+
const mlx = await mlxIsInstalled(config.clefHome);
|
|
2839
|
+
if (!mlx) {
|
|
2840
|
+
throw new ClefError(ClefErrorCode.MODEL_NOT_INSTALLED, "The MLX model is not installed.", "Nothing to remove.");
|
|
2841
|
+
}
|
|
2842
|
+
const ok2 = await confirm(`Remove the MLX snapshot for ${spec.id}?`, opts.yes === true);
|
|
2843
|
+
if (!ok2) {
|
|
2844
|
+
console.log("Aborted.");
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
await removeMlxModel(config.clefHome);
|
|
2848
|
+
console.log("Removed MLX snapshot.");
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
const manifest = await findInstalledManifest(config.clefHome, spec);
|
|
2145
2852
|
if (!manifest && !opts.runtime) {
|
|
2146
2853
|
throw new ClefError(
|
|
2147
2854
|
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
@@ -2160,7 +2867,7 @@ async function runUninstall(opts) {
|
|
|
2160
2867
|
return;
|
|
2161
2868
|
}
|
|
2162
2869
|
if (target) {
|
|
2163
|
-
await
|
|
2870
|
+
await fsp9.rm(target, { recursive: true, force: true });
|
|
2164
2871
|
console.log(`Removed: ${target}`);
|
|
2165
2872
|
}
|
|
2166
2873
|
if (opts.runtime) {
|
|
@@ -2187,7 +2894,7 @@ program.action(async () => {
|
|
|
2187
2894
|
handleCliError(err);
|
|
2188
2895
|
}
|
|
2189
2896
|
});
|
|
2190
|
-
program.command("install").description("Detect the machine, download a Clef model + runtime, verify checksum and inference.").option("--model <id>", "model to install (default: clef-flash)").option("--quant <id>", "quantization override: Q4_K_M | Q8_0 | BF16").option("-y, --yes", "assume yes for all prompts (non-interactive)").option("--skip-runtime", "do not download/verify the llama.cpp runtime").option("--skip-probe", "skip the post-install inference verification").option("--setup", "also register the MCP server + agent skill in detected clients (same as running `clef-mcp setup`)").action(async (opts) => {
|
|
2897
|
+
program.command("install").description("Detect the machine, download a Clef model + runtime, verify checksum and inference.").option("--model <id>", "model to install (default: clef-flash)").option("--quant <id>", "quantization override: Q4_K_M | Q8_0 | BF16").option("-y, --yes", "assume yes for all prompts (non-interactive)").option("--skip-runtime", "do not download/verify the llama.cpp runtime").option("--skip-probe", "skip the post-install inference verification").option("--runtime <id>", "inference runtime: llama-cpp (default) | mlx (Apple Silicon)").option("--setup", "also register the MCP server + agent skill in detected clients (same as running `clef-mcp setup`)").action(async (opts) => {
|
|
2191
2898
|
try {
|
|
2192
2899
|
await runInstall(opts);
|
|
2193
2900
|
} catch (err) {
|