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.
@@ -2,7 +2,7 @@ import { createRequire as __muCreateRequire } from "node:module"; const require
2
2
 
3
3
  // packages/kyrn-judge/src/setup/main.ts
4
4
  import { constants as constants2 } from "node:os";
5
- import { join as join9 } from "node:path";
5
+ import { join as join10 } from "node:path";
6
6
  import {
7
7
  applyHttpProxySettings,
8
8
  configureHttpDispatcher,
@@ -54,9 +54,9 @@ function drop(lock) {
54
54
  // packages/kyrn-judge/src/mude/command.ts
55
55
  import { execFileSync as execFileSync2 } from "node:child_process";
56
56
  import { randomBytes } from "node:crypto";
57
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, rmSync as rmSync4, statfsSync, writeFileSync as writeFileSync6 } from "node:fs";
57
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync4, statfsSync, writeFileSync as writeFileSync7 } from "node:fs";
58
58
  import { homedir as homedir4, release, totalmem as totalmem2 } from "node:os";
59
- import { join as join7 } from "node:path";
59
+ import { join as join8 } from "node:path";
60
60
 
61
61
  // packages/kyrn-judge/src/mude/config.ts
62
62
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
@@ -154,21 +154,21 @@ function readEnvEntries(text) {
154
154
  }
155
155
  let body = rest.slice(1);
156
156
  let end = closingQuote(body, quote);
157
- const startedAt = index;
157
+ const startedAt2 = index;
158
158
  while (end < 0 && index + 1 < lines.length) {
159
159
  body += `
160
160
  ${lines[++index]}`;
161
161
  end = closingQuote(body, quote);
162
162
  }
163
163
  if (end < 0) {
164
- index = startedAt;
164
+ index = startedAt2;
165
165
  continue;
166
166
  }
167
167
  const value = body.slice(0, end);
