clef-mcp 0.2.0 → 0.3.0

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