mu-agent 0.2.2 → 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";
@@ -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
@@ -445,7 +445,7 @@ async function startServer(paths, options) {
445
445
  "--port",
446
446
  String(options.port),
447
447
  "--name",
448
- MUDE_MODEL,
448
+ options.name ?? MUDE_MODEL,
449
449
  "--verify",
450
450
  "fast"
451
451
  ],
@@ -526,6 +526,11 @@ function serverMemory(pid) {
526
526
  var ROUTED_AWAY = ["tool.description", "tool.injection"];
527
527
  var MUDE = "mude";
528
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
+ };
529
534
  function readState(paths) {
530
535
  try {
531
536
  const value = JSON.parse(readFileSync3(paths.state, "utf8"));
@@ -538,18 +543,22 @@ function writeState(paths, state) {
538
543
  writeFileSync3(paths.state, `${JSON.stringify(state, null, 2)}
539
544
  `, { mode: 384 });
540
545
  }
541
- function judgeSetting(files) {
546
+ function judgeSetting(files, name = MUDE) {
542
547
  const file = readJsonFile(judgeConfigPath(files.agentDir));
543
548
  if (file.state === "unreadable") return { enabled: false, localOnly: false, fallback: [], unreadable: true };
544
549
  const config = file.state === "ok" ? file.value : {};
545
550
  const tiers = tiersOf(config);
546
- const enabled = tiers[0] === MUDE && !judgeOff(config);
547
- const fallback = tiers.filter((tier) => tier !== MUDE);
551
+ const enabled = tiers[0] === name && !judgeOff(config);
552
+ const fallback = tiers.filter((tier) => tier !== name);
548
553
  return { enabled, localOnly: enabled && fallback.length === 0, fallback };
549
554
  }
550
- function mudeInTiers(files) {
555
+ function mudeInTiers(files, name = MUDE) {
556
+ const file = readJsonFile(judgeConfigPath(files.agentDir));
557
+ return file.state === "ok" && tiersOf(file.value).includes(name);
558
+ }
559
+ function currentTiers(files) {
551
560
  const file = readJsonFile(judgeConfigPath(files.agentDir));
552
- return file.state === "ok" && tiersOf(file.value).includes(MUDE);
561
+ return file.state === "ok" ? tiersOf(file.value) : [];
553
562
  }
554
563
  function readConfig(files) {
555
564
  const path = judgeConfigPath(files.agentDir);
@@ -583,18 +592,20 @@ function withoutOwnRoutes(config, state) {
583
592
  function entryFor(port2) {
584
593
  return { ...BUILT_IN_JUDGES[MUDE], baseUrl: mudeBaseUrl(port2) };
585
594
  }
586
- function enableMude(paths, files, options) {
595
+ function enableLocal(judge, paths, files, options) {
596
+ const name = judge.name;
587
597
  const { path, config } = readConfig(files);
588
598
  const state = readState(paths);
589
- const current = tiersOf(config).filter((tier) => tier !== MUDE);
599
+ const current = tiersOf(config).filter((tier) => tier !== name);
590
600
  const before = current.length > 0 ? current : [...state?.previousTiers ?? ["jev"]];
591
- const tiers = options.localOnly ? [MUDE] : [MUDE, ...before.filter((tier) => tier !== MUDE)];
601
+ const tiers = options.localOnly ? [name] : [name, ...before.filter((tier) => tier !== name)];
592
602
  const judgeWasOff = state?.judgeWasOff ?? judgeOff(config);
593
603
  let next = withJudge(withTiers(withoutOwnRoutes(config, state), tiers), true);
594
604
  const judges = judgesOf(next);
595
- const entry = options.port !== DEFAULT_PORT;
596
- if (entry) judges[MUDE] = entryFor(options.port);
597
- 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;
598
609
  next = withSection(next, "judges", judges);
599
610
  const fallback = tiers.slice(1);
600
611
  const routes = recordOf(next.routes);
@@ -610,9 +621,9 @@ function enableMude(paths, files, options) {
610
621
  const backups = new Backups(files.home, files.now ?? /* @__PURE__ */ new Date());
611
622
  backups.save(path);
612
623
  writeJsonFile(path, next);
613
- if (envFileValue(readText(files.envFile), KEY_VARIABLE) !== options.key) {
624
+ if (envFileValue(readText(files.envFile), judge.keyVariable) !== options.key) {
614
625
  backups.save(files.envFile);
615
- writeEnvValue(files.envFile, KEY_VARIABLE, options.key);
626
+ writeEnvValue(files.envFile, judge.keyVariable, options.key);
616
627
  }
617
628
  writeState(paths, {
618
629
  port: options.port,
@@ -624,15 +635,18 @@ function enableMude(paths, files, options) {
624
635
  return tiers;
625
636
  }
626
637
  function disableMude(paths, files, options = {}) {
638
+ return disableLocal(MUDE_JUDGE, paths, files, options);
639
+ }
640
+ function disableLocal(judge, paths, files, options = {}) {
627
641
  const { path, config } = readConfig(files);
628
642
  const state = readState(paths);
629
- const rest = tiersOf(config).filter((tier) => tier !== MUDE);
643
+ const rest = tiersOf(config).filter((tier) => tier !== judge.name);
630
644
  const tiers = rest.length > 0 ? rest : [...state?.previousTiers ?? ["jev"]];
631
645
  let next = withTiers(withoutOwnRoutes(config, state), tiers);
632
646
  if (state?.judgeWasOff) next = withJudge(next, false);
633
647
  if (options.removeEntry && state?.wroteEntry) {
634
648
  const judges = judgesOf(next);
635
- delete judges[MUDE];
649
+ delete judges[judge.name];
636
650
  next = withSection(next, "judges", judges);
637
651
  }
638
652
  new Backups(files.home, files.now ?? /* @__PURE__ */ new Date()).save(path);
@@ -698,6 +712,14 @@ async function sha256File(path) {
698
712
  await pipeline(createReadStream(path), hash);
699
713
  return hash.digest("hex");
700
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
+ }
701
723
  function readVerified(dir) {
702
724
  try {
703
725
  const value = JSON.parse(readFileSync4(join3(dir, ".mu-verified.json"), "utf8"));
@@ -708,7 +730,7 @@ function readVerified(dir) {
708
730
  }
709
731
  function knownGood(dir, file, verified) {
710
732
  const seen = verified[file.path];
711
- if (!seen || seen.sha256 !== file.sha256) return false;
733
+ if (!seen || seen.sha256 !== pinnedHash(file)) return false;
712
734
  try {
713
735
  const stats = statSync3(join3(dir, file.path));
714
736
  return stats.size === file.size && stats.mtimeMs === seen.mtimeMs;
@@ -718,7 +740,7 @@ function knownGood(dir, file, verified) {
718
740
  }
719
741
  function remember(dir, verified, file) {
720
742
  const stats = statSync3(join3(dir, file.path));
721
- 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) };
722
744
  writeFileSync4(join3(dir, ".mu-verified.json"), `${JSON.stringify(verified, null, 1)}
723
745
  `);
724
746
  }
@@ -769,7 +791,7 @@ async function downloadFiles(options) {
769
791
  mkdirSync3(dirname2(target), { recursive: true });
770
792
  if (existsSync3(target) && statSync3(target).size === file.size) {
771
793
  options.onProgress?.({ file: file.path, checking: true, done: settled, total });
772
- if (await sha256File(target) === file.sha256) {
794
+ if (await hashOf(target, file) === pinnedHash(file)) {
773
795
  remember(dir, verified, file);
774
796
  settled += file.size;
775
797
  continue;
@@ -780,17 +802,17 @@ async function downloadFiles(options) {
780
802
  const onBytes = (bytes) => options.onProgress?.({ file: file.path, checking: false, done: settled + bytes, total });
781
803
  let resumed = await fetchFile(options, file, part, onBytes);
782
804
  options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
783
- let actual = await sha256File(part);
784
- if (actual !== file.sha256 && resumed) {
805
+ let actual = await hashOf(part, file);
806
+ if (actual !== pinnedHash(file) && resumed) {
785
807
  dropPart(part);
786
808
  resumed = await fetchFile(options, file, part, onBytes);
787
809
  options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
788
- actual = await sha256File(part);
810
+ actual = await hashOf(part, file);
789
811
  }
790
- if (actual !== file.sha256) {
812
+ if (actual !== pinnedHash(file)) {
791
813
  dropPart(part);
792
814
  throw new DownloadError(
793
- `${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`,
794
816
  "integrity"
795
817
  );
796
818
  }
@@ -934,6 +956,10 @@ async function fetchOnce(options, file, part, onBytes) {
934
956
  }
935
957
  }
936
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
+
937
963
  // packages/kyrn-judge/src/mude/paths.ts
938
964
  import { join as join4 } from "node:path";
939
965
  function mudePaths(muHome) {
@@ -957,12 +983,204 @@ function mudePaths(muHome) {
957
983
  };
958
984
  }
959
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
+ })
1175
+ };
1176
+ }
1177
+
960
1178
  // packages/kyrn-judge/src/mude/python.ts
961
1179
  import { spawn as spawn2 } from "node:child_process";
962
1180
  import { createHash as createHash2 } from "node:crypto";
963
- 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";
964
1182
  import { homedir as homedir2 } from "node:os";
965
- import { delimiter, join as join5 } from "node:path";
1183
+ import { delimiter, join as join6 } from "node:path";
966
1184
  var PYTHON_VERSION = "3.13";
967
1185
  var UV_RELEASE = {
968
1186
  version: "0.13.1",
@@ -980,14 +1198,14 @@ function executable(path) {
980
1198
  function findUv(paths, env = process.env, home = homedir2()) {
981
1199
  const dirs = [
982
1200
  ...(env.PATH ?? "").split(delimiter),
983
- join5(home, ".local", "bin"),
984
- join5(home, ".cargo", "bin"),
1201
+ join6(home, ".local", "bin"),
1202
+ join6(home, ".cargo", "bin"),
985
1203
  "/opt/homebrew/bin",
986
1204
  "/usr/local/bin",
987
1205
  paths.tools
988
1206
  ];
989
1207
  for (const dir of dirs) {
990
- if (dir && executable(join5(dir, "uv"))) return join5(dir, "uv");
1208
+ if (dir && executable(join6(dir, "uv"))) return join6(dir, "uv");
991
1209
  }
992
1210
  return void 0;
993
1211
  }
@@ -1017,7 +1235,7 @@ function run(command, args, options = {}) {
1017
1235
  });
1018
1236
  }
1019
1237
  async function downloadUv(paths, options = {}) {
1020
- mkdirSync4(paths.tools, { recursive: true });
1238
+ mkdirSync5(paths.tools, { recursive: true });
1021
1239
  const archive = "uv.tar.gz";
1022
1240
  options.onLine?.(`Downloading uv ${UV_RELEASE.version}`);
1023
1241
  const timeoutMs = options.timeoutMs ?? 3e5;
@@ -1037,27 +1255,27 @@ async function downloadUv(paths, options = {}) {
1037
1255
  const actual = createHash2("sha256").update(bytes).digest("hex");
1038
1256
  if (actual !== UV_RELEASE.sha256)
1039
1257
  throw new Error("The downloaded uv does not match its pinned sha256; nothing was installed");
1040
- writeFileSync5(join5(paths.tools, archive), bytes);
1258
+ writeFileSync6(join6(paths.tools, archive), bytes);
1041
1259
  const unpacked = await run("/usr/bin/tar", [
1042
1260
  "-xzf",
1043
- join5(paths.tools, archive),
1261
+ join6(paths.tools, archive),
1044
1262
  "-C",
1045
1263
  paths.tools,
1046
1264
  "--strip-components",
1047
1265
  "1"
1048
1266
  ]);
1049
- rmSync3(join5(paths.tools, archive), { force: true });
1050
- if (unpacked.code !== 0 || !executable(join5(paths.tools, "uv")))
1267
+ rmSync3(join6(paths.tools, archive), { force: true });
1268
+ if (unpacked.code !== 0 || !executable(join6(paths.tools, "uv")))
1051
1269
  throw new Error(`Couldn't unpack uv: ${unpacked.output}`);
1052
- return join5(paths.tools, "uv");
1270
+ return join6(paths.tools, "uv");
1053
1271
  }
1054
1272
  function lockDigest(lockFile) {
1055
- return createHash2("sha256").update(readFileSync5(lockFile)).digest("hex");
1273
+ return createHash2("sha256").update(readFileSync6(lockFile)).digest("hex");
1056
1274
  }
1057
1275
  function venvReady(paths, lockFile) {
1058
1276
  if (!existsSync4(paths.python)) return false;
1059
1277
  try {
1060
- return readFileSync5(paths.venvLock, "utf8").trim() === lockDigest(lockFile);
1278
+ return readFileSync6(paths.venvLock, "utf8").trim() === lockDigest(lockFile);
1061
1279
  } catch {
1062
1280
  return false;
1063
1281
  }
@@ -1091,7 +1309,7 @@ ${made.output}`);
1091
1309
  if (installed2.code !== 0)
1092
1310
  throw new Error(`Couldn't install mude's Python packages (uv pip install):
1093
1311
  ${installed2.output}`);
1094
- writeFileSync5(paths.venvLock, `${lockDigest(lockFile)}
1312
+ writeFileSync6(paths.venvLock, `${lockDigest(lockFile)}
1095
1313
  `);
1096
1314
  }
1097
1315
 
@@ -1129,7 +1347,7 @@ function jsonOut(write) {
1129
1347
  error: (message, kind) => emit({ type: "error", message, kind })
1130
1348
  };
1131
1349
  }
1132
- function humanOut(write, tty) {
1350
+ function humanOut(write, tty, pkg = MUDE_PACKAGE) {
1133
1351
  let shown = -1;
1134
1352
  return {
1135
1353
  step: (_step, message) => {
@@ -1153,7 +1371,7 @@ function humanOut(write, tty) {
1153
1371
  },
1154
1372
  line: (text) => write(` ${text}
1155
1373
  `),
1156
- status: (status2) => write(describe(status2)),
1374
+ status: (status2) => write(describe(status2, pkg)),
1157
1375
  error: (message) => {
1158
1376
  if (shown >= 0 && tty) write("\n");
1159
1377
  shown = -1;
@@ -1162,31 +1380,43 @@ function humanOut(write, tty) {
1162
1380
  }
1163
1381
  };
1164
1382
  }
1165
- function describe(status2) {
1383
+ function describe(status2, pkg) {
1166
1384
  const lines = [];
1167
1385
  if (!status2.supported) {
1168
- lines.push(`mude 1.0 can't run here: ${status2.problems.join("; ")}`);
1386
+ lines.push(`${pkg.label} can't run here: ${status2.problems.join("; ")}`);
1169
1387
  return `${lines.join("\n")}
1170
1388
  `;
1171
1389
  }
1172
1390
  lines.push(
1173
- `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 resumes it` : "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}`}`
1174
1392
  );
1175
- 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}`);
1176
1394
  lines.push(
1177
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"}`
1178
1396
  );
1179
1397
  if (status2.enabled) {
1180
1398
  lines.push(
1181
- 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)`
1182
1400
  );
1183
1401
  } else {
1184
- 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"}`);
1185
1403
  }
1186
1404
  lines.push(`folder ${status2.folder}`);
1187
1405
  return `${lines.join("\n")}
1188
1406
  `;
1189
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;
1190
1420
  function problems(options = {}) {
1191
1421
  const platform = options.platform ?? process.platform;
1192
1422
  if (platform !== "darwin") return ["it needs a Mac with Apple silicon (an M1 or newer)"];
@@ -1213,7 +1443,7 @@ function appleSilicon() {
1213
1443
  function freeBytes(path) {
1214
1444
  let dir = path;
1215
1445
  while (!existsSync5(dir)) {
1216
- const parent = join6(dir, "..");
1446
+ const parent = join7(dir, "..");
1217
1447
  if (parent === dir) return void 0;
1218
1448
  dir = parent;
1219
1449
  }
@@ -1229,7 +1459,7 @@ function installLocalOnly(ctx, args) {
1229
1459
  if (args.includes("--local-only")) return true;
1230
1460
  if (args.includes("--with-fallback")) return false;
1231
1461
  try {
1232
- const value = JSON.parse(readFileSync6(ctx.paths.install, "utf8"));
1462
+ const value = JSON.parse(readFileSync7(ctx.paths.install, "utf8"));
1233
1463
  return typeof value === "object" && value !== null && "localOnly" in value && value.localOnly === true;
1234
1464
  } catch {
1235
1465
  return false;
@@ -1242,46 +1472,46 @@ function startedAt(pid) {
1242
1472
  return void 0;
1243
1473
  }
1244
1474
  }
1245
- function holdInstall(paths) {
1246
- mkdirSync5(paths.root, { recursive: true });
1475
+ function holdInstall(paths, pkg = MUDE_PACKAGE) {
1476
+ mkdirSync6(paths.root, { recursive: true });
1247
1477
  const mine = `${JSON.stringify({ pid: process.pid, started: startedAt(process.pid) ?? "" })}
1248
1478
  `;
1249
1479
  try {
1250
- writeFileSync6(paths.installLock, mine, { flag: "wx" });
1480
+ writeFileSync7(paths.installLock, mine, { flag: "wx" });
1251
1481
  } catch (error) {
1252
1482
  if (error.code !== "EEXIST") throw error;
1253
1483
  let holder = {};
1254
1484
  try {
1255
- holder = JSON.parse(readFileSync6(paths.installLock, "utf8"));
1485
+ holder = JSON.parse(readFileSync7(paths.installLock, "utf8"));
1256
1486
  } catch {
1257
1487
  }
1258
1488
  const pid = typeof holder.pid === "number" ? holder.pid : 0;
1259
1489
  if (pid > 0 && pid !== process.pid && holder.started && startedAt(pid) === holder.started) {
1260
1490
  throw new Error(
1261
- `Another mude 1.0 install is running (process ${pid}). Wait for it to finish, or stop it, then run mu judge mude install again`
1491
+ `Another ${pkg.label} install is running (process ${pid}). Wait for it to finish, or stop it, then run ${pkg.install} again`
1262
1492
  );
1263
1493
  }
1264
- writeFileSync6(paths.installLock, mine);
1494
+ writeFileSync7(paths.installLock, mine);
1265
1495
  }
1266
1496
  return () => {
1267
1497
  try {
1268
- if (readFileSync6(paths.installLock, "utf8") === mine) rmSync4(paths.installLock, { force: true });
1498
+ if (readFileSync7(paths.installLock, "utf8") === mine) rmSync4(paths.installLock, { force: true });
1269
1499
  } catch {
1270
1500
  }
1271
1501
  };
1272
1502
  }
1273
1503
  function installed(ctx) {
1274
- return missingFiles(ctx.paths.model, MUDE_FILES).length === 0;
1504
+ return missingFiles(ctx.paths.model, pkgOf(ctx).files).length === 0;
1275
1505
  }
1276
1506
  function key(ctx) {
1277
1507
  try {
1278
- const existing = readFileSync6(ctx.paths.key, "utf8").trim();
1508
+ const existing = readFileSync7(ctx.paths.key, "utf8").trim();
1279
1509
  if (existing) return existing;
1280
1510
  } catch {
1281
1511
  }
1282
- mkdirSync5(ctx.paths.root, { recursive: true });
1512
+ mkdirSync6(ctx.paths.root, { recursive: true });
1283
1513
  const made = randomBytes(32).toString("hex");
1284
- writeFileSync6(ctx.paths.key, `${made}
1514
+ writeFileSync7(ctx.paths.key, `${made}
1285
1515
  `, { mode: 384 });
1286
1516
  return made;
1287
1517
  }
@@ -1291,18 +1521,20 @@ async function port(ctx) {
1291
1521
  return choosePort(DEFAULT_PORT);
1292
1522
  }
1293
1523
  async function status(ctx) {
1524
+ const pkg = pkgOf(ctx);
1294
1525
  const found = problems();
1295
1526
  const state = readState(ctx.paths);
1296
1527
  const at = state?.port ?? DEFAULT_PORT;
1297
1528
  const pid = readPid(ctx.paths);
1298
1529
  const running = serverAlive(ctx.paths, pid);
1299
1530
  const ready = running && (await health(at))?.status === "ready";
1300
- const setting = judgeSetting(ctx.files);
1531
+ const setting = judgeSetting(ctx.files, pkg.judge.name);
1301
1532
  return {
1533
+ name: pkg.judge.name,
1302
1534
  supported: found.length === 0,
1303
1535
  problems: found,
1304
1536
  installed: installed(ctx),
1305
- downloadedBytes: downloadedBytes(ctx.paths.model, MUDE_FILES),
1537
+ downloadedBytes: downloadedBytes(ctx.paths.model, pkg.files),
1306
1538
  pythonReady: venvReady(ctx.paths, ctx.lockFile),
1307
1539
  running,
1308
1540
  ready,
@@ -1312,32 +1544,33 @@ async function status(ctx) {
1312
1544
  enabled: setting.enabled,
1313
1545
  localOnly: setting.localOnly,
1314
1546
  fallback: setting.fallback,
1315
- model: `${MUDE_REPO}@${MUDE_REVISION}`,
1547
+ model: `${pkg.repo}@${pkg.revision}`,
1316
1548
  folder: ctx.paths.root,
1317
1549
  log: ctx.paths.log
1318
1550
  };
1319
1551
  }
1320
1552
  async function ensureInstalled(ctx, out) {
1321
- const missing = missingFiles(ctx.paths.model, MUDE_FILES);
1553
+ const pkg = pkgOf(ctx);
1554
+ const missing = missingFiles(ctx.paths.model, pkg.files);
1322
1555
  if (missing.length > 0) {
1323
1556
  const need = missing.reduce((sum, file) => sum + file.size, 0);
1324
- const already = downloadedBytes(ctx.paths.model, MUDE_FILES);
1557
+ const already = downloadedBytes(ctx.paths.model, pkg.files);
1325
1558
  const free = freeBytes(ctx.paths.root);
1326
- const room = MUDE_BYTES - already + 2e9;
1559
+ const room = pkg.bytes - already + 2e9;
1327
1560
  if (free !== void 0 && free < room) {
1328
1561
  throw new Error(
1329
- `Not enough free disk space: mude 1.0 needs about ${gb(room)} GB, and only ${gb(free)} GB are free. Free up some space, then run mu judge mude install again`
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`
1330
1563
  );
1331
1564
  }
1332
1565
  out.step(
1333
1566
  "download",
1334
- already > 0 ? `Resuming the mude 1.0 download (${MUDE_REPO}): ${gb(already)} of ${gb(MUDE_BYTES)} GB already downloaded` : `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`
1335
1568
  );
1336
1569
  try {
1337
1570
  await downloadFiles({
1338
1571
  dir: ctx.paths.model,
1339
- files: MUDE_FILES,
1340
- 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),
1341
1574
  token: huggingFaceToken(ctx.env),
1342
1575
  onProgress: (event) => out.progress(event),
1343
1576
  signal: ctx.signal
@@ -1345,7 +1578,7 @@ async function ensureInstalled(ctx, out) {
1345
1578
  } catch (error) {
1346
1579
  if (error instanceof DownloadError && error.kind === "aborted") {
1347
1580
  throw new DownloadError(
1348
- `Stopped with ${gb(downloadedBytes(ctx.paths.model, MUDE_FILES))} of ${gb(MUDE_BYTES)} GB downloaded. Run mu judge mude install to resume`,
1581
+ `Stopped with ${gb(downloadedBytes(ctx.paths.model, pkg.files))} of ${gb(pkg.bytes)} GB downloaded. Run ${pkg.install} to resume`,
1349
1582
  "aborted"
1350
1583
  );
1351
1584
  }
@@ -1353,7 +1586,7 @@ async function ensureInstalled(ctx, out) {
1353
1586
  }
1354
1587
  }
1355
1588
  if (!venvReady(ctx.paths, ctx.lockFile)) {
1356
- out.step("python", "Setting up the Python environment for mude 1.0 (MLX, transformers; about 1 GB, once)");
1589
+ out.step("python", `Setting up the Python environment for ${pkg.label} (MLX, transformers; about 1 GB, once)`);
1357
1590
  try {
1358
1591
  let uv = findUv(ctx.paths, ctx.env);
1359
1592
  if (!uv) uv = await downloadUv(ctx.paths, { onLine: (line) => out.line(line), signal: ctx.signal });
@@ -1361,7 +1594,7 @@ async function ensureInstalled(ctx, out) {
1361
1594
  } catch (error) {
1362
1595
  if (ctx.signal?.aborted) {
1363
1596
  throw new DownloadError(
1364
- "Stopped while setting up the Python environment. Run mu judge mude install to resume",
1597
+ `Stopped while setting up the Python environment. Run ${pkg.install} to resume`,
1365
1598
  "aborted"
1366
1599
  );
1367
1600
  }
@@ -1371,21 +1604,23 @@ async function ensureInstalled(ctx, out) {
1371
1604
  }
1372
1605
  function requireInstalled(ctx) {
1373
1606
  if (!installed(ctx) || !venvReady(ctx.paths, ctx.lockFile)) {
1374
- 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}`);
1375
1609
  }
1376
1610
  }
1377
1611
  async function start(ctx, out, command, wait2 = true) {
1612
+ const pkg = pkgOf(ctx);
1378
1613
  const at = await port(ctx);
1379
1614
  out.step(
1380
1615
  "start",
1381
- 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)`
1382
1617
  );
1383
1618
  try {
1384
- await startServer(ctx.paths, { port: at, key: key(ctx), wait: wait2, signal: ctx.signal });
1619
+ await startServer(ctx.paths, { port: at, key: key(ctx), wait: wait2, signal: ctx.signal, name: pkg.served });
1385
1620
  } catch (error) {
1386
1621
  if (error instanceof StartStopped) {
1387
1622
  throw new DownloadError(
1388
- error.serverStopped ? `Stopped while the mude 1.0 server was starting, and stopped the server too. Run mu judge mude ${command} again to finish` : `Stopped waiting for the mude 1.0 server, which is still starting. Run mu judge mude ${command} again to finish`,
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`,
1389
1624
  "aborted"
1390
1625
  );
1391
1626
  }
@@ -1393,16 +1628,60 @@ async function start(ctx, out, command, wait2 = true) {
1393
1628
  }
1394
1629
  return at;
1395
1630
  }
1396
- function enable(ctx, out, at, localOnly) {
1397
- 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 });
1398
1651
  out.step(
1399
1652
  "enable",
1400
- 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.`
1401
1654
  );
1402
1655
  }
1403
- async function main(argv, ctx, write, tty = false) {
1404
- const json = argv.includes("--json");
1405
- 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");
1406
1685
  const out = json ? jsonOut(write) : humanOut(write, tty);
1407
1686
  const command = args[0] ?? "status";
1408
1687
  const flag = (name) => args.includes(name);
@@ -1426,29 +1705,12 @@ async function main(argv, ctx, write, tty = false) {
1426
1705
  return 1;
1427
1706
  }
1428
1707
  switch (command) {
1429
- case "install": {
1430
- const localOnly = installLocalOnly(ctx, args);
1431
- const release2 = holdInstall(ctx.paths);
1432
- try {
1433
- writeFileSync6(ctx.paths.install, `${JSON.stringify({ localOnly })}
1434
- `);
1435
- await ensureInstalled(ctx, out);
1436
- const at = await start(ctx, out, "install");
1437
- enable(ctx, out, at, localOnly);
1438
- rmSync4(ctx.paths.install, { force: true });
1439
- } finally {
1440
- release2();
1441
- }
1708
+ case "install":
1709
+ await install(ctx, out, args);
1442
1710
  break;
1443
- }
1444
- case "enable": {
1445
- requireInstalled(ctx);
1446
- const current = judgeSetting(ctx.files);
1447
- const localOnly = flag("--local-only") ? true : flag("--with-fallback") ? false : current.enabled && current.localOnly;
1448
- const at = await start(ctx, out, "enable");
1449
- enable(ctx, out, at, localOnly);
1711
+ case "enable":
1712
+ await enableCommand(ctx, out, args);
1450
1713
  break;
1451
- }
1452
1714
  case "disable": {
1453
1715
  if (!mudeInTiers(ctx.files)) {
1454
1716
  const stopped2 = await stopServer(ctx.paths);
@@ -1479,8 +1741,20 @@ async function main(argv, ctx, write, tty = false) {
1479
1741
  out.step("disable", `mude 1.0 is no longer the judge: ${tiers.join(" > ")} again.`);
1480
1742
  }
1481
1743
  await stopServer(ctx.paths);
1482
- rmSync4(ctx.paths.root, { recursive: true, force: true });
1483
- 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
+ }
1484
1758
  break;
1485
1759
  }
1486
1760
  default:
@@ -1498,16 +1772,245 @@ ${USAGE}`, "usage");
1498
1772
  }
1499
1773
  }
1500
1774
  function contextFromEnv(env = process.env, home = homedir3()) {
1501
- const muHome = env.MU_MUDE_HOME || join6(home, ".mu");
1502
- 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");
1503
1777
  return {
1504
1778
  paths: mudePaths(muHome),
1505
- files: { agentDir, home: muHome, envFile: env.MU_MUDE_ENV_FILE || join6(muHome, ".env") },
1506
- 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"),
1507
1781
  env
1508
1782
  };
1509
1783
  }
1510
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
+
1511
2014
  // packages/kyrn-judge/src/mude/cli.ts
1512
2015
  var stop = new AbortController();
1513
2016
  var onSignal = (signal) => {
@@ -1517,8 +2020,10 @@ var onSignal = (signal) => {
1517
2020
  };
1518
2021
  process.on("SIGINT", onSignal);
1519
2022
  process.on("SIGTERM", onSignal);
1520
- main(
1521
- 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,
1522
2027
  { ...contextFromEnv(), signal: stop.signal },
1523
2028
  (text) => process.stdout.write(text),
1524
2029
  Boolean(process.stdout.isTTY)
@@ -1527,8 +2032,10 @@ main(
1527
2032
  process.exitCode = code;
1528
2033
  },
1529
2034
  (error) => {
1530
- process.stderr.write(`mu judge mude: ${error instanceof Error ? error.message : String(error)}
1531
- `);
2035
+ process.stderr.write(
2036
+ `mu judge ${argv[0] === "model" ? "model" : "mude"}: ${error instanceof Error ? error.message : String(error)}
2037
+ `
2038
+ );
1532
2039
  process.exitCode = 1;
1533
2040
  }
1534
2041
  );