clef-mcp 0.2.1 → 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/index.d.ts CHANGED
@@ -32,6 +32,8 @@ interface ClefConfig {
32
32
  llamaBin?: string;
33
33
  /** Pin the llama.cpp release tag used by the managed runtime download. */
34
34
  llamaReleaseTag?: string;
35
+ /** Daemon: unload the model after this many idle seconds (CLEF_DAEMON_IDLE, 0 = never). */
36
+ daemonIdleSeconds: number;
35
37
  limits: Limits;
36
38
  }
37
39
  declare function defaultClefHome(): string;
@@ -118,11 +120,22 @@ interface ClefDecideInput {
118
120
  }
119
121
  /** Probability distribution over answer options for one question. */
120
122
  type ClefDecisionAnswer = Record<string, number>;
123
+ /**
124
+ * Model-reported token usage / latency. Fields are optional because runtimes
125
+ * report different subsets (llama.cpp: tokens; the MLX loader: latency_ms).
126
+ */
127
+ type ClefUsage = {
128
+ input_tokens?: number;
129
+ output_tokens?: number;
130
+ latency_ms?: number;
131
+ };
121
132
  type ClefDecideOutput = {
122
133
  model: string;
123
134
  decisions: Record<string, {
124
135
  answer: ClefDecisionAnswer;
136
+ confidence?: number;
125
137
  }>;
138
+ usage?: ClefUsage;
126
139
  };
127
140
 
128
141
  /**
@@ -386,6 +399,16 @@ declare function detectPlatform(): Promise<PlatformInfo>;
386
399
  declare function assertSupportedPlatform(info: Pick<PlatformInfo, 'platform' | 'arch'>): void;
387
400
  declare function formatBytes(bytes: number): string;
388
401
 
402
+ /** Test seam: override runtime construction (defaults per config.runtime). */
403
+ type RuntimeFactory = (opts: {
404
+ kind: 'llama-cpp' | 'mlx';
405
+ serverBin?: string;
406
+ modelPath?: string;
407
+ snapshotPath?: string;
408
+ alias: string;
409
+ log?: Logger;
410
+ }) => ClefRuntime;
411
+
389
412
  declare const TOOL_NAME = "clef_decide";
390
413
  declare const SERVER_INSTRUCTIONS: string;
