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 +5 -1
- package/dist/cli.js +36 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/examples/hooks/pretooluse-guard/README.md +39 -6
- package/examples/hooks/pretooluse-guard/guard.sh +6 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
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
|
-
|
|
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.
|
|
3039
|
+
var VERSION = "0.3.1";
|
|
3017
3040
|
|
|
3018
3041
|
// src/cli/install.ts
|
|
3019
3042
|
var OS_NAMES = {
|