mu-agent 0.1.13 → 0.2.0
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 +47 -0
- package/README.md +5 -3
- package/dist/bundle/chunks/{chunk-WDURT4ZM.js → chunk-AN7MSYCK.js} +1 -1
- package/dist/bundle/chunks/{chunk-3J3HSHLX.js → chunk-BK7M6BDT.js} +3 -3
- package/dist/bundle/chunks/{virtual-modules-TJPHLTRT.js → virtual-modules-U6MRV5BC.js} +1 -1
- package/dist/bundle/cli-runtime.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/judge/dist/auth.js +3 -0
- package/judge/dist/mude.js +1338 -0
- package/judge/dist/setup.js +15 -4
- package/judge/manifest.json +139 -71
- package/judge/mu/{chunk-SKGYWHW6.js → chunk-6XRUK3PM.js} +1 -1
- package/judge/mu/{cloud-code-V7FOGB4J.js → cloud-code-XR7SZMJC.js} +1 -1
- package/judge/mu/index.js +501 -214
- package/kyrn/bin/kyrn-doctor +67 -13
- package/kyrn/bin/mu.mjs +143 -9
- package/kyrn/local-judge/mude-requirements.lock.txt +746 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1338 @@
|
|
|
1
|
+
import { createRequire as __muCreateRequire } from "node:module"; const require = __muCreateRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// packages/kyrn-judge/src/mude/command.ts
|
|
4
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, statfsSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7
|
+
import { homedir as homedir3, release, totalmem as totalmem2 } from "node:os";
|
|
8
|
+
import { join as join6 } from "node:path";
|
|
9
|
+
|
|
10
|
+
// packages/kyrn-judge/src/mude/config.ts
|
|
11
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
12
|
+
|
|
13
|
+
// packages/kyrn-judge/src/naming.ts
|
|
14
|
+
var CONFIG_FILE = "mu.json";
|
|
15
|
+
var LEGACY_CONFIG_FILE = "kyrn.json";
|
|
16
|
+
|
|
17
|
+
// packages/kyrn-judge/src/config.ts
|
|
18
|
+
var BUILT_IN_JUDGES = {
|
|
19
|
+
jev: { type: "jev" },
|
|
20
|
+
"jev-direct": { type: "typesafe" },
|
|
21
|
+
// OpenRouter serves Jev over the same System One protocol. The key is Jev's own, not the one for OpenRouter's
|
|
22
|
+
// chat models, so setting one never changes the other.
|
|
23
|
+
"jev-openrouter": {
|
|
24
|
+
type: "typesafe",
|
|
25
|
+
baseUrl: "https://openrouter.ai/api/v1/systemone",
|
|
26
|
+
model: "~typesafe/jev-latest",
|
|
27
|
+
apiKeyEnv: "MU_JUDGE_OPENROUTER_API_KEY"
|
|
28
|
+
},
|
|
29
|
+
"jev-gateway": { type: "gateway", model: "typesafe-ai/jev" },
|
|
30
|
+
// Jev on OpenCode Zen, with the key of OpenCode's models (OPENCODE_API_KEY, or `mu auth`). The free one is a
|
|
31
|
+
// limited-time offer of OpenCode's and needs no key at all. OpenCode does not train on what Jev is sent.
|
|
32
|
+
"jev-opencode": { type: "classifier", model: "opencode/jev-1.13" },
|
|
33
|
+
"jev-opencode-free": { type: "classifier", model: "opencode/jev-1.13-free" },
|
|
34
|
+
// Jev on Cloudflare Workers AI, with Cloudflare's key and account (CLOUDFLARE_API_KEY, CLOUDFLARE_ACCOUNT_ID).
|
|
35
|
+
"jev-cloudflare": { type: "classifier", model: "cloudflare-workers-ai/typesafe/jev" },
|
|
36
|
+
// Cloudflare's own System One classifiers. Not measured on mu's questions yet.
|
|
37
|
+
clef: { type: "classifier", model: "cloudflare-workers-ai/@cf/cloudflare/clef" },
|
|
38
|
+
"clef-flash": { type: "classifier", model: "cloudflare-workers-ai/@cf/cloudflare/clef-flash" },
|
|
39
|
+
// CLM-8B on this machine, where `clm-serve` listens by default. Its profile is not measured on mu's questions yet.
|
|
40
|
+
clm: { type: "clm" },
|
|
41
|
+
// Measured in kyrn/docs/03-local-judge.md: the base checkpoint classifies one text well, says "yes" to
|
|
42
|
+
// nearly every relational question, and cannot use a rubric or judge the request itself. Measured on
|
|
43
|
+
// Terminal-Bench 2.1 (Oct 2026): on tool output it is wrong or late. All 15 passages it withheld as injections were
|
|
44
|
+
// clean (directory listings, href lists, headers, JSON), its one admission drop was a makefile, and most of its
|
|
45
|
+
// calls on both failed or came too late. So screening tool output (`screen`) goes to the next tier, or to the
|
|
46
|
+
// rules. It also read a finished "Done. Created /app/meeting_scheduled.ics …" as a promise of more work, twice in one
|
|
47
|
+
// run (constraints-scheduling): 4 extra requests and the model searching / for test files. So whether a closing
|
|
48
|
+
// message promised more (`intent`, turn.continue) goes to the next tier too; `continuation.localPromised` asks it.
|
|
49
|
+
laya: {
|
|
50
|
+
type: "local",
|
|
51
|
+
profile: { capabilities: { relate: false, rate: false, meta: false, screen: false, intent: false } }
|
|
52
|
+
},
|
|
53
|
+
// mude, the decision model trained for mu's judgment layer, on this Mac (`mu judge mude install`, Apple silicon):
|
|
54
|
+
// the System One server that comes in its package, on 127.0.0.1:8790, with the key the install wrote to the .env.
|
|
55
|
+
// Trusted on every capability: on mu's own dev scenarios (an internal benchmark, not public yet) it did as well as Jev
|
|
56
|
+
// or better on relate, rate, meta, screening tool output (`tool.admission`) and promises (`turn.continue`), and on
|
|
57
|
+
// classify but for one check: a tool's description screened for injected instructions (`tool.description`), where
|
|
58
|
+
// Jev did better. Nothing measured it on injections in fetched content (`tool.injection`). So with a judge behind it,
|
|
59
|
+
// `mu judge mude` routes those two checks to that judge (`routes` in mu.json). It answers in about 0.1 s here.
|
|
60
|
+
mude: {
|
|
61
|
+
type: "typesafe",
|
|
62
|
+
baseUrl: "http://127.0.0.1:8790/v1/systemone",
|
|
63
|
+
model: "mude",
|
|
64
|
+
apiKeyEnv: "MU_JUDGE_MUDE_KEY",
|
|
65
|
+
timeoutMs: 4e3
|
|
66
|
+
},
|
|
67
|
+
mock: { type: "mock" }
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// packages/kyrn-judge/src/setup/env-file.ts
|
|
71
|
+
var DEFINITION = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
|
|
72
|
+
function closingQuote(text, quote) {
|
|
73
|
+
for (let index = 0; index < text.length; index++) {
|
|
74
|
+
if (quote === '"' && text[index] === "\\") index++;
|
|
75
|
+
else if (text[index] === quote) return index;
|
|
76
|
+
}
|
|
77
|
+
return -1;
|
|
78
|
+
}
|
|
79
|
+
function readEnvEntries(text) {
|
|
80
|
+
const entries = [];
|
|
81
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
82
|
+
for (let index = 0; index < lines.length; index++) {
|
|
83
|
+
const line = lines[index].trim();
|
|
84
|
+
if (!line || line.startsWith("#")) continue;
|
|
85
|
+
const match = DEFINITION.exec(line);
|
|
86
|
+
if (!match) continue;
|
|
87
|
+
const [, name, rest] = match;
|
|
88
|
+
const quote = rest[0];
|
|
89
|
+
if (quote !== '"' && quote !== "'") {
|
|
90
|
+
entries.push({ name, value: rest.replace(/\s+#.*$/, "").trim(), first: index, last: index });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let body = rest.slice(1);
|
|
94
|
+
let end = closingQuote(body, quote);
|
|
95
|
+
const startedAt = index;
|
|
96
|
+
while (end < 0 && index + 1 < lines.length) {
|
|
97
|
+
body += `
|
|
98
|
+
${lines[++index]}`;
|
|
99
|
+
end = closingQuote(body, quote);
|
|
100
|
+
}
|
|
101
|
+
if (end < 0) {
|
|
102
|
+
index = startedAt;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const value = body.slice(0, end);
|
|
106
|
+
entries.push({
|
|
107
|
+
name,
|
|
108
|
+
value: quote === '"' ? value.replace(/\\(["\\$`])/g, "$1") : value,
|
|
109
|
+
first: startedAt,
|
|
110
|
+
last: index
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return entries;
|
|
114
|
+
}
|
|
115
|
+
function envFileValue(text, name) {
|
|
116
|
+
return readEnvEntries(text).find((entry) => entry.name === name)?.value;
|
|
117
|
+
}
|
|
118
|
+
function formatEnvValue(value) {
|
|
119
|
+
if (/^[A-Za-z0-9_./:@+-]*$/.test(value)) return value;
|
|
120
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
121
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
122
|
+
}
|
|
123
|
+
function setEnvValue(text, name, value) {
|
|
124
|
+
const newline = text.includes("\r\n") ? "\r\n" : "\n";
|
|
125
|
+
const bom = text.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
126
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
127
|
+
const found = readEnvEntries(text).filter((entry) => entry.name === name);
|
|
128
|
+
const assignment = (exported2) => `${exported2 ? "export " : ""}${name}=${formatEnvValue(value)}`;
|
|
129
|
+
if (found.length === 0) {
|
|
130
|
+
const body = lines.join(newline);
|
|
131
|
+
const separator = body === "" || body.endsWith(newline) ? "" : newline;
|
|
132
|
+
return `${bom}${body}${separator}${assignment(false)}${newline}`;
|
|
133
|
+
}
|
|
134
|
+
const drop = /* @__PURE__ */ new Set();
|
|
135
|
+
for (const entry of found.slice(1)) for (let line = entry.first; line <= entry.last; line++) drop.add(line);
|
|
136
|
+
const [first] = found;
|
|
137
|
+
const exported = /^\s*export\s/.test(lines[first.first]);
|
|
138
|
+
const next = [];
|
|
139
|
+
for (let index = 0; index < lines.length; index++) {
|
|
140
|
+
if (drop.has(index)) continue;
|
|
141
|
+
if (index === first.first) next.push(assignment(exported));
|
|
142
|
+
else if (index > first.first && index <= first.last) continue;
|
|
143
|
+
else next.push(lines[index]);
|
|
144
|
+
}
|
|
145
|
+
return `${bom}${next.join(newline)}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// packages/kyrn-judge/src/setup/files.ts
|
|
149
|
+
import {
|
|
150
|
+
chmodSync,
|
|
151
|
+
copyFileSync,
|
|
152
|
+
existsSync,
|
|
153
|
+
mkdirSync,
|
|
154
|
+
readFileSync,
|
|
155
|
+
renameSync,
|
|
156
|
+
statSync,
|
|
157
|
+
unlinkSync,
|
|
158
|
+
writeFileSync
|
|
159
|
+
} from "node:fs";
|
|
160
|
+
import { basename, dirname, join } from "node:path";
|
|
161
|
+
var PRIVATE_FILE = 384;
|
|
162
|
+
var PRIVATE_FOLDER = 448;
|
|
163
|
+
function backupStamp(now) {
|
|
164
|
+
const two = (value) => String(value).padStart(2, "0");
|
|
165
|
+
return `${now.getFullYear()}-${two(now.getMonth() + 1)}-${two(now.getDate())}T${two(now.getHours())}-${two(now.getMinutes())}-${two(now.getSeconds())}`;
|
|
166
|
+
}
|
|
167
|
+
var holdsNothing = (file) => {
|
|
168
|
+
try {
|
|
169
|
+
return /^\s*(\{\s*\})?\s*$/.test(readFileSync(file, "utf8"));
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var Backups = class {
|
|
175
|
+
root;
|
|
176
|
+
stamp;
|
|
177
|
+
made;
|
|
178
|
+
saved = /* @__PURE__ */ new Set();
|
|
179
|
+
constructor(home, now) {
|
|
180
|
+
this.root = join(home, "backups");
|
|
181
|
+
this.stamp = backupStamp(now);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Copies `file` the first time it is about to change in this run. A file that does not exist, or holds nothing (empty,
|
|
185
|
+
* or the `{}` pi writes to auth.json when it first starts), has nothing to keep: a first setup makes no backup folder
|
|
186
|
+
* and does not say it kept the previous files.
|
|
187
|
+
*/
|
|
188
|
+
save(file) {
|
|
189
|
+
if (this.saved.has(file) || !existsSync(file) || holdsNothing(file)) return;
|
|
190
|
+
const folder = this.folder();
|
|
191
|
+
const copy = join(folder, basename(file));
|
|
192
|
+
copyFileSync(file, copy);
|
|
193
|
+
try {
|
|
194
|
+
chmodSync(copy, PRIVATE_FILE);
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
this.saved.add(file);
|
|
198
|
+
}
|
|
199
|
+
/** Where this run's copies are, once one was made. */
|
|
200
|
+
get path() {
|
|
201
|
+
return this.made;
|
|
202
|
+
}
|
|
203
|
+
folder() {
|
|
204
|
+
if (this.made) return this.made;
|
|
205
|
+
mkdirSync(this.root, { recursive: true, mode: PRIVATE_FOLDER });
|
|
206
|
+
for (let attempt = 1; ; attempt++) {
|
|
207
|
+
const candidate = join(this.root, attempt === 1 ? this.stamp : `${this.stamp}-${attempt}`);
|
|
208
|
+
try {
|
|
209
|
+
mkdirSync(candidate, { mode: PRIVATE_FOLDER });
|
|
210
|
+
this.made = candidate;
|
|
211
|
+
return candidate;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error.code !== "EEXIST") throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
function readJsonFile(path) {
|
|
219
|
+
let text;
|
|
220
|
+
try {
|
|
221
|
+
text = readFileSync(path, "utf8");
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error.code === "ENOENT") return { state: "absent" };
|
|
224
|
+
return { state: "unreadable" };
|
|
225
|
+
}
|
|
226
|
+
if (!text.trim()) return { state: "ok", value: {} };
|
|
227
|
+
try {
|
|
228
|
+
const value = JSON.parse(text.replace(/^/, ""));
|
|
229
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? { state: "ok", value } : { state: "unreadable" };
|
|
230
|
+
} catch {
|
|
231
|
+
return { state: "unreadable" };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function writeFileAtomic(path, text) {
|
|
235
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
236
|
+
let mode = PRIVATE_FILE;
|
|
237
|
+
try {
|
|
238
|
+
mode = statSync(path).mode & 511;
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
const temporary = `${path}.mu-setup-${process.pid}`;
|
|
242
|
+
writeFileSync(temporary, text, { mode });
|
|
243
|
+
try {
|
|
244
|
+
chmodSync(temporary, mode);
|
|
245
|
+
renameSync(temporary, path);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
try {
|
|
248
|
+
unlinkSync(temporary);
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function writeJsonFile(path, value) {
|
|
255
|
+
writeFileAtomic(path, `${JSON.stringify(value, null, 2)}
|
|
256
|
+
`);
|
|
257
|
+
}
|
|
258
|
+
function isRecord(value) {
|
|
259
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
260
|
+
}
|
|
261
|
+
function judgeConfigPath(agentDir, exists = existsSync) {
|
|
262
|
+
const current = join(agentDir, CONFIG_FILE);
|
|
263
|
+
const legacy = join(agentDir, LEGACY_CONFIG_FILE);
|
|
264
|
+
return !exists(current) && exists(legacy) ? legacy : current;
|
|
265
|
+
}
|
|
266
|
+
function tiersOf(config) {
|
|
267
|
+
const tiers = config?.tiers;
|
|
268
|
+
const named = Array.isArray(tiers) ? tiers.filter((tier) => typeof tier === "string") : [];
|
|
269
|
+
return named.length > 0 ? named : ["jev"];
|
|
270
|
+
}
|
|
271
|
+
function withTiers(config, tiers) {
|
|
272
|
+
return { ...config, tiers: [...tiers] };
|
|
273
|
+
}
|
|
274
|
+
function judgeOff(config) {
|
|
275
|
+
return isRecord(config?.modes) && config.modes.default === "off";
|
|
276
|
+
}
|
|
277
|
+
function withJudge(config, on) {
|
|
278
|
+
const { default: _default, ...modes } = isRecord(config.modes) ? config.modes : {};
|
|
279
|
+
if (!on) return { ...config, modes: { default: "off", ...modes } };
|
|
280
|
+
if (!judgeOff(config)) return config;
|
|
281
|
+
const { modes: _modes, ...rest } = config;
|
|
282
|
+
return Object.keys(modes).length > 0 ? { ...rest, modes } : rest;
|
|
283
|
+
}
|
|
284
|
+
function readTextFile(path) {
|
|
285
|
+
try {
|
|
286
|
+
return readFileSync(path, "utf8");
|
|
287
|
+
} catch {
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function writeEnvValue(path, name, value) {
|
|
292
|
+
writeFileAtomic(path, setEnvValue(readTextFile(path) ?? "", name, value));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// packages/kyrn-judge/src/mude/server.ts
|
|
296
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
297
|
+
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
298
|
+
import { createServer } from "node:net";
|
|
299
|
+
import { totalmem } from "node:os";
|
|
300
|
+
import { join as join2 } from "node:path";
|
|
301
|
+
|
|
302
|
+
// packages/kyrn-judge/src/mude/manifest.ts
|
|
303
|
+
var MUDE_REPO = "qybaihe/mude-1.0-mlx-8bit";
|
|
304
|
+
var MUDE_REVISION = "269b71dbc4276770b5a021d7adbad100010473c8";
|
|
305
|
+
var MUDE_MODEL = "mude";
|
|
306
|
+
var FILES = [
|
|
307
|
+
[".gitattributes", 1570, "34448b82c17d60fec9b65b1f093c115ddbaadc04beb1b0140b6bfed2e012a930"],
|
|
308
|
+
["LICENSE", 1482, "793a9f63a9cdfd3436ca19a4a70e54d1acf9d639d84ba27129918284765f3a05"],
|
|
309
|
+
["LICENSES/Apache-2.0.txt", 11357, "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4"],
|
|
310
|
+
["MODEL_MANIFEST.json", 19177, "2d197005a61e693566e75946e034937ed13b192a150df583cfe2dde4f9f6ebcd"],
|
|
311
|
+
["NOTICE", 1899, "d57889a1687573fb059329fde16fc6d1feeadc683fc50aa592370b6a727bf42b"],
|
|
312
|
+
["README.md", 2356, "f86e44bd8573b306daafa86a3a4a261b430d99d5cd0df6bda223f187c5f56756"],
|
|
313
|
+
["chat_template.jinja", 7755, "273d8e0e683b885071fb17e08d71e5f2a5ddfb5309756181681de4f5a1822d80"],
|
|
314
|
+
["config.json", 2783, "4fba61002dea71507b2ea7d9910a70bb7df8f0a0639e2b2ffae2b978ddb9874b"],
|
|
315
|
+
["d3_engine.py", 4413, "086238942762df45047a32cd9a8e2bedd9b63167eb40be260c776de567979f53"],
|
|
316
|
+
["d3_fast.py", 22331, "68a1fb9a2c3bde9ac2763bf504decae592ca12722698fbde1b39a31262955535"],
|
|
317
|
+
["d3_format.py", 7008, "3badfbc70561bb3f5b8ec16a11d2c455b96ddb32caace0a8a54ca102eae1f3fa"],
|
|
318
|
+
["d3_kernels.py", 16129, "8f7534ca1e1846ebf7c4c73058002f3be4dbf0d5d426f0815150ae0cbe7677bd"],
|
|
319
|
+
["d3_mac.py", 10824, "5ab4f773a7361656a765b5be351e2cde519b3094c0b4a66123d01b2ac2f40372"],
|
|
320
|
+
["d3_mlx.py", 15876, "04890d713a240f7ff7d53ebb250b88ef85f9c04948f322a46006506f1e33e48d"],
|
|
321
|
+
["d3_runtime.py", 98229, "a77a0b84c1732a0fd6296e0daa44cd86130f67bfea5e9c87eee8c5a1df293def"],
|
|
322
|
+
["d3_server.py", 17705, "766a20a69101e36ab0535a1ec683379db422380a83091f0eccd7e82edb6a02de"],
|
|
323
|
+
["d3_server_mac.py", 1216, "0e351b255fac166d71489e95b68d761d1d91d10297dbcfc16a1c42a659b7b396"],
|
|
324
|
+
["decision_config.json", 5509, "19c62c4a8b0ff7bd1f82c05ef0d7af8df4b2ce7af2cfe19e3989e7d9f6c1c1cc"],
|
|
325
|
+
["merges.txt", 3353259, "a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d"],
|
|
326
|
+
["mlx-q8.safetensors", 1756751774, "9757238f601c7b2eea997069add5856ba5ed8370786bfffae0b9ac550c33f697"],
|
|
327
|
+
["mlx_weights.json", 293, "747ece4d690e493dda9637917b3d8b9c1296ccfa4a258f51c0ae0ad716a1d698"],
|
|
328
|
+
["modeling_d3.py", 12079, "f8f8db23ff58f1a990ffa24977d744a50e1b4446d02a64c22070967705b6dbef"],
|
|
329
|
+
["pipeline_d3.py", 2936, "4f9297c7c74e596e9b67df91a1e6823cfada48bf781336f141974ff6a2f1038e"],
|
|
330
|
+
["preprocessor_config.json", 390, "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516"],
|
|
331
|
+
["readout.safetensors", 2089040, "aeb18e2f9289aab0012e2059264326fb850aa8c5d8537975a891cf9655811674"],
|
|
332
|
+
["requirements-mac.txt", 289, "ed16543a58d7ffd2f19e17b38f8b9e46775548700d9d3eaca7a592309a8741b5"],
|
|
333
|
+
["requirements.txt", 723, "de9defd71ae4e559a7f5bdcc935b23716f9788e8755f2ba6db1ab6862bcda12e"],
|
|
334
|
+
["tokenizer.json", 12807982, "5f9e4d4901a92b997e463c1f46055088b6cca5ca61a6522d1b9f64c4bb81cb42"],
|
|
335
|
+
["tokenizer_config.json", 16709, "49e2b6e395f959f077f1e992b338919c0d4a9732fc6e613995e06557f843500c"],
|
|
336
|
+
["video_preprocessor_config.json", 385, "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13"],
|
|
337
|
+
["vocab.json", 6722759, "ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003"]
|
|
338
|
+
];
|
|
339
|
+
var MUDE_FILES = FILES.map(([path, size, sha256]) => ({ path, size, sha256 }));
|
|
340
|
+
var MUDE_BYTES = MUDE_FILES.reduce((sum, file) => sum + file.size, 0);
|
|
341
|
+
function mudeFileUrl(path, endpoint = "https://huggingface.co") {
|
|
342
|
+
return `${endpoint.replace(/\/+$/, "")}/${MUDE_REPO}/resolve/${MUDE_REVISION}/${path.split("/").map(encodeURIComponent).join("/")}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// packages/kyrn-judge/src/mude/server.ts
|
|
346
|
+
var DEFAULT_PORT = 8790;
|
|
347
|
+
function mudeBaseUrl(port2) {
|
|
348
|
+
return `http://127.0.0.1:${port2}/v1/systemone`;
|
|
349
|
+
}
|
|
350
|
+
async function health(port2, timeoutMs = 2e3, fetcher = fetch) {
|
|
351
|
+
try {
|
|
352
|
+
const response = await fetcher(`http://127.0.0.1:${port2}/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
353
|
+
if (!response.ok) return void 0;
|
|
354
|
+
const body = await response.json();
|
|
355
|
+
return { status: String(body.status ?? ""), model: typeof body.model === "string" ? body.model : void 0 };
|
|
356
|
+
} catch {
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function portFree(port2) {
|
|
361
|
+
return new Promise((resolve) => {
|
|
362
|
+
const probe = createServer();
|
|
363
|
+
probe.once("error", () => resolve(false));
|
|
364
|
+
probe.listen({ port: port2, host: "127.0.0.1", exclusive: true }, () => probe.close(() => resolve(true)));
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async function choosePort(preferred = DEFAULT_PORT) {
|
|
368
|
+
if (await portFree(preferred)) return preferred;
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
370
|
+
const probe = createServer();
|
|
371
|
+
probe.once("error", reject);
|
|
372
|
+
probe.listen({ port: 0, host: "127.0.0.1" }, () => {
|
|
373
|
+
const address = probe.address();
|
|
374
|
+
const port2 = typeof address === "object" && address ? address.port : 0;
|
|
375
|
+
probe.close(() => resolve(port2));
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function readPid(paths) {
|
|
380
|
+
try {
|
|
381
|
+
const pid = Number.parseInt(readFileSync2(paths.pid, "utf8").trim(), 10);
|
|
382
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
383
|
+
} catch {
|
|
384
|
+
return void 0;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function serverAlive(paths, pid = readPid(paths)) {
|
|
388
|
+
if (!pid) return false;
|
|
389
|
+
try {
|
|
390
|
+
process.kill(pid, 0);
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const command = execFileSync("/bin/ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
|
|
396
|
+
return command.includes("d3_server_mac.py") && command.includes(paths.model);
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function cacheGb(memoryBytes = totalmem()) {
|
|
402
|
+
return memoryBytes >= 16 * 1024 ** 3 ? "1" : "0.25";
|
|
403
|
+
}
|
|
404
|
+
function tail(file, lines = 12) {
|
|
405
|
+
try {
|
|
406
|
+
const text = readFileSync2(file, "utf8");
|
|
407
|
+
return text.split(/\r?\n/).filter(Boolean).slice(-lines).join("\n");
|
|
408
|
+
} catch {
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function startServer(paths, options) {
|
|
413
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
414
|
+
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
415
|
+
const running = readPid(paths);
|
|
416
|
+
if (running && serverAlive(paths, running)) {
|
|
417
|
+
if (options.wait !== false) await untilReady(paths, running, options.port, timeoutMs, sleep);
|
|
418
|
+
return { pid: running, port: options.port, already: true };
|
|
419
|
+
}
|
|
420
|
+
if (!await portFree(options.port)) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Port ${options.port} on 127.0.0.1 is taken by another program; mu judge mude enable picks a free one`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
mkdirSync2(paths.root, { recursive: true });
|
|
426
|
+
rmSync(paths.log, { force: true });
|
|
427
|
+
const log = openSync(paths.log, "a", 384);
|
|
428
|
+
const child = spawn(
|
|
429
|
+
paths.python,
|
|
430
|
+
[
|
|
431
|
+
join2(paths.model, "d3_server_mac.py"),
|
|
432
|
+
"--model",
|
|
433
|
+
paths.model,
|
|
434
|
+
"--host",
|
|
435
|
+
"127.0.0.1",
|
|
436
|
+
"--port",
|
|
437
|
+
String(options.port),
|
|
438
|
+
"--name",
|
|
439
|
+
MUDE_MODEL,
|
|
440
|
+
"--verify",
|
|
441
|
+
"fast"
|
|
442
|
+
],
|
|
443
|
+
{
|
|
444
|
+
cwd: paths.model,
|
|
445
|
+
detached: true,
|
|
446
|
+
stdio: ["ignore", log, log],
|
|
447
|
+
env: {
|
|
448
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
449
|
+
HOME: process.env.HOME,
|
|
450
|
+
LANG: process.env.LANG ?? "en_US.UTF-8",
|
|
451
|
+
DECISION_API_KEY: options.key,
|
|
452
|
+
D3_MLX_CACHE_GB: cacheGb(),
|
|
453
|
+
// Everything it needs is in the package: nothing is fetched from the Hub at run time.
|
|
454
|
+
HF_HUB_OFFLINE: "1",
|
|
455
|
+
TRANSFORMERS_OFFLINE: "1",
|
|
456
|
+
PYTHONUNBUFFERED: "1"
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
closeSync(log);
|
|
461
|
+
const pid = child.pid;
|
|
462
|
+
if (!pid) throw new Error("mude's server could not be started");
|
|
463
|
+
child.unref();
|
|
464
|
+
writeFileSync2(paths.pid, `${pid}
|
|
465
|
+
`, { mode: 384 });
|
|
466
|
+
if (options.wait !== false) await untilReady(paths, pid, options.port, timeoutMs, sleep);
|
|
467
|
+
return { pid, port: options.port, already: false };
|
|
468
|
+
}
|
|
469
|
+
async function untilReady(paths, pid, port2, timeoutMs, sleep) {
|
|
470
|
+
const deadline = Date.now() + timeoutMs;
|
|
471
|
+
while (Date.now() < deadline) {
|
|
472
|
+
if ((await health(port2))?.status === "ready") return;
|
|
473
|
+
if (!serverAlive(paths, pid)) {
|
|
474
|
+
rmSync(paths.pid, { force: true });
|
|
475
|
+
throw new Error(`mude's server stopped while starting. Its log (${paths.log}) ends with:
|
|
476
|
+
${tail(paths.log)}`);
|
|
477
|
+
}
|
|
478
|
+
await sleep(500);
|
|
479
|
+
}
|
|
480
|
+
throw new Error(`mude's server did not answer within ${Math.round(timeoutMs / 1e3)} s; see ${paths.log}`);
|
|
481
|
+
}
|
|
482
|
+
async function stopServer(paths, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
483
|
+
const pid = readPid(paths);
|
|
484
|
+
if (!pid || !serverAlive(paths, pid)) {
|
|
485
|
+
rmSync(paths.pid, { force: true });
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
process.kill(pid, "SIGTERM");
|
|
489
|
+
for (let waited = 0; waited < 5e3 && serverAlive(paths, pid); waited += 200) await sleep(200);
|
|
490
|
+
if (serverAlive(paths, pid)) process.kill(pid, "SIGKILL");
|
|
491
|
+
rmSync(paths.pid, { force: true });
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
function serverMemory(pid) {
|
|
495
|
+
if (!pid) return void 0;
|
|
496
|
+
try {
|
|
497
|
+
const kb = Number.parseInt(
|
|
498
|
+
execFileSync("/bin/ps", ["-p", String(pid), "-o", "rss="], { encoding: "utf8" }).trim(),
|
|
499
|
+
10
|
|
500
|
+
);
|
|
501
|
+
return Number.isFinite(kb) ? kb * 1024 : void 0;
|
|
502
|
+
} catch {
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// packages/kyrn-judge/src/mude/config.ts
|
|
508
|
+
var ROUTED_AWAY = ["tool.description", "tool.injection"];
|
|
509
|
+
var MUDE = "mude";
|
|
510
|
+
var KEY_VARIABLE = "MU_JUDGE_MUDE_KEY";
|
|
511
|
+
function readState(paths) {
|
|
512
|
+
try {
|
|
513
|
+
const value = JSON.parse(readFileSync3(paths.state, "utf8"));
|
|
514
|
+
return Number.isInteger(value.port) ? value : void 0;
|
|
515
|
+
} catch {
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function writeState(paths, state) {
|
|
520
|
+
writeFileSync3(paths.state, `${JSON.stringify(state, null, 2)}
|
|
521
|
+
`, { mode: 384 });
|
|
522
|
+
}
|
|
523
|
+
function judgeSetting(files) {
|
|
524
|
+
const file = readJsonFile(judgeConfigPath(files.agentDir));
|
|
525
|
+
if (file.state === "unreadable") return { enabled: false, localOnly: false, fallback: [], unreadable: true };
|
|
526
|
+
const config = file.state === "ok" ? file.value : {};
|
|
527
|
+
const tiers = tiersOf(config);
|
|
528
|
+
const enabled = tiers[0] === MUDE && !judgeOff(config);
|
|
529
|
+
const fallback = tiers.filter((tier) => tier !== MUDE);
|
|
530
|
+
return { enabled, localOnly: enabled && fallback.length === 0, fallback };
|
|
531
|
+
}
|
|
532
|
+
function mudeInTiers(files) {
|
|
533
|
+
const file = readJsonFile(judgeConfigPath(files.agentDir));
|
|
534
|
+
return file.state === "ok" && tiersOf(file.value).includes(MUDE);
|
|
535
|
+
}
|
|
536
|
+
function readConfig(files) {
|
|
537
|
+
const path = judgeConfigPath(files.agentDir);
|
|
538
|
+
const file = readJsonFile(path);
|
|
539
|
+
if (file.state === "unreadable") {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`${path} is not plain JSON (comments, or a typo), so mu leaves it alone. Fix it, then run this again.`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return { path, config: file.state === "ok" ? file.value : {} };
|
|
545
|
+
}
|
|
546
|
+
function recordOf(value) {
|
|
547
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : {};
|
|
548
|
+
}
|
|
549
|
+
function judgesOf(config) {
|
|
550
|
+
return recordOf(config.judges);
|
|
551
|
+
}
|
|
552
|
+
function withSection(config, key2, value) {
|
|
553
|
+
if (Object.keys(value).length > 0) return { ...config, [key2]: value };
|
|
554
|
+
const { [key2]: _gone, ...rest } = config;
|
|
555
|
+
return rest;
|
|
556
|
+
}
|
|
557
|
+
var sameTiers = (a, b) => Array.isArray(a) && a.length === b.length && a.every((tier, index) => tier === b[index]);
|
|
558
|
+
function withoutOwnRoutes(config, state) {
|
|
559
|
+
const routes = recordOf(config.routes);
|
|
560
|
+
for (const [spec, tiers] of Object.entries(state?.wroteRoutes ?? {})) {
|
|
561
|
+
if (sameTiers(routes[spec], tiers)) delete routes[spec];
|
|
562
|
+
}
|
|
563
|
+
return withSection(config, "routes", routes);
|
|
564
|
+
}
|
|
565
|
+
function entryFor(port2) {
|
|
566
|
+
return { ...BUILT_IN_JUDGES[MUDE], baseUrl: mudeBaseUrl(port2) };
|
|
567
|
+
}
|
|
568
|
+
function enableMude(paths, files, options) {
|
|
569
|
+
const { path, config } = readConfig(files);
|
|
570
|
+
const state = readState(paths);
|
|
571
|
+
const current = tiersOf(config).filter((tier) => tier !== MUDE);
|
|
572
|
+
const before = current.length > 0 ? current : [...state?.previousTiers ?? ["jev"]];
|
|
573
|
+
const tiers = options.localOnly ? [MUDE] : [MUDE, ...before.filter((tier) => tier !== MUDE)];
|
|
574
|
+
const judgeWasOff = state?.judgeWasOff ?? judgeOff(config);
|
|
575
|
+
let next = withJudge(withTiers(withoutOwnRoutes(config, state), tiers), true);
|
|
576
|
+
const judges = judgesOf(next);
|
|
577
|
+
const entry = options.port !== DEFAULT_PORT;
|
|
578
|
+
if (entry) judges[MUDE] = entryFor(options.port);
|
|
579
|
+
else if (state?.wroteEntry) delete judges[MUDE];
|
|
580
|
+
next = withSection(next, "judges", judges);
|
|
581
|
+
const fallback = tiers.slice(1);
|
|
582
|
+
const routes = recordOf(next.routes);
|
|
583
|
+
const wroteRoutes = {};
|
|
584
|
+
if (fallback.length > 0) {
|
|
585
|
+
for (const spec of ROUTED_AWAY) {
|
|
586
|
+
if (routes[spec] !== void 0) continue;
|
|
587
|
+
routes[spec] = [...fallback];
|
|
588
|
+
wroteRoutes[spec] = fallback;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
next = withSection(next, "routes", routes);
|
|
592
|
+
const backups = new Backups(files.home, files.now ?? /* @__PURE__ */ new Date());
|
|
593
|
+
backups.save(path);
|
|
594
|
+
writeJsonFile(path, next);
|
|
595
|
+
if (envFileValue(readText(files.envFile), KEY_VARIABLE) !== options.key) {
|
|
596
|
+
backups.save(files.envFile);
|
|
597
|
+
writeEnvValue(files.envFile, KEY_VARIABLE, options.key);
|
|
598
|
+
}
|
|
599
|
+
writeState(paths, {
|
|
600
|
+
port: options.port,
|
|
601
|
+
previousTiers: before,
|
|
602
|
+
judgeWasOff,
|
|
603
|
+
wroteEntry: entry || void 0,
|
|
604
|
+
wroteRoutes: Object.keys(wroteRoutes).length > 0 ? wroteRoutes : void 0
|
|
605
|
+
});
|
|
606
|
+
return tiers;
|
|
607
|
+
}
|
|
608
|
+
function disableMude(paths, files, options = {}) {
|
|
609
|
+
const { path, config } = readConfig(files);
|
|
610
|
+
const state = readState(paths);
|
|
611
|
+
const rest = tiersOf(config).filter((tier) => tier !== MUDE);
|
|
612
|
+
const tiers = rest.length > 0 ? rest : [...state?.previousTiers ?? ["jev"]];
|
|
613
|
+
let next = withTiers(withoutOwnRoutes(config, state), tiers);
|
|
614
|
+
if (state?.judgeWasOff) next = withJudge(next, false);
|
|
615
|
+
if (options.removeEntry && state?.wroteEntry) {
|
|
616
|
+
const judges = judgesOf(next);
|
|
617
|
+
delete judges[MUDE];
|
|
618
|
+
next = withSection(next, "judges", judges);
|
|
619
|
+
}
|
|
620
|
+
new Backups(files.home, files.now ?? /* @__PURE__ */ new Date()).save(path);
|
|
621
|
+
writeJsonFile(path, next);
|
|
622
|
+
if (state) {
|
|
623
|
+
writeState(paths, {
|
|
624
|
+
...state,
|
|
625
|
+
judgeWasOff: void 0,
|
|
626
|
+
wroteRoutes: void 0,
|
|
627
|
+
wroteEntry: options.removeEntry ? void 0 : state.wroteEntry
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
return tiers;
|
|
631
|
+
}
|
|
632
|
+
function readText(path) {
|
|
633
|
+
try {
|
|
634
|
+
return readFileSync3(path, "utf8");
|
|
635
|
+
} catch {
|
|
636
|
+
return "";
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// packages/kyrn-judge/src/mude/download.ts
|
|
641
|
+
import { createHash } from "node:crypto";
|
|
642
|
+
import {
|
|
643
|
+
createReadStream,
|
|
644
|
+
createWriteStream,
|
|
645
|
+
existsSync as existsSync3,
|
|
646
|
+
mkdirSync as mkdirSync3,
|
|
647
|
+
readFileSync as readFileSync4,
|
|
648
|
+
renameSync as renameSync2,
|
|
649
|
+
rmSync as rmSync2,
|
|
650
|
+
statSync as statSync3,
|
|
651
|
+
writeFileSync as writeFileSync4
|
|
652
|
+
} from "node:fs";
|
|
653
|
+
import { homedir } from "node:os";
|
|
654
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
655
|
+
import { Readable } from "node:stream";
|
|
656
|
+
import { pipeline } from "node:stream/promises";
|
|
657
|
+
function huggingFaceToken(env = process.env, home = homedir()) {
|
|
658
|
+
const direct = env.HF_TOKEN?.trim();
|
|
659
|
+
if (direct) return direct;
|
|
660
|
+
const files = [
|
|
661
|
+
env.HF_TOKEN_PATH,
|
|
662
|
+
env.HF_HOME ? join3(env.HF_HOME, "token") : void 0,
|
|
663
|
+
join3(env.XDG_CACHE_HOME || join3(home, ".cache"), "huggingface", "token")
|
|
664
|
+
];
|
|
665
|
+
for (const file of files) {
|
|
666
|
+
if (!file) continue;
|
|
667
|
+
try {
|
|
668
|
+
const token = readFileSync4(file, "utf8").trim();
|
|
669
|
+
if (token) return token;
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return void 0;
|
|
674
|
+
}
|
|
675
|
+
async function sha256File(path) {
|
|
676
|
+
const hash = createHash("sha256");
|
|
677
|
+
await pipeline(createReadStream(path), hash);
|
|
678
|
+
return hash.digest("hex");
|
|
679
|
+
}
|
|
680
|
+
function readVerified(dir) {
|
|
681
|
+
try {
|
|
682
|
+
const value = JSON.parse(readFileSync4(join3(dir, ".mu-verified.json"), "utf8"));
|
|
683
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
684
|
+
} catch {
|
|
685
|
+
return {};
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function knownGood(dir, file, verified) {
|
|
689
|
+
const seen = verified[file.path];
|
|
690
|
+
if (!seen || seen.sha256 !== file.sha256) return false;
|
|
691
|
+
try {
|
|
692
|
+
const stats = statSync3(join3(dir, file.path));
|
|
693
|
+
return stats.size === file.size && stats.mtimeMs === seen.mtimeMs;
|
|
694
|
+
} catch {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function remember(dir, verified, file) {
|
|
699
|
+
const stats = statSync3(join3(dir, file.path));
|
|
700
|
+
verified[file.path] = { size: stats.size, mtimeMs: stats.mtimeMs, sha256: file.sha256 };
|
|
701
|
+
writeFileSync4(join3(dir, ".mu-verified.json"), `${JSON.stringify(verified, null, 1)}
|
|
702
|
+
`);
|
|
703
|
+
}
|
|
704
|
+
function missingFiles(dir, files) {
|
|
705
|
+
const verified = readVerified(dir);
|
|
706
|
+
return files.filter((file) => !knownGood(dir, file, verified));
|
|
707
|
+
}
|
|
708
|
+
function downloadedBytes(dir, files) {
|
|
709
|
+
const verified = readVerified(dir);
|
|
710
|
+
let bytes = 0;
|
|
711
|
+
for (const file of files) {
|
|
712
|
+
if (knownGood(dir, file, verified)) {
|
|
713
|
+
bytes += file.size;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const target = join3(dir, file.path);
|
|
717
|
+
bytes += Math.min(file.size, Math.max(sizeOf(target), sizeOf(`${target}.part`)));
|
|
718
|
+
}
|
|
719
|
+
return bytes;
|
|
720
|
+
}
|
|
721
|
+
function sizeOf(path) {
|
|
722
|
+
try {
|
|
723
|
+
return statSync3(path).size;
|
|
724
|
+
} catch {
|
|
725
|
+
return 0;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
var DownloadError = class extends Error {
|
|
729
|
+
kind;
|
|
730
|
+
constructor(message, kind) {
|
|
731
|
+
super(message);
|
|
732
|
+
this.name = "DownloadError";
|
|
733
|
+
this.kind = kind;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
async function downloadFiles(options) {
|
|
737
|
+
const { dir, files } = options;
|
|
738
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
739
|
+
mkdirSync3(dir, { recursive: true });
|
|
740
|
+
const verified = readVerified(dir);
|
|
741
|
+
let settled = 0;
|
|
742
|
+
for (const file of files) {
|
|
743
|
+
if (knownGood(dir, file, verified)) {
|
|
744
|
+
settled += file.size;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const target = join3(dir, file.path);
|
|
748
|
+
mkdirSync3(dirname2(target), { recursive: true });
|
|
749
|
+
if (existsSync3(target) && statSync3(target).size === file.size) {
|
|
750
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled, total });
|
|
751
|
+
if (await sha256File(target) === file.sha256) {
|
|
752
|
+
remember(dir, verified, file);
|
|
753
|
+
settled += file.size;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
rmSync2(target, { force: true });
|
|
757
|
+
}
|
|
758
|
+
await fetchFile(
|
|
759
|
+
options,
|
|
760
|
+
file,
|
|
761
|
+
target,
|
|
762
|
+
(bytes) => options.onProgress?.({ file: file.path, checking: false, done: settled + bytes, total })
|
|
763
|
+
);
|
|
764
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
|
|
765
|
+
const part = `${target}.part`;
|
|
766
|
+
const actual = await sha256File(part);
|
|
767
|
+
if (actual !== file.sha256) {
|
|
768
|
+
rmSync2(part, { force: true });
|
|
769
|
+
throw new DownloadError(
|
|
770
|
+
`${file.path} does not match its pinned sha256 (got ${actual.slice(0, 12)}\u2026, want ${file.sha256.slice(0, 12)}\u2026); it was removed`,
|
|
771
|
+
"integrity"
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
renameSync2(part, target);
|
|
775
|
+
remember(dir, verified, file);
|
|
776
|
+
settled += file.size;
|
|
777
|
+
}
|
|
778
|
+
options.onProgress?.({ file: "", checking: false, done: total, total });
|
|
779
|
+
}
|
|
780
|
+
async function fetchFile(options, file, target, onBytes) {
|
|
781
|
+
const part = `${target}.part`;
|
|
782
|
+
const attempts = options.attempts ?? 4;
|
|
783
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
784
|
+
let lastError;
|
|
785
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
786
|
+
if (options.signal?.aborted) throw new DownloadError("The download was stopped", "aborted");
|
|
787
|
+
try {
|
|
788
|
+
await fetchOnce(options, file, part, onBytes);
|
|
789
|
+
return;
|
|
790
|
+
} catch (error) {
|
|
791
|
+
if (error instanceof DownloadError && error.kind !== "network") throw error;
|
|
792
|
+
lastError = error;
|
|
793
|
+
if (attempt < attempts) await sleep([2e3, 5e3, 1e4][attempt - 1] ?? 1e4);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const reason = lastError instanceof Error ? lastError.message : String(lastError);
|
|
797
|
+
throw new DownloadError(
|
|
798
|
+
`${file.path} could not be downloaded (${reason}); run the install again to go on from here`,
|
|
799
|
+
"network"
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
async function fetchOnce(options, file, part, onBytes) {
|
|
803
|
+
let have = 0;
|
|
804
|
+
try {
|
|
805
|
+
have = statSync3(part).size;
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
if (have > file.size) {
|
|
809
|
+
rmSync2(part, { force: true });
|
|
810
|
+
have = 0;
|
|
811
|
+
}
|
|
812
|
+
if (have === file.size) return;
|
|
813
|
+
const controller = new AbortController();
|
|
814
|
+
const stallMs = options.stallMs ?? 6e4;
|
|
815
|
+
let stall = setTimeout(() => controller.abort(new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
|
|
816
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
817
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
818
|
+
try {
|
|
819
|
+
const headers = {};
|
|
820
|
+
if (have > 0) headers.Range = `bytes=${have}-`;
|
|
821
|
+
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
822
|
+
const response = await (options.fetch ?? fetch)(options.url(file.path), {
|
|
823
|
+
headers,
|
|
824
|
+
redirect: "follow",
|
|
825
|
+
signal: controller.signal
|
|
826
|
+
});
|
|
827
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
828
|
+
throw new DownloadError(
|
|
829
|
+
`Hugging Face answered ${response.status} for ${file.path}: the model is not public yet, or this machine's token cannot read it`,
|
|
830
|
+
"access"
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
if (response.status === 416 && have > 0) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
|
|
837
|
+
const resumed = response.status === 206;
|
|
838
|
+
if (!resumed) have = 0;
|
|
839
|
+
const out = createWriteStream(part, { flags: resumed ? "a" : "w" });
|
|
840
|
+
let received = have;
|
|
841
|
+
onBytes(received);
|
|
842
|
+
const body = Readable.fromWeb(response.body);
|
|
843
|
+
body.on("data", (chunk) => {
|
|
844
|
+
received += chunk.length;
|
|
845
|
+
clearTimeout(stall);
|
|
846
|
+
stall = setTimeout(() => controller.abort(new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
|
|
847
|
+
onBytes(received);
|
|
848
|
+
});
|
|
849
|
+
await pipeline(body, out);
|
|
850
|
+
if (received !== file.size) throw new Error(`got ${received} of ${file.size} bytes`);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
if (options.signal?.aborted) throw new DownloadError("The download was stopped", "aborted");
|
|
853
|
+
throw error;
|
|
854
|
+
} finally {
|
|
855
|
+
clearTimeout(stall);
|
|
856
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// packages/kyrn-judge/src/mude/paths.ts
|
|
861
|
+
import { join as join4 } from "node:path";
|
|
862
|
+
function mudePaths(muHome) {
|
|
863
|
+
const root = join4(muHome, "local-judge", "mude");
|
|
864
|
+
const model = join4(root, "model", MUDE_REVISION);
|
|
865
|
+
const venv = join4(root, "venv");
|
|
866
|
+
return {
|
|
867
|
+
root,
|
|
868
|
+
model,
|
|
869
|
+
verified: join4(model, ".mu-verified.json"),
|
|
870
|
+
venv,
|
|
871
|
+
python: join4(venv, "bin", "python"),
|
|
872
|
+
venvLock: join4(venv, ".mu-lock-sha256"),
|
|
873
|
+
tools: join4(root, "tools"),
|
|
874
|
+
key: join4(root, "key"),
|
|
875
|
+
pid: join4(root, "server.pid"),
|
|
876
|
+
log: join4(root, "server.log"),
|
|
877
|
+
state: join4(root, "state.json")
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// packages/kyrn-judge/src/mude/python.ts
|
|
882
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
883
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
884
|
+
import { accessSync, constants, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
885
|
+
import { homedir as homedir2 } from "node:os";
|
|
886
|
+
import { delimiter, join as join5 } from "node:path";
|
|
887
|
+
var PYTHON_VERSION = "3.13";
|
|
888
|
+
var UV_RELEASE = {
|
|
889
|
+
version: "0.13.1",
|
|
890
|
+
url: "https://github.com/astral-sh/uv/releases/download/0.13.1/uv-aarch64-apple-darwin.tar.gz",
|
|
891
|
+
sha256: "cfe454573412ff7380d15fc7dae5997ab53ffce72fce0a78e91ddbc84001acd2"
|
|
892
|
+
};
|
|
893
|
+
function executable(path) {
|
|
894
|
+
try {
|
|
895
|
+
accessSync(path, constants.X_OK);
|
|
896
|
+
return true;
|
|
897
|
+
} catch {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function findUv(paths, env = process.env, home = homedir2()) {
|
|
902
|
+
const dirs = [
|
|
903
|
+
...(env.PATH ?? "").split(delimiter),
|
|
904
|
+
join5(home, ".local", "bin"),
|
|
905
|
+
join5(home, ".cargo", "bin"),
|
|
906
|
+
"/opt/homebrew/bin",
|
|
907
|
+
"/usr/local/bin",
|
|
908
|
+
paths.tools
|
|
909
|
+
];
|
|
910
|
+
for (const dir of dirs) {
|
|
911
|
+
if (dir && executable(join5(dir, "uv"))) return join5(dir, "uv");
|
|
912
|
+
}
|
|
913
|
+
return void 0;
|
|
914
|
+
}
|
|
915
|
+
function run(command, args, options = {}) {
|
|
916
|
+
return new Promise((resolve) => {
|
|
917
|
+
const child = spawn2(command, args, { env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
918
|
+
const lines = [];
|
|
919
|
+
let pending = "";
|
|
920
|
+
const take = (chunk) => {
|
|
921
|
+
pending += chunk.toString("utf8");
|
|
922
|
+
const parts = pending.split(/\r?\n/);
|
|
923
|
+
pending = parts.pop() ?? "";
|
|
924
|
+
for (const line of parts) {
|
|
925
|
+
if (!line.trim()) continue;
|
|
926
|
+
lines.push(line);
|
|
927
|
+
if (lines.length > 40) lines.shift();
|
|
928
|
+
options.onLine?.(line);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
child.stdout.on("data", take);
|
|
932
|
+
child.stderr.on("data", take);
|
|
933
|
+
child.once("error", (error) => resolve({ code: null, output: error.message }));
|
|
934
|
+
child.once("close", (code) => {
|
|
935
|
+
if (pending.trim()) lines.push(pending);
|
|
936
|
+
resolve({ code, output: lines.slice(-12).join("\n") });
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
async function downloadUv(paths, options = {}) {
|
|
941
|
+
mkdirSync4(paths.tools, { recursive: true });
|
|
942
|
+
const archive = "uv.tar.gz";
|
|
943
|
+
options.onLine?.(`Downloading uv ${UV_RELEASE.version}`);
|
|
944
|
+
const response = await (options.fetch ?? fetch)(UV_RELEASE.url, { redirect: "follow" });
|
|
945
|
+
if (!response.ok) throw new Error(`uv could not be downloaded: HTTP ${response.status}`);
|
|
946
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
947
|
+
const actual = createHash2("sha256").update(bytes).digest("hex");
|
|
948
|
+
if (actual !== UV_RELEASE.sha256)
|
|
949
|
+
throw new Error("The downloaded uv does not match its pinned sha256; nothing was installed");
|
|
950
|
+
writeFileSync5(join5(paths.tools, archive), bytes);
|
|
951
|
+
const unpacked = await run("/usr/bin/tar", [
|
|
952
|
+
"-xzf",
|
|
953
|
+
join5(paths.tools, archive),
|
|
954
|
+
"-C",
|
|
955
|
+
paths.tools,
|
|
956
|
+
"--strip-components",
|
|
957
|
+
"1"
|
|
958
|
+
]);
|
|
959
|
+
rmSync3(join5(paths.tools, archive), { force: true });
|
|
960
|
+
if (unpacked.code !== 0 || !executable(join5(paths.tools, "uv")))
|
|
961
|
+
throw new Error(`uv could not be unpacked: ${unpacked.output}`);
|
|
962
|
+
return join5(paths.tools, "uv");
|
|
963
|
+
}
|
|
964
|
+
function lockDigest(lockFile) {
|
|
965
|
+
return createHash2("sha256").update(readFileSync5(lockFile)).digest("hex");
|
|
966
|
+
}
|
|
967
|
+
function venvReady(paths, lockFile) {
|
|
968
|
+
if (!existsSync4(paths.python)) return false;
|
|
969
|
+
try {
|
|
970
|
+
return readFileSync5(paths.venvLock, "utf8").trim() === lockDigest(lockFile);
|
|
971
|
+
} catch {
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function ensureVenv(paths, lockFile, uv, options = {}) {
|
|
976
|
+
if (venvReady(paths, lockFile)) return;
|
|
977
|
+
rmSync3(paths.venv, { recursive: true, force: true });
|
|
978
|
+
const env = { ...options.env ?? process.env, UV_NO_CONFIG: "1", VIRTUAL_ENV: void 0 };
|
|
979
|
+
const made = await run(uv, ["venv", "--python", PYTHON_VERSION, "--no-project", "--quiet", paths.venv], {
|
|
980
|
+
env,
|
|
981
|
+
onLine: options.onLine
|
|
982
|
+
});
|
|
983
|
+
if (made.code !== 0) throw new Error(`The Python environment could not be made (uv venv):
|
|
984
|
+
${made.output}`);
|
|
985
|
+
const installed2 = await run(
|
|
986
|
+
uv,
|
|
987
|
+
[
|
|
988
|
+
"pip",
|
|
989
|
+
"install",
|
|
990
|
+
"--python",
|
|
991
|
+
paths.python,
|
|
992
|
+
"--require-hashes",
|
|
993
|
+
"--no-deps",
|
|
994
|
+
"--index-url",
|
|
995
|
+
"https://pypi.org/simple",
|
|
996
|
+
"-r",
|
|
997
|
+
lockFile
|
|
998
|
+
],
|
|
999
|
+
{ env, onLine: options.onLine }
|
|
1000
|
+
);
|
|
1001
|
+
if (installed2.code !== 0)
|
|
1002
|
+
throw new Error(`mude's Python packages could not be installed (uv pip install):
|
|
1003
|
+
${installed2.output}`);
|
|
1004
|
+
writeFileSync5(paths.venvLock, `${lockDigest(lockFile)}
|
|
1005
|
+
`);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// packages/kyrn-judge/src/mude/command.ts
|
|
1009
|
+
var USAGE = `mu judge mude: mude 1.0, mu's own decision model optimized for coding, as the judge on this Mac
|
|
1010
|
+
(Apple silicon, macOS 14 or later)
|
|
1011
|
+
|
|
1012
|
+
mu judge mude install [--local-only] download it (${(MUDE_BYTES / 1e9).toFixed(1)} GB), set it up, start it and make it the judge
|
|
1013
|
+
mu judge mude enable [--local-only | --with-fallback]
|
|
1014
|
+
make it the judge again, or change what happens when it can't answer
|
|
1015
|
+
mu judge mude disable back to the judges from before, and stop its server
|
|
1016
|
+
mu judge mude start | stop start or stop its server (about 4 GB of memory while it runs)
|
|
1017
|
+
mu judge mude status what is installed, running and enabled
|
|
1018
|
+
mu judge mude uninstall disable it and delete the model and its Python environment
|
|
1019
|
+
|
|
1020
|
+
What mude 1.0 cannot answer, because its server is not running or it is unsure, goes to the judges that were there
|
|
1021
|
+
before it (Jev by default): those are hosted, so what they read leaves this machine. With --local-only, it is the
|
|
1022
|
+
only judge, and such a decision takes its fallback instead. Add --json for one JSON object per line.
|
|
1023
|
+
`;
|
|
1024
|
+
function jsonOut(write) {
|
|
1025
|
+
const emit = (value) => write(`${JSON.stringify(value)}
|
|
1026
|
+
`);
|
|
1027
|
+
let last = 0;
|
|
1028
|
+
return {
|
|
1029
|
+
step: (step, message) => emit({ type: "step", step, message }),
|
|
1030
|
+
progress: (event) => {
|
|
1031
|
+
const now = Date.now();
|
|
1032
|
+
if (now - last < 250 && event.done < event.total && !event.checking) return;
|
|
1033
|
+
last = now;
|
|
1034
|
+
emit({ type: "progress", ...event });
|
|
1035
|
+
},
|
|
1036
|
+
line: (text) => emit({ type: "log", line: text }),
|
|
1037
|
+
status: (status2) => emit({ type: "status", ...status2 }),
|
|
1038
|
+
error: (message, kind) => emit({ type: "error", message, kind })
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function humanOut(write, tty) {
|
|
1042
|
+
let shown = -1;
|
|
1043
|
+
return {
|
|
1044
|
+
step: (_step, message) => {
|
|
1045
|
+
if (shown >= 0 && tty) write("\n");
|
|
1046
|
+
shown = -1;
|
|
1047
|
+
write(`${message}
|
|
1048
|
+
`);
|
|
1049
|
+
},
|
|
1050
|
+
progress: ({ done, total, checking }) => {
|
|
1051
|
+
const percent = total > 0 ? Math.floor(done / total * 100) : 100;
|
|
1052
|
+
if (tty) {
|
|
1053
|
+
write(
|
|
1054
|
+
`\r ${percent}% ${(done / 1e9).toFixed(2)} of ${(total / 1e9).toFixed(2)} GB${checking ? " (checking)" : ""} `
|
|
1055
|
+
);
|
|
1056
|
+
shown = percent;
|
|
1057
|
+
} else if (percent >= shown + 10 || percent === 100) {
|
|
1058
|
+
shown = percent - percent % 10;
|
|
1059
|
+
write(` ${percent}%
|
|
1060
|
+
`);
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
line: (text) => write(` ${text}
|
|
1064
|
+
`),
|
|
1065
|
+
status: (status2) => write(describe(status2)),
|
|
1066
|
+
error: (message) => write(`${message}
|
|
1067
|
+
`)
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function describe(status2) {
|
|
1071
|
+
const lines = [];
|
|
1072
|
+
if (!status2.supported) {
|
|
1073
|
+
lines.push(`mude 1.0 can't run here: ${status2.problems.join("; ")}`);
|
|
1074
|
+
return `${lines.join("\n")}
|
|
1075
|
+
`;
|
|
1076
|
+
}
|
|
1077
|
+
lines.push(
|
|
1078
|
+
`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"}`
|
|
1079
|
+
);
|
|
1080
|
+
if (status2.installed && !status2.pythonReady) lines.push("python not set up: mu judge mude install");
|
|
1081
|
+
lines.push(
|
|
1082
|
+
`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"}`
|
|
1083
|
+
);
|
|
1084
|
+
if (status2.enabled) {
|
|
1085
|
+
lines.push(
|
|
1086
|
+
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)`
|
|
1087
|
+
);
|
|
1088
|
+
} else {
|
|
1089
|
+
lines.push(`judge ${status2.installed ? "not enabled: mu judge mude enable" : "not enabled"}`);
|
|
1090
|
+
}
|
|
1091
|
+
lines.push(`folder ${status2.folder}`);
|
|
1092
|
+
return `${lines.join("\n")}
|
|
1093
|
+
`;
|
|
1094
|
+
}
|
|
1095
|
+
function problems(options = {}) {
|
|
1096
|
+
const platform = options.platform ?? process.platform;
|
|
1097
|
+
if (platform !== "darwin") return ["it needs a Mac with Apple silicon (an M1 or newer)"];
|
|
1098
|
+
const out = [];
|
|
1099
|
+
const arm64 = options.arm64 ?? appleSilicon();
|
|
1100
|
+
if (!arm64) out.push("it needs Apple silicon (an M1 or newer); this Mac has an Intel processor");
|
|
1101
|
+
const darwinMajor = options.darwinMajor ?? Number.parseInt(release().split(".")[0] ?? "0", 10);
|
|
1102
|
+
if (darwinMajor < 23) out.push("it needs macOS 14 or later");
|
|
1103
|
+
const memory = options.memoryBytes ?? totalmem2();
|
|
1104
|
+
if (memory < 8 * 1024 ** 3)
|
|
1105
|
+
out.push(
|
|
1106
|
+
`it needs 8 GB of memory or more (it uses about 4 GB); this Mac has ${Math.round(memory / 1024 ** 3)} GB`
|
|
1107
|
+
);
|
|
1108
|
+
return out;
|
|
1109
|
+
}
|
|
1110
|
+
function appleSilicon() {
|
|
1111
|
+
if (process.arch === "arm64") return true;
|
|
1112
|
+
try {
|
|
1113
|
+
return execFileSync2("/usr/sbin/sysctl", ["-n", "hw.optional.arm64"], { encoding: "utf8" }).trim() === "1";
|
|
1114
|
+
} catch {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function freeBytes(path) {
|
|
1119
|
+
let dir = path;
|
|
1120
|
+
while (!existsSync5(dir)) {
|
|
1121
|
+
const parent = join6(dir, "..");
|
|
1122
|
+
if (parent === dir) return void 0;
|
|
1123
|
+
dir = parent;
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
const stats = statfsSync(dir);
|
|
1127
|
+
return stats.bavail * stats.bsize;
|
|
1128
|
+
} catch {
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function installed(ctx) {
|
|
1133
|
+
return missingFiles(ctx.paths.model, MUDE_FILES).length === 0;
|
|
1134
|
+
}
|
|
1135
|
+
function key(ctx) {
|
|
1136
|
+
try {
|
|
1137
|
+
const existing = readFileSync6(ctx.paths.key, "utf8").trim();
|
|
1138
|
+
if (existing) return existing;
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
1141
|
+
mkdirSync5(ctx.paths.root, { recursive: true });
|
|
1142
|
+
const made = randomBytes(32).toString("hex");
|
|
1143
|
+
writeFileSync6(ctx.paths.key, `${made}
|
|
1144
|
+
`, { mode: 384 });
|
|
1145
|
+
return made;
|
|
1146
|
+
}
|
|
1147
|
+
async function port(ctx) {
|
|
1148
|
+
const previous = readState(ctx.paths)?.port;
|
|
1149
|
+
if (previous && (serverAlive(ctx.paths) || await portFree(previous))) return previous;
|
|
1150
|
+
return choosePort(DEFAULT_PORT);
|
|
1151
|
+
}
|
|
1152
|
+
async function status(ctx) {
|
|
1153
|
+
const found = problems();
|
|
1154
|
+
const state = readState(ctx.paths);
|
|
1155
|
+
const at = state?.port ?? DEFAULT_PORT;
|
|
1156
|
+
const pid = readPid(ctx.paths);
|
|
1157
|
+
const running = serverAlive(ctx.paths, pid);
|
|
1158
|
+
const ready = running && (await health(at))?.status === "ready";
|
|
1159
|
+
const setting = judgeSetting(ctx.files);
|
|
1160
|
+
return {
|
|
1161
|
+
supported: found.length === 0,
|
|
1162
|
+
problems: found,
|
|
1163
|
+
installed: installed(ctx),
|
|
1164
|
+
downloadedBytes: downloadedBytes(ctx.paths.model, MUDE_FILES),
|
|
1165
|
+
pythonReady: venvReady(ctx.paths, ctx.lockFile),
|
|
1166
|
+
running,
|
|
1167
|
+
ready,
|
|
1168
|
+
port: at,
|
|
1169
|
+
pid: running ? pid : void 0,
|
|
1170
|
+
memoryBytes: running ? serverMemory(pid) : void 0,
|
|
1171
|
+
enabled: setting.enabled,
|
|
1172
|
+
localOnly: setting.localOnly,
|
|
1173
|
+
fallback: setting.fallback,
|
|
1174
|
+
model: `${MUDE_REPO}@${MUDE_REVISION}`,
|
|
1175
|
+
folder: ctx.paths.root,
|
|
1176
|
+
log: ctx.paths.log
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
async function ensureInstalled(ctx, out) {
|
|
1180
|
+
const missing = missingFiles(ctx.paths.model, MUDE_FILES);
|
|
1181
|
+
if (missing.length > 0) {
|
|
1182
|
+
const need = missing.reduce((sum, file) => sum + file.size, 0);
|
|
1183
|
+
const free = freeBytes(ctx.paths.root);
|
|
1184
|
+
if (free !== void 0 && free < need + 2e9) {
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
`Not enough free disk space: mude 1.0 needs about ${((need + 2e9) / 1e9).toFixed(1)} GB, and ${(free / 1e9).toFixed(1)} GB are free`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
out.step(
|
|
1190
|
+
"download",
|
|
1191
|
+
`Downloading mude 1.0 (${MUDE_REPO}, ${(need / 1e9).toFixed(1)} GB) and checking every file`
|
|
1192
|
+
);
|
|
1193
|
+
await downloadFiles({
|
|
1194
|
+
dir: ctx.paths.model,
|
|
1195
|
+
files: MUDE_FILES,
|
|
1196
|
+
url: (path) => mudeFileUrl(path, ctx.env.HF_ENDPOINT || void 0),
|
|
1197
|
+
token: huggingFaceToken(ctx.env),
|
|
1198
|
+
onProgress: (event) => out.progress(event)
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
if (!venvReady(ctx.paths, ctx.lockFile)) {
|
|
1202
|
+
out.step("python", "Setting up the Python environment for mude 1.0 (MLX, transformers; about 1 GB, once)");
|
|
1203
|
+
let uv = findUv(ctx.paths, ctx.env);
|
|
1204
|
+
if (!uv) uv = await downloadUv(ctx.paths, { onLine: (line) => out.line(line) });
|
|
1205
|
+
await ensureVenv(ctx.paths, ctx.lockFile, uv, { onLine: (line) => out.line(line), env: ctx.env });
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function requireInstalled(ctx) {
|
|
1209
|
+
if (!installed(ctx) || !venvReady(ctx.paths, ctx.lockFile)) {
|
|
1210
|
+
throw new Error("mude 1.0 is not installed here yet: mu judge mude install");
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
async function start(ctx, out, wait = true) {
|
|
1214
|
+
const at = await port(ctx);
|
|
1215
|
+
out.step(
|
|
1216
|
+
"start",
|
|
1217
|
+
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)`
|
|
1218
|
+
);
|
|
1219
|
+
await startServer(ctx.paths, { port: at, key: key(ctx), wait });
|
|
1220
|
+
return at;
|
|
1221
|
+
}
|
|
1222
|
+
function enable(ctx, out, at, localOnly) {
|
|
1223
|
+
const tiers = enableMude(ctx.paths, ctx.files, { port: at, key: key(ctx), localOnly });
|
|
1224
|
+
out.step(
|
|
1225
|
+
"enable",
|
|
1226
|
+
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.`
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
async function main(argv, ctx, write, tty = false) {
|
|
1230
|
+
const json = argv.includes("--json");
|
|
1231
|
+
const args = argv.filter((arg) => arg !== "--json");
|
|
1232
|
+
const out = json ? jsonOut(write) : humanOut(write, tty);
|
|
1233
|
+
const command = args[0] ?? "status";
|
|
1234
|
+
const flag = (name) => args.includes(name);
|
|
1235
|
+
try {
|
|
1236
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
1237
|
+
write(USAGE);
|
|
1238
|
+
return 0;
|
|
1239
|
+
}
|
|
1240
|
+
const unknown = args.slice(1).find((arg) => !["--local-only", "--with-fallback", "--no-wait", "--yes"].includes(arg));
|
|
1241
|
+
if (unknown) {
|
|
1242
|
+
out.error(`Unknown option for mu judge mude ${command}: ${unknown} (mu judge mude help)`, "usage");
|
|
1243
|
+
return 2;
|
|
1244
|
+
}
|
|
1245
|
+
if (command === "status") {
|
|
1246
|
+
out.status(await status(ctx));
|
|
1247
|
+
return 0;
|
|
1248
|
+
}
|
|
1249
|
+
const found = problems();
|
|
1250
|
+
if (found.length > 0 && command !== "stop" && command !== "disable" && command !== "uninstall") {
|
|
1251
|
+
out.error(`mude 1.0 can't run on this machine: ${found.join("; ")}.`, "unsupported");
|
|
1252
|
+
return 1;
|
|
1253
|
+
}
|
|
1254
|
+
switch (command) {
|
|
1255
|
+
case "install": {
|
|
1256
|
+
await ensureInstalled(ctx, out);
|
|
1257
|
+
const at = await start(ctx, out);
|
|
1258
|
+
enable(ctx, out, at, flag("--local-only"));
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
case "enable": {
|
|
1262
|
+
requireInstalled(ctx);
|
|
1263
|
+
const current = judgeSetting(ctx.files);
|
|
1264
|
+
const localOnly = flag("--local-only") ? true : flag("--with-fallback") ? false : current.enabled && current.localOnly;
|
|
1265
|
+
const at = await start(ctx, out);
|
|
1266
|
+
enable(ctx, out, at, localOnly);
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
case "disable": {
|
|
1270
|
+
if (!mudeInTiers(ctx.files)) {
|
|
1271
|
+
const stopped2 = await stopServer(ctx.paths);
|
|
1272
|
+
out.step("disable", `mude 1.0 was not the judge${stopped2 ? "; its server is stopped" : ""}.`);
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
const tiers = disableMude(ctx.paths, ctx.files);
|
|
1276
|
+
const stopped = await stopServer(ctx.paths);
|
|
1277
|
+
out.step(
|
|
1278
|
+
"disable",
|
|
1279
|
+
`mude 1.0 is no longer the judge: ${tiers.join(" > ")} again${stopped ? ", and its server is stopped" : ""}.`
|
|
1280
|
+
);
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
case "start": {
|
|
1284
|
+
requireInstalled(ctx);
|
|
1285
|
+
await start(ctx, out, !flag("--no-wait"));
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
case "stop": {
|
|
1289
|
+
const stopped = await stopServer(ctx.paths);
|
|
1290
|
+
out.step("stop", stopped ? "The mude 1.0 server is stopped." : "The mude 1.0 server was not running.");
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
case "uninstall": {
|
|
1294
|
+
if (mudeInTiers(ctx.files) || readState(ctx.paths)?.wroteEntry) {
|
|
1295
|
+
const tiers = disableMude(ctx.paths, ctx.files, { removeEntry: true });
|
|
1296
|
+
out.step("disable", `mude 1.0 is no longer the judge: ${tiers.join(" > ")} again.`);
|
|
1297
|
+
}
|
|
1298
|
+
await stopServer(ctx.paths);
|
|
1299
|
+
rmSync4(ctx.paths.root, { recursive: true, force: true });
|
|
1300
|
+
out.step("uninstall", `Deleted ${ctx.paths.root}: the model, its Python environment and its key.`);
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
default:
|
|
1304
|
+
out.error(`Unknown command: mu judge mude ${command}
|
|
1305
|
+
|
|
1306
|
+
${USAGE}`, "usage");
|
|
1307
|
+
return 2;
|
|
1308
|
+
}
|
|
1309
|
+
out.status(await status(ctx));
|
|
1310
|
+
return 0;
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
const kind = error instanceof DownloadError ? error.kind : void 0;
|
|
1313
|
+
out.error(error instanceof Error ? error.message : String(error), kind);
|
|
1314
|
+
return 1;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function contextFromEnv(env = process.env, home = homedir3()) {
|
|
1318
|
+
const muHome = env.MU_MUDE_HOME || join6(home, ".mu");
|
|
1319
|
+
const agentDir = env.MU_AGENT_DIR || join6(muHome, "agent");
|
|
1320
|
+
return {
|
|
1321
|
+
paths: mudePaths(muHome),
|
|
1322
|
+
files: { agentDir, home: muHome, envFile: env.MU_MUDE_ENV_FILE || join6(muHome, ".env") },
|
|
1323
|
+
lockFile: env.MU_MUDE_LOCK || join6(import.meta.dirname, "..", "..", "..", "..", "kyrn", "local-judge", "mude-requirements.lock.txt"),
|
|
1324
|
+
env
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// packages/kyrn-judge/src/mude/cli.ts
|
|
1329
|
+
main(process.argv.slice(2), contextFromEnv(), (text) => process.stdout.write(text), Boolean(process.stdout.isTTY)).then(
|
|
1330
|
+
(code) => {
|
|
1331
|
+
process.exitCode = code;
|
|
1332
|
+
},
|
|
1333
|
+
(error) => {
|
|
1334
|
+
process.stderr.write(`mu judge mude: ${error instanceof Error ? error.message : String(error)}
|
|
1335
|
+
`);
|
|
1336
|
+
process.exitCode = 1;
|
|
1337
|
+
}
|
|
1338
|
+
);
|