391
414
  interface ClefServerDeps {
@@ -393,14 +416,7 @@ interface ClefServerDeps {
393
416
  version?: string;
394
417
  log?: Logger;
395
418
  /** Test seam: override runtime construction (defaults per config.runtime). */
396
- runtimeFactory?: (opts: {
397
- kind: 'llama-cpp' | 'mlx';
398
- serverBin?: string;
399
- modelPath?: string;
400
- snapshotPath?: string;
401
- alias: string;
402
- log?: Logger;
403
- }) => ClefRuntime;
419
+ runtimeFactory?: RuntimeFactory;
404
420
  }
405
421
  interface RunningClefServer {
406
422
  server: McpServer;
@@ -430,11 +446,17 @@ declare function clefDecideOutputSchema(): z.ZodObject<{
430
446
  model: z.ZodString;
431
447
  decisions: z.ZodRecord<z.ZodString, z.ZodObject<{
432
448
  answer: z.ZodRecord<z.ZodString, z.ZodNumber>;
449
+ confidence: z.ZodOptional<z.ZodNumber>;
450
+ }, z.core.$strip>>;
451
+ usage: z.ZodOptional<z.ZodObject<{
452
+ input_tokens: z.ZodOptional<z.ZodNumber>;
453
+ output_tokens: z.ZodOptional<z.ZodNumber>;
454
+ latency_ms: z.ZodOptional<z.ZodNumber>;
433
455
  }, z.core.$strip>>;
434
456
  }, z.core.$strip>;
435
457
  /** `state` cannot be size-checked inside zod (it is `unknown`), so it is validated separately. */
436
458
  declare function validateStateSize(state: unknown, limits: Limits): void;
437
459
 
438
- declare const VERSION = "0.2.1";
460
+ declare const VERSION = "0.3.0";
439
461
 
440
- export { CLEF_FLASH, type ClefConfig, type ClefDecideInput, type ClefDecideOutput, type ClefDecisionAnswer, ClefError, ClefErrorCode, type ClefQuestion, type ClefRuntime, type ClefServerDeps, DEFAULT_LIMITS, type DownloadProgress, type InstallModelResult, type Limits, LlamaCppRuntime, type LlamaCppRuntimeOptions, type LogLevel, type Logger, MODELS, type ModelManifest, type ModelSpec, type PlatformInfo, type ProbeResult, type QuantSpec, type QuestionType, type ResolvedBinary, type RunningClefServer, type RuntimeHealth, SERVER_INSTRUCTIONS, StderrLogger, type SystemOneRequest, type SystemOneResponse, TOOL_NAME, VERSION, assertSupportedPlatform, clefDecideInputSchema, clefDecideOutputSchema, createClefServer, createLogger, deepVerifyChecksum, defaultClefHome, detectPlatform, downloadFile, downloadManagedLlama, ensureLlamaRuntime, errorJson, exitCodeFor, fetchFileInfo, findInstalledManifest, formatBytes, fromSystemOneResponse, getModelSpec, getQuantSpec, installModel, limitsFromEnv, loadConfig, manifestPathFor, modelQuantDir, pathsFor, pickQuant, probeInference, readModelManifest, removeManagedRuntime, resolveLlamaServerBinary, toSystemOneRequest, validateStateSize, verifyModelFiles, writeModelManifest };
462
+ export { CLEF_FLASH, type ClefConfig, type ClefDecideInput, type ClefDecideOutput, type ClefDecisionAnswer, ClefError, ClefErrorCode, type ClefQuestion, type ClefRuntime, type ClefServerDeps, type ClefUsage, DEFAULT_LIMITS, type DownloadProgress, type InstallModelResult, type Limits, LlamaCppRuntime, type LlamaCppRuntimeOptions, type LogLevel, type Logger, MODELS, type ModelManifest, type ModelSpec, type PlatformInfo, type ProbeResult, type QuantSpec, type QuestionType, type ResolvedBinary, type RunningClefServer, type RuntimeHealth, SERVER_INSTRUCTIONS, StderrLogger, type SystemOneRequest, type SystemOneResponse, TOOL_NAME, VERSION, assertSupportedPlatform, clefDecideInputSchema, clefDecideOutputSchema, createClefServer, createLogger, deepVerifyChecksum, defaultClefHome, detectPlatform, downloadFile, downloadManagedLlama, ensureLlamaRuntime, errorJson, exitCodeFor, fetchFileInfo, findInstalledManifest, formatBytes, fromSystemOneResponse, getModelSpec, getQuantSpec, installModel, limitsFromEnv, loadConfig, manifestPathFor, modelQuantDir, pathsFor, pickQuant, probeInference, readModelManifest, removeManagedRuntime, resolveLlamaServerBinary, toSystemOneRequest, validateStateSize, verifyModelFiles, writeModelManifest };
package/dist/index.js CHANGED
@@ -48,6 +48,7 @@ function loadConfig(env = process.env) {
48
48
  const runtime = runtimeRaw;
49
49
  const llamaBin = env.CLEF_LLAMA_BIN?.trim() || void 0;
50
50
  const llamaReleaseTag = env.CLEF_LLAMA_RELEASE_TAG?.trim() || void 0;
51
+ const idleRaw = Number.parseInt(env.CLEF_DAEMON_IDLE?.trim() ?? "", 10);
51
52
  return {
52
53
  model: env.CLEF_MODEL?.trim() || "clef-flash",
53
54
  clefHome: env.CLEF_HOME?.trim() || defaultClefHome(),
@@ -55,6 +56,7 @@ function loadConfig(env = process.env) {
55
56
  logLevel,
56
57
  llamaBin: runtime === "llama-cpp" ? llamaBin : void 0,
57
58
  llamaReleaseTag: runtime === "llama-cpp" ? llamaReleaseTag : void 0,
59
+ daemonIdleSeconds: Number.isFinite(idleRaw) && idleRaw >= 0 ? idleRaw : 600,
58
60
  limits: limitsFromEnv(env)
59
61
  };
60
62
  }
@@ -333,6 +335,21 @@ function normalizeScore(ans, qid, question) {
333
335
  function normalizeChoice(ans, qid) {
334
336
  return probabilitiesMap(ans, qid);
335
337
  }
338
+ function confidenceOf(ans) {
339
+ return typeof ans.confidence === "number" && Number.isFinite(ans.confidence) ? ans.confidence : void 0;
340
+ }
341
+ function normalizeUsage(usage) {
342
+ if (!usage || typeof usage !== "object") return void 0;
343
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
344
+ const out = {};
345
+ const input = num(usage.input_tokens) ?? num(usage.prompt_tokens);
346
+ const output = num(usage.output_tokens) ?? num(usage.completion_tokens);
347
+ const latency = num(usage.latency_ms);
348
+ if (input !== void 0) out.input_tokens = input;
349
+ if (output !== void 0) out.output_tokens = output;
350
+ if (latency !== void 0) out.latency_ms = latency;
351
+ return Object.keys(out).length > 0 ? out : void 0;
352
+ }
336
353
  function fromSystemOneResponse(response, questions, fallbackModel) {
337
354
  const answers = response.answers;
338
355
  if (!answers || typeof answers !== "object") {
@@ -353,9 +370,11 @@ function fromSystemOneResponse(response, questions, fallbackModel) {
353
370
  answer = normalizeScore(ans, qid, question);
354
371
  break;
355
372
  }
356
- decisions[qid] = { answer };
373
+ const confidence = confidenceOf(ans);
374
+ decisions[qid] = confidence === void 0 ? { answer } : { answer, confidence };
357
375
  }
358
- return { model: response.model ?? fallbackModel, decisions };
376
+ const usage = normalizeUsage(response.usage);
377
+ return usage === void 0 ? { model: response.model ?? fallbackModel, decisions } : { model: response.model ?? fallbackModel, decisions, usage };
359
378
  }
360
379
 
361
380
  // src/runtime/llama-cpp.ts
@@ -1470,6 +1489,75 @@ function formatBytes(bytes) {
1470
1489
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1471
1490
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1472
1491
 
1492
+ // src/runtime/loader.ts
1493
+ var RuntimeLoader = class {
1494
+ constructor(config, log, runtimeFactory) {
1495
+ this.config = config;
1496
+ this.log = log;
1497
+ this.runtimeFactory = runtimeFactory;
1498
+ }
1499
+ config;
1500
+ log;
1501
+ runtimeFactory;
1502
+ loaded;
1503
+ loadPromise;
1504
+ async ensure(spec) {
1505
+ if (this.loaded && this.loaded.model === spec.id) return this.loaded;
1506
+ if (this.loaded && this.loaded.model !== spec.id) {
1507
+ await this.loaded.runtime.unload();
1508
+ this.loaded = void 0;
1509
+ }
1510
+ if (this.loadPromise) return this.loadPromise;
1511
+ this.loadPromise = (async () => {
1512
+ if (this.config.runtime === "mlx") {
1513
+ const manifest2 = await mlxIsInstalled(this.config.clefHome);
1514
+ if (!manifest2) {
1515
+ throw new ClefError(
1516
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1517
+ `Clef model "${spec.id}" is not installed for the MLX runtime.`,
1518
+ "Run `clef-mcp install --runtime mlx`."
1519
+ );
1520
+ }
1521
+ const uvBin = resolveUv(process.env.CLEF_MLX_UV);
1522
+ 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 });
1523
+ this.log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
1524
+ const loadStarted2 = Date.now();
1525
+ await runtime2.load();
1526
+ this.log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
1527
+ return { runtime: runtime2, model: spec.id };
1528
+ }
1529
+ const manifest = await findInstalledManifest(this.config.clefHome, spec);
1530
+ if (!manifest) {
1531
+ throw new ClefError(
1532
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1533
+ `Clef model "${spec.id}" is not installed.`,
1534
+ "Run `clef-mcp install`."
1535
+ );
1536
+ }
1537
+ const binary = resolveLlamaServerBinary(this.config);
1538
+ 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 });
1539
+ this.log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
1540
+ const loadStarted = Date.now();
1541
+ await runtime.load();
1542
+ this.log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
1543
+ return { runtime, model: spec.id };
1544
+ })();
1545
+ try {
1546
+ this.loaded = await this.loadPromise;
1547
+ return this.loaded;
1548
+ } finally {
1549
+ this.loadPromise = void 0;
1550
+ }
1551
+ }
1552
+ /** Unload the current runtime (kills the llama-server subprocess, if any). */
1553
+ async dispose() {
1554
+ if (this.loaded) {
1555
+ await this.loaded.runtime.unload();
1556
+ this.loaded = void 0;
1557
+ }
1558
+ }
1559
+ };
1560
+
1473
1561
  // src/mcp/schema.ts
1474
1562
  import { z } from "zod";
1475
1563
  function criteriaDescription(limits) {
@@ -1561,7 +1649,20 @@ function clefDecideInputSchema(limits) {
1561
1649
  function clefDecideOutputSchema() {
1562
1650
  return z.object({
1563
1651
  model: z.string(),
1564
- decisions: z.record(z.string(), z.object({ answer: z.record(z.string(), z.number()) }))
1652
+ decisions: z.record(
1653
+ z.string(),
1654
+ z.object({
1655
+ answer: z.record(z.string(), z.number()),
1656
+ // Model-reported confidence; present only when the runtime sends it.
1657
+ confidence: z.number().min(0).max(1).optional()
1658
+ })
1659
+ ),
1660
+ // Token usage / latency; present only when the runtime reports it.
1661
+ usage: z.object({
1662
+ input_tokens: z.number().optional(),
1663
+ output_tokens: z.number().optional(),
1664
+ latency_ms: z.number().optional()
1665
+ }).optional()
1565
1666
  });
1566
1667
  }
1567
1668
  function validateStateSize(state, limits) {
@@ -1809,61 +1910,12 @@ function createClefServer(deps = {}) {
1809
1910
  const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
1810
1911
  registerPrompts(server);
1811
1912
  registerResources(server, config, version);
1812
- let loaded;
1813
- let loadPromise;
1814
- async function ensureRuntime(spec) {
1815
- if (loaded && loaded.model === spec.id) return loaded;
1816
- if (loaded && loaded.model !== spec.id) {
1817
- await loaded.runtime.unload();
1818
- loaded = void 0;
1819
- }
1820
- if (loadPromise) return loadPromise;
1821
- loadPromise = (async () => {
1822
- if (config.runtime === "mlx") {
1823
- const manifest2 = await mlxIsInstalled(config.clefHome);
1824
- if (!manifest2) {
1825
- throw new ClefError(
1826
- ClefErrorCode.MODEL_NOT_INSTALLED,
1827
- `Clef model "${spec.id}" is not installed for the MLX runtime.`,
1828
- "Run `clef-mcp install --runtime mlx`."
1829
- );
1830
- }
1831
- const uvBin = resolveUv(process.env.CLEF_MLX_UV);
1832
- const runtime2 = deps.runtimeFactory ? deps.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log });
1833
- log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
1834
- const loadStarted2 = Date.now();
1835
- await runtime2.load();
1836
- log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
1837
- return { runtime: runtime2, model: spec.id };
1838
- }
1839
- const manifest = await findInstalledManifest(config.clefHome, spec);
1840
- if (!manifest) {
1841
- throw new ClefError(
1842
- ClefErrorCode.MODEL_NOT_INSTALLED,
1843
- `Clef model "${spec.id}" is not installed.`,
1844
- "Run `clef-mcp install`."
1845
- );
1846
- }
1847
- const binary = resolveLlamaServerBinary(config);
1848
- 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 });
1849
- log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
1850
- const loadStarted = Date.now();
1851
- await runtime.load();
1852
- log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
1853
- return { runtime, model: spec.id };
1854
- })();
1855
- try {
1856
- loaded = await loadPromise;
1857
- return loaded;
1858
- } finally {
1859
- loadPromise = void 0;
1860
- }
1861
- }
1913
+ const loader = new RuntimeLoader(config, log, deps.runtimeFactory);
1862
1914
  server.registerTool(
1863
1915
  TOOL_NAME,
1864
1916
  {
1865
1917
  title: "Clef Decide",
1866
- 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).',
1918
+ 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.',
1867
1919
  inputSchema: clefDecideInputShape(config.limits),
1868
1920
  outputSchema: clefDecideOutputSchema().shape,
1869
1921
  annotations: {
@@ -1885,7 +1937,7 @@ function createClefServer(deps = {}) {
1885
1937
  options: parsed.options
1886
1938
  };
1887
1939
  const request = toSystemOneRequest(input, spec.id);
1888
- const { runtime } = await ensureRuntime(spec);
1940
+ const { runtime } = await loader.ensure(spec);
1889
1941
  const response = await runtime.decide(request);
1890
1942
  const output = fromSystemOneResponse(response, parsed.questions, spec.id);
1891
1943
  return {
@@ -1903,10 +1955,7 @@ function createClefServer(deps = {}) {
1903
1955
  }
1904
1956
  );
1905
1957
  async function shutdown() {
1906
- if (loaded) {
1907
- await loaded.runtime.unload();
1908
- loaded = void 0;
1909
- }
1958
+ await loader.dispose();
1910
1959
  }
1911
1960
  async function start() {
1912
1961
  const transport = new StdioServerTransport();
@@ -1926,7 +1975,7 @@ function createClefServer(deps = {}) {
1926
1975
  }
1927
1976
 
1928
1977
  // src/version.ts
1929
- var VERSION = "0.2.1";
1978
+ var VERSION = "0.3.0";
1930
1979
  export {
1931
1980
  CLEF_FLASH,
1932
1981
  ClefError,