clef-mcp 0.2.0 → 0.3.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 +27 -4
- package/dist/cli.js +557 -165
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +32 -10
- package/dist/index.js +130 -69
- package/dist/index.js.map +1 -1
- package/examples/claude-code/README.md +21 -0
- package/examples/claude-code/mcp.json +8 -0
- package/examples/codex/config.toml +21 -0
- package/examples/cursor/README.md +12 -0
- package/examples/cursor/mcp.json +8 -0
- package/examples/hooks/README.md +12 -0
- package/examples/hooks/ci-triage/README.md +68 -0
- package/examples/hooks/pretooluse-guard/README.md +54 -0
- package/examples/hooks/pretooluse-guard/guard.sh +61 -0
- package/package.json +3 -2
- package/skills/clef-decisions/SKILL.md +15 -5
package/dist/cli.js
CHANGED
|
@@ -104,9 +104,9 @@ __export(setup_exports, {
|
|
|
104
104
|
});
|
|
105
105
|
import { execFile as execFile5 } from "child_process";
|
|
106
106
|
import fsSync4 from "fs";
|
|
107
|
-
import
|
|
107
|
+
import fsp10 from "fs/promises";
|
|
108
108
|
import os4 from "os";
|
|
109
|
-
import
|
|
109
|
+
import path11 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 fsp10.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 fsp10.mkdir(path11.dirname(file), { recursive: true });
|
|
135
|
+
await fsp10.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 = path11.join(HOME(), ".codex", "config.toml");
|
|
140
140
|
let content = "";
|
|
141
141
|
try {
|
|
142
|
-
content = await
|
|
142
|
+
content = await fsp10.readFile(file, "utf8");
|
|
143
143
|
} catch {
|
|
144
144
|
content = "";
|
|
145
145
|
}
|
|
@@ -147,24 +147,24 @@ 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 fsp10.mkdir(path11.dirname(file), { recursive: true });
|
|
151
|
+
await fsp10.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 = path11.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 = path11.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(path11.delimiter).filter(Boolean);
|
|
166
166
|
for (const dir of dirs) {
|
|
167
|
-
const candidate =
|
|
167
|
+
const candidate = path11.join(dir, bin);
|
|
168
168
|
try {
|
|
169
169
|
fsSync4.accessSync(candidate, fsSync4.constants.X_OK);
|
|
170
170
|
return candidate;
|
|
@@ -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 = path11.join(HOME(), ".agents", "skills", "clef-decisions");
|
|
204
|
+
const dest = path11.join(destDir, "SKILL.md");
|
|
205
205
|
let content = "";
|
|
206
206
|
try {
|
|
207
|
-
content = await
|
|
207
|
+
content = await fsp10.readFile(dest, "utf8");
|
|
208
208
|
} catch {
|
|
209
209
|
}
|
|
210
|
-
const incoming = await
|
|
210
|
+
const incoming = await fsp10.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 fsp10.mkdir(destDir, { recursive: true });
|
|
215
|
+
await fsp10.writeFile(dest, incoming, "utf8");
|
|
216
216
|
return { client: "skill", status: "configured", detail: dest };
|
|
217
217
|
}
|
|
218
218
|
function parseClients(raw) {
|
|
@@ -437,6 +437,21 @@ function normalizeScore(ans, qid, question) {
|
|
|
437
437
|
function normalizeChoice(ans, qid) {
|
|
438
438
|
return probabilitiesMap(ans, qid);
|
|
439
439
|
}
|
|
440
|
+
function confidenceOf(ans) {
|
|
441
|
+
return typeof ans.confidence === "number" && Number.isFinite(ans.confidence) ? ans.confidence : void 0;
|
|
442
|
+
}
|
|
443
|
+
function normalizeUsage(usage) {
|
|
444
|
+
if (!usage || typeof usage !== "object") return void 0;
|
|
445
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
446
|
+
const out = {};
|
|
447
|
+
const input = num(usage.input_tokens) ?? num(usage.prompt_tokens);
|
|
448
|
+
const output = num(usage.output_tokens) ?? num(usage.completion_tokens);
|
|
449
|
+
const latency = num(usage.latency_ms);
|
|
450
|
+
if (input !== void 0) out.input_tokens = input;
|
|
451
|
+
if (output !== void 0) out.output_tokens = output;
|
|
452
|
+
if (latency !== void 0) out.latency_ms = latency;
|
|
453
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
454
|
+
}
|
|
440
455
|
function fromSystemOneResponse(response, questions, fallbackModel) {
|
|
441
456
|
const answers = response.answers;
|
|
442
457
|
if (!answers || typeof answers !== "object") {
|
|
@@ -457,9 +472,11 @@ function fromSystemOneResponse(response, questions, fallbackModel) {
|
|
|
457
472
|
answer = normalizeScore(ans, qid, question);
|
|
458
473
|
break;
|
|
459
474
|
}
|
|
460
|
-
|
|
475
|
+
const confidence = confidenceOf(ans);
|
|
476
|
+
decisions[qid] = confidence === void 0 ? { answer } : { answer, confidence };
|
|
461
477
|
}
|
|
462
|
-
|
|
478
|
+
const usage = normalizeUsage(response.usage);
|
|
479
|
+
return usage === void 0 ? { model: response.model ?? fallbackModel, decisions } : { model: response.model ?? fallbackModel, decisions, usage };
|
|
463
480
|
}
|
|
464
481
|
|
|
465
482
|
// src/config/env.ts
|
|
@@ -512,6 +529,7 @@ function loadConfig(env = process.env) {
|
|
|
512
529
|
const runtime = runtimeRaw;
|
|
513
530
|
const llamaBin = env.CLEF_LLAMA_BIN?.trim() || void 0;
|
|
514
531
|
const llamaReleaseTag = env.CLEF_LLAMA_RELEASE_TAG?.trim() || void 0;
|
|
532
|
+
const idleRaw = Number.parseInt(env.CLEF_DAEMON_IDLE?.trim() ?? "", 10);
|
|
515
533
|
return {
|
|
516
534
|
model: env.CLEF_MODEL?.trim() || "clef-flash",
|
|
517
535
|
clefHome: env.CLEF_HOME?.trim() || defaultClefHome(),
|
|
@@ -519,6 +537,7 @@ function loadConfig(env = process.env) {
|
|
|
519
537
|
logLevel,
|
|
520
538
|
llamaBin: runtime === "llama-cpp" ? llamaBin : void 0,
|
|
521
539
|
llamaReleaseTag: runtime === "llama-cpp" ? llamaReleaseTag : void 0,
|
|
540
|
+
daemonIdleSeconds: Number.isFinite(idleRaw) && idleRaw >= 0 ? idleRaw : 600,
|
|
522
541
|
limits: limitsFromEnv(env)
|
|
523
542
|
};
|
|
524
543
|
}
|
|
@@ -570,48 +589,6 @@ function createLogger(level, sink) {
|
|
|
570
589
|
return new StderrLogger(level, sink);
|
|
571
590
|
}
|
|
572
591
|
|
|
573
|
-
// src/models/manifest.ts
|
|
574
|
-
import { createHash } from "crypto";
|
|
575
|
-
import fs from "fs";
|
|
576
|
-
import fsp from "fs/promises";
|
|
577
|
-
import path3 from "path";
|
|
578
|
-
|
|
579
|
-
// src/config/paths.ts
|
|
580
|
-
import path2 from "path";
|
|
581
|
-
function pathsFor(clefHome) {
|
|
582
|
-
const root = path2.resolve(clefHome);
|
|
583
|
-
return {
|
|
584
|
-
root,
|
|
585
|
-
modelsDir: path2.join(root, "models"),
|
|
586
|
-
runtimeDir: path2.join(root, "runtime"),
|
|
587
|
-
configJson: path2.join(root, "config.json")
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
function quantDirLabel(quant) {
|
|
591
|
-
switch (quant) {
|
|
592
|
-
case "Q4_K_M":
|
|
593
|
-
return "4bit";
|
|
594
|
-
case "Q8_0":
|
|
595
|
-
return "8bit";
|
|
596
|
-
case "BF16":
|
|
597
|
-
return "bf16";
|
|
598
|
-
default:
|
|
599
|
-
return quant.toLowerCase();
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function modelQuantDir(modelsDir, model, quant) {
|
|
603
|
-
return path2.join(modelsDir, model, quantDirLabel(quant));
|
|
604
|
-
}
|
|
605
|
-
function manifestPathFor(modelsDir, model, quant) {
|
|
606
|
-
return path2.join(modelQuantDir(modelsDir, model, quant), "manifest.json");
|
|
607
|
-
}
|
|
608
|
-
function llamaRuntimeDir(runtimeDir, platform, arch) {
|
|
609
|
-
return path2.join(runtimeDir, "llama.cpp", `${platform}-${arch}`);
|
|
610
|
-
}
|
|
611
|
-
function llamaServerName(platform) {
|
|
612
|
-
return platform === "win32" ? "llama-server.exe" : "llama-server";
|
|
613
|
-
}
|
|
614
|
-
|
|
615
592
|
// src/models/registry.ts
|
|
616
593
|
init_errors();
|
|
617
594
|
var GB = 1024 ** 3;
|
|
@@ -667,6 +644,51 @@ function pickQuant(spec, totalMemBytes, requested) {
|
|
|
667
644
|
}
|
|
668
645
|
var QUANT_PREFERENCE = ["Q4_K_M", "Q8_0", "BF16"];
|
|
669
646
|
|
|
647
|
+
// src/runtime/loader.ts
|
|
648
|
+
init_errors();
|
|
649
|
+
|
|
650
|
+
// src/models/manifest.ts
|
|
651
|
+
import { createHash } from "crypto";
|
|
652
|
+
import fs from "fs";
|
|
653
|
+
import fsp from "fs/promises";
|
|
654
|
+
import path3 from "path";
|
|
655
|
+
|
|
656
|
+
// src/config/paths.ts
|
|
657
|
+
import path2 from "path";
|
|
658
|
+
function pathsFor(clefHome) {
|
|
659
|
+
const root = path2.resolve(clefHome);
|
|
660
|
+
return {
|
|
661
|
+
root,
|
|
662
|
+
modelsDir: path2.join(root, "models"),
|
|
663
|
+
runtimeDir: path2.join(root, "runtime"),
|
|
664
|
+
configJson: path2.join(root, "config.json")
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function quantDirLabel(quant) {
|
|
668
|
+
switch (quant) {
|
|
669
|
+
case "Q4_K_M":
|
|
670
|
+
return "4bit";
|
|
671
|
+
case "Q8_0":
|
|
672
|
+
return "8bit";
|
|
673
|
+
case "BF16":
|
|
674
|
+
return "bf16";
|
|
675
|
+
default:
|
|
676
|
+
return quant.toLowerCase();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function modelQuantDir(modelsDir, model, quant) {
|
|
680
|
+
return path2.join(modelsDir, model, quantDirLabel(quant));
|
|
681
|
+
}
|
|
682
|
+
function manifestPathFor(modelsDir, model, quant) {
|
|
683
|
+
return path2.join(modelQuantDir(modelsDir, model, quant), "manifest.json");
|
|
684
|
+
}
|
|
685
|
+
function llamaRuntimeDir(runtimeDir, platform, arch) {
|
|
686
|
+
return path2.join(runtimeDir, "llama.cpp", `${platform}-${arch}`);
|
|
687
|
+
}
|
|
688
|
+
function llamaServerName(platform) {
|
|
689
|
+
return platform === "win32" ? "llama-server.exe" : "llama-server";
|
|
690
|
+
}
|
|
691
|
+
|
|
670
692
|
// src/models/manifest.ts
|
|
671
693
|
function isModelManifest(v) {
|
|
672
694
|
if (!v || typeof v !== "object") return false;
|
|
@@ -1398,6 +1420,75 @@ ${tail}` : ""), "Run `clef-mcp doctor` for a full diagnosis.")
|
|
|
1398
1420
|
}
|
|
1399
1421
|
};
|
|
1400
1422
|
|
|
1423
|
+
// src/runtime/loader.ts
|
|
1424
|
+
var RuntimeLoader = class {
|
|
1425
|
+
constructor(config, log, runtimeFactory) {
|
|
1426
|
+
this.config = config;
|
|
1427
|
+
this.log = log;
|
|
1428
|
+
this.runtimeFactory = runtimeFactory;
|
|
1429
|
+
}
|
|
1430
|
+
config;
|
|
1431
|
+
log;
|
|
1432
|
+
runtimeFactory;
|
|
1433
|
+
loaded;
|
|
1434
|
+
loadPromise;
|
|
1435
|
+
async ensure(spec) {
|
|
1436
|
+
if (this.loaded && this.loaded.model === spec.id) return this.loaded;
|
|
1437
|
+
if (this.loaded && this.loaded.model !== spec.id) {
|
|
1438
|
+
await this.loaded.runtime.unload();
|
|
1439
|
+
this.loaded = void 0;
|
|
1440
|
+
}
|
|
1441
|
+
if (this.loadPromise) return this.loadPromise;
|
|
1442
|
+
this.loadPromise = (async () => {
|
|
1443
|
+
if (this.config.runtime === "mlx") {
|
|
1444
|
+
const manifest2 = await mlxIsInstalled(this.config.clefHome);
|
|
1445
|
+
if (!manifest2) {
|
|
1446
|
+
throw new ClefError(
|
|
1447
|
+
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1448
|
+
`Clef model "${spec.id}" is not installed for the MLX runtime.`,
|
|
1449
|
+
"Run `clef-mcp install --runtime mlx`."
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
const uvBin = resolveUv(process.env.CLEF_MLX_UV);
|
|
1453
|
+
const runtime2 = this.runtimeFactory ? this.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log: this.log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log: this.log });
|
|
1454
|
+
this.log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
|
|
1455
|
+
const loadStarted2 = Date.now();
|
|
1456
|
+
await runtime2.load();
|
|
1457
|
+
this.log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
|
|
1458
|
+
return { runtime: runtime2, model: spec.id };
|
|
1459
|
+
}
|
|
1460
|
+
const manifest = await findInstalledManifest(this.config.clefHome, spec);
|
|
1461
|
+
if (!manifest) {
|
|
1462
|
+
throw new ClefError(
|
|
1463
|
+
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1464
|
+
`Clef model "${spec.id}" is not installed.`,
|
|
1465
|
+
"Run `clef-mcp install`."
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
const binary = resolveLlamaServerBinary(this.config);
|
|
1469
|
+
const runtime = this.runtimeFactory ? this.runtimeFactory({ kind: "llama-cpp", serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log: this.log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log: this.log });
|
|
1470
|
+
this.log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
|
|
1471
|
+
const loadStarted = Date.now();
|
|
1472
|
+
await runtime.load();
|
|
1473
|
+
this.log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
|
|
1474
|
+
return { runtime, model: spec.id };
|
|
1475
|
+
})();
|
|
1476
|
+
try {
|
|
1477
|
+
this.loaded = await this.loadPromise;
|
|
1478
|
+
return this.loaded;
|
|
1479
|
+
} finally {
|
|
1480
|
+
this.loadPromise = void 0;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
/** Unload the current runtime (kills the llama-server subprocess, if any). */
|
|
1484
|
+
async dispose() {
|
|
1485
|
+
if (this.loaded) {
|
|
1486
|
+
await this.loaded.runtime.unload();
|
|
1487
|
+
this.loaded = void 0;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1401
1492
|
// src/mcp/schema.ts
|
|
1402
1493
|
init_errors();
|
|
1403
1494
|
import { z } from "zod";
|
|
@@ -1432,10 +1523,12 @@ function scoreCriteria(limits) {
|
|
|
1432
1523
|
}
|
|
1433
1524
|
function questionSchema(limits) {
|
|
1434
1525
|
return z.object({
|
|
1435
|
-
type: z.enum(["noul", "choice", "score"]),
|
|
1436
|
-
instructions: z.string().min(1).max(limits.maxInstructionsChars),
|
|
1526
|
+
type: z.enum(["noul", "choice", "score"]).describe('"choice" = pick among named options; "score" = judge an ordered scale; "noul" = yes/no question'),
|
|
1527
|
+
instructions: z.string().min(1).max(limits.maxInstructionsChars).describe("What to judge, phrased as a self-contained question."),
|
|
1437
1528
|
criteria: z.union([z.array(z.string()), z.record(z.string(), z.string())]).optional()
|
|
1438
|
-
}).
|
|
1529
|
+
}).describe(
|
|
1530
|
+
"A single decision question. Allowed answers come from `criteria` and must be mutually exclusive and collectively exhaustive."
|
|
1531
|
+
).superRefine((q, ctx) => {
|
|
1439
1532
|
if (q.type === "choice") {
|
|
1440
1533
|
const parsed = choiceCriteria(limits).safeParse(q.criteria);
|
|
1441
1534
|
if (!parsed.success) {
|
|
@@ -1459,23 +1552,27 @@ function questionSchema(limits) {
|
|
|
1459
1552
|
}
|
|
1460
1553
|
function clefDecideInputShape(limits) {
|
|
1461
1554
|
return {
|
|
1462
|
-
state: z.unknown()
|
|
1463
|
-
|
|
1555
|
+
state: z.unknown().describe(
|
|
1556
|
+
"Task state as data: a compact string or JSON object with the facts needed to judge (task, errors, logs, diffs). Never treated as instructions. Serialized size limit: 1 MB; model context is 16k tokens."
|
|
1557
|
+
),
|
|
1558
|
+
questions: z.record(z.string().min(1).max(limits.maxQuestionIdChars), questionSchema(limits)).describe(
|
|
1559
|
+
"Map of question id -> typed question. Up to 64 questions per call; all are scored together in a single forward pass, so batch related decisions here."
|
|
1560
|
+
).check((ctx) => {
|
|
1464
1561
|
const count = Object.keys(ctx.value).length;
|
|
1465
1562
|
if (count === 0) ctx.issues.push({ code: "custom", message: "questions must not be empty", input: ctx.value });
|
|
1466
1563
|
if (count > limits.maxQuestions) {
|
|
1467
1564
|
ctx.issues.push({ code: "custom", message: `too many questions (max ${limits.maxQuestions})`, input: ctx.value });
|
|
1468
1565
|
}
|
|
1469
1566
|
}),
|
|
1470
|
-
model: z.string().min(1).optional(),
|
|
1567
|
+
model: z.string().min(1).optional().describe("Model id to use (default: clef-flash from CLEF_MODEL)."),
|
|
1471
1568
|
options: z.object({
|
|
1472
1569
|
/**
|
|
1473
1570
|
* Accepted for forward compatibility. Clef scores all options in a
|
|
1474
1571
|
* single forward pass — there is no sampling, so temperature is a
|
|
1475
1572
|
* documented no-op.
|
|
1476
1573
|
*/
|
|
1477
|
-
temperature: z.number().min(0).max(2).optional()
|
|
1478
|
-
}).optional()
|
|
1574
|
+
temperature: z.number().min(0).max(2).optional().describe("Accepted for forward compatibility; no-op. Clef scores in a single forward pass without sampling.")
|
|
1575
|
+
}).optional().describe("Extra options. Currently only temperature, which is a documented no-op.")
|
|
1479
1576
|
};
|
|
1480
1577
|
}
|
|
1481
1578
|
function clefDecideInputSchema(limits) {
|
|
@@ -1484,7 +1581,20 @@ function clefDecideInputSchema(limits) {
|
|
|
1484
1581
|
function clefDecideOutputSchema() {
|
|
1485
1582
|
return z.object({
|
|
1486
1583
|
model: z.string(),
|
|
1487
|
-
decisions: z.record(
|
|
1584
|
+
decisions: z.record(
|
|
1585
|
+
z.string(),
|
|
1586
|
+
z.object({
|
|
1587
|
+
answer: z.record(z.string(), z.number()),
|
|
1588
|
+
// Model-reported confidence; present only when the runtime sends it.
|
|
1589
|
+
confidence: z.number().min(0).max(1).optional()
|
|
1590
|
+
})
|
|
1591
|
+
),
|
|
1592
|
+
// Token usage / latency; present only when the runtime reports it.
|
|
1593
|
+
usage: z.object({
|
|
1594
|
+
input_tokens: z.number().optional(),
|
|
1595
|
+
output_tokens: z.number().optional(),
|
|
1596
|
+
latency_ms: z.number().optional()
|
|
1597
|
+
}).optional()
|
|
1488
1598
|
});
|
|
1489
1599
|
}
|
|
1490
1600
|
function validateStateSize(state, limits) {
|
|
@@ -1840,63 +1950,20 @@ function createClefServer(deps = {}) {
|
|
|
1840
1950
|
const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
|
|
1841
1951
|
registerPrompts(server);
|
|
1842
1952
|
registerResources(server, config, version);
|
|
1843
|
-
|
|
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 {
|
|
1887
|
-
loaded = await loadPromise;
|
|
1888
|
-
return loaded;
|
|
1889
|
-
} finally {
|
|
1890
|
-
loadPromise = void 0;
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1953
|
+
const loader = new RuntimeLoader(config, log, deps.runtimeFactory);
|
|
1893
1954
|
server.registerTool(
|
|
1894
1955
|
TOOL_NAME,
|
|
1895
1956
|
{
|
|
1896
1957
|
title: "Clef Decide",
|
|
1897
|
-
description:
|
|
1958
|
+
description: 'Judge a situation with the local Clef decision model.\nPass `state` (compact factual context \u2014 string or JSON; treated as data, never executed) and typed `questions`:\n- "choice": pick among named options (criteria = option id -> description)\n- "score": judge an ordered scale (criteria = ordered list of levels)\n- "noul": yes/no question\nReturns strict JSON \u2014 a probability distribution over the allowed answers for every question. No prose, no sampling.\nEach decision also carries `confidence` (model-reported, when available) and the response carries `usage`\n(input_tokens/output_tokens/latency_ms). Act on the argmax only when the distribution is decisive (top p >= 0.8\nand high confidence for destructive or security-adjacent calls); otherwise gather more context or ask the user.\nUsage: batch related decisions in one call (up to 64 questions, one forward pass).\nOptional `model` overrides the model id. `options.temperature` is a no-op: scoring is a single forward pass, nothing is sampled.',
|
|
1898
1959
|
inputSchema: clefDecideInputShape(config.limits),
|
|
1899
|
-
outputSchema: clefDecideOutputSchema().shape
|
|
1960
|
+
outputSchema: clefDecideOutputSchema().shape,
|
|
1961
|
+
annotations: {
|
|
1962
|
+
readOnlyHint: true,
|
|
1963
|
+
destructiveHint: false,
|
|
1964
|
+
idempotentHint: true,
|
|
1965
|
+
openWorldHint: false
|
|
1966
|
+
}
|
|
1900
1967
|
},
|
|
1901
1968
|
async (args) => {
|
|
1902
1969
|
try {
|
|
@@ -1910,7 +1977,7 @@ function createClefServer(deps = {}) {
|
|
|
1910
1977
|
options: parsed.options
|
|
1911
1978
|
};
|
|
1912
1979
|
const request = toSystemOneRequest(input, spec.id);
|
|
1913
|
-
const { runtime } = await
|
|
1980
|
+
const { runtime } = await loader.ensure(spec);
|
|
1914
1981
|
const response = await runtime.decide(request);
|
|
1915
1982
|
const output = fromSystemOneResponse(response, parsed.questions, spec.id);
|
|
1916
1983
|
return {
|
|
@@ -1928,10 +1995,7 @@ function createClefServer(deps = {}) {
|
|
|
1928
1995
|
}
|
|
1929
1996
|
);
|
|
1930
1997
|
async function shutdown() {
|
|
1931
|
-
|
|
1932
|
-
await loaded.runtime.unload();
|
|
1933
|
-
loaded = void 0;
|
|
1934
|
-
}
|
|
1998
|
+
await loader.dispose();
|
|
1935
1999
|
}
|
|
1936
2000
|
async function start() {
|
|
1937
2001
|
const transport = new StdioServerTransport();
|
|
@@ -1950,16 +2014,326 @@ function createClefServer(deps = {}) {
|
|
|
1950
2014
|
return { server, start, shutdown };
|
|
1951
2015
|
}
|
|
1952
2016
|
|
|
2017
|
+
// src/daemon/daemon.ts
|
|
2018
|
+
init_errors();
|
|
2019
|
+
import net3 from "net";
|
|
2020
|
+
import fsp5 from "fs/promises";
|
|
2021
|
+
import path7 from "path";
|
|
2022
|
+
function daemonSocketPath(clefHome) {
|
|
2023
|
+
return path7.join(clefHome, "daemon.sock");
|
|
2024
|
+
}
|
|
2025
|
+
function createQueue() {
|
|
2026
|
+
let tail = Promise.resolve();
|
|
2027
|
+
return function enqueue(task) {
|
|
2028
|
+
const run = tail.then(task, task);
|
|
2029
|
+
tail = run.catch(() => void 0);
|
|
2030
|
+
return run;
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function startDaemon(config, log) {
|
|
2034
|
+
const socketPath = daemonSocketPath(config.clefHome);
|
|
2035
|
+
const loader = new RuntimeLoader(config, log);
|
|
2036
|
+
const enqueue = createQueue();
|
|
2037
|
+
let lastUsedAt = Date.now();
|
|
2038
|
+
let closed = false;
|
|
2039
|
+
const ready = (async () => {
|
|
2040
|
+
await fsp5.mkdir(config.clefHome, { recursive: true });
|
|
2041
|
+
const live = await new Promise((resolve) => {
|
|
2042
|
+
const probe = net3.connect(socketPath);
|
|
2043
|
+
probe.once("connect", () => {
|
|
2044
|
+
probe.destroy();
|
|
2045
|
+
resolve(true);
|
|
2046
|
+
});
|
|
2047
|
+
probe.once("error", () => resolve(false));
|
|
2048
|
+
});
|
|
2049
|
+
if (live) {
|
|
2050
|
+
throw new ClefError(
|
|
2051
|
+
ClefErrorCode.RUNTIME_INIT_FAILED,
|
|
2052
|
+
`Another clef-mcp daemon is already listening on ${socketPath}.`,
|
|
2053
|
+
"Stop it (kill $(cat the daemon pid)) or remove CLEF_DAEMON_IDLE-stale sockets and retry."
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
await fsp5.rm(socketPath, { force: true });
|
|
2057
|
+
const server = net3.createServer((socket) => {
|
|
2058
|
+
let buffer = "";
|
|
2059
|
+
socket.on("data", (chunk) => {
|
|
2060
|
+
buffer += chunk.toString("utf8");
|
|
2061
|
+
let nl;
|
|
2062
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
2063
|
+
const line = buffer.slice(0, nl);
|
|
2064
|
+
buffer = buffer.slice(nl + 1);
|
|
2065
|
+
if (!line.trim()) continue;
|
|
2066
|
+
handleLine(line, socket).catch((err) => {
|
|
2067
|
+
log.error("daemon request failed", errorJson(err).error);
|
|
2068
|
+
writeLine(socket, errorJson(err));
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
});
|
|
2073
|
+
server.listen(socketPath);
|
|
2074
|
+
log.info("clef-mcp daemon listening", { socket: socketPath });
|
|
2075
|
+
const idleMs = config.daemonIdleSeconds * 1e3;
|
|
2076
|
+
const idleTimer = config.daemonIdleSeconds > 0 ? setInterval(() => {
|
|
2077
|
+
if (closed || Date.now() - lastUsedAt < idleMs) return;
|
|
2078
|
+
void loader.dispose().then(() => log.info("daemon idle, model unloaded"));
|
|
2079
|
+
}, Math.min(idleMs, 6e4)) : void 0;
|
|
2080
|
+
if (idleTimer) idleTimer.unref();
|
|
2081
|
+
const shutdown = () => {
|
|
2082
|
+
closed = true;
|
|
2083
|
+
if (idleTimer) clearInterval(idleTimer);
|
|
2084
|
+
void (async () => {
|
|
2085
|
+
server.close();
|
|
2086
|
+
await fsp5.rm(socketPath, { force: true });
|
|
2087
|
+
await loader.dispose();
|
|
2088
|
+
log.info("clef-mcp daemon stopped");
|
|
2089
|
+
})().finally(() => process.exit(0));
|
|
2090
|
+
};
|
|
2091
|
+
process.once("SIGINT", shutdown);
|
|
2092
|
+
process.once("SIGTERM", shutdown);
|
|
2093
|
+
await new Promise((resolve, reject) => {
|
|
2094
|
+
server.once("listening", resolve);
|
|
2095
|
+
server.once("error", reject);
|
|
2096
|
+
});
|
|
2097
|
+
})();
|
|
2098
|
+
async function handleLine(line, socket) {
|
|
2099
|
+
let request;
|
|
2100
|
+
try {
|
|
2101
|
+
request = JSON.parse(line);
|
|
2102
|
+
} catch (err) {
|
|
2103
|
+
writeLine(socket, errorJson(new ClefError(ClefErrorCode.INVALID_INPUT, `request is not valid JSON: ${err instanceof Error ? err.message : String(err)}`)));
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
if (request.ping) {
|
|
2107
|
+
writeLine(socket, { pong: true, model: config.model });
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
lastUsedAt = Date.now();
|
|
2111
|
+
const result = await enqueue(() => decideOnce(request));
|
|
2112
|
+
writeLine(socket, result);
|
|
2113
|
+
}
|
|
2114
|
+
async function decideOnce(request) {
|
|
2115
|
+
let parsed;
|
|
2116
|
+
try {
|
|
2117
|
+
parsed = clefDecideInputSchema(config.limits).parse({ state: request.state, questions: request.questions, ...typeof request.model === "string" ? { model: request.model } : {} });
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
if (err instanceof ClefError) throw err;
|
|
2120
|
+
const issue = err.issues?.[0];
|
|
2121
|
+
const where = issue?.path?.length ? issue.path.join(".") : "input";
|
|
2122
|
+
throw new ClefError(ClefErrorCode.INVALID_INPUT, `${where}: ${issue?.message ?? "invalid input"}`);
|
|
2123
|
+
}
|
|
2124
|
+
validateStateSize(parsed.state, config.limits);
|
|
2125
|
+
const spec = getModelSpec(parsed.model ?? config.model);
|
|
2126
|
+
const { runtime } = await loader.ensure(spec);
|
|
2127
|
+
const response = await runtime.decide(toSystemOneRequest({ state: parsed.state, questions: parsed.questions, model: spec.id }, spec.id));
|
|
2128
|
+
return fromSystemOneResponse(response, parsed.questions, spec.id);
|
|
2129
|
+
}
|
|
2130
|
+
return {
|
|
2131
|
+
ready,
|
|
2132
|
+
close: async () => {
|
|
2133
|
+
closed = true;
|
|
2134
|
+
await loader.dispose();
|
|
2135
|
+
await fsp5.rm(socketPath, { force: true });
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
function writeLine(socket, payload) {
|
|
2140
|
+
socket.write(`${JSON.stringify(payload)}
|
|
2141
|
+
`);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1953
2144
|
// src/cli/index.ts
|
|
1954
2145
|
import { Command } from "commander";
|
|
1955
2146
|
|
|
2147
|
+
// src/cli/decide.ts
|
|
2148
|
+
init_errors();
|
|
2149
|
+
import fsp6 from "fs/promises";
|
|
2150
|
+
import { ZodError } from "zod";
|
|
2151
|
+
|
|
2152
|
+
// src/daemon/client.ts
|
|
2153
|
+
import net4 from "net";
|
|
2154
|
+
function decideViaDaemon(socketPath, doc, timeoutMs = 12e4) {
|
|
2155
|
+
return new Promise((resolve, reject) => {
|
|
2156
|
+
const socket = net4.connect(socketPath);
|
|
2157
|
+
let buffer = "";
|
|
2158
|
+
let settled = false;
|
|
2159
|
+
const timer = setTimeout(() => {
|
|
2160
|
+
if (settled) return;
|
|
2161
|
+
settled = true;
|
|
2162
|
+
socket.destroy();
|
|
2163
|
+
reject(new Error(`daemon did not answer within ${timeoutMs} ms`));
|
|
2164
|
+
}, timeoutMs);
|
|
2165
|
+
const finish = (fn) => {
|
|
2166
|
+
if (settled) return;
|
|
2167
|
+
settled = true;
|
|
2168
|
+
clearTimeout(timer);
|
|
2169
|
+
socket.destroy();
|
|
2170
|
+
fn();
|
|
2171
|
+
};
|
|
2172
|
+
socket.once("connect", () => {
|
|
2173
|
+
socket.write(`${JSON.stringify(doc)}
|
|
2174
|
+
`);
|
|
2175
|
+
});
|
|
2176
|
+
socket.on("data", (chunk) => {
|
|
2177
|
+
buffer += chunk.toString("utf8");
|
|
2178
|
+
const nl = buffer.indexOf("\n");
|
|
2179
|
+
if (nl < 0) return;
|
|
2180
|
+
const line = buffer.slice(0, nl);
|
|
2181
|
+
finish(() => {
|
|
2182
|
+
try {
|
|
2183
|
+
resolve(JSON.parse(line));
|
|
2184
|
+
} catch {
|
|
2185
|
+
reject(new Error("daemon returned a non-JSON answer"));
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
});
|
|
2189
|
+
socket.once("error", (err) => {
|
|
2190
|
+
if (settled) return;
|
|
2191
|
+
finish(() => {
|
|
2192
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
2193
|
+
resolve(void 0);
|
|
2194
|
+
} else {
|
|
2195
|
+
reject(err);
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
});
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// src/cli/decide.ts
|
|
2203
|
+
function invalid(message, hint) {
|
|
2204
|
+
return new ClefError(ClefErrorCode.INVALID_INPUT, message, hint);
|
|
2205
|
+
}
|
|
2206
|
+
async function readStdin() {
|
|
2207
|
+
if (process.stdin.isTTY) return "";
|
|
2208
|
+
const chunks = [];
|
|
2209
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
2210
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2211
|
+
}
|
|
2212
|
+
function parseJson(text, what) {
|
|
2213
|
+
try {
|
|
2214
|
+
return JSON.parse(text);
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
throw invalid(
|
|
2217
|
+
`${what} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
2218
|
+
`${what} must be a JSON document (object for state, { "qid": {...} } map for questions).`
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
async function readSource(source, what, stdin) {
|
|
2223
|
+
if (source === void 0 || source === "") {
|
|
2224
|
+
throw invalid(`${what} is required`, `Pass --${what} <file|"-" for stdin|inline JSON> or pipe a full document to stdin.`);
|
|
2225
|
+
}
|
|
2226
|
+
if (source === "-") {
|
|
2227
|
+
if (!stdin) throw invalid(`no data on stdin for --${what} -`, "Pipe the JSON in or pass a file path.");
|
|
2228
|
+
return parseJson(stdin, what);
|
|
2229
|
+
}
|
|
2230
|
+
try {
|
|
2231
|
+
await fsp6.access(source);
|
|
2232
|
+
const content = await fsp6.readFile(source, "utf8");
|
|
2233
|
+
return parseJson(content, what);
|
|
2234
|
+
} catch (err) {
|
|
2235
|
+
if (err instanceof ClefError) throw err;
|
|
2236
|
+
if (err.code !== "ENOENT") throw err;
|
|
2237
|
+
if (!source.includes("\n")) {
|
|
2238
|
+
process.stderr.write(`clef-mcp decide: ${source} is not an existing file; using the value as inline ${what}
|
|
2239
|
+
`);
|
|
2240
|
+
}
|
|
2241
|
+
const trimmed = source.trim();
|
|
2242
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return parseJson(trimmed, what);
|
|
2243
|
+
return source;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
async function runDecide(opts) {
|
|
2247
|
+
const config = loadConfig();
|
|
2248
|
+
const log = createLogger(config.logLevel);
|
|
2249
|
+
const stdin = await readStdin();
|
|
2250
|
+
let state;
|
|
2251
|
+
let questions;
|
|
2252
|
+
let model;
|
|
2253
|
+
if (opts.questions !== void 0) {
|
|
2254
|
+
questions = await readSource(opts.questions, "questions", stdin);
|
|
2255
|
+
state = await readSource(opts.state, "state", opts.state === "-" ? stdin : "");
|
|
2256
|
+
} else {
|
|
2257
|
+
if (!stdin) {
|
|
2258
|
+
throw invalid(
|
|
2259
|
+
"no input on stdin",
|
|
2260
|
+
'Pipe a JSON document {"state": ..., "questions": {...}} to stdin, or pass --questions <file> (and optionally --state <file>).'
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
const doc2 = parseJson(stdin, "input");
|
|
2264
|
+
if (!doc2 || typeof doc2 !== "object" || Array.isArray(doc2)) {
|
|
2265
|
+
throw invalid('stdin input must be a JSON object with "state" and "questions"', "See the clef-mcp README for the document shape.");
|
|
2266
|
+
}
|
|
2267
|
+
const record = doc2;
|
|
2268
|
+
state = record.state;
|
|
2269
|
+
questions = record.questions;
|
|
2270
|
+
if (typeof record.model === "string") model = record.model;
|
|
2271
|
+
}
|
|
2272
|
+
const doc = { state, questions, ...model ? { model } : {} };
|
|
2273
|
+
if (opts.daemon) {
|
|
2274
|
+
const answered = await tryDaemon(config, doc);
|
|
2275
|
+
if (answered) {
|
|
2276
|
+
await new Promise(
|
|
2277
|
+
(resolve, reject) => process.stdout.write(`${JSON.stringify(answered)}
|
|
2278
|
+
`, (err) => err ? reject(err) : resolve())
|
|
2279
|
+
);
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
const parsed = validateDoc(config, doc);
|
|
2284
|
+
const spec = getModelSpec(parsed.model ?? config.model);
|
|
2285
|
+
const loader = new RuntimeLoader(config, log);
|
|
2286
|
+
try {
|
|
2287
|
+
const request = toSystemOneRequest({ state: parsed.state, questions: parsed.questions, model: spec.id }, spec.id);
|
|
2288
|
+
const { runtime } = await loader.ensure(spec);
|
|
2289
|
+
const response = await runtime.decide(request);
|
|
2290
|
+
const output = fromSystemOneResponse(response, parsed.questions, spec.id);
|
|
2291
|
+
await new Promise(
|
|
2292
|
+
(resolve, reject) => process.stdout.write(`${JSON.stringify(output)}
|
|
2293
|
+
`, (err) => err ? reject(err) : resolve())
|
|
2294
|
+
);
|
|
2295
|
+
} finally {
|
|
2296
|
+
await loader.dispose();
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
async function tryDaemon(config, doc) {
|
|
2300
|
+
const result = await decideViaDaemon(daemonSocketPath(config.clefHome), doc);
|
|
2301
|
+
if (result === void 0) {
|
|
2302
|
+
process.stderr.write("clef-mcp decide: no daemon reachable; running a cold decision\n");
|
|
2303
|
+
return void 0;
|
|
2304
|
+
}
|
|
2305
|
+
if (result.error && typeof result.error === "object") {
|
|
2306
|
+
const e = result.error;
|
|
2307
|
+
throw new ClefError(e.code, e.message, e.hint);
|
|
2308
|
+
}
|
|
2309
|
+
return result;
|
|
2310
|
+
}
|
|
2311
|
+
function validateDoc(config, doc) {
|
|
2312
|
+
let parsed;
|
|
2313
|
+
try {
|
|
2314
|
+
parsed = clefDecideInputSchema(config.limits).parse(doc);
|
|
2315
|
+
} catch (err) {
|
|
2316
|
+
if (err instanceof ZodError) {
|
|
2317
|
+
const issue = err.issues[0];
|
|
2318
|
+
const where = issue?.path?.length ? issue.path.join(".") : "input";
|
|
2319
|
+
throw invalid(
|
|
2320
|
+
`${where}: ${issue?.message ?? "invalid input"}`,
|
|
2321
|
+
"Fix the field the path names and retry. Shapes: state is a JSON value or string; questions map qid -> {type, instructions, criteria?}."
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
throw err;
|
|
2325
|
+
}
|
|
2326
|
+
validateStateSize(parsed.state, config.limits);
|
|
2327
|
+
return parsed;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
1956
2330
|
// src/cli/doctor.ts
|
|
1957
2331
|
init_errors();
|
|
1958
2332
|
import { execFile as execFile3 } from "child_process";
|
|
1959
2333
|
import os3 from "os";
|
|
1960
2334
|
import { promisify } from "util";
|
|
1961
|
-
import
|
|
1962
|
-
import
|
|
2335
|
+
import fsp7 from "fs/promises";
|
|
2336
|
+
import path8 from "path";
|
|
1963
2337
|
|
|
1964
2338
|
// src/models/platform.ts
|
|
1965
2339
|
init_errors();
|
|
@@ -2113,14 +2487,14 @@ async function checkBinaryVersion(binaryPath) {
|
|
|
2113
2487
|
async function detectMcpConfigs() {
|
|
2114
2488
|
const home = os3.homedir();
|
|
2115
2489
|
const hints = [
|
|
2116
|
-
{ client: "Codex", path:
|
|
2117
|
-
{ client: "Claude Code (user)", path:
|
|
2118
|
-
{ client: "Claude Code (project)", path:
|
|
2119
|
-
{ client: "Cursor", path:
|
|
2490
|
+
{ client: "Codex", path: path8.join(home, ".codex", "config.toml"), configured: false },
|
|
2491
|
+
{ client: "Claude Code (user)", path: path8.join(home, ".claude.json"), configured: false },
|
|
2492
|
+
{ client: "Claude Code (project)", path: path8.resolve(".mcp.json"), configured: false },
|
|
2493
|
+
{ client: "Cursor", path: path8.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
|
|
2120
2494
|
];
|
|
2121
2495
|
for (const hint of hints) {
|
|
2122
2496
|
try {
|
|
2123
|
-
const content = await
|
|
2497
|
+
const content = await fsp7.readFile(hint.path, "utf8");
|
|
2124
2498
|
hint.configured = content.includes("clef-mcp");
|
|
2125
2499
|
} catch {
|
|
2126
2500
|
hint.configured = false;
|
|
@@ -2333,14 +2707,14 @@ init_errors();
|
|
|
2333
2707
|
// src/models/llama-install.ts
|
|
2334
2708
|
init_errors();
|
|
2335
2709
|
import { execFile as execFile4 } from "child_process";
|
|
2336
|
-
import
|
|
2337
|
-
import
|
|
2710
|
+
import fsp9 from "fs/promises";
|
|
2711
|
+
import path10 from "path";
|
|
2338
2712
|
|
|
2339
2713
|
// src/models/hf.ts
|
|
2340
2714
|
init_errors();
|
|
2341
2715
|
import { createHash as createHash2 } from "crypto";
|
|
2342
|
-
import
|
|
2343
|
-
import
|
|
2716
|
+
import fsp8 from "fs/promises";
|
|
2717
|
+
import path9 from "path";
|
|
2344
2718
|
var HF_BASE_URL = "https://huggingface.co";
|
|
2345
2719
|
function resolveUrl(repo, revision, file) {
|
|
2346
2720
|
return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
|
|
@@ -2398,7 +2772,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2398
2772
|
}
|
|
2399
2773
|
const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
|
|
2400
2774
|
const hash = createHash2("sha256");
|
|
2401
|
-
const handle = await
|
|
2775
|
+
const handle = await fsp8.open(dest, "w");
|
|
2402
2776
|
let bytes = 0;
|
|
2403
2777
|
try {
|
|
2404
2778
|
for await (const chunk of res.body) {
|
|
@@ -2413,7 +2787,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2413
2787
|
}
|
|
2414
2788
|
const sha256 = hash.digest("hex");
|
|
2415
2789
|
if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
|
|
2416
|
-
await
|
|
2790
|
+
await fsp8.rm(dest, { force: true });
|
|
2417
2791
|
throw new ClefError(
|
|
2418
2792
|
ClefErrorCode.CHECKSUM_MISMATCH,
|
|
2419
2793
|
`Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
|
|
@@ -2421,7 +2795,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2421
2795
|
);
|
|
2422
2796
|
}
|
|
2423
2797
|
if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
|
|
2424
|
-
await
|
|
2798
|
+
await fsp8.rm(dest, { force: true });
|
|
2425
2799
|
throw new ClefError(
|
|
2426
2800
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
2427
2801
|
`Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
|
|
@@ -2439,17 +2813,17 @@ async function installModel(opts) {
|
|
|
2439
2813
|
return { manifest: existing, alreadyInstalled: true };
|
|
2440
2814
|
}
|
|
2441
2815
|
const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
|
|
2442
|
-
const finalPath =
|
|
2443
|
-
await
|
|
2444
|
-
const tmpPath =
|
|
2445
|
-
await
|
|
2816
|
+
const finalPath = path9.join(path9.dirname(manifestPath2), quantSpec.file);
|
|
2817
|
+
await fsp8.mkdir(path9.dirname(finalPath), { recursive: true });
|
|
2818
|
+
const tmpPath = path9.join(path9.dirname(finalPath), `.download-${quantSpec.file}`);
|
|
2819
|
+
await fsp8.rm(tmpPath, { force: true });
|
|
2446
2820
|
opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
|
|
2447
2821
|
const result = await downloadFile(info.downloadUrl, tmpPath, {
|
|
2448
2822
|
expectedSha256: info.sha256,
|
|
2449
2823
|
expectedSize: info.size ?? quantSpec.bytes,
|
|
2450
2824
|
onProgress: opts.onProgress
|
|
2451
2825
|
});
|
|
2452
|
-
await
|
|
2826
|
+
await fsp8.rename(tmpPath, finalPath);
|
|
2453
2827
|
const manifest = {
|
|
2454
2828
|
schemaVersion: 1,
|
|
2455
2829
|
model: modelSpec.id,
|
|
@@ -2537,12 +2911,12 @@ async function findServerBinary(dir, platform) {
|
|
|
2537
2911
|
const current = queue.shift();
|
|
2538
2912
|
let entries;
|
|
2539
2913
|
try {
|
|
2540
|
-
entries = await
|
|
2914
|
+
entries = await fsp9.readdir(current, { withFileTypes: true });
|
|
2541
2915
|
} catch {
|
|
2542
2916
|
continue;
|
|
2543
2917
|
}
|
|
2544
2918
|
for (const entry of entries) {
|
|
2545
|
-
const full =
|
|
2919
|
+
const full = path10.join(current, entry.name);
|
|
2546
2920
|
if (entry.isDirectory()) queue.push(full);
|
|
2547
2921
|
else if (entry.name === target) return full;
|
|
2548
2922
|
}
|
|
@@ -2553,14 +2927,14 @@ async function findServerBinary(dir, platform) {
|
|
|
2553
2927
|
);
|
|
2554
2928
|
}
|
|
2555
2929
|
async function extractRelease(archivePath, destDir, platform) {
|
|
2556
|
-
await
|
|
2930
|
+
await fsp9.mkdir(destDir, { recursive: true });
|
|
2557
2931
|
const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
|
|
2558
2932
|
try {
|
|
2559
2933
|
await execTar(args, destDir);
|
|
2560
2934
|
} catch (err) {
|
|
2561
2935
|
throw new ClefError(
|
|
2562
2936
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
2563
|
-
`Could not extract ${
|
|
2937
|
+
`Could not extract ${path10.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2564
2938
|
"Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
|
|
2565
2939
|
);
|
|
2566
2940
|
}
|
|
@@ -2570,7 +2944,7 @@ async function ensureLlamaRuntime(cfg, log) {
|
|
|
2570
2944
|
const resolved = resolveLlamaServerBinary(cfg);
|
|
2571
2945
|
if (resolved.source === "managed") {
|
|
2572
2946
|
const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
|
|
2573
|
-
const manifest = readRuntimeManifest(
|
|
2947
|
+
const manifest = readRuntimeManifest(path10.join(platformDir, "runtime-manifest.json"));
|
|
2574
2948
|
return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
|
|
2575
2949
|
}
|
|
2576
2950
|
return { ...resolved, releaseTag: resolved.source, freshInstall: false };
|
|
@@ -2602,16 +2976,16 @@ async function downloadManagedLlama(cfg, log) {
|
|
|
2602
2976
|
}
|
|
2603
2977
|
const asset = pickAsset(release, platform, arch);
|
|
2604
2978
|
log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
|
|
2605
|
-
const tagDir =
|
|
2606
|
-
await
|
|
2607
|
-
const tmpArchive =
|
|
2608
|
-
await
|
|
2979
|
+
const tagDir = path10.join(platformDir, release.tag_name);
|
|
2980
|
+
await fsp9.mkdir(tagDir, { recursive: true });
|
|
2981
|
+
const tmpArchive = path10.join(platformDir, `.download-${asset.name}`);
|
|
2982
|
+
await fsp9.rm(tmpArchive, { force: true });
|
|
2609
2983
|
const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
|
|
2610
2984
|
await extractRelease(tmpArchive, tagDir, platform);
|
|
2611
|
-
await
|
|
2985
|
+
await fsp9.rm(tmpArchive, { force: true });
|
|
2612
2986
|
const binaryPath = await findServerBinary(tagDir, platform);
|
|
2613
|
-
if (platform !== "win32") await
|
|
2614
|
-
await writeRuntimeManifest(
|
|
2987
|
+
if (platform !== "win32") await fsp9.chmod(binaryPath, 493);
|
|
2988
|
+
await writeRuntimeManifest(path10.join(platformDir, "runtime-manifest.json"), {
|
|
2615
2989
|
schemaVersion: 1,
|
|
2616
2990
|
kind: "llama-cpp",
|
|
2617
2991
|
releaseTag: release.tag_name,
|
|
@@ -2627,11 +3001,11 @@ async function removeManagedRuntime(cfg) {
|
|
|
2627
3001
|
const { runtimeDir } = pathsFor(cfg.clefHome);
|
|
2628
3002
|
const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
|
|
2629
3003
|
try {
|
|
2630
|
-
await
|
|
3004
|
+
await fsp9.stat(dir);
|
|
2631
3005
|
} catch {
|
|
2632
3006
|
return false;
|
|
2633
3007
|
}
|
|
2634
|
-
await
|
|
3008
|
+
await fsp9.rm(dir, { recursive: true, force: true });
|
|
2635
3009
|
return true;
|
|
2636
3010
|
}
|
|
2637
3011
|
|
|
@@ -2639,7 +3013,7 @@ async function removeManagedRuntime(cfg) {
|
|
|
2639
3013
|
init_prompt();
|
|
2640
3014
|
|
|
2641
3015
|
// src/version.ts
|
|
2642
|
-
var VERSION = "0.
|
|
3016
|
+
var VERSION = "0.3.0";
|
|
2643
3017
|
|
|
2644
3018
|
// src/cli/install.ts
|
|
2645
3019
|
var OS_NAMES = {
|
|
@@ -2828,7 +3202,7 @@ async function runStatus() {
|
|
|
2828
3202
|
|
|
2829
3203
|
// src/cli/uninstall.ts
|
|
2830
3204
|
init_errors();
|
|
2831
|
-
import
|
|
3205
|
+
import fsp11 from "fs/promises";
|
|
2832
3206
|
init_prompt();
|
|
2833
3207
|
async function runUninstall(opts) {
|
|
2834
3208
|
const config = loadConfig();
|
|
@@ -2867,7 +3241,7 @@ async function runUninstall(opts) {
|
|
|
2867
3241
|
return;
|
|
2868
3242
|
}
|
|
2869
3243
|
if (target) {
|
|
2870
|
-
await
|
|
3244
|
+
await fsp11.rm(target, { recursive: true, force: true });
|
|
2871
3245
|
console.log(`Removed: ${target}`);
|
|
2872
3246
|
}
|
|
2873
3247
|
if (opts.runtime) {
|
|
@@ -2894,6 +3268,24 @@ program.action(async () => {
|
|
|
2894
3268
|
handleCliError(err);
|
|
2895
3269
|
}
|
|
2896
3270
|
});
|
|
3271
|
+
program.command("decide").description("One-shot decision (no MCP session): JSON in, strict JSON result out. For hooks, CI and scripts.").option("--state <source>", 'state: file path, "-" for stdin, or inline JSON/text').option("--questions <source>", 'questions map: file path, "-" for stdin, or inline JSON').option("--model <id>", "model to use (default: CLEF_MODEL or clef-flash)").option("--daemon", "route through a running `clef-mcp daemon` (falls back to a cold run when none is reachable)").action(async (opts) => {
|
|
3272
|
+
try {
|
|
3273
|
+
await runDecide(opts);
|
|
3274
|
+
process.exit(0);
|
|
3275
|
+
} catch (err) {
|
|
3276
|
+
handleCliError(err);
|
|
3277
|
+
}
|
|
3278
|
+
});
|
|
3279
|
+
program.command("daemon").description("Keep the model warm and answer decide requests on a local unix socket in CLEF_HOME (used by decide --daemon).").action(async () => {
|
|
3280
|
+
try {
|
|
3281
|
+
const config = loadConfig();
|
|
3282
|
+
const log = createLogger(config.logLevel);
|
|
3283
|
+
const daemon = startDaemon(config, log);
|
|
3284
|
+
await daemon.ready;
|
|
3285
|
+
} catch (err) {
|
|
3286
|
+
handleCliError(err);
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
2897
3289
|
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) => {
|
|
2898
3290
|
try {
|
|
2899
3291
|
await runInstall(opts);
|