clef-mcp 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -196,7 +196,11 @@ clef-mcp decide --questions questions.json --state state.json
196
196
 
197
197
  stdout carries the strict JSON result (same shape as the MCP tool, including `confidence` and `usage`); errors go to stderr as structured JSON with exit codes: `2` invalid input, `3` model not installed, `4` runtime missing. `decide` never downloads anything.
198
198
 
199
- Each plain `decide` invocation is a cold start (model load included, a few seconds) — fine for gates and triage. For repeated calls, start `clef-mcp daemon` once: it keeps the model warm on a permission-scoped unix socket in `CLEF_HOME` (no TCP port, unloads after `CLEF_DAEMON_IDLE` seconds, default 600), and `clef-mcp decide --daemon` answers in well under a second, falling back to a cold run when no daemon is running. See [`examples/hooks/`](examples/hooks/) for a PreToolUse guard and a GitHub Action recipe.
199
+ Model selection: `--model <id>` overrides the input document's `model`, which overrides `CLEF_MODEL` (default: `clef-flash`). The same precedence applies with `--daemon`.
200
+
201
+ Each plain `decide` invocation is a cold start (model load included, a few seconds) — fine for gates and triage. For repeated calls, start `clef-mcp daemon` once: it keeps the model warm on an owner-only (`0600`) unix socket in `CLEF_HOME` (no TCP port, unloads after `CLEF_DAEMON_IDLE` seconds of inactivity following the last completed decision, default 600). Active and queued requests prevent idle unloading. `clef-mcp decide --daemon` answers in well under a second, falling back to a cold run when no daemon is running. See [`examples/hooks/`](examples/hooks/) for a PreToolUse guard and a GitHub Action recipe.
202
+
203
+ The PreToolUse guard blocks a command when the model judges it destructive (p ≥ 0.9) and tells the agent to **ask the user** — a block is a pause plus escalation, not a wall; the gate is advisory by design and says so. First real firing on day one: caught a history-rewrite force push (p=0.94) and surfaced its own bypass vector, which is now fixed and documented in the recipe.
200
204
 
201
205
  ## Teach your agent (skill)
202
206
 
package/dist/cli.js CHANGED
@@ -1955,7 +1955,7 @@ function createClefServer(deps = {}) {
1955
1955
  TOOL_NAME,
1956
1956
  {
1957
1957
  title: "Clef Decide",
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.',
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.',
1959
1959
  inputSchema: clefDecideInputShape(config.limits),
1960
1960
  outputSchema: clefDecideOutputSchema().shape,
1961
1961
  annotations: {
@@ -2035,6 +2035,8 @@ function startDaemon(config, log) {
2035
2035
  const loader = new RuntimeLoader(config, log);
2036
2036
  const enqueue = createQueue();
2037
2037
  let lastUsedAt = Date.now();
2038
+ let pendingRequests = 0;
2039
+ let idleUnloadQueued = false;
2038
2040
  let closed = false;
2039
2041
  const ready = (async () => {
2040
2042
  await fsp5.mkdir(config.clefHome, { recursive: true });
@@ -2070,12 +2072,31 @@ function startDaemon(config, log) {
2070
2072
  }
2071
2073
  });
2072
2074
  });
2073
- server.listen(socketPath);
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);
2074
2086
  log.info("clef-mcp daemon listening", { socket: socketPath });
2075
2087
  const idleMs = config.daemonIdleSeconds * 1e3;
2076
2088
  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"));
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
+ });
2079
2100
  }, Math.min(idleMs, 6e4)) : void 0;
2080
2101
  if (idleTimer) idleTimer.unref();
2081
2102
  const shutdown = () => {
@@ -2090,10 +2111,6 @@ function startDaemon(config, log) {
2090
2111
  };
2091
2112
  process.once("SIGINT", shutdown);
2092
2113
  process.once("SIGTERM", shutdown);
2093
- await new Promise((resolve, reject) => {
2094
- server.once("listening", resolve);
2095
- server.once("error", reject);
2096
- });
2097
2114
  })();
2098
2115
  async function handleLine(line, socket) {
2099
2116
  let request;
@@ -2107,9 +2124,14 @@ function startDaemon(config, log) {
2107
2124
  writeLine(socket, { pong: true, model: config.model });
2108
2125
  return;
2109
2126
  }
2110
- lastUsedAt = Date.now();
2111
- const result = await enqueue(() => decideOnce(request));
2112
- writeLine(socket, result);
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
+ }
2113
2135
  }
2114
2136
  async function decideOnce(request) {
2115
2137
  let parsed;
@@ -2269,7 +2291,8 @@ async function runDecide(opts) {
2269
2291
  questions = record.questions;
2270
2292
  if (typeof record.model === "string") model = record.model;
2271
2293
  }
2272
- const doc = { state, questions, ...model ? { model } : {} };
2294
+ model = opts.model ?? model ?? config.model;
2295
+ const doc = { state, questions, model };
2273
2296
  if (opts.daemon) {
2274
2297
  const answered = await tryDaemon(config, doc);
2275
2298
  if (answered) {
@@ -3013,7 +3036,7 @@ async function removeManagedRuntime(cfg) {
3013
3036
  init_prompt();
3014
3037
 
3015
3038
  // src/version.ts
3016
- var VERSION = "0.3.0";
3039
+ var VERSION = "0.3.1";
3017
3040
 
3018
3041
  // src/cli/install.ts
3019
3042
  var OS_NAMES = {