clef-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1970 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/clef/errors.ts
4
+ var ClefErrorCode = {
5
+ MODEL_NOT_INSTALLED: "MODEL_NOT_INSTALLED",
6
+ MODEL_LOAD_FAILED: "MODEL_LOAD_FAILED",
7
+ RUNTIME_NOT_FOUND: "RUNTIME_NOT_FOUND",
8
+ RUNTIME_INIT_FAILED: "RUNTIME_INIT_FAILED",
9
+ RUNTIME_NOT_SUPPORTED: "RUNTIME_NOT_SUPPORTED",
10
+ INVALID_INPUT: "INVALID_INPUT",
11
+ CLEF_INFERENCE_FAILED: "CLEF_INFERENCE_FAILED",
12
+ UNSUPPORTED_PLATFORM: "UNSUPPORTED_PLATFORM",
13
+ OUT_OF_MEMORY: "OUT_OF_MEMORY",
14
+ CHECKSUM_MISMATCH: "CHECKSUM_MISMATCH",
15
+ DOWNLOAD_FAILED: "DOWNLOAD_FAILED"
16
+ };
17
+ var ClefError = class extends Error {
18
+ code;
19
+ hint;
20
+ constructor(code, message, hint) {
21
+ super(message);
22
+ this.name = "ClefError";
23
+ this.code = code;
24
+ this.hint = hint;
25
+ }
26
+ };
27
+ function errorJson(err) {
28
+ if (err instanceof ClefError) {
29
+ return {
30
+ error: {
31
+ code: err.code,
32
+ message: err.message,
33
+ ...err.hint ? { hint: err.hint } : {}
34
+ }
35
+ };
36
+ }
37
+ const message = err instanceof Error ? err.message : String(err);
38
+ return { error: { code: ClefErrorCode.CLEF_INFERENCE_FAILED, message } };
39
+ }
40
+ function exitCodeFor(err) {
41
+ if (err instanceof ClefError) {
42
+ switch (err.code) {
43
+ case ClefErrorCode.INVALID_INPUT:
44
+ return 2;
45
+ case ClefErrorCode.MODEL_NOT_INSTALLED:
46
+ return 3;
47
+ case ClefErrorCode.RUNTIME_NOT_FOUND:
48
+ case ClefErrorCode.RUNTIME_NOT_SUPPORTED:
49
+ return 4;
50
+ default:
51
+ return 1;
52
+ }
53
+ }
54
+ return 1;
55
+ }
56
+
57
+ // src/mcp/server.ts
58
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
59
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
60
+
61
+ // src/clef/systemone.ts
62
+ function stateToString(state) {
63
+ if (typeof state === "string") return state;
64
+ try {
65
+ return JSON.stringify(state) ?? "";
66
+ } catch (err) {
67
+ throw new ClefError(
68
+ ClefErrorCode.INVALID_INPUT,
69
+ `state must be JSON-serializable: ${err instanceof Error ? err.message : String(err)}`
70
+ );
71
+ }
72
+ }
73
+ function toSystemOneRequest(input, defaultModel) {
74
+ const questions = {};
75
+ for (const [qid, q] of Object.entries(input.questions)) {
76
+ const question = { type: q.type, instructions: q.instructions };
77
+ if (q.criteria !== void 0) question.criteria = q.criteria;
78
+ questions[qid] = question;
79
+ }
80
+ return {
81
+ model: input.model ?? defaultModel,
82
+ state: stateToString(input.state),
83
+ questions
84
+ };
85
+ }
86
+ function finiteNumber(value, where) {
87
+ if (typeof value !== "number" || !Number.isFinite(value)) {
88
+ throw new ClefError(
89
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
90
+ `Runtime returned a non-numeric probability for ${where}`
91
+ );
92
+ }
93
+ return value;
94
+ }
95
+ function answerFor(answers, qid) {
96
+ const ans = answers[qid];
97
+ if (!ans || typeof ans !== "object") {
98
+ throw new ClefError(
99
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
100
+ `Runtime response is missing an answer for question "${qid}"`
101
+ );
102
+ }
103
+ return ans;
104
+ }
105
+ function probabilitiesMap(ans, qid) {
106
+ const raw = ans.probabilities;
107
+ if (raw && !Array.isArray(raw) && typeof raw === "object") {
108
+ const out = {};
109
+ for (const [k, v] of Object.entries(raw)) out[k] = finiteNumber(v, `"${qid}" option "${k}"`);
110
+ return out;
111
+ }
112
+ if (Array.isArray(raw)) {
113
+ const out = {};
114
+ for (const [i, v] of raw.entries()) out[String(i)] = finiteNumber(v, `"${qid}" option #${i}`);
115
+ return out;
116
+ }
117
+ throw new ClefError(
118
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
119
+ `Runtime response is missing probabilities for question "${qid}"`
120
+ );
121
+ }
122
+ function normalizeNoul(ans, qid) {
123
+ let pTrue;
124
+ if (typeof ans.noul === "number") {
125
+ pTrue = finiteNumber(ans.noul, `"${qid}"`);
126
+ } else if (typeof ans.probability_true === "number") {
127
+ pTrue = finiteNumber(ans.probability_true, `"${qid}"`);
128
+ } else if (ans.probabilities && !Array.isArray(ans.probabilities)) {
129
+ const map = ans.probabilities;
130
+ const direct = map.true ?? map.yes ?? map.TRUE;
131
+ if (typeof direct === "number") pTrue = finiteNumber(direct, `"${qid}"`);
132
+ } else if (Array.isArray(ans.probabilities) && typeof ans.probabilities[0] === "number") {
133
+ pTrue = finiteNumber(ans.probabilities[0], `"${qid}"`);
134
+ }
135
+ if (pTrue === void 0) {
136
+ throw new ClefError(
137
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
138
+ `Runtime response is missing the true-probability for noul question "${qid}"`
139
+ );
140
+ }
141
+ const p = Math.min(1, Math.max(0, pTrue));
142
+ return { true: p, false: 1 - p };
143
+ }
144
+ function normalizeScore(ans, qid, question) {
145
+ const criteria = question.criteria;
146
+ if (!Array.isArray(criteria) || criteria.length === 0) {
147
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `Score question "${qid}" requires an array of criteria`);
148
+ }
149
+ const raw = ans.probabilities;
150
+ let pairs;
151
+ if (Array.isArray(raw)) {
152
+ if (raw.length !== criteria.length) {
153
+ throw new ClefError(
154
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
155
+ `Runtime returned ${raw.length} probabilities for score question "${qid}" with ${criteria.length} criteria`
156
+ );
157
+ }
158
+ pairs = raw.map((v, i) => [criteria[i], finiteNumber(v, `"${qid}" criterion "${criteria[i]}"`)]);
159
+ } else if (raw && typeof raw === "object") {
160
+ const entries = Object.entries(raw);
161
+ const allNumericKeys = entries.length > 0 && entries.every(([k]) => /^\d+$/.test(k));
162
+ if (allNumericKeys) {
163
+ pairs = entries.map(([k, v]) => {
164
+ const idx = Number(k);
165
+ let label;
166
+ if (Array.isArray(ans.legend)) label = ans.legend[idx];
167
+ else if (ans.legend && typeof ans.legend === "object") label = ans.legend[k];
168
+ const fallback = Number.isInteger(idx) ? criteria[idx] : void 0;
169
+ return [label ?? fallback ?? k, finiteNumber(v, `"${qid}" option #${k}`)];
170
+ });
171
+ } else {
172
+ pairs = entries.map(([k, v]) => [k, finiteNumber(v, `"${qid}" criterion "${k}"`)]);
173
+ }
174
+ } else {
175
+ throw new ClefError(
176
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
177
+ `Runtime response is missing the probability distribution for score question "${qid}"`
178
+ );
179
+ }
180
+ const out = {};
181
+ for (const criterion of criteria) {
182
+ const pair = pairs.find(([label]) => label === criterion);
183
+ if (!pair) {
184
+ throw new ClefError(
185
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
186
+ `Runtime response is missing probability for score question "${qid}" criterion "${criterion}"`
187
+ );
188
+ }
189
+ out[criterion] = pair[1];
190
+ }
191
+ return out;
192
+ }
193
+ function normalizeChoice(ans, qid) {
194
+ return probabilitiesMap(ans, qid);
195
+ }
196
+ function fromSystemOneResponse(response, questions, fallbackModel) {
197
+ const answers = response.answers;
198
+ if (!answers || typeof answers !== "object") {
199
+ throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, "Runtime response is missing the answers object");
200
+ }
201
+ const decisions = {};
202
+ for (const [qid, question] of Object.entries(questions)) {
203
+ const ans = answerFor(answers, qid);
204
+ let answer;
205
+ switch (question.type) {
206
+ case "choice":
207
+ answer = normalizeChoice(ans, qid);
208
+ break;
209
+ case "noul":
210
+ answer = normalizeNoul(ans, qid);
211
+ break;
212
+ case "score":
213
+ answer = normalizeScore(ans, qid, question);
214
+ break;
215
+ }
216
+ decisions[qid] = { answer };
217
+ }
218
+ return { model: response.model ?? fallbackModel, decisions };
219
+ }
220
+
221
+ // src/config/env.ts
222
+ import os from "os";
223
+ import path from "path";
224
+
225
+ // src/config/limits.ts
226
+ var DEFAULT_LIMITS = {
227
+ maxQuestions: 64,
228
+ maxStateBytes: 1024 * 1024,
229
+ maxInstructionsChars: 1e4,
230
+ maxCriteriaPerQuestion: 100,
231
+ maxCriteriaDescChars: 1e3,
232
+ maxQuestionIdChars: 128
233
+ };
234
+ function intFromEnv(env, key, fallback) {
235
+ const raw = env[key];
236
+ if (!raw?.trim()) return fallback;
237
+ const n = Number.parseInt(raw, 10);
238
+ return Number.isFinite(n) && n > 0 ? n : fallback;
239
+ }
240
+ function limitsFromEnv(env = process.env) {
241
+ return {
242
+ maxQuestions: intFromEnv(env, "CLEF_MAX_QUESTIONS", DEFAULT_LIMITS.maxQuestions),
243
+ maxStateBytes: intFromEnv(env, "CLEF_MAX_STATE_BYTES", DEFAULT_LIMITS.maxStateBytes),
244
+ maxInstructionsChars: intFromEnv(env, "CLEF_MAX_INSTRUCTION_CHARS", DEFAULT_LIMITS.maxInstructionsChars),
245
+ maxCriteriaPerQuestion: intFromEnv(env, "CLEF_MAX_CRITERIA", DEFAULT_LIMITS.maxCriteriaPerQuestion),
246
+ maxCriteriaDescChars: intFromEnv(env, "CLEF_MAX_CRITERIA_DESC_CHARS", DEFAULT_LIMITS.maxCriteriaDescChars),
247
+ maxQuestionIdChars: intFromEnv(env, "CLEF_MAX_QUESTION_ID_CHARS", DEFAULT_LIMITS.maxQuestionIdChars)
248
+ };
249
+ }
250
+
251
+ // src/config/env.ts
252
+ var SUPPORTED_LOG_LEVELS = ["error", "warn", "info", "debug"];
253
+ function parseLogLevel(value) {
254
+ if (!value) return void 0;
255
+ const v = value.trim().toLowerCase();
256
+ return SUPPORTED_LOG_LEVELS.includes(v) ? v : void 0;
257
+ }
258
+ function defaultClefHome() {
259
+ return path.join(os.homedir(), ".cache", "clef-mcp");
260
+ }
261
+ function loadConfig(env = process.env) {
262
+ const logLevel = parseLogLevel(env.CLEF_LOG_LEVEL) ?? "error";
263
+ const runtime = env.CLEF_RUNTIME?.trim() || "llama-cpp";
264
+ if (runtime !== "llama-cpp") {
265
+ throw new Error(`Unsupported CLEF_RUNTIME "${runtime}". Supported: llama-cpp`);
266
+ }
267
+ const llamaBin = env.CLEF_LLAMA_BIN?.trim() || void 0;
268
+ const llamaReleaseTag = env.CLEF_LLAMA_RELEASE_TAG?.trim() || void 0;
269
+ return {
270
+ model: env.CLEF_MODEL?.trim() || "clef-flash",
271
+ clefHome: env.CLEF_HOME?.trim() || defaultClefHome(),
272
+ runtime: "llama-cpp",
273
+ logLevel,
274
+ llamaBin,
275
+ llamaReleaseTag,
276
+ limits: limitsFromEnv(env)
277
+ };
278
+ }
279
+
280
+ // src/config/logger.ts
281
+ var LEVEL_WEIGHT = {
282
+ error: 0,
283
+ warn: 1,
284
+ info: 2,
285
+ debug: 3
286
+ };
287
+ function stderrSink(line) {
288
+ process.stderr.write(line + "\n");
289
+ }
290
+ var StderrLogger = class {
291
+ constructor(level, sink = stderrSink) {
292
+ this.level = level;
293
+ this.sink = sink;
294
+ }
295
+ level;
296
+ sink;
297
+ write(level, msg, extra) {
298
+ if (LEVEL_WEIGHT[level] > LEVEL_WEIGHT[this.level]) return;
299
+ const entry = {
300
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
301
+ level,
302
+ msg,
303
+ ...extra ? { data: extra } : {}
304
+ };
305
+ try {
306
+ this.sink(JSON.stringify(entry));
307
+ } catch {
308
+ }
309
+ }
310
+ error(msg, extra) {
311
+ this.write("error", msg, extra);
312
+ }
313
+ warn(msg, extra) {
314
+ this.write("warn", msg, extra);
315
+ }
316
+ info(msg, extra) {
317
+ this.write("info", msg, extra);
318
+ }
319
+ debug(msg, extra) {
320
+ this.write("debug", msg, extra);
321
+ }
322
+ };
323
+ function createLogger(level, sink) {
324
+ return new StderrLogger(level, sink);
325
+ }
326
+
327
+ // src/models/manifest.ts
328
+ import { createHash } from "crypto";
329
+ import fs from "fs";
330
+ import fsp from "fs/promises";
331
+ import path3 from "path";
332
+
333
+ // src/config/paths.ts
334
+ import path2 from "path";
335
+ function pathsFor(clefHome) {
336
+ const root = path2.resolve(clefHome);
337
+ return {
338
+ root,
339
+ modelsDir: path2.join(root, "models"),
340
+ runtimeDir: path2.join(root, "runtime"),
341
+ configJson: path2.join(root, "config.json")
342
+ };
343
+ }
344
+ function quantDirLabel(quant) {
345
+ switch (quant) {
346
+ case "Q4_K_M":
347
+ return "4bit";
348
+ case "Q8_0":
349
+ return "8bit";
350
+ case "BF16":
351
+ return "bf16";
352
+ default:
353
+ return quant.toLowerCase();
354
+ }
355
+ }
356
+ function modelQuantDir(modelsDir, model, quant) {
357
+ return path2.join(modelsDir, model, quantDirLabel(quant));
358
+ }
359
+ function manifestPathFor(modelsDir, model, quant) {
360
+ return path2.join(modelQuantDir(modelsDir, model, quant), "manifest.json");
361
+ }
362
+ function llamaRuntimeDir(runtimeDir, platform, arch) {
363
+ return path2.join(runtimeDir, "llama.cpp", `${platform}-${arch}`);
364
+ }
365
+ function llamaServerName(platform) {
366
+ return platform === "win32" ? "llama-server.exe" : "llama-server";
367
+ }
368
+
369
+ // src/models/registry.ts
370
+ var GB = 1024 ** 3;
371
+ var CLEF_FLASH = {
372
+ id: "clef-flash",
373
+ repo: "ggml-org/Clef-Flash-GGUF",
374
+ revision: "4a7a08c09bc63baf043b62b5ba89dd67a0357d95",
375
+ licenseId: "Apache-2.0",
376
+ licenseUrl: "https://huggingface.co/Cloudflare/clef-flash",
377
+ originUrl: "https://huggingface.co/Cloudflare/clef-flash",
378
+ defaultQuant: "Q4_K_M",
379
+ quants: [
380
+ { id: "Q4_K_M", file: "Clef-Flash-Q4_K_M.gguf", bytes: 6486448192, dirLabel: "4bit", minTotalMemBytes: 16 * GB },
381
+ { id: "Q8_0", file: "Clef-Flash-Q8_0.gguf", bytes: 9657260096, dirLabel: "8bit", minTotalMemBytes: 32 * GB },
382
+ { id: "BF16", file: "Clef-Flash-BF16.gguf", bytes: 18164488256, dirLabel: "bf16", minTotalMemBytes: 64 * GB }
383
+ ]
384
+ };
385
+ var MODELS = {
386
+ [CLEF_FLASH.id]: CLEF_FLASH
387
+ };
388
+ function getModelSpec(model) {
389
+ const spec = MODELS[model];
390
+ if (!spec) {
391
+ throw new ClefError(
392
+ ClefErrorCode.INVALID_INPUT,
393
+ `Unknown model "${model}".`,
394
+ `Available models: ${Object.keys(MODELS).join(", ")}`
395
+ );
396
+ }
397
+ return spec;
398
+ }
399
+ function getQuantSpec(spec, quant) {
400
+ const found = spec.quants.find((q) => q.id === quant);
401
+ if (!found) {
402
+ throw new ClefError(
403
+ ClefErrorCode.INVALID_INPUT,
404
+ `Unknown quantization "${quant}" for model ${spec.id}.`,
405
+ `Available: ${spec.quants.map((q) => q.id).join(", ")}`
406
+ );
407
+ }
408
+ return found;
409
+ }
410
+ function pickQuant(spec, totalMemBytes, requested) {
411
+ if (requested) return getQuantSpec(spec, requested);
412
+ const fitting = [...spec.quants].sort((a, b) => b.minTotalMemBytes - a.minTotalMemBytes).find((q) => totalMemBytes >= q.minTotalMemBytes);
413
+ if (!fitting) {
414
+ throw new ClefError(
415
+ ClefErrorCode.UNSUPPORTED_PLATFORM,
416
+ `This machine has less than ${Math.round(spec.quants[spec.quants.length - 1].minTotalMemBytes / GB)} GB of memory; the smallest Clef-Flash build (Q4_K_M) needs 16 GB.`
417
+ );
418
+ }
419
+ return fitting;
420
+ }
421
+ var QUANT_PREFERENCE = ["Q4_K_M", "Q8_0", "BF16"];
422
+
423
+ // src/models/manifest.ts
424
+ function isModelManifest(v) {
425
+ if (!v || typeof v !== "object") return false;
426
+ const m = v;
427
+ return m.schemaVersion === 1 && typeof m.model === "string" && typeof m.quant === "string" && typeof m.file === "string" && typeof m.modelPath === "string" && typeof m.bytes === "number";
428
+ }
429
+ async function readModelManifest(manifestPath) {
430
+ let raw;
431
+ try {
432
+ raw = await fsp.readFile(manifestPath, "utf8");
433
+ } catch {
434
+ return void 0;
435
+ }
436
+ try {
437
+ const parsed = JSON.parse(raw);
438
+ return isModelManifest(parsed) ? parsed : void 0;
439
+ } catch {
440
+ return void 0;
441
+ }
442
+ }
443
+ async function writeModelManifest(manifestPath, manifest) {
444
+ await fsp.mkdir(path3.dirname(manifestPath), { recursive: true });
445
+ await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
446
+ }
447
+ async function findInstalledQuant(modelsDir, model, specs) {
448
+ const installed = [];
449
+ for (const spec of specs) {
450
+ const manifest = await readModelManifest(manifestPathFor(modelsDir, model, spec.id));
451
+ if (!manifest) continue;
452
+ try {
453
+ const stat = await fsp.stat(manifest.modelPath);
454
+ if (stat.isFile() && stat.size === manifest.bytes) installed.push(spec);
455
+ } catch {
456
+ }
457
+ }
458
+ return QUANT_PREFERENCE.map((id) => installed.find((s) => s.id === id)).find(Boolean);
459
+ }
460
+ async function verifyModelFiles(manifest) {
461
+ const problems = [];
462
+ let stat;
463
+ try {
464
+ stat = await fsp.stat(manifest.modelPath);
465
+ } catch {
466
+ return { ok: false, problems: [`model file missing: ${manifest.modelPath}`] };
467
+ }
468
+ if (!stat.isFile()) problems.push(`model path is not a file: ${manifest.modelPath}`);
469
+ if (stat.size !== manifest.bytes) {
470
+ problems.push(`size mismatch: expected ${manifest.bytes} bytes, found ${stat.size}`);
471
+ }
472
+ return { ok: problems.length === 0, problems };
473
+ }
474
+ async function deepVerifyChecksum(manifest) {
475
+ const base = await verifyModelFiles(manifest);
476
+ if (!base.ok) return base;
477
+ if (!manifest.sha256) return { ok: false, problems: ["manifest has no recorded sha256"] };
478
+ const hash = createHash("sha256");
479
+ const stream = fs.createReadStream(manifest.modelPath);
480
+ for await (const chunk of stream) hash.update(chunk);
481
+ const digest = hash.digest("hex");
482
+ if (digest !== manifest.sha256) {
483
+ return { ok: false, problems: [`checksum mismatch: expected ${manifest.sha256}, got ${digest}`] };
484
+ }
485
+ return { ok: true, problems: [] };
486
+ }
487
+ async function findInstalledManifest(clefHome, spec) {
488
+ const { modelsDir } = pathsFor(clefHome);
489
+ const quant = await findInstalledQuant(modelsDir, spec.id, spec.quants);
490
+ if (!quant) return void 0;
491
+ return readModelManifest(manifestPathFor(modelsDir, spec.id, quant.id));
492
+ }
493
+ async function quantIsInstalled(clefHome, spec, quant) {
494
+ const manifest = await readModelManifest(manifestPathFor(pathsFor(clefHome).modelsDir, spec.id, quant.id));
495
+ if (!manifest) return false;
496
+ return (await verifyModelFiles(manifest)).ok;
497
+ }
498
+ function isRuntimeManifest(v) {
499
+ if (!v || typeof v !== "object") return false;
500
+ const m = v;
501
+ return m.schemaVersion === 1 && m.kind === "llama-cpp" && typeof m.releaseTag === "string" && typeof m.asset === "string" && typeof m.binaryPath === "string";
502
+ }
503
+ async function writeRuntimeManifest(manifestPath, manifest) {
504
+ await fsp.mkdir(path3.dirname(manifestPath), { recursive: true });
505
+ await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
506
+ }
507
+ function readRuntimeManifest(manifestPath) {
508
+ try {
509
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
510
+ return isRuntimeManifest(parsed) ? parsed : void 0;
511
+ } catch {
512
+ return void 0;
513
+ }
514
+ }
515
+
516
+ // src/runtime/binary.ts
517
+ import fs2 from "fs";
518
+ import path4 from "path";
519
+ function existsExecutable(p) {
520
+ try {
521
+ fs2.accessSync(p, fs2.constants.X_OK);
522
+ return fs2.statSync(p).isFile();
523
+ } catch {
524
+ return false;
525
+ }
526
+ }
527
+ function whichBin(name, platform) {
528
+ const dirs = (process.env.PATH ?? "").split(path4.delimiter).filter(Boolean);
529
+ const exts = platform === "win32" ? (process.env.PATHEXT ?? ".exe").split(";") : [""];
530
+ for (const dir of dirs) {
531
+ for (const ext of exts) {
532
+ const candidate = path4.join(dir, name + ext.toLowerCase());
533
+ if (existsExecutable(candidate)) return candidate;
534
+ }
535
+ }
536
+ return void 0;
537
+ }
538
+ function managedBinary(clefHome, platform, arch, exists) {
539
+ const platformDir = llamaRuntimeDir(pathsFor(clefHome).runtimeDir, platform, arch);
540
+ const manifest = readRuntimeManifest(path4.join(platformDir, "runtime-manifest.json"));
541
+ if (manifest && exists(manifest.binaryPath)) return manifest.binaryPath;
542
+ const legacy = path4.join(platformDir, llamaServerName(platform));
543
+ if (exists(legacy)) return legacy;
544
+ return void 0;
545
+ }
546
+ function resolveLlamaServerBinary(cfg, deps = {}) {
547
+ const exists = deps.existsExecutable ?? existsExecutable;
548
+ const which = deps.which ?? ((name) => whichBin(name, deps.platform ?? process.platform));
549
+ const platform = deps.platform ?? process.platform;
550
+ const arch = deps.arch ?? process.arch;
551
+ if (cfg.llamaBin) {
552
+ if (!exists(cfg.llamaBin)) {
553
+ throw new ClefError(
554
+ ClefErrorCode.RUNTIME_NOT_FOUND,
555
+ `CLEF_LLAMA_BIN points to a missing or non-executable file: ${cfg.llamaBin}`
556
+ );
557
+ }
558
+ return { source: "env", path: cfg.llamaBin };
559
+ }
560
+ const managed = managedBinary(cfg.clefHome, platform, arch, exists);
561
+ if (managed) return { source: "managed", path: managed };
562
+ const onPath = which("llama-server");
563
+ if (onPath) return { source: "path", path: onPath };
564
+ throw new ClefError(
565
+ ClefErrorCode.RUNTIME_NOT_FOUND,
566
+ "No llama-server binary found.",
567
+ "Install llama.cpp with clef support (`brew install llama.cpp`, or `curl -LsSf https://llama.app/install.sh | sh`), or run `clef-mcp install` to download a managed runtime."
568
+ );
569
+ }
570
+
571
+ // src/runtime/llama-cpp.ts
572
+ import { spawn } from "child_process";
573
+ import net from "net";
574
+ var STDERR_TAIL_LIMIT = 4e3;
575
+ function freePort() {
576
+ return new Promise((resolve, reject) => {
577
+ const srv = net.createServer();
578
+ srv.unref();
579
+ srv.on("error", reject);
580
+ srv.listen(0, "127.0.0.1", () => {
581
+ const addr = srv.address();
582
+ const port = typeof addr === "object" && addr ? addr.port : 0;
583
+ srv.close(() => port > 0 ? resolve(port) : reject(new Error("could not acquire a free port")));
584
+ });
585
+ });
586
+ }
587
+ async function fetchWithTimeout(url, init, timeoutMs) {
588
+ const controller = new AbortController();
589
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
590
+ try {
591
+ return await fetch(url, { ...init, signal: controller.signal });
592
+ } finally {
593
+ clearTimeout(timer);
594
+ }
595
+ }
596
+ var OOM_PATTERNS = [
597
+ "out of memory",
598
+ "failed to allocate",
599
+ "unable to allocate",
600
+ "not enough memory",
601
+ "cannot allocate memory"
602
+ ];
603
+ var LlamaCppRuntime = class {
604
+ constructor(opts) {
605
+ this.opts = opts;
606
+ this.host = opts.host ?? "127.0.0.1";
607
+ this.loadTimeoutMs = opts.loadTimeoutMs ?? 12e4;
608
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e4;
609
+ }
610
+ opts;
611
+ proc;
612
+ resolvedPort;
613
+ host;
614
+ loadTimeoutMs;
615
+ requestTimeoutMs;
616
+ /** Serializes decide() calls: the llama.cpp clef path handles one sequence per batch. */
617
+ queue = Promise.resolve();
618
+ get endpoint() {
619
+ if (!this.resolvedPort) throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "Runtime is not started");
620
+ return `http://${this.host}:${this.resolvedPort}`;
621
+ }
622
+ get running() {
623
+ return this.proc !== void 0 && this.proc.exitCode === null;
624
+ }
625
+ async load() {
626
+ if (this.running) return;
627
+ const port = this.opts.port ?? await freePort();
628
+ this.resolvedPort = port;
629
+ const args = [
630
+ "-m",
631
+ this.opts.modelPath,
632
+ "--host",
633
+ this.host,
634
+ "--port",
635
+ String(port),
636
+ "--no-webui",
637
+ ...this.opts.alias ? ["--alias", this.opts.alias] : []
638
+ ];
639
+ this.opts.log?.debug("spawning llama-server", { bin: this.opts.serverBin, args });
640
+ let proc;
641
+ try {
642
+ proc = spawn(this.opts.serverBin, args, { stdio: ["ignore", "pipe", "pipe"] });
643
+ } catch (err) {
644
+ throw new ClefError(
645
+ ClefErrorCode.RUNTIME_INIT_FAILED,
646
+ `Failed to start llama-server: ${err instanceof Error ? err.message : String(err)}`,
647
+ "Check that the runtime binary is executable, or run `clef-mcp doctor`."
648
+ );
649
+ }
650
+ this.proc = proc;
651
+ let stderrTail = "";
652
+ proc.stderr?.on("data", (chunk) => {
653
+ const text = chunk.toString();
654
+ stderrTail = (stderrTail + text).slice(-STDERR_TAIL_LIMIT);
655
+ this.opts.log?.debug(text.trimEnd());
656
+ });
657
+ proc.stdout?.on("data", (chunk) => {
658
+ this.opts.log?.debug(chunk.toString().trimEnd());
659
+ });
660
+ let rejectOnExit;
661
+ const exitedBeforeReady = new Promise((_resolve, reject) => {
662
+ rejectOnExit = reject;
663
+ });
664
+ const onEarlyExit = (code, signal) => {
665
+ const oom = OOM_PATTERNS.some((p) => stderrTail.toLowerCase().includes(p));
666
+ const message = `llama-server exited before becoming ready (code=${code ?? "null"}, signal=${signal ?? "null"})`;
667
+ const tail = stderrTail.trim();
668
+ rejectOnExit?.(
669
+ oom ? new ClefError(
670
+ ClefErrorCode.OUT_OF_MEMORY,
671
+ message,
672
+ "Not enough free memory for this model. Try a smaller quantization (`clef-mcp install --quant Q4_K_M`)."
673
+ ) : new ClefError(
674
+ ClefErrorCode.RUNTIME_INIT_FAILED,
675
+ message + (tail ? `
676
+ stderr tail:
677
+ ${tail}` : ""),
678
+ "Run `clef-mcp doctor` for a full diagnosis."
679
+ )
680
+ );
681
+ };
682
+ proc.once("exit", onEarlyExit);
683
+ try {
684
+ await Promise.race([this.waitUntilReady(), exitedBeforeReady]);
685
+ } finally {
686
+ proc.removeListener("exit", onEarlyExit);
687
+ }
688
+ }
689
+ async waitUntilReady() {
690
+ const deadline = Date.now() + this.loadTimeoutMs;
691
+ let lastError = "";
692
+ while (Date.now() < deadline) {
693
+ if (!this.running) throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "llama-server is not running");
694
+ try {
695
+ const res = await fetchWithTimeout(`${this.endpoint}/health`, { method: "GET" }, 2e3);
696
+ if (res.status === 200) return;
697
+ lastError = `health status ${res.status}`;
698
+ } catch (err) {
699
+ lastError = err instanceof Error ? err.message : String(err);
700
+ }
701
+ await new Promise((r) => setTimeout(r, 250));
702
+ }
703
+ throw new ClefError(
704
+ ClefErrorCode.MODEL_LOAD_FAILED,
705
+ `llama-server did not become ready within ${Math.round(this.loadTimeoutMs / 1e3)}s (last health check: ${lastError})`,
706
+ "Large models can take minutes to load. Run `clef-mcp doctor` or try a smaller quantization."
707
+ );
708
+ }
709
+ async decide(input) {
710
+ const run = this.queue.then(() => this.decideOnce(input));
711
+ this.queue = run.catch(() => void 0);
712
+ return run;
713
+ }
714
+ async decideOnce(input) {
715
+ if (!this.running) {
716
+ throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "Runtime is not loaded; call load() first");
717
+ }
718
+ let res;
719
+ try {
720
+ res = await fetchWithTimeout(
721
+ `${this.endpoint}/v1/systemone`,
722
+ {
723
+ method: "POST",
724
+ headers: { "content-type": "application/json" },
725
+ body: JSON.stringify(input)
726
+ },
727
+ this.requestTimeoutMs
728
+ );
729
+ } catch (err) {
730
+ const msg = err instanceof Error ? err.message : String(err);
731
+ throw new ClefError(
732
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
733
+ `Inference request failed: ${msg}`,
734
+ msg.includes("abort") ? "Request timed out. Reduce state size or increase the request timeout." : void 0
735
+ );
736
+ }
737
+ const bodyText = await res.text();
738
+ let body = void 0;
739
+ try {
740
+ body = bodyText ? JSON.parse(bodyText) : void 0;
741
+ } catch {
742
+ }
743
+ if (!res.ok) {
744
+ const apiMessage = body && typeof body === "object" && "error" in body ? String(body.error?.message ?? bodyText) : bodyText;
745
+ if (res.status === 413) {
746
+ throw new ClefError(
747
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
748
+ `Input exceeds the model context window: ${apiMessage}`,
749
+ "Reduce the state size or the number/size of questions."
750
+ );
751
+ }
752
+ throw new ClefError(
753
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
754
+ `Inference failed with HTTP ${res.status}: ${apiMessage.slice(0, 2e3)}`
755
+ );
756
+ }
757
+ if (!body || typeof body !== "object") {
758
+ throw new ClefError(
759
+ ClefErrorCode.CLEF_INFERENCE_FAILED,
760
+ `Runtime returned a non-JSON response (HTTP ${res.status})`,
761
+ "The installed llama.cpp build may not support clef models. Run `clef-mcp doctor`."
762
+ );
763
+ }
764
+ return body;
765
+ }
766
+ async health() {
767
+ if (!this.running) return { status: "unavailable", detail: "not started" };
768
+ try {
769
+ const res = await fetchWithTimeout(`${this.endpoint}/health`, { method: "GET" }, 1500);
770
+ if (res.status === 200) return { status: "ready" };
771
+ return { status: "loading", detail: `health status ${res.status}` };
772
+ } catch (err) {
773
+ return { status: "unavailable", detail: err instanceof Error ? err.message : String(err) };
774
+ }
775
+ }
776
+ async unload() {
777
+ const proc = this.proc;
778
+ if (!proc) return;
779
+ this.proc = void 0;
780
+ if (proc.exitCode !== null || proc.signalCode) return;
781
+ await new Promise((resolve) => {
782
+ const killTimer = setTimeout(() => {
783
+ try {
784
+ proc.kill("SIGKILL");
785
+ } catch {
786
+ }
787
+ }, 3e3);
788
+ proc.once("exit", () => {
789
+ clearTimeout(killTimer);
790
+ resolve();
791
+ });
792
+ try {
793
+ proc.kill("SIGTERM");
794
+ } catch {
795
+ clearTimeout(killTimer);
796
+ resolve();
797
+ }
798
+ });
799
+ }
800
+ };
801
+
802
+ // src/mcp/schema.ts
803
+ import { z } from "zod";
804
+ function criteriaDescription(limits) {
805
+ return z.string().min(1).max(limits.maxCriteriaDescChars);
806
+ }
807
+ function choiceCriteria(limits) {
808
+ return z.record(z.string().min(1).max(limits.maxCriteriaDescChars), criteriaDescription(limits)).check((ctx) => {
809
+ const count = Object.keys(ctx.value).length;
810
+ if (count === 0) ctx.issues.push({ code: "custom", message: "criteria must not be empty", input: ctx.value });
811
+ if (count > limits.maxCriteriaPerQuestion) {
812
+ ctx.issues.push({ code: "custom", message: `too many criteria (max ${limits.maxCriteriaPerQuestion})`, input: ctx.value });
813
+ }
814
+ });
815
+ }
816
+ function scoreCriteria(limits) {
817
+ return z.array(criteriaDescription(limits)).check((ctx) => {
818
+ if (ctx.value.length === 0) ctx.issues.push({ code: "custom", message: "criteria must not be empty", input: ctx.value });
819
+ if (ctx.value.length > limits.maxCriteriaPerQuestion) {
820
+ ctx.issues.push({ code: "custom", message: `too many criteria (max ${limits.maxCriteriaPerQuestion})`, input: ctx.value });
821
+ }
822
+ });
823
+ }
824
+ function questionSchema(limits) {
825
+ return z.object({
826
+ type: z.enum(["noul", "choice", "score"]),
827
+ instructions: z.string().min(1).max(limits.maxInstructionsChars),
828
+ criteria: z.union([z.array(z.string()), z.record(z.string(), z.string())]).optional()
829
+ }).superRefine((q, ctx) => {
830
+ if (q.type === "choice") {
831
+ const parsed = choiceCriteria(limits).safeParse(q.criteria);
832
+ if (!parsed.success) {
833
+ ctx.addIssue({
834
+ code: "custom",
835
+ message: `"choice" questions require non-empty criteria as a map of option id -> description (${parsed.error.issues[0]?.message ?? "invalid criteria"})`
836
+ });
837
+ }
838
+ } else if (q.type === "score") {
839
+ const parsed = scoreCriteria(limits).safeParse(q.criteria);
840
+ if (!parsed.success) {
841
+ ctx.addIssue({
842
+ code: "custom",
843
+ message: `"score" questions require non-empty criteria as an ordered list (${parsed.error.issues[0]?.message ?? "invalid criteria"})`
844
+ });
845
+ }
846
+ } else if (q.criteria !== void 0 && Array.isArray(q.criteria)) {
847
+ ctx.addIssue({ code: "custom", message: '"noul" criteria (if given) must be a map with "true"/"false" descriptions' });
848
+ }
849
+ });
850
+ }
851
+ function clefDecideInputShape(limits) {
852
+ return {
853
+ state: z.unknown(),
854
+ questions: z.record(z.string().min(1).max(limits.maxQuestionIdChars), questionSchema(limits)).check((ctx) => {
855
+ const count = Object.keys(ctx.value).length;
856
+ if (count === 0) ctx.issues.push({ code: "custom", message: "questions must not be empty", input: ctx.value });
857
+ if (count > limits.maxQuestions) {
858
+ ctx.issues.push({ code: "custom", message: `too many questions (max ${limits.maxQuestions})`, input: ctx.value });
859
+ }
860
+ }),
861
+ model: z.string().min(1).optional(),
862
+ options: z.object({
863
+ /**
864
+ * Accepted for forward compatibility. Clef scores all options in a
865
+ * single forward pass — there is no sampling, so temperature is a
866
+ * documented no-op.
867
+ */
868
+ temperature: z.number().min(0).max(2).optional()
869
+ }).optional()
870
+ };
871
+ }
872
+ function clefDecideInputSchema(limits) {
873
+ return z.object(clefDecideInputShape(limits));
874
+ }
875
+ function clefDecideOutputSchema() {
876
+ return z.object({
877
+ model: z.string(),
878
+ decisions: z.record(z.string(), z.object({ answer: z.record(z.string(), z.number()) }))
879
+ });
880
+ }
881
+ function validateStateSize(state, limits) {
882
+ if (state === void 0) {
883
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, "state is required", "Pass the task state as a string or JSON value.");
884
+ }
885
+ let bytes;
886
+ if (typeof state === "string") {
887
+ bytes = Buffer.byteLength(state, "utf8");
888
+ } else {
889
+ let json;
890
+ try {
891
+ json = JSON.stringify(state) ?? "";
892
+ } catch (err) {
893
+ throw new ClefError(
894
+ ClefErrorCode.INVALID_INPUT,
895
+ `state must be JSON-serializable: ${err instanceof Error ? err.message : String(err)}`
896
+ );
897
+ }
898
+ bytes = Buffer.byteLength(json, "utf8");
899
+ }
900
+ if (bytes > limits.maxStateBytes) {
901
+ throw new ClefError(
902
+ ClefErrorCode.INVALID_INPUT,
903
+ `state exceeds the maximum size of ${limits.maxStateBytes} bytes (got ${bytes})`,
904
+ `Trim the state or raise the limit with CLEF_MAX_STATE_BYTES. Note the model context is 16k tokens.`
905
+ );
906
+ }
907
+ }
908
+
909
+ // src/mcp/server.ts
910
+ var TOOL_NAME = "clef_decide";
911
+ 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.";
912
+ function createClefServer(deps = {}) {
913
+ const config = deps.config ?? loadConfig();
914
+ const log = deps.log ?? createLogger(config.logLevel);
915
+ const server = new McpServer({ name: "clef-mcp", version: deps.version ?? "0.1.0" }, { instructions: SERVER_INSTRUCTIONS });
916
+ let loaded;
917
+ let loadPromise;
918
+ async function ensureRuntime(spec) {
919
+ if (loaded && loaded.model === spec.id) return loaded;
920
+ if (loaded && loaded.model !== spec.id) {
921
+ await loaded.runtime.unload();
922
+ loaded = void 0;
923
+ }
924
+ if (loadPromise) return loadPromise;
925
+ loadPromise = (async () => {
926
+ const manifest = await findInstalledManifest(config.clefHome, spec);
927
+ if (!manifest) {
928
+ throw new ClefError(
929
+ ClefErrorCode.MODEL_NOT_INSTALLED,
930
+ `Clef model "${spec.id}" is not installed.`,
931
+ "Run `clef-mcp install`."
932
+ );
933
+ }
934
+ const binary = resolveLlamaServerBinary(config);
935
+ const runtime = deps.runtimeFactory ? deps.runtimeFactory({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
936
+ log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
937
+ const loadStarted = Date.now();
938
+ await runtime.load();
939
+ log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
940
+ return { runtime, model: spec.id };
941
+ })();
942
+ try {
943
+ loaded = await loadPromise;
944
+ return loaded;
945
+ } finally {
946
+ loadPromise = void 0;
947
+ }
948
+ }
949
+ server.registerTool(
950
+ TOOL_NAME,
951
+ {
952
+ title: "Clef Decide",
953
+ description: "Pass task state and a set of structured questions (noul / choice / score) to the local Clef decision model. Returns a probability distribution over the allowed answers for every question. The result is structured JSON.",
954
+ inputSchema: clefDecideInputShape(config.limits),
955
+ outputSchema: clefDecideOutputSchema().shape
956
+ },
957
+ async (args) => {
958
+ try {
959
+ const parsed = clefDecideInputSchema(config.limits).parse(args);
960
+ validateStateSize(parsed.state, config.limits);
961
+ const spec = getModelSpec(parsed.model ?? config.model);
962
+ const input = {
963
+ state: parsed.state,
964
+ questions: parsed.questions,
965
+ model: spec.id,
966
+ options: parsed.options
967
+ };
968
+ const request = toSystemOneRequest(input, spec.id);
969
+ const { runtime } = await ensureRuntime(spec);
970
+ const response = await runtime.decide(request);
971
+ const output = fromSystemOneResponse(response, parsed.questions, spec.id);
972
+ return {
973
+ content: [{ type: "text", text: JSON.stringify(output) }],
974
+ structuredContent: output
975
+ };
976
+ } catch (err) {
977
+ const structured = errorJson(err);
978
+ log.error("clef_decide failed", structured.error);
979
+ return {
980
+ isError: true,
981
+ content: [{ type: "text", text: JSON.stringify(structured) }]
982
+ };
983
+ }
984
+ }
985
+ );
986
+ async function shutdown() {
987
+ if (loaded) {
988
+ await loaded.runtime.unload();
989
+ loaded = void 0;
990
+ }
991
+ }
992
+ async function start() {
993
+ const transport = new StdioServerTransport();
994
+ await server.connect(transport);
995
+ log.debug("clef-mcp listening on stdio");
996
+ const exitAfterUnload = (signal) => {
997
+ log.debug("shutting down", { signal });
998
+ void shutdown().finally(() => process.exit(0));
999
+ };
1000
+ process.once("SIGINT", exitAfterUnload);
1001
+ process.once("SIGTERM", exitAfterUnload);
1002
+ process.stdin.once("end", () => {
1003
+ void shutdown().finally(() => process.exit(0));
1004
+ });
1005
+ }
1006
+ return { server, start, shutdown };
1007
+ }
1008
+
1009
+ // src/cli/index.ts
1010
+ import { Command } from "commander";
1011
+
1012
+ // src/cli/doctor.ts
1013
+ import { execFile as execFile2 } from "child_process";
1014
+ import os3 from "os";
1015
+ import { promisify } from "util";
1016
+ import fsp2 from "fs/promises";
1017
+ import path5 from "path";
1018
+
1019
+ // src/models/platform.ts
1020
+ import { execFile } from "child_process";
1021
+ import os2 from "os";
1022
+ function exec(cmd, args, timeoutMs) {
1023
+ return new Promise((resolve, reject) => {
1024
+ execFile(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
1025
+ if (err) reject(err);
1026
+ else resolve(stdout);
1027
+ });
1028
+ });
1029
+ }
1030
+ function parseSizeToBytes(raw) {
1031
+ if (!raw) return void 0;
1032
+ const m = raw.trim().match(/^([\d.]+)\s*(GB|MB|GiB|MiB|B)?$/i);
1033
+ if (!m) return void 0;
1034
+ const n = Number.parseFloat(m[1]);
1035
+ if (!Number.isFinite(n)) return void 0;
1036
+ const unit = (m[2] ?? "B").toLowerCase();
1037
+ const mult = unit === "gb" || unit === "gib" ? 1024 ** 3 : unit === "mb" || unit === "mib" ? 1024 ** 2 : 1;
1038
+ return Math.round(n * mult);
1039
+ }
1040
+ async function gpuInfoDarwin() {
1041
+ try {
1042
+ const out = await exec("system_profiler", ["SPDisplaysDataType", "-json"], 1e4);
1043
+ const parsed = JSON.parse(out);
1044
+ const gpu = parsed.SPDisplaysDataType?.[0];
1045
+ if (!gpu) return void 0;
1046
+ const info = { name: typeof gpu.sppci_model === "string" ? gpu.sppci_model : void 0 };
1047
+ for (const key of ["spdisplays_vram_shared", "spdisplays_vram"]) {
1048
+ const bytes = parseSizeToBytes(typeof gpu[key] === "string" ? gpu[key] : void 0);
1049
+ if (bytes) {
1050
+ info.vramBytes = bytes;
1051
+ break;
1052
+ }
1053
+ }
1054
+ return info;
1055
+ } catch {
1056
+ return void 0;
1057
+ }
1058
+ }
1059
+ async function gpuInfoLinux() {
1060
+ try {
1061
+ const out = await exec("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader"], 5e3);
1062
+ const [name, mib] = out.trim().split(",").map((s) => s.trim());
1063
+ if (name && mib) return { name, vramBytes: Number.parseFloat(mib) * 1024 ** 2 };
1064
+ return void 0;
1065
+ } catch {
1066
+ return void 0;
1067
+ }
1068
+ }
1069
+ async function detectPlatform() {
1070
+ const platform = process.platform;
1071
+ const info = {
1072
+ platform,
1073
+ arch: process.arch,
1074
+ totalMemBytes: os2.totalmem()
1075
+ };
1076
+ if (platform === "darwin") info.gpu = await gpuInfoDarwin();
1077
+ else if (platform === "linux") info.gpu = await gpuInfoLinux();
1078
+ return info;
1079
+ }
1080
+ function assertSupportedPlatform(info) {
1081
+ const ok = info.platform === "darwin" && (info.arch === "arm64" || info.arch === "x64") || info.platform === "linux" && (info.arch === "x64" || info.arch === "arm64") || info.platform === "win32" && info.arch === "x64";
1082
+ if (!ok) {
1083
+ throw new ClefError(
1084
+ ClefErrorCode.UNSUPPORTED_PLATFORM,
1085
+ `Unsupported platform: ${info.platform}/${info.arch}`,
1086
+ "clef-mcp v0.1 supports macOS (arm64/x64), Linux (x64/arm64) and Windows x64."
1087
+ );
1088
+ }
1089
+ }
1090
+ function formatBytes(bytes) {
1091
+ const gib = bytes / 1024 ** 3;
1092
+ if (gib >= 1) return `${gib.toFixed(1)} GB`;
1093
+ return `${Math.round(bytes / 1024 ** 2)} MB`;
1094
+ }
1095
+
1096
+ // src/runtime/probe.ts
1097
+ import fs3 from "fs/promises";
1098
+ async function probeInference(cfg, manifest, log, loadTimeoutMs = 12e4) {
1099
+ await fs3.access(manifest.modelPath).catch(() => {
1100
+ throw new ClefError(
1101
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1102
+ `Model file is missing: ${manifest.modelPath}`,
1103
+ "Run `clef-mcp install` to (re-)download the model."
1104
+ );
1105
+ });
1106
+ const binary = resolveLlamaServerBinary(cfg);
1107
+ const runtime = new LlamaCppRuntime({
1108
+ serverBin: binary.path,
1109
+ modelPath: manifest.modelPath,
1110
+ alias: manifest.model,
1111
+ loadTimeoutMs,
1112
+ log
1113
+ });
1114
+ const started = Date.now();
1115
+ await runtime.load();
1116
+ try {
1117
+ await runtime.decide({
1118
+ model: manifest.model,
1119
+ state: "This is an internal clef-mcp installation probe.",
1120
+ questions: {
1121
+ probe: { type: "noul", instructions: "Is this a clef-mcp installation probe?" }
1122
+ }
1123
+ });
1124
+ } finally {
1125
+ await runtime.unload();
1126
+ }
1127
+ return { latencyMs: Date.now() - started, binarySource: binary.source, binaryPath: binary.path };
1128
+ }
1129
+
1130
+ // src/cli/doctor.ts
1131
+ var execFileP = promisify(execFile2);
1132
+ function mark(ok) {
1133
+ return ok ? "\u2714" : "\u2718";
1134
+ }
1135
+ function printCheck(check) {
1136
+ const skip = check.informational && !check.ok ? "\u2139" : mark(check.ok);
1137
+ console.log(`${skip} ${check.name}${check.detail ? ` \u2014 ${check.detail}` : ""}`);
1138
+ if (check.hint) console.log(` hint: ${check.hint}`);
1139
+ }
1140
+ async function checkBinaryVersion(binaryPath) {
1141
+ try {
1142
+ const { stdout } = await execFileP(binaryPath, ["--version"], { timeout: 2e4 });
1143
+ return stdout.trim().split("\n")[0];
1144
+ } catch {
1145
+ return void 0;
1146
+ }
1147
+ }
1148
+ async function detectMcpConfigs() {
1149
+ const home = os3.homedir();
1150
+ const hints = [
1151
+ { client: "Codex", path: path5.join(home, ".codex", "config.toml"), configured: false },
1152
+ { client: "Claude Code (user)", path: path5.join(home, ".claude.json"), configured: false },
1153
+ { client: "Claude Code (project)", path: path5.resolve(".mcp.json"), configured: false },
1154
+ { client: "Cursor", path: path5.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
1155
+ ];
1156
+ for (const hint of hints) {
1157
+ try {
1158
+ const content = await fsp2.readFile(hint.path, "utf8");
1159
+ hint.configured = content.includes("clef-mcp");
1160
+ } catch {
1161
+ hint.configured = false;
1162
+ }
1163
+ }
1164
+ return hints;
1165
+ }
1166
+ async function runDoctor(opts) {
1167
+ const config = loadConfig();
1168
+ const checks = [];
1169
+ const { modelsDir } = pathsFor(config.clefHome);
1170
+ const info = await detectPlatform();
1171
+ try {
1172
+ assertSupportedPlatform(info);
1173
+ checks.push({ name: "OS / architecture", ok: true, detail: `${info.platform}/${info.arch}` });
1174
+ } catch (err) {
1175
+ checks.push({
1176
+ name: "OS / architecture",
1177
+ ok: false,
1178
+ detail: `${info.platform}/${info.arch}`,
1179
+ hint: err instanceof Error ? err.message : String(err)
1180
+ });
1181
+ }
1182
+ const memGb = info.totalMemBytes / 1024 ** 3;
1183
+ checks.push({
1184
+ name: "Memory",
1185
+ ok: memGb >= 16,
1186
+ informational: true,
1187
+ detail: formatBytes(info.totalMemBytes),
1188
+ hint: memGb < 16 ? "Clef-Flash Q4_K_M needs ~16 GB of memory to run comfortably." : void 0
1189
+ });
1190
+ checks.push({
1191
+ name: "GPU",
1192
+ ok: true,
1193
+ informational: true,
1194
+ 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)"
1195
+ });
1196
+ let binaryPath;
1197
+ try {
1198
+ const binary = resolveLlamaServerBinary(config);
1199
+ binaryPath = binary.path;
1200
+ const version = await checkBinaryVersion(binary.path);
1201
+ checks.push({
1202
+ name: "Runtime binary",
1203
+ ok: version !== void 0,
1204
+ detail: `${binary.source}: ${binary.path}${version ? ` (${version})` : ""}`,
1205
+ hint: version === void 0 ? "The binary exists but did not answer --version; it may be broken or incompatible." : void 0
1206
+ });
1207
+ } catch (err) {
1208
+ checks.push({
1209
+ name: "Runtime binary",
1210
+ ok: false,
1211
+ detail: err instanceof Error ? err.message : String(err),
1212
+ hint: err instanceof ClefError ? err.hint : void 0
1213
+ });
1214
+ }
1215
+ let spec;
1216
+ let manifest;
1217
+ try {
1218
+ spec = getModelSpec(config.model);
1219
+ manifest = await findInstalledManifest(config.clefHome, spec);
1220
+ } catch {
1221
+ checks.push({ name: "Model", ok: false, detail: `unknown model id "${config.model}"` });
1222
+ }
1223
+ if (spec) {
1224
+ if (!manifest) {
1225
+ checks.push({
1226
+ name: "Model",
1227
+ ok: false,
1228
+ detail: `${spec.id} is not installed`,
1229
+ hint: "Run `clef-mcp install`."
1230
+ });
1231
+ } else {
1232
+ const verify = await verifyModelFiles(manifest);
1233
+ let detail = `${manifest.quant} at ${manifest.modelPath} (${formatBytes(manifest.bytes)})`;
1234
+ let ok = verify.ok;
1235
+ if (ok && opts.deep) {
1236
+ const deep = await deepVerifyChecksum(manifest);
1237
+ ok = deep.ok;
1238
+ detail += deep.ok ? " \u2014 checksum OK" : ` \u2014 ${deep.problems.join("; ")}`;
1239
+ } else if (ok) {
1240
+ detail += " \u2014 size OK (use --deep to verify checksum)";
1241
+ }
1242
+ checks.push({
1243
+ name: "Model",
1244
+ ok,
1245
+ detail,
1246
+ hint: ok ? void 0 : "Re-run `clef-mcp install` to re-download the model."
1247
+ });
1248
+ checks.push({
1249
+ name: "Model license",
1250
+ ok: true,
1251
+ informational: true,
1252
+ detail: `${spec.licenseId} \u2014 ${spec.licenseUrl}`
1253
+ });
1254
+ }
1255
+ }
1256
+ if (binaryPath && manifest && (await verifyModelFiles(manifest)).ok) {
1257
+ try {
1258
+ const probe = await probeInference(config, manifest, void 0, 18e4);
1259
+ checks.push({
1260
+ name: "Inference",
1261
+ ok: true,
1262
+ detail: `probe decision returned in ${(probe.latencyMs / 1e3).toFixed(1)}s (incl. model load)`
1263
+ });
1264
+ } catch (err) {
1265
+ const clefErr = err instanceof ClefError ? err : void 0;
1266
+ checks.push({
1267
+ name: "Inference",
1268
+ ok: false,
1269
+ detail: clefErr?.message ?? (err instanceof Error ? err.message : String(err)),
1270
+ hint: clefErr?.hint ?? "The installed llama.cpp build may lack clef support; update llama.cpp or run `clef-mcp install`."
1271
+ });
1272
+ }
1273
+ } else {
1274
+ checks.push({
1275
+ name: "Inference",
1276
+ ok: false,
1277
+ informational: true,
1278
+ detail: "skipped (model or runtime missing)"
1279
+ });
1280
+ }
1281
+ const mcpHints = await detectMcpConfigs();
1282
+ const configured = mcpHints.filter((h) => h.configured);
1283
+ checks.push({
1284
+ name: "MCP client configuration",
1285
+ ok: true,
1286
+ informational: true,
1287
+ detail: configured.length ? configured.map((h) => `${h.client}: ${h.path}`).join("; ") : "no clef-mcp registration found (add it to your client; see README)"
1288
+ });
1289
+ if (opts.json) {
1290
+ console.log(JSON.stringify({ clefHome: pathsFor(config.clefHome).root, modelCache: modelsDir, checks }, null, 2));
1291
+ } else {
1292
+ console.log(`clef-mcp doctor (cache: ${pathsFor(config.clefHome).root})
1293
+ `);
1294
+ for (const check of checks) printCheck(check);
1295
+ }
1296
+ const failed = checks.filter((c) => !c.ok && !c.informational);
1297
+ if (!opts.json) {
1298
+ console.log(
1299
+ failed.length === 0 ? "\nAll critical checks passed." : `
1300
+ ${failed.length} critical check(s) failed.`
1301
+ );
1302
+ }
1303
+ return failed.length > 0;
1304
+ }
1305
+
1306
+ // src/cli/evals.ts
1307
+ import fsp3 from "fs/promises";
1308
+ import fsSync from "fs";
1309
+ import { fileURLToPath } from "url";
1310
+ import path6 from "path";
1311
+ function defaultDatasetPath() {
1312
+ let dir = path6.dirname(fileURLToPath(import.meta.url));
1313
+ for (let i = 0; i < 6; i++) {
1314
+ const candidate = path6.join(dir, "tests", "evals", "dataset.jsonl");
1315
+ if (fsSync.existsSync(candidate)) return candidate;
1316
+ dir = path6.dirname(dir);
1317
+ }
1318
+ return fileURLToPath(new URL("../tests/evals/dataset.jsonl", import.meta.url));
1319
+ }
1320
+ function parseLine(line, lineNo) {
1321
+ const raw = JSON.parse(line);
1322
+ if (typeof raw.id !== "string" || !raw.question?.id || !raw.expected?.kind) {
1323
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `Eval dataset line ${lineNo} is missing id/question/expected`);
1324
+ }
1325
+ return raw;
1326
+ }
1327
+ function evaluate(answer, exp) {
1328
+ const prob = (key) => answer[key] ?? 0;
1329
+ switch (exp.kind) {
1330
+ case "argmax": {
1331
+ const keys = exp.among ?? Object.keys(answer);
1332
+ let best = "";
1333
+ let bestP = -1;
1334
+ for (const k of keys) {
1335
+ if (prob(k) > bestP) {
1336
+ bestP = prob(k);
1337
+ best = k;
1338
+ }
1339
+ }
1340
+ return { pass: best === exp.expect, detail: `argmax=${best || "n/a"} (${bestP.toFixed(3)}), expected=${exp.expect}` };
1341
+ }
1342
+ case "prob_gt":
1343
+ 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)}` };
1344
+ case "min_prob": {
1345
+ const p = prob(exp.option);
1346
+ return { pass: p >= exp.min, detail: `p(${exp.option})=${p.toFixed(3)}, required >= ${exp.min}` };
1347
+ }
1348
+ case "top_mass": {
1349
+ const mass = exp.options.reduce((sum, k) => sum + prob(k), 0);
1350
+ return { pass: mass >= exp.min, detail: `mass(${exp.options.join("+")})=${mass.toFixed(3)}, required >= ${exp.min}` };
1351
+ }
1352
+ }
1353
+ }
1354
+ async function runEvals(opts) {
1355
+ const config = loadConfig();
1356
+ const log = createLogger(config.logLevel);
1357
+ const spec = getModelSpec(config.model);
1358
+ const manifest = await findInstalledManifest(config.clefHome, spec);
1359
+ if (!manifest) {
1360
+ throw new ClefError(
1361
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1362
+ `Model "${spec.id}" is not installed; evals need the local model.`,
1363
+ "Run `clef-mcp install` first."
1364
+ );
1365
+ }
1366
+ const datasetPath = opts.dataset ?? defaultDatasetPath();
1367
+ const content = await fsp3.readFile(datasetPath, "utf8");
1368
+ let cases = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l, i) => parseLine(l, i + 1));
1369
+ if (opts.limit && opts.limit > 0) cases = cases.slice(0, opts.limit);
1370
+ if (cases.length === 0) {
1371
+ throw new ClefError(ClefErrorCode.INVALID_INPUT, `No eval cases found in ${datasetPath}`);
1372
+ }
1373
+ const binary = resolveLlamaServerBinary(config);
1374
+ const runtime = new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
1375
+ console.log(`Running ${cases.length} eval case(s) with ${spec.id} ${manifest.quant}...
1376
+ `);
1377
+ await runtime.load();
1378
+ let passed = 0;
1379
+ const results = [];
1380
+ try {
1381
+ for (const [i, c] of cases.entries()) {
1382
+ process.stderr.write(`\r case ${i + 1}/${cases.length} \u2014 ${c.id}`);
1383
+ try {
1384
+ const request = toSystemOneRequest({ state: c.state, questions: { [c.question.id]: c.question } }, spec.id);
1385
+ const response = await runtime.decide(request);
1386
+ const output = fromSystemOneResponse(response, { [c.question.id]: c.question }, spec.id);
1387
+ const answer = output.decisions[c.question.id]?.answer ?? {};
1388
+ const verdict = evaluate(answer, c.expected);
1389
+ if (verdict.pass) passed++;
1390
+ results.push({ id: c.id, category: c.category, ok: verdict.pass, detail: verdict.detail });
1391
+ } catch (err) {
1392
+ results.push({
1393
+ id: c.id,
1394
+ category: c.category,
1395
+ ok: false,
1396
+ detail: `error: ${err instanceof Error ? err.message : String(err)}`
1397
+ });
1398
+ }
1399
+ }
1400
+ } finally {
1401
+ process.stderr.write("\n");
1402
+ await runtime.unload();
1403
+ }
1404
+ console.log("");
1405
+ for (const r of results) {
1406
+ console.log(`${r.ok ? "\u2714" : "\u2718"} [${r.category}] ${r.id} \u2014 ${r.detail}`);
1407
+ }
1408
+ const rate = passed / results.length;
1409
+ const minRate = opts.minRate ?? 0.8;
1410
+ console.log(`
1411
+ ${passed}/${results.length} passed (${(rate * 100).toFixed(1)}%), required >= ${(minRate * 100).toFixed(0)}%`);
1412
+ return rate >= minRate;
1413
+ }
1414
+
1415
+ // src/models/llama-install.ts
1416
+ import { execFile as execFile3 } from "child_process";
1417
+ import fsp5 from "fs/promises";
1418
+ import path8 from "path";
1419
+
1420
+ // src/models/hf.ts
1421
+ import { createHash as createHash2 } from "crypto";
1422
+ import fsp4 from "fs/promises";
1423
+ import path7 from "path";
1424
+ var HF_BASE_URL = "https://huggingface.co";
1425
+ function resolveUrl(repo, revision, file) {
1426
+ return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
1427
+ }
1428
+ async function fetchFileInfo(repo, revision, file) {
1429
+ const url = resolveUrl(repo, revision, file);
1430
+ let res;
1431
+ try {
1432
+ res = await fetch(url, { method: "HEAD", redirect: "manual" });
1433
+ } catch (err) {
1434
+ throw new ClefError(
1435
+ ClefErrorCode.DOWNLOAD_FAILED,
1436
+ `Could not reach Hugging Face: ${err instanceof Error ? err.message : String(err)}`,
1437
+ "Check network connectivity and try again."
1438
+ );
1439
+ }
1440
+ if (res.status !== 302 && res.status !== 200) {
1441
+ throw new ClefError(
1442
+ ClefErrorCode.DOWNLOAD_FAILED,
1443
+ `Hugging Face returned HTTP ${res.status} for ${repo}@${revision}/${file}`,
1444
+ res.status === 404 ? "The pinned model revision may have moved. Re-check the model registry." : void 0
1445
+ );
1446
+ }
1447
+ const sha256 = res.headers.get("x-linked-etag")?.replace(/"/g, "");
1448
+ const size = res.headers.get("x-linked-size") ? Number(res.headers.get("x-linked-size")) : void 0;
1449
+ return {
1450
+ downloadUrl: res.headers.get("location") ?? url,
1451
+ sha256: sha256 && /^[0-9a-f]{64}$/i.test(sha256) ? sha256 : void 0,
1452
+ size: Number.isFinite(size) ? size : void 0
1453
+ };
1454
+ }
1455
+ async function downloadFile(url, dest, opts = {}) {
1456
+ const attempts = opts.attempts ?? 3;
1457
+ let lastError;
1458
+ for (let attempt = 1; attempt <= attempts; attempt++) {
1459
+ try {
1460
+ return await downloadOnce(url, dest, opts);
1461
+ } catch (err) {
1462
+ if (err instanceof ClefError && err.code === ClefErrorCode.CHECKSUM_MISMATCH) throw err;
1463
+ lastError = err;
1464
+ if (attempt < attempts) {
1465
+ await new Promise((r) => setTimeout(r, 1e3 * 2 ** (attempt - 1)));
1466
+ }
1467
+ }
1468
+ }
1469
+ throw new ClefError(
1470
+ ClefErrorCode.DOWNLOAD_FAILED,
1471
+ `Download failed after ${attempts} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`
1472
+ );
1473
+ }
1474
+ async function downloadOnce(url, dest, opts) {
1475
+ const res = await fetch(url, { redirect: "follow", signal: opts.signal });
1476
+ if (!res.ok || !res.body) {
1477
+ throw new Error(`HTTP ${res.status}`);
1478
+ }
1479
+ const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
1480
+ const hash = createHash2("sha256");
1481
+ const handle = await fsp4.open(dest, "w");
1482
+ let bytes = 0;
1483
+ try {
1484
+ for await (const chunk of res.body) {
1485
+ const buf = chunk;
1486
+ hash.update(buf);
1487
+ await handle.write(buf);
1488
+ bytes += buf.length;
1489
+ opts.onProgress?.({ bytes, total });
1490
+ }
1491
+ } finally {
1492
+ await handle.close();
1493
+ }
1494
+ const sha256 = hash.digest("hex");
1495
+ if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
1496
+ await fsp4.rm(dest, { force: true });
1497
+ throw new ClefError(
1498
+ ClefErrorCode.CHECKSUM_MISMATCH,
1499
+ `Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
1500
+ "The download was discarded. Re-run `clef-mcp install`."
1501
+ );
1502
+ }
1503
+ if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
1504
+ await fsp4.rm(dest, { force: true });
1505
+ throw new ClefError(
1506
+ ClefErrorCode.DOWNLOAD_FAILED,
1507
+ `Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
1508
+ );
1509
+ }
1510
+ return { bytes, sha256 };
1511
+ }
1512
+ async function installModel(opts) {
1513
+ const { modelSpec, quantSpec } = opts;
1514
+ const { modelsDir } = pathsFor(opts.clefHome);
1515
+ const manifestPath = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
1516
+ const existing = await readModelManifest(manifestPath);
1517
+ if (existing && (await verifyModelFiles(existing)).ok) {
1518
+ opts.log?.debug("model already installed", { model: modelSpec.id, quant: quantSpec.id });
1519
+ return { manifest: existing, alreadyInstalled: true };
1520
+ }
1521
+ const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
1522
+ const finalPath = path7.join(path7.dirname(manifestPath), quantSpec.file);
1523
+ await fsp4.mkdir(path7.dirname(finalPath), { recursive: true });
1524
+ const tmpPath = path7.join(path7.dirname(finalPath), `.download-${quantSpec.file}`);
1525
+ await fsp4.rm(tmpPath, { force: true });
1526
+ opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
1527
+ const result = await downloadFile(info.downloadUrl, tmpPath, {
1528
+ expectedSha256: info.sha256,
1529
+ expectedSize: info.size ?? quantSpec.bytes,
1530
+ onProgress: opts.onProgress
1531
+ });
1532
+ await fsp4.rename(tmpPath, finalPath);
1533
+ const manifest = {
1534
+ schemaVersion: 1,
1535
+ model: modelSpec.id,
1536
+ quant: quantSpec.id,
1537
+ file: quantSpec.file,
1538
+ modelPath: finalPath,
1539
+ bytes: result.bytes,
1540
+ sha256: result.sha256,
1541
+ repo: modelSpec.repo,
1542
+ revision: modelSpec.revision,
1543
+ licenseId: modelSpec.licenseId,
1544
+ licenseUrl: modelSpec.licenseUrl,
1545
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1546
+ clefMcpVersion: opts.clefMcpVersion
1547
+ };
1548
+ await writeModelManifest(manifestPath, manifest);
1549
+ opts.log?.info("model installed", { model: modelSpec.id, quant: quantSpec.id, sha256: result.sha256 });
1550
+ return { manifest, alreadyInstalled: false };
1551
+ }
1552
+
1553
+ // src/models/llama-install.ts
1554
+ var GITHUB_API = "https://api.github.com/repos/ggml-org/llama.cpp";
1555
+ async function ghJson(url) {
1556
+ let res;
1557
+ try {
1558
+ res = await fetch(url, {
1559
+ headers: { accept: "application/vnd.github+json", "user-agent": "clef-mcp" }
1560
+ });
1561
+ } catch (err) {
1562
+ throw new ClefError(
1563
+ ClefErrorCode.DOWNLOAD_FAILED,
1564
+ `Could not reach GitHub: ${err instanceof Error ? err.message : String(err)}`
1565
+ );
1566
+ }
1567
+ if (!res.ok) {
1568
+ throw new ClefError(
1569
+ ClefErrorCode.DOWNLOAD_FAILED,
1570
+ `GitHub API returned HTTP ${res.status} for ${url}`,
1571
+ res.status === 403 ? "GitHub API rate limit may be hit; install llama.cpp manually and retry." : void 0
1572
+ );
1573
+ }
1574
+ return await res.json();
1575
+ }
1576
+ function assetPatterns(platform, arch) {
1577
+ switch (platform) {
1578
+ case "darwin":
1579
+ return arch === "arm64" ? { match: /^llama-.*-bin-macos-arm64\.tar\.gz$/ } : { match: /^llama-.*-bin-macos-x64\.tar\.gz$/ };
1580
+ case "linux":
1581
+ return arch === "arm64" ? { match: /^llama-.*-bin-ubuntu-arm64\.tar\.gz$/ } : { match: /^llama-.*-bin-ubuntu-x64\.tar\.gz$/ };
1582
+ case "win32":
1583
+ return {
1584
+ match: /^llama-.*-bin-win-avx2-x64\.zip$/,
1585
+ alternates: [/^llama-.*-bin-win-noavx-x64\.zip$/, /^llama-.*-bin-win-x64\.zip$/]
1586
+ };
1587
+ default:
1588
+ throw new ClefError(ClefErrorCode.UNSUPPORTED_PLATFORM, `Unsupported platform ${platform}/${arch}`);
1589
+ }
1590
+ }
1591
+ function pickAsset(release, platform, arch) {
1592
+ const { match, alternates } = assetPatterns(platform, arch);
1593
+ const names = release.assets.map((a) => a.name.toLowerCase());
1594
+ const find = (re) => release.assets[names.findIndex((n) => re.test(n))];
1595
+ const asset = find(match) ?? alternates?.map(find).find(Boolean);
1596
+ if (!asset) {
1597
+ throw new ClefError(
1598
+ ClefErrorCode.DOWNLOAD_FAILED,
1599
+ `No llama.cpp release asset for ${platform}/${arch} in ${release.tag_name}`,
1600
+ "Install llama.cpp manually (`brew install llama.cpp` or https://llama.app) and re-run install."
1601
+ );
1602
+ }
1603
+ return asset;
1604
+ }
1605
+ function execTar(args, cwd) {
1606
+ return new Promise((resolve, reject) => {
1607
+ execFile3("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
1608
+ if (err) reject(new Error(`tar ${args.join(" ")} failed: ${stderr || err.message}`));
1609
+ else resolve();
1610
+ });
1611
+ });
1612
+ }
1613
+ async function findServerBinary(dir, platform) {
1614
+ const target = llamaServerName(platform);
1615
+ const queue = [dir];
1616
+ while (queue.length > 0) {
1617
+ const current = queue.shift();
1618
+ let entries;
1619
+ try {
1620
+ entries = await fsp5.readdir(current, { withFileTypes: true });
1621
+ } catch {
1622
+ continue;
1623
+ }
1624
+ for (const entry of entries) {
1625
+ const full = path8.join(current, entry.name);
1626
+ if (entry.isDirectory()) queue.push(full);
1627
+ else if (entry.name === target) return full;
1628
+ }
1629
+ }
1630
+ throw new ClefError(
1631
+ ClefErrorCode.DOWNLOAD_FAILED,
1632
+ `Extracted llama.cpp release did not contain ${target}`
1633
+ );
1634
+ }
1635
+ async function extractRelease(archivePath, destDir, platform) {
1636
+ await fsp5.mkdir(destDir, { recursive: true });
1637
+ const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
1638
+ try {
1639
+ await execTar(args, destDir);
1640
+ } catch (err) {
1641
+ throw new ClefError(
1642
+ ClefErrorCode.DOWNLOAD_FAILED,
1643
+ `Could not extract ${path8.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
1644
+ "Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
1645
+ );
1646
+ }
1647
+ }
1648
+ async function ensureLlamaRuntime(cfg, log) {
1649
+ try {
1650
+ const resolved = resolveLlamaServerBinary(cfg);
1651
+ if (resolved.source === "managed") {
1652
+ const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
1653
+ const manifest = readRuntimeManifest(path8.join(platformDir, "runtime-manifest.json"));
1654
+ return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
1655
+ }
1656
+ return { ...resolved, releaseTag: resolved.source, freshInstall: false };
1657
+ } catch (err) {
1658
+ if (!(err instanceof ClefError) || err.code !== ClefErrorCode.RUNTIME_NOT_FOUND) throw err;
1659
+ }
1660
+ const fresh = await downloadManagedLlama(cfg, log);
1661
+ return { ...fresh, freshInstall: true };
1662
+ }
1663
+ async function downloadManagedLlama(cfg, log) {
1664
+ const platform = process.platform;
1665
+ const arch = process.arch;
1666
+ const { runtimeDir } = pathsFor(cfg.clefHome);
1667
+ const platformDir = llamaRuntimeDir(runtimeDir, platform, arch);
1668
+ let release;
1669
+ if (cfg.llamaReleaseTag) {
1670
+ release = await ghJson(`${GITHUB_API}/releases/tags/${cfg.llamaReleaseTag}`);
1671
+ } else {
1672
+ const releases = await ghJson(`${GITHUB_API}/releases?per_page=15`);
1673
+ const nightly = releases.find((r) => /^b\d+$/.test(r.tag_name));
1674
+ if (!nightly) {
1675
+ throw new ClefError(
1676
+ ClefErrorCode.DOWNLOAD_FAILED,
1677
+ "No llama.cpp nightly release found on GitHub.",
1678
+ "Install llama.cpp manually (`brew install llama.cpp` or https://llama.app) and re-run install."
1679
+ );
1680
+ }
1681
+ release = nightly;
1682
+ }
1683
+ const asset = pickAsset(release, platform, arch);
1684
+ log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
1685
+ const tagDir = path8.join(platformDir, release.tag_name);
1686
+ await fsp5.mkdir(tagDir, { recursive: true });
1687
+ const tmpArchive = path8.join(platformDir, `.download-${asset.name}`);
1688
+ await fsp5.rm(tmpArchive, { force: true });
1689
+ const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
1690
+ await extractRelease(tmpArchive, tagDir, platform);
1691
+ await fsp5.rm(tmpArchive, { force: true });
1692
+ const binaryPath = await findServerBinary(tagDir, platform);
1693
+ if (platform !== "win32") await fsp5.chmod(binaryPath, 493);
1694
+ await writeRuntimeManifest(path8.join(platformDir, "runtime-manifest.json"), {
1695
+ schemaVersion: 1,
1696
+ kind: "llama-cpp",
1697
+ releaseTag: release.tag_name,
1698
+ asset: asset.name,
1699
+ bytes: result.bytes,
1700
+ binaryPath,
1701
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
1702
+ });
1703
+ log?.info("llama.cpp runtime installed", { tag: release.tag_name, path: binaryPath });
1704
+ return { source: "managed", path: binaryPath, releaseTag: release.tag_name };
1705
+ }
1706
+ async function removeManagedRuntime(cfg) {
1707
+ const { runtimeDir } = pathsFor(cfg.clefHome);
1708
+ const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
1709
+ try {
1710
+ await fsp5.stat(dir);
1711
+ } catch {
1712
+ return false;
1713
+ }
1714
+ await fsp5.rm(dir, { recursive: true, force: true });
1715
+ return true;
1716
+ }
1717
+
1718
+ // src/cli/prompt.ts
1719
+ import readline from "readline/promises";
1720
+ async function confirm(question, yes) {
1721
+ if (yes) return true;
1722
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1723
+ throw new ClefError(
1724
+ ClefErrorCode.INVALID_INPUT,
1725
+ "Refusing to proceed interactively without a terminal.",
1726
+ "Re-run with --yes to confirm non-interactively."
1727
+ );
1728
+ }
1729
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1730
+ try {
1731
+ const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
1732
+ return answer === "" || answer === "y" || answer === "yes";
1733
+ } finally {
1734
+ rl.close();
1735
+ }
1736
+ }
1737
+
1738
+ // src/version.ts
1739
+ var VERSION = "0.1.0";
1740
+
1741
+ // src/cli/install.ts
1742
+ var OS_NAMES = {
1743
+ darwin: "macOS",
1744
+ linux: "Linux",
1745
+ win32: "Windows"
1746
+ };
1747
+ function prettyOs(platform) {
1748
+ return OS_NAMES[platform] ?? platform;
1749
+ }
1750
+ function printProgress(downloaded, total) {
1751
+ const dl = formatBytes(downloaded);
1752
+ const line = total ? ` Downloading... ${dl} / ${formatBytes(total)}` : ` Downloading... ${dl}`;
1753
+ process.stderr.write(`\r${line.padEnd(48)}`);
1754
+ }
1755
+ function printHeader() {
1756
+ console.log();
1757
+ }
1758
+ async function runInstall(opts) {
1759
+ const config = loadConfig();
1760
+ const log = createLogger("info");
1761
+ printHeader();
1762
+ process.stdout.write("Detecting hardware...\n");
1763
+ const info = await detectPlatform();
1764
+ const modelId = opts.model ?? config.model;
1765
+ const spec = getModelSpec(modelId);
1766
+ const quant = pickQuant(spec, info.totalMemBytes, opts.quant);
1767
+ const gpuLine = info.gpu?.name ? ` GPU: ${info.gpu.name}${info.gpu.vramBytes ? ` (${formatBytes(info.gpu.vramBytes)})` : ""}` : void 0;
1768
+ console.log("\nDetected:");
1769
+ console.log(` OS: ${prettyOs(info.platform)}`);
1770
+ console.log(` Architecture: ${info.arch}`);
1771
+ console.log(` Memory: ${formatBytes(info.totalMemBytes)}`);
1772
+ if (gpuLine) console.log(gpuLine);
1773
+ console.log("\nRecommended:");
1774
+ console.log(` Model: ${spec.id} (${spec.repo})`);
1775
+ console.log(` Quantization: ${quant.id}`);
1776
+ console.log(` Download size: ~${formatBytes(quant.bytes)}`);
1777
+ console.log(`
1778
+ Model license: ${spec.licenseId} \u2014 ${spec.licenseUrl}`);
1779
+ console.log("The model is downloaded from the official GGUF conversion and is not repackaged by clef-mcp.\n");
1780
+ const ok = await confirm("Continue?", opts.yes === true);
1781
+ if (!ok) {
1782
+ console.log("Aborted. Nothing was downloaded.");
1783
+ return;
1784
+ }
1785
+ let runtime;
1786
+ if (!opts.skipRuntime) {
1787
+ process.stdout.write("\nEnsuring llama.cpp runtime...\n");
1788
+ runtime = await ensureLlamaRuntime(config, log);
1789
+ console.log(` Runtime: llama.cpp (${runtime.source}, ${runtime.releaseTag})`);
1790
+ console.log(` Binary: ${runtime.path}`);
1791
+ }
1792
+ console.log("");
1793
+ const { manifest, alreadyInstalled } = await installModel({
1794
+ clefHome: config.clefHome,
1795
+ modelSpec: spec,
1796
+ quantSpec: quant,
1797
+ clefMcpVersion: VERSION,
1798
+ onProgress: (p) => printProgress(p.bytes, p.total),
1799
+ log
1800
+ });
1801
+ process.stderr.write("\n");
1802
+ console.log(alreadyInstalled ? ` Model already installed: ${manifest.modelPath}` : ` Installed: ${manifest.modelPath}`);
1803
+ console.log(` sha256: ${manifest.sha256 ?? "n/a (not advertised upstream)"}`);
1804
+ if (!opts.skipProbe) {
1805
+ process.stdout.write("\nVerifying inference (loads the model; first run can take a few minutes)...\n");
1806
+ const probe = await probeInference(config, manifest, log);
1807
+ console.log(` Inference OK (${(probe.latencyMs / 1e3).toFixed(1)}s incl. model load, via ${probe.binarySource} binary)`);
1808
+ }
1809
+ console.log("\nDone. Start the MCP server with:\n\n clef-mcp\n");
1810
+ console.log("Then register it with your MCP client, e.g.:\n");
1811
+ console.log(" Claude Code : claude mcp add clef-mcp -- clef-mcp");
1812
+ console.log(" Codex : add [mcp_servers.clef-mcp] to ~/.codex/config.toml (see README)");
1813
+ console.log(" Cursor : add clef-mcp to .cursor/mcp.json (see README)\n");
1814
+ }
1815
+
1816
+ // src/cli/models-cmd.ts
1817
+ async function runModels() {
1818
+ const config = loadConfig();
1819
+ const { modelsDir } = pathsFor(config.clefHome);
1820
+ console.log(`Cache home: ${pathsFor(config.clefHome).root}
1821
+ `);
1822
+ for (const spec of Object.values(MODELS)) {
1823
+ console.log(`${spec.id} (${spec.repo}, ${spec.licenseId})`);
1824
+ for (const quant of spec.quants) {
1825
+ const installed = await quantIsInstalled(config.clefHome, spec, quant);
1826
+ const defaultMark = quant.id === spec.defaultQuant ? " [default]" : "";
1827
+ console.log(` ${quant.id.padEnd(8)} ~${formatBytes(quant.bytes).padStart(8)} ${installed ? "installed" : "not installed"}${defaultMark}`);
1828
+ }
1829
+ console.log();
1830
+ }
1831
+ }
1832
+
1833
+ // src/cli/status.ts
1834
+ async function runStatus() {
1835
+ const config = loadConfig();
1836
+ const info = await detectPlatform();
1837
+ let spec;
1838
+ try {
1839
+ spec = getModelSpec(config.model);
1840
+ } catch (err) {
1841
+ if (err instanceof ClefError && err.code === "INVALID_INPUT") {
1842
+ console.log(`Runtime: llama.cpp`);
1843
+ console.log(`Model: ${config.model} (unknown model id)`);
1844
+ console.log("Model status: unknown");
1845
+ console.log(`Memory: ${formatBytes(info.totalMemBytes)}`);
1846
+ return;
1847
+ }
1848
+ throw err;
1849
+ }
1850
+ const manifest = await findInstalledManifest(config.clefHome, spec);
1851
+ let runtimeLine;
1852
+ try {
1853
+ const binary = resolveLlamaServerBinary(config);
1854
+ runtimeLine = `ready (llama.cpp via ${binary.source}: ${binary.path})`;
1855
+ } catch (err) {
1856
+ const structured = err.hint ? `${err.message}` : String(err);
1857
+ runtimeLine = `not found \u2014 ${structured}`;
1858
+ }
1859
+ console.log(`Runtime: llama.cpp`);
1860
+ console.log(`Model: ${spec.id}${manifest ? ` ${manifest.quant}` : ""}`);
1861
+ console.log(`Model status: ${manifest ? "installed" : "not installed (run: clef-mcp install)"}`);
1862
+ console.log(`Runtime status: ${runtimeLine}`);
1863
+ console.log(`Memory: ${formatBytes(info.totalMemBytes)}`);
1864
+ if (manifest) {
1865
+ console.log(`Model path: ${manifest.modelPath}`);
1866
+ console.log(`Installed: ${manifest.installedAt}`);
1867
+ } else {
1868
+ console.log(`Cache home: ${pathsFor(config.clefHome).root}`);
1869
+ }
1870
+ }
1871
+
1872
+ // src/cli/uninstall.ts
1873
+ import fsp6 from "fs/promises";
1874
+ async function runUninstall(opts) {
1875
+ const config = loadConfig();
1876
+ const spec = getModelSpec(opts.model ?? config.model);
1877
+ const manifest = await findInstalledManifest(config.clefHome, spec);
1878
+ const { modelsDir } = pathsFor(config.clefHome);
1879
+ if (!manifest && !opts.runtime) {
1880
+ throw new ClefError(
1881
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1882
+ `${spec.id} is not installed.`,
1883
+ "Nothing to remove. Use --runtime to also remove the managed llama.cpp runtime."
1884
+ );
1885
+ }
1886
+ const target = manifest ? modelQuantDir(modelsDir, spec.id, manifest.quant) : void 0;
1887
+ const sizeLine = manifest ? ` (${formatBytes(manifest.bytes)})` : "";
1888
+ const ok = await confirm(
1889
+ `Remove ${spec.id}${manifest ? ` ${manifest.quant}` : ""}${sizeLine}${opts.runtime ? " and the managed runtime" : ""}?`,
1890
+ opts.yes === true
1891
+ );
1892
+ if (!ok) {
1893
+ console.log("Aborted.");
1894
+ return;
1895
+ }
1896
+ if (target) {
1897
+ await fsp6.rm(target, { recursive: true, force: true });
1898
+ console.log(`Removed: ${target}`);
1899
+ }
1900
+ if (opts.runtime) {
1901
+ const removed = await removeManagedRuntime(config);
1902
+ console.log(removed ? "Removed managed llama.cpp runtime." : "No managed runtime found.");
1903
+ }
1904
+ }
1905
+
1906
+ // src/cli/index.ts
1907
+ function handleCliError(err) {
1908
+ const structured = errorJson(err);
1909
+ console.error(`error: [${structured.error.code}] ${structured.error.message}`);
1910
+ if (structured.error.hint) console.error(`hint: ${structured.error.hint}`);
1911
+ process.exit(exitCodeFor(err));
1912
+ }
1913
+ var program = new Command();
1914
+ program.name("clef-mcp").description("Local MCP server for the Clef decision model (structured decisions, probability outputs).").version(VERSION);
1915
+ program.action(async () => {
1916
+ try {
1917
+ const config = loadConfig();
1918
+ const running = createClefServer({ config, version: VERSION });
1919
+ await running.start();
1920
+ } catch (err) {
1921
+ handleCliError(err);
1922
+ }
1923
+ });
1924
+ 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").action(async (opts) => {
1925
+ try {
1926
+ await runInstall(opts);
1927
+ } catch (err) {
1928
+ handleCliError(err);
1929
+ }
1930
+ });
1931
+ program.command("models").description("List known Clef models, quantizations and install status.").action(async () => {
1932
+ try {
1933
+ await runModels();
1934
+ } catch (err) {
1935
+ handleCliError(err);
1936
+ }
1937
+ });
1938
+ program.command("status").description("Show runtime, model and memory status.").action(async () => {
1939
+ try {
1940
+ await runStatus();
1941
+ } catch (err) {
1942
+ handleCliError(err);
1943
+ }
1944
+ });
1945
+ program.command("doctor").description("Diagnose platform, runtime, model, checksum, inference and MCP configuration.").option("--deep", "re-verify model checksum (hashes the full model file)").option("--json", "machine-readable output").action(async (opts) => {
1946
+ try {
1947
+ const failed = await runDoctor(opts);
1948
+ process.exit(failed ? 1 : 0);
1949
+ } catch (err) {
1950
+ handleCliError(err);
1951
+ }
1952
+ });
1953
+ program.command("uninstall").description("Remove the installed model (and optionally the managed runtime).").option("--model <id>", "model to remove (default: CLEF_MODEL or clef-flash)").option("--runtime", "also remove the managed llama.cpp runtime").option("-y, --yes", "assume yes for all prompts (non-interactive)").action(async (opts) => {
1954
+ try {
1955
+ await runUninstall(opts);
1956
+ } catch (err) {
1957
+ handleCliError(err);
1958
+ }
1959
+ });
1960
+ program.command("evals").description("Run the evaluation dataset against the installed model.").option("--dataset <path>", "path to a dataset.jsonl (default: bundled eval dataset)").option("--limit <n>", "limit the number of cases", Number.parseInt).option("--min-rate <r>", "minimum pass rate, 0..1 (default 0.8)", Number.parseFloat).action(async (opts) => {
1961
+ try {
1962
+ const passed = await runEvals(opts);
1963
+ process.exit(passed ? 0 : 1);
1964
+ } catch (err) {
1965
+ if (err instanceof ClefError) handleCliError(err);
1966
+ handleCliError(err);
1967
+ }
1968
+ });
1969
+ program.parseAsync(process.argv).catch(handleCliError);
1970
+ //# sourceMappingURL=cli.js.map