168
168
  entries.push({
169
169
  name,
170
170
  value: quote === '"' ? value.replace(/\\(["\\$`])/g, "$1") : value,
171
- first: startedAt,
171
+ first: startedAt2,
172
172
  last: index
173
173
  });
174
174
  }
@@ -432,8 +432,8 @@ var FILES = [
432
432
  ];
433
433
  var MUDE_FILES = FILES.map(([path, size, sha256]) => ({ path, size, sha256 }));
434
434
  var MUDE_BYTES = MUDE_FILES.reduce((sum, file) => sum + file.size, 0);
435
- function mudeFileUrl(path, endpoint = "https://huggingface.co") {
436
- return `${endpoint.replace(/\/+$/, "")}/${MUDE_REPO}/resolve/${MUDE_REVISION}/${path.split("/").map(encodeURIComponent).join("/")}`;
435
+ function fileUrl(repo, revision, path, endpoint = "https://huggingface.co") {
436
+ return `${endpoint.replace(/\/+$/, "")}/${repo}/resolve/${revision}/${path.split("/").map(encodeURIComponent).join("/")}`;
437
437
  }
438
438
 
439
439
  // packages/kyrn-judge/src/mude/server.ts
@@ -503,12 +503,21 @@ function tail(file, lines = 12) {
503
503
  return "";
504
504
  }
505
505
  }
506
+ var StartStopped = class extends Error {
507
+ /** Whether the server went too: one this call started does, one that was running before does not. */
508
+ serverStopped;
509
+ constructor(serverStopped = false) {
510
+ super("Stopped while the mude 1.0 server was starting");
511
+ this.name = "StartStopped";
512
+ this.serverStopped = serverStopped;
513
+ }
514
+ };
506
515
  async function startServer(paths, options) {
507
516
  const sleep2 = options.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
508
517
  const timeoutMs = options.timeoutMs ?? 18e4;
509
518
  const running = readPid(paths);
510
519
  if (running && serverAlive(paths, running)) {
511
- if (options.wait !== false) await untilReady(paths, running, options.port, timeoutMs, sleep2);
520
+ if (options.wait !== false) await untilReady(paths, running, options.port, timeoutMs, sleep2, options.signal);
512
521
  return { pid: running, port: options.port, already: true };
513
522
  }
514
523
  if (!await portFree(options.port)) {
@@ -530,7 +539,7 @@ async function startServer(paths, options) {
530
539
  "--port",
531
540
  String(options.port),
532
541
  "--name",
533
- MUDE_MODEL,
542
+ options.name ?? MUDE_MODEL,
534
543
  "--verify",
535
544
  "fast"
536
545
  ],
@@ -557,12 +566,21 @@ async function startServer(paths, options) {
557
566
  child.unref();
558
567
  writeFileSync2(paths.pid, `${pid}
559
568
  `, { mode: 384 });
560
- if (options.wait !== false) await untilReady(paths, pid, options.port, timeoutMs, sleep2);
569
+ if (options.wait !== false) {
570
+ try {
571
+ await untilReady(paths, pid, options.port, timeoutMs, sleep2, options.signal);
572
+ } catch (error) {
573
+ if (!(error instanceof StartStopped)) throw error;
574
+ await stopServer(paths);
575
+ throw new StartStopped(true);
576
+ }
577
+ }
561
578
  return { pid, port: options.port, already: false };
562
579
  }
563
- async function untilReady(paths, pid, port2, timeoutMs, sleep2) {
580
+ async function untilReady(paths, pid, port2, timeoutMs, sleep2, signal) {
564
581
  const deadline = Date.now() + timeoutMs;
565
582
  while (Date.now() < deadline) {
583
+ if (signal?.aborted) throw new StartStopped();
566
584
  if ((await health(port2))?.status === "ready") return;
567
585
  if (!serverAlive(paths, pid)) {
568
586
  rmSync(paths.pid, { force: true });
@@ -602,6 +620,11 @@ function serverMemory(pid) {
602
620
  var ROUTED_AWAY = ["tool.description", "tool.injection"];
603
621
  var MUDE = "mude";
604
622
  var KEY_VARIABLE = "MU_JUDGE_MUDE_KEY";
623
+ var MUDE_JUDGE = {
624
+ name: MUDE,
625
+ keyVariable: KEY_VARIABLE,
626
+ entry: (port2) => port2 === DEFAULT_PORT ? void 0 : entryFor(port2)
627
+ };
605
628
  function readState(paths) {
606
629
  try {
607
630
  const value = JSON.parse(readFileSync3(paths.state, "utf8"));
@@ -614,18 +637,22 @@ function writeState(paths, state) {
614
637
  writeFileSync3(paths.state, `${JSON.stringify(state, null, 2)}
615
638
  `, { mode: 384 });
616
639
  }
617
- function judgeSetting(files) {
640
+ function judgeSetting(files, name = MUDE) {
618
641
  const file = readJsonFile(judgeConfigPath(files.agentDir));
619
642
  if (file.state === "unreadable") return { enabled: false, localOnly: false, fallback: [], unreadable: true };
620
643
  const config = file.state === "ok" ? file.value : {};
621
644
  const tiers = tiersOf(config);
622
- const enabled = tiers[0] === MUDE && !judgeOff(config);
623
- const fallback = tiers.filter((tier) => tier !== MUDE);
645
+ const enabled = tiers[0] === name && !judgeOff(config);
646
+ const fallback = tiers.filter((tier) => tier !== name);
624
647
  return { enabled, localOnly: enabled && fallback.length === 0, fallback };
625
648
  }
626
- function mudeInTiers(files) {
649
+ function mudeInTiers(files, name = MUDE) {
650
+ const file = readJsonFile(judgeConfigPath(files.agentDir));
651
+ return file.state === "ok" && tiersOf(file.value).includes(name);
652
+ }
653
+ function currentTiers(files) {
627
654
  const file = readJsonFile(judgeConfigPath(files.agentDir));
628
- return file.state === "ok" && tiersOf(file.value).includes(MUDE);
655
+ return file.state === "ok" ? tiersOf(file.value) : [];
629
656
  }
630
657
  function readConfig(files) {
631
658
  const path = judgeConfigPath(files.agentDir);
@@ -659,18 +686,20 @@ function withoutOwnRoutes(config, state) {
659
686
  function entryFor(port2) {
660
687
  return { ...BUILT_IN_JUDGES[MUDE], baseUrl: mudeBaseUrl(port2) };
661
688
  }
662
- function enableMude(paths, files, options) {
689
+ function enableLocal(judge, paths, files, options) {
690
+ const name = judge.name;
663
691
  const { path, config } = readConfig(files);
664
692
  const state = readState(paths);
665
- const current = tiersOf(config).filter((tier) => tier !== MUDE);
693
+ const current = tiersOf(config).filter((tier) => tier !== name);
666
694
  const before = current.length > 0 ? current : [...state?.previousTiers ?? ["jev"]];
667
- const tiers = options.localOnly ? [MUDE] : [MUDE, ...before.filter((tier) => tier !== MUDE)];
695
+ const tiers = options.localOnly ? [name] : [name, ...before.filter((tier) => tier !== name)];
668
696
  const judgeWasOff = state?.judgeWasOff ?? judgeOff(config);
669
697
  let next = withJudge(withTiers(withoutOwnRoutes(config, state), tiers), true);
670
698
  const judges = judgesOf(next);
671
- const entry = options.port !== DEFAULT_PORT;
672
- if (entry) judges[MUDE] = entryFor(options.port);
673
- else if (state?.wroteEntry) delete judges[MUDE];
699
+ const written = judge.entry(options.port);
700
+ if (written) judges[name] = written;
701
+ else if (state?.wroteEntry) delete judges[name];
702
+ const entry = written !== void 0;
674
703
  next = withSection(next, "judges", judges);
675
704
  const fallback = tiers.slice(1);
676
705
  const routes = recordOf(next.routes);
@@ -686,9 +715,9 @@ function enableMude(paths, files, options) {
686
715
  const backups = new Backups(files.home, files.now ?? /* @__PURE__ */ new Date());
687
716
  backups.save(path);
688
717
  writeJsonFile(path, next);
689
- if (envFileValue(readText(files.envFile), KEY_VARIABLE) !== options.key) {
718
+ if (envFileValue(readText(files.envFile), judge.keyVariable) !== options.key) {
690
719
  backups.save(files.envFile);
691
- writeEnvValue(files.envFile, KEY_VARIABLE, options.key);
720
+ writeEnvValue(files.envFile, judge.keyVariable, options.key);
692
721
  }
693
722
  writeState(paths, {
694
723
  port: options.port,
@@ -700,15 +729,18 @@ function enableMude(paths, files, options) {
700
729
  return tiers;
701
730
  }
702
731
  function disableMude(paths, files, options = {}) {
732
+ return disableLocal(MUDE_JUDGE, paths, files, options);
733
+ }
734
+ function disableLocal(judge, paths, files, options = {}) {
703
735
  const { path, config } = readConfig(files);
704
736
  const state = readState(paths);
705
- const rest = tiersOf(config).filter((tier) => tier !== MUDE);
737
+ const rest = tiersOf(config).filter((tier) => tier !== judge.name);
706
738
  const tiers = rest.length > 0 ? rest : [...state?.previousTiers ?? ["jev"]];
707
739
  let next = withTiers(withoutOwnRoutes(config, state), tiers);
708
740
  if (state?.judgeWasOff) next = withJudge(next, false);
709
741
  if (options.removeEntry && state?.wroteEntry) {
710
742
  const judges = judgesOf(next);
711
- delete judges[MUDE];
743
+ delete judges[judge.name];
712
744
  next = withSection(next, "judges", judges);
713
745
  }
714
746
  new Backups(files.home, files.now ?? /* @__PURE__ */ new Date()).save(path);
@@ -748,6 +780,7 @@ import { open } from "node:fs/promises";
748
780
  import { homedir as homedir2 } from "node:os";
749
781
  import { dirname as dirname2, join as join4 } from "node:path";
750
782
  import { pipeline } from "node:stream/promises";
783
+ import { setTimeout as wait } from "node:timers/promises";
751
784
  var CHECKPOINT_BYTES = 32 * 1024 * 1024;
752
785
  var RETRY_WAITS = [2e3, 5e3, 1e4, 2e4, 3e4];
753
786
  function huggingFaceToken(env = process.env, home2 = homedir2()) {
@@ -773,6 +806,14 @@ async function sha256File(path) {
773
806
  await pipeline(createReadStream(path), hash);
774
807
  return hash.digest("hex");
775
808
  }
809
+ var pinnedHash = (file) => file.sha256 ?? file.gitSha1 ?? "";
810
+ async function hashOf(path, file) {
811
+ if (file.sha256 || !file.gitSha1) return sha256File(path);
812
+ const hash = createHash("sha1");
813
+ hash.update(`blob ${statSync3(path).size}\0`);
814
+ await pipeline(createReadStream(path), hash);
815
+ return hash.digest("hex");
816
+ }
776
817
  function readVerified(dir) {
777
818
  try {
778
819
  const value = JSON.parse(readFileSync4(join4(dir, ".mu-verified.json"), "utf8"));
@@ -783,7 +824,7 @@ function readVerified(dir) {
783
824
  }
784
825
  function knownGood(dir, file, verified) {
785
826
  const seen = verified[file.path];
786
- if (!seen || seen.sha256 !== file.sha256) return false;
827
+ if (!seen || seen.sha256 !== pinnedHash(file)) return false;
787
828
  try {
788
829
  const stats = statSync3(join4(dir, file.path));
789
830
  return stats.size === file.size && stats.mtimeMs === seen.mtimeMs;
@@ -793,7 +834,7 @@ function knownGood(dir, file, verified) {
793
834
  }
794
835
  function remember(dir, verified, file) {
795
836
  const stats = statSync3(join4(dir, file.path));
796
- verified[file.path] = { size: stats.size, mtimeMs: stats.mtimeMs, sha256: file.sha256 };
837
+ verified[file.path] = { size: stats.size, mtimeMs: stats.mtimeMs, sha256: pinnedHash(file) };
797
838
  writeFileSync4(join4(dir, ".mu-verified.json"), `${JSON.stringify(verified, null, 1)}
798
839
  `);
799
840
  }
@@ -844,7 +885,7 @@ async function downloadFiles(options) {
844
885
  mkdirSync4(dirname2(target), { recursive: true });
845
886
  if (existsSync4(target) && statSync3(target).size === file.size) {
846
887
  options.onProgress?.({ file: file.path, checking: true, done: settled, total });
847
- if (await sha256File(target) === file.sha256) {
888
+ if (await hashOf(target, file) === pinnedHash(file)) {
848
889
  remember(dir, verified, file);
849
890
  settled += file.size;
850
891
  continue;
@@ -855,17 +896,17 @@ async function downloadFiles(options) {
855
896
  const onBytes = (bytes) => options.onProgress?.({ file: file.path, checking: false, done: settled + bytes, total });
856
897
  let resumed = await fetchFile(options, file, part, onBytes);
857
898
  options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
858
- let actual = await sha256File(part);
859
- if (actual !== file.sha256 && resumed) {
899
+ let actual = await hashOf(part, file);
900
+ if (actual !== pinnedHash(file) && resumed) {
860
901
  dropPart(part);
861
902
  resumed = await fetchFile(options, file, part, onBytes);
862
903
  options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
863
- actual = await sha256File(part);
904
+ actual = await hashOf(part, file);
864
905
  }
865
- if (actual !== file.sha256) {
906
+ if (actual !== pinnedHash(file)) {
866
907
  dropPart(part);
867
908
  throw new DownloadError(
868
- `${file.path} does not match its pinned sha256 (got ${actual.slice(0, 12)}\u2026, want ${file.sha256.slice(0, 12)}\u2026); it was removed`,
909
+ `${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`,
869
910
  "integrity"
870
911
  );
871
912
  }
@@ -899,7 +940,8 @@ function writeSaved(part, bytes) {
899
940
  }
900
941
  async function fetchFile(options, file, part, onBytes) {
901
942
  const attempts = options.attempts ?? 8;
902
- const sleep2 = options.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
943
+ const sleep2 = options.sleep ?? ((ms) => wait(ms, void 0, { signal: options.signal }).catch(() => {
944
+ }));
903
945
  let resumed = false;
904
946
  let failures = 0;
905
947
  let lastError;
@@ -911,6 +953,12 @@ async function fetchFile(options, file, part, onBytes) {
911
953
  return resumed;
912
954
  } catch (error) {
913
955
  if (error instanceof DownloadError && error.kind !== "network") throw error;
956
+ const code = error.code;
957
+ if (code === "ENOSPC" || code === "EDQUOT") {
958
+ throw new Error(
959
+ `The disk is full: couldn't write ${file.path}. Free up some space, then run mu judge mude install to resume`
960
+ );
961
+ }
914
962
  lastError = error;
915
963
  failures = resumeFrom(part, file.size, options.checkpointBytes) > before ? 1 : failures + 1;
916
964
  if (failures >= attempts) break;
@@ -919,7 +967,7 @@ async function fetchFile(options, file, part, onBytes) {
919
967
  }
920
968
  const reason = lastError instanceof Error ? lastError.message : String(lastError);
921
969
  throw new DownloadError(
922
- `${file.path} could not be downloaded (${reason}); run the install again to go on from here`,
970
+ `${file.path} could not be downloaded (${reason}). Run mu judge mude install again to resume`,
923
971
  "network"
924
972
  );
925
973
  }
@@ -949,7 +997,7 @@ async function fetchOnce(options, file, part, onBytes) {
949
997
  });
950
998
  if (response.status === 401 || response.status === 403 || response.status === 404) {
951
999
  throw new DownloadError(
952
- `Hugging Face answered ${response.status} for ${file.path}: the model is not public yet, or this machine's token cannot read it`,
1000
+ `Hugging Face returned ${response.status} for ${file.path}: the model isn't public yet, or this machine's token can't access it`,
953
1001
  "access"
954
1002
  );
955
1003
  }
@@ -1002,6 +1050,10 @@ async function fetchOnce(options, file, part, onBytes) {
1002
1050
  }
1003
1051
  }
1004
1052
 
1053
+ // packages/kyrn-judge/src/mude/models.ts
1054
+ import { mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
1055
+ import { join as join6 } from "node:path";
1056
+
1005
1057
  // packages/kyrn-judge/src/mude/paths.ts
1006
1058
  import { join as join5 } from "node:path";
1007
1059
  function mudePaths(muHome2) {
@@ -1020,16 +1072,77 @@ function mudePaths(muHome2) {
1020
1072
  pid: join5(root, "server.pid"),
1021
1073
  log: join5(root, "server.log"),
1022
1074
  state: join5(root, "state.json"),
1023
- install: join5(root, "install.json")
1075
+ install: join5(root, "install.json"),
1076
+ installLock: join5(root, "install.lock")
1077
+ };
1078
+ }
1079
+
1080
+ // packages/kyrn-judge/src/mude/models.ts
1081
+ function modelsDir(muHome2) {
1082
+ return join6(muHome2, "local-judge", "models");
1083
+ }
1084
+ function modelPaths(muHome2, record) {
1085
+ const shared = mudePaths(muHome2);
1086
+ const root = join6(modelsDir(muHome2), record.id);
1087
+ const model = join6(root, "model", record.revision);
1088
+ return {
1089
+ ...shared,
1090
+ root,
1091
+ model,
1092
+ verified: join6(model, ".mu-verified.json"),
1093
+ key: join6(root, "key"),
1094
+ pid: join6(root, "server.pid"),
1095
+ log: join6(root, "server.log"),
1096
+ state: join6(root, "state.json"),
1097
+ install: join6(root, "install.json"),
1098
+ installLock: join6(root, "install.lock")
1099
+ };
1100
+ }
1101
+ function isRecord2(value) {
1102
+ if (typeof value !== "object" || value === null) return false;
1103
+ const record = value;
1104
+ return typeof record.id === "string" && typeof record.repo === "string" && typeof record.revision === "string" && typeof record.bytes === "number" && Array.isArray(record.files);
1105
+ }
1106
+ function listRecords(muHome2) {
1107
+ let names;
1108
+ try {
1109
+ names = readdirSync(modelsDir(muHome2));
1110
+ } catch {
1111
+ return [];
1112
+ }
1113
+ const records = [];
1114
+ for (const name of names.sort()) {
1115
+ try {
1116
+ const value = JSON.parse(readFileSync5(join6(modelsDir(muHome2), name, "model.json"), "utf8"));
1117
+ if (isRecord2(value) && value.id === name) records.push(value);
1118
+ } catch {
1119
+ }
1120
+ }
1121
+ return records;
1122
+ }
1123
+ function keyVariable(id) {
1124
+ return `MU_JUDGE_${id.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_KEY`;
1125
+ }
1126
+ function localJudgeOf(id) {
1127
+ return {
1128
+ name: id,
1129
+ keyVariable: keyVariable(id),
1130
+ entry: (port2) => ({
1131
+ type: "typesafe",
1132
+ baseUrl: mudeBaseUrl(port2),
1133
+ model: id,
1134
+ apiKeyEnv: keyVariable(id),
1135
+ timeoutMs: 4e3
1136
+ })
1024
1137
  };
1025
1138
  }
1026
1139
 
1027
1140
  // packages/kyrn-judge/src/mude/python.ts
1028
1141
  import { spawn as spawn2 } from "node:child_process";
1029
1142
  import { createHash as createHash2 } from "node:crypto";
1030
- import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
1143
+ import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
1031
1144
  import { homedir as homedir3 } from "node:os";
1032
- import { delimiter, join as join6 } from "node:path";
1145
+ import { delimiter, join as join7 } from "node:path";
1033
1146
  var PYTHON_VERSION = "3.13";
1034
1147
  var UV_RELEASE = {
1035
1148
  version: "0.13.1",
@@ -1047,14 +1160,14 @@ function executable(path) {
1047
1160
  function findUv(paths, env = process.env, home2 = homedir3()) {
1048
1161
  const dirs = [
1049
1162
  ...(env.PATH ?? "").split(delimiter),
1050
- join6(home2, ".local", "bin"),
1051
- join6(home2, ".cargo", "bin"),
1163
+ join7(home2, ".local", "bin"),
1164
+ join7(home2, ".cargo", "bin"),
1052
1165
  "/opt/homebrew/bin",
1053
1166
  "/usr/local/bin",
1054
1167
  paths.tools
1055
1168
  ];
1056
1169
  for (const dir of dirs) {
1057
- if (dir && executable(join6(dir, "uv"))) return join6(dir, "uv");
1170
+ if (dir && executable(join7(dir, "uv"))) return join7(dir, "uv");
1058
1171
  }
1059
1172
  return void 0;
1060
1173
  }
@@ -1084,36 +1197,47 @@ function run(command, args, options = {}) {
1084
1197
  });
1085
1198
  }
1086
1199
  async function downloadUv(paths, options = {}) {
1087
- mkdirSync5(paths.tools, { recursive: true });
1200
+ mkdirSync6(paths.tools, { recursive: true });
1088
1201
  const archive = "uv.tar.gz";
1089
1202
  options.onLine?.(`Downloading uv ${UV_RELEASE.version}`);
1090
- const response = await (options.fetch ?? fetch)(UV_RELEASE.url, { redirect: "follow" });
1091
- if (!response.ok) throw new Error(`uv could not be downloaded: HTTP ${response.status}`);
1092
- const bytes = Buffer.from(await response.arrayBuffer());
1203
+ const timeoutMs = options.timeoutMs ?? 3e5;
1204
+ const timeout = AbortSignal.timeout(timeoutMs);
1205
+ let bytes;
1206
+ try {
1207
+ const response = await (options.fetch ?? fetch)(UV_RELEASE.url, {
1208
+ redirect: "follow",
1209
+ signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout
1210
+ });
1211
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1212
+ bytes = Buffer.from(await response.arrayBuffer());
1213
+ } catch (error) {
1214
+ const reason = timeout.aborted ? `no answer in ${timeoutMs / 1e3} s` : error instanceof Error ? error.message : String(error);
1215
+ throw new Error(`uv could not be downloaded from GitHub (${reason}): run mu judge mude install again`);
1216
+ }
1093
1217
  const actual = createHash2("sha256").update(bytes).digest("hex");
1094
1218
  if (actual !== UV_RELEASE.sha256)
1095
1219
  throw new Error("The downloaded uv does not match its pinned sha256; nothing was installed");
1096
- writeFileSync5(join6(paths.tools, archive), bytes);
1220
+ writeFileSync6(join7(paths.tools, archive), bytes);
1097
1221
  const unpacked = await run("/usr/bin/tar", [
1098
1222
  "-xzf",
1099
- join6(paths.tools, archive),
1223
+ join7(paths.tools, archive),
1100
1224
  "-C",
1101
1225
  paths.tools,
1102
1226
  "--strip-components",
1103
1227
  "1"
1104
1228
  ]);
1105
- rmSync3(join6(paths.tools, archive), { force: true });
1106
- if (unpacked.code !== 0 || !executable(join6(paths.tools, "uv")))
1107
- throw new Error(`uv could not be unpacked: ${unpacked.output}`);
1108
- return join6(paths.tools, "uv");
1229
+ rmSync3(join7(paths.tools, archive), { force: true });
1230
+ if (unpacked.code !== 0 || !executable(join7(paths.tools, "uv")))
1231
+ throw new Error(`Couldn't unpack uv: ${unpacked.output}`);
1232
+ return join7(paths.tools, "uv");
1109
1233
  }
1110
1234
  function lockDigest(lockFile) {
1111
- return createHash2("sha256").update(readFileSync5(lockFile)).digest("hex");
1235
+ return createHash2("sha256").update(readFileSync6(lockFile)).digest("hex");
1112
1236
  }
1113
1237
  function venvReady(paths, lockFile) {
1114
1238
  if (!existsSync5(paths.python)) return false;
1115
1239
  try {
1116
- return readFileSync5(paths.venvLock, "utf8").trim() === lockDigest(lockFile);
1240
+ return readFileSync6(paths.venvLock, "utf8").trim() === lockDigest(lockFile);
1117
1241
  } catch {
1118
1242
  return false;
1119
1243
  }
@@ -1126,7 +1250,7 @@ async function ensureVenv(paths, lockFile, uv, options = {}) {
1126
1250
  env,
1127
1251
  onLine: options.onLine
1128
1252
  });
1129
- if (made.code !== 0) throw new Error(`The Python environment could not be made (uv venv):
1253
+ if (made.code !== 0) throw new Error(`Couldn't create the Python environment (uv venv):
1130
1254
  ${made.output}`);
1131
1255
  const installed2 = await run(
1132
1256
  uv,
@@ -1145,9 +1269,9 @@ ${made.output}`);
1145
1269
  { env, onLine: options.onLine }
1146
1270
  );
1147
1271
  if (installed2.code !== 0)
1148
- throw new Error(`mude's Python packages could not be installed (uv pip install):
1272
+ throw new Error(`Couldn't install mude's Python packages (uv pip install):
1149
1273
  ${installed2.output}`);
1150
- writeFileSync5(paths.venvLock, `${lockDigest(lockFile)}
1274
+ writeFileSync6(paths.venvLock, `${lockDigest(lockFile)}
1151
1275
  `);
1152
1276
  }
1153
1277
 
@@ -1156,7 +1280,7 @@ var USAGE = `mu judge mude: mude 1.0, mu's own decision model optimized for codi
1156
1280
  (Apple silicon, macOS 14 or later)
1157
1281
 
1158
1282
  mu judge mude install [--local-only] download it (${(MUDE_BYTES / 1e9).toFixed(1)} GB), set it up, start it and make it the judge;
1159
- after a stop (Ctrl-C, a lost connection, a power cut), it goes on from there
1283
+ after a stop (Ctrl-C, a lost connection, a power cut), it picks up where it left off
1160
1284
  mu judge mude enable [--local-only | --with-fallback]
1161
1285
  make it the judge again, or change what happens when it can't answer
1162
1286
  mu judge mude disable back to the judges from before, and stop its server
@@ -1185,7 +1309,7 @@ function jsonOut(write) {
1185
1309
  error: (message, kind) => emit({ type: "error", message, kind })
1186
1310
  };
1187
1311
  }
1188
- function humanOut(write, tty) {
1312
+ function humanOut(write, tty, pkg = MUDE_PACKAGE) {
1189
1313
  let shown = -1;
1190
1314
  return {
1191
1315
  step: (_step, message) => {
@@ -1209,7 +1333,7 @@ function humanOut(write, tty) {
1209
1333
  },
1210
1334
  line: (text) => write(` ${text}
1211
1335
  `),
1212
- status: (status2) => write(describe(status2)),
1336
+ status: (status2) => write(describe(status2, pkg)),
1213
1337
  error: (message) => {
1214
1338
  if (shown >= 0 && tty) write("\n");
1215
1339
  shown = -1;
@@ -1218,31 +1342,43 @@ function humanOut(write, tty) {
1218
1342
  }
1219
1343
  };
1220
1344
  }
1221
- function describe(status2) {
1345
+ function describe(status2, pkg) {
1222
1346
  const lines = [];
1223
1347
  if (!status2.supported) {
1224
- lines.push(`mude 1.0 can't run here: ${status2.problems.join("; ")}`);
1348
+ lines.push(`${pkg.label} can't run here: ${status2.problems.join("; ")}`);
1225
1349
  return `${lines.join("\n")}
1226
1350
  `;
1227
1351
  }
1228
1352
  lines.push(
1229
- `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"}`
1353
+ `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}`}`
1230
1354
  );
1231
- if (status2.installed && !status2.pythonReady) lines.push("python not set up: mu judge mude install");
1355
+ if (status2.installed && !status2.pythonReady) lines.push(`python not set up: ${pkg.install}`);
1232
1356
  lines.push(
1233
1357
  `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"}`
1234
1358
  );
1235
1359
  if (status2.enabled) {
1236
1360
  lines.push(
1237
- 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)`
1361
+ 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)`
1238
1362
  );
1239
1363
  } else {
1240
- lines.push(`judge ${status2.installed ? "not enabled: mu judge mude enable" : "not enabled"}`);
1364
+ lines.push(`judge ${status2.installed ? `not enabled: ${pkg.again("enable")}` : "not enabled"}`);
1241
1365
  }
1242
1366
  lines.push(`folder ${status2.folder}`);
1243
1367
  return `${lines.join("\n")}
1244
1368
  `;
1245
1369
  }
1370
+ var MUDE_PACKAGE = {
1371
+ label: "mude 1.0",
1372
+ repo: MUDE_REPO,
1373
+ revision: MUDE_REVISION,
1374
+ files: MUDE_FILES,
1375
+ bytes: MUDE_BYTES,
1376
+ judge: MUDE_JUDGE,
1377
+ served: MUDE_MODEL,
1378
+ install: "mu judge mude install",
1379
+ again: (what) => `mu judge mude ${what}`
1380
+ };
1381
+ var pkgOf = (ctx) => ctx.pkg ?? MUDE_PACKAGE;
1246
1382
  function problems(options = {}) {
1247
1383
  const platform = options.platform ?? process.platform;
1248
1384
  if (platform !== "darwin") return ["it needs a Mac with Apple silicon (an M1 or newer)"];
@@ -1269,7 +1405,7 @@ function appleSilicon() {
1269
1405
  function freeBytes(path) {
1270
1406
  let dir = path;
1271
1407
  while (!existsSync6(dir)) {
1272
- const parent = join7(dir, "..");
1408
+ const parent = join8(dir, "..");
1273
1409
  if (parent === dir) return void 0;
1274
1410
  dir = parent;
1275
1411
  }
@@ -1285,24 +1421,59 @@ function installLocalOnly(ctx, args) {
1285
1421
  if (args.includes("--local-only")) return true;
1286
1422
  if (args.includes("--with-fallback")) return false;
1287
1423
  try {
1288
- const value = JSON.parse(readFileSync6(ctx.paths.install, "utf8"));
1424
+ const value = JSON.parse(readFileSync7(ctx.paths.install, "utf8"));
1289
1425
  return typeof value === "object" && value !== null && "localOnly" in value && value.localOnly === true;
1290
1426
  } catch {
1291
1427
  return false;
1292
1428
  }
1293
1429
  }
1430
+ function startedAt(pid) {
1431
+ try {
1432
+ return execFileSync2("/bin/ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8" }).trim() || void 0;
1433
+ } catch {
1434
+ return void 0;
1435
+ }
1436
+ }
1437
+ function holdInstall(paths, pkg = MUDE_PACKAGE) {
1438
+ mkdirSync7(paths.root, { recursive: true });
1439
+ const mine = `${JSON.stringify({ pid: process.pid, started: startedAt(process.pid) ?? "" })}
1440
+ `;
1441
+ try {
1442
+ writeFileSync7(paths.installLock, mine, { flag: "wx" });
1443
+ } catch (error) {
1444
+ if (error.code !== "EEXIST") throw error;
1445
+ let holder = {};
1446
+ try {
1447
+ holder = JSON.parse(readFileSync7(paths.installLock, "utf8"));
1448
+ } catch {
1449
+ }
1450
+ const pid = typeof holder.pid === "number" ? holder.pid : 0;
1451
+ if (pid > 0 && pid !== process.pid && holder.started && startedAt(pid) === holder.started) {
1452
+ throw new Error(
1453
+ `Another ${pkg.label} install is running (process ${pid}). Wait for it to finish, or stop it, then run ${pkg.install} again`
1454
+ );
1455
+ }
1456
+ writeFileSync7(paths.installLock, mine);
1457
+ }
1458
+ return () => {
1459
+ try {
1460
+ if (readFileSync7(paths.installLock, "utf8") === mine) rmSync4(paths.installLock, { force: true });
1461
+ } catch {
1462
+ }
1463
+ };
1464
+ }
1294
1465
  function installed(ctx) {
1295
- return missingFiles(ctx.paths.model, MUDE_FILES).length === 0;
1466
+ return missingFiles(ctx.paths.model, pkgOf(ctx).files).length === 0;
1296
1467
  }
1297
1468
  function key(ctx) {
1298
1469
  try {
1299
- const existing = readFileSync6(ctx.paths.key, "utf8").trim();
1470
+ const existing = readFileSync7(ctx.paths.key, "utf8").trim();
1300
1471
  if (existing) return existing;
1301
1472
  } catch {
1302
1473
  }
1303
- mkdirSync6(ctx.paths.root, { recursive: true });
1474
+ mkdirSync7(ctx.paths.root, { recursive: true });
1304
1475
  const made = randomBytes(32).toString("hex");
1305
- writeFileSync6(ctx.paths.key, `${made}
1476
+ writeFileSync7(ctx.paths.key, `${made}
1306
1477
  `, { mode: 384 });
1307
1478
  return made;
1308
1479
  }
@@ -1312,18 +1483,20 @@ async function port(ctx) {
1312
1483
  return choosePort(DEFAULT_PORT);
1313
1484
  }
1314
1485
  async function status(ctx) {
1486
+ const pkg = pkgOf(ctx);
1315
1487
  const found = problems();
1316
1488
  const state = readState(ctx.paths);
1317
1489
  const at = state?.port ?? DEFAULT_PORT;
1318
1490
  const pid = readPid(ctx.paths);
1319
1491
  const running = serverAlive(ctx.paths, pid);
1320
1492
  const ready = running && (await health(at))?.status === "ready";
1321
- const setting = judgeSetting(ctx.files);
1493
+ const setting = judgeSetting(ctx.files, pkg.judge.name);
1322
1494
  return {
1495
+ name: pkg.judge.name,
1323
1496
  supported: found.length === 0,
1324
1497
  problems: found,
1325
1498
  installed: installed(ctx),
1326
- downloadedBytes: downloadedBytes(ctx.paths.model, MUDE_FILES),
1499
+ downloadedBytes: downloadedBytes(ctx.paths.model, pkg.files),
1327
1500
  pythonReady: venvReady(ctx.paths, ctx.lockFile),
1328
1501
  running,
1329
1502
  ready,
@@ -1333,31 +1506,33 @@ async function status(ctx) {
1333
1506
  enabled: setting.enabled,
1334
1507
  localOnly: setting.localOnly,
1335
1508
  fallback: setting.fallback,
1336
- model: `${MUDE_REPO}@${MUDE_REVISION}`,
1509
+ model: `${pkg.repo}@${pkg.revision}`,
1337
1510
  folder: ctx.paths.root,
1338
1511
  log: ctx.paths.log
1339
1512
  };
1340
1513
  }
1341
1514
  async function ensureInstalled(ctx, out) {
1342
- const missing = missingFiles(ctx.paths.model, MUDE_FILES);
1515
+ const pkg = pkgOf(ctx);
1516
+ const missing = missingFiles(ctx.paths.model, pkg.files);
1343
1517
  if (missing.length > 0) {
1344
1518
  const need = missing.reduce((sum, file) => sum + file.size, 0);
1519
+ const already = downloadedBytes(ctx.paths.model, pkg.files);
1345
1520
  const free = freeBytes(ctx.paths.root);
1346
- if (free !== void 0 && free < need + 2e9) {
1521
+ const room = pkg.bytes - already + 2e9;
1522
+ if (free !== void 0 && free < room) {
1347
1523
  throw new Error(
1348
- `Not enough free disk space: mude 1.0 needs about ${((need + 2e9) / 1e9).toFixed(1)} GB, and ${(free / 1e9).toFixed(1)} GB are free`
1524
+ `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`
1349
1525
  );
1350
1526
  }
1351
- const already = downloadedBytes(ctx.paths.model, MUDE_FILES);
1352
1527
  out.step(
1353
1528
  "download",
1354
- 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`
1529
+ 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`
1355
1530
  );
1356
1531
  try {
1357
1532
  await downloadFiles({
1358
1533
  dir: ctx.paths.model,
1359
- files: MUDE_FILES,
1360
- url: (path) => mudeFileUrl(path, ctx.env.HF_ENDPOINT || void 0),
1534
+ files: pkg.files,
1535
+ url: (path) => fileUrl(pkg.repo, pkg.revision, path, ctx.env.HF_ENDPOINT || void 0),
1361
1536
  token: huggingFaceToken(ctx.env),
1362
1537
  onProgress: (event) => out.progress(event),
1363
1538
  signal: ctx.signal
@@ -1365,7 +1540,7 @@ async function ensureInstalled(ctx, out) {
1365
1540
  } catch (error) {
1366
1541
  if (error instanceof DownloadError && error.kind === "aborted") {
1367
1542
  throw new DownloadError(
1368
- `Stopped with ${gb(downloadedBytes(ctx.paths.model, MUDE_FILES))} of ${gb(MUDE_BYTES)} GB downloaded: mu judge mude install goes on from there`,
1543
+ `Stopped with ${gb(downloadedBytes(ctx.paths.model, pkg.files))} of ${gb(pkg.bytes)} GB downloaded. Run ${pkg.install} to resume`,
1369
1544
  "aborted"
1370
1545
  );
1371
1546
  }
@@ -1373,33 +1548,99 @@ async function ensureInstalled(ctx, out) {
1373
1548
  }
1374
1549
  }
1375
1550
  if (!venvReady(ctx.paths, ctx.lockFile)) {
1376
- out.step("python", "Setting up the Python environment for mude 1.0 (MLX, transformers; about 1 GB, once)");
1377
- let uv = findUv(ctx.paths, ctx.env);
1378
- if (!uv) uv = await downloadUv(ctx.paths, { onLine: (line) => out.line(line) });
1379
- await ensureVenv(ctx.paths, ctx.lockFile, uv, { onLine: (line) => out.line(line), env: ctx.env });
1551
+ out.step("python", `Setting up the Python environment for ${pkg.label} (MLX, transformers; about 1 GB, once)`);
1552
+ try {
1553
+ let uv = findUv(ctx.paths, ctx.env);
1554
+ if (!uv) uv = await downloadUv(ctx.paths, { onLine: (line) => out.line(line), signal: ctx.signal });
1555
+ await ensureVenv(ctx.paths, ctx.lockFile, uv, { onLine: (line) => out.line(line), env: ctx.env });
1556
+ } catch (error) {
1557
+ if (ctx.signal?.aborted) {
1558
+ throw new DownloadError(
1559
+ `Stopped while setting up the Python environment. Run ${pkg.install} to resume`,
1560
+ "aborted"
1561
+ );
1562
+ }
1563
+ throw error;
1564
+ }
1380
1565
  }
1381
1566
  }
1382
1567
  function requireInstalled(ctx) {
1383
1568
  if (!installed(ctx) || !venvReady(ctx.paths, ctx.lockFile)) {
1384
- throw new Error("mude 1.0 is not installed here yet: mu judge mude install");
1569
+ const pkg = pkgOf(ctx);
1570
+ throw new Error(`${pkg.label} is not installed here yet: ${pkg.install}`);
1385
1571
  }
1386
1572
  }
1387
- async function start(ctx, out, wait = true) {
1573
+ async function start(ctx, out, command, wait2 = true) {
1574
+ const pkg = pkgOf(ctx);
1388
1575
  const at = await port(ctx);
1389
1576
  out.step(
1390
1577
  "start",
1391
- 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)`
1578
+ 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)`
1392
1579
  );
1393
- await startServer(ctx.paths, { port: at, key: key(ctx), wait });
1580
+ try {
1581
+ await startServer(ctx.paths, { port: at, key: key(ctx), wait: wait2, signal: ctx.signal, name: pkg.served });
1582
+ } catch (error) {
1583
+ if (error instanceof StartStopped) {
1584
+ throw new DownloadError(
1585
+ 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`,
1586
+ "aborted"
1587
+ );
1588
+ }
1589
+ throw error;
1590
+ }
1394
1591
  return at;
1395
1592
  }
1396
- function enable(ctx, out, at, localOnly) {
1397
- const tiers = enableMude(ctx.paths, ctx.files, { port: at, key: key(ctx), localOnly });
1593
+ function others(ctx) {
1594
+ const name = pkgOf(ctx).judge.name;
1595
+ const home2 = ctx.files.home;
1596
+ const found = [];
1597
+ if (name !== MUDE) found.push({ judge: MUDE_JUDGE, paths: mudePaths(home2) });
1598
+ for (const record of listRecords(home2)) {
1599
+ if (record.id !== name) found.push({ judge: localJudgeOf(record.id), paths: modelPaths(home2, record) });
1600
+ }
1601
+ return found;
1602
+ }
1603
+ async function stopOthers(ctx) {
1604
+ for (const other of others(ctx)) await stopServer(other.paths);
1605
+ }
1606
+ async function enable(ctx, out, at, localOnly) {
1607
+ const pkg = pkgOf(ctx);
1608
+ const tiers = currentTiers(ctx.files);
1609
+ for (const other of others(ctx)) {
1610
+ if (tiers.includes(other.judge.name)) disableLocal(other.judge, other.paths, ctx.files);
1611
+ }
1612
+ const order = enableLocal(pkg.judge, ctx.paths, ctx.files, { port: at, key: key(ctx), localOnly });
1398
1613
  out.step(
1399
1614
  "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.`
1615
+ 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
1616
  );
1402
1617
  }
1618
+ async function install(ctx, out, args, holdAlso) {
1619
+ const localOnly = installLocalOnly(ctx, args);
1620
+ const release2 = holdInstall(ctx.paths, pkgOf(ctx));
1621
+ let releaseAlso;
1622
+ try {
1623
+ if (holdAlso && !venvReady(ctx.paths, ctx.lockFile)) releaseAlso = holdInstall(holdAlso);
1624
+ writeFileSync7(ctx.paths.install, `${JSON.stringify({ localOnly })}
1625
+ `);
1626
+ await ensureInstalled(ctx, out);
1627
+ await stopOthers(ctx);
1628
+ const at = await start(ctx, out, "install");
1629
+ await enable(ctx, out, at, localOnly);
1630
+ rmSync4(ctx.paths.install, { force: true });
1631
+ } finally {
1632
+ releaseAlso?.();
1633
+ release2();
1634
+ }
1635
+ }
1636
+ async function enableCommand(ctx, out, args) {
1637
+ requireInstalled(ctx);
1638
+ const current = judgeSetting(ctx.files, pkgOf(ctx).judge.name);
1639
+ const localOnly = args.includes("--local-only") ? true : args.includes("--with-fallback") ? false : current.enabled && current.localOnly;
1640
+ await stopOthers(ctx);
1641
+ const at = await start(ctx, out, "enable");
1642
+ await enable(ctx, out, at, localOnly);
1643
+ }
1403
1644
  async function main(argv, ctx, write, tty = false) {
1404
1645
  const json = argv.includes("--json");
1405
1646
  const args = argv.filter((arg) => arg !== "--json");
@@ -1426,25 +1667,12 @@ async function main(argv, ctx, write, tty = false) {
1426
1667
  return 1;
1427
1668
  }
1428
1669
  switch (command) {
1429
- case "install": {
1430
- const localOnly = installLocalOnly(ctx, args);
1431
- mkdirSync6(ctx.paths.root, { recursive: true });
1432
- writeFileSync6(ctx.paths.install, `${JSON.stringify({ localOnly })}
1433
- `);
1434
- await ensureInstalled(ctx, out);
1435
- const at = await start(ctx, out);
1436
- enable(ctx, out, at, localOnly);
1437
- rmSync4(ctx.paths.install, { force: true });
1670
+ case "install":
1671
+ await install(ctx, out, args);
1438
1672
  break;
1439
- }
1440
- case "enable": {
1441
- requireInstalled(ctx);
1442
- const current = judgeSetting(ctx.files);
1443
- const localOnly = flag("--local-only") ? true : flag("--with-fallback") ? false : current.enabled && current.localOnly;
1444
- const at = await start(ctx, out);
1445
- enable(ctx, out, at, localOnly);
1673
+ case "enable":
1674
+ await enableCommand(ctx, out, args);
1446
1675
  break;
1447
- }
1448
1676
  case "disable": {
1449
1677
  if (!mudeInTiers(ctx.files)) {
1450
1678
  const stopped2 = await stopServer(ctx.paths);
@@ -1461,7 +1689,7 @@ async function main(argv, ctx, write, tty = false) {
1461
1689
  }
1462
1690
  case "start": {
1463
1691
  requireInstalled(ctx);
1464
- await start(ctx, out, !flag("--no-wait"));
1692
+ await start(ctx, out, "start", !flag("--no-wait"));
1465
1693
  break;
1466
1694
  }
1467
1695
  case "stop": {
@@ -1475,8 +1703,20 @@ async function main(argv, ctx, write, tty = false) {
1475
1703
  out.step("disable", `mude 1.0 is no longer the judge: ${tiers.join(" > ")} again.`);
1476
1704
  }
1477
1705
  await stopServer(ctx.paths);
1478
- rmSync4(ctx.paths.root, { recursive: true, force: true });
1479
- out.step("uninstall", `Deleted ${ctx.paths.root}: the model, its Python environment and its key.`);
1706
+ if (listRecords(ctx.files.home).length > 0) {
1707
+ for (const name of existsSync6(ctx.paths.root) ? readdirSync2(ctx.paths.root) : []) {
1708
+ const path = join8(ctx.paths.root, name);
1709
+ if (path !== ctx.paths.venv && path !== ctx.paths.tools)
1710
+ rmSync4(path, { recursive: true, force: true });
1711
+ }
1712
+ out.step(
1713
+ "uninstall",
1714
+ `Deleted mude 1.0 and its key from ${ctx.paths.root}. Its Python environment stays there for the other decision models.`
1715
+ );
1716
+ } else {
1717
+ rmSync4(ctx.paths.root, { recursive: true, force: true });
1718
+ out.step("uninstall", `Deleted ${ctx.paths.root}: the model, its Python environment and its key.`);
1719
+ }
1480
1720
  break;
1481
1721
  }
1482
1722
  default:
@@ -1494,12 +1734,12 @@ ${USAGE}`, "usage");
1494
1734
  }
1495
1735
  }
1496
1736
  function contextFromEnv(env = process.env, home2 = homedir4()) {
1497
- const muHome2 = env.MU_MUDE_HOME || join7(home2, ".mu");
1498
- const agentDir2 = env.MU_AGENT_DIR || join7(muHome2, "agent");
1737
+ const muHome2 = env.MU_MUDE_HOME || join8(home2, ".mu");
1738
+ const agentDir2 = env.MU_AGENT_DIR || join8(muHome2, "agent");
1499
1739
  return {
1500
1740
  paths: mudePaths(muHome2),
1501
- files: { agentDir: agentDir2, home: muHome2, envFile: env.MU_MUDE_ENV_FILE || join7(muHome2, ".env") },
1502
- lockFile: env.MU_MUDE_LOCK || join7(import.meta.dirname, "..", "..", "..", "..", "kyrn", "local-judge", "mude-requirements.lock.txt"),
1741
+ files: { agentDir: agentDir2, home: muHome2, envFile: env.MU_MUDE_ENV_FILE || join8(muHome2, ".env") },
1742
+ lockFile: env.MU_MUDE_LOCK || join8(import.meta.dirname, "..", "..", "..", "..", "kyrn", "local-judge", "mude-requirements.lock.txt"),
1503
1743
  env
1504
1744
  };
1505
1745
  }
@@ -1701,8 +1941,8 @@ async function readAll(input = process.stdin) {
1701
1941
  }
1702
1942
 
1703
1943
  // packages/kyrn-judge/src/setup/wizard.ts
1704
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
1705
- import { join as join8 } from "node:path";
1944
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
1945
+ import { join as join9 } from "node:path";
1706
1946
 
1707
1947
  // packages/kyrn-judge/src/setup/check.ts
1708
1948
  var CHECK_TIMEOUT_MS = 1e4;
@@ -2502,14 +2742,6 @@ var Setup = class {
2502
2742
  SETUP_EXIT.usage
2503
2743
  );
2504
2744
  }
2505
- if (!args.firstRun) {
2506
- this.io.say(
2507
- this.t({
2508
- zh: "mu setup\uFF1A\u7ED9 mu \u63A5\u4E0A\u4E00\u4E2A\u5E72\u6D3B\u7684\u6A21\u578B\uFF0C\u518D\u9009\u4E00\u4E2A\u5224\u5B9A\u5668\u3002",
2509
- en: "mu setup: connect a model for mu to work with, and choose a judge."
2510
- })
2511
- );
2512
- }
2513
2745
  const start2 = await this.start(key2 === void 0 ? void 0 : cleanKey(key2));
2514
2746
  if (start2 === "subscription") {
2515
2747
  const judge2 = await this.judge(void 0);
@@ -2541,7 +2773,7 @@ var Setup = class {
2541
2773
  this.io.warn(
2542
2774
  this.t({
2543
2775
  zh: "mude 1.0 \u8FD8\u6CA1\u6709\u88C5\u597D\uFF0C\u5728\u90A3\u4E4B\u524D\u7531 Jev \u5224\u5B9A\u3002\u8FD0\u884C mu judge mude install \u4F1A\u4ECE\u4E2D\u65AD\u7684\u5730\u65B9\u63A5\u7740\u88C5\u3002",
2544
- en: "mude 1.0 isn't installed yet, so Jev is the judge until it is. Run mu judge mude install to go on from where it stopped."
2776
+ en: "mude 1.0 isn't installed yet, so Jev is the judge until it is. Run mu judge mude install to resume it."
2545
2777
  })
2546
2778
  );
2547
2779
  return this.scripted ? SETUP_EXIT.failed : SETUP_EXIT.done;
@@ -2557,9 +2789,9 @@ var Setup = class {
2557
2789
  );
2558
2790
  if (yes) return true;
2559
2791
  try {
2560
- mkdirSync7(this.deps.paths.agentDir, { recursive: true });
2561
- writeFileSync7(
2562
- join8(this.deps.paths.agentDir, SETUP_DECLINED_FILE),
2792
+ mkdirSync8(this.deps.paths.agentDir, { recursive: true });
2793
+ writeFileSync8(
2794
+ join9(this.deps.paths.agentDir, SETUP_DECLINED_FILE),
2563
2795
  "mu asked on its first start whether to set up a model, and the answer was no.\nDelete this file to be asked again; run mu setup any time.\n"
2564
2796
  );
2565
2797
  } catch {
@@ -2600,11 +2832,10 @@ var Setup = class {
2600
2832
  ]
2601
2833
  );
2602
2834
  if (route === "subscription") {
2603
- const auth = this.path(join8(this.deps.paths.agentDir, "auth.json"));
2604
2835
  this.io.say(
2605
2836
  this.t({
2606
- zh: `\u8BA2\u9605\u8D26\u53F7\u5728 mu \u91CC\u7528 /login \u767B\u5F55\uFF1A\u9009 Sign in with an account\uFF0C\u518D\u9009 OpenAI\uFF08ChatGPT\uFF09\u3001xAI\uFF08Grok\uFF09\u6216 Google \u7684\u4E00\u9879\uFF0C\u6309\u63D0\u793A\u5728\u6D4F\u89C8\u5668\u91CC\u5B8C\u6210\u3002setup \u63A5\u7740\u542F\u52A8 mu \u65F6\uFF0C/login \u5DF2\u7ECF\u6253\u5F00\u3002\u767B\u5F55\u4FE1\u606F\u4FDD\u5B58\u5728 ${auth}\u3002`,
2607
- en: `You sign in to a subscription inside mu, with /login: choose Sign in with an account, then OpenAI for ChatGPT, xAI for Grok, or one of the Google sign-ins, and finish in the browser. When setup starts mu, /login is already open. The sign-in is saved in ${auth}.`
2837
+ zh: "\u8BA2\u9605\u8D26\u53F7\u5728 mu \u91CC\u7528 /login \u767B\u5F55\uFF1A\u9009 Sign in with an account\uFF0C\u518D\u9009 OpenAI\uFF08ChatGPT\uFF09\u3001xAI\uFF08Grok\uFF09\u6216 Google \u7684\u4E00\u9879\uFF0C\u6309\u63D0\u793A\u5728\u6D4F\u89C8\u5668\u91CC\u5B8C\u6210\u3002setup \u63A5\u7740\u542F\u52A8 mu \u65F6\uFF0C/login \u5DF2\u7ECF\u6253\u5F00\u3002",
2838
+ en: "Sign in to your subscription from inside mu with /login: choose Sign in with an account, then OpenAI for ChatGPT, xAI for Grok or one of the Google sign-ins, and finish in the browser. When setup starts mu, /login is already open."
2608
2839
  })
2609
2840
  );
2610
2841
  return "subscription";
@@ -2773,7 +3004,7 @@ var Setup = class {
2773
3004
  return [{ provider, builtIn: false, baseUrl: base, api }];
2774
3005
  }
2775
3006
  inModelsFile(provider) {
2776
- const models = readJsonFile(join8(this.deps.paths.agentDir, "models.json"));
3007
+ const models = readJsonFile(join9(this.deps.paths.agentDir, "models.json"));
2777
3008
  return models.state === "ok" && existingProvider(models.value, provider) !== void 0;
2778
3009
  }
2779
3010
  /** A failure as the person can act on it: a local service that is silent is not ready yet, whatever the key. */
@@ -2893,7 +3124,7 @@ var Setup = class {
2893
3124
  }
2894
3125
  }
2895
3126
  modelsFileWritable() {
2896
- const path = join8(this.deps.paths.agentDir, "models.json");
3127
+ const path = join9(this.deps.paths.agentDir, "models.json");
2897
3128
  if (readJsonFile(path).state !== "unreadable") return;
2898
3129
  throw this.fail({
2899
3130
  zh: `${this.path(path)} \u4E0D\u662F\u7EAF JSON\uFF08\u4E5F\u8BB8\u6709\u6CE8\u91CA\u6216\u5199\u9519\u4E86\uFF09\uFF0Cmu setup \u4E0D\u6539\u5199\u5B83\u3002\u5148\u4FEE\u597D\u5B83\uFF0C\u6216\u624B\u52A8\u628A\u670D\u52A1\u52A0\u8FDB\u53BB\u3002`,
@@ -2975,7 +3206,7 @@ var Setup = class {
2975
3206
  /** A provider models.json has at another address is moved only with a yes. */
2976
3207
  async conflicts(target) {
2977
3208
  if (target.reach.builtIn && !this.args.baseUrl) return;
2978
- const models = readJsonFile(join8(this.deps.paths.agentDir, "models.json"));
3209
+ const models = readJsonFile(join9(this.deps.paths.agentDir, "models.json"));
2979
3210
  if (models.state !== "ok") return;
2980
3211
  const current = existingProvider(models.value, target.reach.provider)?.baseUrl;
2981
3212
  const old = typeof current === "string" ? current.replace(/\/+$/, "") : void 0;
@@ -3093,8 +3324,8 @@ var Setup = class {
3093
3324
  });
3094
3325
  const picked = await this.choose(
3095
3326
  this.t({
3096
- zh: "mu \u5DE5\u4F5C\u65F6\u8981\u505A\u5F88\u591A\u5C0F\u5224\u65AD\uFF1A\u8FD9\u6761\u6D88\u606F\u8BE5\u600E\u4E48\u5904\u7406\u3001\u8FD9\u4E00\u6B65\u4F1A\u4E0D\u4F1A\u8FDD\u53CD\u4F60\u5B9A\u7684\u89C4\u5219\u2026\u2026\u5224\u5B9A\u5668\u8D1F\u8D23\u56DE\u7B54\u3002\u9009\u54EA\u4E2A\u5224\u5B9A\u5668\uFF1F",
3097
- en: "As it works, mu faces many small decisions: how to handle a message, whether a step breaks your rules. A judge answers them. Which judge do you want?"
3327
+ zh: "mu \u5DE5\u4F5C\u4E2D\u7684\u5C0F\u5224\u65AD\u4EA4\u7ED9\u54EA\u4E2A\u5224\u5B9A\u5668\uFF1F",
3328
+ en: "Which judge should answer mu's small decisions?"
3098
3329
  }),
3099
3330
  choices
3100
3331
  );
@@ -3200,9 +3431,9 @@ var Setup = class {
3200
3431
  const backups = new Backups(deps.paths.home, deps.now());
3201
3432
  const saved = [];
3202
3433
  const notes = [];
3203
- const modelsPath = join8(agentDir2, "models.json");
3204
- const authPath2 = join8(agentDir2, "auth.json");
3205
- const settingsPath = join8(agentDir2, "settings.json");
3434
+ const modelsPath = join9(agentDir2, "models.json");
3435
+ const authPath2 = join9(agentDir2, "auth.json");
3436
+ const settingsPath = join9(agentDir2, "settings.json");
3206
3437
  try {
3207
3438
  if (target && model) {
3208
3439
  const { service, reach, key: key2 } = target;
@@ -3406,12 +3637,8 @@ var Setup = class {
3406
3637
  }) : this.t({ zh: "\u5224\u5B9A\u5668\uFF1AJev\u3002", en: " Judge: Jev." });
3407
3638
  this.io.say("");
3408
3639
  this.io.say(`${done}${judged}`);
3409
- this.io.say(
3410
- target ? this.t({
3411
- zh: "\u5728 mu \u91CC\u7528 /model \u6362\u6A21\u578B\uFF1B\u968F\u65F6\u53EF\u4EE5\u518D\u8FD0\u884C mu setup\u3002",
3412
- en: "Switch models inside mu with /model, and run mu setup again any time."
3413
- }) : this.t({ zh: "\u63A5\u4E0B\u6765\u5728 mu \u91CC\u7528 /login \u767B\u5F55\u3002", en: "Next, sign in with /login in mu." })
3414
- );
3640
+ if (!target)
3641
+ this.io.say(this.t({ zh: "\u63A5\u4E0B\u6765\u5728 mu \u91CC\u7528 /login \u767B\u5F55\u3002", en: "Next, sign in with /login in mu." }));
3415
3642
  }
3416
3643
  /**
3417
3644
  * On its own, a finished setup offers a session; on a first start the launcher goes on into one anyway. After a
@@ -3451,10 +3678,10 @@ async function runSetup(argv, deps) {
3451
3678
  var givenKey = process.env.MU_SETUP_KEY || void 0;
3452
3679
  delete process.env.MU_SETUP_KEY;
3453
3680
  var home = process.env.MU_SETUP_HOME || muHome();
3454
- var agentDir = muEnv("AGENT_DIR") || join9(home, "agent");
3455
- var envFile = process.env.MU_SETUP_ENV_FILE || join9(home, ".env");
3456
- var authPath = join9(agentDir, "auth.json");
3457
- var modelsStorePath = join9(agentDir, "models-store.json");
3681
+ var agentDir = muEnv("AGENT_DIR") || join10(home, "agent");
3682
+ var envFile = process.env.MU_SETUP_ENV_FILE || join10(home, ".env");
3683
+ var authPath = join10(agentDir, "auth.json");
3684
+ var modelsStorePath = join10(agentDir, "models-store.json");
3458
3685
  var givenEnv = { ...process.env };
3459
3686
  var PROXY_VARIABLES = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
3460
3687
  function useSessionProxy() {
@@ -3478,7 +3705,7 @@ async function main2() {
3478
3705
  const proxy = useSessionProxy();
3479
3706
  const runtime = await ModelRuntime.create({
3480
3707
  authPath,
3481
- modelsPath: join9(agentDir, "models.json"),
3708
+ modelsPath: join10(agentDir, "models.json"),
3482
3709
  modelsStorePath,
3483
3710
  refreshOnCreate: false
3484
3711
  });