@clutor/cli 0.1.1 → 0.2.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.
@@ -320,7 +320,8 @@ function withPrefixSnapshots(inner, snaps) {
320
320
  }
321
321
 
322
322
  // src/models.ts
323
- import { createWriteStream } from "fs";
323
+ import { createHash as createHash3 } from "crypto";
324
+ import { createReadStream, createWriteStream } from "fs";
324
325
  import { mkdir, readdir, rename, rm, stat, statfs } from "fs/promises";
325
326
  import { Readable } from "stream";
326
327
  import { pipeline } from "stream/promises";
@@ -380,8 +381,9 @@ async function listLocalModels(dir) {
380
381
  }
381
382
  var GiB_ = 2 ** 30;
382
383
  var DOWNLOADS = [
383
- { id: "olmoe-1b-7b-q4_k_m", catalogId: "olmoe-1b-7b", repo: "bartowski/OLMoE-1B-7B-0924-Instruct-GGUF", file: "OLMoE-1B-7B-0924-Instruct-Q4_K_M.gguf", bytes: Math.round(3.92 * GiB_), note: "a real mixture-of-experts model (64 experts, 8 used): the one to test expert streaming with" },
384
- { id: "qwen2.5-coder-7b-q4_k_m", catalogId: "qwen2.5-coder-7b", repo: "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF", file: "qwen2.5-coder-7b-instruct-q4_k_m.gguf", bytes: Math.round(4.36 * GiB_), note: "a dense coding model for the agent (not a mixture of experts)" }
384
+ { id: "olmoe-1b-7b-q4_k_m", catalogId: "olmoe-1b-7b", repo: "allenai/OLMoE-1B-7B-0924-Instruct-GGUF", file: "olmoe-1b-7b-0924-instruct-q4_k_m.gguf", bytes: 4213512672, sha256: "8c310f1435a1222338fd2d3d974975be9cd908180b644bab0c2a94da1ac32f3f", note: "a real mixture-of-experts model (64 experts, 8 used) that fits in memory: the one `clutor generate` was validated on" },
385
+ { id: "qwen3-30b-a3b-q4_k_m", catalogId: "qwen3-30b-a3b", repo: "Qwen/Qwen3-30B-A3B-GGUF", file: "Qwen3-30B-A3B-Q4_K_M.gguf", bytes: 18556685824, sha256: "0d003f6662faee786ed5da3e31b29c978de5ae5d275c8794c606a7f3c01aa8f5", note: "a mixture-of-experts model (128 experts, 8 used) LARGER than 16 GiB of RAM: run it with `clutor generate ... --cache-mb 4096`" },
386
+ { id: "qwen2.5-coder-7b-q4_k_m", catalogId: "qwen2.5-coder-7b", repo: "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF", file: "qwen2.5-coder-7b-instruct-q4_k_m.gguf", bytes: 4683073536, sha256: "509287f78cb4d4cf6b3843734733b914b2c158e43e22a7f4bf5e963800894d3c", note: "a dense coding model for the agent (not a mixture of experts)" }
385
387
  ];
386
388
  var downloadUrl = (d) => `https://huggingface.co/${d.repo}/resolve/main/${d.file}`;
