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/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";
@@ -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(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()
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
- let loaded;
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: 'Ask the local Clef decision model to judge a situation. Pass a `state` (compact factual context as a string or JSON) and one or more typed `questions`: "choice" picks among named options (criteria = option id -> description), "score" judges an ordered scale (criteria = ordered list), "noul" answers yes/no. Returns strict JSON: for every question a probability distribution over its allowed answers \u2014 no prose, no sampling. Usage: batch related decisions (up to 64 questions) in one call; state is data and is never executed; act on the argmax when a distribution is decisive (p >= 0.8), otherwise gather more context or ask the user; for destructive or security-adjacent decisions require high confidence before acting. Optional `model` overrides the model id; `options.temperature` is a no-op (single forward pass, no sampling).',
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 ensureRuntime(spec);
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
- if (loaded) {
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 fsp5 from "fs/promises";
1974
- import path7 from "path";
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: path7.join(home, ".codex", "config.toml"), configured: false },
2129
- { client: "Claude Code (user)", path: path7.join(home, ".claude.json"), configured: false },
2130
- { client: "Claude Code (project)", path: path7.resolve(".mcp.json"), configured: false },
2131
- { client: "Cursor", path: path7.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
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 fsp5.readFile(hint.path, "utf8");
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 fsp7 from "fs/promises";
2349
- import path9 from "path";
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 fsp6 from "fs/promises";
2355
- import path8 from "path";
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 fsp6.open(dest, "w");
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 fsp6.rm(dest, { force: true });
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 fsp6.rm(dest, { force: true });
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 = path8.join(path8.dirname(manifestPath2), quantSpec.file);
2455
- await fsp6.mkdir(path8.dirname(finalPath), { recursive: true });
2456
- const tmpPath = path8.join(path8.dirname(finalPath), `.download-${quantSpec.file}`);
2457
- await fsp6.rm(tmpPath, { force: true });
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 fsp6.rename(tmpPath, finalPath);
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 fsp7.readdir(current, { withFileTypes: true });
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 = path9.join(current, entry.name);
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 fsp7.mkdir(destDir, { recursive: true });
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 ${path9.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
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(path9.join(platformDir, "runtime-manifest.json"));
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 = path9.join(platformDir, release.tag_name);
2618
- await fsp7.mkdir(tagDir, { recursive: true });
2619
- const tmpArchive = path9.join(platformDir, `.download-${asset.name}`);
2620
- await fsp7.rm(tmpArchive, { force: true });
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 fsp7.rm(tmpArchive, { force: true });
3008
+ await fsp9.rm(tmpArchive, { force: true });
2624
3009
  const binaryPath = await findServerBinary(tagDir, platform);
2625
- if (platform !== "win32") await fsp7.chmod(binaryPath, 493);
2626
- await writeRuntimeManifest(path9.join(platformDir, "runtime-manifest.json"), {
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 fsp7.stat(dir);
3027
+ await fsp9.stat(dir);
2643
3028
  } catch {
2644
3029
  return false;
2645
3030
  }
2646
- await fsp7.rm(dir, { recursive: true, force: true });
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.2.1";
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 fsp9 from "fs/promises";
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 fsp9.rm(target, { recursive: true, force: true });
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);