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