387
389
  var DownloadError = class extends Error {
@@ -393,29 +395,49 @@ var DownloadError = class extends Error {
393
395
  async function downloadFile(url, dest, opts = {}) {
394
396
  const f = opts.fetch ?? globalThis.fetch;
395
397
  await mkdir(resolve(dest, ".."), { recursive: true });
396
- const res = await f(url, { redirect: "follow" });
398
+ const part = `${dest}.part`;
399
+ const have = await stat(part).then((s) => s.size).catch(() => 0);
400
+ let res = await f(url, { redirect: "follow", ...have > 0 ? { headers: { Range: `bytes=${have}-` } } : {} });
401
+ if (res.status === 416) {
402
+ await rm(part, { force: true });
403
+ res = await f(url, { redirect: "follow" });
404
+ }
397
405
  if (!res.ok || !res.body) throw new DownloadError(`the server answered ${res.status} for ${url}`);
398
- const total = Number(res.headers.get("content-length") ?? 0);
406
+ const resumed = res.status === 206;
407
+ let start = 0, total;
408
+ if (resumed) {
409
+ const m = /^bytes (\d+)-\d+\/(\d+)$/.exec(res.headers.get("content-range") ?? "");
410
+ if (!m || Number(m[1]) !== have) throw new DownloadError("the server's answer to the resume request does not continue where the partial file stops");
411
+ start = have;
412
+ total = Number(m[2]);
413
+ } else {
414
+ total = Number(res.headers.get("content-length") ?? 0);
415
+ }
399
416
  if (!total) throw new DownloadError("the server did not say how large the file is, so it cannot be checked");
400
417
  if (opts.expectedBytes && Math.abs(total - opts.expectedBytes) / opts.expectedBytes > 0.05) throw new DownloadError(`expected about ${opts.expectedBytes} bytes but the server offers ${total}; not downloading`);
401
418
  const fs = await statfs(resolve(dest, ".."));
402
419
  const free = Number(fs.bavail) * Number(fs.bsize);
403
- if (free < total * 1.1) throw new DownloadError(`not enough free disk: need ${(total / GiB_).toFixed(2)} GiB, have ${(free / GiB_).toFixed(2)} GiB`);
404
- const part = `${dest}.part`;
405
- let done = 0;
420
+ if (free < (total - start) * 1.1) throw new DownloadError(`not enough free disk: need ${((total - start) / GiB_).toFixed(2)} GiB, have ${(free / GiB_).toFixed(2)} GiB`);
421
+ const hash = opts.expectedSha256 ? createHash3("sha256") : void 0;
422
+ if (hash && start > 0) for await (const chunk of createReadStream(part)) hash.update(chunk);
423
+ let done = start;
406
424
  const body = Readable.fromWeb(res.body);
407
425
  body.on("data", (c) => {
408
426
  done += c.length;
427
+ hash?.update(c);
409
428
  opts.onProgress?.(done, total);
410
429
  });
411
430
  try {
412
- await pipeline(body, createWriteStream(part));
413
- if (done !== total) throw new DownloadError(`the download stopped at ${done} of ${total} bytes`);
414
- await rename(part, dest);
431
+ await pipeline(body, createWriteStream(part, { flags: resumed ? "a" : "w" }));
415
432
  } catch (e) {
433
+ throw new DownloadError(`the download was interrupted at ${done} of ${total} bytes (${e instanceof Error ? e.message : String(e)}); run the same command again to resume`);
434
+ }
435
+ if (done !== total) throw new DownloadError(`the download stopped at ${done} of ${total} bytes; run the same command again to resume`);
436
+ if (hash && hash.digest("hex") !== opts.expectedSha256?.toLowerCase()) {
416
437
  await rm(part, { force: true });
417
- throw e;
438
+ throw new DownloadError("the downloaded file does not match the SHA-256 the publisher lists; it was deleted");
418
439
  }
440
+ await rename(part, dest);
419
441
  return total;
420
442
  }
421
443
 
@@ -539,7 +561,7 @@ async function handleMcp(req, ctx) {
539
561
  if (req.jsonrpc !== "2.0" || typeof req.method !== "string") return err(-32600, "not a JSON-RPC 2.0 request");
540
562
  switch (req.method) {
541
563
  case "initialize":
542
- return ok({ protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "clutor", version: "0.1.0" } });
564
+ return ok({ protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "clutor", version: ctx.version ?? "unknown" } });
543
565
  case "notifications/initialized":
544
566
  return { status: 202, body: "" };
545
567
  case "ping":
@@ -1000,8 +1022,8 @@ function createClutorServer(o) {
1000
1022
  }
1001
1023
  } else {
1002
1024
  const { mkdtemp, rm: rm2 } = await import("fs/promises");
1003
- const { tmpdir: tmpdir2 } = await import("os");
1004
- const dir = await mkdtemp(`${tmpdir2()}/clutor-bench-`);
1025
+ const { tmpdir: tmpdir3 } = await import("os");
1026
+ const dir = await mkdtemp(`${tmpdir3()}/clutor-bench-`);
1005
1027
  cleanup = () => rm2(dir, { recursive: true, force: true });
1006
1028
  file = `${dir}/bench.gguf`;
1007
1029
  await writeSyntheticGguf(file, { arch: "qwen2moe", name: "bench-moe", layers: 8, embedding: 1024, heads: 16, kvHeads: 4, contextLength: 4096, expertCount: 32, expertsUsed: 4, expertFfn: 1024, denseFfn: 0, vocab: 4096, sharedExperts: 0 });
@@ -1022,7 +1044,7 @@ function createClutorServer(o) {
1022
1044
  }
1023
1045
  }
1024
1046
  if (method === "POST" && path === "/api/mcp") {
1025
- const r = await handleMcp(await readBody(req), { isRoot: (d) => rootFor(d).catch(() => null) });
1047
+ const r = await handleMcp(await readBody(req), { isRoot: (d) => rootFor(d).catch(() => null), version: o.version });
1026
1048
  return r.status === 202 ? (res.writeHead(202, cors), void res.end()) : reply(r.status, r.body);
1027
1049
  }
1028
1050
  if (method === "POST" && path === "/api/agent/run") {
@@ -1134,8 +1156,9 @@ function createClutorServer(o) {
1134
1156
 
1135
1157
  // src/engine.ts
1136
1158
  import { spawn as spawn2 } from "child_process";
1137
- import { accessSync as accessSync2, constants as constants2, existsSync } from "fs";
1138
- import { homedir as homedir3 } from "os";
1159
+ import { createHash as createHash4 } from "crypto";
1160
+ import { accessSync as accessSync2, chmodSync, constants as constants2, copyFileSync, existsSync, mkdirSync as mkdirSync2, mkdtempSync, rmSync, writeFileSync } from "fs";
1161
+ import { homedir as homedir3, tmpdir } from "os";
1139
1162
  import { delimiter as delimiter2, dirname, join as join5 } from "path";
1140
1163
  import { fileURLToPath } from "url";
1141
1164
  var EngineError = class extends Error {
@@ -1222,6 +1245,7 @@ function streamArgs(file, o) {
1222
1245
  add("--window", o.window);
1223
1246
  if (o.shift) add("--shift", `${o.shift.at},${o.shift.len}`);
1224
1247
  if (o.noOsCache) a.push("--no-os-cache");
1248
+ if (o.trace) a.push("--trace", o.trace);
1225
1249
  return a;
1226
1250
  }
1227
1251
  async function engineInfo(bin, file) {
@@ -1238,10 +1262,85 @@ async function engineStream(bin, file, o = {}, timeoutMs = 9e5) {
1238
1262
  throw new EngineError("the engine printed something that is not a report");
1239
1263
  }
1240
1264
  }
1265
+ function engineRun(bin, args, onOut, timeoutMs = 36e5) {
1266
+ return new Promise((resolve3, reject) => {
1267
+ const child = spawn2(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
1268
+ let stderr = "";
1269
+ const timer = setTimeout(() => {
1270
+ child.kill("SIGKILL");
1271
+ reject(new EngineError(`${bin} did not finish within ${timeoutMs / 1e3} seconds`));
1272
+ }, timeoutMs);
1273
+ child.stdout.setEncoding("utf8");
1274
+ child.stdout.on("data", onOut);
1275
+ child.stderr.on("data", (b) => {
1276
+ stderr += b.toString();
1277
+ });
1278
+ child.on("error", (e) => {
1279
+ clearTimeout(timer);
1280
+ reject(new EngineError(`could not start ${bin}: ${e.message}`));
1281
+ });
1282
+ child.on("close", (code2) => {
1283
+ clearTimeout(timer);
1284
+ resolve3({ code: code2 ?? 1, stderr });
1285
+ });
1286
+ });
1287
+ }
1288
+ function enginePlatform() {
1289
+ const os = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : void 0;
1290
+ const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : void 0;
1291
+ return os && arch ? `${os}-${arch}` : void 0;
1292
+ }
1293
+ var RELEASE_BASE = (version2) => process.env["CLUTOR_RELEASE_BASE"] ?? `https://github.com/isagarpinninti/clutor/releases/download/v${version2}`;
1294
+ var MAX_ASSET = 64 * 2 ** 20;
1295
+ async function download(url) {
1296
+ if (!url.startsWith("https://") && !/^http:\/\/(127\.0\.0\.1|localhost)[:/]/.test(url)) throw new EngineError(`refusing to download over an insecure connection: ${url}`);
1297
+ let res;
1298
+ try {
1299
+ res = await fetch(url);
1300
+ } catch (e) {
1301
+ throw new EngineError(`could not reach ${url}: ${e instanceof Error ? e.message : String(e)}`);
1302
+ }
1303
+ if (!res.ok) throw new EngineError(`${url} answered ${res.status}${res.status === 404 ? ": no prebuilt engine is published for this version and platform; use `clutor engine build` instead" : ""}`);
1304
+ const buf = Buffer.from(await res.arrayBuffer());
1305
+ if (buf.length > MAX_ASSET) throw new EngineError(`${url} is larger than ${MAX_ASSET / 2 ** 20} MiB; refusing it`);
1306
+ return buf;
1307
+ }
1308
+ async function installPrebuilt(version2, dest = join5(homedir3(), ".clutor", "bin")) {
1309
+ const platform = enginePlatform();
1310
+ if (!platform) throw new EngineError(`no prebuilt engine is published for ${process.platform}/${process.arch}; use \`clutor engine build\` (needs a C compiler)`);
1311
+ const base = RELEASE_BASE(version2), asset = `clutor-engine-${platform}.tar.gz`;
1312
+ const sums = (await download(`${base}/SHA256SUMS`)).toString("utf8");
1313
+ const line = sums.split("\n").map((l) => l.trim().split(/\s+/)).find((p) => p.length >= 2 && p[1].replace(/^\*/, "") === asset);
1314
+ if (!line) throw new EngineError(`SHA256SUMS lists no ${asset}`);
1315
+ const tarball = await download(`${base}/${asset}`);
1316
+ const got = createHash4("sha256").update(tarball).digest("hex");
1317
+ if (got !== line[0].toLowerCase()) throw new EngineError(`checksum mismatch for ${asset}: expected ${line[0]}, got ${got}. Nothing was installed`);
1318
+ const work = mkdtempSync(join5(tmpdir(), "clutor-engine-install-"));
1319
+ try {
1320
+ const file = join5(work, asset);
1321
+ writeFileSync(file, tarball);
1322
+ const x = await run("tar", ["-xzf", file, "-C", work], { timeoutMs: 6e4 });
1323
+ if (x.code !== 0) throw new EngineError(`could not unpack ${asset}: ${x.stderr.trim()}`);
1324
+ const unpacked = join5(work, `clutor-engine-${platform}`, EXE);
1325
+ if (!existsSync(unpacked)) throw new EngineError(`${asset} does not contain ${EXE}`);
1326
+ chmodSync(unpacked, 493);
1327
+ const probe = await run(unpacked, ["--version"], { timeoutMs: 15e3 }).catch((e) => {
1328
+ throw new EngineError(`the downloaded engine does not run here: ${e instanceof Error ? e.message : String(e)}`);
1329
+ });
1330
+ if (probe.code !== 0) throw new EngineError(`the downloaded engine does not run here (exit ${probe.code}); use \`clutor engine build\``);
1331
+ mkdirSync2(dest, { recursive: true });
1332
+ const out = join5(dest, EXE);
1333
+ copyFileSync(unpacked, out);
1334
+ chmodSync(out, 493);
1335
+ return out;
1336
+ } finally {
1337
+ rmSync(work, { recursive: true, force: true });
1338
+ }
1339
+ }
1241
1340
 
1242
1341
  // src/cli.ts
1243
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
1244
- import { tmpdir } from "os";
1342
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1343
+ import { tmpdir as tmpdir2 } from "os";
1245
1344
  import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
1246
1345
  import { fileURLToPath as fileURLToPath2 } from "url";
1247
1346
  import {
@@ -1292,9 +1391,13 @@ Usage: clutor <command> [options]
1292
1391
  Replay a routing trace through the expert cache: baseline LRU against clutor
1293
1392
  bench [--file model.gguf] [--engine] [--cache-mb N] [--tokens N] [--compute kernel|checksum]
1294
1393
  Measure real disk reads on this machine; with the native engine, also compare cache and prefetch strategies
1295
- stream <model.gguf> [--tokens N] [--cache-mb N] [--policy lru|wtinylfu|adaptive] [--smart J,M,TAU|off] [--prefetch on|off] [--dense auto|resident|ring] [--compute kernel|checksum] [--json]
1394
+ stream <model.gguf> [--tokens N] [--cache-mb N] [--policy lru|wtinylfu|adaptive] [--smart J,M,TAU|off] [--prefetch on|off] [--dense auto|resident|ring] [--compute kernel|checksum] [--trace FILE] [--json]
1296
1395
  Stream a model's experts from disk through the native engine (synthetic routing; measured I/O and cache)
1297
- engine build|where Compile the native engine from c/ (needs a C compiler), or show which one is used
1396
+ generate <model.gguf> "<prompt>" [--n N] [--cache-mb N] [--smart J,M,TAU] [--prefetch off|prev|ahead] [--trace FILE]
1397
+ Generate text with the native engine's real forward pass (olmoe models). With --cache-mb the experts stream from disk through a cache of that size
1398
+ quality <model.gguf> <text-file> --cache-mb N [--smart J,M,TAU] [--max-tokens N] [--json]
1399
+ Perplexity, KL divergence and top-token agreement of a streamed (optionally smart-route) run against exact routing
1400
+ engine install|build|where Install the prebuilt native engine (checked against a SHA-256), or compile it from c/ (needs a C compiler), or show which one is used
1298
1401
  code "<task>" [--dir folder] [--apply] [--yes] [--verify "npm test"] [--max-steps N]
1299
1402
  Run the coding agent on a folder; review the diff before anything is written
1300
1403
  serve [dir] [--port ${DEFAULT_PORT}] [--folder dir]... Start the local server: OpenAI and Ollama compatible API, agent, MCP
@@ -1320,7 +1423,7 @@ async function main(argv, io, session) {
1320
1423
  const json = flagBool(args, "json");
1321
1424
  try {
1322
1425
  if (flagBool(args, "version") || command === "version" || argv.includes("-v")) return io.out(version()), 0;
1323
- if (!command && !flagBool(args, "help") && !argv.includes("-h") && io.prompt && !session) return await (await import("./repl-OVODHQI5.js")).runRepl(io, (a, s) => main(a, io, s), (cfg, what) => engineSlashCommand(io, cfg, what));
1426
+ if (!command && !flagBool(args, "help") && !argv.includes("-h") && io.prompt && !session) return await (await import("./repl-DOWMDXFR.js")).runRepl(io, (a, s) => main(a, io, s), (cfg, what) => engineSlashCommand(io, cfg, what), version());
1324
1427
  if (!command || flagBool(args, "help") || command === "help" || argv.includes("-h")) return io.out(HELP), 0;
1325
1428
  switch (command) {
1326
1429
  case "init":
@@ -1329,6 +1432,10 @@ async function main(argv, io, session) {
1329
1432
  return await models(io, args, rest[0], json);
1330
1433
  case "stream":
1331
1434
  return await streamCmd(io, args, rest[0], json);
1435
+ case "generate":
1436
+ return await generateCmd(io, args, rest[0], rest.slice(1).join(" "));
1437
+ case "quality":
1438
+ return await qualityCmd(io, args, rest[0], rest[1], json);
1332
1439
  case "engine":
1333
1440
  return await engineCmd(io, rest[0]);
1334
1441
  case "pull":
@@ -1377,13 +1484,13 @@ async function main(argv, io, session) {
1377
1484
  }
1378
1485
  function init(io, dirArg) {
1379
1486
  const dir = resolve2(io.cwd, dirArg ?? ".");
1380
- mkdirSync2(join6(dir, "models"), { recursive: true });
1487
+ mkdirSync3(join6(dir, "models"), { recursive: true });
1381
1488
  const path = join6(dir, CONFIG_FILE);
1382
1489
  if (existsSync2(path)) {
1383
1490
  io.err(`${path} already exists; leaving it alone`);
1384
1491
  return 1;
1385
1492
  }
1386
- writeFileSync(path, `${JSON.stringify({ port: DEFAULT_PORT, models: "models", backend: { kind: "none" }, memory: { contextTokens: 8192, kvBits: 16 }, cache: { policy: "adaptive" }, agent: { toolMode: "json", maxSteps: 30 } }, null, 2)}
1493
+ writeFileSync2(path, `${JSON.stringify({ port: DEFAULT_PORT, models: "models", backend: { kind: "none" }, memory: { contextTokens: 8192, kvBits: 16 }, cache: { policy: "adaptive" }, agent: { toolMode: "json", maxSteps: 30 } }, null, 2)}
1387
1494
  `);
1388
1495
  const h = hostInfo3();
1389
1496
  io.out(`Created ${path} and ${join6(dir, "models")}.`);
@@ -1502,8 +1609,8 @@ async function bench(io, args, json) {
1502
1609
  let cleanup;
1503
1610
  if (file) path = resolve2(io.cwd, file);
1504
1611
  else {
1505
- const dir = mkdtempSync(join6(tmpdir(), "clutor-bench-"));
1506
- cleanup = () => rmSync(dir, { recursive: true, force: true });
1612
+ const dir = mkdtempSync2(join6(tmpdir2(), "clutor-bench-"));
1613
+ cleanup = () => rmSync2(dir, { recursive: true, force: true });
1507
1614
  path = join6(dir, "bench.gguf");
1508
1615
  io.err("No --file given: writing a test model of about 1 GiB to the temp folder (it will be deleted).");
1509
1616
  await writeSyntheticGguf2(path, { arch: "qwen2moe", name: "bench-moe", layers: 8, embedding: 1024, heads: 16, kvHeads: 4, contextLength: 4096, expertCount: 32, expertsUsed: 4, expertFfn: 1024, denseFfn: 0, vocab: 4096, sharedExperts: 0 });
@@ -1516,7 +1623,7 @@ async function bench(io, args, json) {
1516
1623
  const m = await measureExpertReads2(path, layout, h.totalRamBytes, 96);
1517
1624
  const dm = diskModelFrom(m, expertSizeBytes2(layout), layout.layers.find((l) => l.experts.length)?.experts[0]?.length ?? 1);
1518
1625
  const bin = findEngine();
1519
- if (flagBool(args, "engine") && !bin) throw new UsageError("the native engine is not built: run `clutor engine build` (needs a C compiler)");
1626
+ if (flagBool(args, "engine") && !bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
1520
1627
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1521
1628
  const cmp = bin ? await compareStrategies(bin, path, flagInt(args, "cache-mb", 1, 1e6) ?? Math.max(64, Math.floor(h.totalRamBytes / 2 ** 20 / 16)), flagInt(args, "tokens", 10, 1e5) ?? 300, flagString(args, "dense"), computeFlag(args)) : void 0;
1522
1629
  if (json) {
@@ -1958,7 +2065,7 @@ async function pull(io, args, id) {
1958
2065
  }
1959
2066
  }
1960
2067
  let last = 0;
1961
- const bytes = await downloadFile(downloadUrl(d), dest, { expectedBytes: d.bytes, onProgress: (done, total) => {
2068
+ const bytes = await downloadFile(downloadUrl(d), dest, { expectedBytes: d.bytes, expectedSha256: d.sha256, onProgress: (done, total) => {
1962
2069
  const p = Math.floor(done / total * 10);
1963
2070
  if (p > last) {
1964
2071
  last = p;
@@ -2017,10 +2124,91 @@ function formatComparison(c) {
2017
2124
  table(rows, ["", ...c.map((x) => x.name)])
2018
2125
  ].join("\n");
2019
2126
  }
2127
+ function engineFlags(args) {
2128
+ const out = [];
2129
+ const num = (n, lo, hi) => {
2130
+ const v = flagInt(args, n, lo, hi);
2131
+ if (v !== void 0) out.push(`--${n}`, String(v));
2132
+ };
2133
+ num("cache-mb", 1, 1e7);
2134
+ num("io-threads", 1, 16);
2135
+ num("prefetch-n", 1, 64);
2136
+ num("stage", 1, 1e5);
2137
+ const pol = flagString(args, "policy");
2138
+ if (pol) {
2139
+ if (!["lru", "wtinylfu", "adaptive"].includes(pol)) throw new UsageError("--policy must be lru, wtinylfu or adaptive");
2140
+ out.push("--policy", pol);
2141
+ }
2142
+ const pf = flagString(args, "prefetch");
2143
+ if (pf) {
2144
+ if (!["on", "off", "prev", "ahead"].includes(pf)) throw new UsageError("--prefetch must be off, prev or ahead");
2145
+ out.push("--prefetch", pf);
2146
+ }
2147
+ const sm = flagString(args, "smart");
2148
+ if (sm && sm !== "off") {
2149
+ if (!/^\d+,\d+,[\d.]+$/.test(sm)) throw new UsageError("--smart wants J,M,TAU, for example 4,16,0.02");
2150
+ out.push("--smart", sm);
2151
+ }
2152
+ if (flagBool(args, "no-os-cache")) out.push("--no-os-cache");
2153
+ return out;
2154
+ }
2155
+ async function generateCmd(io, args, file, prompt) {
2156
+ if (!file || !prompt.trim()) throw new UsageError('usage: clutor generate <model.gguf> "<prompt>" [--n N] [--cache-mb N] [--smart J,M,TAU]');
2157
+ const bin = findEngine();
2158
+ if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
2159
+ const path = resolve2(io.cwd, file);
2160
+ if (!existsSync2(path)) throw new UsageError(`${path} does not exist`);
2161
+ const n = flagInt(args, "n", 1, 1e5) ?? 64;
2162
+ const trace = flagString(args, "trace");
2163
+ const argv = ["generate", path, prompt, "--n", String(n), ...engineFlags(args), ...trace ? ["--trace", resolve2(io.cwd, trace)] : []];
2164
+ const r = await engineRun(bin, argv, (s) => (io.write ?? ((t) => io.out(t.replace(/\n$/, ""))))(s));
2165
+ io.write?.("\n");
2166
+ if (r.stderr.trim()) io.err(r.stderr.trim());
2167
+ if (r.code !== 0) {
2168
+ io.err(`the engine exited with ${r.code}`);
2169
+ return 1;
2170
+ }
2171
+ return 0;
2172
+ }
2173
+ async function qualityCmd(io, args, file, text, json) {
2174
+ if (!file || !text) throw new UsageError("usage: clutor quality <model.gguf> <text-file> --cache-mb N [--smart J,M,TAU] [--max-tokens N]");
2175
+ if (flagInt(args, "cache-mb", 1, 1e7) === void 0) throw new UsageError("quality compares against a streamed run: give --cache-mb N");
2176
+ const bin = findEngine();
2177
+ if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
2178
+ const path = resolve2(io.cwd, file), textPath = resolve2(io.cwd, text);
2179
+ if (!existsSync2(path)) throw new UsageError(`${path} does not exist`);
2180
+ if (!existsSync2(textPath)) throw new UsageError(`${textPath} does not exist`);
2181
+ const mt = flagInt(args, "max-tokens", 2, 1e6);
2182
+ const r = await run(bin, ["quality", path, textPath, ...engineFlags(args), ...mt ? ["--max-tokens", String(mt)] : []], { timeoutMs: 36e5 });
2183
+ if (r.code !== 0) {
2184
+ io.err(r.stderr.trim() || `the engine exited with ${r.code}`);
2185
+ return 1;
2186
+ }
2187
+ if (json) {
2188
+ io.out(r.stdout.trim());
2189
+ return 0;
2190
+ }
2191
+ let q;
2192
+ try {
2193
+ q = JSON.parse(r.stdout);
2194
+ } catch {
2195
+ throw new EngineError("the engine printed something that is not a report");
2196
+ }
2197
+ const n = (k) => q[k];
2198
+ io.out([
2199
+ `MEASURED ${n("tokensScored")} tokens scored, teacher-forced`,
2200
+ `MEASURED perplexity ${n("perplexityExact").toFixed(3)} exact routing, ${n("perplexityRun").toFixed(3)} this run (x${n("perplexityRatio").toFixed(4)})`,
2201
+ `MEASURED KL divergence from exact ${n("meanKL").toFixed(4)}; the top token is unchanged at ${(n("top1Agreement") * 100).toFixed(1)}% of positions`,
2202
+ q["smart"] ? `MEASURED smart-route used a resident expert for ${(100 * n("substitutions") / n("expertSlots")).toFixed(1)}% of expert slots; the largest probability moved was ${n("maxProbMoved").toFixed(4)}` : "smart-route off",
2203
+ `MEASURED expert cache hit rate ${(n("hitRate") * 100).toFixed(1)}%, ${n("gbPerToken").toFixed(3)} GB read per token`,
2204
+ "The exact reference maps the whole file, so the OS cache is warm: time is not reported here."
2205
+ ].join("\n"));
2206
+ return 0;
2207
+ }
2020
2208
  async function streamCmd(io, args, file, json) {
2021
2209
  if (!file) throw new UsageError("usage: clutor stream <model.gguf> [--tokens N] [--cache-mb N] [--policy lru|wtinylfu|adaptive] [--smart J,M,TAU|off] [--prefetch on|off]");
2022
2210
  const bin = findEngine();
2023
- if (!bin) throw new UsageError("the native engine is not built: run `clutor engine build` (needs a C compiler)");
2211
+ if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
2024
2212
  const num = (n, lo, hi) => flagInt(args, n, lo, hi);
2025
2213
  const pol = flagString(args, "policy");
2026
2214
  if (pol && !["lru", "wtinylfu", "adaptive"].includes(pol)) throw new UsageError("--policy must be lru, wtinylfu or adaptive");
@@ -2040,6 +2228,7 @@ async function streamCmd(io, args, file, json) {
2040
2228
  ...dn ? { dense: dn } : {},
2041
2229
  ...computeFlag(args) ? { compute: computeFlag(args) } : {},
2042
2230
  ...flagBool(args, "no-os-cache") ? { noOsCache: true } : {},
2231
+ ...flagString(args, "trace") ? { trace: flagString(args, "trace") } : {},
2043
2232
  ...num("window", 1, 1e5) !== void 0 ? { window: num("window", 1, 1e5) } : {},
2044
2233
  ...shiftFlag(args)
2045
2234
  };
@@ -2069,7 +2258,7 @@ function describeStream(r) {
2069
2258
  lines.push(`MEASURED hit rate and peak RSS per window of tokens${note}:
2070
2259
  ${table(r.windows.map((w, i) => [`${i ? r.windows[i - 1].tokEnd + 1 : 1}-${w.tokEnd}`, pct(w.accesses ? w.hits / w.accesses : 0), `${(w.peakRssBytes / 2 ** 20).toFixed(0)} MiB`]), ["tokens", "hit rate", "peak RSS"])}`);
2071
2260
  }
2072
- lines.push(`${t.tokensPerSecond.toFixed(1)} passes per second over SYNTHETIC routing; the compute is ${r.config.computeMode === "kernel" ? `a Q4_0 x Q8_0 kernel (${r.config.computeKernel}) over the loaded bytes (${t.computeSeconds.toFixed(2)} s in total), not this model's maths` : "a checksum"}: this is not model tokens per second.`);
2261
+ lines.push(`${t.tokensPerSecond.toFixed(1)} passes per second over ${r.provenance.routing === "real" ? "REAL (replayed) routing recorded from this model" : "SYNTHETIC routing"}; the compute is ${r.config.computeMode === "kernel" ? `a Q4_0 x Q8_0 kernel (${r.config.computeKernel}) over the loaded bytes (${t.computeSeconds.toFixed(2)} s in total), not this model's maths` : "a checksum"}: this is not model tokens per second.`);
2073
2262
  return lines.join("\n");
2074
2263
  }
2075
2264
  async function engineCmd(io, sub) {
@@ -2091,7 +2280,20 @@ async function engineCmd(io, sub) {
2091
2280
  throw e;
2092
2281
  }
2093
2282
  }
2094
- throw new UsageError("usage: clutor engine build | where");
2283
+ if (sub === "install") {
2284
+ io.err("Downloading the prebuilt native engine and checking its SHA-256...");
2285
+ try {
2286
+ io.out(`Installed ${await installPrebuilt(version())}`);
2287
+ return 0;
2288
+ } catch (e) {
2289
+ if (e instanceof EngineError) {
2290
+ io.err(e.message);
2291
+ return 1;
2292
+ }
2293
+ throw e;
2294
+ }
2295
+ }
2296
+ throw new UsageError("usage: clutor engine install | build | where");
2095
2297
  }
2096
2298
  async function engineSlashCommand(io, config, what) {
2097
2299
  const file = config.backend.llamaServer.model;
@@ -2160,8 +2362,11 @@ export {
2160
2362
  buildEngine,
2161
2363
  engineInfo,
2162
2364
  engineStream,
2365
+ engineRun,
2366
+ enginePlatform,
2367
+ installPrebuilt,
2163
2368
  version,
2164
2369
  main,
2165
2370
  startBackendIfConfigured
2166
2371
  };
2167
- //# sourceMappingURL=chunk-YL63VENN.js.map
2372
+ //# sourceMappingURL=chunk-2FBPYNHP.js.map