clef-mcp 0.2.1 → 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 +536 -156
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +32 -10
- package/dist/index.js +109 -60
- 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";
|
|
@@ -1490,7 +1581,20 @@ function clefDecideInputSchema(limits) {
|
|
|
1490
1581
|
function clefDecideOutputSchema() {
|
|
1491
1582
|
return z.object({
|
|
1492
1583
|
model: z.string(),
|
|
1493
|
-
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()
|
|
1494
1598
|
});
|
|
1495
1599
|
}
|
|
1496
1600
|
function validateStateSize(state, limits) {
|
|
@@ -1846,61 +1950,12 @@ function createClefServer(deps = {}) {
|
|
|
1846
1950
|
const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
|
|
1847
1951
|
registerPrompts(server);
|
|
1848
1952
|
registerResources(server, config, version);
|
|
1849
|
-
|
|
1850
|
-
let loadPromise;
|
|
1851
|
-
async function ensureRuntime(spec) {
|
|
1852
|
-
if (loaded && loaded.model === spec.id) return loaded;
|
|
1853
|
-
if (loaded && loaded.model !== spec.id) {
|
|
1854
|
-
await loaded.runtime.unload();
|
|
1855
|
-
loaded = void 0;
|
|
1856
|
-
}
|
|
1857
|
-
if (loadPromise) return loadPromise;
|
|
1858
|
-
loadPromise = (async () => {
|
|
1859
|
-
if (config.runtime === "mlx") {
|
|
1860
|
-
const manifest2 = await mlxIsInstalled(config.clefHome);
|
|
1861
|
-
if (!manifest2) {
|
|
1862
|
-
throw new ClefError(
|
|
1863
|
-
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1864
|
-
`Clef model "${spec.id}" is not installed for the MLX runtime.`,
|
|
1865
|
-
"Run `clef-mcp install --runtime mlx`."
|
|
1866
|
-
);
|
|
1867
|
-
}
|
|
1868
|
-
const uvBin = resolveUv(process.env.CLEF_MLX_UV);
|
|
1869
|
-
const runtime2 = deps.runtimeFactory ? deps.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log });
|
|
1870
|
-
log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
|
|
1871
|
-
const loadStarted2 = Date.now();
|
|
1872
|
-
await runtime2.load();
|
|
1873
|
-
log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
|
|
1874
|
-
return { runtime: runtime2, model: spec.id };
|
|
1875
|
-
}
|
|
1876
|
-
const manifest = await findInstalledManifest(config.clefHome, spec);
|
|
1877
|
-
if (!manifest) {
|
|
1878
|
-
throw new ClefError(
|
|
1879
|
-
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1880
|
-
`Clef model "${spec.id}" is not installed.`,
|
|
1881
|
-
"Run `clef-mcp install`."
|
|
1882
|
-
);
|
|
1883
|
-
}
|
|
1884
|
-
const binary = resolveLlamaServerBinary(config);
|
|
1885
|
-
const runtime = deps.runtimeFactory ? deps.runtimeFactory({ kind: "llama-cpp", serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
|
|
1886
|
-
log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
|
|
1887
|
-
const loadStarted = Date.now();
|
|
1888
|
-
await runtime.load();
|
|
1889
|
-
log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
|
|
1890
|
-
return { runtime, model: spec.id };
|
|
1891
|
-
})();
|
|
1892
|
-
try {
|
|
1893
|
-
loaded = await loadPromise;
|
|
1894
|
-
return loaded;
|
|
1895
|
-
} finally {
|
|
1896
|
-
loadPromise = void 0;
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1953
|
+
const loader = new RuntimeLoader(config, log, deps.runtimeFactory);
|
|
1899
1954
|
server.registerTool(
|
|
1900
1955
|
TOOL_NAME,
|
|
1901
1956
|
{
|
|
1902
1957
|
title: "Clef Decide",
|
|
1903
|
-
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.',
|
|
1904
1959
|
inputSchema: clefDecideInputShape(config.limits),
|
|
1905
1960
|
outputSchema: clefDecideOutputSchema().shape,
|
|
1906
1961
|
annotations: {
|
|
@@ -1922,7 +1977,7 @@ function createClefServer(deps = {}) {
|
|
|
1922
1977
|
options: parsed.options
|
|
1923
1978
|
};
|
|
1924
1979
|
const request = toSystemOneRequest(input, spec.id);
|
|
1925
|
-
const { runtime } = await
|
|
1980
|
+
const { runtime } = await loader.ensure(spec);
|
|
1926
1981
|
const response = await runtime.decide(request);
|
|
1927
1982
|
const output = fromSystemOneResponse(response, parsed.questions, spec.id);
|
|
1928
1983
|
return {
|
|
@@ -1940,10 +1995,7 @@ function createClefServer(deps = {}) {
|
|
|
1940
1995
|
}
|
|
1941
1996
|
);
|
|
1942
1997
|
async function shutdown() {
|
|
1943
|
-
|
|
1944
|
-
await loaded.runtime.unload();
|
|
1945
|
-
loaded = void 0;
|
|
1946
|
-
}
|
|
1998
|
+
await loader.dispose();
|
|
1947
1999
|
}
|
|
1948
2000
|
async function start() {
|
|
1949
2001
|
const transport = new StdioServerTransport();
|
|
@@ -1962,16 +2014,326 @@ function createClefServer(deps = {}) {
|
|
|
1962
2014
|
return { server, start, shutdown };
|
|
1963
2015
|
}
|
|
1964
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
|
+
|
|
1965
2144
|
// src/cli/index.ts
|
|
1966
2145
|
import { Command } from "commander";
|
|
1967
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
|
+
|
|
1968
2330
|
// src/cli/doctor.ts
|
|
1969
2331
|
init_errors();
|
|
1970
2332
|
import { execFile as execFile3 } from "child_process";
|
|
1971
2333
|
import os3 from "os";
|
|
1972
2334
|
import { promisify } from "util";
|
|
1973
|
-
import
|
|
1974
|
-
import
|
|
2335
|
+
import fsp7 from "fs/promises";
|
|
2336
|
+
import path8 from "path";
|
|
1975
2337
|
|
|
1976
2338
|
// src/models/platform.ts
|
|
1977
2339
|
init_errors();
|
|
@@ -2125,14 +2487,14 @@ async function checkBinaryVersion(binaryPath) {
|
|
|
2125
2487
|
async function detectMcpConfigs() {
|
|
2126
2488
|
const home = os3.homedir();
|
|
2127
2489
|
const hints = [
|
|
2128
|
-
{ client: "Codex", path:
|
|
2129
|
-
{ client: "Claude Code (user)", path:
|
|
2130
|
-
{ client: "Claude Code (project)", path:
|
|
2131
|
-
{ 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 }
|
|
2132
2494
|
];
|
|
2133
2495
|
for (const hint of hints) {
|
|
2134
2496
|
try {
|
|
2135
|
-
const content = await
|
|
2497
|
+
const content = await fsp7.readFile(hint.path, "utf8");
|
|
2136
2498
|
hint.configured = content.includes("clef-mcp");
|
|
2137
2499
|
} catch {
|
|
2138
2500
|
hint.configured = false;
|
|
@@ -2345,14 +2707,14 @@ init_errors();
|
|
|
2345
2707
|
// src/models/llama-install.ts
|
|
2346
2708
|
init_errors();
|
|
2347
2709
|
import { execFile as execFile4 } from "child_process";
|
|
2348
|
-
import
|
|
2349
|
-
import
|
|
2710
|
+
import fsp9 from "fs/promises";
|
|
2711
|
+
import path10 from "path";
|
|
2350
2712
|
|
|
2351
2713
|
// src/models/hf.ts
|
|
2352
2714
|
init_errors();
|
|
2353
2715
|
import { createHash as createHash2 } from "crypto";
|
|
2354
|
-
import
|
|
2355
|
-
import
|
|
2716
|
+
import fsp8 from "fs/promises";
|
|
2717
|
+
import path9 from "path";
|
|
2356
2718
|
var HF_BASE_URL = "https://huggingface.co";
|
|
2357
2719
|
function resolveUrl(repo, revision, file) {
|
|
2358
2720
|
return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
|
|
@@ -2410,7 +2772,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2410
2772
|
}
|
|
2411
2773
|
const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
|
|
2412
2774
|
const hash = createHash2("sha256");
|
|
2413
|
-
const handle = await
|
|
2775
|
+
const handle = await fsp8.open(dest, "w");
|
|
2414
2776
|
let bytes = 0;
|
|
2415
2777
|
try {
|
|
2416
2778
|
for await (const chunk of res.body) {
|
|
@@ -2425,7 +2787,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2425
2787
|
}
|
|
2426
2788
|
const sha256 = hash.digest("hex");
|
|
2427
2789
|
if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
|
|
2428
|
-
await
|
|
2790
|
+
await fsp8.rm(dest, { force: true });
|
|
2429
2791
|
throw new ClefError(
|
|
2430
2792
|
ClefErrorCode.CHECKSUM_MISMATCH,
|
|
2431
2793
|
`Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
|
|
@@ -2433,7 +2795,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2433
2795
|
);
|
|
2434
2796
|
}
|
|
2435
2797
|
if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
|
|
2436
|
-
await
|
|
2798
|
+
await fsp8.rm(dest, { force: true });
|
|
2437
2799
|
throw new ClefError(
|
|
2438
2800
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
2439
2801
|
`Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
|
|
@@ -2451,17 +2813,17 @@ async function installModel(opts) {
|
|
|
2451
2813
|
return { manifest: existing, alreadyInstalled: true };
|
|
2452
2814
|
}
|
|
2453
2815
|
const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
|
|
2454
|
-
const finalPath =
|
|
2455
|
-
await
|
|
2456
|
-
const tmpPath =
|
|
2457
|
-
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 });
|
|
2458
2820
|
opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
|
|
2459
2821
|
const result = await downloadFile(info.downloadUrl, tmpPath, {
|
|
2460
2822
|
expectedSha256: info.sha256,
|
|
2461
2823
|
expectedSize: info.size ?? quantSpec.bytes,
|
|
2462
2824
|
onProgress: opts.onProgress
|
|
2463
2825
|
});
|
|
2464
|
-
await
|
|
2826
|
+
await fsp8.rename(tmpPath, finalPath);
|
|
2465
2827
|
const manifest = {
|
|
2466
2828
|
schemaVersion: 1,
|
|
2467
2829
|
model: modelSpec.id,
|
|
@@ -2549,12 +2911,12 @@ async function findServerBinary(dir, platform) {
|
|
|
2549
2911
|
const current = queue.shift();
|
|
2550
2912
|
let entries;
|
|
2551
2913
|
try {
|
|
2552
|
-
entries = await
|
|
2914
|
+
entries = await fsp9.readdir(current, { withFileTypes: true });
|
|
2553
2915
|
} catch {
|
|
2554
2916
|
continue;
|
|
2555
2917
|
}
|
|
2556
2918
|
for (const entry of entries) {
|
|
2557
|
-
const full =
|
|
2919
|
+
const full = path10.join(current, entry.name);
|
|
2558
2920
|
if (entry.isDirectory()) queue.push(full);
|
|
2559
2921
|
else if (entry.name === target) return full;
|
|
2560
2922
|
}
|
|
@@ -2565,14 +2927,14 @@ async function findServerBinary(dir, platform) {
|
|
|
2565
2927
|
);
|
|
2566
2928
|
}
|
|
2567
2929
|
async function extractRelease(archivePath, destDir, platform) {
|
|
2568
|
-
await
|
|
2930
|
+
await fsp9.mkdir(destDir, { recursive: true });
|
|
2569
2931
|
const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
|
|
2570
2932
|
try {
|
|
2571
2933
|
await execTar(args, destDir);
|
|
2572
2934
|
} catch (err) {
|
|
2573
2935
|
throw new ClefError(
|
|
2574
2936
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
2575
|
-
`Could not extract ${
|
|
2937
|
+
`Could not extract ${path10.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2576
2938
|
"Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
|
|
2577
2939
|
);
|
|
2578
2940
|
}
|
|
@@ -2582,7 +2944,7 @@ async function ensureLlamaRuntime(cfg, log) {
|
|
|
2582
2944
|
const resolved = resolveLlamaServerBinary(cfg);
|
|
2583
2945
|
if (resolved.source === "managed") {
|
|
2584
2946
|
const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
|
|
2585
|
-
const manifest = readRuntimeManifest(
|
|
2947
|
+
const manifest = readRuntimeManifest(path10.join(platformDir, "runtime-manifest.json"));
|
|
2586
2948
|
return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
|
|
2587
2949
|
}
|
|
2588
2950
|
return { ...resolved, releaseTag: resolved.source, freshInstall: false };
|
|
@@ -2614,16 +2976,16 @@ async function downloadManagedLlama(cfg, log) {
|
|
|
2614
2976
|
}
|
|
2615
2977
|
const asset = pickAsset(release, platform, arch);
|
|
2616
2978
|
log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
|
|
2617
|
-
const tagDir =
|
|
2618
|
-
await
|
|
2619
|
-
const tmpArchive =
|
|
2620
|
-
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 });
|
|
2621
2983
|
const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
|
|
2622
2984
|
await extractRelease(tmpArchive, tagDir, platform);
|
|
2623
|
-
await
|
|
2985
|
+
await fsp9.rm(tmpArchive, { force: true });
|
|
2624
2986
|
const binaryPath = await findServerBinary(tagDir, platform);
|
|
2625
|
-
if (platform !== "win32") await
|
|
2626
|
-
await writeRuntimeManifest(
|
|
2987
|
+
if (platform !== "win32") await fsp9.chmod(binaryPath, 493);
|
|
2988
|
+
await writeRuntimeManifest(path10.join(platformDir, "runtime-manifest.json"), {
|
|
2627
2989
|
schemaVersion: 1,
|
|
2628
2990
|
kind: "llama-cpp",
|
|
2629
2991
|
releaseTag: release.tag_name,
|
|
@@ -2639,11 +3001,11 @@ async function removeManagedRuntime(cfg) {
|
|
|
2639
3001
|
const { runtimeDir } = pathsFor(cfg.clefHome);
|
|
2640
3002
|
const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
|
|
2641
3003
|
try {
|
|
2642
|
-
await
|
|
3004
|
+
await fsp9.stat(dir);
|
|
2643
3005
|
} catch {
|
|
2644
3006
|
return false;
|
|
2645
3007
|
}
|
|
2646
|
-
await
|
|
3008
|
+
await fsp9.rm(dir, { recursive: true, force: true });
|
|
2647
3009
|
return true;
|
|
2648
3010
|
}
|
|
2649
3011
|
|
|
@@ -2651,7 +3013,7 @@ async function removeManagedRuntime(cfg) {
|
|
|
2651
3013
|
init_prompt();
|
|
2652
3014
|
|
|
2653
3015
|
// src/version.ts
|
|
2654
|
-
var VERSION = "0.
|
|
3016
|
+
var VERSION = "0.3.0";
|
|
2655
3017
|
|
|
2656
3018
|
// src/cli/install.ts
|
|
2657
3019
|
var OS_NAMES = {
|
|
@@ -2840,7 +3202,7 @@ async function runStatus() {
|
|
|
2840
3202
|
|
|
2841
3203
|
// src/cli/uninstall.ts
|
|
2842
3204
|
init_errors();
|
|
2843
|
-
import
|
|
3205
|
+
import fsp11 from "fs/promises";
|
|
2844
3206
|
init_prompt();
|
|
2845
3207
|
async function runUninstall(opts) {
|
|
2846
3208
|
const config = loadConfig();
|
|
@@ -2879,7 +3241,7 @@ async function runUninstall(opts) {
|
|
|
2879
3241
|
return;
|
|
2880
3242
|
}
|
|
2881
3243
|
if (target) {
|
|
2882
|
-
await
|
|
3244
|
+
await fsp11.rm(target, { recursive: true, force: true });
|
|
2883
3245
|
console.log(`Removed: ${target}`);
|
|
2884
3246
|
}
|
|
2885
3247
|
if (opts.runtime) {
|
|
@@ -2906,6 +3268,24 @@ program.action(async () => {
|
|
|
2906
3268
|
handleCliError(err);
|
|
2907
3269
|
}
|
|
2908
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
|
+
});
|
|
2909
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) => {
|
|
2910
3290
|
try {
|
|
2911
3291
|
await runInstall(opts);
|