mu-agent 0.2.1 → 0.2.3

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.
@@ -3,9 +3,9 @@ import { createRequire as __muCreateRequire } from "node:module"; const require
3
3
  // packages/kyrn-judge/src/mude/command.ts
4
4
  import { execFileSync as execFileSync2 } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, statfsSync, writeFileSync as writeFileSync6 } from "node:fs";
6
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync4, statfsSync, writeFileSync as writeFileSync7 } from "node:fs";
7
7
  import { homedir as homedir3, release, totalmem as totalmem2 } from "node:os";
8
- import { join as join6 } from "node:path";
8
+ import { join as join7 } from "node:path";
9
9
 
10
10
  // packages/kyrn-judge/src/mude/config.ts
11
11
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
@@ -92,21 +92,21 @@ function readEnvEntries(text) {
92
92
  }
93
93
  let body = rest.slice(1);
94
94
  let end = closingQuote(body, quote);
95
- const startedAt = index;
95
+ const startedAt2 = index;
96
96
  while (end < 0 && index + 1 < lines.length) {
97
97
  body += `
98
98
  ${lines[++index]}`;
99
99
  end = closingQuote(body, quote);
100
100
  }
101
101
  if (end < 0) {
102
- index = startedAt;
102
+ index = startedAt2;
103
103
  continue;
104
104
  }
105
105
  const value = body.slice(0, end);
