clef-mcp 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -4
- package/dist/cli.js +559 -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 +87 -0
- package/examples/hooks/pretooluse-guard/guard.sh +65 -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: a probability distribution over the allowed answers per question \u2014 no prose, no sampling.\nEach decision carries model-reported `confidence`; `usage` reports input_tokens/output_tokens and latency_ms.\nAct on the argmax only when decisive: top p >= 0.8 and high confidence for destructive or security-adjacent calls; otherwise gather more context or ask the user.\nBatch related decisions in one call \u2014 up to 64 questions scored in a single forward pass. `model` overrides the default id.',
|
|
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,349 @@ 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 pendingRequests = 0;
|
|
2039
|
+
let idleUnloadQueued = false;
|
|
2040
|
+
let closed = false;
|
|
2041
|
+
const ready = (async () => {
|
|
2042
|
+
await fsp5.mkdir(config.clefHome, { recursive: true });
|
|
2043
|
+
const live = await new Promise((resolve) => {
|
|
2044
|
+
const probe = net3.connect(socketPath);
|
|
2045
|
+
probe.once("connect", () => {
|
|
2046
|
+
probe.destroy();
|
|
2047
|
+
resolve(true);
|
|
2048
|
+
});
|
|
2049
|
+
probe.once("error", () => resolve(false));
|
|
2050
|
+
});
|
|
2051
|
+
if (live) {
|
|
2052
|
+
throw new ClefError(
|
|
2053
|
+
ClefErrorCode.RUNTIME_INIT_FAILED,
|
|
2054
|
+
`Another clef-mcp daemon is already listening on ${socketPath}.`,
|
|
2055
|
+
"Stop it (kill $(cat the daemon pid)) or remove CLEF_DAEMON_IDLE-stale sockets and retry."
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
await fsp5.rm(socketPath, { force: true });
|
|
2059
|
+
const server = net3.createServer((socket) => {
|
|
2060
|
+
let buffer = "";
|
|
2061
|
+
socket.on("data", (chunk) => {
|
|
2062
|
+
buffer += chunk.toString("utf8");
|
|
2063
|
+
let nl;
|
|
2064
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
2065
|
+
const line = buffer.slice(0, nl);
|
|
2066
|
+
buffer = buffer.slice(nl + 1);
|
|
2067
|
+
if (!line.trim()) continue;
|
|
2068
|
+
handleLine(line, socket).catch((err) => {
|
|
2069
|
+
log.error("daemon request failed", errorJson(err).error);
|
|
2070
|
+
writeLine(socket, errorJson(err));
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
});
|
|
2075
|
+
await new Promise((resolve, reject) => {
|
|
2076
|
+
server.once("listening", resolve);
|
|
2077
|
+
server.once("error", reject);
|
|
2078
|
+
const previousUmask = process.umask(127);
|
|
2079
|
+
try {
|
|
2080
|
+
server.listen(socketPath);
|
|
2081
|
+
} finally {
|
|
2082
|
+
process.umask(previousUmask);
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
await fsp5.chmod(socketPath, 384);
|
|
2086
|
+
log.info("clef-mcp daemon listening", { socket: socketPath });
|
|
2087
|
+
const idleMs = config.daemonIdleSeconds * 1e3;
|
|
2088
|
+
const idleTimer = config.daemonIdleSeconds > 0 ? setInterval(() => {
|
|
2089
|
+
if (closed || pendingRequests > 0 || idleUnloadQueued || Date.now() - lastUsedAt < idleMs) return;
|
|
2090
|
+
idleUnloadQueued = true;
|
|
2091
|
+
void enqueue(async () => {
|
|
2092
|
+
if (closed || pendingRequests > 0 || Date.now() - lastUsedAt < idleMs) return;
|
|
2093
|
+
await loader.dispose();
|
|
2094
|
+
log.info("daemon idle, model unloaded");
|
|
2095
|
+
}).catch((err) => {
|
|
2096
|
+
log.error("daemon idle unload failed", errorJson(err).error);
|
|
2097
|
+
}).finally(() => {
|
|
2098
|
+
idleUnloadQueued = false;
|
|
2099
|
+
});
|
|
2100
|
+
}, Math.min(idleMs, 6e4)) : void 0;
|
|
2101
|
+
if (idleTimer) idleTimer.unref();
|
|
2102
|
+
const shutdown = () => {
|
|
2103
|
+
closed = true;
|
|
2104
|
+
if (idleTimer) clearInterval(idleTimer);
|
|
2105
|
+
void (async () => {
|
|
2106
|
+
server.close();
|
|
2107
|
+
await fsp5.rm(socketPath, { force: true });
|
|
2108
|
+
await loader.dispose();
|
|
2109
|
+
log.info("clef-mcp daemon stopped");
|
|
2110
|
+
})().finally(() => process.exit(0));
|
|
2111
|
+
};
|
|
2112
|
+
process.once("SIGINT", shutdown);
|
|
2113
|
+
process.once("SIGTERM", shutdown);
|
|
2114
|
+
})();
|
|
2115
|
+
async function handleLine(line, socket) {
|
|
2116
|
+
let request;
|
|
2117
|
+
try {
|
|
2118
|
+
request = JSON.parse(line);
|
|
2119
|
+
} catch (err) {
|
|
2120
|
+
writeLine(socket, errorJson(new ClefError(ClefErrorCode.INVALID_INPUT, `request is not valid JSON: ${err instanceof Error ? err.message : String(err)}`)));
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (request.ping) {
|
|
2124
|
+
writeLine(socket, { pong: true, model: config.model });
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
pendingRequests += 1;
|
|
2128
|
+
try {
|
|
2129
|
+
const result = await enqueue(() => decideOnce(request));
|
|
2130
|
+
writeLine(socket, result);
|
|
2131
|
+
} finally {
|
|
2132
|
+
pendingRequests -= 1;
|
|
2133
|
+
lastUsedAt = Date.now();
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async function decideOnce(request) {
|
|
2137
|
+
let parsed;
|
|
2138
|
+
try {
|
|
2139
|
+
parsed = clefDecideInputSchema(config.limits).parse({ state: request.state, questions: request.questions, ...typeof request.model === "string" ? { model: request.model } : {} });
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
if (err instanceof ClefError) throw err;
|
|
2142
|
+
const issue = err.issues?.[0];
|
|
2143
|
+
const where = issue?.path?.length ? issue.path.join(".") : "input";
|
|
2144
|
+
throw new ClefError(ClefErrorCode.INVALID_INPUT, `${where}: ${issue?.message ?? "invalid input"}`);
|
|
2145
|
+
}
|
|
2146
|
+
validateStateSize(parsed.state, config.limits);
|
|
2147
|
+
const spec = getModelSpec(parsed.model ?? config.model);
|
|
2148
|
+
const { runtime } = await loader.ensure(spec);
|
|
2149
|
+
const response = await runtime.decide(toSystemOneRequest({ state: parsed.state, questions: parsed.questions, model: spec.id }, spec.id));
|
|
2150
|
+
return fromSystemOneResponse(response, parsed.questions, spec.id);
|
|
2151
|
+
}
|
|
2152
|
+
return {
|
|
2153
|
+
ready,
|
|
2154
|
+
close: async () => {
|
|
2155
|
+
closed = true;
|
|
2156
|
+
await loader.dispose();
|
|
2157
|
+
await fsp5.rm(socketPath, { force: true });
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function writeLine(socket, payload) {
|
|
2162
|
+
socket.write(`${JSON.stringify(payload)}
|
|
2163
|
+
`);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1965
2166
|
// src/cli/index.ts
|
|
1966
2167
|
import { Command } from "commander";
|
|
1967
2168
|
|
|
2169
|
+
// src/cli/decide.ts
|
|
2170
|
+
init_errors();
|
|
2171
|
+
import fsp6 from "fs/promises";
|
|
2172
|
+
import { ZodError } from "zod";
|
|
2173
|
+
|
|
2174
|
+
// src/daemon/client.ts
|
|
2175
|
+
import net4 from "net";
|
|
2176
|
+
function decideViaDaemon(socketPath, doc, timeoutMs = 12e4) {
|
|
2177
|
+
return new Promise((resolve, reject) => {
|
|
2178
|
+
const socket = net4.connect(socketPath);
|
|
2179
|
+
let buffer = "";
|
|
2180
|
+
let settled = false;
|
|
2181
|
+
const timer = setTimeout(() => {
|
|
2182
|
+
if (settled) return;
|
|
2183
|
+
settled = true;
|
|
2184
|
+
socket.destroy();
|
|
2185
|
+
reject(new Error(`daemon did not answer within ${timeoutMs} ms`));
|
|
2186
|
+
}, timeoutMs);
|
|
2187
|
+
const finish = (fn) => {
|
|
2188
|
+
if (settled) return;
|
|
2189
|
+
settled = true;
|
|
2190
|
+
clearTimeout(timer);
|
|
2191
|
+
socket.destroy();
|
|
2192
|
+
fn();
|
|
2193
|
+
};
|
|
2194
|
+
socket.once("connect", () => {
|
|
2195
|
+
socket.write(`${JSON.stringify(doc)}
|
|
2196
|
+
`);
|
|
2197
|
+
});
|
|
2198
|
+
socket.on("data", (chunk) => {
|
|
2199
|
+
buffer += chunk.toString("utf8");
|
|
2200
|
+
const nl = buffer.indexOf("\n");
|
|
2201
|
+
if (nl < 0) return;
|
|
2202
|
+
const line = buffer.slice(0, nl);
|
|
2203
|
+
finish(() => {
|
|
2204
|
+
try {
|
|
2205
|
+
resolve(JSON.parse(line));
|
|
2206
|
+
} catch {
|
|
2207
|
+
reject(new Error("daemon returned a non-JSON answer"));
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
});
|
|
2211
|
+
socket.once("error", (err) => {
|
|
2212
|
+
if (settled) return;
|
|
2213
|
+
finish(() => {
|
|
2214
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
2215
|
+
resolve(void 0);
|
|
2216
|
+
} else {
|
|
2217
|
+
reject(err);
|
|
2218
|
+
}
|
|
2219
|
+
});
|
|
2220
|
+
});
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// src/cli/decide.ts
|
|
2225
|
+
function invalid(message, hint) {
|
|
2226
|
+
return new ClefError(ClefErrorCode.INVALID_INPUT, message, hint);
|
|
2227
|
+
}
|
|
2228
|
+
async function readStdin() {
|
|
2229
|
+
if (process.stdin.isTTY) return "";
|
|
2230
|
+
const chunks = [];
|
|
2231
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
2232
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2233
|
+
}
|
|
2234
|
+
function parseJson(text, what) {
|
|
2235
|
+
try {
|
|
2236
|
+
return JSON.parse(text);
|
|
2237
|
+
} catch (err) {
|
|
2238
|
+
throw invalid(
|
|
2239
|
+
`${what} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
2240
|
+
`${what} must be a JSON document (object for state, { "qid": {...} } map for questions).`
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
async function readSource(source, what, stdin) {
|
|
2245
|
+
if (source === void 0 || source === "") {
|
|
2246
|
+
throw invalid(`${what} is required`, `Pass --${what} <file|"-" for stdin|inline JSON> or pipe a full document to stdin.`);
|
|
2247
|
+
}
|
|
2248
|
+
if (source === "-") {
|
|
2249
|
+
if (!stdin) throw invalid(`no data on stdin for --${what} -`, "Pipe the JSON in or pass a file path.");
|
|
2250
|
+
return parseJson(stdin, what);
|
|
2251
|
+
}
|
|
2252
|
+
try {
|
|
2253
|
+
await fsp6.access(source);
|
|
2254
|
+
const content = await fsp6.readFile(source, "utf8");
|
|
2255
|
+
return parseJson(content, what);
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
if (err instanceof ClefError) throw err;
|
|
2258
|
+
if (err.code !== "ENOENT") throw err;
|
|
2259
|
+
if (!source.includes("\n")) {
|
|
2260
|
+
process.stderr.write(`clef-mcp decide: ${source} is not an existing file; using the value as inline ${what}
|
|
2261
|
+
`);
|
|
2262
|
+
}
|
|
2263
|
+
const trimmed = source.trim();
|
|
2264
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return parseJson(trimmed, what);
|
|
2265
|
+
return source;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
async function runDecide(opts) {
|
|
2269
|
+
const config = loadConfig();
|
|
2270
|
+
const log = createLogger(config.logLevel);
|
|
2271
|
+
const stdin = await readStdin();
|
|
2272
|
+
let state;
|
|
2273
|
+
let questions;
|
|
2274
|
+
let model;
|
|
2275
|
+
if (opts.questions !== void 0) {
|
|
2276
|
+
questions = await readSource(opts.questions, "questions", stdin);
|
|
2277
|
+
state = await readSource(opts.state, "state", opts.state === "-" ? stdin : "");
|
|
2278
|
+
} else {
|
|
2279
|
+
if (!stdin) {
|
|
2280
|
+
throw invalid(
|
|
2281
|
+
"no input on stdin",
|
|
2282
|
+
'Pipe a JSON document {"state": ..., "questions": {...}} to stdin, or pass --questions <file> (and optionally --state <file>).'
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
const doc2 = parseJson(stdin, "input");
|
|
2286
|
+
if (!doc2 || typeof doc2 !== "object" || Array.isArray(doc2)) {
|
|
2287
|
+
throw invalid('stdin input must be a JSON object with "state" and "questions"', "See the clef-mcp README for the document shape.");
|
|
2288
|
+
}
|
|
2289
|
+
const record = doc2;
|
|
2290
|
+
state = record.state;
|
|
2291
|
+
questions = record.questions;
|
|
2292
|
+
if (typeof record.model === "string") model = record.model;
|
|
2293
|
+
}
|
|
2294
|
+
model = opts.model ?? model ?? config.model;
|
|
2295
|
+
const doc = { state, questions, model };
|
|
2296
|
+
if (opts.daemon) {
|
|
2297
|
+
const answered = await tryDaemon(config, doc);
|
|
2298
|
+
if (answered) {
|
|
2299
|
+
await new Promise(
|
|
2300
|
+
(resolve, reject) => process.stdout.write(`${JSON.stringify(answered)}
|
|
2301
|
+
`, (err) => err ? reject(err) : resolve())
|
|
2302
|
+
);
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
const parsed = validateDoc(config, doc);
|
|
2307
|
+
const spec = getModelSpec(parsed.model ?? config.model);
|
|
2308
|
+
const loader = new RuntimeLoader(config, log);
|
|
2309
|
+
try {
|
|
2310
|
+
const request = toSystemOneRequest({ state: parsed.state, questions: parsed.questions, model: spec.id }, spec.id);
|
|
2311
|
+
const { runtime } = await loader.ensure(spec);
|
|
2312
|
+
const response = await runtime.decide(request);
|
|
2313
|
+
const output = fromSystemOneResponse(response, parsed.questions, spec.id);
|
|
2314
|
+
await new Promise(
|
|
2315
|
+
(resolve, reject) => process.stdout.write(`${JSON.stringify(output)}
|
|
2316
|
+
`, (err) => err ? reject(err) : resolve())
|
|
2317
|
+
);
|
|
2318
|
+
} finally {
|
|
2319
|
+
await loader.dispose();
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
async function tryDaemon(config, doc) {
|
|
2323
|
+
const result = await decideViaDaemon(daemonSocketPath(config.clefHome), doc);
|
|
2324
|
+
if (result === void 0) {
|
|
2325
|
+
process.stderr.write("clef-mcp decide: no daemon reachable; running a cold decision\n");
|
|
2326
|
+
return void 0;
|
|
2327
|
+
}
|
|
2328
|
+
if (result.error && typeof result.error === "object") {
|
|
2329
|
+
const e = result.error;
|
|
2330
|
+
throw new ClefError(e.code, e.message, e.hint);
|
|
2331
|
+
}
|
|
2332
|
+
return result;
|
|
2333
|
+
}
|
|
2334
|
+
function validateDoc(config, doc) {
|
|
2335
|
+
let parsed;
|
|
2336
|
+
try {
|
|
2337
|
+
parsed = clefDecideInputSchema(config.limits).parse(doc);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
if (err instanceof ZodError) {
|
|
2340
|
+
const issue = err.issues[0];
|
|
2341
|
+
const where = issue?.path?.length ? issue.path.join(".") : "input";
|
|
2342
|
+
throw invalid(
|
|
2343
|
+
`${where}: ${issue?.message ?? "invalid input"}`,
|
|
2344
|
+
"Fix the field the path names and retry. Shapes: state is a JSON value or string; questions map qid -> {type, instructions, criteria?}."
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
throw err;
|
|
2348
|
+
}
|
|
2349
|
+
validateStateSize(parsed.state, config.limits);
|
|
2350
|
+
return parsed;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
1968
2353
|
// src/cli/doctor.ts
|
|
1969
2354
|
init_errors();
|
|
1970
2355
|
import { execFile as execFile3 } from "child_process";
|
|
1971
2356
|
import os3 from "os";
|
|
1972
2357
|
import { promisify } from "util";
|
|
1973
|
-
import
|
|
1974
|
-
import
|
|
2358
|
+
import fsp7 from "fs/promises";
|
|
2359
|
+
import path8 from "path";
|
|
1975
2360
|
|
|
1976
2361
|
// src/models/platform.ts
|
|
1977
2362
|
init_errors();
|
|
@@ -2125,14 +2510,14 @@ async function checkBinaryVersion(binaryPath) {
|
|
|
2125
2510
|
async function detectMcpConfigs() {
|
|
2126
2511
|
const home = os3.homedir();
|
|
2127
2512
|
const hints = [
|
|
2128
|
-
{ client: "Codex", path:
|
|
2129
|
-
{ client: "Claude Code (user)", path:
|
|
2130
|
-
{ client: "Claude Code (project)", path:
|
|
2131
|
-
{ client: "Cursor", path:
|
|
2513
|
+
{ client: "Codex", path: path8.join(home, ".codex", "config.toml"), configured: false },
|
|
2514
|
+
{ client: "Claude Code (user)", path: path8.join(home, ".claude.json"), configured: false },
|
|
2515
|
+
{ client: "Claude Code (project)", path: path8.resolve(".mcp.json"), configured: false },
|
|
2516
|
+
{ client: "Cursor", path: path8.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
|
|
2132
2517
|
];
|
|
2133
2518
|
for (const hint of hints) {
|
|
2134
2519
|
try {
|
|
2135
|
-
const content = await
|
|
2520
|
+
const content = await fsp7.readFile(hint.path, "utf8");
|
|
2136
2521
|
hint.configured = content.includes("clef-mcp");
|
|
2137
2522
|
} catch {
|
|
2138
2523
|
hint.configured = false;
|
|
@@ -2345,14 +2730,14 @@ init_errors();
|
|
|
2345
2730
|
// src/models/llama-install.ts
|
|
2346
2731
|
init_errors();
|
|
2347
2732
|
import { execFile as execFile4 } from "child_process";
|
|
2348
|
-
import
|
|
2349
|
-
import
|
|
2733
|
+
import fsp9 from "fs/promises";
|
|
2734
|
+
import path10 from "path";
|
|
2350
2735
|
|
|
2351
2736
|
// src/models/hf.ts
|
|
2352
2737
|
init_errors();
|
|
2353
2738
|
import { createHash as createHash2 } from "crypto";
|
|
2354
|
-
import
|
|
2355
|
-
import
|
|
2739
|
+
import fsp8 from "fs/promises";
|
|
2740
|
+
import path9 from "path";
|
|
2356
2741
|
var HF_BASE_URL = "https://huggingface.co";
|
|
2357
2742
|
function resolveUrl(repo, revision, file) {
|
|
2358
2743
|
return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
|
|
@@ -2410,7 +2795,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2410
2795
|
}
|
|
2411
2796
|
const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
|
|
2412
2797
|
const hash = createHash2("sha256");
|
|
2413
|
-
const handle = await
|
|
2798
|
+
const handle = await fsp8.open(dest, "w");
|
|
2414
2799
|
let bytes = 0;
|
|
2415
2800
|
try {
|
|
2416
2801
|
for await (const chunk of res.body) {
|
|
@@ -2425,7 +2810,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2425
2810
|
}
|
|
2426
2811
|
const sha256 = hash.digest("hex");
|
|
2427
2812
|
if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
|
|
2428
|
-
await
|
|
2813
|
+
await fsp8.rm(dest, { force: true });
|
|
2429
2814
|
throw new ClefError(
|
|
2430
2815
|
ClefErrorCode.CHECKSUM_MISMATCH,
|
|
2431
2816
|
`Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
|
|
@@ -2433,7 +2818,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
2433
2818
|
);
|
|
2434
2819
|
}
|
|
2435
2820
|
if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
|
|
2436
|
-
await
|
|
2821
|
+
await fsp8.rm(dest, { force: true });
|
|
2437
2822
|
throw new ClefError(
|
|
2438
2823
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
2439
2824
|
`Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
|
|
@@ -2451,17 +2836,17 @@ async function installModel(opts) {
|
|
|
2451
2836
|
return { manifest: existing, alreadyInstalled: true };
|
|
2452
2837
|
}
|
|
2453
2838
|
const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
|
|
2454
|
-
const finalPath =
|
|
2455
|
-
await
|
|
2456
|
-
const tmpPath =
|
|
2457
|
-
await
|
|
2839
|
+
const finalPath = path9.join(path9.dirname(manifestPath2), quantSpec.file);
|
|
2840
|
+
await fsp8.mkdir(path9.dirname(finalPath), { recursive: true });
|
|
2841
|
+
const tmpPath = path9.join(path9.dirname(finalPath), `.download-${quantSpec.file}`);
|
|
2842
|
+
await fsp8.rm(tmpPath, { force: true });
|
|
2458
2843
|
opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
|
|
2459
2844
|
const result = await downloadFile(info.downloadUrl, tmpPath, {
|
|
2460
2845
|
expectedSha256: info.sha256,
|
|
2461
2846
|
expectedSize: info.size ?? quantSpec.bytes,
|
|
2462
2847
|
onProgress: opts.onProgress
|
|
2463
2848
|
});
|
|
2464
|
-
await
|
|
2849
|
+
await fsp8.rename(tmpPath, finalPath);
|
|
2465
2850
|
const manifest = {
|
|
2466
2851
|
schemaVersion: 1,
|
|
2467
2852
|
model: modelSpec.id,
|
|
@@ -2549,12 +2934,12 @@ async function findServerBinary(dir, platform) {
|
|
|
2549
2934
|
const current = queue.shift();
|
|
2550
2935
|
let entries;
|
|
2551
2936
|
try {
|
|
2552
|
-
entries = await
|
|
2937
|
+
entries = await fsp9.readdir(current, { withFileTypes: true });
|
|
2553
2938
|
} catch {
|
|
2554
2939
|
continue;
|
|
2555
2940
|
}
|
|
2556
2941
|
for (const entry of entries) {
|
|
2557
|
-
const full =
|
|
2942
|
+
const full = path10.join(current, entry.name);
|
|
2558
2943
|
if (entry.isDirectory()) queue.push(full);
|
|
2559
2944
|
else if (entry.name === target) return full;
|
|
2560
2945
|
}
|
|
@@ -2565,14 +2950,14 @@ async function findServerBinary(dir, platform) {
|
|
|
2565
2950
|
);
|
|
2566
2951
|
}
|
|
2567
2952
|
async function extractRelease(archivePath, destDir, platform) {
|
|
2568
|
-
await
|
|
2953
|
+
await fsp9.mkdir(destDir, { recursive: true });
|
|
2569
2954
|
const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
|
|
2570
2955
|
try {
|
|
2571
2956
|
await execTar(args, destDir);
|
|
2572
2957
|
} catch (err) {
|
|
2573
2958
|
throw new ClefError(
|
|
2574
2959
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
2575
|
-
`Could not extract ${
|
|
2960
|
+
`Could not extract ${path10.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2576
2961
|
"Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
|
|
2577
2962
|
);
|
|
2578
2963
|
}
|
|
@@ -2582,7 +2967,7 @@ async function ensureLlamaRuntime(cfg, log) {
|
|
|
2582
2967
|
const resolved = resolveLlamaServerBinary(cfg);
|
|
2583
2968
|
if (resolved.source === "managed") {
|
|
2584
2969
|
const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
|
|
2585
|
-
const manifest = readRuntimeManifest(
|
|
2970
|
+
const manifest = readRuntimeManifest(path10.join(platformDir, "runtime-manifest.json"));
|
|
2586
2971
|
return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
|
|
2587
2972
|
}
|
|
2588
2973
|
return { ...resolved, releaseTag: resolved.source, freshInstall: false };
|
|
@@ -2614,16 +2999,16 @@ async function downloadManagedLlama(cfg, log) {
|
|
|
2614
2999
|
}
|
|
2615
3000
|
const asset = pickAsset(release, platform, arch);
|
|
2616
3001
|
log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
|
|
2617
|
-
const tagDir =
|
|
2618
|
-
await
|
|
2619
|
-
const tmpArchive =
|
|
2620
|
-
await
|
|
3002
|
+
const tagDir = path10.join(platformDir, release.tag_name);
|
|
3003
|
+
await fsp9.mkdir(tagDir, { recursive: true });
|
|
3004
|
+
const tmpArchive = path10.join(platformDir, `.download-${asset.name}`);
|
|
3005
|
+
await fsp9.rm(tmpArchive, { force: true });
|
|
2621
3006
|
const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
|
|
2622
3007
|
await extractRelease(tmpArchive, tagDir, platform);
|
|
2623
|
-
await
|
|
3008
|
+
await fsp9.rm(tmpArchive, { force: true });
|
|
2624
3009
|
const binaryPath = await findServerBinary(tagDir, platform);
|
|
2625
|
-
if (platform !== "win32") await
|
|
2626
|
-
await writeRuntimeManifest(
|
|
3010
|
+
if (platform !== "win32") await fsp9.chmod(binaryPath, 493);
|
|
3011
|
+
await writeRuntimeManifest(path10.join(platformDir, "runtime-manifest.json"), {
|
|
2627
3012
|
schemaVersion: 1,
|
|
2628
3013
|
kind: "llama-cpp",
|
|
2629
3014
|
releaseTag: release.tag_name,
|
|
@@ -2639,11 +3024,11 @@ async function removeManagedRuntime(cfg) {
|
|
|
2639
3024
|
const { runtimeDir } = pathsFor(cfg.clefHome);
|
|
2640
3025
|
const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
|
|
2641
3026
|
try {
|
|
2642
|
-
await
|
|
3027
|
+
await fsp9.stat(dir);
|
|
2643
3028
|
} catch {
|
|
2644
3029
|
return false;
|
|
2645
3030
|
}
|
|
2646
|
-
await
|
|
3031
|
+
await fsp9.rm(dir, { recursive: true, force: true });
|
|
2647
3032
|
return true;
|
|
2648
3033
|
}
|
|
2649
3034
|
|
|
@@ -2651,7 +3036,7 @@ async function removeManagedRuntime(cfg) {
|
|
|
2651
3036
|
init_prompt();
|
|
2652
3037
|
|
|
2653
3038
|
// src/version.ts
|
|
2654
|
-
var VERSION = "0.
|
|
3039
|
+
var VERSION = "0.3.1";
|
|
2655
3040
|
|
|
2656
3041
|
// src/cli/install.ts
|
|
2657
3042
|
var OS_NAMES = {
|
|
@@ -2840,7 +3225,7 @@ async function runStatus() {
|
|
|
2840
3225
|
|
|
2841
3226
|
// src/cli/uninstall.ts
|
|
2842
3227
|
init_errors();
|
|
2843
|
-
import
|
|
3228
|
+
import fsp11 from "fs/promises";
|
|
2844
3229
|
init_prompt();
|
|
2845
3230
|
async function runUninstall(opts) {
|
|
2846
3231
|
const config = loadConfig();
|
|
@@ -2879,7 +3264,7 @@ async function runUninstall(opts) {
|
|
|
2879
3264
|
return;
|
|
2880
3265
|
}
|
|
2881
3266
|
if (target) {
|
|
2882
|
-
await
|
|
3267
|
+
await fsp11.rm(target, { recursive: true, force: true });
|
|
2883
3268
|
console.log(`Removed: ${target}`);
|
|
2884
3269
|
}
|
|
2885
3270
|
if (opts.runtime) {
|
|
@@ -2906,6 +3291,24 @@ program.action(async () => {
|
|
|
2906
3291
|
handleCliError(err);
|
|
2907
3292
|
}
|
|
2908
3293
|
});
|
|
3294
|
+
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) => {
|
|
3295
|
+
try {
|
|
3296
|
+
await runDecide(opts);
|
|
3297
|
+
process.exit(0);
|
|
3298
|
+
} catch (err) {
|
|
3299
|
+
handleCliError(err);
|
|
3300
|
+
}
|
|
3301
|
+
});
|
|
3302
|
+
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 () => {
|
|
3303
|
+
try {
|
|
3304
|
+
const config = loadConfig();
|
|
3305
|
+
const log = createLogger(config.logLevel);
|
|
3306
|
+
const daemon = startDaemon(config, log);
|
|
3307
|
+
await daemon.ready;
|
|
3308
|
+
} catch (err) {
|
|
3309
|
+
handleCliError(err);
|
|
3310
|
+
}
|
|
3311
|
+
});
|
|
2909
3312
|
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
3313
|
try {
|
|
2911
3314
|
await runInstall(opts);
|