mu-agent 0.2.0 → 0.2.1
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 +16 -0
- package/judge/dist/mude.js +152 -54
- package/judge/dist/setup.js +1409 -160
- package/kyrn/bin/mu.mjs +6 -0
- package/package.json +2 -2
package/judge/dist/setup.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __muCreateRequire } from "node:module"; const require = __muCreateRequire(import.meta.url);
|
|
2
2
|
|
|
3
3
|
// packages/kyrn-judge/src/setup/main.ts
|
|
4
|
-
import { constants } from "node:os";
|
|
5
|
-
import { join as
|
|
4
|
+
import { constants as constants2 } from "node:os";
|
|
5
|
+
import { join as join9 } from "node:path";
|
|
6
6
|
import {
|
|
7
7
|
applyHttpProxySettings,
|
|
8
8
|
configureHttpDispatcher,
|
|
@@ -51,6 +51,16 @@ function drop(lock) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// packages/kyrn-judge/src/mude/command.ts
|
|
55
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
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";
|
|
58
|
+
import { homedir as homedir4, release, totalmem as totalmem2 } from "node:os";
|
|
59
|
+
import { join as join7 } from "node:path";
|
|
60
|
+
|
|
61
|
+
// packages/kyrn-judge/src/mude/config.ts
|
|
62
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
63
|
+
|
|
54
64
|
// packages/kyrn-judge/src/naming.ts
|
|
55
65
|
import { existsSync } from "node:fs";
|
|
56
66
|
import { homedir } from "node:os";
|
|
@@ -66,42 +76,58 @@ function muHome(home2 = homedir()) {
|
|
|
66
76
|
return existsSync(current) || !existsSync(legacy) ? current : legacy;
|
|
67
77
|
}
|
|
68
78
|
|
|
69
|
-
// packages/kyrn-judge/src/
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
79
|
+
// packages/kyrn-judge/src/config.ts
|
|
80
|
+
var BUILT_IN_JUDGES = {
|
|
81
|
+
jev: { type: "jev" },
|
|
82
|
+
"jev-direct": { type: "typesafe" },
|
|
83
|
+
// OpenRouter serves Jev over the same System One protocol. The key is Jev's own, not the one for OpenRouter's
|
|
84
|
+
// chat models, so setting one never changes the other.
|
|
85
|
+
"jev-openrouter": {
|
|
86
|
+
type: "typesafe",
|
|
87
|
+
baseUrl: "https://openrouter.ai/api/v1/systemone",
|
|
88
|
+
model: "~typesafe/jev-latest",
|
|
89
|
+
apiKeyEnv: "MU_JUDGE_OPENROUTER_API_KEY"
|
|
90
|
+
},
|
|
91
|
+
"jev-gateway": { type: "gateway", model: "typesafe-ai/jev" },
|
|
92
|
+
// Jev on OpenCode Zen, with the key of OpenCode's models (OPENCODE_API_KEY, or `mu auth`). The free one is a
|
|
93
|
+
// limited-time offer of OpenCode's and needs no key at all. OpenCode does not train on what Jev is sent.
|
|
94
|
+
"jev-opencode": { type: "classifier", model: "opencode/jev-1.13" },
|
|
95
|
+
"jev-opencode-free": { type: "classifier", model: "opencode/jev-1.13-free" },
|
|
96
|
+
// Jev on Cloudflare Workers AI, with Cloudflare's key and account (CLOUDFLARE_API_KEY, CLOUDFLARE_ACCOUNT_ID).
|
|
97
|
+
"jev-cloudflare": { type: "classifier", model: "cloudflare-workers-ai/typesafe/jev" },
|
|
98
|
+
// Cloudflare's own System One classifiers. Not measured on mu's questions yet.
|
|
99
|
+
clef: { type: "classifier", model: "cloudflare-workers-ai/@cf/cloudflare/clef" },
|
|
100
|
+
"clef-flash": { type: "classifier", model: "cloudflare-workers-ai/@cf/cloudflare/clef-flash" },
|
|
101
|
+
// CLM-8B on this machine, where `clm-serve` listens by default. Its profile is not measured on mu's questions yet.
|
|
102
|
+
clm: { type: "clm" },
|
|
103
|
+
// Measured in kyrn/docs/03-local-judge.md: the base checkpoint classifies one text well, says "yes" to
|
|
104
|
+
// nearly every relational question, and cannot use a rubric or judge the request itself. Measured on
|
|
105
|
+
// Terminal-Bench 2.1 (Oct 2026): on tool output it is wrong or late. All 15 passages it withheld as injections were
|
|
106
|
+
// clean (directory listings, href lists, headers, JSON), its one admission drop was a makefile, and most of its
|
|
107
|
+
// calls on both failed or came too late. So screening tool output (`screen`) goes to the next tier, or to the
|
|
108
|
+
// rules. It also read a finished "Done. Created /app/meeting_scheduled.ics …" as a promise of more work, twice in one
|
|
109
|
+
// run (constraints-scheduling): 4 extra requests and the model searching / for test files. So whether a closing
|
|
110
|
+
// message promised more (`intent`, turn.continue) goes to the next tier too; `continuation.localPromised` asks it.
|
|
111
|
+
laya: {
|
|
112
|
+
type: "local",
|
|
113
|
+
profile: { capabilities: { relate: false, rate: false, meta: false, screen: false, intent: false } }
|
|
114
|
+
},
|
|
115
|
+
// mude, the decision model trained for mu's judgment layer, on this Mac (`mu judge mude install`, Apple silicon):
|
|
116
|
+
// the System One server that comes in its package, on 127.0.0.1:8790, with the key the install wrote to the .env.
|
|
117
|
+
// Trusted on every capability: on mu's own dev scenarios (an internal benchmark, not public yet) it did as well as Jev
|
|
118
|
+
// or better on relate, rate, meta, screening tool output (`tool.admission`) and promises (`turn.continue`), and on
|
|
119
|
+
// classify but for one check: a tool's description screened for injected instructions (`tool.description`), where
|
|
120
|
+
// Jev did better. Nothing measured it on injections in fetched content (`tool.injection`). So with a judge behind it,
|
|
121
|
+
// `mu judge mude` routes those two checks to that judge (`routes` in mu.json). It answers in about 0.1 s here.
|
|
122
|
+
mude: {
|
|
123
|
+
type: "typesafe",
|
|
124
|
+
baseUrl: "http://127.0.0.1:8790/v1/systemone",
|
|
125
|
+
model: "mude",
|
|
126
|
+
apiKeyEnv: "MU_JUDGE_MUDE_KEY",
|
|
127
|
+
timeoutMs: 4e3
|
|
128
|
+
},
|
|
129
|
+
mock: { type: "mock" }
|
|
130
|
+
};
|
|
105
131
|
|
|
106
132
|
// packages/kyrn-judge/src/setup/env-file.ts
|
|
107
133
|
var DEFINITION = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
|
|
@@ -360,6 +386,1161 @@ function writeEnvValue(path, name, value) {
|
|
|
360
386
|
writeFileAtomic(path, setEnvValue(readTextFile(path) ?? "", name, value));
|
|
361
387
|
}
|
|
362
388
|
|
|
389
|
+
// packages/kyrn-judge/src/mude/server.ts
|
|
390
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
391
|
+
import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
392
|
+
import { createServer } from "node:net";
|
|
393
|
+
import { totalmem } from "node:os";
|
|
394
|
+
import { join as join3 } from "node:path";
|
|
395
|
+
|
|
396
|
+
// packages/kyrn-judge/src/mude/manifest.ts
|
|
397
|
+
var MUDE_REPO = "qybaihe/mude-1.0-mlx-8bit";
|
|
398
|
+
var MUDE_REVISION = "269b71dbc4276770b5a021d7adbad100010473c8";
|
|
399
|
+
var MUDE_MODEL = "mude";
|
|
400
|
+
var FILES = [
|
|
401
|
+
[".gitattributes", 1570, "34448b82c17d60fec9b65b1f093c115ddbaadc04beb1b0140b6bfed2e012a930"],
|
|
402
|
+
["LICENSE", 1482, "793a9f63a9cdfd3436ca19a4a70e54d1acf9d639d84ba27129918284765f3a05"],
|
|
403
|
+
["LICENSES/Apache-2.0.txt", 11357, "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4"],
|
|
404
|
+
["MODEL_MANIFEST.json", 19177, "2d197005a61e693566e75946e034937ed13b192a150df583cfe2dde4f9f6ebcd"],
|
|
405
|
+
["NOTICE", 1899, "d57889a1687573fb059329fde16fc6d1feeadc683fc50aa592370b6a727bf42b"],
|
|
406
|
+
["README.md", 2356, "f86e44bd8573b306daafa86a3a4a261b430d99d5cd0df6bda223f187c5f56756"],
|
|
407
|
+
["chat_template.jinja", 7755, "273d8e0e683b885071fb17e08d71e5f2a5ddfb5309756181681de4f5a1822d80"],
|
|
408
|
+
["config.json", 2783, "4fba61002dea71507b2ea7d9910a70bb7df8f0a0639e2b2ffae2b978ddb9874b"],
|
|
409
|
+
["d3_engine.py", 4413, "086238942762df45047a32cd9a8e2bedd9b63167eb40be260c776de567979f53"],
|
|
410
|
+
["d3_fast.py", 22331, "68a1fb9a2c3bde9ac2763bf504decae592ca12722698fbde1b39a31262955535"],
|
|
411
|
+
["d3_format.py", 7008, "3badfbc70561bb3f5b8ec16a11d2c455b96ddb32caace0a8a54ca102eae1f3fa"],
|
|
412
|
+
["d3_kernels.py", 16129, "8f7534ca1e1846ebf7c4c73058002f3be4dbf0d5d426f0815150ae0cbe7677bd"],
|
|
413
|
+
["d3_mac.py", 10824, "5ab4f773a7361656a765b5be351e2cde519b3094c0b4a66123d01b2ac2f40372"],
|
|
414
|
+
["d3_mlx.py", 15876, "04890d713a240f7ff7d53ebb250b88ef85f9c04948f322a46006506f1e33e48d"],
|
|
415
|
+
["d3_runtime.py", 98229, "a77a0b84c1732a0fd6296e0daa44cd86130f67bfea5e9c87eee8c5a1df293def"],
|
|
416
|
+
["d3_server.py", 17705, "766a20a69101e36ab0535a1ec683379db422380a83091f0eccd7e82edb6a02de"],
|
|
417
|
+
["d3_server_mac.py", 1216, "0e351b255fac166d71489e95b68d761d1d91d10297dbcfc16a1c42a659b7b396"],
|
|
418
|
+
["decision_config.json", 5509, "19c62c4a8b0ff7bd1f82c05ef0d7af8df4b2ce7af2cfe19e3989e7d9f6c1c1cc"],
|
|
419
|
+
["merges.txt", 3353259, "a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d"],
|
|
420
|
+
["mlx-q8.safetensors", 1756751774, "9757238f601c7b2eea997069add5856ba5ed8370786bfffae0b9ac550c33f697"],
|
|
421
|
+
["mlx_weights.json", 293, "747ece4d690e493dda9637917b3d8b9c1296ccfa4a258f51c0ae0ad716a1d698"],
|
|
422
|
+
["modeling_d3.py", 12079, "f8f8db23ff58f1a990ffa24977d744a50e1b4446d02a64c22070967705b6dbef"],
|
|
423
|
+
["pipeline_d3.py", 2936, "4f9297c7c74e596e9b67df91a1e6823cfada48bf781336f141974ff6a2f1038e"],
|
|
424
|
+
["preprocessor_config.json", 390, "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516"],
|
|
425
|
+
["readout.safetensors", 2089040, "aeb18e2f9289aab0012e2059264326fb850aa8c5d8537975a891cf9655811674"],
|
|
426
|
+
["requirements-mac.txt", 289, "ed16543a58d7ffd2f19e17b38f8b9e46775548700d9d3eaca7a592309a8741b5"],
|
|
427
|
+
["requirements.txt", 723, "de9defd71ae4e559a7f5bdcc935b23716f9788e8755f2ba6db1ab6862bcda12e"],
|
|
428
|
+
["tokenizer.json", 12807982, "5f9e4d4901a92b997e463c1f46055088b6cca5ca61a6522d1b9f64c4bb81cb42"],
|
|
429
|
+
["tokenizer_config.json", 16709, "49e2b6e395f959f077f1e992b338919c0d4a9732fc6e613995e06557f843500c"],
|
|
430
|
+
["video_preprocessor_config.json", 385, "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13"],
|
|
431
|
+
["vocab.json", 6722759, "ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003"]
|
|
432
|
+
];
|
|
433
|
+
var MUDE_FILES = FILES.map(([path, size, sha256]) => ({ path, size, sha256 }));
|
|
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("/")}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// packages/kyrn-judge/src/mude/server.ts
|
|
440
|
+
var DEFAULT_PORT = 8790;
|
|
441
|
+
function mudeBaseUrl(port2) {
|
|
442
|
+
return `http://127.0.0.1:${port2}/v1/systemone`;
|
|
443
|
+
}
|
|
444
|
+
async function health(port2, timeoutMs = 2e3, fetcher = fetch) {
|
|
445
|
+
try {
|
|
446
|
+
const response = await fetcher(`http://127.0.0.1:${port2}/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
447
|
+
if (!response.ok) return void 0;
|
|
448
|
+
const body = await response.json();
|
|
449
|
+
return { status: String(body.status ?? ""), model: typeof body.model === "string" ? body.model : void 0 };
|
|
450
|
+
} catch {
|
|
451
|
+
return void 0;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function portFree(port2) {
|
|
455
|
+
return new Promise((resolve2) => {
|
|
456
|
+
const probe = createServer();
|
|
457
|
+
probe.once("error", () => resolve2(false));
|
|
458
|
+
probe.listen({ port: port2, host: "127.0.0.1", exclusive: true }, () => probe.close(() => resolve2(true)));
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
async function choosePort(preferred = DEFAULT_PORT) {
|
|
462
|
+
if (await portFree(preferred)) return preferred;
|
|
463
|
+
return new Promise((resolve2, reject) => {
|
|
464
|
+
const probe = createServer();
|
|
465
|
+
probe.once("error", reject);
|
|
466
|
+
probe.listen({ port: 0, host: "127.0.0.1" }, () => {
|
|
467
|
+
const address = probe.address();
|
|
468
|
+
const port2 = typeof address === "object" && address ? address.port : 0;
|
|
469
|
+
probe.close(() => resolve2(port2));
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function readPid(paths) {
|
|
474
|
+
try {
|
|
475
|
+
const pid = Number.parseInt(readFileSync2(paths.pid, "utf8").trim(), 10);
|
|
476
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
477
|
+
} catch {
|
|
478
|
+
return void 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function serverAlive(paths, pid = readPid(paths)) {
|
|
482
|
+
if (!pid) return false;
|
|
483
|
+
try {
|
|
484
|
+
process.kill(pid, 0);
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const command = execFileSync("/bin/ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
|
|
490
|
+
return command.includes("d3_server_mac.py") && command.includes(paths.model);
|
|
491
|
+
} catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function cacheGb(memoryBytes = totalmem()) {
|
|
496
|
+
return memoryBytes >= 16 * 1024 ** 3 ? "1" : "0.25";
|
|
497
|
+
}
|
|
498
|
+
function tail(file, lines = 12) {
|
|
499
|
+
try {
|
|
500
|
+
const text = readFileSync2(file, "utf8");
|
|
501
|
+
return text.split(/\r?\n/).filter(Boolean).slice(-lines).join("\n");
|
|
502
|
+
} catch {
|
|
503
|
+
return "";
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function startServer(paths, options) {
|
|
507
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
508
|
+
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
509
|
+
const running = readPid(paths);
|
|
510
|
+
if (running && serverAlive(paths, running)) {
|
|
511
|
+
if (options.wait !== false) await untilReady(paths, running, options.port, timeoutMs, sleep2);
|
|
512
|
+
return { pid: running, port: options.port, already: true };
|
|
513
|
+
}
|
|
514
|
+
if (!await portFree(options.port)) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Port ${options.port} on 127.0.0.1 is taken by another program; mu judge mude enable picks a free one`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
mkdirSync3(paths.root, { recursive: true });
|
|
520
|
+
rmSync(paths.log, { force: true });
|
|
521
|
+
const log = openSync(paths.log, "a", 384);
|
|
522
|
+
const child = spawn(
|
|
523
|
+
paths.python,
|
|
524
|
+
[
|
|
525
|
+
join3(paths.model, "d3_server_mac.py"),
|
|
526
|
+
"--model",
|
|
527
|
+
paths.model,
|
|
528
|
+
"--host",
|
|
529
|
+
"127.0.0.1",
|
|
530
|
+
"--port",
|
|
531
|
+
String(options.port),
|
|
532
|
+
"--name",
|
|
533
|
+
MUDE_MODEL,
|
|
534
|
+
"--verify",
|
|
535
|
+
"fast"
|
|
536
|
+
],
|
|
537
|
+
{
|
|
538
|
+
cwd: paths.model,
|
|
539
|
+
detached: true,
|
|
540
|
+
stdio: ["ignore", log, log],
|
|
541
|
+
env: {
|
|
542
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
543
|
+
HOME: process.env.HOME,
|
|
544
|
+
LANG: process.env.LANG ?? "en_US.UTF-8",
|
|
545
|
+
DECISION_API_KEY: options.key,
|
|
546
|
+
D3_MLX_CACHE_GB: cacheGb(),
|
|
547
|
+
// Everything it needs is in the package: nothing is fetched from the Hub at run time.
|
|
548
|
+
HF_HUB_OFFLINE: "1",
|
|
549
|
+
TRANSFORMERS_OFFLINE: "1",
|
|
550
|
+
PYTHONUNBUFFERED: "1"
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
closeSync(log);
|
|
555
|
+
const pid = child.pid;
|
|
556
|
+
if (!pid) throw new Error("mude's server could not be started");
|
|
557
|
+
child.unref();
|
|
558
|
+
writeFileSync2(paths.pid, `${pid}
|
|
559
|
+
`, { mode: 384 });
|
|
560
|
+
if (options.wait !== false) await untilReady(paths, pid, options.port, timeoutMs, sleep2);
|
|
561
|
+
return { pid, port: options.port, already: false };
|
|
562
|
+
}
|
|
563
|
+
async function untilReady(paths, pid, port2, timeoutMs, sleep2) {
|
|
564
|
+
const deadline = Date.now() + timeoutMs;
|
|
565
|
+
while (Date.now() < deadline) {
|
|
566
|
+
if ((await health(port2))?.status === "ready") return;
|
|
567
|
+
if (!serverAlive(paths, pid)) {
|
|
568
|
+
rmSync(paths.pid, { force: true });
|
|
569
|
+
throw new Error(`mude's server stopped while starting. Its log (${paths.log}) ends with:
|
|
570
|
+
${tail(paths.log)}`);
|
|
571
|
+
}
|
|
572
|
+
await sleep2(500);
|
|
573
|
+
}
|
|
574
|
+
throw new Error(`mude's server did not answer within ${Math.round(timeoutMs / 1e3)} s; see ${paths.log}`);
|
|
575
|
+
}
|
|
576
|
+
async function stopServer(paths, sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))) {
|
|
577
|
+
const pid = readPid(paths);
|
|
578
|
+
if (!pid || !serverAlive(paths, pid)) {
|
|
579
|
+
rmSync(paths.pid, { force: true });
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
process.kill(pid, "SIGTERM");
|
|
583
|
+
for (let waited = 0; waited < 5e3 && serverAlive(paths, pid); waited += 200) await sleep2(200);
|
|
584
|
+
if (serverAlive(paths, pid)) process.kill(pid, "SIGKILL");
|
|
585
|
+
rmSync(paths.pid, { force: true });
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
function serverMemory(pid) {
|
|
589
|
+
if (!pid) return void 0;
|
|
590
|
+
try {
|
|
591
|
+
const kb = Number.parseInt(
|
|
592
|
+
execFileSync("/bin/ps", ["-p", String(pid), "-o", "rss="], { encoding: "utf8" }).trim(),
|
|
593
|
+
10
|
|
594
|
+
);
|
|
595
|
+
return Number.isFinite(kb) ? kb * 1024 : void 0;
|
|
596
|
+
} catch {
|
|
597
|
+
return void 0;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// packages/kyrn-judge/src/mude/config.ts
|
|
602
|
+
var ROUTED_AWAY = ["tool.description", "tool.injection"];
|
|
603
|
+
var MUDE = "mude";
|
|
604
|
+
var KEY_VARIABLE = "MU_JUDGE_MUDE_KEY";
|
|
605
|
+
function readState(paths) {
|
|
606
|
+
try {
|
|
607
|
+
const value = JSON.parse(readFileSync3(paths.state, "utf8"));
|
|
608
|
+
return Number.isInteger(value.port) ? value : void 0;
|
|
609
|
+
} catch {
|
|
610
|
+
return void 0;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function writeState(paths, state) {
|
|
614
|
+
writeFileSync3(paths.state, `${JSON.stringify(state, null, 2)}
|
|
615
|
+
`, { mode: 384 });
|
|
616
|
+
}
|
|
617
|
+
function judgeSetting(files) {
|
|
618
|
+
const file = readJsonFile(judgeConfigPath(files.agentDir));
|
|
619
|
+
if (file.state === "unreadable") return { enabled: false, localOnly: false, fallback: [], unreadable: true };
|
|
620
|
+
const config = file.state === "ok" ? file.value : {};
|
|
621
|
+
const tiers = tiersOf(config);
|
|
622
|
+
const enabled = tiers[0] === MUDE && !judgeOff(config);
|
|
623
|
+
const fallback = tiers.filter((tier) => tier !== MUDE);
|
|
624
|
+
return { enabled, localOnly: enabled && fallback.length === 0, fallback };
|
|
625
|
+
}
|
|
626
|
+
function mudeInTiers(files) {
|
|
627
|
+
const file = readJsonFile(judgeConfigPath(files.agentDir));
|
|
628
|
+
return file.state === "ok" && tiersOf(file.value).includes(MUDE);
|
|
629
|
+
}
|
|
630
|
+
function readConfig(files) {
|
|
631
|
+
const path = judgeConfigPath(files.agentDir);
|
|
632
|
+
const file = readJsonFile(path);
|
|
633
|
+
if (file.state === "unreadable") {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`${path} is not plain JSON (comments, or a typo), so mu leaves it alone. Fix it, then run this again.`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return { path, config: file.state === "ok" ? file.value : {} };
|
|
639
|
+
}
|
|
640
|
+
function recordOf(value) {
|
|
641
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : {};
|
|
642
|
+
}
|
|
643
|
+
function judgesOf(config) {
|
|
644
|
+
return recordOf(config.judges);
|
|
645
|
+
}
|
|
646
|
+
function withSection(config, key2, value) {
|
|
647
|
+
if (Object.keys(value).length > 0) return { ...config, [key2]: value };
|
|
648
|
+
const { [key2]: _gone, ...rest } = config;
|
|
649
|
+
return rest;
|
|
650
|
+
}
|
|
651
|
+
var sameTiers = (a, b) => Array.isArray(a) && a.length === b.length && a.every((tier, index) => tier === b[index]);
|
|
652
|
+
function withoutOwnRoutes(config, state) {
|
|
653
|
+
const routes = recordOf(config.routes);
|
|
654
|
+
for (const [spec, tiers] of Object.entries(state?.wroteRoutes ?? {})) {
|
|
655
|
+
if (sameTiers(routes[spec], tiers)) delete routes[spec];
|
|
656
|
+
}
|
|
657
|
+
return withSection(config, "routes", routes);
|
|
658
|
+
}
|
|
659
|
+
function entryFor(port2) {
|
|
660
|
+
return { ...BUILT_IN_JUDGES[MUDE], baseUrl: mudeBaseUrl(port2) };
|
|
661
|
+
}
|
|
662
|
+
function enableMude(paths, files, options) {
|
|
663
|
+
const { path, config } = readConfig(files);
|
|
664
|
+
const state = readState(paths);
|
|
665
|
+
const current = tiersOf(config).filter((tier) => tier !== MUDE);
|
|
666
|
+
const before = current.length > 0 ? current : [...state?.previousTiers ?? ["jev"]];
|
|
667
|
+
const tiers = options.localOnly ? [MUDE] : [MUDE, ...before.filter((tier) => tier !== MUDE)];
|
|
668
|
+
const judgeWasOff = state?.judgeWasOff ?? judgeOff(config);
|
|
669
|
+
let next = withJudge(withTiers(withoutOwnRoutes(config, state), tiers), true);
|
|
670
|
+
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];
|
|
674
|
+
next = withSection(next, "judges", judges);
|
|
675
|
+
const fallback = tiers.slice(1);
|
|
676
|
+
const routes = recordOf(next.routes);
|
|
677
|
+
const wroteRoutes = {};
|
|
678
|
+
if (fallback.length > 0) {
|
|
679
|
+
for (const spec of ROUTED_AWAY) {
|
|
680
|
+
if (routes[spec] !== void 0) continue;
|
|
681
|
+
routes[spec] = [...fallback];
|
|
682
|
+
wroteRoutes[spec] = fallback;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
next = withSection(next, "routes", routes);
|
|
686
|
+
const backups = new Backups(files.home, files.now ?? /* @__PURE__ */ new Date());
|
|
687
|
+
backups.save(path);
|
|
688
|
+
writeJsonFile(path, next);
|
|
689
|
+
if (envFileValue(readText(files.envFile), KEY_VARIABLE) !== options.key) {
|
|
690
|
+
backups.save(files.envFile);
|
|
691
|
+
writeEnvValue(files.envFile, KEY_VARIABLE, options.key);
|
|
692
|
+
}
|
|
693
|
+
writeState(paths, {
|
|
694
|
+
port: options.port,
|
|
695
|
+
previousTiers: before,
|
|
696
|
+
judgeWasOff,
|
|
697
|
+
wroteEntry: entry || void 0,
|
|
698
|
+
wroteRoutes: Object.keys(wroteRoutes).length > 0 ? wroteRoutes : void 0
|
|
699
|
+
});
|
|
700
|
+
return tiers;
|
|
701
|
+
}
|
|
702
|
+
function disableMude(paths, files, options = {}) {
|
|
703
|
+
const { path, config } = readConfig(files);
|
|
704
|
+
const state = readState(paths);
|
|
705
|
+
const rest = tiersOf(config).filter((tier) => tier !== MUDE);
|
|
706
|
+
const tiers = rest.length > 0 ? rest : [...state?.previousTiers ?? ["jev"]];
|
|
707
|
+
let next = withTiers(withoutOwnRoutes(config, state), tiers);
|
|
708
|
+
if (state?.judgeWasOff) next = withJudge(next, false);
|
|
709
|
+
if (options.removeEntry && state?.wroteEntry) {
|
|
710
|
+
const judges = judgesOf(next);
|
|
711
|
+
delete judges[MUDE];
|
|
712
|
+
next = withSection(next, "judges", judges);
|
|
713
|
+
}
|
|
714
|
+
new Backups(files.home, files.now ?? /* @__PURE__ */ new Date()).save(path);
|
|
715
|
+
writeJsonFile(path, next);
|
|
716
|
+
if (state) {
|
|
717
|
+
writeState(paths, {
|
|
718
|
+
...state,
|
|
719
|
+
judgeWasOff: void 0,
|
|
720
|
+
wroteRoutes: void 0,
|
|
721
|
+
wroteEntry: options.removeEntry ? void 0 : state.wroteEntry
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
return tiers;
|
|
725
|
+
}
|
|
726
|
+
function readText(path) {
|
|
727
|
+
try {
|
|
728
|
+
return readFileSync3(path, "utf8");
|
|
729
|
+
} catch {
|
|
730
|
+
return "";
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// packages/kyrn-judge/src/mude/download.ts
|
|
735
|
+
import { createHash } from "node:crypto";
|
|
736
|
+
import {
|
|
737
|
+
createReadStream,
|
|
738
|
+
existsSync as existsSync4,
|
|
739
|
+
mkdirSync as mkdirSync4,
|
|
740
|
+
readFileSync as readFileSync4,
|
|
741
|
+
renameSync as renameSync2,
|
|
742
|
+
rmSync as rmSync2,
|
|
743
|
+
statSync as statSync3,
|
|
744
|
+
truncateSync,
|
|
745
|
+
writeFileSync as writeFileSync4
|
|
746
|
+
} from "node:fs";
|
|
747
|
+
import { open } from "node:fs/promises";
|
|
748
|
+
import { homedir as homedir2 } from "node:os";
|
|
749
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
750
|
+
import { pipeline } from "node:stream/promises";
|
|
751
|
+
var CHECKPOINT_BYTES = 32 * 1024 * 1024;
|
|
752
|
+
var RETRY_WAITS = [2e3, 5e3, 1e4, 2e4, 3e4];
|
|
753
|
+
function huggingFaceToken(env = process.env, home2 = homedir2()) {
|
|
754
|
+
const direct = env.HF_TOKEN?.trim();
|
|
755
|
+
if (direct) return direct;
|
|
756
|
+
const files = [
|
|
757
|
+
env.HF_TOKEN_PATH,
|
|
758
|
+
env.HF_HOME ? join4(env.HF_HOME, "token") : void 0,
|
|
759
|
+
join4(env.XDG_CACHE_HOME || join4(home2, ".cache"), "huggingface", "token")
|
|
760
|
+
];
|
|
761
|
+
for (const file of files) {
|
|
762
|
+
if (!file) continue;
|
|
763
|
+
try {
|
|
764
|
+
const token = readFileSync4(file, "utf8").trim();
|
|
765
|
+
if (token) return token;
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return void 0;
|
|
770
|
+
}
|
|
771
|
+
async function sha256File(path) {
|
|
772
|
+
const hash = createHash("sha256");
|
|
773
|
+
await pipeline(createReadStream(path), hash);
|
|
774
|
+
return hash.digest("hex");
|
|
775
|
+
}
|
|
776
|
+
function readVerified(dir) {
|
|
777
|
+
try {
|
|
778
|
+
const value = JSON.parse(readFileSync4(join4(dir, ".mu-verified.json"), "utf8"));
|
|
779
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
780
|
+
} catch {
|
|
781
|
+
return {};
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function knownGood(dir, file, verified) {
|
|
785
|
+
const seen = verified[file.path];
|
|
786
|
+
if (!seen || seen.sha256 !== file.sha256) return false;
|
|
787
|
+
try {
|
|
788
|
+
const stats = statSync3(join4(dir, file.path));
|
|
789
|
+
return stats.size === file.size && stats.mtimeMs === seen.mtimeMs;
|
|
790
|
+
} catch {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function remember(dir, verified, file) {
|
|
795
|
+
const stats = statSync3(join4(dir, file.path));
|
|
796
|
+
verified[file.path] = { size: stats.size, mtimeMs: stats.mtimeMs, sha256: file.sha256 };
|
|
797
|
+
writeFileSync4(join4(dir, ".mu-verified.json"), `${JSON.stringify(verified, null, 1)}
|
|
798
|
+
`);
|
|
799
|
+
}
|
|
800
|
+
function missingFiles(dir, files) {
|
|
801
|
+
const verified = readVerified(dir);
|
|
802
|
+
return files.filter((file) => !knownGood(dir, file, verified));
|
|
803
|
+
}
|
|
804
|
+
function downloadedBytes(dir, files) {
|
|
805
|
+
const verified = readVerified(dir);
|
|
806
|
+
let bytes = 0;
|
|
807
|
+
for (const file of files) {
|
|
808
|
+
if (knownGood(dir, file, verified)) {
|
|
809
|
+
bytes += file.size;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const target = join4(dir, file.path);
|
|
813
|
+
bytes += Math.min(file.size, Math.max(sizeOf(target), sizeOf(`${target}.part`)));
|
|
814
|
+
}
|
|
815
|
+
return bytes;
|
|
816
|
+
}
|
|
817
|
+
function sizeOf(path) {
|
|
818
|
+
try {
|
|
819
|
+
return statSync3(path).size;
|
|
820
|
+
} catch {
|
|
821
|
+
return 0;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
var DownloadError = class extends Error {
|
|
825
|
+
kind;
|
|
826
|
+
constructor(message, kind) {
|
|
827
|
+
super(message);
|
|
828
|
+
this.name = "DownloadError";
|
|
829
|
+
this.kind = kind;
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
async function downloadFiles(options) {
|
|
833
|
+
const { dir, files } = options;
|
|
834
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
835
|
+
mkdirSync4(dir, { recursive: true });
|
|
836
|
+
const verified = readVerified(dir);
|
|
837
|
+
let settled = 0;
|
|
838
|
+
for (const file of files) {
|
|
839
|
+
if (knownGood(dir, file, verified)) {
|
|
840
|
+
settled += file.size;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const target = join4(dir, file.path);
|
|
844
|
+
mkdirSync4(dirname2(target), { recursive: true });
|
|
845
|
+
if (existsSync4(target) && statSync3(target).size === file.size) {
|
|
846
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled, total });
|
|
847
|
+
if (await sha256File(target) === file.sha256) {
|
|
848
|
+
remember(dir, verified, file);
|
|
849
|
+
settled += file.size;
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
rmSync2(target, { force: true });
|
|
853
|
+
}
|
|
854
|
+
const part = `${target}.part`;
|
|
855
|
+
const onBytes = (bytes) => options.onProgress?.({ file: file.path, checking: false, done: settled + bytes, total });
|
|
856
|
+
let resumed = await fetchFile(options, file, part, onBytes);
|
|
857
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
|
|
858
|
+
let actual = await sha256File(part);
|
|
859
|
+
if (actual !== file.sha256 && resumed) {
|
|
860
|
+
dropPart(part);
|
|
861
|
+
resumed = await fetchFile(options, file, part, onBytes);
|
|
862
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
|
|
863
|
+
actual = await sha256File(part);
|
|
864
|
+
}
|
|
865
|
+
if (actual !== file.sha256) {
|
|
866
|
+
dropPart(part);
|
|
867
|
+
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`,
|
|
869
|
+
"integrity"
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
renameSync2(part, target);
|
|
873
|
+
rmSync2(savedPath(part), { force: true });
|
|
874
|
+
remember(dir, verified, file);
|
|
875
|
+
settled += file.size;
|
|
876
|
+
}
|
|
877
|
+
options.onProgress?.({ file: "", checking: false, done: total, total });
|
|
878
|
+
}
|
|
879
|
+
var savedPath = (part) => `${part}.saved`;
|
|
880
|
+
function dropPart(part) {
|
|
881
|
+
rmSync2(part, { force: true });
|
|
882
|
+
rmSync2(savedPath(part), { force: true });
|
|
883
|
+
}
|
|
884
|
+
function resumeFrom(part, size, checkpointBytes = CHECKPOINT_BYTES) {
|
|
885
|
+
const have = Math.min(sizeOf(part), size);
|
|
886
|
+
let saved;
|
|
887
|
+
try {
|
|
888
|
+
const value = Number.parseInt(readFileSync4(savedPath(part), "utf8").trim(), 10);
|
|
889
|
+
if (Number.isSafeInteger(value) && value >= 0) saved = value;
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
return saved === void 0 ? Math.max(0, have - checkpointBytes) : Math.min(saved, have);
|
|
893
|
+
}
|
|
894
|
+
function writeSaved(part, bytes) {
|
|
895
|
+
const path = savedPath(part);
|
|
896
|
+
writeFileSync4(`${path}.tmp`, `${bytes}
|
|
897
|
+
`);
|
|
898
|
+
renameSync2(`${path}.tmp`, path);
|
|
899
|
+
}
|
|
900
|
+
async function fetchFile(options, file, part, onBytes) {
|
|
901
|
+
const attempts = options.attempts ?? 8;
|
|
902
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
903
|
+
let resumed = false;
|
|
904
|
+
let failures = 0;
|
|
905
|
+
let lastError;
|
|
906
|
+
for (; ; ) {
|
|
907
|
+
if (options.signal?.aborted) throw new DownloadError("The download was stopped", "aborted");
|
|
908
|
+
const before = resumeFrom(part, file.size, options.checkpointBytes);
|
|
909
|
+
try {
|
|
910
|
+
resumed = await fetchOnce(options, file, part, onBytes) || resumed;
|
|
911
|
+
return resumed;
|
|
912
|
+
} catch (error) {
|
|
913
|
+
if (error instanceof DownloadError && error.kind !== "network") throw error;
|
|
914
|
+
lastError = error;
|
|
915
|
+
failures = resumeFrom(part, file.size, options.checkpointBytes) > before ? 1 : failures + 1;
|
|
916
|
+
if (failures >= attempts) break;
|
|
917
|
+
await sleep2(RETRY_WAITS[Math.min(failures, RETRY_WAITS.length) - 1]);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const reason = lastError instanceof Error ? lastError.message : String(lastError);
|
|
921
|
+
throw new DownloadError(
|
|
922
|
+
`${file.path} could not be downloaded (${reason}); run the install again to go on from here`,
|
|
923
|
+
"network"
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
async function fetchOnce(options, file, part, onBytes) {
|
|
927
|
+
const checkpointBytes = options.checkpointBytes ?? CHECKPOINT_BYTES;
|
|
928
|
+
let have = resumeFrom(part, file.size, checkpointBytes);
|
|
929
|
+
if (have === file.size) {
|
|
930
|
+
if (sizeOf(part) > file.size) truncateSync(part, file.size);
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
const controller = new AbortController();
|
|
934
|
+
const stallMs = options.stallMs ?? 6e4;
|
|
935
|
+
let stall = setTimeout(() => controller.abort(new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
|
|
936
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
937
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
938
|
+
let handle;
|
|
939
|
+
let written = 0;
|
|
940
|
+
let synced = 0;
|
|
941
|
+
try {
|
|
942
|
+
const headers = {};
|
|
943
|
+
if (have > 0) headers.Range = `bytes=${have}-`;
|
|
944
|
+
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
945
|
+
const response = await (options.fetch ?? fetch)(options.url(file.path), {
|
|
946
|
+
headers,
|
|
947
|
+
redirect: "follow",
|
|
948
|
+
signal: controller.signal
|
|
949
|
+
});
|
|
950
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
951
|
+
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`,
|
|
953
|
+
"access"
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (response.status === 416) {
|
|
957
|
+
dropPart(part);
|
|
958
|
+
throw new Error("HTTP 416");
|
|
959
|
+
}
|
|
960
|
+
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
|
|
961
|
+
const resumed = response.status === 206 && have > 0;
|
|
962
|
+
if (!resumed) have = 0;
|
|
963
|
+
mkdirSync4(dirname2(part), { recursive: true });
|
|
964
|
+
handle = await open(part, existsSync4(part) ? "r+" : "w");
|
|
965
|
+
await handle.truncate(have);
|
|
966
|
+
writeSaved(part, have);
|
|
967
|
+
written = have;
|
|
968
|
+
synced = have;
|
|
969
|
+
onBytes(written);
|
|
970
|
+
const reader = response.body.getReader();
|
|
971
|
+
for (; ; ) {
|
|
972
|
+
const { done, value: chunk } = await reader.read();
|
|
973
|
+
if (done) break;
|
|
974
|
+
clearTimeout(stall);
|
|
975
|
+
stall = setTimeout(() => controller.abort(new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
|
|
976
|
+
if (written + chunk.length > file.size) throw new Error(`got more than the ${file.size} bytes of the file`);
|
|
977
|
+
await handle.write(chunk, 0, chunk.length, written);
|
|
978
|
+
written += chunk.length;
|
|
979
|
+
if (written - synced >= checkpointBytes) {
|
|
980
|
+
await handle.sync();
|
|
981
|
+
synced = written;
|
|
982
|
+
writeSaved(part, synced);
|
|
983
|
+
}
|
|
984
|
+
onBytes(written);
|
|
985
|
+
}
|
|
986
|
+
if (written !== file.size) throw new Error(`got ${written} of ${file.size} bytes`);
|
|
987
|
+
return resumed;
|
|
988
|
+
} catch (error) {
|
|
989
|
+
if (options.signal?.aborted) throw new DownloadError("The download was stopped", "aborted");
|
|
990
|
+
throw error;
|
|
991
|
+
} finally {
|
|
992
|
+
clearTimeout(stall);
|
|
993
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
994
|
+
if (handle) {
|
|
995
|
+
try {
|
|
996
|
+
await handle.sync();
|
|
997
|
+
if (written > synced) writeSaved(part, written);
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
await handle.close();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// packages/kyrn-judge/src/mude/paths.ts
|
|
1006
|
+
import { join as join5 } from "node:path";
|
|
1007
|
+
function mudePaths(muHome2) {
|
|
1008
|
+
const root = join5(muHome2, "local-judge", "mude");
|
|
1009
|
+
const model = join5(root, "model", MUDE_REVISION);
|
|
1010
|
+
const venv = join5(root, "venv");
|
|
1011
|
+
return {
|
|
1012
|
+
root,
|
|
1013
|
+
model,
|
|
1014
|
+
verified: join5(model, ".mu-verified.json"),
|
|
1015
|
+
venv,
|
|
1016
|
+
python: join5(venv, "bin", "python"),
|
|
1017
|
+
venvLock: join5(venv, ".mu-lock-sha256"),
|
|
1018
|
+
tools: join5(root, "tools"),
|
|
1019
|
+
key: join5(root, "key"),
|
|
1020
|
+
pid: join5(root, "server.pid"),
|
|
1021
|
+
log: join5(root, "server.log"),
|
|
1022
|
+
state: join5(root, "state.json"),
|
|
1023
|
+
install: join5(root, "install.json")
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// packages/kyrn-judge/src/mude/python.ts
|
|
1028
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1029
|
+
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";
|
|
1031
|
+
import { homedir as homedir3 } from "node:os";
|
|
1032
|
+
import { delimiter, join as join6 } from "node:path";
|
|
1033
|
+
var PYTHON_VERSION = "3.13";
|
|
1034
|
+
var UV_RELEASE = {
|
|
1035
|
+
version: "0.13.1",
|
|
1036
|
+
url: "https://github.com/astral-sh/uv/releases/download/0.13.1/uv-aarch64-apple-darwin.tar.gz",
|
|
1037
|
+
sha256: "cfe454573412ff7380d15fc7dae5997ab53ffce72fce0a78e91ddbc84001acd2"
|
|
1038
|
+
};
|
|
1039
|
+
function executable(path) {
|
|
1040
|
+
try {
|
|
1041
|
+
accessSync(path, constants.X_OK);
|
|
1042
|
+
return true;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function findUv(paths, env = process.env, home2 = homedir3()) {
|
|
1048
|
+
const dirs = [
|
|
1049
|
+
...(env.PATH ?? "").split(delimiter),
|
|
1050
|
+
join6(home2, ".local", "bin"),
|
|
1051
|
+
join6(home2, ".cargo", "bin"),
|
|
1052
|
+
"/opt/homebrew/bin",
|
|
1053
|
+
"/usr/local/bin",
|
|
1054
|
+
paths.tools
|
|
1055
|
+
];
|
|
1056
|
+
for (const dir of dirs) {
|
|
1057
|
+
if (dir && executable(join6(dir, "uv"))) return join6(dir, "uv");
|
|
1058
|
+
}
|
|
1059
|
+
return void 0;
|
|
1060
|
+
}
|
|
1061
|
+
function run(command, args, options = {}) {
|
|
1062
|
+
return new Promise((resolve2) => {
|
|
1063
|
+
const child = spawn2(command, args, { env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
1064
|
+
const lines = [];
|
|
1065
|
+
let pending = "";
|
|
1066
|
+
const take = (chunk) => {
|
|
1067
|
+
pending += chunk.toString("utf8");
|
|
1068
|
+
const parts = pending.split(/\r?\n/);
|
|
1069
|
+
pending = parts.pop() ?? "";
|
|
1070
|
+
for (const line of parts) {
|
|
1071
|
+
if (!line.trim()) continue;
|
|
1072
|
+
lines.push(line);
|
|
1073
|
+
if (lines.length > 40) lines.shift();
|
|
1074
|
+
options.onLine?.(line);
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
child.stdout.on("data", take);
|
|
1078
|
+
child.stderr.on("data", take);
|
|
1079
|
+
child.once("error", (error) => resolve2({ code: null, output: error.message }));
|
|
1080
|
+
child.once("close", (code) => {
|
|
1081
|
+
if (pending.trim()) lines.push(pending);
|
|
1082
|
+
resolve2({ code, output: lines.slice(-12).join("\n") });
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
async function downloadUv(paths, options = {}) {
|
|
1087
|
+
mkdirSync5(paths.tools, { recursive: true });
|
|
1088
|
+
const archive = "uv.tar.gz";
|
|
1089
|
+
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());
|
|
1093
|
+
const actual = createHash2("sha256").update(bytes).digest("hex");
|
|
1094
|
+
if (actual !== UV_RELEASE.sha256)
|
|
1095
|
+
throw new Error("The downloaded uv does not match its pinned sha256; nothing was installed");
|
|
1096
|
+
writeFileSync5(join6(paths.tools, archive), bytes);
|
|
1097
|
+
const unpacked = await run("/usr/bin/tar", [
|
|
1098
|
+
"-xzf",
|
|
1099
|
+
join6(paths.tools, archive),
|
|
1100
|
+
"-C",
|
|
1101
|
+
paths.tools,
|
|
1102
|
+
"--strip-components",
|
|
1103
|
+
"1"
|
|
1104
|
+
]);
|
|
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");
|
|
1109
|
+
}
|
|
1110
|
+
function lockDigest(lockFile) {
|
|
1111
|
+
return createHash2("sha256").update(readFileSync5(lockFile)).digest("hex");
|
|
1112
|
+
}
|
|
1113
|
+
function venvReady(paths, lockFile) {
|
|
1114
|
+
if (!existsSync5(paths.python)) return false;
|
|
1115
|
+
try {
|
|
1116
|
+
return readFileSync5(paths.venvLock, "utf8").trim() === lockDigest(lockFile);
|
|
1117
|
+
} catch {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async function ensureVenv(paths, lockFile, uv, options = {}) {
|
|
1122
|
+
if (venvReady(paths, lockFile)) return;
|
|
1123
|
+
rmSync3(paths.venv, { recursive: true, force: true });
|
|
1124
|
+
const env = { ...options.env ?? process.env, UV_NO_CONFIG: "1", VIRTUAL_ENV: void 0 };
|
|
1125
|
+
const made = await run(uv, ["venv", "--python", PYTHON_VERSION, "--no-project", "--quiet", paths.venv], {
|
|
1126
|
+
env,
|
|
1127
|
+
onLine: options.onLine
|
|
1128
|
+
});
|
|
1129
|
+
if (made.code !== 0) throw new Error(`The Python environment could not be made (uv venv):
|
|
1130
|
+
${made.output}`);
|
|
1131
|
+
const installed2 = await run(
|
|
1132
|
+
uv,
|
|
1133
|
+
[
|
|
1134
|
+
"pip",
|
|
1135
|
+
"install",
|
|
1136
|
+
"--python",
|
|
1137
|
+
paths.python,
|
|
1138
|
+
"--require-hashes",
|
|
1139
|
+
"--no-deps",
|
|
1140
|
+
"--index-url",
|
|
1141
|
+
"https://pypi.org/simple",
|
|
1142
|
+
"-r",
|
|
1143
|
+
lockFile
|
|
1144
|
+
],
|
|
1145
|
+
{ env, onLine: options.onLine }
|
|
1146
|
+
);
|
|
1147
|
+
if (installed2.code !== 0)
|
|
1148
|
+
throw new Error(`mude's Python packages could not be installed (uv pip install):
|
|
1149
|
+
${installed2.output}`);
|
|
1150
|
+
writeFileSync5(paths.venvLock, `${lockDigest(lockFile)}
|
|
1151
|
+
`);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// packages/kyrn-judge/src/mude/command.ts
|
|
1155
|
+
var USAGE = `mu judge mude: mude 1.0, mu's own decision model optimized for coding, as the judge on this Mac
|
|
1156
|
+
(Apple silicon, macOS 14 or later)
|
|
1157
|
+
|
|
1158
|
+
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
|
|
1160
|
+
mu judge mude enable [--local-only | --with-fallback]
|
|
1161
|
+
make it the judge again, or change what happens when it can't answer
|
|
1162
|
+
mu judge mude disable back to the judges from before, and stop its server
|
|
1163
|
+
mu judge mude start | stop start or stop its server (about 4 GB of memory while it runs)
|
|
1164
|
+
mu judge mude status what is installed, running and enabled
|
|
1165
|
+
mu judge mude uninstall disable it and delete the model and its Python environment
|
|
1166
|
+
|
|
1167
|
+
What mude 1.0 cannot answer, because its server is not running or it is unsure, goes to the judges that were there
|
|
1168
|
+
before it (Jev by default): those are hosted, so what they read leaves this machine. With --local-only, it is the
|
|
1169
|
+
only judge, and such a decision takes its fallback instead. Add --json for one JSON object per line.
|
|
1170
|
+
`;
|
|
1171
|
+
function jsonOut(write) {
|
|
1172
|
+
const emit = (value) => write(`${JSON.stringify(value)}
|
|
1173
|
+
`);
|
|
1174
|
+
let last = 0;
|
|
1175
|
+
return {
|
|
1176
|
+
step: (step, message) => emit({ type: "step", step, message }),
|
|
1177
|
+
progress: (event) => {
|
|
1178
|
+
const now = Date.now();
|
|
1179
|
+
if (now - last < 250 && event.done < event.total && !event.checking) return;
|
|
1180
|
+
last = now;
|
|
1181
|
+
emit({ type: "progress", ...event });
|
|
1182
|
+
},
|
|
1183
|
+
line: (text) => emit({ type: "log", line: text }),
|
|
1184
|
+
status: (status2) => emit({ type: "status", ...status2 }),
|
|
1185
|
+
error: (message, kind) => emit({ type: "error", message, kind })
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function humanOut(write, tty) {
|
|
1189
|
+
let shown = -1;
|
|
1190
|
+
return {
|
|
1191
|
+
step: (_step, message) => {
|
|
1192
|
+
if (shown >= 0 && tty) write("\n");
|
|
1193
|
+
shown = -1;
|
|
1194
|
+
write(`${message}
|
|
1195
|
+
`);
|
|
1196
|
+
},
|
|
1197
|
+
progress: ({ done, total, checking }) => {
|
|
1198
|
+
const percent = total > 0 ? Math.floor(done / total * 100) : 100;
|
|
1199
|
+
if (tty) {
|
|
1200
|
+
write(
|
|
1201
|
+
`\r ${percent}% ${(done / 1e9).toFixed(2)} of ${(total / 1e9).toFixed(2)} GB${checking ? " (checking)" : ""} `
|
|
1202
|
+
);
|
|
1203
|
+
shown = percent;
|
|
1204
|
+
} else if (percent >= shown + 10 || percent === 100) {
|
|
1205
|
+
shown = percent - percent % 10;
|
|
1206
|
+
write(` ${percent}%
|
|
1207
|
+
`);
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
line: (text) => write(` ${text}
|
|
1211
|
+
`),
|
|
1212
|
+
status: (status2) => write(describe(status2)),
|
|
1213
|
+
error: (message) => {
|
|
1214
|
+
if (shown >= 0 && tty) write("\n");
|
|
1215
|
+
shown = -1;
|
|
1216
|
+
write(`${message}
|
|
1217
|
+
`);
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function describe(status2) {
|
|
1222
|
+
const lines = [];
|
|
1223
|
+
if (!status2.supported) {
|
|
1224
|
+
lines.push(`mude 1.0 can't run here: ${status2.problems.join("; ")}`);
|
|
1225
|
+
return `${lines.join("\n")}
|
|
1226
|
+
`;
|
|
1227
|
+
}
|
|
1228
|
+
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"}`
|
|
1230
|
+
);
|
|
1231
|
+
if (status2.installed && !status2.pythonReady) lines.push("python not set up: mu judge mude install");
|
|
1232
|
+
lines.push(
|
|
1233
|
+
`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
|
+
);
|
|
1235
|
+
if (status2.enabled) {
|
|
1236
|
+
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)`
|
|
1238
|
+
);
|
|
1239
|
+
} else {
|
|
1240
|
+
lines.push(`judge ${status2.installed ? "not enabled: mu judge mude enable" : "not enabled"}`);
|
|
1241
|
+
}
|
|
1242
|
+
lines.push(`folder ${status2.folder}`);
|
|
1243
|
+
return `${lines.join("\n")}
|
|
1244
|
+
`;
|
|
1245
|
+
}
|
|
1246
|
+
function problems(options = {}) {
|
|
1247
|
+
const platform = options.platform ?? process.platform;
|
|
1248
|
+
if (platform !== "darwin") return ["it needs a Mac with Apple silicon (an M1 or newer)"];
|
|
1249
|
+
const out = [];
|
|
1250
|
+
const arm64 = options.arm64 ?? appleSilicon();
|
|
1251
|
+
if (!arm64) out.push("it needs Apple silicon (an M1 or newer); this Mac has an Intel processor");
|
|
1252
|
+
const darwinMajor = options.darwinMajor ?? Number.parseInt(release().split(".")[0] ?? "0", 10);
|
|
1253
|
+
if (darwinMajor < 23) out.push("it needs macOS 14 or later");
|
|
1254
|
+
const memory = options.memoryBytes ?? totalmem2();
|
|
1255
|
+
if (memory < 8 * 1024 ** 3)
|
|
1256
|
+
out.push(
|
|
1257
|
+
`it needs 8 GB of memory or more (it uses about 4 GB); this Mac has ${Math.round(memory / 1024 ** 3)} GB`
|
|
1258
|
+
);
|
|
1259
|
+
return out;
|
|
1260
|
+
}
|
|
1261
|
+
function appleSilicon() {
|
|
1262
|
+
if (process.arch === "arm64") return true;
|
|
1263
|
+
try {
|
|
1264
|
+
return execFileSync2("/usr/sbin/sysctl", ["-n", "hw.optional.arm64"], { encoding: "utf8" }).trim() === "1";
|
|
1265
|
+
} catch {
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function freeBytes(path) {
|
|
1270
|
+
let dir = path;
|
|
1271
|
+
while (!existsSync6(dir)) {
|
|
1272
|
+
const parent = join7(dir, "..");
|
|
1273
|
+
if (parent === dir) return void 0;
|
|
1274
|
+
dir = parent;
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
const stats = statfsSync(dir);
|
|
1278
|
+
return stats.bavail * stats.bsize;
|
|
1279
|
+
} catch {
|
|
1280
|
+
return void 0;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
var gb = (bytes) => (bytes / 1e9).toFixed(1);
|
|
1284
|
+
function installLocalOnly(ctx, args) {
|
|
1285
|
+
if (args.includes("--local-only")) return true;
|
|
1286
|
+
if (args.includes("--with-fallback")) return false;
|
|
1287
|
+
try {
|
|
1288
|
+
const value = JSON.parse(readFileSync6(ctx.paths.install, "utf8"));
|
|
1289
|
+
return typeof value === "object" && value !== null && "localOnly" in value && value.localOnly === true;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function installed(ctx) {
|
|
1295
|
+
return missingFiles(ctx.paths.model, MUDE_FILES).length === 0;
|
|
1296
|
+
}
|
|
1297
|
+
function key(ctx) {
|
|
1298
|
+
try {
|
|
1299
|
+
const existing = readFileSync6(ctx.paths.key, "utf8").trim();
|
|
1300
|
+
if (existing) return existing;
|
|
1301
|
+
} catch {
|
|
1302
|
+
}
|
|
1303
|
+
mkdirSync6(ctx.paths.root, { recursive: true });
|
|
1304
|
+
const made = randomBytes(32).toString("hex");
|
|
1305
|
+
writeFileSync6(ctx.paths.key, `${made}
|
|
1306
|
+
`, { mode: 384 });
|
|
1307
|
+
return made;
|
|
1308
|
+
}
|
|
1309
|
+
async function port(ctx) {
|
|
1310
|
+
const previous = readState(ctx.paths)?.port;
|
|
1311
|
+
if (previous && (serverAlive(ctx.paths) || await portFree(previous))) return previous;
|
|
1312
|
+
return choosePort(DEFAULT_PORT);
|
|
1313
|
+
}
|
|
1314
|
+
async function status(ctx) {
|
|
1315
|
+
const found = problems();
|
|
1316
|
+
const state = readState(ctx.paths);
|
|
1317
|
+
const at = state?.port ?? DEFAULT_PORT;
|
|
1318
|
+
const pid = readPid(ctx.paths);
|
|
1319
|
+
const running = serverAlive(ctx.paths, pid);
|
|
1320
|
+
const ready = running && (await health(at))?.status === "ready";
|
|
1321
|
+
const setting = judgeSetting(ctx.files);
|
|
1322
|
+
return {
|
|
1323
|
+
supported: found.length === 0,
|
|
1324
|
+
problems: found,
|
|
1325
|
+
installed: installed(ctx),
|
|
1326
|
+
downloadedBytes: downloadedBytes(ctx.paths.model, MUDE_FILES),
|
|
1327
|
+
pythonReady: venvReady(ctx.paths, ctx.lockFile),
|
|
1328
|
+
running,
|
|
1329
|
+
ready,
|
|
1330
|
+
port: at,
|
|
1331
|
+
pid: running ? pid : void 0,
|
|
1332
|
+
memoryBytes: running ? serverMemory(pid) : void 0,
|
|
1333
|
+
enabled: setting.enabled,
|
|
1334
|
+
localOnly: setting.localOnly,
|
|
1335
|
+
fallback: setting.fallback,
|
|
1336
|
+
model: `${MUDE_REPO}@${MUDE_REVISION}`,
|
|
1337
|
+
folder: ctx.paths.root,
|
|
1338
|
+
log: ctx.paths.log
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
async function ensureInstalled(ctx, out) {
|
|
1342
|
+
const missing = missingFiles(ctx.paths.model, MUDE_FILES);
|
|
1343
|
+
if (missing.length > 0) {
|
|
1344
|
+
const need = missing.reduce((sum, file) => sum + file.size, 0);
|
|
1345
|
+
const free = freeBytes(ctx.paths.root);
|
|
1346
|
+
if (free !== void 0 && free < need + 2e9) {
|
|
1347
|
+
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`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
const already = downloadedBytes(ctx.paths.model, MUDE_FILES);
|
|
1352
|
+
out.step(
|
|
1353
|
+
"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`
|
|
1355
|
+
);
|
|
1356
|
+
try {
|
|
1357
|
+
await downloadFiles({
|
|
1358
|
+
dir: ctx.paths.model,
|
|
1359
|
+
files: MUDE_FILES,
|
|
1360
|
+
url: (path) => mudeFileUrl(path, ctx.env.HF_ENDPOINT || void 0),
|
|
1361
|
+
token: huggingFaceToken(ctx.env),
|
|
1362
|
+
onProgress: (event) => out.progress(event),
|
|
1363
|
+
signal: ctx.signal
|
|
1364
|
+
});
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
if (error instanceof DownloadError && error.kind === "aborted") {
|
|
1367
|
+
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`,
|
|
1369
|
+
"aborted"
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
throw error;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
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 });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function requireInstalled(ctx) {
|
|
1383
|
+
if (!installed(ctx) || !venvReady(ctx.paths, ctx.lockFile)) {
|
|
1384
|
+
throw new Error("mude 1.0 is not installed here yet: mu judge mude install");
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async function start(ctx, out, wait = true) {
|
|
1388
|
+
const at = await port(ctx);
|
|
1389
|
+
out.step(
|
|
1390
|
+
"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)`
|
|
1392
|
+
);
|
|
1393
|
+
await startServer(ctx.paths, { port: at, key: key(ctx), wait });
|
|
1394
|
+
return at;
|
|
1395
|
+
}
|
|
1396
|
+
function enable(ctx, out, at, localOnly) {
|
|
1397
|
+
const tiers = enableMude(ctx.paths, ctx.files, { port: at, key: key(ctx), localOnly });
|
|
1398
|
+
out.step(
|
|
1399
|
+
"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.`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
async function main(argv, ctx, write, tty = false) {
|
|
1404
|
+
const json = argv.includes("--json");
|
|
1405
|
+
const args = argv.filter((arg) => arg !== "--json");
|
|
1406
|
+
const out = json ? jsonOut(write) : humanOut(write, tty);
|
|
1407
|
+
const command = args[0] ?? "status";
|
|
1408
|
+
const flag = (name) => args.includes(name);
|
|
1409
|
+
try {
|
|
1410
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
1411
|
+
write(USAGE);
|
|
1412
|
+
return 0;
|
|
1413
|
+
}
|
|
1414
|
+
const unknown = args.slice(1).find((arg) => !["--local-only", "--with-fallback", "--no-wait", "--yes"].includes(arg));
|
|
1415
|
+
if (unknown) {
|
|
1416
|
+
out.error(`Unknown option for mu judge mude ${command}: ${unknown} (mu judge mude help)`, "usage");
|
|
1417
|
+
return 2;
|
|
1418
|
+
}
|
|
1419
|
+
if (command === "status") {
|
|
1420
|
+
out.status(await status(ctx));
|
|
1421
|
+
return 0;
|
|
1422
|
+
}
|
|
1423
|
+
const found = problems();
|
|
1424
|
+
if (found.length > 0 && command !== "stop" && command !== "disable" && command !== "uninstall") {
|
|
1425
|
+
out.error(`mude 1.0 can't run on this machine: ${found.join("; ")}.`, "unsupported");
|
|
1426
|
+
return 1;
|
|
1427
|
+
}
|
|
1428
|
+
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 });
|
|
1438
|
+
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);
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
case "disable": {
|
|
1449
|
+
if (!mudeInTiers(ctx.files)) {
|
|
1450
|
+
const stopped2 = await stopServer(ctx.paths);
|
|
1451
|
+
out.step("disable", `mude 1.0 was not the judge${stopped2 ? "; its server is stopped" : ""}.`);
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
const tiers = disableMude(ctx.paths, ctx.files);
|
|
1455
|
+
const stopped = await stopServer(ctx.paths);
|
|
1456
|
+
out.step(
|
|
1457
|
+
"disable",
|
|
1458
|
+
`mude 1.0 is no longer the judge: ${tiers.join(" > ")} again${stopped ? ", and its server is stopped" : ""}.`
|
|
1459
|
+
);
|
|
1460
|
+
break;
|
|
1461
|
+
}
|
|
1462
|
+
case "start": {
|
|
1463
|
+
requireInstalled(ctx);
|
|
1464
|
+
await start(ctx, out, !flag("--no-wait"));
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
case "stop": {
|
|
1468
|
+
const stopped = await stopServer(ctx.paths);
|
|
1469
|
+
out.step("stop", stopped ? "The mude 1.0 server is stopped." : "The mude 1.0 server was not running.");
|
|
1470
|
+
break;
|
|
1471
|
+
}
|
|
1472
|
+
case "uninstall": {
|
|
1473
|
+
if (mudeInTiers(ctx.files) || readState(ctx.paths)?.wroteEntry) {
|
|
1474
|
+
const tiers = disableMude(ctx.paths, ctx.files, { removeEntry: true });
|
|
1475
|
+
out.step("disable", `mude 1.0 is no longer the judge: ${tiers.join(" > ")} again.`);
|
|
1476
|
+
}
|
|
1477
|
+
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.`);
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
default:
|
|
1483
|
+
out.error(`Unknown command: mu judge mude ${command}
|
|
1484
|
+
|
|
1485
|
+
${USAGE}`, "usage");
|
|
1486
|
+
return 2;
|
|
1487
|
+
}
|
|
1488
|
+
out.status(await status(ctx));
|
|
1489
|
+
return 0;
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
const kind = error instanceof DownloadError ? error.kind : void 0;
|
|
1492
|
+
out.error(error instanceof Error ? error.message : String(error), kind);
|
|
1493
|
+
return 1;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
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");
|
|
1499
|
+
return {
|
|
1500
|
+
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"),
|
|
1503
|
+
env
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// packages/kyrn-judge/src/redact.ts
|
|
1508
|
+
var REDACTED = "[redacted]";
|
|
1509
|
+
var SHAPES = [
|
|
1510
|
+
// A private key, the whole block, or to the end when the text was cut inside it.
|
|
1511
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g,
|
|
1512
|
+
// OpenAI, Anthropic, OpenRouter, DeepSeek and the like.
|
|
1513
|
+
/\bsk-[A-Za-z0-9_-]{20,}/g,
|
|
1514
|
+
/\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{22,})/g,
|
|
1515
|
+
/\bglpat-[A-Za-z0-9_-]{20,}/g,
|
|
1516
|
+
/\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
|
|
1517
|
+
/\bAIza[0-9A-Za-z_-]{35}/g,
|
|
1518
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
|
|
1519
|
+
/\b[rs]k_(?:live|test)_[0-9A-Za-z]{16,}/g,
|
|
1520
|
+
/\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
1521
|
+
/\bhf_[A-Za-z0-9]{30,}/g,
|
|
1522
|
+
// A JSON web token.
|
|
1523
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g
|
|
1524
|
+
];
|
|
1525
|
+
var BEARER = /\b(Bearer|Basic)(\s+)(?=[A-Za-z._~+/=-]*[0-9])[A-Za-z0-9._~+/=-]{12,}/g;
|
|
1526
|
+
var URL_PASSWORD = /\b([a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:)[^\s@/]+(@)/gi;
|
|
1527
|
+
var KEY_WORD = "(?:SECRET|TOKEN|PASSWD|PASSWORD|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY)";
|
|
1528
|
+
var ENV_VALUE = new RegExp(`\\b([A-Z0-9_]*${KEY_WORD}[A-Z0-9_]*=)("[^"\\n]{8,}"|'[^'\\n]{8,}'|[^\\s"';&|]{8,})`, "g");
|
|
1529
|
+
var QUOTED_VALUE = new RegExp(
|
|
1530
|
+
`(["']?[A-Za-z0-9_-]*${KEY_WORD.replaceAll("_?", "[_-]?")}[A-Za-z0-9_-]*["']?\\s*[:=]\\s*)(["'])[^"'\\s]{8,}\\2`,
|
|
1531
|
+
"gi"
|
|
1532
|
+
);
|
|
1533
|
+
var LINE_VALUE = new RegExp(
|
|
1534
|
+
`^(\\s*[A-Za-z0-9_.-]*${KEY_WORD.replaceAll("_?", "[_.-]?")}[A-Za-z0-9_.-]*\\s*[:=][ \\t]*)[^\\s#"']{8,}[ \\t]*$`,
|
|
1535
|
+
"gim"
|
|
1536
|
+
);
|
|
1537
|
+
function redactSecrets(text, known = []) {
|
|
1538
|
+
let clean = text;
|
|
1539
|
+
for (const secret of known) if (clean.includes(secret)) clean = clean.split(secret).join(REDACTED);
|
|
1540
|
+
for (const shape of SHAPES) clean = clean.replace(shape, REDACTED);
|
|
1541
|
+
return clean.replace(BEARER, `$1$2${REDACTED}`).replace(URL_PASSWORD, `$1${REDACTED}$2`).replace(ENV_VALUE, `$1${REDACTED}`).replace(QUOTED_VALUE, `$1$2${REDACTED}$2`).replace(LINE_VALUE, `$1${REDACTED}`);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
363
1544
|
// packages/kyrn-judge/src/setup/language.ts
|
|
364
1545
|
function languageOf(value) {
|
|
365
1546
|
const tag = value?.trim().split(/[.@]/)[0];
|
|
@@ -396,7 +1577,7 @@ function piAccess(runtime, agentDir2) {
|
|
|
396
1577
|
if (!found) return void 0;
|
|
397
1578
|
return found.type === "oauth" ? "oauth" : "api_key";
|
|
398
1579
|
},
|
|
399
|
-
storeKey: async (provider,
|
|
1580
|
+
storeKey: async (provider, key2) => {
|
|
400
1581
|
await runtime.refresh({ allowNetwork: false, providers: [provider] });
|
|
401
1582
|
if (!runtime.getProvider(provider)) {
|
|
402
1583
|
throw new Error(
|
|
@@ -411,7 +1592,7 @@ function piAccess(runtime, agentDir2) {
|
|
|
411
1592
|
if (asked || prompt.type !== "secret")
|
|
412
1593
|
throw new Error(`the sign-in for ${provider} asked for more than a key`);
|
|
413
1594
|
asked = true;
|
|
414
|
-
return
|
|
1595
|
+
return key2;
|
|
415
1596
|
},
|
|
416
1597
|
notify: () => {
|
|
417
1598
|
}
|
|
@@ -520,30 +1701,30 @@ async function readAll(input = process.stdin) {
|
|
|
520
1701
|
}
|
|
521
1702
|
|
|
522
1703
|
// packages/kyrn-judge/src/setup/wizard.ts
|
|
523
|
-
import { mkdirSync as
|
|
524
|
-
import { join as
|
|
1704
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
1705
|
+
import { join as join8 } from "node:path";
|
|
525
1706
|
|
|
526
1707
|
// packages/kyrn-judge/src/setup/check.ts
|
|
527
1708
|
var CHECK_TIMEOUT_MS = 1e4;
|
|
528
|
-
function checkRequest(kind, baseUrl,
|
|
1709
|
+
function checkRequest(kind, baseUrl, key2) {
|
|
529
1710
|
const base = baseUrl.replace(/\/+$/, "");
|
|
530
1711
|
const headers = { accept: "application/json" };
|
|
531
1712
|
switch (kind) {
|
|
532
1713
|
case "anthropic-models":
|
|
533
1714
|
headers["anthropic-version"] = "2023-06-01";
|
|
534
|
-
if (
|
|
535
|
-
headers.authorization = `Bearer ${
|
|
1715
|
+
if (key2?.includes("sk-ant-oat")) {
|
|
1716
|
+
headers.authorization = `Bearer ${key2}`;
|
|
536
1717
|
headers["anthropic-beta"] = "oauth-2025-04-20";
|
|
537
|
-
} else if (
|
|
1718
|
+
} else if (key2) headers["x-api-key"] = key2;
|
|
538
1719
|
return { url: `${base}/v1/models?limit=1000`, headers };
|
|
539
1720
|
case "google-models":
|
|
540
|
-
if (
|
|
1721
|
+
if (key2) headers["x-goog-api-key"] = key2;
|
|
541
1722
|
return { url: `${base}/models?pageSize=1000`, headers };
|
|
542
1723
|
case "openrouter-key":
|
|
543
|
-
if (
|
|
1724
|
+
if (key2) headers.authorization = `Bearer ${key2}`;
|
|
544
1725
|
return { url: `${base}/key`, headers };
|
|
545
1726
|
default:
|
|
546
|
-
if (
|
|
1727
|
+
if (key2) headers.authorization = `Bearer ${key2}`;
|
|
547
1728
|
return { url: `${base}/models`, headers };
|
|
548
1729
|
}
|
|
549
1730
|
}
|
|
@@ -568,23 +1749,23 @@ function messageOf(body) {
|
|
|
568
1749
|
}
|
|
569
1750
|
return body.trimStart().startsWith("<") ? "" : body;
|
|
570
1751
|
}
|
|
571
|
-
function safeMessage(text,
|
|
572
|
-
const known =
|
|
1752
|
+
function safeMessage(text, key2) {
|
|
1753
|
+
const known = key2 ? [key2] : [];
|
|
573
1754
|
const clean = redactSecrets(text, known).replace(/\*{2,}[A-Za-z0-9_-]{0,12}/g, "\u2026").replace(/\s+/g, " ").trim();
|
|
574
1755
|
return clean.length > 160 ? `${clean.slice(0, 157)}...` : clean;
|
|
575
1756
|
}
|
|
576
|
-
function classifyAnswer(
|
|
1757
|
+
function classifyAnswer(status2, body) {
|
|
577
1758
|
const text = `${messageOf(body)} ${body}`;
|
|
578
|
-
if (
|
|
579
|
-
if (
|
|
580
|
-
if (
|
|
581
|
-
if (
|
|
582
|
-
if (
|
|
1759
|
+
if (status2 === 401) return "key";
|
|
1760
|
+
if (status2 === 402) return "balance";
|
|
1761
|
+
if (status2 === 404) return "address";
|
|
1762
|
+
if (status2 >= 500) return "server";
|
|
1763
|
+
if (status2 === 400 || status2 === 403 || status2 === 429) {
|
|
583
1764
|
if (BALANCE.test(text)) return "balance";
|
|
584
1765
|
if (REGION.test(text)) return "region";
|
|
585
1766
|
if (WRONG_KEY.test(text)) return "key";
|
|
586
|
-
if (
|
|
587
|
-
if (
|
|
1767
|
+
if (status2 === 429) return "rate";
|
|
1768
|
+
if (status2 === 403) return PERMISSION.test(text) ? "permission" : "refused";
|
|
588
1769
|
}
|
|
589
1770
|
return "other";
|
|
590
1771
|
}
|
|
@@ -630,19 +1811,19 @@ function listedModels(kind, body) {
|
|
|
630
1811
|
async function checkKey({
|
|
631
1812
|
kind,
|
|
632
1813
|
baseUrl,
|
|
633
|
-
key,
|
|
634
|
-
fetch,
|
|
1814
|
+
key: key2,
|
|
1815
|
+
fetch: fetch2,
|
|
635
1816
|
timeoutMs = CHECK_TIMEOUT_MS
|
|
636
1817
|
}) {
|
|
637
1818
|
if (kind === "opencode-key") {
|
|
638
|
-
const refused = await opencodeRefuses(baseUrl,
|
|
1819
|
+
const refused = await opencodeRefuses(baseUrl, key2, fetch2, timeoutMs);
|
|
639
1820
|
if (refused) return { ok: false, problem: refused };
|
|
640
1821
|
}
|
|
641
|
-
const request = checkRequest(kind, baseUrl,
|
|
1822
|
+
const request = checkRequest(kind, baseUrl, key2);
|
|
642
1823
|
const url = request.url.split("?")[0];
|
|
643
1824
|
let response;
|
|
644
1825
|
try {
|
|
645
|
-
response = await
|
|
1826
|
+
response = await fetch2(request.url, { headers: request.headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
646
1827
|
} catch (error) {
|
|
647
1828
|
return { ok: false, problem: { kind: classifyFailure(error), url } };
|
|
648
1829
|
}
|
|
@@ -658,7 +1839,7 @@ async function checkKey({
|
|
|
658
1839
|
kind: classifyAnswer(response.status, text),
|
|
659
1840
|
url,
|
|
660
1841
|
status: response.status,
|
|
661
|
-
said: safeMessage(messageOf(text),
|
|
1842
|
+
said: safeMessage(messageOf(text), key2) || void 0
|
|
662
1843
|
}
|
|
663
1844
|
};
|
|
664
1845
|
}
|
|
@@ -722,13 +1903,13 @@ function explainProblem(problem, context, language) {
|
|
|
722
1903
|
return zh ? `${service} \u62D2\u7EDD\u4E86\u8FD9\u6B21\u68C0\u67E5\uFF08HTTP ${problem.status ?? "?"}\uFF09\u3002${said}` : `${service} rejected the check (HTTP ${problem.status ?? "?"}).${said}`;
|
|
723
1904
|
}
|
|
724
1905
|
}
|
|
725
|
-
async function opencodeRefuses(baseUrl,
|
|
1906
|
+
async function opencodeRefuses(baseUrl, key2, fetch2, timeoutMs) {
|
|
726
1907
|
const url = `${baseUrl.replace(/\/+$/, "")}/systemone`;
|
|
727
1908
|
let response;
|
|
728
1909
|
try {
|
|
729
|
-
response = await
|
|
1910
|
+
response = await fetch2(url, {
|
|
730
1911
|
method: "POST",
|
|
731
|
-
headers: { "content-type": "application/json", ...
|
|
1912
|
+
headers: { "content-type": "application/json", ...key2 ? { authorization: `Bearer ${key2}` } : {} },
|
|
732
1913
|
body: JSON.stringify({
|
|
733
1914
|
model: "jev-1.13-free",
|
|
734
1915
|
state: "mu setup is checking a key.",
|
|
@@ -745,7 +1926,7 @@ async function opencodeRefuses(baseUrl, key, fetch, timeoutMs) {
|
|
|
745
1926
|
kind: classifyAnswer(response.status, text),
|
|
746
1927
|
url,
|
|
747
1928
|
status: response.status,
|
|
748
|
-
said: safeMessage(messageOf(text),
|
|
1929
|
+
said: safeMessage(messageOf(text), key2) || void 0
|
|
749
1930
|
};
|
|
750
1931
|
}
|
|
751
1932
|
|
|
@@ -939,15 +2120,15 @@ function serviceById(id) {
|
|
|
939
2120
|
return SETUP_SERVICES.find((service) => service.id === id);
|
|
940
2121
|
}
|
|
941
2122
|
function cleanKey(raw) {
|
|
942
|
-
let
|
|
943
|
-
if (
|
|
944
|
-
return
|
|
2123
|
+
let key2 = raw.trim();
|
|
2124
|
+
if (key2.length >= 2 && (key2[0] === '"' || key2[0] === "'") && key2.at(-1) === key2[0]) key2 = key2.slice(1, -1).trim();
|
|
2125
|
+
return key2;
|
|
945
2126
|
}
|
|
946
|
-
function isClaudeSubscriptionToken(
|
|
947
|
-
return cleanKey(
|
|
2127
|
+
function isClaudeSubscriptionToken(key2) {
|
|
2128
|
+
return cleanKey(key2).startsWith("sk-ant-oat");
|
|
948
2129
|
}
|
|
949
|
-
function servicesForKey(
|
|
950
|
-
const clean = cleanKey(
|
|
2130
|
+
function servicesForKey(key2) {
|
|
2131
|
+
const clean = cleanKey(key2);
|
|
951
2132
|
return clean ? SETUP_SERVICES.filter((service) => service.keyShape?.test(clean)) : [];
|
|
952
2133
|
}
|
|
953
2134
|
function checkKindOf(service, api) {
|
|
@@ -1060,11 +2241,11 @@ function parseSetupArgs(argv) {
|
|
|
1060
2241
|
return { error: { zh: "--api \u53EA\u80FD\u662F openai \u6216 anthropic", en: "--api must be openai or anthropic" } };
|
|
1061
2242
|
}
|
|
1062
2243
|
const judge = values.get("--judge");
|
|
1063
|
-
if (judge !== void 0 && !["jev", "opencode", "free", "model", "off", "none"].includes(judge)) {
|
|
2244
|
+
if (judge !== void 0 && !["jev", "opencode", "free", "model", "mude", "off", "none"].includes(judge)) {
|
|
1064
2245
|
return {
|
|
1065
2246
|
error: {
|
|
1066
|
-
zh: "--judge \u53EA\u80FD\u662F jev\u3001opencode\u3001free\u3001model\u3001off \u6216 none",
|
|
1067
|
-
en: "--judge must be jev, opencode, free, model, off or none"
|
|
2247
|
+
zh: "--judge \u53EA\u80FD\u662F jev\u3001opencode\u3001free\u3001model\u3001mude\u3001off \u6216 none",
|
|
2248
|
+
en: "--judge must be jev, opencode, free, model, mude, off or none"
|
|
1068
2249
|
}
|
|
1069
2250
|
};
|
|
1070
2251
|
}
|
|
@@ -1111,6 +2292,7 @@ function setupUsage(language) {
|
|
|
1111
2292
|
" --judge opencode OpenCode Zen \u4E0A\u7684 Jev\uFF0C\u7528 OpenCode \u7684\u5BC6\u94A5\uFF08--service opencode \u6216 OPENCODE_API_KEY\uFF09",
|
|
1112
2293
|
" --judge free OpenCode Zen \u4E0A\u9650\u65F6\u514D\u8D39\u7684 Jev\uFF0C\u4E0D\u7528\u5BC6\u94A5\uFF08\u6CA1\u6709\u5BC6\u94A5\u65F6\u672C\u6765\u5C31\u7528\u5B83\uFF09",
|
|
1113
2294
|
" --judge model \u7528\u521A\u8BBE\u7F6E\u7684\u6A21\u578B\u5F53\u5224\u5B9A\u5668\uFF08\u66F4\u6162\uFF0C\u6BCF\u6B21\u5224\u65AD\u90FD\u82B1 token\uFF09",
|
|
2295
|
+
" --judge mude \u5728\u8FD9\u53F0 Mac \u4E0A\u88C5\u597D mude 1.0\uFF08mu \u81EA\u5DF1\u7684\u51B3\u7B56\u6A21\u578B\uFF0C\u4E0B\u8F7D 1.8 GB\uFF09\u5F53\u5224\u5B9A\u5668\uFF0CJev \u5728\u5B83\u540E\u9762",
|
|
1114
2296
|
" --judge off \u4E0D\u7528\u5224\u5B9A\u5668\uFF1A\u6240\u6709\u5224\u5B9A\u70B9\u5173\u95ED\uFF0C\u4EC0\u4E48\u90FD\u4E0D\u53D1\u7ED9\u5224\u5B9A\u5668",
|
|
1115
2297
|
" --judge none \u4E0D\u6539\u5224\u5B9A\u5668\uFF1B\u4E0D\u5199 --judge \u65F6\u4E5F\u4E0D\u6539",
|
|
1116
2298
|
" --api, --name \u670D\u52A1 other \u7684\u63A5\u53E3\uFF08openai \u6216 anthropic\uFF09\u548C\u5B83\u5728 mu \u91CC\u7684\u540D\u5B57",
|
|
@@ -1137,6 +2319,8 @@ function setupUsage(language) {
|
|
|
1137
2319
|
" --judge opencode use Jev on OpenCode Zen with an OpenCode key (--service opencode or OPENCODE_API_KEY)",
|
|
1138
2320
|
" --judge free use the free Jev on OpenCode Zen (limited time, no key; the default while no key is set)",
|
|
1139
2321
|
" --judge model use the model you just set up as the judge (slower, and every verdict costs tokens)",
|
|
2322
|
+
" --judge mude install mude 1.0, mu's own decision model, on this Mac (a 1.8 GB download) as the judge,",
|
|
2323
|
+
" with Jev behind it",
|
|
1140
2324
|
" --judge off no judge: every decision point is off, and nothing is sent to a judge",
|
|
1141
2325
|
" --judge none leave the judge unchanged (the same as omitting --judge)",
|
|
1142
2326
|
" --api, --name for the service `other`: its API (openai or anthropic) and its name in mu",
|
|
@@ -1161,13 +2345,13 @@ function providerIdFor(baseUrl) {
|
|
|
1161
2345
|
} catch {
|
|
1162
2346
|
return "custom";
|
|
1163
2347
|
}
|
|
1164
|
-
const [name,
|
|
2348
|
+
const [name, port2] = host.split(/:(?=\d+$)/);
|
|
1165
2349
|
if (name === "localhost" || /^\d+(\.\d+){3}$/.test(name) || name.startsWith("[")) {
|
|
1166
|
-
return
|
|
2350
|
+
return port2 ? `local-${port2}` : "local";
|
|
1167
2351
|
}
|
|
1168
2352
|
const parts = name.split(".").filter((part) => part !== "api" && part !== "www");
|
|
1169
|
-
const
|
|
1170
|
-
return
|
|
2353
|
+
const main3 = parts.length >= 2 ? parts[parts.length - 2] : parts[0] ?? "";
|
|
2354
|
+
return main3.replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "custom";
|
|
1171
2355
|
}
|
|
1172
2356
|
function hostOf2(url) {
|
|
1173
2357
|
try {
|
|
@@ -1257,16 +2441,16 @@ var Setup = class {
|
|
|
1257
2441
|
for (; ; ) {
|
|
1258
2442
|
const answer = await this.io.secret(question);
|
|
1259
2443
|
if (answer === void 0) throw this.cancelled();
|
|
1260
|
-
const
|
|
1261
|
-
if (
|
|
2444
|
+
const key2 = cleanKey(answer);
|
|
2445
|
+
if (key2 || allowEmpty) return key2;
|
|
1262
2446
|
}
|
|
1263
2447
|
}
|
|
1264
2448
|
async pasteKey() {
|
|
1265
2449
|
for (; ; ) {
|
|
1266
|
-
const
|
|
2450
|
+
const key2 = await this.secret(
|
|
1267
2451
|
this.t({ zh: "\u7C98\u8D34 API \u5BC6\u94A5\uFF08\u4E0D\u4F1A\u663E\u793A\u51FA\u6765\uFF09\uFF1A", en: "Paste the API key (it won't be shown): " })
|
|
1268
2452
|
);
|
|
1269
|
-
if (!isClaudeSubscriptionToken(
|
|
2453
|
+
if (!isClaudeSubscriptionToken(key2)) return key2;
|
|
1270
2454
|
this.io.warn(this.t(CLAUDE_TOKEN));
|
|
1271
2455
|
}
|
|
1272
2456
|
}
|
|
@@ -1293,7 +2477,7 @@ var Setup = class {
|
|
|
1293
2477
|
async steps() {
|
|
1294
2478
|
const { args, deps } = this;
|
|
1295
2479
|
if (args.firstRun && !await this.firstStart()) return SETUP_EXIT.done;
|
|
1296
|
-
let
|
|
2480
|
+
let key2 = deps.givenKey ?? args.words[0];
|
|
1297
2481
|
if (args.keyStdin) {
|
|
1298
2482
|
if (deps.stdinTerminal) {
|
|
1299
2483
|
throw this.fail(
|
|
@@ -1305,11 +2489,11 @@ var Setup = class {
|
|
|
1305
2489
|
);
|
|
1306
2490
|
}
|
|
1307
2491
|
const [modelKey, jevKey] = (await deps.readStdin()).split(/\r?\n/).map((line) => cleanKey(line)).filter(Boolean);
|
|
1308
|
-
|
|
2492
|
+
key2 = modelKey;
|
|
1309
2493
|
this.jevFromStdin = jevKey;
|
|
1310
2494
|
}
|
|
1311
|
-
if (
|
|
1312
|
-
if (this.scripted &&
|
|
2495
|
+
if (key2 !== void 0 && isClaudeSubscriptionToken(key2)) throw this.fail(CLAUDE_TOKEN, SETUP_EXIT.usage);
|
|
2496
|
+
if (this.scripted && key2 === void 0 && args.service === void 0) {
|
|
1313
2497
|
throw this.fail(
|
|
1314
2498
|
deps.interactive ? { zh: "\u6CA1\u6709\u5BC6\u94A5\uFF0C\u4E5F\u6CA1\u6709 --service", en: "no key or --service given" } : {
|
|
1315
2499
|
zh: "\u95EE\u9898\u8981\u5728\u7EC8\u7AEF\u91CC\u56DE\u7B54\u3002\u811A\u672C\u91CC\u7528\uFF1Amu setup --service <id> --key-stdin\uFF08\u89C1 mu setup --help\uFF09",
|
|
@@ -1326,18 +2510,41 @@ var Setup = class {
|
|
|
1326
2510
|
})
|
|
1327
2511
|
);
|
|
1328
2512
|
}
|
|
1329
|
-
const
|
|
1330
|
-
if (
|
|
1331
|
-
const
|
|
1332
|
-
|
|
2513
|
+
const start2 = await this.start(key2 === void 0 ? void 0 : cleanKey(key2));
|
|
2514
|
+
if (start2 === "subscription") {
|
|
2515
|
+
const judge2 = await this.judge(void 0);
|
|
2516
|
+
const code2 = await this.write(void 0, void 0, judge2);
|
|
2517
|
+
if (code2 !== SETUP_EXIT.done) return code2;
|
|
2518
|
+
const installed3 = await this.installMude(judge2);
|
|
2519
|
+
return installed3 === SETUP_EXIT.done ? this.offerSession(true) : installed3;
|
|
1333
2520
|
}
|
|
1334
|
-
const target = await this.checked(
|
|
2521
|
+
const target = await this.checked(start2.service, start2.key);
|
|
1335
2522
|
this.modelKey = target.key;
|
|
1336
2523
|
const model = await this.model(target);
|
|
1337
2524
|
await this.conflicts(target);
|
|
1338
2525
|
const judge = await this.judge({ provider: target.reach.provider, model });
|
|
1339
2526
|
const code = await this.write(target, model, judge);
|
|
1340
|
-
|
|
2527
|
+
if (code !== SETUP_EXIT.done) return code;
|
|
2528
|
+
const installed2 = await this.installMude(judge);
|
|
2529
|
+
return installed2 === SETUP_EXIT.done ? this.offerSession() : installed2;
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* mude, when it was picked: installed now that the files name Jev, which it goes in front of. One that does not get
|
|
2533
|
+
* installed (no connection, stopped) leaves Jev as the judge, and `mu judge mude install` goes on from where it got.
|
|
2534
|
+
*/
|
|
2535
|
+
async installMude(judge) {
|
|
2536
|
+
const mude = this.deps.mude;
|
|
2537
|
+
if (judge?.kind !== "mude" || !mude) return SETUP_EXIT.done;
|
|
2538
|
+
this.io.say("");
|
|
2539
|
+
const code = await mude.install();
|
|
2540
|
+
if (code === 0) return SETUP_EXIT.done;
|
|
2541
|
+
this.io.warn(
|
|
2542
|
+
this.t({
|
|
2543
|
+
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."
|
|
2545
|
+
})
|
|
2546
|
+
);
|
|
2547
|
+
return this.scripted ? SETUP_EXIT.failed : SETUP_EXIT.done;
|
|
1341
2548
|
}
|
|
1342
2549
|
/** The launcher's question on a first start. A no is remembered: the question is never asked again. */
|
|
1343
2550
|
async firstStart() {
|
|
@@ -1350,9 +2557,9 @@ var Setup = class {
|
|
|
1350
2557
|
);
|
|
1351
2558
|
if (yes) return true;
|
|
1352
2559
|
try {
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
2560
|
+
mkdirSync7(this.deps.paths.agentDir, { recursive: true });
|
|
2561
|
+
writeFileSync7(
|
|
2562
|
+
join8(this.deps.paths.agentDir, SETUP_DECLINED_FILE),
|
|
1356
2563
|
"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"
|
|
1357
2564
|
);
|
|
1358
2565
|
} catch {
|
|
@@ -1368,10 +2575,10 @@ var Setup = class {
|
|
|
1368
2575
|
// -----------------------------------------------------------------------------------------------------------
|
|
1369
2576
|
// Steps 1 and 2: how a model is reached, and which service a key belongs to
|
|
1370
2577
|
// -----------------------------------------------------------------------------------------------------------
|
|
1371
|
-
async start(
|
|
2578
|
+
async start(key2) {
|
|
1372
2579
|
const named2 = this.args.service === void 0 ? void 0 : serviceById(this.args.service);
|
|
1373
|
-
if (named2) return this.keyFor(named2,
|
|
1374
|
-
if (
|
|
2580
|
+
if (named2) return this.keyFor(named2, key2);
|
|
2581
|
+
if (key2 !== void 0) return { service: await this.serviceOf(key2), key: key2 };
|
|
1375
2582
|
const route = await this.choose(
|
|
1376
2583
|
this.t({ zh: "mu \u7528\u4EC0\u4E48\u65B9\u5F0F\u8FDE\u63A5\u6A21\u578B\uFF1F", en: "How should mu connect to a model?" }),
|
|
1377
2584
|
[
|
|
@@ -1393,7 +2600,7 @@ var Setup = class {
|
|
|
1393
2600
|
]
|
|
1394
2601
|
);
|
|
1395
2602
|
if (route === "subscription") {
|
|
1396
|
-
const auth = this.path(
|
|
2603
|
+
const auth = this.path(join8(this.deps.paths.agentDir, "auth.json"));
|
|
1397
2604
|
this.io.say(
|
|
1398
2605
|
this.t({
|
|
1399
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`,
|
|
@@ -1416,8 +2623,8 @@ var Setup = class {
|
|
|
1416
2623
|
* The service a key belongs to, by its shape. When the shape fits several, or none, the person says which: a key is
|
|
1417
2624
|
* never tried on one vendor after another.
|
|
1418
2625
|
*/
|
|
1419
|
-
async serviceOf(
|
|
1420
|
-
const matches = servicesForKey(
|
|
2626
|
+
async serviceOf(key2) {
|
|
2627
|
+
const matches = servicesForKey(key2);
|
|
1421
2628
|
if (matches.length === 1) {
|
|
1422
2629
|
this.io.say(
|
|
1423
2630
|
this.t({
|
|
@@ -1460,23 +2667,23 @@ var Setup = class {
|
|
|
1460
2667
|
async keyFor(service, given) {
|
|
1461
2668
|
if (service.keyless) return { service };
|
|
1462
2669
|
if (service.keyOptional && given === void 0) return { service };
|
|
1463
|
-
let
|
|
1464
|
-
if (
|
|
2670
|
+
let key2 = given;
|
|
2671
|
+
if (key2 === void 0 && !this.scripted) {
|
|
1465
2672
|
if (service.keyPage) {
|
|
1466
2673
|
this.io.say(this.t({ zh: `\u5BC6\u94A5\u5728\u8FD9\u91CC\u521B\u5EFA\uFF1A${service.keyPage}`, en: `Create a key at ${service.keyPage}` }));
|
|
1467
2674
|
}
|
|
1468
|
-
|
|
2675
|
+
key2 = await this.pasteKey();
|
|
1469
2676
|
}
|
|
1470
|
-
if (!
|
|
2677
|
+
if (!key2) {
|
|
1471
2678
|
if (service.keyOptional) return { service };
|
|
1472
2679
|
throw this.fail(
|
|
1473
2680
|
{ zh: `${this.name(service)} \u9700\u8981\u4E00\u4E2A API \u5BC6\u94A5`, en: `${this.name(service)} needs an API key` },
|
|
1474
2681
|
SETUP_EXIT.usage
|
|
1475
2682
|
);
|
|
1476
2683
|
}
|
|
1477
|
-
const matches = servicesForKey(
|
|
2684
|
+
const matches = servicesForKey(key2);
|
|
1478
2685
|
const ownAddress = service.id === "other" || this.args.baseUrl !== void 0;
|
|
1479
|
-
if (matches.length === 0 || matches.includes(service) || ownAddress) return { service, key };
|
|
2686
|
+
if (matches.length === 0 || matches.includes(service) || ownAddress) return { service, key: key2 };
|
|
1480
2687
|
if (this.scripted) {
|
|
1481
2688
|
this.io.warn(
|
|
1482
2689
|
this.t({
|
|
@@ -1484,7 +2691,7 @@ var Setup = class {
|
|
|
1484
2691
|
en: `Note: this key looks like ${article(this.names(matches))} key; sending it to ${this.name(service)}, as --service says.`
|
|
1485
2692
|
})
|
|
1486
2693
|
);
|
|
1487
|
-
return { service, key };
|
|
2694
|
+
return { service, key: key2 };
|
|
1488
2695
|
}
|
|
1489
2696
|
const chosen = await this.pickService(
|
|
1490
2697
|
[service, ...matches],
|
|
@@ -1493,7 +2700,7 @@ var Setup = class {
|
|
|
1493
2700
|
en: `This key looks like ${article(this.names(matches))} key, not ${article(this.name(service))} one. Which service is it for?`
|
|
1494
2701
|
})
|
|
1495
2702
|
);
|
|
1496
|
-
return { service: chosen, key };
|
|
2703
|
+
return { service: chosen, key: key2 };
|
|
1497
2704
|
}
|
|
1498
2705
|
// -----------------------------------------------------------------------------------------------------------
|
|
1499
2706
|
// Step 3: one request to the service
|
|
@@ -1566,7 +2773,7 @@ var Setup = class {
|
|
|
1566
2773
|
return [{ provider, builtIn: false, baseUrl: base, api }];
|
|
1567
2774
|
}
|
|
1568
2775
|
inModelsFile(provider) {
|
|
1569
|
-
const models = readJsonFile(
|
|
2776
|
+
const models = readJsonFile(join8(this.deps.paths.agentDir, "models.json"));
|
|
1570
2777
|
return models.state === "ok" && existingProvider(models.value, provider) !== void 0;
|
|
1571
2778
|
}
|
|
1572
2779
|
/** A failure as the person can act on it: a local service that is silent is not ready yet, whatever the key. */
|
|
@@ -1580,10 +2787,10 @@ var Setup = class {
|
|
|
1580
2787
|
}
|
|
1581
2788
|
/** The key, checked: asked again, retried, or taken unchecked until the person has one that works, or stops. */
|
|
1582
2789
|
async checked(service, firstKey) {
|
|
1583
|
-
let
|
|
2790
|
+
let key2 = firstKey;
|
|
1584
2791
|
const reaches = await this.reaches(service);
|
|
1585
|
-
if (service.keyOptional &&
|
|
1586
|
-
|
|
2792
|
+
if (service.keyOptional && key2 === void 0 && !this.scripted) {
|
|
2793
|
+
key2 = await this.secret(
|
|
1587
2794
|
this.t({
|
|
1588
2795
|
zh: "\u7C98\u8D34\u5B83\u7684 API \u5BC6\u94A5\uFF1B\u4E0D\u9700\u8981\u5BC6\u94A5\u5C31\u76F4\u63A5\u56DE\u8F66\uFF08\u4E0D\u4F1A\u663E\u793A\uFF09\uFF1A",
|
|
1589
2796
|
en: "Paste its API key, or press Enter if it doesn't need one (it won't be shown): "
|
|
@@ -1594,8 +2801,8 @@ var Setup = class {
|
|
|
1594
2801
|
if (!reaches[0].builtIn || this.args.baseUrl) this.modelsFileWritable();
|
|
1595
2802
|
await this.oauthConflict(service, reaches);
|
|
1596
2803
|
for (; ; ) {
|
|
1597
|
-
const result = await this.tryReaches(service, reaches,
|
|
1598
|
-
if (result.ok) return { service, reach: result.reach, key, listed: result.listed };
|
|
2804
|
+
const result = await this.tryReaches(service, reaches, key2);
|
|
2805
|
+
if (result.ok) return { service, reach: result.reach, key: key2, listed: result.listed };
|
|
1599
2806
|
const said = this.explain(service, result.problem);
|
|
1600
2807
|
if (this.scripted) throw this.fail({ zh: said, en: said });
|
|
1601
2808
|
this.io.say(said);
|
|
@@ -1615,19 +2822,19 @@ var Setup = class {
|
|
|
1615
2822
|
});
|
|
1616
2823
|
const next = await this.choose(this.t({ zh: "\u63A5\u4E0B\u6765\u600E\u4E48\u529E\uFF1F", en: "What next?" }), choices);
|
|
1617
2824
|
if (next === "stop") throw this.cancelled();
|
|
1618
|
-
if (next === "unchecked") return { service, reach: reaches[0], key };
|
|
1619
|
-
if (next === "key")
|
|
2825
|
+
if (next === "unchecked") return { service, reach: reaches[0], key: key2 };
|
|
2826
|
+
if (next === "key") key2 = await this.pasteKey();
|
|
1620
2827
|
}
|
|
1621
2828
|
}
|
|
1622
2829
|
/** Each address of the service in turn: Kimi's .cn, then .ai; an address of one's own, then with /v1 added. */
|
|
1623
|
-
async tryReaches(service, reaches,
|
|
2830
|
+
async tryReaches(service, reaches, key2) {
|
|
1624
2831
|
let first;
|
|
1625
2832
|
const candidates = [...reaches];
|
|
1626
2833
|
for (let index = 0; index < candidates.length; index++) {
|
|
1627
2834
|
const reach = candidates[index];
|
|
1628
2835
|
const where = `${this.name(service)}\uFF08${hostOf2(reach.baseUrl)}\uFF09`;
|
|
1629
2836
|
this.io.say(
|
|
1630
|
-
|
|
2837
|
+
key2 ? this.t({
|
|
1631
2838
|
zh: `\u6B63\u5728\u7528 ${where}\u68C0\u67E5\u5BC6\u94A5\u2026\u2026`,
|
|
1632
2839
|
en: `Checking the key with ${this.name(service)} (${hostOf2(reach.baseUrl)})\u2026`
|
|
1633
2840
|
}) : this.t({
|
|
@@ -1638,7 +2845,7 @@ var Setup = class {
|
|
|
1638
2845
|
const result = await checkKey({
|
|
1639
2846
|
kind: checkKindOf(service, reach.api),
|
|
1640
2847
|
baseUrl: reach.baseUrl,
|
|
1641
|
-
key,
|
|
2848
|
+
key: key2,
|
|
1642
2849
|
fetch: this.deps.fetch,
|
|
1643
2850
|
timeoutMs: this.deps.timeoutMs ?? CHECK_TIMEOUT_MS
|
|
1644
2851
|
});
|
|
@@ -1686,7 +2893,7 @@ var Setup = class {
|
|
|
1686
2893
|
}
|
|
1687
2894
|
}
|
|
1688
2895
|
modelsFileWritable() {
|
|
1689
|
-
const path =
|
|
2896
|
+
const path = join8(this.deps.paths.agentDir, "models.json");
|
|
1690
2897
|
if (readJsonFile(path).state !== "unreadable") return;
|
|
1691
2898
|
throw this.fail({
|
|
1692
2899
|
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`,
|
|
@@ -1768,7 +2975,7 @@ var Setup = class {
|
|
|
1768
2975
|
/** A provider models.json has at another address is moved only with a yes. */
|
|
1769
2976
|
async conflicts(target) {
|
|
1770
2977
|
if (target.reach.builtIn && !this.args.baseUrl) return;
|
|
1771
|
-
const models = readJsonFile(
|
|
2978
|
+
const models = readJsonFile(join8(this.deps.paths.agentDir, "models.json"));
|
|
1772
2979
|
if (models.state !== "ok") return;
|
|
1773
2980
|
const current = existingProvider(models.value, target.reach.provider)?.baseUrl;
|
|
1774
2981
|
const old = typeof current === "string" ? current.replace(/\/+$/, "") : void 0;
|
|
@@ -1802,10 +3009,23 @@ var Setup = class {
|
|
|
1802
3009
|
en: `${existing.name} in ${this.path(paths.envFile)}`
|
|
1803
3010
|
}) : void 0;
|
|
1804
3011
|
const choices = [];
|
|
3012
|
+
const mude = this.deps.mude !== void 0;
|
|
3013
|
+
if (mude) {
|
|
3014
|
+
choices.push({
|
|
3015
|
+
label: this.t({
|
|
3016
|
+
zh: "mude 1.0\uFF08\u63A8\u8350\uFF09\uFF0C\u5728\u8FD9\u53F0 Mac \u4E0A\u8FD0\u884C\uFF1Amu \u81EA\u5DF1\u7684\u51B3\u7B56\u6A21\u578B\uFF0C\u5224\u5B9A\u4E0D\u51FA\u672C\u673A\u3002\u4E0B\u8F7D\u4E00\u6B21 1.8 GB\uFF0C\u8FD0\u884C\u65F6\u7EA6\u5360 4 GB \u5185\u5B58\uFF1B\u5B83\u7B54\u4E0D\u4E86\u7684\u4EA4\u7ED9 Jev",
|
|
3017
|
+
en: "mude 1.0 on this Mac (recommended): mu's own decision model, and its verdicts stay on this Mac. A one-time 1.8 GB download; it uses about 4 GB of memory while it runs, and Jev takes what it can't answer"
|
|
3018
|
+
}),
|
|
3019
|
+
value: "mude"
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
1805
3022
|
const keyless = !opencode && !where;
|
|
1806
3023
|
if (keyless) {
|
|
1807
3024
|
choices.push({
|
|
1808
|
-
label: this.t({
|
|
3025
|
+
label: mude ? this.t({
|
|
3026
|
+
zh: "Jev \u514D\u8D39\u7248\uFF0C\u5728 OpenCode Zen \u4E0A\uFF1A\u4E0D\u7528\u5BC6\u94A5\uFF0C\u9650\u65F6\u514D\u8D39\uFF1B\u4EE5\u540E\u52A0\u4E86\u5BC6\u94A5\u81EA\u52A8\u6362\u7528",
|
|
3027
|
+
en: "Free Jev on OpenCode Zen: no key needed, free for a limited time; a key you add later takes over"
|
|
3028
|
+
}) : this.t({
|
|
1809
3029
|
zh: "Jev \u514D\u8D39\u7248\uFF08\u63A8\u8350\uFF09\uFF0C\u5728 OpenCode Zen \u4E0A\uFF1A\u4E0D\u7528\u5BC6\u94A5\uFF0C\u9650\u65F6\u514D\u8D39\uFF1B\u4EE5\u540E\u52A0\u4E86\u5BC6\u94A5\u81EA\u52A8\u6362\u7528",
|
|
1810
3030
|
en: "Free Jev on OpenCode Zen (recommended): no key needed, free for a limited time; a key you add later takes over"
|
|
1811
3031
|
}),
|
|
@@ -1814,7 +3034,10 @@ var Setup = class {
|
|
|
1814
3034
|
}
|
|
1815
3035
|
if (opencode && !where) {
|
|
1816
3036
|
choices.push({
|
|
1817
|
-
label: this.t({
|
|
3037
|
+
label: mude ? this.t({
|
|
3038
|
+
zh: "Jev\uFF0C\u5728 OpenCode Zen \u4E0A\uFF0C\u7528\u4F60\u7684 OpenCode \u5BC6\u94A5\uFF0C\u6309\u6B21\u8BA1\u8D39",
|
|
3039
|
+
en: "Jev on OpenCode Zen with your OpenCode key, billed per call"
|
|
3040
|
+
}) : this.t({
|
|
1818
3041
|
zh: "Jev\uFF08\u63A8\u8350\uFF09\uFF0C\u5728 OpenCode Zen \u4E0A\uFF0C\u7528\u4F60\u7684 OpenCode \u5BC6\u94A5\uFF0C\u6309\u6B21\u8BA1\u8D39",
|
|
1819
3042
|
en: "Jev on OpenCode Zen with your OpenCode key (recommended), billed per call"
|
|
1820
3043
|
}),
|
|
@@ -1823,7 +3046,10 @@ var Setup = class {
|
|
|
1823
3046
|
}
|
|
1824
3047
|
if (where) {
|
|
1825
3048
|
choices.push({
|
|
1826
|
-
label: this.t({
|
|
3049
|
+
label: mude ? this.t({
|
|
3050
|
+
zh: `Jev\uFF0C\u7528\u5DF2\u7ECF\u8BBE\u7F6E\u7684\u5BC6\u94A5\uFF08${where}\uFF09`,
|
|
3051
|
+
en: `Jev with the key that's already set: ${where}`
|
|
3052
|
+
}) : this.t({
|
|
1827
3053
|
zh: `Jev\uFF08\u63A8\u8350\uFF09\uFF0C\u7528\u5DF2\u7ECF\u8BBE\u7F6E\u7684\u5BC6\u94A5\uFF08${where}\uFF09`,
|
|
1828
3054
|
en: `Jev with the key that's already set (recommended): ${where}`
|
|
1829
3055
|
}),
|
|
@@ -1890,11 +3116,22 @@ var Setup = class {
|
|
|
1890
3116
|
return { kind: "opencode" };
|
|
1891
3117
|
case "free":
|
|
1892
3118
|
return { kind: "free" };
|
|
3119
|
+
case "mude":
|
|
3120
|
+
if (!this.deps.mude) {
|
|
3121
|
+
throw this.fail(
|
|
3122
|
+
{
|
|
3123
|
+
zh: "--judge mude \u9700\u8981 Apple \u82AF\u7247\u7684 Mac\uFF08macOS 14 \u53CA\u4EE5\u4E0A\uFF0C\u5185\u5B58 8 GB \u53CA\u4EE5\u4E0A\uFF09",
|
|
3124
|
+
en: "--judge mude needs a Mac with Apple silicon (macOS 14 or later, 8 GB of memory or more)"
|
|
3125
|
+
},
|
|
3126
|
+
SETUP_EXIT.usage
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
return { kind: "mude" };
|
|
1893
3130
|
case "off":
|
|
1894
3131
|
return { kind: "none" };
|
|
1895
3132
|
case "jev": {
|
|
1896
|
-
const
|
|
1897
|
-
if (!
|
|
3133
|
+
const key2 = this.jevFromStdin;
|
|
3134
|
+
if (!key2) {
|
|
1898
3135
|
if (keySet) return { kind: "keep" };
|
|
1899
3136
|
throw this.fail(
|
|
1900
3137
|
{
|
|
@@ -1904,20 +3141,20 @@ var Setup = class {
|
|
|
1904
3141
|
SETUP_EXIT.usage
|
|
1905
3142
|
);
|
|
1906
3143
|
}
|
|
1907
|
-
if (
|
|
3144
|
+
if (key2 === this.modelKey) {
|
|
1908
3145
|
throw this.fail(
|
|
1909
3146
|
{ zh: "Jev \u5BC6\u94A5\u548C\u6A21\u578B\u7684\u5BC6\u94A5\u662F\u540C\u4E00\u4E2A", en: "the Jev key is the same as the model key" },
|
|
1910
3147
|
SETUP_EXIT.usage
|
|
1911
3148
|
);
|
|
1912
3149
|
}
|
|
1913
3150
|
const current = envText === void 0 ? void 0 : envFileValue(envText, "TYPESAFE_API_KEY");
|
|
1914
|
-
if (current && current !==
|
|
3151
|
+
if (current && current !== key2 && !this.args.yes) {
|
|
1915
3152
|
throw this.fail({
|
|
1916
3153
|
zh: `${this.path(this.deps.paths.envFile)} \u91CC\u5DF2\u7ECF\u6709 TYPESAFE_API_KEY\u3002\u8981\u66FF\u6362\u5B83\uFF0C\u52A0\u4E0A --yes`,
|
|
1917
3154
|
en: `${this.path(this.deps.paths.envFile)} already has a TYPESAFE_API_KEY. To replace it, add --yes`
|
|
1918
3155
|
});
|
|
1919
3156
|
}
|
|
1920
|
-
return { kind: "jev", key };
|
|
3157
|
+
return { kind: "jev", key: key2 };
|
|
1921
3158
|
}
|
|
1922
3159
|
case "model":
|
|
1923
3160
|
return model ? { kind: "model" } : void 0;
|
|
@@ -1927,13 +3164,13 @@ var Setup = class {
|
|
|
1927
3164
|
}
|
|
1928
3165
|
async jevKey() {
|
|
1929
3166
|
for (; ; ) {
|
|
1930
|
-
const
|
|
3167
|
+
const key2 = await this.secret(
|
|
1931
3168
|
this.t({
|
|
1932
3169
|
zh: "\u7C98\u8D34 Jev \u5BC6\u94A5\uFF08TYPESAFE_API_KEY\uFF0C\u4E0D\u4F1A\u663E\u793A\uFF09\uFF1A",
|
|
1933
3170
|
en: "Paste the Jev key (TYPESAFE_API_KEY; it won't be shown): "
|
|
1934
3171
|
})
|
|
1935
3172
|
);
|
|
1936
|
-
if (
|
|
3173
|
+
if (key2 === this.modelKey) {
|
|
1937
3174
|
this.io.say(
|
|
1938
3175
|
this.t({
|
|
1939
3176
|
zh: `\u8FD9\u662F\u521A\u624D\u7ED9\u6A21\u578B\u7684\u5BC6\u94A5\u3002Jev \u7684\u5BC6\u94A5\u5728 TypeSafe \u7533\u8BF7\uFF1A${JEV_PAGE}`,
|
|
@@ -1942,8 +3179,8 @@ var Setup = class {
|
|
|
1942
3179
|
);
|
|
1943
3180
|
continue;
|
|
1944
3181
|
}
|
|
1945
|
-
const branded = servicesForKey(
|
|
1946
|
-
if (!branded) return
|
|
3182
|
+
const branded = servicesForKey(key2).find((service) => BRANDED.has(service.id));
|
|
3183
|
+
if (!branded) return key2;
|
|
1947
3184
|
const anyway = await this.confirm(
|
|
1948
3185
|
this.t({
|
|
1949
3186
|
zh: `\u8FD9\u770B\u8D77\u6765\u662F ${this.name(branded)} \u7684\u5BC6\u94A5\uFF0C\u4E0D\u662F Jev \u7684\uFF0C\u5B83\u4F1A\u88AB\u53D1\u7ED9 TypeSafe\u3002\u4ECD\u7136\u4F7F\u7528\u5417\uFF1F[y/N] `,
|
|
@@ -1951,7 +3188,7 @@ var Setup = class {
|
|
|
1951
3188
|
}),
|
|
1952
3189
|
false
|
|
1953
3190
|
);
|
|
1954
|
-
if (anyway) return
|
|
3191
|
+
if (anyway) return key2;
|
|
1955
3192
|
}
|
|
1956
3193
|
}
|
|
1957
3194
|
// -----------------------------------------------------------------------------------------------------------
|
|
@@ -1963,12 +3200,12 @@ var Setup = class {
|
|
|
1963
3200
|
const backups = new Backups(deps.paths.home, deps.now());
|
|
1964
3201
|
const saved = [];
|
|
1965
3202
|
const notes = [];
|
|
1966
|
-
const modelsPath =
|
|
1967
|
-
const authPath2 =
|
|
1968
|
-
const settingsPath =
|
|
3203
|
+
const modelsPath = join8(agentDir2, "models.json");
|
|
3204
|
+
const authPath2 = join8(agentDir2, "auth.json");
|
|
3205
|
+
const settingsPath = join8(agentDir2, "settings.json");
|
|
1969
3206
|
try {
|
|
1970
3207
|
if (target && model) {
|
|
1971
|
-
const { service, reach, key } = target;
|
|
3208
|
+
const { service, reach, key: key2 } = target;
|
|
1972
3209
|
const provider = reach.provider;
|
|
1973
3210
|
const custom = !reach.builtIn;
|
|
1974
3211
|
const moved = reach.builtIn && this.args.baseUrl !== void 0;
|
|
@@ -1983,7 +3220,7 @@ var Setup = class {
|
|
|
1983
3220
|
baseUrl: reach.baseUrl,
|
|
1984
3221
|
api: reach.api,
|
|
1985
3222
|
// pi counts a provider without a key as unusable: a keyless service gets a placeholder, never a real key.
|
|
1986
|
-
apiKey: service.keyless ? "ollama" :
|
|
3223
|
+
apiKey: service.keyless ? "ollama" : key2 ? void 0 : "none",
|
|
1987
3224
|
compat: service.compat,
|
|
1988
3225
|
// What the service lists goes into /model, the one to start with first.
|
|
1989
3226
|
models: [model, ...chatModels(target.listed ?? []).filter((id) => id !== model)].map((id) => ({
|
|
@@ -2009,9 +3246,9 @@ var Setup = class {
|
|
|
2009
3246
|
)
|
|
2010
3247
|
);
|
|
2011
3248
|
}
|
|
2012
|
-
if (
|
|
3249
|
+
if (key2) {
|
|
2013
3250
|
backups.save(authPath2);
|
|
2014
|
-
await deps.pi.storeKey(provider,
|
|
3251
|
+
await deps.pi.storeKey(provider, key2);
|
|
2015
3252
|
saved.push(
|
|
2016
3253
|
this.savedLine(
|
|
2017
3254
|
authPath2,
|
|
@@ -2107,7 +3344,7 @@ var Setup = class {
|
|
|
2107
3344
|
if (judge.kind === "model") {
|
|
2108
3345
|
if (target && model) tiers = [`llm:${target.reach.provider}/${model}`];
|
|
2109
3346
|
} else if (judge.kind === "opencode") tiers = ["jev-opencode"];
|
|
2110
|
-
else if (judge.kind === "free") {
|
|
3347
|
+
else if (judge.kind === "free" || judge.kind === "mude") {
|
|
2111
3348
|
if (!tiersOf(current).some((tier) => tier === "jev" || tier === "jev-opencode-free")) tiers = ["jev"];
|
|
2112
3349
|
} else if (!tiersOf(current).some((tier) => JEV_TIERS.has(tier))) tiers = ["jev"];
|
|
2113
3350
|
const turnOn = judgeOff(current);
|
|
@@ -2160,7 +3397,10 @@ var Setup = class {
|
|
|
2160
3397
|
zh: `\u5B8C\u6210\u3002mu \u4F1A\u7528 ${this.name(target.service)} \xB7 ${model} \u5F00\u59CB\u5DE5\u4F5C\u3002`,
|
|
2161
3398
|
en: `Done. mu will start on ${this.name(target.service)} \xB7 ${model}.`
|
|
2162
3399
|
}) : this.t({ zh: "\u5B8C\u6210\u3002", en: "Done." });
|
|
2163
|
-
const judged = judge === void 0 ? "" : judge.kind === "model" ? this.t({ zh: "\u5224\u5B9A\u5668\uFF1A\u540C\u4E00\u4E2A\u6A21\u578B\u3002", en: " Judge: the same model." }) : judge.kind === "none" ? this.t({ zh: "\u5224\u5B9A\u5668\uFF1A\u6682\u65F6\u6CA1\u6709\u3002", en: " Judge: none for now." }) : judge.kind === "opencode" ? this.t({ zh: "\u5224\u5B9A\u5668\uFF1AOpenCode Zen \u4E0A\u7684 Jev\u3002", en: " Judge: Jev on OpenCode Zen." }) : judge.kind === "
|
|
3400
|
+
const judged = judge === void 0 ? "" : judge.kind === "model" ? this.t({ zh: "\u5224\u5B9A\u5668\uFF1A\u540C\u4E00\u4E2A\u6A21\u578B\u3002", en: " Judge: the same model." }) : judge.kind === "none" ? this.t({ zh: "\u5224\u5B9A\u5668\uFF1A\u6682\u65F6\u6CA1\u6709\u3002", en: " Judge: none for now." }) : judge.kind === "opencode" ? this.t({ zh: "\u5224\u5B9A\u5668\uFF1AOpenCode Zen \u4E0A\u7684 Jev\u3002", en: " Judge: Jev on OpenCode Zen." }) : judge.kind === "mude" ? this.t({
|
|
3401
|
+
zh: "\u5224\u5B9A\u5668\uFF1Amude 1.0\uFF0C\u4E0B\u9762\u63A5\u7740\u5B89\u88C5\uFF1B\u5B83\u7B54\u4E0D\u4E86\u7684\u4EA4\u7ED9 Jev\u3002",
|
|
3402
|
+
en: " Judge: mude 1.0, installed next, with Jev for what it can't answer."
|
|
3403
|
+
}) : judge.kind === "free" ? this.t({
|
|
2164
3404
|
zh: "\u5224\u5B9A\u5668\uFF1AJev\uFF0C\u52A0\u5BC6\u94A5\u4E4B\u524D\u7528 OpenCode Zen \u4E0A\u9650\u65F6\u514D\u8D39\u7684\u7248\u672C\u3002",
|
|
2165
3405
|
en: " Judge: Jev, using the free version on OpenCode Zen until you add a key."
|
|
2166
3406
|
}) : this.t({ zh: "\u5224\u5B9A\u5668\uFF1AJev\u3002", en: " Judge: Jev." });
|
|
@@ -2180,8 +3420,8 @@ var Setup = class {
|
|
|
2180
3420
|
async offerSession(signIn = false) {
|
|
2181
3421
|
if (this.scripted) return SETUP_EXIT.done;
|
|
2182
3422
|
if (this.args.firstRun) return signIn ? SETUP_EXIT.signIn : SETUP_EXIT.done;
|
|
2183
|
-
const
|
|
2184
|
-
if (!
|
|
3423
|
+
const start2 = await this.confirm(this.t({ zh: "\u73B0\u5728\u542F\u52A8 mu \u5417\uFF1F[Y/n] ", en: "Start mu now? [Y/n] " }), true);
|
|
3424
|
+
if (!start2) return SETUP_EXIT.done;
|
|
2185
3425
|
return signIn ? SETUP_EXIT.signIn : SETUP_EXIT.start;
|
|
2186
3426
|
}
|
|
2187
3427
|
};
|
|
@@ -2211,10 +3451,10 @@ async function runSetup(argv, deps) {
|
|
|
2211
3451
|
var givenKey = process.env.MU_SETUP_KEY || void 0;
|
|
2212
3452
|
delete process.env.MU_SETUP_KEY;
|
|
2213
3453
|
var home = process.env.MU_SETUP_HOME || muHome();
|
|
2214
|
-
var agentDir = muEnv("AGENT_DIR") ||
|
|
2215
|
-
var envFile = process.env.MU_SETUP_ENV_FILE ||
|
|
2216
|
-
var authPath =
|
|
2217
|
-
var modelsStorePath =
|
|
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");
|
|
2218
3458
|
var givenEnv = { ...process.env };
|
|
2219
3459
|
var PROXY_VARIABLES = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
2220
3460
|
function useSessionProxy() {
|
|
@@ -2234,11 +3474,11 @@ function useSessionProxy() {
|
|
|
2234
3474
|
if (named2) return named2 in givenEnv ? named2 : `${named2}, .env`;
|
|
2235
3475
|
return fromSettings ? "httpProxy, settings.json" : void 0;
|
|
2236
3476
|
}
|
|
2237
|
-
async function
|
|
3477
|
+
async function main2() {
|
|
2238
3478
|
const proxy = useSessionProxy();
|
|
2239
3479
|
const runtime = await ModelRuntime.create({
|
|
2240
3480
|
authPath,
|
|
2241
|
-
modelsPath:
|
|
3481
|
+
modelsPath: join9(agentDir, "models.json"),
|
|
2242
3482
|
modelsStorePath,
|
|
2243
3483
|
refreshOnCreate: false
|
|
2244
3484
|
});
|
|
@@ -2255,7 +3495,16 @@ async function main() {
|
|
|
2255
3495
|
stdinTerminal: process.stdin.isTTY === true,
|
|
2256
3496
|
readStdin: () => readAll(),
|
|
2257
3497
|
givenKey,
|
|
2258
|
-
proxy
|
|
3498
|
+
proxy,
|
|
3499
|
+
// mude 1.0 where this Mac can run it: `mu judge mude install`, here, its progress in this terminal.
|
|
3500
|
+
mude: problems().length === 0 ? {
|
|
3501
|
+
install: () => main(
|
|
3502
|
+
["install"],
|
|
3503
|
+
contextFromEnv(process.env),
|
|
3504
|
+
(text) => process.stdout.write(text),
|
|
3505
|
+
process.stdout.isTTY === true
|
|
3506
|
+
)
|
|
3507
|
+
} : void 0
|
|
2259
3508
|
});
|
|
2260
3509
|
}
|
|
2261
3510
|
var leaving = false;
|
|
@@ -2266,8 +3515,8 @@ var leave = (code) => {
|
|
|
2266
3515
|
void exitWhenUnlocked(code, [authPath, modelsStorePath], (exitCode) => process.exit(exitCode));
|
|
2267
3516
|
};
|
|
2268
3517
|
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"])
|
|
2269
|
-
process.on(signal, () => leave(128 +
|
|
2270
|
-
|
|
3518
|
+
process.on(signal, () => leave(128 + constants2.signals[signal]));
|
|
3519
|
+
main2().then(leave, (error) => {
|
|
2271
3520
|
const message = error instanceof Error ? error.message : String(error);
|
|
2272
3521
|
process.stderr.write(`mu setup: ${redactSecrets(message, givenKey ? [givenKey] : [])}
|
|
2273
3522
|
`);
|