106
106
  entries.push({
107
107
  name,
108
108
  value: quote === '"' ? value.replace(/\\(["\\$`])/g, "$1") : value,
109
- first: startedAt,
109
+ first: startedAt2,
110
110
  last: index
111
111
  });
112
112
  }
@@ -338,8 +338,8 @@ var FILES = [
338
338
  ];
339
339
  var MUDE_FILES = FILES.map(([path, size, sha256]) => ({ path, size, sha256 }));
340
340
  var MUDE_BYTES = MUDE_FILES.reduce((sum, file) => sum + file.size, 0);
341
- function mudeFileUrl(path, endpoint = "https://huggingface.co") {
342
- return `${endpoint.replace(/\/+$/, "")}/${MUDE_REPO}/resolve/${MUDE_REVISION}/${path.split("/").map(encodeURIComponent).join("/")}`;
341
+ function fileUrl(repo, revision, path, endpoint = "https://huggingface.co") {
342
+ return `${endpoint.replace(/\/+$/, "")}/${repo}/resolve/${revision}/${path.split("/").map(encodeURIComponent).join("/")}`;
343
343
  }
344
344
 
345
345
  // packages/kyrn-judge/src/mude/server.ts
@@ -409,12 +409,21 @@ function tail(file, lines = 12) {
409
409
  return "";
410
410
  }
411
411
  }
412
+ var StartStopped = class extends Error {
413
+ /** Whether the server went too: one this call started does, one that was running before does not. */
414
+ serverStopped;
415
+ constructor(serverStopped = false) {
416
+ super("Stopped while the mude 1.0 server was starting");
417
+ this.name = "StartStopped";
418
+ this.serverStopped = serverStopped;
419
+ }
420
+ };
412
421
  async function startServer(paths, options) {
413
422
  const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
414
423
  const timeoutMs = options.timeoutMs ?? 18e4;
415
424
  const running = readPid(paths);
416
425
  if (running && serverAlive(paths, running)) {
417
- if (options.wait !== false) await untilReady(paths, running, options.port, timeoutMs, sleep);
426
+ if (options.wait !== false) await untilReady(paths, running, options.port, timeoutMs, sleep, options.signal);
418
427
  return { pid: running, port: options.port, already: true };
419
428
  }
420
429
  if (!await portFree(options.port)) {
@@ -436,7 +445,7 @@ async function startServer(paths, options) {
436
445
  "--port",
437
446
  String(options.port),
438
447
  "--name",
439
- MUDE_MODEL,
448
+ options.name ?? MUDE_MODEL,
440
449
  "--verify",
441
450
  "fast"
442
451
  ],
@@ -463,12 +472,21 @@ async function startServer(paths, options) {
463
472
  child.unref();
464
473
  writeFileSync2(paths.pid, `${pid}
465
474
  `, { mode: 384 });
466
- if (options.wait !== false) await untilReady(paths, pid, options.port, timeoutMs, sleep);
475
+ if (options.wait !== false) {
476
+ try {
477
+ await untilReady(paths, pid, options.port, timeoutMs, sleep, options.signal);
478
+ } catch (error) {
479
+ if (!(error instanceof StartStopped)) throw error;
480
+ await stopServer(paths);
481
+ throw new StartStopped(true);
482
+ }
483
+ }
467
484
  return { pid, port: options.port, already: false };
468
485
  }
469
- async function untilReady(paths, pid, port2, timeoutMs, sleep) {
486
+ async function untilReady(paths, pid, port2, timeoutMs, sleep, signal) {
470
487
  const deadline = Date.now() + timeoutMs;
471
488
  while (Date.now() < deadline) {
489
+ if (signal?.aborted) throw new StartStopped();
472
490
  if ((await health(port2))?.status === "ready") return;
473
491
  if (!serverAlive(paths, pid)) {
474
492
  rmSync(paths.pid, { force: true });
@@ -508,6 +526,11 @@ function serverMemory(pid) {
508
526
  var ROUTED_AWAY = ["tool.description", "tool.injection"];
509
527
  var MUDE = "mude";
510
528
  var KEY_VARIABLE = "MU_JUDGE_MUDE_KEY";
529
+ var MUDE_JUDGE = {
530
+ name: MUDE,
531
+ keyVariable: KEY_VARIABLE,
532
+ entry: (port2) => port2 === DEFAULT_PORT ? void 0 : entryFor(port2)
533
+ };
511
534
  function readState(paths) {
512
535
  try {
513
536
  const value = JSON.parse(readFileSync3(paths.state, "utf8"));
@@ -520,18 +543,22 @@ function writeState(paths, state) {
520
543
  writeFileSync3(paths.state, `${JSON.stringify(state, null, 2)}
521
544
  `, { mode: 384 });
522
545
  }
523
- function judgeSetting(files) {
546
+ function judgeSetting(files, name = MUDE) {
524
547
  const file = readJsonFile(judgeConfigPath(files.agentDir));
525
548
  if (file.state === "unreadable") return { enabled: false, localOnly: false, fallback: [], unreadable: true };
526
549
  const config = file.state === "ok" ? file.value : {};
527
550
  const tiers = tiersOf(config);
528
- const enabled = tiers[0] === MUDE && !judgeOff(config);
529
- const fallback = tiers.filter((tier) => tier !== MUDE);
551
+ const enabled = tiers[0] === name && !judgeOff(config);
552
+ const fallback = tiers.filter((tier) => tier !== name);
530
553
  return { enabled, localOnly: enabled && fallback.length === 0, fallback };
531
554
  }
532
- function mudeInTiers(files) {
555
+ function mudeInTiers(files, name = MUDE) {
533
556
  const file = readJsonFile(judgeConfigPath(files.agentDir));
534
- return file.state === "ok" && tiersOf(file.value).includes(MUDE);
557
+ return file.state === "ok" && tiersOf(file.value).includes(name);
558
+ }
559
+ function currentTiers(files) {
560
+ const file = readJsonFile(judgeConfigPath(files.agentDir));
561
+ return file.state === "ok" ? tiersOf(file.value) : [];
535
562
  }
536
563
  function readConfig(files) {
537
564
  const path = judgeConfigPath(files.agentDir);
@@ -565,18 +592,20 @@ function withoutOwnRoutes(config, state) {
565
592
  function entryFor(port2) {
566
593
  return { ...BUILT_IN_JUDGES[MUDE], baseUrl: mudeBaseUrl(port2) };
567
594
  }
568
- function enableMude(paths, files, options) {
595
+ function enableLocal(judge, paths, files, options) {
596
+ const name = judge.name;
569
597
  const { path, config } = readConfig(files);
570
598
  const state = readState(paths);
571
- const current = tiersOf(config).filter((tier) => tier !== MUDE);
599
+ const current = tiersOf(config).filter((tier) => tier !== name);
572
600
  const before = current.length > 0 ? current : [...state?.previousTiers ?? ["jev"]];
573
- const tiers = options.localOnly ? [MUDE] : [MUDE, ...before.filter((tier) => tier !== MUDE)];
601
+ const tiers = options.localOnly ? [name] : [name, ...before.filter((tier) => tier !== name)];
574
602
  const judgeWasOff = state?.judgeWasOff ?? judgeOff(config);
575
603
  let next = withJudge(withTiers(withoutOwnRoutes(config, state), tiers), true);
576
604
  const judges = judgesOf(next);
577
- const entry = options.port !== DEFAULT_PORT;
578
- if (entry) judges[MUDE] = entryFor(options.port);
579
- else if (state?.wroteEntry) delete judges[MUDE];
605
+ const written = judge.entry(options.port);
606
+ if (written) judges[name] = written;
607
+ else if (state?.wroteEntry) delete judges[name];
608
+ const entry = written !== void 0;
580
609
  next = withSection(next, "judges", judges);
581
610
  const fallback = tiers.slice(1);
582
611
  const routes = recordOf(next.routes);
@@ -592,9 +621,9 @@ function enableMude(paths, files, options) {
592
621
  const backups = new Backups(files.home, files.now ?? /* @__PURE__ */ new Date());
593
622
  backups.save(path);
594
623
  writeJsonFile(path, next);
595
- if (envFileValue(readText(files.envFile), KEY_VARIABLE) !== options.key) {
624
+ if (envFileValue(readText(files.envFile), judge.keyVariable) !== options.key) {
596
625
  backups.save(files.envFile);
597
- writeEnvValue(files.envFile, KEY_VARIABLE, options.key);
626
+ writeEnvValue(files.envFile, judge.keyVariable, options.key);
598
627
  }
599
628
  writeState(paths, {
600
629
  port: options.port,
@@ -606,15 +635,18 @@ function enableMude(paths, files, options) {
606
635
  return tiers;
607
636
  }
608
637
  function disableMude(paths, files, options = {}) {
638
+ return disableLocal(MUDE_JUDGE, paths, files, options);
639
+ }
640
+ function disableLocal(judge, paths, files, options = {}) {
609
641
  const { path, config } = readConfig(files);
610
642
  const state = readState(paths);
611
- const rest = tiersOf(config).filter((tier) => tier !== MUDE);
643
+ const rest = tiersOf(config).filter((tier) => tier !== judge.name);
612
644
  const tiers = rest.length > 0 ? rest : [...state?.previousTiers ?? ["jev"]];
613
645
  let next = withTiers(withoutOwnRoutes(config, state), tiers);
614
646
  if (state?.judgeWasOff) next = withJudge(next, false);
615
647
  if (options.removeEntry && state?.wroteEntry) {
616
648
  const judges = judgesOf(next);
617
- delete judges[MUDE];
649
+ delete judges[judge.name];
618
650
  next = withSection(next, "judges", judges);
619
651
  }
620
652
  new Backups(files.home, files.now ?? /* @__PURE__ */ new Date()).save(path);
@@ -654,6 +686,7 @@ import { open } from "node:fs/promises";
654
686
  import { homedir } from "node:os";
655
687
  import { dirname as dirname2, join as join3 } from "node:path";
656
688
  import { pipeline } from "node:stream/promises";
689
+ import { setTimeout as wait } from "node:timers/promises";
657
690
  var CHECKPOINT_BYTES = 32 * 1024 * 1024;
658
691
  var RETRY_WAITS = [2e3, 5e3, 1e4, 2e4, 3e4];
659
692
  function huggingFaceToken(env = process.env, home = homedir()) {
@@ -679,6 +712,14 @@ async function sha256File(path) {
679
712
  await pipeline(createReadStream(path), hash);
680
713
  return hash.digest("hex");
681
714
  }
715
+ var pinnedHash = (file) => file.sha256 ?? file.gitSha1 ?? "";
716
+ async function hashOf(path, file) {
717
+ if (file.sha256 || !file.gitSha1) return sha256File(path);
718
+ const hash = createHash("sha1");
719
+ hash.update(`blob ${statSync3(path).size}\0`);
720
+ await pipeline(createReadStream(path), hash);
721
+ return hash.digest("hex");
722
+ }
682
723
  function readVerified(dir) {
683
724
  try {
684
725
  const value = JSON.parse(readFileSync4(join3(dir, ".mu-verified.json"), "utf8"));
@@ -689,7 +730,7 @@ function readVerified(dir) {
689
730
  }
690
731
  function knownGood(dir, file, verified) {
691
732
  const seen = verified[file.path];
692
- if (!seen || seen.sha256 !== file.sha256) return false;
733
+ if (!seen || seen.sha256 !== pinnedHash(file)) return false;
693
734
  try {
694
735
  const stats = statSync3(join3(dir, file.path));
695
736
  return stats.size === file.size && stats.mtimeMs === seen.mtimeMs;
@@ -699,7 +740,7 @@ function knownGood(dir, file, verified) {
699
740
  }
700
741
  function remember(dir, verified, file) {
701
742
  const stats = statSync3(join3(dir, file.path));
702
- verified[file.path] = { size: stats.size, mtimeMs: stats.mtimeMs, sha256: file.sha256 };
743
+ verified[file.path] = { size: stats.size, mtimeMs: stats.mtimeMs, sha256: pinnedHash(file) };
703
744
  writeFileSync4(join3(dir, ".mu-verified.json"), `${JSON.stringify(verified, null, 1)}
704
745
  `);
705
746
  }
@@ -750,7 +791,7 @@ async function downloadFiles(options) {
750
791
  mkdirSync3(dirname2(target), { recursive: true });
751
792
  if (existsSync3(target) && statSync3(target).size === file.size) {
752
793
  options.onProgress?.({ file: file.path, checking: true, done: settled, total });
753
- if (await sha256File(target) === file.sha256) {
794
+ if (await hashOf(target, file) === pinnedHash(file)) {
754
795
  remember(dir, verified, file);
755
796
  settled += file.size;
756
797
  continue;
@@ -761,17 +802,17 @@ async function downloadFiles(options) {
761
802
  const onBytes = (bytes) => options.onProgress?.({ file: file.path, checking: false, done: settled + bytes, total });
762
803
  let resumed = await fetchFile(options, file, part, onBytes);
763
804
  options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
764
- let actual = await sha256File(part);
765
- if (actual !== file.sha256 && resumed) {
805
+ let actual = await hashOf(part, file);
806
+ if (actual !== pinnedHash(file) && resumed) {
766
807
  dropPart(part);
767
808
  resumed = await fetchFile(options, file, part, onBytes);
768
809
  options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
769
- actual = await sha256File(part);
810
+ actual = await hashOf(part, file);
770
811
  }
771
- if (actual !== file.sha256) {
812
+ if (actual !== pinnedHash(file)) {
772
813
  dropPart(part);
773
814
  throw new DownloadError(
774
- `${file.path} does not match its pinned sha256 (got ${actual.slice(0, 12)}\u2026, want ${file.sha256.slice(0, 12)}\u2026); it was removed`,
815
+ `${file.path} does not match its pinned ${file.sha256 ? "sha256" : "git hash"} (got ${actual.slice(0, 12)}\u2026, want ${pinnedHash(file).slice(0, 12)}\u2026); it was removed`,
775
816
  "integrity"
776
817
  );
777
818
  }
@@ -805,7 +846,8 @@ function writeSaved(part, bytes) {
805
846
  }
806
847
  async function fetchFile(options, file, part, onBytes) {
807
848
  const attempts = options.attempts ?? 8;
808
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
849
+ const sleep = options.sleep ?? ((ms) => wait(ms, void 0, { signal: options.signal }).catch(() => {
850
+ }));
809
851
  let resumed = false;
810
852
  let failures = 0;
811
853
  let lastError;
@@ -817,6 +859,12 @@ async function fetchFile(options, file, part, onBytes) {
817
859
  return resumed;
818
860
  } catch (error) {
819
861
  if (error instanceof DownloadError && error.kind !== "network") throw error;
862
+ const code = error.code;
863
+ if (code === "ENOSPC" || code === "EDQUOT") {
864
+ throw new Error(
865
+ `The disk is full: couldn't write ${file.path}. Free up some space, then run mu judge mude install to resume`
866
+ );
867
+ }
820
868
  lastError = error;
821
869
  failures = resumeFrom(part, file.size, options.checkpointBytes) > before ? 1 : failures + 1;
822
870
  if (failures >= attempts) break;
@@ -825,7 +873,7 @@ async function fetchFile(options, file, part, onBytes) {
825
873
  }
826
874
  const reason = lastError instanceof Error ? lastError.message : String(lastError);
827
875
  throw new DownloadError(
828
- `${file.path} could not be downloaded (${reason}); run the install again to go on from here`,
876
+ `${file.path} could not be downloaded (${reason}). Run mu judge mude install again to resume`,
829
877
  "network"
830
878
  );
831
879
  }
@@ -855,7 +903,7 @@ async function fetchOnce(options, file, part, onBytes) {
855
903
  });
856
904
  if (response.status === 401 || response.status === 403 || response.status === 404) {
857
905
  throw new DownloadError(
858
- `Hugging Face answered ${response.status} for ${file.path}: the model is not public yet, or this machine's token cannot read it`,
906
+ `Hugging Face returned ${response.status} for ${file.path}: the model isn't public yet, or this machine's token can't access it`,
859
907
  "access"
860
908
  );
861
909
  }
@@ -908,6 +956,10 @@ async function fetchOnce(options, file, part, onBytes) {
908
956
  }
909
957
  }
910
958
 
959
+ // packages/kyrn-judge/src/mude/models.ts
960
+ import { mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
961
+ import { join as join5 } from "node:path";
962
+
911
963
  // packages/kyrn-judge/src/mude/paths.ts
912
964
  import { join as join4 } from "node:path";
913
965
  function mudePaths(muHome) {
@@ -926,16 +978,209 @@ function mudePaths(muHome) {
926
978
  pid: join4(root, "server.pid"),
927
979
  log: join4(root, "server.log"),
928
980
  state: join4(root, "state.json"),
929
- install: join4(root, "install.json")
981
+ install: join4(root, "install.json"),
982
+ installLock: join4(root, "install.lock")
983
+ };
984
+ }
985
+
986
+ // packages/kyrn-judge/src/mude/models.ts
987
+ var ModelError = class extends Error {
988
+ kind;
989
+ constructor(message, kind) {
990
+ super(message);
991
+ this.name = "ModelError";
992
+ this.kind = kind;
993
+ }
994
+ };
995
+ var PART = /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/;
996
+ function parseModelRef(input) {
997
+ const text = input.trim();
998
+ let repo;
999
+ let revision;
1000
+ if (/^https?:\/\//i.test(text) || /^(www\.)?(huggingface\.co|hf\.co)\//i.test(text)) {
1001
+ let url;
1002
+ try {
1003
+ url = new URL(/^https?:\/\//i.test(text) ? text : `https://${text}`);
1004
+ } catch {
1005
+ throw new ModelError(`${text} isn't a Hugging Face link`, "usage");
1006
+ }
1007
+ if (!/^(www\.)?(huggingface\.co|hf\.co)$/i.test(url.hostname))
1008
+ throw new ModelError(`${text} isn't a Hugging Face link`, "usage");
1009
+ const parts = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
1010
+ if (parts[0] === "datasets" || parts[0] === "spaces") {
1011
+ throw new ModelError(
1012
+ `${text} is a Hugging Face ${parts[0] === "datasets" ? "dataset" : "Space"}, not a model`,
1013
+ "usage"
1014
+ );
1015
+ }
1016
+ if (parts.length < 2)
1017
+ throw new ModelError(
1018
+ `${text} doesn't name a model: use a link like https://huggingface.co/owner/name`,
1019
+ "usage"
1020
+ );
1021
+ repo = `${parts[0]}/${parts[1]}`;
1022
+ if (["tree", "blob", "resolve"].includes(parts[2] ?? "") && parts[3]) revision = parts[3];
1023
+ } else {
1024
+ const at = text.lastIndexOf("@");
1025
+ repo = at > 0 ? text.slice(0, at) : text;
1026
+ revision = at > 0 ? text.slice(at + 1) : void 0;
1027
+ }
1028
+ const [owner, name, extra] = repo.split("/");
1029
+ if (!owner || !name || extra !== void 0 || !PART.test(owner) || !PART.test(name)) {
1030
+ throw new ModelError(
1031
+ `${text} isn't a Hugging Face model: give its link or owner/name (for example qybaihe/mude-1.0-mlx-8bit)`,
1032
+ "usage"
1033
+ );
1034
+ }
1035
+ if (revision !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/.test(revision)) {
1036
+ throw new ModelError(`${revision} isn't a branch, tag or commit`, "usage");
1037
+ }
1038
+ return { repo, revision };
1039
+ }
1040
+ var SERVER_FILE = "d3_server_mac.py";
1041
+ async function resolveModel(ref, options = {}) {
1042
+ const endpoint = (options.endpoint ?? "https://huggingface.co").replace(/\/+$/, "");
1043
+ const get = options.fetch ?? fetch;
1044
+ const headers = options.token ? { Authorization: `Bearer ${options.token}` } : {};
1045
+ const request = async (url) => {
1046
+ let response;
1047
+ try {
1048
+ response = await get(url, { headers, signal: AbortSignal.timeout(3e4) });
1049
+ } catch (error) {
1050
+ throw new ModelError(
1051
+ `Couldn't reach Hugging Face (${error instanceof Error ? error.message : String(error)}): check the connection, then add it again`,
1052
+ "network"
1053
+ );
1054
+ }
1055
+ if (response.status === 401 || response.status === 403) {
1056
+ throw new ModelError(
1057
+ `Hugging Face won't show ${ref.repo} (${response.status}): it's private or gated. Accept its terms on huggingface.co, set HF_TOKEN, then add it again`,
1058
+ "access"
1059
+ );
1060
+ }
1061
+ if (response.status === 404) {
1062
+ throw new ModelError(
1063
+ `Hugging Face has no model ${ref.repo}${ref.revision ? ` at ${ref.revision}` : ""}`,
1064
+ "notFound"
1065
+ );
1066
+ }
1067
+ if (!response.ok) throw new ModelError(`Hugging Face returned ${response.status} for ${ref.repo}`, "network");
1068
+ return response;
1069
+ };
1070
+ const info = await (await request(`${endpoint}/api/models/${ref.repo}/revision/${encodeURIComponent(ref.revision ?? "main")}`)).json();
1071
+ const revision = typeof info.sha === "string" ? info.sha : "";
1072
+ if (!/^[0-9a-f]{40}$/.test(revision)) throw new Error(`Hugging Face didn't say which commit ${ref.repo} is at`);
1073
+ const files = [];
1074
+ let next = `${endpoint}/api/models/${ref.repo}/tree/${revision}?recursive=true`;
1075
+ while (next) {
1076
+ const response = await request(next);
1077
+ for (const entry of await response.json()) {
1078
+ if (entry.type !== "file" || typeof entry.path !== "string" || typeof entry.size !== "number") continue;
1079
+ if (entry.path.split("/").some((part) => part === ".." || part === "")) continue;
1080
+ const sha256 = entry.lfs?.oid;
1081
+ if (sha256 && /^[0-9a-f]{64}$/.test(sha256)) files.push({ path: entry.path, size: entry.size, sha256 });
1082
+ else if (entry.oid && /^[0-9a-f]{40}$/.test(entry.oid))
1083
+ files.push({ path: entry.path, size: entry.size, gitSha1: entry.oid });
1084
+ else throw new Error(`Hugging Face listed ${entry.path} of ${ref.repo} without a hash, so mu can't check it`);
1085
+ }
1086
+ next = /<([^>]+)>;\s*rel="next"/.exec(response.headers.get("link") ?? "")?.[1];
1087
+ }
1088
+ if (!files.some((file) => file.path === SERVER_FILE)) {
1089
+ throw new ModelError(
1090
+ `${ref.repo} isn't packaged as a decision model mu can run: it has no ${SERVER_FILE}. mu runs models packaged like mude 1.0's MLX edition, with their own System One server`,
1091
+ "notRunnable"
1092
+ );
1093
+ }
1094
+ return { revision, files };
1095
+ }
1096
+ function reserved(name) {
1097
+ return name in BUILT_IN_JUDGES || name === "off" || name.startsWith("jev") || name.includes(":");
1098
+ }
1099
+ var slug = (text) => text.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1100
+ function modelId(repo, taken) {
1101
+ const [owner = "", name = ""] = repo.split("/");
1102
+ const short = slug(name);
1103
+ if (short && !reserved(short) && !taken.has(short)) return short;
1104
+ const long = slug(`${owner}-${name}`);
1105
+ if (!reserved(long) && !taken.has(long)) return long;
1106
+ for (let n = 2; ; n++) if (!taken.has(`${long}-${n}`)) return `${long}-${n}`;
1107
+ }
1108
+ function modelsDir(muHome) {
1109
+ return join5(muHome, "local-judge", "models");
1110
+ }
1111
+ function modelPaths(muHome, record) {
1112
+ const shared = mudePaths(muHome);
1113
+ const root = join5(modelsDir(muHome), record.id);
1114
+ const model = join5(root, "model", record.revision);
1115
+ return {
1116
+ ...shared,
1117
+ root,
1118
+ model,
1119
+ verified: join5(model, ".mu-verified.json"),
1120
+ key: join5(root, "key"),
1121
+ pid: join5(root, "server.pid"),
1122
+ log: join5(root, "server.log"),
1123
+ state: join5(root, "state.json"),
1124
+ install: join5(root, "install.json"),
1125
+ installLock: join5(root, "install.lock")
1126
+ };
1127
+ }
1128
+ function writeRecord(muHome, record) {
1129
+ const root = join5(modelsDir(muHome), record.id);
1130
+ mkdirSync4(root, { recursive: true });
1131
+ writeFileSync5(join5(root, "model.json"), `${JSON.stringify(record, null, 1)}
1132
+ `);
1133
+ }
1134
+ function isRecord2(value) {
1135
+ if (typeof value !== "object" || value === null) return false;
1136
+ const record = value;
1137
+ return typeof record.id === "string" && typeof record.repo === "string" && typeof record.revision === "string" && typeof record.bytes === "number" && Array.isArray(record.files);
1138
+ }
1139
+ function listRecords(muHome) {
1140
+ let names;
1141
+ try {
1142
+ names = readdirSync(modelsDir(muHome));
1143
+ } catch {
1144
+ return [];
1145
+ }
1146
+ const records = [];
1147
+ for (const name of names.sort()) {
1148
+ try {
1149
+ const value = JSON.parse(readFileSync5(join5(modelsDir(muHome), name, "model.json"), "utf8"));
1150
+ if (isRecord2(value) && value.id === name) records.push(value);
1151
+ } catch {
1152
+ }
1153
+ }
1154
+ return records;
1155
+ }
1156
+ function findRecord(muHome, name) {
1157
+ const records = listRecords(muHome);
1158
+ const lower = name.toLowerCase();
1159
+ return records.find((record) => record.id === lower) ?? records.find((record) => record.repo.toLowerCase() === lower);
1160
+ }
1161
+ function keyVariable(id) {
1162
+ return `MU_JUDGE_${id.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_KEY`;
1163
+ }
1164
+ function localJudgeOf(id) {
1165
+ return {
1166
+ name: id,
1167
+ keyVariable: keyVariable(id),
1168
+ entry: (port2) => ({
1169
+ type: "typesafe",
1170
+ baseUrl: mudeBaseUrl(port2),
1171
+ model: id,
1172
+ apiKeyEnv: keyVariable(id),
1173
+ timeoutMs: 4e3
1174
+ })
930
1175
  };
931
1176
  }
932
1177
 
933
1178
  // packages/kyrn-judge/src/mude/python.ts
934
1179
  import { spawn as spawn2 } from "node:child_process";
935
1180
  import { createHash as createHash2 } from "node:crypto";
936
- import { accessSync, constants, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
1181
+ import { accessSync, constants, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
937
1182
  import { homedir as homedir2 } from "node:os";
938
- import { delimiter, join as join5 } from "node:path";
1183
+ import { delimiter, join as join6 } from "node:path";
939
1184
  var PYTHON_VERSION = "3.13";
940
1185
  var UV_RELEASE = {
941
1186
  version: "0.13.1",
@@ -953,14 +1198,14 @@ function executable(path) {
953
1198
  function findUv(paths, env = process.env, home = homedir2()) {
954
1199
  const dirs = [
955
1200
  ...(env.PATH ?? "").split(delimiter),
956
- join5(home, ".local", "bin"),
957
- join5(home, ".cargo", "bin"),
1201
+ join6(home, ".local", "bin"),
1202
+ join6(home, ".cargo", "bin"),
958
1203
  "/opt/homebrew/bin",
959
1204
  "/usr/local/bin",
960
1205
  paths.tools
961
1206
  ];
962
1207
  for (const dir of dirs) {
963
- if (dir && executable(join5(dir, "uv"))) return join5(dir, "uv");
1208
+ if (dir && executable(join6(dir, "uv"))) return join6(dir, "uv");
964
1209
  }
965
1210
  return void 0;
966
1211
  }
@@ -990,36 +1235,47 @@ function run(command, args, options = {}) {
990
1235
  });
991
1236
  }
992
1237
  async function downloadUv(paths, options = {}) {
993
- mkdirSync4(paths.tools, { recursive: true });
1238
+ mkdirSync5(paths.tools, { recursive: true });
994
1239
  const archive = "uv.tar.gz";
995
1240
  options.onLine?.(`Downloading uv ${UV_RELEASE.version}`);
996
- const response = await (options.fetch ?? fetch)(UV_RELEASE.url, { redirect: "follow" });
997
- if (!response.ok) throw new Error(`uv could not be downloaded: HTTP ${response.status}`);
998
- const bytes = Buffer.from(await response.arrayBuffer());
1241
+ const timeoutMs = options.timeoutMs ?? 3e5;
1242
+ const timeout = AbortSignal.timeout(timeoutMs);
1243
+ let bytes;
1244
+ try {
1245
+ const response = await (options.fetch ?? fetch)(UV_RELEASE.url, {
1246
+ redirect: "follow",
1247
+ signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout
1248
+ });
1249
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1250
+ bytes = Buffer.from(await response.arrayBuffer());
1251
+ } catch (error) {
1252
+ const reason = timeout.aborted ? `no answer in ${timeoutMs / 1e3} s` : error instanceof Error ? error.message : String(error);
1253
+ throw new Error(`uv could not be downloaded from GitHub (${reason}): run mu judge mude install again`);
1254
+ }
999
1255
  const actual = createHash2("sha256").update(bytes).digest("hex");
1000
1256
  if (actual !== UV_RELEASE.sha256)
1001
1257
  throw new Error("The downloaded uv does not match its pinned sha256; nothing was installed");
1002
- writeFileSync5(join5(paths.tools, archive), bytes);
1258
+ writeFileSync6(join6(paths.tools, archive), bytes);
1003
1259
  const unpacked = await run("/usr/bin/tar", [
1004
1260
  "-xzf",
1005
- join5(paths.tools, archive),
1261
+ join6(paths.tools, archive),
1006
1262
  "-C",
1007
1263
  paths.tools,
1008
1264
  "--strip-components",
1009
1265
  "1"
1010
1266
  ]);
1011
- rmSync3(join5(paths.tools, archive), { force: true });
1012
- if (unpacked.code !== 0 || !executable(join5(paths.tools, "uv")))
1013
- throw new Error(`uv could not be unpacked: ${unpacked.output}`);
1014
- return join5(paths.tools, "uv");
1267
+ rmSync3(join6(paths.tools, archive), { force: true });
1268
+ if (unpacked.code !== 0 || !executable(join6(paths.tools, "uv")))
1269
+ throw new Error(`Couldn't unpack uv: ${unpacked.output}`);
1270
+ return join6(paths.tools, "uv");
1015
1271
  }
1016
1272
  function lockDigest(lockFile) {
1017
- return createHash2("sha256").update(readFileSync5(lockFile)).digest("hex");
1273
+ return createHash2("sha256").update(readFileSync6(lockFile)).digest("hex");
1018
1274
  }
1019
1275
  function venvReady(paths, lockFile) {
1020
1276
  if (!existsSync4(paths.python)) return false;
1021
1277
  try {
1022
- return readFileSync5(paths.venvLock, "utf8").trim() === lockDigest(lockFile);
1278
+ return readFileSync6(paths.venvLock, "utf8").trim() === lockDigest(lockFile);
1023
1279
  } catch {
1024
1280
  return false;
1025
1281
  }
@@ -1032,7 +1288,7 @@ async function ensureVenv(paths, lockFile, uv, options = {}) {
1032
1288
  env,
1033
1289
  onLine: options.onLine
1034
1290
  });
1035
- if (made.code !== 0) throw new Error(`The Python environment could not be made (uv venv):
1291
+ if (made.code !== 0) throw new Error(`Couldn't create the Python environment (uv venv):
1036
1292
  ${made.output}`);
1037
1293
  const installed2 = await run(
1038
1294
  uv,
@@ -1051,9 +1307,9 @@ ${made.output}`);
1051
1307
  { env, onLine: options.onLine }
1052
1308
  );
1053
1309
  if (installed2.code !== 0)
1054
- throw new Error(`mude's Python packages could not be installed (uv pip install):
1310
+ throw new Error(`Couldn't install mude's Python packages (uv pip install):
1055
1311
  ${installed2.output}`);
1056
- writeFileSync5(paths.venvLock, `${lockDigest(lockFile)}
1312
+ writeFileSync6(paths.venvLock, `${lockDigest(lockFile)}
1057
1313
  `);
1058
1314
  }
1059
1315
 
@@ -1062,7 +1318,7 @@ var USAGE = `mu judge mude: mude 1.0, mu's own decision model optimized for codi
1062
1318
  (Apple silicon, macOS 14 or later)
1063
1319
 
1064
1320
  mu judge mude install [--local-only] download it (${(MUDE_BYTES / 1e9).toFixed(1)} GB), set it up, start it and make it the judge;
1065
- after a stop (Ctrl-C, a lost connection, a power cut), it goes on from there
1321
+ after a stop (Ctrl-C, a lost connection, a power cut), it picks up where it left off
1066
1322
  mu judge mude enable [--local-only | --with-fallback]
1067
1323
  make it the judge again, or change what happens when it can't answer
1068
1324
  mu judge mude disable back to the judges from before, and stop its server
@@ -1091,7 +1347,7 @@ function jsonOut(write) {
1091
1347
  error: (message, kind) => emit({ type: "error", message, kind })
1092
1348
  };
1093
1349
  }
1094
- function humanOut(write, tty) {
1350
+ function humanOut(write, tty, pkg = MUDE_PACKAGE) {
1095
1351
  let shown = -1;
1096
1352
  return {
1097
1353
  step: (_step, message) => {
@@ -1115,7 +1371,7 @@ function humanOut(write, tty) {
1115
1371
  },
1116
1372
  line: (text) => write(` ${text}
1117
1373
  `),
1118
- status: (status2) => write(describe(status2)),
1374
+ status: (status2) => write(describe(status2, pkg)),
1119
1375
  error: (message) => {
1120
1376
  if (shown >= 0 && tty) write("\n");
1121
1377
  shown = -1;
@@ -1124,31 +1380,43 @@ function humanOut(write, tty) {
1124
1380
  }
1125
1381
  };
1126
1382
  }
1127
- function describe(status2) {
1383
+ function describe(status2, pkg) {
1128
1384
  const lines = [];
1129
1385
  if (!status2.supported) {
1130
- lines.push(`mude 1.0 can't run here: ${status2.problems.join("; ")}`);
1386
+ lines.push(`${pkg.label} can't run here: ${status2.problems.join("; ")}`);
1131
1387
  return `${lines.join("\n")}
1132
1388
  `;
1133
1389
  }
1134
1390
  lines.push(
1135
- `model ${status2.installed ? `installed (${MUDE_REPO} @ ${MUDE_REVISION.slice(0, 7)})` : status2.downloadedBytes > 0 ? `partly downloaded (${(status2.downloadedBytes / 1e9).toFixed(1)} of ${(MUDE_BYTES / 1e9).toFixed(1)} GB): mu judge mude install goes on from there` : "not installed: mu judge mude install"}`
1391
+ `model ${status2.installed ? `installed (${pkg.repo} @ ${pkg.revision.slice(0, 7)})` : status2.downloadedBytes > 0 ? `partly downloaded (${(status2.downloadedBytes / 1e9).toFixed(1)} of ${(pkg.bytes / 1e9).toFixed(1)} GB): ${pkg.install} resumes it` : `not installed: ${pkg.install}`}`
1136
1392
  );
1137
- if (status2.installed && !status2.pythonReady) lines.push("python not set up: mu judge mude install");
1393
+ if (status2.installed && !status2.pythonReady) lines.push(`python not set up: ${pkg.install}`);
1138
1394
  lines.push(
1139
1395
  `server ${status2.ready ? `running on 127.0.0.1:${status2.port}${status2.memoryBytes ? ` \xB7 ${(status2.memoryBytes / 1e9).toFixed(1)} GB of memory` : ""}` : status2.running ? `starting on 127.0.0.1:${status2.port}` : "stopped"}`
1140
1396
  );
1141
1397
  if (status2.enabled) {
1142
1398
  lines.push(
1143
- status2.localOnly ? "judge mude 1.0 only: a decision it cannot answer takes its fallback, and nothing goes to a hosted judge" : `judge mude 1.0, then ${status2.fallback.join(" > ")} for what it cannot answer (what those read leaves this machine)`
1399
+ status2.localOnly ? `judge ${pkg.label} only: a decision it cannot answer takes its fallback, and nothing goes to a hosted judge` : `judge ${pkg.label}, then ${status2.fallback.join(" > ")} for what it cannot answer (what those read leaves this machine)`
1144
1400
  );
1145
1401
  } else {
1146
- lines.push(`judge ${status2.installed ? "not enabled: mu judge mude enable" : "not enabled"}`);
1402
+ lines.push(`judge ${status2.installed ? `not enabled: ${pkg.again("enable")}` : "not enabled"}`);
1147
1403
  }
1148
1404
  lines.push(`folder ${status2.folder}`);
1149
1405
  return `${lines.join("\n")}
1150
1406
  `;
1151
1407
  }
1408
+ var MUDE_PACKAGE = {
1409
+ label: "mude 1.0",
1410
+ repo: MUDE_REPO,
1411
+ revision: MUDE_REVISION,
1412
+ files: MUDE_FILES,
1413
+ bytes: MUDE_BYTES,
1414
+ judge: MUDE_JUDGE,
1415
+ served: MUDE_MODEL,
1416
+ install: "mu judge mude install",
1417
+ again: (what) => `mu judge mude ${what}`
1418
+ };
1419
+ var pkgOf = (ctx) => ctx.pkg ?? MUDE_PACKAGE;
1152
1420
  function problems(options = {}) {
1153
1421
  const platform = options.platform ?? process.platform;
1154
1422
  if (platform !== "darwin") return ["it needs a Mac with Apple silicon (an M1 or newer)"];
@@ -1175,7 +1443,7 @@ function appleSilicon() {
1175
1443
  function freeBytes(path) {
1176
1444
  let dir = path;
1177
1445
  while (!existsSync5(dir)) {
1178
- const parent = join6(dir, "..");
1446
+ const parent = join7(dir, "..");
1179
1447
  if (parent === dir) return void 0;
1180
1448
  dir = parent;
1181
1449
  }
@@ -1191,24 +1459,59 @@ function installLocalOnly(ctx, args) {
1191
1459
  if (args.includes("--local-only")) return true;
1192
1460
  if (args.includes("--with-fallback")) return false;
1193
1461
  try {
1194
- const value = JSON.parse(readFileSync6(ctx.paths.install, "utf8"));
1462
+ const value = JSON.parse(readFileSync7(ctx.paths.install, "utf8"));
1195
1463
  return typeof value === "object" && value !== null && "localOnly" in value && value.localOnly === true;
1196
1464
  } catch {
1197
1465
  return false;
1198
1466
  }
1199
1467
  }
1468
+ function startedAt(pid) {
1469
+ try {
1470
+ return execFileSync2("/bin/ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8" }).trim() || void 0;
1471
+ } catch {
1472
+ return void 0;
1473
+ }
1474
+ }
1475
+ function holdInstall(paths, pkg = MUDE_PACKAGE) {
1476
+ mkdirSync6(paths.root, { recursive: true });
1477
+ const mine = `${JSON.stringify({ pid: process.pid, started: startedAt(process.pid) ?? "" })}
1478
+ `;
1479
+ try {
1480
+ writeFileSync7(paths.installLock, mine, { flag: "wx" });
1481
+ } catch (error) {
1482
+ if (error.code !== "EEXIST") throw error;
1483
+ let holder = {};
1484
+ try {
1485
+ holder = JSON.parse(readFileSync7(paths.installLock, "utf8"));
1486
+ } catch {
1487
+ }
1488
+ const pid = typeof holder.pid === "number" ? holder.pid : 0;
1489
+ if (pid > 0 && pid !== process.pid && holder.started && startedAt(pid) === holder.started) {
1490
+ throw new Error(
1491
+ `Another ${pkg.label} install is running (process ${pid}). Wait for it to finish, or stop it, then run ${pkg.install} again`
1492
+ );
1493
+ }
1494
+ writeFileSync7(paths.installLock, mine);
1495
+ }
1496
+ return () => {
1497
+ try {
1498
+ if (readFileSync7(paths.installLock, "utf8") === mine) rmSync4(paths.installLock, { force: true });
1499
+ } catch {
1500
+ }
1501
+ };
1502
+ }
1200
1503
  function installed(ctx) {
1201
- return missingFiles(ctx.paths.model, MUDE_FILES).length === 0;
1504
+ return missingFiles(ctx.paths.model, pkgOf(ctx).files).length === 0;
1202
1505
  }
1203
1506
  function key(ctx) {
1204
1507
  try {
1205
- const existing = readFileSync6(ctx.paths.key, "utf8").trim();
1508
+ const existing = readFileSync7(ctx.paths.key, "utf8").trim();
1206
1509
  if (existing) return existing;
1207
1510
  } catch {
1208
1511
  }
1209
- mkdirSync5(ctx.paths.root, { recursive: true });
1512
+ mkdirSync6(ctx.paths.root, { recursive: true });
1210
1513
  const made = randomBytes(32).toString("hex");
1211
- writeFileSync6(ctx.paths.key, `${made}
1514
+ writeFileSync7(ctx.paths.key, `${made}
1212
1515
  `, { mode: 384 });
1213
1516
  return made;
1214
1517
  }
@@ -1218,18 +1521,20 @@ async function port(ctx) {
1218
1521
  return choosePort(DEFAULT_PORT);
1219
1522
  }
1220
1523
  async function status(ctx) {
1524
+ const pkg = pkgOf(ctx);
1221
1525
  const found = problems();
1222
1526
  const state = readState(ctx.paths);
1223
1527
  const at = state?.port ?? DEFAULT_PORT;
1224
1528
  const pid = readPid(ctx.paths);
1225
1529
  const running = serverAlive(ctx.paths, pid);
1226
1530
  const ready = running && (await health(at))?.status === "ready";
1227
- const setting = judgeSetting(ctx.files);
1531
+ const setting = judgeSetting(ctx.files, pkg.judge.name);
1228
1532
  return {
1533
+ name: pkg.judge.name,
1229
1534
  supported: found.length === 0,
1230
1535
  problems: found,
1231
1536
  installed: installed(ctx),
1232
- downloadedBytes: downloadedBytes(ctx.paths.model, MUDE_FILES),
1537
+ downloadedBytes: downloadedBytes(ctx.paths.model, pkg.files),
1233
1538
  pythonReady: venvReady(ctx.paths, ctx.lockFile),
1234
1539
  running,
1235
1540
  ready,
@@ -1239,31 +1544,33 @@ async function status(ctx) {
1239
1544
  enabled: setting.enabled,
1240
1545
  localOnly: setting.localOnly,
1241
1546
  fallback: setting.fallback,
1242
- model: `${MUDE_REPO}@${MUDE_REVISION}`,
1547
+ model: `${pkg.repo}@${pkg.revision}`,
1243
1548
  folder: ctx.paths.root,
1244
1549
  log: ctx.paths.log
1245
1550
  };
1246
1551
  }
1247
1552
  async function ensureInstalled(ctx, out) {
1248
- const missing = missingFiles(ctx.paths.model, MUDE_FILES);
1553
+ const pkg = pkgOf(ctx);
1554
+ const missing = missingFiles(ctx.paths.model, pkg.files);
1249
1555
  if (missing.length > 0) {
1250
1556
  const need = missing.reduce((sum, file) => sum + file.size, 0);
1557
+ const already = downloadedBytes(ctx.paths.model, pkg.files);
1251
1558
  const free = freeBytes(ctx.paths.root);
1252
- if (free !== void 0 && free < need + 2e9) {
1559
+ const room = pkg.bytes - already + 2e9;
1560
+ if (free !== void 0 && free < room) {
1253
1561
  throw new Error(
1254
- `Not enough free disk space: mude 1.0 needs about ${((need + 2e9) / 1e9).toFixed(1)} GB, and ${(free / 1e9).toFixed(1)} GB are free`
1562
+ `Not enough free disk space: ${pkg.label} needs about ${gb(room)} GB, and only ${gb(free)} GB are free. Free up some space, then run ${pkg.install} again`
1255
1563
  );
1256
1564
  }
1257
- const already = downloadedBytes(ctx.paths.model, MUDE_FILES);
1258
1565
  out.step(
1259
1566
  "download",
1260
- already > 0 ? `Going on with the mude 1.0 download (${MUDE_REPO}): ${gb(already)} of ${gb(MUDE_BYTES)} GB are here already` : `Downloading mude 1.0 (${MUDE_REPO}, ${gb(need)} GB) and checking every file`
1567
+ already > 0 ? `Resuming the ${pkg.label} download (${pkg.repo}): ${gb(already)} of ${gb(pkg.bytes)} GB already downloaded` : `Downloading ${pkg.label} (${pkg.repo}, ${gb(need)} GB) and checking every file`
1261
1568
  );
1262
1569
  try {
1263
1570
  await downloadFiles({
1264
1571
  dir: ctx.paths.model,
1265
- files: MUDE_FILES,
1266
- url: (path) => mudeFileUrl(path, ctx.env.HF_ENDPOINT || void 0),
1572
+ files: pkg.files,
1573
+ url: (path) => fileUrl(pkg.repo, pkg.revision, path, ctx.env.HF_ENDPOINT || void 0),
1267
1574
  token: huggingFaceToken(ctx.env),
1268
1575
  onProgress: (event) => out.progress(event),
1269
1576
  signal: ctx.signal
@@ -1271,7 +1578,7 @@ async function ensureInstalled(ctx, out) {
1271
1578
  } catch (error) {
1272
1579
  if (error instanceof DownloadError && error.kind === "aborted") {
1273
1580
  throw new DownloadError(
1274
- `Stopped with ${gb(downloadedBytes(ctx.paths.model, MUDE_FILES))} of ${gb(MUDE_BYTES)} GB downloaded: mu judge mude install goes on from there`,
1581
+ `Stopped with ${gb(downloadedBytes(ctx.paths.model, pkg.files))} of ${gb(pkg.bytes)} GB downloaded. Run ${pkg.install} to resume`,
1275
1582
  "aborted"
1276
1583
  );
1277
1584
  }
@@ -1279,36 +1586,102 @@ async function ensureInstalled(ctx, out) {
1279
1586
  }
1280
1587
  }
1281
1588
  if (!venvReady(ctx.paths, ctx.lockFile)) {
1282
- out.step("python", "Setting up the Python environment for mude 1.0 (MLX, transformers; about 1 GB, once)");
1283
- let uv = findUv(ctx.paths, ctx.env);
1284
- if (!uv) uv = await downloadUv(ctx.paths, { onLine: (line) => out.line(line) });
1285
- await ensureVenv(ctx.paths, ctx.lockFile, uv, { onLine: (line) => out.line(line), env: ctx.env });
1589
+ out.step("python", `Setting up the Python environment for ${pkg.label} (MLX, transformers; about 1 GB, once)`);
1590
+ try {
1591
+ let uv = findUv(ctx.paths, ctx.env);
1592
+ if (!uv) uv = await downloadUv(ctx.paths, { onLine: (line) => out.line(line), signal: ctx.signal });
1593
+ await ensureVenv(ctx.paths, ctx.lockFile, uv, { onLine: (line) => out.line(line), env: ctx.env });
1594
+ } catch (error) {
1595
+ if (ctx.signal?.aborted) {
1596
+ throw new DownloadError(
1597
+ `Stopped while setting up the Python environment. Run ${pkg.install} to resume`,
1598
+ "aborted"
1599
+ );
1600
+ }
1601
+ throw error;
1602
+ }
1286
1603
  }
1287
1604
  }
1288
1605
  function requireInstalled(ctx) {
1289
1606
  if (!installed(ctx) || !venvReady(ctx.paths, ctx.lockFile)) {
1290
- throw new Error("mude 1.0 is not installed here yet: mu judge mude install");
1607
+ const pkg = pkgOf(ctx);
1608
+ throw new Error(`${pkg.label} is not installed here yet: ${pkg.install}`);
1291
1609
  }
1292
1610
  }
1293
- async function start(ctx, out, wait = true) {
1611
+ async function start(ctx, out, command, wait2 = true) {
1612
+ const pkg = pkgOf(ctx);
1294
1613
  const at = await port(ctx);
1295
1614
  out.step(
1296
1615
  "start",
1297
- serverAlive(ctx.paths) ? `The mude 1.0 server is running on 127.0.0.1:${at}` : `Starting the mude 1.0 server on 127.0.0.1:${at} (loading the model takes a few seconds)`
1616
+ serverAlive(ctx.paths) ? `The ${pkg.label} server is running on 127.0.0.1:${at}` : `Starting the ${pkg.label} server on 127.0.0.1:${at} (loading the model takes a few seconds)`
1298
1617
  );
1299
- await startServer(ctx.paths, { port: at, key: key(ctx), wait });
1618
+ try {
1619
+ await startServer(ctx.paths, { port: at, key: key(ctx), wait: wait2, signal: ctx.signal, name: pkg.served });
1620
+ } catch (error) {
1621
+ if (error instanceof StartStopped) {
1622
+ throw new DownloadError(
1623
+ error.serverStopped ? `Stopped while the ${pkg.label} server was starting, and stopped the server too. Run ${pkg.again(command)} again to finish` : `Stopped waiting for the ${pkg.label} server, which is still starting. Run ${pkg.again(command)} again to finish`,
1624
+ "aborted"
1625
+ );
1626
+ }
1627
+ throw error;
1628
+ }
1300
1629
  return at;
1301
1630
  }
1302
- function enable(ctx, out, at, localOnly) {
1303
- const tiers = enableMude(ctx.paths, ctx.files, { port: at, key: key(ctx), localOnly });
1631
+ function others(ctx) {
1632
+ const name = pkgOf(ctx).judge.name;
1633
+ const home = ctx.files.home;
1634
+ const found = [];
1635
+ if (name !== MUDE) found.push({ judge: MUDE_JUDGE, paths: mudePaths(home) });
1636
+ for (const record of listRecords(home)) {
1637
+ if (record.id !== name) found.push({ judge: localJudgeOf(record.id), paths: modelPaths(home, record) });
1638
+ }
1639
+ return found;
1640
+ }
1641
+ async function stopOthers(ctx) {
1642
+ for (const other of others(ctx)) await stopServer(other.paths);
1643
+ }
1644
+ async function enable(ctx, out, at, localOnly) {
1645
+ const pkg = pkgOf(ctx);
1646
+ const tiers = currentTiers(ctx.files);
1647
+ for (const other of others(ctx)) {
1648
+ if (tiers.includes(other.judge.name)) disableLocal(other.judge, other.paths, ctx.files);
1649
+ }
1650
+ const order = enableLocal(pkg.judge, ctx.paths, ctx.files, { port: at, key: key(ctx), localOnly });
1304
1651
  out.step(
1305
1652
  "enable",
1306
- localOnly ? "mude 1.0 is the judge, on its own. New mu sessions use it; one that is open now picks it up when it starts again." : `mude 1.0 is the judge, with ${tiers.slice(1).join(" > ")} behind it for what it cannot answer. New mu sessions use it.`
1653
+ localOnly ? `${pkg.label} is the judge, on its own. New mu sessions use it; one that is open now picks it up when it starts again.` : `${pkg.label} is the judge, with ${order.slice(1).join(" > ")} behind it for what it cannot answer. New mu sessions use it.`
1307
1654
  );
1308
1655
  }
1309
- async function main(argv, ctx, write, tty = false) {
1310
- const json = argv.includes("--json");
1311
- const args = argv.filter((arg) => arg !== "--json");
1656
+ async function install(ctx, out, args, holdAlso) {
1657
+ const localOnly = installLocalOnly(ctx, args);
1658
+ const release2 = holdInstall(ctx.paths, pkgOf(ctx));
1659
+ let releaseAlso;
1660
+ try {
1661
+ if (holdAlso && !venvReady(ctx.paths, ctx.lockFile)) releaseAlso = holdInstall(holdAlso);
1662
+ writeFileSync7(ctx.paths.install, `${JSON.stringify({ localOnly })}
1663
+ `);
1664
+ await ensureInstalled(ctx, out);
1665
+ await stopOthers(ctx);
1666
+ const at = await start(ctx, out, "install");
1667
+ await enable(ctx, out, at, localOnly);
1668
+ rmSync4(ctx.paths.install, { force: true });
1669
+ } finally {
1670
+ releaseAlso?.();
1671
+ release2();
1672
+ }
1673
+ }
1674
+ async function enableCommand(ctx, out, args) {
1675
+ requireInstalled(ctx);
1676
+ const current = judgeSetting(ctx.files, pkgOf(ctx).judge.name);
1677
+ const localOnly = args.includes("--local-only") ? true : args.includes("--with-fallback") ? false : current.enabled && current.localOnly;
1678
+ await stopOthers(ctx);
1679
+ const at = await start(ctx, out, "enable");
1680
+ await enable(ctx, out, at, localOnly);
1681
+ }
1682
+ async function main(argv2, ctx, write, tty = false) {
1683
+ const json = argv2.includes("--json");
1684
+ const args = argv2.filter((arg) => arg !== "--json");
1312
1685
  const out = json ? jsonOut(write) : humanOut(write, tty);
1313
1686
  const command = args[0] ?? "status";
1314
1687
  const flag = (name) => args.includes(name);
@@ -1332,25 +1705,12 @@ async function main(argv, ctx, write, tty = false) {
1332
1705
  return 1;
1333
1706
  }
1334
1707
  switch (command) {
1335
- case "install": {
1336
- const localOnly = installLocalOnly(ctx, args);
1337
- mkdirSync5(ctx.paths.root, { recursive: true });
1338
- writeFileSync6(ctx.paths.install, `${JSON.stringify({ localOnly })}
1339
- `);
1340
- await ensureInstalled(ctx, out);
1341
- const at = await start(ctx, out);
1342
- enable(ctx, out, at, localOnly);
1343
- rmSync4(ctx.paths.install, { force: true });
1708
+ case "install":
1709
+ await install(ctx, out, args);
1344
1710
  break;
1345
- }
1346
- case "enable": {
1347
- requireInstalled(ctx);
1348
- const current = judgeSetting(ctx.files);
1349
- const localOnly = flag("--local-only") ? true : flag("--with-fallback") ? false : current.enabled && current.localOnly;
1350
- const at = await start(ctx, out);
1351
- enable(ctx, out, at, localOnly);
1711
+ case "enable":
1712
+ await enableCommand(ctx, out, args);
1352
1713
  break;
1353
- }
1354
1714
  case "disable": {
1355
1715
  if (!mudeInTiers(ctx.files)) {
1356
1716
  const stopped2 = await stopServer(ctx.paths);
@@ -1367,7 +1727,7 @@ async function main(argv, ctx, write, tty = false) {
1367
1727
  }
1368
1728
  case "start": {
1369
1729
  requireInstalled(ctx);
1370
- await start(ctx, out, !flag("--no-wait"));
1730
+ await start(ctx, out, "start", !flag("--no-wait"));
1371
1731
  break;
1372
1732
  }
1373
1733
  case "stop": {
@@ -1381,8 +1741,20 @@ async function main(argv, ctx, write, tty = false) {
1381
1741
  out.step("disable", `mude 1.0 is no longer the judge: ${tiers.join(" > ")} again.`);
1382
1742
  }
1383
1743
  await stopServer(ctx.paths);
1384
- rmSync4(ctx.paths.root, { recursive: true, force: true });
1385
- out.step("uninstall", `Deleted ${ctx.paths.root}: the model, its Python environment and its key.`);
1744
+ if (listRecords(ctx.files.home).length > 0) {
1745
+ for (const name of existsSync5(ctx.paths.root) ? readdirSync2(ctx.paths.root) : []) {
1746
+ const path = join7(ctx.paths.root, name);
1747
+ if (path !== ctx.paths.venv && path !== ctx.paths.tools)
1748
+ rmSync4(path, { recursive: true, force: true });
1749
+ }
1750
+ out.step(
1751
+ "uninstall",
1752
+ `Deleted mude 1.0 and its key from ${ctx.paths.root}. Its Python environment stays there for the other decision models.`
1753
+ );
1754
+ } else {
1755
+ rmSync4(ctx.paths.root, { recursive: true, force: true });
1756
+ out.step("uninstall", `Deleted ${ctx.paths.root}: the model, its Python environment and its key.`);
1757
+ }
1386
1758
  break;
1387
1759
  }
1388
1760
  default:
@@ -1400,16 +1772,245 @@ ${USAGE}`, "usage");
1400
1772
  }
1401
1773
  }
1402
1774
  function contextFromEnv(env = process.env, home = homedir3()) {
1403
- const muHome = env.MU_MUDE_HOME || join6(home, ".mu");
1404
- const agentDir = env.MU_AGENT_DIR || join6(muHome, "agent");
1775
+ const muHome = env.MU_MUDE_HOME || join7(home, ".mu");
1776
+ const agentDir = env.MU_AGENT_DIR || join7(muHome, "agent");
1405
1777
  return {
1406
1778
  paths: mudePaths(muHome),
1407
- files: { agentDir, home: muHome, envFile: env.MU_MUDE_ENV_FILE || join6(muHome, ".env") },
1408
- lockFile: env.MU_MUDE_LOCK || join6(import.meta.dirname, "..", "..", "..", "..", "kyrn", "local-judge", "mude-requirements.lock.txt"),
1779
+ files: { agentDir, home: muHome, envFile: env.MU_MUDE_ENV_FILE || join7(muHome, ".env") },
1780
+ lockFile: env.MU_MUDE_LOCK || join7(import.meta.dirname, "..", "..", "..", "..", "kyrn", "local-judge", "mude-requirements.lock.txt"),
1409
1781
  env
1410
1782
  };
1411
1783
  }
1412
1784
 
1785
+ // packages/kyrn-judge/src/mude/models-command.ts
1786
+ import { rmSync as rmSync5 } from "node:fs";
1787
+ var USAGE2 = `mu judge model: decision models from Hugging Face, run on this Mac like mude 1.0
1788
+
1789
+ mu judge model add <link or owner/name>[@revision] [--local-only]
1790
+ download it, set it up, start it and make it the judge; run it again to resume
1791
+ mu judge model list mude 1.0 and the models added here
1792
+ mu judge model use <name> [--local-only | --with-fallback]
1793
+ make it the judge (mude for mude 1.0)
1794
+ mu judge model start | stop | status | disable <name>
1795
+ mu judge model remove <name>
1796
+ stop it, take it out of the judge order and delete it
1797
+
1798
+ mu runs a repository packaged like mude 1.0's MLX edition, with its own ${SERVER_FILE}. Add --json for one JSON object per line.
1799
+ `;
1800
+ function packageOf(record) {
1801
+ const command = (what) => what === "install" ? "add" : what === "enable" ? "use" : what;
1802
+ return {
1803
+ label: record.id,
1804
+ repo: record.repo,
1805
+ revision: record.revision,
1806
+ files: record.files,
1807
+ bytes: record.bytes,
1808
+ judge: localJudgeOf(record.id),
1809
+ served: record.id,
1810
+ install: `mu judge model add ${record.id}`,
1811
+ again: (what) => `mu judge model ${command(what)} ${record.id}`
1812
+ };
1813
+ }
1814
+ function modelContext(ctx, record) {
1815
+ return { ...ctx, paths: modelPaths(ctx.files.home, record), pkg: packageOf(record) };
1816
+ }
1817
+ function summary(pkg, state, builtIn) {
1818
+ return {
1819
+ name: pkg.judge.name,
1820
+ label: pkg.label,
1821
+ repo: pkg.repo,
1822
+ revision: pkg.revision,
1823
+ bytes: pkg.bytes,
1824
+ builtIn,
1825
+ installed: state.installed && state.pythonReady,
1826
+ downloadedBytes: state.downloadedBytes,
1827
+ running: state.running,
1828
+ ready: state.ready,
1829
+ enabled: state.enabled,
1830
+ localOnly: state.localOnly
1831
+ };
1832
+ }
1833
+ async function listModels(ctx) {
1834
+ const mude = { ...ctx, paths: mudePaths(ctx.files.home), pkg: void 0 };
1835
+ const models = [summary(MUDE_PACKAGE, await status(mude), true)];
1836
+ for (const record of listRecords(ctx.files.home)) {
1837
+ const each = modelContext(ctx, record);
1838
+ models.push(summary(packageOf(record), await status(each), false));
1839
+ }
1840
+ return models;
1841
+ }
1842
+ var gb2 = (bytes) => (bytes / 1e9).toFixed(1);
1843
+ function describeList(models) {
1844
+ const width = Math.max(...models.map((model) => model.name.length));
1845
+ const lines = models.map((model) => {
1846
+ const state = model.installed ? model.ready ? "running" : model.running ? "starting" : "installed" : model.downloadedBytes > 0 ? `${gb2(model.downloadedBytes)} of ${gb2(model.bytes)} GB downloaded` : model.builtIn ? "not installed" : "not downloaded";
1847
+ const judge = model.enabled ? model.localOnly ? " \xB7 the judge, on its own" : " \xB7 the judge" : "";
1848
+ return `${model.name.padEnd(width)} ${model.repo} @ ${model.revision.slice(0, 7)} \xB7 ${gb2(model.bytes)} GB \xB7 ${state}${judge}`;
1849
+ });
1850
+ return `${lines.join("\n")}
1851
+ `;
1852
+ }
1853
+ function isMude(name) {
1854
+ if (name.toLowerCase() === MUDE) return true;
1855
+ try {
1856
+ return parseModelRef(name).repo.toLowerCase() === MUDE_REPO.toLowerCase();
1857
+ } catch {
1858
+ return false;
1859
+ }
1860
+ }
1861
+ async function addRecord(ctx, name, out, options) {
1862
+ const home = ctx.files.home;
1863
+ const known = findRecord(home, name);
1864
+ if (known) return known;
1865
+ const ref = parseModelRef(name);
1866
+ const existing = findRecord(home, ref.repo);
1867
+ if (existing && ref.revision === void 0) return existing;
1868
+ out.step("resolve", `Looking up ${ref.repo}${ref.revision ? ` at ${ref.revision}` : ""} on Hugging Face`);
1869
+ const resolved = await resolveModel(ref, {
1870
+ endpoint: ctx.env.HF_ENDPOINT || void 0,
1871
+ token: huggingFaceToken(ctx.env),
1872
+ fetch: options.fetch
1873
+ });
1874
+ const id = existing?.id ?? modelId(ref.repo, new Set(listRecords(home).map((record2) => record2.id)));
1875
+ if (existing && existing.revision !== resolved.revision) {
1876
+ rmSync5(modelPaths(home, existing).model, { recursive: true, force: true });
1877
+ }
1878
+ const record = {
1879
+ id,
1880
+ repo: ref.repo,
1881
+ revision: resolved.revision,
1882
+ files: resolved.files,
1883
+ bytes: resolved.files.reduce((sum, file) => sum + file.size, 0)
1884
+ };
1885
+ writeRecord(home, record);
1886
+ return record;
1887
+ }
1888
+ async function modelMain(argv2, ctx, write, tty = false, options = {}) {
1889
+ const json = argv2.includes("--json");
1890
+ const args = argv2.filter((arg) => arg !== "--json");
1891
+ let out = json ? jsonOut(write) : humanOut(write, tty);
1892
+ const command = args[0] ?? "list";
1893
+ const names = args.slice(1).filter((arg) => !arg.startsWith("--"));
1894
+ const flags = args.slice(1).filter((arg) => arg.startsWith("--"));
1895
+ try {
1896
+ if (command === "help" || command === "--help" || command === "-h") {
1897
+ write(USAGE2);
1898
+ return 0;
1899
+ }
1900
+ const unknown = flags.find((flag) => !["--local-only", "--with-fallback", "--no-wait"].includes(flag));
1901
+ if (unknown) {
1902
+ out.error(`Unknown option for mu judge model ${command}: ${unknown} (mu judge model help)`, "usage");
1903
+ return 2;
1904
+ }
1905
+ if (command === "list") {
1906
+ const models = await listModels(ctx);
1907
+ if (json) write(`${JSON.stringify({ type: "models", models })}
1908
+ `);
1909
+ else write(describeList(models));
1910
+ return 0;
1911
+ }
1912
+ if (!["add", "use", "start", "stop", "status", "disable", "remove"].includes(command)) {
1913
+ out.error(`Unknown command: mu judge model ${command}
1914
+
1915
+ ${USAGE2}`, "usage");
1916
+ return 2;
1917
+ }
1918
+ const name = names[0];
1919
+ if (!name || names.length > 1) {
1920
+ out.error(
1921
+ `Name one model: mu judge model ${command} <${command === "add" ? "link or owner/name" : "name"}>`,
1922
+ "usage"
1923
+ );
1924
+ return 2;
1925
+ }
1926
+ if (isMude(name)) {
1927
+ const as = { add: "install", use: "enable", remove: "uninstall" }[command] ?? command;
1928
+ return main([as, ...flags, ...json ? ["--json"] : []], ctx, write, tty);
1929
+ }
1930
+ const home = ctx.files.home;
1931
+ const unsupported = problems();
1932
+ if (command === "add") {
1933
+ if (unsupported.length > 0) {
1934
+ out.error(`Decision models can't run on this machine: ${unsupported.join("; ")}.`, "unsupported");
1935
+ return 1;
1936
+ }
1937
+ const record2 = await addRecord(ctx, name, out, options);
1938
+ const each2 = modelContext(ctx, record2);
1939
+ if (json)
1940
+ write(
1941
+ `${JSON.stringify({ type: "model", name: record2.id, repo: record2.repo, revision: record2.revision, bytes: record2.bytes })}
1942
+ `
1943
+ );
1944
+ else out = humanOut(write, tty, each2.pkg);
1945
+ out.step(
1946
+ "model",
1947
+ `${record2.repo} @ ${record2.revision.slice(0, 7)} (${gb2(record2.bytes)} GB) is the decision model ${record2.id}. It runs its own server code (${SERVER_FILE}) on this Mac: add only models you trust.`
1948
+ );
1949
+ await install(each2, out, flags, mudePaths(home));
1950
+ out.status(await status(each2));
1951
+ return 0;
1952
+ }
1953
+ const record = findRecord(home, name);
1954
+ if (!record) {
1955
+ out.error(
1956
+ `No decision model ${name} here (mu judge model list). Add it: mu judge model add <link or owner/name>`,
1957
+ "usage"
1958
+ );
1959
+ return 1;
1960
+ }
1961
+ const each = modelContext(ctx, record);
1962
+ const pkg = packageOf(record);
1963
+ if (!json) out = humanOut(write, tty, pkg);
1964
+ if (unsupported.length > 0 && (command === "use" || command === "start")) {
1965
+ out.error(`${record.id} can't run on this machine: ${unsupported.join("; ")}.`, "unsupported");
1966
+ return 1;
1967
+ }
1968
+ switch (command) {
1969
+ case "use":
1970
+ await enableCommand(each, out, flags);
1971
+ break;
1972
+ case "start":
1973
+ requireInstalled(each);
1974
+ await start(each, out, "start", !flags.includes("--no-wait"));
1975
+ break;
1976
+ case "stop": {
1977
+ const stopped = await stopServer(each.paths);
1978
+ out.step(
1979
+ "stop",
1980
+ stopped ? `The ${record.id} server is stopped.` : `The ${record.id} server was not running.`
1981
+ );
1982
+ break;
1983
+ }
1984
+ case "disable": {
1985
+ const was = mudeInTiers(ctx.files, record.id);
1986
+ const tiers = was ? disableLocal(pkg.judge, each.paths, ctx.files) : [];
1987
+ const stopped = await stopServer(each.paths);
1988
+ out.step(
1989
+ "disable",
1990
+ was ? `${record.id} is no longer the judge: ${tiers.join(" > ")} again${stopped ? ", and its server is stopped" : ""}.` : `${record.id} was not the judge${stopped ? "; its server is stopped" : ""}.`
1991
+ );
1992
+ break;
1993
+ }
1994
+ case "remove": {
1995
+ if (mudeInTiers(ctx.files, record.id) || readState(each.paths)?.wroteEntry) {
1996
+ const tiers = disableLocal(pkg.judge, each.paths, ctx.files, { removeEntry: true });
1997
+ out.step("disable", `${record.id} is no longer the judge: ${tiers.join(" > ")} again.`);
1998
+ }
1999
+ await stopServer(each.paths);
2000
+ rmSync5(each.paths.root, { recursive: true, force: true });
2001
+ out.step("remove", `Deleted ${each.paths.root}: the model and its key.`);
2002
+ return 0;
2003
+ }
2004
+ }
2005
+ out.status(await status(each));
2006
+ return 0;
2007
+ } catch (error) {
2008
+ const kind = error instanceof DownloadError || error instanceof ModelError ? error.kind : void 0;
2009
+ out.error(error instanceof Error ? error.message : String(error), kind);
2010
+ return 1;
2011
+ }
2012
+ }
2013
+
1413
2014
  // packages/kyrn-judge/src/mude/cli.ts
1414
2015
  var stop = new AbortController();
1415
2016
  var onSignal = (signal) => {
@@ -1419,8 +2020,10 @@ var onSignal = (signal) => {
1419
2020
  };
1420
2021
  process.on("SIGINT", onSignal);
1421
2022
  process.on("SIGTERM", onSignal);
1422
- main(
1423
- process.argv.slice(2),
2023
+ var argv = process.argv.slice(2);
2024
+ var run2 = argv[0] === "model" ? modelMain : main;
2025
+ run2(
2026
+ argv[0] === "model" ? argv.slice(1) : argv,
1424
2027
  { ...contextFromEnv(), signal: stop.signal },
1425
2028
  (text) => process.stdout.write(text),
1426
2029
  Boolean(process.stdout.isTTY)
@@ -1429,8 +2032,10 @@ main(
1429
2032
  process.exitCode = code;
1430
2033
  },
1431
2034
  (error) => {
1432
- process.stderr.write(`mu judge mude: ${error instanceof Error ? error.message : String(error)}
1433
- `);
2035
+ process.stderr.write(
2036
+ `mu judge ${argv[0] === "model" ? "model" : "mude"}: ${error instanceof Error ? error.message : String(error)}
2037
+ `
2038
+ );
1434
2039
  process.exitCode = 1;
1435
2040
  }
1436
2041
  );