pudu-ai 0.2.18 → 0.2.21
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/README.es.md +85 -0
- package/README.md +19 -2
- package/dist/cli/brew-ZRR7D4XL.js +80 -0
- package/dist/cli/chunk-6C5EPKRI.js +1597 -0
- package/dist/cli/chunk-QGEXKMQ5.js +501 -0
- package/dist/cli/chunk-SS7OXXQQ.js +82 -0
- package/dist/cli/index.js +191 -2991
- package/dist/cli/render-3UBPZNF4.js +812 -0
- package/dist/cli/repo-cli-4GSG5MD3.js +255 -0
- package/package.json +3 -2
- package/python/pudu_agent/__init__.py +1 -0
- package/python/pudu_agent/__main__.py +4 -0
- package/python/pudu_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/pudu_agent/__pycache__/__main__.cpython-312.pyc +0 -0
- package/python/pudu_agent/__pycache__/ast_search.cpython-312.pyc +0 -0
- package/python/pudu_agent/__pycache__/protocol.cpython-312.pyc +0 -0
- package/python/pudu_agent/__pycache__/search.cpython-312.pyc +0 -0
- package/python/pudu_agent/ast_search.py +153 -0
- package/python/pudu_agent/protocol.py +96 -0
- package/python/pudu_agent/search.py +342 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,195 +1,92 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return String(value);
|
|
47
|
-
}
|
|
48
|
-
async function runCommand(file, args2 = [], options = {}) {
|
|
49
|
-
const result = await spawnTracked(file, args2, options);
|
|
50
|
-
return {
|
|
51
|
-
stdout: toText(result.stdout),
|
|
52
|
-
stderr: toText(result.stderr),
|
|
53
|
-
exitCode: result.exitCode,
|
|
54
|
-
timedOut: Boolean(result.timedOut)
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
async function killAll(signal = "SIGTERM") {
|
|
58
|
-
for (const child of [...children]) {
|
|
59
|
-
child.kill(signal);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
var children, installed;
|
|
63
|
-
var init_process = __esm({
|
|
64
|
-
"src/shared/process.ts"() {
|
|
65
|
-
"use strict";
|
|
66
|
-
children = /* @__PURE__ */ new Set();
|
|
67
|
-
installed = false;
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// src/shared/which.ts
|
|
72
|
-
import { access } from "node:fs/promises";
|
|
73
|
-
import { constants } from "node:fs";
|
|
74
|
-
import path2 from "node:path";
|
|
75
|
-
async function commandExists(name) {
|
|
76
|
-
if (name.includes("/") || name.includes("\\")) {
|
|
77
|
-
try {
|
|
78
|
-
await access(name, constants.X_OK);
|
|
79
|
-
return name;
|
|
80
|
-
} catch {
|
|
81
|
-
return void 0;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
const pathEnv = process.env.PATH ?? "";
|
|
85
|
-
const parts = pathEnv.split(path2.delimiter);
|
|
86
|
-
for (const dir of parts) {
|
|
87
|
-
const candidate = path2.join(dir, name);
|
|
88
|
-
try {
|
|
89
|
-
await access(candidate, constants.X_OK);
|
|
90
|
-
return candidate;
|
|
91
|
-
} catch {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return void 0;
|
|
96
|
-
}
|
|
97
|
-
var init_which = __esm({
|
|
98
|
-
"src/shared/which.ts"() {
|
|
99
|
-
"use strict";
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// src/integrations/brew.ts
|
|
104
|
-
var brew_exports = {};
|
|
105
|
-
__export(brew_exports, {
|
|
106
|
-
brewInstall: () => brewInstall,
|
|
107
|
-
brewInstallCask: () => brewInstallCask,
|
|
108
|
-
dockerReady: () => dockerReady,
|
|
109
|
-
ensureBrew: () => ensureBrew,
|
|
110
|
-
resolveBrew: () => resolveBrew,
|
|
111
|
-
startDockerDesktop: () => startDockerDesktop,
|
|
112
|
-
waitForDocker: () => waitForDocker
|
|
113
|
-
});
|
|
114
|
-
async function resolveBrew() {
|
|
115
|
-
for (const bin of BREW_CANDIDATES) {
|
|
116
|
-
const found = await commandExists(bin);
|
|
117
|
-
if (found) return found;
|
|
118
|
-
}
|
|
119
|
-
return void 0;
|
|
120
|
-
}
|
|
121
|
-
async function dockerReady() {
|
|
122
|
-
const docker = await commandExists("docker");
|
|
123
|
-
if (!docker) return false;
|
|
124
|
-
const info = await runCommand("docker", ["info"], { timeout: 8e3 });
|
|
125
|
-
return info.exitCode === 0;
|
|
126
|
-
}
|
|
127
|
-
async function ensureBrew() {
|
|
128
|
-
const existing = await resolveBrew();
|
|
129
|
-
if (existing) return { ok: true, brew: existing, log: existing };
|
|
130
|
-
const install = await runCommand(
|
|
131
|
-
"/bin/bash",
|
|
132
|
-
["-c", 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'],
|
|
133
|
-
{ timeout: 15 * 6e4 }
|
|
134
|
-
);
|
|
135
|
-
const after = await resolveBrew();
|
|
136
|
-
if (after) return { ok: true, brew: after, log: after };
|
|
137
|
-
return {
|
|
138
|
-
ok: false,
|
|
139
|
-
log: install.stderr || "Homebrew missing. Install: https://brew.sh"
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
async function brewInstall(args2) {
|
|
143
|
-
const ensured = await ensureBrew();
|
|
144
|
-
if (!ensured.ok || !ensured.brew) return { ok: false, log: ensured.log };
|
|
145
|
-
const result = await runCommand(ensured.brew, args2, { timeout: 15 * 6e4 });
|
|
146
|
-
return {
|
|
147
|
-
ok: result.exitCode === 0,
|
|
148
|
-
log: `${ensured.brew} ${args2.join(" ")}
|
|
149
|
-
${result.stdout || result.stderr}`
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
async function brewInstallCask(name) {
|
|
153
|
-
const brew = await resolveBrew();
|
|
154
|
-
if (!brew) {
|
|
155
|
-
const ensured = await ensureBrew();
|
|
156
|
-
if (!ensured.ok || !ensured.brew) return { ok: false, log: ensured.log };
|
|
157
|
-
const result2 = await runCommand(ensured.brew, ["install", "--cask", name], { timeout: 15 * 6e4 });
|
|
158
|
-
return { ok: result2.exitCode === 0, log: result2.stderr || result2.stdout };
|
|
159
|
-
}
|
|
160
|
-
const result = await runCommand(brew, ["install", "--cask", name], { timeout: 15 * 6e4 });
|
|
161
|
-
if (result.exitCode === 0) return { ok: true, log: result.stdout };
|
|
162
|
-
const formula = await runCommand(brew, ["install", name], { timeout: 15 * 6e4 });
|
|
163
|
-
return { ok: formula.exitCode === 0, log: formula.stderr || formula.stdout };
|
|
164
|
-
}
|
|
165
|
-
async function startDockerDesktop() {
|
|
166
|
-
if (process.platform === "darwin") {
|
|
167
|
-
await runCommand("open", ["-a", "Docker"], { timeout: 15e3 });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
async function waitForDocker(ms = 9e4) {
|
|
171
|
-
const start = Date.now();
|
|
172
|
-
while (Date.now() - start < ms) {
|
|
173
|
-
if (await dockerReady()) return true;
|
|
174
|
-
await new Promise((resolve) => setTimeout(resolve, 2500));
|
|
175
|
-
}
|
|
176
|
-
return dockerReady();
|
|
177
|
-
}
|
|
178
|
-
var BREW_CANDIDATES;
|
|
179
|
-
var init_brew = __esm({
|
|
180
|
-
"src/integrations/brew.ts"() {
|
|
181
|
-
"use strict";
|
|
182
|
-
init_which();
|
|
183
|
-
init_process();
|
|
184
|
-
BREW_CANDIDATES = ["/opt/homebrew/bin/brew", "/usr/local/bin/brew", "brew"];
|
|
185
|
-
}
|
|
186
|
-
});
|
|
2
|
+
import {
|
|
3
|
+
bytesToGiB,
|
|
4
|
+
canInstallGrade,
|
|
5
|
+
canirunCachePath,
|
|
6
|
+
compareText,
|
|
7
|
+
configPath,
|
|
8
|
+
decideAll,
|
|
9
|
+
decideLaunch,
|
|
10
|
+
detectAgents,
|
|
11
|
+
detectHardware,
|
|
12
|
+
doctorText,
|
|
13
|
+
ensureStorage,
|
|
14
|
+
executeLaunch,
|
|
15
|
+
hardwareText,
|
|
16
|
+
historyCsv,
|
|
17
|
+
historyText,
|
|
18
|
+
idsLikelyMatch,
|
|
19
|
+
launchText,
|
|
20
|
+
listBenchmarks,
|
|
21
|
+
localCompatibility,
|
|
22
|
+
modelsText,
|
|
23
|
+
parseAnswers,
|
|
24
|
+
parseIntegration,
|
|
25
|
+
parseSizeToBytes,
|
|
26
|
+
planTasks,
|
|
27
|
+
pullOllamaModel,
|
|
28
|
+
recommendText,
|
|
29
|
+
reportMarkdown,
|
|
30
|
+
resolveOllamaTag,
|
|
31
|
+
resultText,
|
|
32
|
+
runBenchmark,
|
|
33
|
+
runtimesText,
|
|
34
|
+
tasksText
|
|
35
|
+
} from "./chunk-6C5EPKRI.js";
|
|
36
|
+
import {
|
|
37
|
+
resolveLocale,
|
|
38
|
+
setLocale,
|
|
39
|
+
t
|
|
40
|
+
} from "./chunk-QGEXKMQ5.js";
|
|
41
|
+
import {
|
|
42
|
+
commandExists,
|
|
43
|
+
killAll,
|
|
44
|
+
runCommand
|
|
45
|
+
} from "./chunk-SS7OXXQQ.js";
|
|
187
46
|
|
|
188
47
|
// src/cli/parse-args.ts
|
|
48
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
49
|
+
"--preset",
|
|
50
|
+
"--lang",
|
|
51
|
+
"--locale",
|
|
52
|
+
"--for",
|
|
53
|
+
"--scope",
|
|
54
|
+
"--priority",
|
|
55
|
+
"--link",
|
|
56
|
+
"--structural",
|
|
57
|
+
"--repo",
|
|
58
|
+
"--intent",
|
|
59
|
+
"--limit",
|
|
60
|
+
"--glob"
|
|
61
|
+
]);
|
|
62
|
+
function flagValue(args2, name) {
|
|
63
|
+
const index = args2.findIndex((a) => a === name);
|
|
64
|
+
if (index < 0) return void 0;
|
|
65
|
+
const value = args2[index + 1];
|
|
66
|
+
if (!value || value.startsWith("-")) return void 0;
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function flagValues(args2, name) {
|
|
70
|
+
const values = [];
|
|
71
|
+
for (let i = 0; i < args2.length; i++) {
|
|
72
|
+
if (args2[i] === name) {
|
|
73
|
+
const value = args2[i + 1];
|
|
74
|
+
if (value && !value.startsWith("-")) values.push(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return values;
|
|
78
|
+
}
|
|
189
79
|
function parseArgs(argv) {
|
|
190
80
|
const args2 = argv.slice(2);
|
|
191
81
|
const flags = new Set(args2.filter((a) => a.startsWith("-")));
|
|
192
|
-
const
|
|
82
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
83
|
+
for (let i = 0; i < args2.length; i++) {
|
|
84
|
+
const current = args2[i];
|
|
85
|
+
if (current && VALUE_FLAGS.has(current) && args2[i + 1] && !args2[i + 1].startsWith("-")) {
|
|
86
|
+
consumed.add(i + 1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const rest = args2.filter((a, i) => !a.startsWith("-") && !consumed.has(i));
|
|
193
90
|
const command = rest[0] ?? "dashboard";
|
|
194
91
|
const known = [
|
|
195
92
|
"dashboard",
|
|
@@ -203,19 +100,14 @@ function parseArgs(argv) {
|
|
|
203
100
|
"report",
|
|
204
101
|
"tasks",
|
|
205
102
|
"launch",
|
|
206
|
-
"setup"
|
|
103
|
+
"setup",
|
|
104
|
+
"repo"
|
|
207
105
|
];
|
|
208
106
|
const isKnown = known.includes(command);
|
|
209
107
|
let addPath;
|
|
210
108
|
if (command === "models" && rest[1] === "add-path") addPath = rest[2];
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const langIndex = args2.findIndex((a) => a === "--lang" || a === "--locale");
|
|
214
|
-
const lang = langIndex >= 0 ? args2[langIndex + 1] : void 0;
|
|
215
|
-
const forIndex = args2.findIndex((a) => a === "--for");
|
|
216
|
-
const scopeIndex = args2.findIndex((a) => a === "--scope");
|
|
217
|
-
const priorityIndex = args2.findIndex((a) => a === "--priority");
|
|
218
|
-
const linkIndex = args2.findIndex((a) => a === "--link");
|
|
109
|
+
const limitRaw = flagValue(args2, "--limit");
|
|
110
|
+
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Number(limitRaw) : void 0;
|
|
219
111
|
return {
|
|
220
112
|
command: isKnown ? command : "dashboard",
|
|
221
113
|
positional: isKnown ? rest.slice(1) : rest,
|
|
@@ -224,301 +116,25 @@ function parseArgs(argv) {
|
|
|
224
116
|
network: !flags.has("--no-network"),
|
|
225
117
|
color: !flags.has("--no-color"),
|
|
226
118
|
verbose: flags.has("--verbose"),
|
|
227
|
-
preset,
|
|
119
|
+
preset: flagValue(args2, "--preset"),
|
|
228
120
|
markdown: flags.has("--markdown"),
|
|
229
121
|
help: flags.has("-h") || flags.has("--help"),
|
|
230
122
|
addPath,
|
|
231
|
-
lang,
|
|
232
|
-
forKinds:
|
|
233
|
-
scope:
|
|
234
|
-
priority:
|
|
123
|
+
lang: flagValue(args2, "--lang") ?? flagValue(args2, "--locale"),
|
|
124
|
+
forKinds: flagValue(args2, "--for"),
|
|
125
|
+
scope: flagValue(args2, "--scope"),
|
|
126
|
+
priority: flagValue(args2, "--priority"),
|
|
235
127
|
yes: flags.has("--yes"),
|
|
236
128
|
install: flags.has("--install"),
|
|
237
|
-
link:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
import { z } from "zod";
|
|
244
|
-
|
|
245
|
-
// src/storage/paths.ts
|
|
246
|
-
import os from "node:os";
|
|
247
|
-
import path from "node:path";
|
|
248
|
-
import { mkdir } from "node:fs/promises";
|
|
249
|
-
function homeDir() {
|
|
250
|
-
return path.join(os.homedir(), ".pudu-ai");
|
|
251
|
-
}
|
|
252
|
-
function configPath() {
|
|
253
|
-
return path.join(homeDir(), "config.json");
|
|
254
|
-
}
|
|
255
|
-
function benchmarksDir() {
|
|
256
|
-
return path.join(homeDir(), "benchmarks");
|
|
257
|
-
}
|
|
258
|
-
function telemetryDir() {
|
|
259
|
-
return path.join(homeDir(), "telemetry");
|
|
260
|
-
}
|
|
261
|
-
function cacheDir() {
|
|
262
|
-
return path.join(homeDir(), "cache");
|
|
263
|
-
}
|
|
264
|
-
function canirunCachePath() {
|
|
265
|
-
return path.join(cacheDir(), "canirun-models.json");
|
|
266
|
-
}
|
|
267
|
-
async function ensureStorage() {
|
|
268
|
-
await mkdir(benchmarksDir(), { recursive: true });
|
|
269
|
-
await mkdir(telemetryDir(), { recursive: true });
|
|
270
|
-
await mkdir(cacheDir(), { recursive: true });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// src/storage/config.ts
|
|
274
|
-
var configSchema = z.object({
|
|
275
|
-
schemaVersion: z.literal(1),
|
|
276
|
-
modelPaths: z.array(z.string()).default([]),
|
|
277
|
-
network: z.boolean().default(true)
|
|
278
|
-
});
|
|
279
|
-
var fallback = { schemaVersion: 1, modelPaths: [], network: true };
|
|
280
|
-
async function loadConfig() {
|
|
281
|
-
await ensureStorage();
|
|
282
|
-
try {
|
|
283
|
-
const raw = await readFile(configPath(), "utf8");
|
|
284
|
-
return configSchema.parse(JSON.parse(raw));
|
|
285
|
-
} catch {
|
|
286
|
-
await saveConfig(fallback);
|
|
287
|
-
return fallback;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
async function saveConfig(config) {
|
|
291
|
-
await ensureStorage();
|
|
292
|
-
await writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
293
|
-
`, "utf8");
|
|
294
|
-
}
|
|
295
|
-
async function addModelPath(dir) {
|
|
296
|
-
const config = await loadConfig();
|
|
297
|
-
if (!config.modelPaths.includes(dir)) config.modelPaths.push(dir);
|
|
298
|
-
await saveConfig(config);
|
|
299
|
-
return config;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// src/hardware/detect.ts
|
|
303
|
-
import os5 from "node:os";
|
|
304
|
-
|
|
305
|
-
// src/platform/macos/hardware.ts
|
|
306
|
-
init_process();
|
|
307
|
-
import os2 from "node:os";
|
|
308
|
-
|
|
309
|
-
// src/shared/bytes.ts
|
|
310
|
-
var GiB = 1024 ** 3;
|
|
311
|
-
var MiB = 1024 ** 2;
|
|
312
|
-
function bytesToGiB(bytes) {
|
|
313
|
-
return bytes / GiB;
|
|
314
|
-
}
|
|
315
|
-
function parseSizeToBytes(input) {
|
|
316
|
-
const match = input.trim().match(/^([\d.]+)\s*(B|KB|MB|GB|TB|KiB|MiB|GiB|TiB)$/i);
|
|
317
|
-
if (!match) return void 0;
|
|
318
|
-
const value = Number(match[1]);
|
|
319
|
-
const unit = match[2].toLowerCase();
|
|
320
|
-
const map = {
|
|
321
|
-
b: 1,
|
|
322
|
-
kb: 1e3,
|
|
323
|
-
mb: 1e3 ** 2,
|
|
324
|
-
gb: 1e3 ** 3,
|
|
325
|
-
tb: 1e3 ** 4,
|
|
326
|
-
kib: 1024,
|
|
327
|
-
mib: 1024 ** 2,
|
|
328
|
-
gib: 1024 ** 3,
|
|
329
|
-
tib: 1024 ** 4
|
|
330
|
-
};
|
|
331
|
-
const factor = map[unit];
|
|
332
|
-
if (!factor || Number.isNaN(value)) return void 0;
|
|
333
|
-
return value * factor;
|
|
334
|
-
}
|
|
335
|
-
function formatBytes(bytes, digits = 1) {
|
|
336
|
-
if (!Number.isFinite(bytes)) return "N/A";
|
|
337
|
-
const abs = Math.abs(bytes);
|
|
338
|
-
if (abs >= GiB) return `${(bytes / GiB).toFixed(digits)} GB`;
|
|
339
|
-
if (abs >= MiB) return `${(bytes / MiB).toFixed(digits)} MB`;
|
|
340
|
-
if (abs >= 1024) return `${(bytes / 1024).toFixed(digits)} KB`;
|
|
341
|
-
return `${Math.round(bytes)} B`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// src/platform/macos/hardware.ts
|
|
345
|
-
function parseAppleSilicon(chip) {
|
|
346
|
-
const match = chip.match(/Apple\s+(M\d+)(?:\s+(Pro|Max|Ultra))?/i);
|
|
347
|
-
if (!match) return void 0;
|
|
348
|
-
const generation = match[1].toUpperCase().replace(/^M/, "M");
|
|
349
|
-
const variant = match[2] ?? "base";
|
|
350
|
-
return { generation, variant };
|
|
351
|
-
}
|
|
352
|
-
function parseVmStat(output, pageSize) {
|
|
353
|
-
const num = (label) => {
|
|
354
|
-
const row = output.match(new RegExp(`${label}:\\s+([\\d.]+)`));
|
|
355
|
-
if (!row) return void 0;
|
|
356
|
-
return Number(row[1]);
|
|
357
|
-
};
|
|
358
|
-
const free = num("Pages free") ?? 0;
|
|
359
|
-
const speculative = num("Pages speculative") ?? 0;
|
|
360
|
-
const inactive = num("Pages inactive") ?? 0;
|
|
361
|
-
const purgeable = num("Pages purgeable") ?? 0;
|
|
362
|
-
const availableBytes = (free + speculative + inactive + purgeable) * pageSize;
|
|
363
|
-
return { availableBytes };
|
|
364
|
-
}
|
|
365
|
-
function parseMemoryPressure(output) {
|
|
366
|
-
const match = output.match(/System-wide memory free percentage:\s+(\d+)/i);
|
|
367
|
-
if (match) {
|
|
368
|
-
const free = Number(match[1]);
|
|
369
|
-
if (free >= 50) return "Nominal";
|
|
370
|
-
if (free >= 25) return "Warn";
|
|
371
|
-
return "Critical";
|
|
372
|
-
}
|
|
373
|
-
if (/warn/i.test(output)) return "Warn";
|
|
374
|
-
if (/critical/i.test(output)) return "Critical";
|
|
375
|
-
if (/nominal/i.test(output)) return "Nominal";
|
|
376
|
-
return void 0;
|
|
377
|
-
}
|
|
378
|
-
function parseMacosSwapUsage(output) {
|
|
379
|
-
const match = output.match(/used\s*=\s*([\d.]+)\s*([KMGT])?/i);
|
|
380
|
-
if (!match) return void 0;
|
|
381
|
-
const value = Number(match[1]);
|
|
382
|
-
if (Number.isNaN(value)) return void 0;
|
|
383
|
-
const unit = (match[2] ?? "M").toUpperCase();
|
|
384
|
-
const factor = { K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 }[unit] ?? 1024 ** 2;
|
|
385
|
-
return value * factor;
|
|
386
|
-
}
|
|
387
|
-
async function detectMacosHardware() {
|
|
388
|
-
const [brand, physical, logical, memsize, pagesize, perf, eff, hwModel, swVers, profiler, vmstat, pressure, swap] = await Promise.all([
|
|
389
|
-
runCommand("sysctl", ["-n", "machdep.cpu.brand_string"], { timeout: 5e3 }),
|
|
390
|
-
runCommand("sysctl", ["-n", "hw.physicalcpu"], { timeout: 5e3 }),
|
|
391
|
-
runCommand("sysctl", ["-n", "hw.logicalcpu"], { timeout: 5e3 }),
|
|
392
|
-
runCommand("sysctl", ["-n", "hw.memsize"], { timeout: 5e3 }),
|
|
393
|
-
runCommand("sysctl", ["-n", "hw.pagesize"], { timeout: 5e3 }),
|
|
394
|
-
runCommand("sysctl", ["-n", "hw.perflevel0.physicalcpu"], { timeout: 5e3 }),
|
|
395
|
-
runCommand("sysctl", ["-n", "hw.perflevel1.physicalcpu"], { timeout: 5e3 }),
|
|
396
|
-
runCommand("sysctl", ["-n", "hw.model"], { timeout: 5e3 }),
|
|
397
|
-
runCommand("sw_vers", ["-productVersion"], { timeout: 5e3 }),
|
|
398
|
-
runCommand("system_profiler", ["SPHardwareDataType"], { timeout: 15e3 }),
|
|
399
|
-
runCommand("vm_stat", [], { timeout: 5e3 }),
|
|
400
|
-
runCommand("memory_pressure", [], { timeout: 5e3 }),
|
|
401
|
-
runCommand("sysctl", ["-n", "vm.swapusage"], { timeout: 5e3 })
|
|
402
|
-
]);
|
|
403
|
-
const profilerText = profiler.stdout;
|
|
404
|
-
const chip = profilerText.match(/Chip:\s+(.+)/)?.[1]?.trim() ?? brand.stdout.trim() ?? os2.cpus()[0]?.model;
|
|
405
|
-
const modelName = profilerText.match(/Model Name:\s+(.+)/)?.[1]?.trim();
|
|
406
|
-
const memoryLine = profilerText.match(/Memory:\s+(.+)/)?.[1]?.trim();
|
|
407
|
-
const totalFromProfiler = memoryLine ? parseSizeToBytes(memoryLine.replace("GB", "GiB")) : void 0;
|
|
408
|
-
const totalBytes = totalFromProfiler ?? Number(memsize.stdout.trim()) ?? os2.totalmem();
|
|
409
|
-
const pageSize = Number(pagesize.stdout.trim()) || 16384;
|
|
410
|
-
const vm = parseVmStat(vmstat.stdout, pageSize);
|
|
411
|
-
const appleSilicon = chip ? parseAppleSilicon(chip) : void 0;
|
|
412
|
-
return {
|
|
413
|
-
os: "macos",
|
|
414
|
-
osVersion: swVers.stdout.trim() || void 0,
|
|
415
|
-
arch: os2.arch(),
|
|
416
|
-
machineModel: modelName ?? hwModel.stdout.trim() ?? void 0,
|
|
417
|
-
cpu: {
|
|
418
|
-
name: chip,
|
|
419
|
-
physicalCores: Number(physical.stdout.trim()) || os2.cpus().length,
|
|
420
|
-
logicalCores: Number(logical.stdout.trim()) || os2.cpus().length,
|
|
421
|
-
performanceCores: Number(perf.stdout.trim()) || void 0,
|
|
422
|
-
efficiencyCores: Number(eff.stdout.trim()) || void 0,
|
|
423
|
-
appleSilicon
|
|
424
|
-
},
|
|
425
|
-
gpu: {
|
|
426
|
-
name: chip,
|
|
427
|
-
metal: Boolean(appleSilicon) || os2.arch() === "arm64"
|
|
428
|
-
},
|
|
429
|
-
memory: {
|
|
430
|
-
totalBytes,
|
|
431
|
-
availableBytes: vm.availableBytes ?? os2.freemem(),
|
|
432
|
-
unified: Boolean(appleSilicon) || os2.arch() === "arm64",
|
|
433
|
-
swapUsedBytes: parseMacosSwapUsage(swap.stdout),
|
|
434
|
-
pressure: parseMemoryPressure(pressure.stdout + pressure.stderr)
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
function parseMacosMemoryPressure(output) {
|
|
439
|
-
return parseMemoryPressure(output);
|
|
440
|
-
}
|
|
441
|
-
function parseMacosVmStat(output, pageSize) {
|
|
442
|
-
return parseVmStat(output, pageSize);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// src/platform/linux/hardware.ts
|
|
446
|
-
import os3 from "node:os";
|
|
447
|
-
async function detectLinuxHardware() {
|
|
448
|
-
return {
|
|
449
|
-
os: "linux",
|
|
450
|
-
arch: os3.arch(),
|
|
451
|
-
cpu: {
|
|
452
|
-
name: os3.cpus()[0]?.model,
|
|
453
|
-
logicalCores: os3.cpus().length
|
|
454
|
-
},
|
|
455
|
-
gpu: {},
|
|
456
|
-
memory: {
|
|
457
|
-
totalBytes: os3.totalmem(),
|
|
458
|
-
availableBytes: os3.freemem(),
|
|
459
|
-
unified: false
|
|
460
|
-
}
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// src/platform/windows/hardware.ts
|
|
465
|
-
import os4 from "node:os";
|
|
466
|
-
async function detectWindowsHardware() {
|
|
467
|
-
return {
|
|
468
|
-
os: "windows",
|
|
469
|
-
arch: os4.arch(),
|
|
470
|
-
cpu: {
|
|
471
|
-
name: os4.cpus()[0]?.model,
|
|
472
|
-
logicalCores: os4.cpus().length
|
|
473
|
-
},
|
|
474
|
-
gpu: {},
|
|
475
|
-
memory: {
|
|
476
|
-
totalBytes: os4.totalmem(),
|
|
477
|
-
availableBytes: os4.freemem(),
|
|
478
|
-
unified: false
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// src/hardware/detect.ts
|
|
484
|
-
function detectOs() {
|
|
485
|
-
switch (process.platform) {
|
|
486
|
-
case "darwin":
|
|
487
|
-
return "macos";
|
|
488
|
-
case "linux":
|
|
489
|
-
return "linux";
|
|
490
|
-
case "win32":
|
|
491
|
-
return "windows";
|
|
492
|
-
default:
|
|
493
|
-
return "unknown";
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
async function detectHardware() {
|
|
497
|
-
const osName = detectOs();
|
|
498
|
-
if (osName === "macos") return detectMacosHardware();
|
|
499
|
-
if (osName === "linux") return detectLinuxHardware();
|
|
500
|
-
if (osName === "windows") return detectWindowsHardware();
|
|
501
|
-
return {
|
|
502
|
-
os: "unknown",
|
|
503
|
-
arch: os5.arch(),
|
|
504
|
-
cpu: { name: os5.cpus()[0]?.model, logicalCores: os5.cpus().length },
|
|
505
|
-
gpu: {},
|
|
506
|
-
memory: {
|
|
507
|
-
totalBytes: os5.totalmem(),
|
|
508
|
-
availableBytes: os5.freemem(),
|
|
509
|
-
unified: false
|
|
510
|
-
}
|
|
129
|
+
link: flagValue(args2, "--link"),
|
|
130
|
+
structural: flagValue(args2, "--structural"),
|
|
131
|
+
repo: flagValue(args2, "--repo"),
|
|
132
|
+
intent: flagValue(args2, "--intent"),
|
|
133
|
+
limit,
|
|
134
|
+
globs: flagValues(args2, "--glob")
|
|
511
135
|
};
|
|
512
136
|
}
|
|
513
137
|
|
|
514
|
-
// src/models/discovery.ts
|
|
515
|
-
import os7 from "node:os";
|
|
516
|
-
import path5 from "node:path";
|
|
517
|
-
|
|
518
|
-
// src/adapters/ollama/index.ts
|
|
519
|
-
init_process();
|
|
520
|
-
init_which();
|
|
521
|
-
|
|
522
138
|
// src/adapters/ollama/parse.ts
|
|
523
139
|
function parseOllamaList(output) {
|
|
524
140
|
const lines = output.split(/\r?\n/).map((l) => l.trimEnd()).filter(Boolean);
|
|
@@ -569,14 +185,7 @@ var ollamaAdapter = {
|
|
|
569
185
|
async listModels() {
|
|
570
186
|
if (!await this.detect()) return [];
|
|
571
187
|
const result = await runCommand("ollama", ["list"], { timeout: 15e3 });
|
|
572
|
-
|
|
573
|
-
const resolved = await Promise.all(
|
|
574
|
-
models.map(async (model) => {
|
|
575
|
-
const artifact = await resolveOllamaBlob(model.id);
|
|
576
|
-
return { ...model, artifactPath: artifact?.path };
|
|
577
|
-
})
|
|
578
|
-
);
|
|
579
|
-
return resolved;
|
|
188
|
+
return parseOllamaList(result.stdout);
|
|
580
189
|
},
|
|
581
190
|
async resolveModel(id) {
|
|
582
191
|
return resolveOllamaBlob(id);
|
|
@@ -594,15 +203,49 @@ async function resolveOllamaBlob(id) {
|
|
|
594
203
|
return { id, path: from, format };
|
|
595
204
|
}
|
|
596
205
|
|
|
206
|
+
// src/storage/config.ts
|
|
207
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
208
|
+
import { z } from "zod";
|
|
209
|
+
var configSchema = z.object({
|
|
210
|
+
schemaVersion: z.literal(1),
|
|
211
|
+
modelPaths: z.array(z.string()).default([]),
|
|
212
|
+
network: z.boolean().default(true)
|
|
213
|
+
});
|
|
214
|
+
var fallback = { schemaVersion: 1, modelPaths: [], network: true };
|
|
215
|
+
async function loadConfig() {
|
|
216
|
+
await ensureStorage();
|
|
217
|
+
try {
|
|
218
|
+
const raw = await readFile(configPath(), "utf8");
|
|
219
|
+
return configSchema.parse(JSON.parse(raw));
|
|
220
|
+
} catch {
|
|
221
|
+
await saveConfig(fallback);
|
|
222
|
+
return fallback;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function saveConfig(config) {
|
|
226
|
+
await ensureStorage();
|
|
227
|
+
await writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
228
|
+
`, "utf8");
|
|
229
|
+
}
|
|
230
|
+
async function addModelPath(dir) {
|
|
231
|
+
const config = await loadConfig();
|
|
232
|
+
if (!config.modelPaths.includes(dir)) config.modelPaths.push(dir);
|
|
233
|
+
await saveConfig(config);
|
|
234
|
+
return config;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/models/discovery.ts
|
|
238
|
+
import os2 from "node:os";
|
|
239
|
+
import path3 from "node:path";
|
|
240
|
+
|
|
597
241
|
// src/adapters/lmstudio/index.ts
|
|
598
|
-
import
|
|
599
|
-
import
|
|
600
|
-
import { access
|
|
242
|
+
import os from "node:os";
|
|
243
|
+
import path2 from "node:path";
|
|
244
|
+
import { access } from "node:fs/promises";
|
|
601
245
|
|
|
602
246
|
// src/adapters/llamacpp/index.ts
|
|
603
|
-
init_which();
|
|
604
247
|
import { readdir, stat } from "node:fs/promises";
|
|
605
|
-
import
|
|
248
|
+
import path from "node:path";
|
|
606
249
|
var BINARIES = ["llama-bench", "llama-cli", "llama-server"];
|
|
607
250
|
async function detectLlamaCpp() {
|
|
608
251
|
const entries = await Promise.all(BINARIES.map(async (bin) => [bin, Boolean(await commandExists(bin))]));
|
|
@@ -625,7 +268,7 @@ async function walk(dir, depth, maxDepth, source, models) {
|
|
|
625
268
|
return;
|
|
626
269
|
}
|
|
627
270
|
for (const entry of entries) {
|
|
628
|
-
const full =
|
|
271
|
+
const full = path.join(dir, entry);
|
|
629
272
|
try {
|
|
630
273
|
const info = await stat(full);
|
|
631
274
|
if (info.isFile() && entry.toLowerCase().endsWith(".gguf")) {
|
|
@@ -669,18 +312,18 @@ var llamaCppAdapter = {
|
|
|
669
312
|
|
|
670
313
|
// src/adapters/lmstudio/index.ts
|
|
671
314
|
function lmStudioCandidateDirs() {
|
|
672
|
-
const home =
|
|
315
|
+
const home = os.homedir();
|
|
673
316
|
return [
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
317
|
+
path2.join(home, ".lmstudio", "models"),
|
|
318
|
+
path2.join(home, ".cache", "lm-studio", "models"),
|
|
319
|
+
path2.join(home, "Library", "Application Support", "LM Studio", "models")
|
|
677
320
|
];
|
|
678
321
|
}
|
|
679
322
|
async function existingDirs() {
|
|
680
323
|
const found = [];
|
|
681
324
|
for (const dir of lmStudioCandidateDirs()) {
|
|
682
325
|
try {
|
|
683
|
-
await
|
|
326
|
+
await access(dir);
|
|
684
327
|
found.push(dir);
|
|
685
328
|
} catch {
|
|
686
329
|
continue;
|
|
@@ -714,12 +357,12 @@ var lmStudioAdapter = {
|
|
|
714
357
|
|
|
715
358
|
// src/models/discovery.ts
|
|
716
359
|
function defaultGgufDirs() {
|
|
717
|
-
const home =
|
|
360
|
+
const home = os2.homedir();
|
|
718
361
|
return [
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
362
|
+
path3.join(home, "Models"),
|
|
363
|
+
path3.join(home, "models"),
|
|
364
|
+
path3.join(home, ".cache", "llama.cpp"),
|
|
365
|
+
path3.join(home, ".local", "share", "llama.cpp")
|
|
723
366
|
];
|
|
724
367
|
}
|
|
725
368
|
async function discoverLocalModels() {
|
|
@@ -739,7 +382,6 @@ async function discoverLocalModels() {
|
|
|
739
382
|
}
|
|
740
383
|
|
|
741
384
|
// src/adapters/mlx/index.ts
|
|
742
|
-
init_which();
|
|
743
385
|
var mlxAdapter = {
|
|
744
386
|
id: "mlx",
|
|
745
387
|
label: "MLX",
|
|
@@ -761,7 +403,6 @@ var mlxAdapter = {
|
|
|
761
403
|
};
|
|
762
404
|
|
|
763
405
|
// src/runtimes/registry.ts
|
|
764
|
-
init_which();
|
|
765
406
|
var runtimes = [ollamaAdapter, llamaCppAdapter, lmStudioAdapter, mlxAdapter];
|
|
766
407
|
async function detectRuntimes() {
|
|
767
408
|
const llamaBench = Boolean(await commandExists("llama-bench"));
|
|
@@ -846,31 +487,31 @@ function hardwarePayload(hardware) {
|
|
|
846
487
|
gpu: hardware.gpu.name ? { name: hardware.gpu.name } : void 0
|
|
847
488
|
};
|
|
848
489
|
}
|
|
849
|
-
async function postJson(
|
|
490
|
+
async function postJson(path4, body, timeoutMs) {
|
|
850
491
|
const controller = new AbortController();
|
|
851
492
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
852
493
|
try {
|
|
853
|
-
const response = await fetch(`${BASE}${
|
|
494
|
+
const response = await fetch(`${BASE}${path4}`, {
|
|
854
495
|
method: "POST",
|
|
855
496
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
856
497
|
body: JSON.stringify(body),
|
|
857
498
|
signal: controller.signal
|
|
858
499
|
});
|
|
859
|
-
if (!response.ok) throw new Error(`CanIRun ${
|
|
500
|
+
if (!response.ok) throw new Error(`CanIRun ${path4} ${response.status}`);
|
|
860
501
|
return await response.json();
|
|
861
502
|
} finally {
|
|
862
503
|
clearTimeout(timer);
|
|
863
504
|
}
|
|
864
505
|
}
|
|
865
|
-
async function getJson(
|
|
506
|
+
async function getJson(path4, timeoutMs) {
|
|
866
507
|
const controller = new AbortController();
|
|
867
508
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
868
509
|
try {
|
|
869
|
-
const response = await fetch(`${BASE}${
|
|
510
|
+
const response = await fetch(`${BASE}${path4}`, {
|
|
870
511
|
headers: { accept: "application/json" },
|
|
871
512
|
signal: controller.signal
|
|
872
513
|
});
|
|
873
|
-
if (!response.ok) throw new Error(`CanIRun ${
|
|
514
|
+
if (!response.ok) throw new Error(`CanIRun ${path4} ${response.status}`);
|
|
874
515
|
return await response.json();
|
|
875
516
|
} finally {
|
|
876
517
|
clearTimeout(timer);
|
|
@@ -919,16 +560,6 @@ async function fetchRecommendations(hardware, useCase, limit = 5) {
|
|
|
919
560
|
}));
|
|
920
561
|
}
|
|
921
562
|
|
|
922
|
-
// src/models/match.ts
|
|
923
|
-
function normalizeModelId(id) {
|
|
924
|
-
return id.toLowerCase().replace(/[:/]/g, "-").replace(/[^a-z0-9.-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
925
|
-
}
|
|
926
|
-
function idsLikelyMatch(a, b) {
|
|
927
|
-
const na2 = normalizeModelId(a);
|
|
928
|
-
const nb = normalizeModelId(b);
|
|
929
|
-
return na2 === nb || na2.includes(nb) || nb.includes(na2);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
563
|
// src/storage/cache.ts
|
|
933
564
|
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
934
565
|
async function saveCatalogCache(models) {
|
|
@@ -947,489 +578,6 @@ async function loadCatalogCache() {
|
|
|
947
578
|
}
|
|
948
579
|
}
|
|
949
580
|
|
|
950
|
-
// src/i18n/en.ts
|
|
951
|
-
var en = {
|
|
952
|
-
appTitle: "PUDU-AI",
|
|
953
|
-
appSubtitle: "Local AI Hardware & Benchmark Lab",
|
|
954
|
-
byline: "by devjaime",
|
|
955
|
-
aboutDashboard: "Opens the live lab: your machine, installed models, and estimates. Use it to see what can run here before you benchmark or launch a tool.",
|
|
956
|
-
aboutHardware: "Detects CPU, GPU, and Unified Memory/RAM. Use it to know what this computer can host before pulling a model.",
|
|
957
|
-
aboutModels: "Lists installed models vs catalog fits. FIT/EST. SPEED are estimated (CanIRun.ai by midudev); MEASURED is llama-bench history. Use it to separate \u201Calready here\u201D from \u201Cmight run\u201D.",
|
|
958
|
-
aboutRecommend: "Suggests models by use case for this hardware. Estimates only, unless you already measured. Use it to pick a size class, not a quality score.",
|
|
959
|
-
aboutTasks: "Asks what you want (code, video, image, transcription, chat) and returns small OpenCode-style harnesses per model, in this language. Use it to try a model on one concrete job.",
|
|
960
|
-
aboutBenchmark: "Runs llama-bench on a local GGUF and records measured t/s plus system telemetry. Use it when you need real numbers, not estimates.",
|
|
961
|
-
aboutCompare: "Compares saved measured runs (speed, memory, power). Quality is never inferred from speed. Use it after two or more benchmarks.",
|
|
962
|
-
compareHow: "[C] Compare needs two measured llama-bench runs of different models. Press [B], pick a model, wait; repeat with another; then [C]. Quality is not a speed score.",
|
|
963
|
-
compareNeedBench: "Need 2+ measured models. Press [B] to benchmark first.",
|
|
964
|
-
reqOllama: "install: https://ollama.com/download or brew install ollama",
|
|
965
|
-
reqLmStudio: "install: https://lmstudio.ai",
|
|
966
|
-
reqDocker: "install: https://docs.docker.com/get-docker/ or brew install --cask docker",
|
|
967
|
-
dockerHint: "press 4 to run ollama/ollama in Docker",
|
|
968
|
-
dockerStarting: "Starting ollama/ollama container\u2026",
|
|
969
|
-
dockerRunOk: "Docker Ollama is up on port 11434 (name: pudu-ollama).",
|
|
970
|
-
dockerRunFail: "Docker daemon is not running. Open Docker Desktop, wait until it is idle, press 4 again.",
|
|
971
|
-
dockerBrewInstall: "Installing Docker with Homebrew (brew install --cask docker)\u2026",
|
|
972
|
-
dockerDesktopWait: "Starting Docker Desktop and waiting for the daemon\u2026",
|
|
973
|
-
brewInstalling: "Homebrew: installing {pkg}\u2026",
|
|
974
|
-
aboutHistory: "Shows local benchmark files in ~/.pudu-ai/benchmarks. Use it to replay what this machine actually did.",
|
|
975
|
-
aboutDoctor: "Checks Node, Apple Silicon/Metal, Ollama, llama-bench, and the CanIRun API. Use it to see what is missing before a bench or launch.",
|
|
976
|
-
aboutReport: "Writes the latest measured run as Markdown. Use it to paste results into GitHub or notes.",
|
|
977
|
-
aboutLaunch: "Explains Ollama integrations (OpenCode, OpenClaw, Hermes, Claude Code). Pull/launch only with --yes if the model grade is S\u2013B and speed is enough. Use it to wire a coding agent only when this hardware can carry it.",
|
|
978
|
-
aboutAddPath: "Adds a GGUF folder to scan (not the whole disk). Use it so llama-bench can find models outside Ollama.",
|
|
979
|
-
nav: "[S] Setup/Install [B] Benchmark [M] Models [R] Recommend [T] Tasks [H] Hardware [C] Compare [L] History [Q] Quit",
|
|
980
|
-
setupTitle: "SETUP \u2014 INSTALL MODELS & AGENTS",
|
|
981
|
-
setupIntro: "Pick a recommended model (grade S\u2013B), install it with Ollama, then link or install OpenCode, Hermes, or OpenClaw.",
|
|
982
|
-
setupStep1: "1. Recommended models (\u2191\u2193 to choose)",
|
|
983
|
-
setupStep2: "2. Install the selected model",
|
|
984
|
-
setupInstallModel: "Press Enter or I \u2192 ollama pull (only if grade is S, A, or B)",
|
|
985
|
-
setupStep3: "3. Coding agent \u2014 link if present, install if missing",
|
|
986
|
-
setupLinkNow: "installed \u2192 press 1/2/3 to link this model",
|
|
987
|
-
setupInstallAgent: "not installed \u2192 press 1/2/3 to install via ollama launch (if eligible)",
|
|
988
|
-
setupKeys: "Enter/I = pull model 1 OpenCode 2 Hermes 3 OpenClaw Esc = back",
|
|
989
|
-
setupCta: "[S] Setup: install model + OpenCode / Hermes / OpenClaw",
|
|
990
|
-
machine: "MACHINE",
|
|
991
|
-
runtimes: "LOCAL AI RUNTIMES",
|
|
992
|
-
installedModels: "INSTALLED MODELS",
|
|
993
|
-
compatible: "COMPATIBLE (catalog, estimated)",
|
|
994
|
-
recommended: "RECOMMENDED FOR THIS MACHINE",
|
|
995
|
-
recommendedHint: "Estimated values come from CanIRun.ai by midudev (canirun.ai \xB7 midu.dev) or a local fallback \u2014 not measured",
|
|
996
|
-
recommendKeys: "[\u2191\u2193] model [I] ollama pull if grade S\u2013B [O] OpenCode [E] Hermes [W] OpenClaw [Esc] back",
|
|
997
|
-
recommendCliHint: "Install: npx pudu-ai recommend --install --yes Link: npx pudu-ai recommend --link opencode --yes",
|
|
998
|
-
agentsTitle: "CODING AGENTS",
|
|
999
|
-
agentMissing: "not installed \u2014 select to install via ollama launch if the model is eligible",
|
|
1000
|
-
installGradeBlock: "Grade {grade} is too low to pull. Need S, A, or B.",
|
|
1001
|
-
linkingTool: "Linking {tool} with {model}\u2026",
|
|
1002
|
-
modelPulled: "Pulled {model} with Ollama.",
|
|
1003
|
-
installNone: "No recommended model has grade S\u2013B to install.",
|
|
1004
|
-
noOllamaTag: "No Ollama library tag for {model}. Catalog names like Agents-A1 are not pullable. Use qwen3:8b, gemma3:4b, qwen3.5:4b, llama3.1:8b.",
|
|
1005
|
-
credits: "Measured: llama-bench + OS telemetry. Estimated: CanIRun.ai by midudev (https://canirun.ai, https://github.com/midudev/canirun.ai, https://midu.dev). Labelled separately; never mixed.",
|
|
1006
|
-
noModels: "No local models detected",
|
|
1007
|
-
notTested: "Not tested",
|
|
1008
|
-
detected: "detected",
|
|
1009
|
-
notDetected: "not detected",
|
|
1010
|
-
unknownMachine: "Unknown machine",
|
|
1011
|
-
cpuNa: "CPU N/A",
|
|
1012
|
-
selectBenchmark: "Select model to benchmark",
|
|
1013
|
-
enterToRun: "Enter to run llama-bench \xB7 Esc back",
|
|
1014
|
-
noGguf: "No GGUF-resolvable models to benchmark.",
|
|
1015
|
-
performance: "PERFORMANCE",
|
|
1016
|
-
system: "SYSTEM",
|
|
1017
|
-
powerThermals: "POWER / THERMALS",
|
|
1018
|
-
elapsed: "Elapsed",
|
|
1019
|
-
measuredAfter: "Prompt / generation t/s appear after llama-bench finishes (measured).",
|
|
1020
|
-
permissionsHint: "GPU % and power need extra permissions; shown as N/A when not measured.",
|
|
1021
|
-
cancelHint: "Ctrl+C / Q cancels llama-bench and telemetry",
|
|
1022
|
-
saved: "Saved",
|
|
1023
|
-
hardwareHint: "Power, GPU %, and thermals require extra OS permissions and show N/A when unavailable.",
|
|
1024
|
-
modelsHint: "FIT and EST. SPEED are estimated. MEASURED comes from local llama-bench history.",
|
|
1025
|
-
historyEmpty: "No benchmark history in ~/.pudu-ai/benchmarks",
|
|
1026
|
-
compareNeedTwo: "Need at least two measured benchmarks to compare.",
|
|
1027
|
-
compareTitle: "LOCAL MODEL BENCHMARKS (measured)",
|
|
1028
|
-
winner: "Winner",
|
|
1029
|
-
qualityNote: "Quality (not derived from speed; see catalog metadata)",
|
|
1030
|
-
doctorTitle: "Pudu-AI Doctor",
|
|
1031
|
-
doctorReady: "Ready to benchmark {count} installed models.",
|
|
1032
|
-
doctorNoBench: "llama-bench unavailable. Install llama.cpp, e.g. `brew install llama.cpp`. Pudu-AI will not install native dependencies.",
|
|
1033
|
-
loading: "Inspecting hardware, runtimes, and local models\u2026",
|
|
1034
|
-
addedPath: "Added model path {path}",
|
|
1035
|
-
modelNotFound: "Model not found: {id}",
|
|
1036
|
-
noHistory: "No benchmark history.",
|
|
1037
|
-
jsonNeedModel: "Pass a model id for JSON mode, e.g. npx pudu-ai benchmark qwen3:8b --json",
|
|
1038
|
-
useCaseCoding: "Coding",
|
|
1039
|
-
useCaseGeneral: "General",
|
|
1040
|
-
useCaseReasoning: "Reasoning",
|
|
1041
|
-
useCaseLightweight: "Lightweight",
|
|
1042
|
-
estimated: "Estimated",
|
|
1043
|
-
measured: "Measured",
|
|
1044
|
-
scoreLabel: "Pudu-AI Score",
|
|
1045
|
-
reportTitle: "Pudu-AI Benchmark",
|
|
1046
|
-
benchmarkTitle: "Pudu-AI Benchmark",
|
|
1047
|
-
tasksTitle: "TASK HARNESSES",
|
|
1048
|
-
tasksHint: "Small OpenCode-style tasks in your selected language. Installed models first. Catalog matches are Estimated (CanIRun.ai by midudev).",
|
|
1049
|
-
tasksKinds: "Work types",
|
|
1050
|
-
tasksScope: "Scope",
|
|
1051
|
-
tasksPriority: "Priority",
|
|
1052
|
-
tasksInstalled: "installed",
|
|
1053
|
-
tasksNotInstalled: "not installed",
|
|
1054
|
-
tasksEmpty: "No matching models for those work types on this machine.",
|
|
1055
|
-
tasksQ1: "What do you want to do? (space to toggle, enter next)",
|
|
1056
|
-
tasksQ2: "Use only installed models, or also catalog estimates?",
|
|
1057
|
-
tasksQ3: "Priority?",
|
|
1058
|
-
tasksOptCode: "Code",
|
|
1059
|
-
tasksOptVideo: "Video",
|
|
1060
|
-
tasksOptImage: "Image",
|
|
1061
|
-
tasksOptTranscription: "Transcription",
|
|
1062
|
-
tasksOptChat: "Tasks / chat",
|
|
1063
|
-
tasksOptInstalled: "Installed only",
|
|
1064
|
-
tasksOptAll: "Installed + catalog (estimated)",
|
|
1065
|
-
tasksOptSpeed: "Speed",
|
|
1066
|
-
tasksOptBalanced: "Balanced",
|
|
1067
|
-
tasksOptQuality: "Quality",
|
|
1068
|
-
taskCodeReviewTitle: "Review a local diff",
|
|
1069
|
-
taskCodeReviewPrompt: "In OpenCode: open a git diff and ask this model to list bugs, missing tests, and a 5-line summary. Do not apply patches unless you ask.",
|
|
1070
|
-
taskCodeTestsTitle: "Write one failing test",
|
|
1071
|
-
taskCodeTestsPrompt: "Pick one function. Ask the model for a single Vitest/Jest test that fails on the current bug. Do not generate extra files.",
|
|
1072
|
-
taskChatPlanTitle: "Turn a goal into a task list",
|
|
1073
|
-
taskChatPlanPrompt: "Give the model one goal. Require a numbered plan of 5 harness-sized tasks. No implementation yet.",
|
|
1074
|
-
taskChatAgentTitle: "Agent loop on one file",
|
|
1075
|
-
taskChatAgentPrompt: "Point the model at one file and one acceptance check. It may edit only that file, then stop.",
|
|
1076
|
-
taskImageCaptionTitle: "Caption a local image",
|
|
1077
|
-
taskImageCaptionPrompt: "If this is a vision/image model, caption one local image in your UI language. If it is text-only, skip and say N/A.",
|
|
1078
|
-
taskImageBriefTitle: "Image generation brief",
|
|
1079
|
-
taskImageBriefPrompt: "Ask for a 6-line prompt brief (subject, lens, light, negative prompt) for one still. Do not invent that the model rendered the image unless it did.",
|
|
1080
|
-
taskVideoBoardTitle: "8-shot storyboard",
|
|
1081
|
-
taskVideoBoardPrompt: "Ask for 8 shots: duration, camera, action, on-screen text. Keep it local. Do not claim a render exists.",
|
|
1082
|
-
taskVideoShotTitle: "Shot list from a script",
|
|
1083
|
-
taskVideoShotPrompt: "Paste a short script. Ask for a shot list and estimated seconds. Text-only models may plan; they cannot encode video.",
|
|
1084
|
-
taskTranscribeCleanTitle: "Clean a transcript",
|
|
1085
|
-
taskTranscribeCleanPrompt: "Paste noisy speech-to-text. Ask the model to punctuate, remove fillers, and keep speaker labels. This is not ASR; it edits text.",
|
|
1086
|
-
taskTranscribeActionsTitle: "Actions from a transcript",
|
|
1087
|
-
taskTranscribeActionsPrompt: "From a meeting transcript, extract owners, due dates, and open questions as a checklist in your UI language.",
|
|
1088
|
-
launchTitle: "OLLAMA INTEGRATIONS",
|
|
1089
|
-
launchHint: "Explain-only by default. Pull/install/launch run only with --yes and only if a recommended model fits this hardware (grade S\u2013B; measured speed when available). Docs: OpenCode, OpenClaw, Hermes, Claude Code via Ollama.",
|
|
1090
|
-
launchNeedOllama: "Ollama is not detected. Install Ollama first. Pudu-AI will not install it.",
|
|
1091
|
-
launchNoModel: "No coding/chat model on this machine meets the hardware bar for this integration.",
|
|
1092
|
-
launchGradeFail: "Grade {grade} is below the allowed set ({allowed}).",
|
|
1093
|
-
launchSlowFail: "Measured {tps} t/s is below the {min} t/s agent floor.",
|
|
1094
|
-
launchEstimateWeak: "Only a weak estimate is available; will not pull or launch.",
|
|
1095
|
-
launchOk: "Eligible on this hardware for recommended tasks.",
|
|
1096
|
-
launchModel: "Model",
|
|
1097
|
-
launchRunHint: "To pull (if needed) and launch: npx pudu-ai launch {tool} --yes",
|
|
1098
|
-
launchBlocked: "Blocked. No pull, install, or launch.",
|
|
1099
|
-
launchNeedTool: "Pass a tool: opencode | openclaw | hermes | claude",
|
|
1100
|
-
launchUnknown: "Unknown integration.",
|
|
1101
|
-
launchPulling: "Pulling {model} with ollama pull\u2026",
|
|
1102
|
-
launchPullFail: "ollama pull failed.",
|
|
1103
|
-
launchExecFail: "ollama launch failed.",
|
|
1104
|
-
help: `Pudu-AI \u2014 local AI hardware & benchmark lab
|
|
1105
|
-
|
|
1106
|
-
Usage:
|
|
1107
|
-
npx pudu-ai
|
|
1108
|
-
npx pudu-ai hardware
|
|
1109
|
-
npx pudu-ai models
|
|
1110
|
-
npx pudu-ai models add-path ~/Models
|
|
1111
|
-
npx pudu-ai setup
|
|
1112
|
-
npx pudu-ai recommend
|
|
1113
|
-
npx pudu-ai recommend --install --yes
|
|
1114
|
-
npx pudu-ai recommend --link opencode --yes
|
|
1115
|
-
npx pudu-ai tasks
|
|
1116
|
-
npx pudu-ai tasks --for code,image --scope all --priority speed
|
|
1117
|
-
npx pudu-ai benchmark [model]
|
|
1118
|
-
npx pudu-ai compare
|
|
1119
|
-
npx pudu-ai history
|
|
1120
|
-
npx pudu-ai doctor
|
|
1121
|
-
npx pudu-ai report --markdown
|
|
1122
|
-
npx pudu-ai launch
|
|
1123
|
-
npx pudu-ai launch opencode
|
|
1124
|
-
npx pudu-ai launch opencode --yes
|
|
1125
|
-
|
|
1126
|
-
Flags:
|
|
1127
|
-
--json Machine-readable JSON (no TUI)
|
|
1128
|
-
--csv CSV output
|
|
1129
|
-
--no-network Skip CanIRun API; use cache/local estimates
|
|
1130
|
-
--no-color Disable ANSI color
|
|
1131
|
-
--verbose Debug logs to stderr
|
|
1132
|
-
--preset quick | standard | stress
|
|
1133
|
-
--lang en | es
|
|
1134
|
-
--for code,video,image,transcription,chat
|
|
1135
|
-
--scope installed | all
|
|
1136
|
-
--priority speed | balanced | quality
|
|
1137
|
-
--yes Execute pull/launch only if the model is eligible
|
|
1138
|
-
|
|
1139
|
-
Credits:
|
|
1140
|
-
Measured values come from llama-bench and OS telemetry.
|
|
1141
|
-
Estimated values come from CanIRun.ai by midudev
|
|
1142
|
-
(https://canirun.ai \xB7 https://github.com/midudev/canirun.ai \xB7 https://midu.dev)
|
|
1143
|
-
or a local fallback, and are labelled as such.
|
|
1144
|
-
`
|
|
1145
|
-
};
|
|
1146
|
-
|
|
1147
|
-
// src/i18n/es.ts
|
|
1148
|
-
var es = {
|
|
1149
|
-
appTitle: "PUDU-AI",
|
|
1150
|
-
appSubtitle: "Laboratorio local de hardware y benchmarks de IA",
|
|
1151
|
-
byline: "by devjaime",
|
|
1152
|
-
aboutDashboard: "Abre el laboratorio en vivo: equipo, modelos instalados y estimaciones. Sirve para ver qu\xE9 puede correr aqu\xED antes de medir o lanzar una herramienta.",
|
|
1153
|
-
aboutHardware: "Detecta CPU, GPU y memoria unificada/RAM. Sirve para saber qu\xE9 puede hospedar este equipo antes de descargar un modelo.",
|
|
1154
|
-
aboutModels: "Lista instalados frente al cat\xE1logo. FIT/EST. SPEED son estimados (CanIRun.ai de midudev); MEASURED es historial de llama-bench. Sirve para separar \u201Cya est\xE1\u201D de \u201Cpodr\xEDa correr\u201D.",
|
|
1155
|
-
aboutRecommend: "Sugiere modelos por caso de uso para este hardware. Solo estimaciones, salvo que ya hayas medido. Sirve para elegir un tama\xF1o, no una nota de calidad.",
|
|
1156
|
-
aboutTasks: "Pregunta qu\xE9 quieres (c\xF3digo, v\xEDdeo, imagen, transcripci\xF3n, chat) y devuelve harnesses peque\xF1os estilo OpenCode por modelo, en este idioma. Sirve para probar un modelo en una tarea concreta.",
|
|
1157
|
-
aboutBenchmark: "Ejecuta llama-bench sobre un GGUF local y guarda t/s medidos m\xE1s telemetr\xEDa. Sirve cuando necesitas n\xFAmeros reales, no estimaciones.",
|
|
1158
|
-
aboutCompare: "Compara corridas medidas (velocidad, memoria, potencia). La calidad no se infiere de la velocidad. Sirve despu\xE9s de dos o m\xE1s benchmarks.",
|
|
1159
|
-
compareHow: "[C] Comparar necesita dos corridas medidas de llama-bench en modelos distintos. Pulsa [B], elige un modelo, espera; repite con otro; luego [C]. La calidad no es un score de velocidad.",
|
|
1160
|
-
compareNeedBench: "Hacen falta 2+ modelos medidos. Pulsa [B] para hacer un benchmark primero.",
|
|
1161
|
-
reqOllama: "instalar: https://ollama.com/download o brew install ollama",
|
|
1162
|
-
reqLmStudio: "instalar: https://lmstudio.ai",
|
|
1163
|
-
reqDocker: "instalar: https://docs.docker.com/get-docker/ o brew install --cask docker",
|
|
1164
|
-
dockerHint: "pulsa 4 para correr ollama/ollama en Docker",
|
|
1165
|
-
dockerStarting: "Arrancando contenedor ollama/ollama\u2026",
|
|
1166
|
-
dockerRunOk: "Ollama en Docker est\xE1 en el puerto 11434 (nombre: pudu-ollama).",
|
|
1167
|
-
dockerRunFail: "El daemon de Docker no corre. Abre Docker Desktop, espera a que est\xE9 listo y pulsa 4 otra vez.",
|
|
1168
|
-
dockerBrewInstall: "Instalando Docker con Homebrew (brew install --cask docker)\u2026",
|
|
1169
|
-
dockerDesktopWait: "Arrancando Docker Desktop y esperando al daemon\u2026",
|
|
1170
|
-
brewInstalling: "Homebrew: instalando {pkg}\u2026",
|
|
1171
|
-
aboutHistory: "Muestra los JSON locales en ~/.pudu-ai/benchmarks. Sirve para ver qu\xE9 hizo realmente esta m\xE1quina.",
|
|
1172
|
-
aboutDoctor: "Revisa Node, Apple Silicon/Metal, Ollama, llama-bench y la API de CanIRun. Sirve para ver qu\xE9 falta antes de un bench o un launch.",
|
|
1173
|
-
aboutReport: "Exporta la \xFAltima corrida medida en Markdown. Sirve para pegar resultados en GitHub o notas.",
|
|
1174
|
-
aboutLaunch: "Explica las integraciones de Ollama (OpenCode, OpenClaw, Hermes, Claude Code). Pull/launch solo con --yes si la nota es S\u2013B y hay velocidad suficiente. Sirve para conectar un agente de c\xF3digo solo cuando el hardware lo aguanta.",
|
|
1175
|
-
aboutAddPath: "A\xF1ade una carpeta GGUF a escanear (no todo el disco). Sirve para que llama-bench encuentre modelos fuera de Ollama.",
|
|
1176
|
-
nav: "[S] Setup/Instalar [B] Benchmark [M] Modelos [R] Recomendaciones [T] Tareas [H] Hardware [C] Comparar [L] Historial [Q] Salir",
|
|
1177
|
-
setupTitle: "SETUP \u2014 INSTALAR MODELOS Y AGENTES",
|
|
1178
|
-
setupIntro: "Elige un modelo recomendado (nota S\u2013B), inst\xE1lalo con Ollama y luego vincula o instala OpenCode, Hermes u OpenClaw.",
|
|
1179
|
-
setupStep1: "1. Modelos recomendados (\u2191\u2193 para elegir)",
|
|
1180
|
-
setupStep2: "2. Instalar el modelo seleccionado",
|
|
1181
|
-
setupInstallModel: "Enter o I \u2192 ollama pull (solo si la nota es S, A o B)",
|
|
1182
|
-
setupStep3: "3. Agente de c\xF3digo \u2014 vincular si existe, instalar si falta",
|
|
1183
|
-
setupLinkNow: "instalado \u2192 pulsa 1/2/3 para vincular este modelo",
|
|
1184
|
-
setupInstallAgent: "no instalado \u2192 pulsa 1/2/3 para instalar con ollama launch (si es elegible)",
|
|
1185
|
-
setupKeys: "Enter/I = descargar modelo 1 OpenCode 2 Hermes 3 OpenClaw Esc = atr\xE1s",
|
|
1186
|
-
setupCta: "[S] Setup: instalar modelo + OpenCode / Hermes / OpenClaw",
|
|
1187
|
-
machine: "EQUIPO",
|
|
1188
|
-
runtimes: "RUNTIMES DE IA LOCAL",
|
|
1189
|
-
installedModels: "MODELOS INSTALADOS",
|
|
1190
|
-
compatible: "COMPATIBLES (cat\xE1logo, estimado)",
|
|
1191
|
-
recommended: "RECOMENDADOS PARA ESTE EQUIPO",
|
|
1192
|
-
recommendedHint: "Los valores estimados vienen de CanIRun.ai de midudev (canirun.ai \xB7 midu.dev) o de un fallback local \u2014 no son mediciones",
|
|
1193
|
-
recommendKeys: "[\u2191\u2193] modelo [I] ollama pull si nota S\u2013B [O] OpenCode [E] Hermes [W] OpenClaw [Esc] atr\xE1s",
|
|
1194
|
-
recommendCliHint: "Instalar: npx pudu-ai recommend --install --yes Vincular: npx pudu-ai recommend --link opencode --yes",
|
|
1195
|
-
agentsTitle: "AGENTES DE C\xD3DIGO",
|
|
1196
|
-
agentMissing: "no instalado \u2014 selecci\xF3nalo para instalar con ollama launch si el modelo es elegible",
|
|
1197
|
-
installGradeBlock: "La nota {grade} es baja para hacer pull. Hace falta S, A o B.",
|
|
1198
|
-
linkingTool: "Vinculando {tool} con {model}\u2026",
|
|
1199
|
-
modelPulled: "Descargado {model} con Ollama.",
|
|
1200
|
-
installNone: "Ning\xFAn modelo recomendado tiene nota S\u2013B para instalar.",
|
|
1201
|
-
noOllamaTag: "No hay tag de Ollama para {model}. Nombres de cat\xE1logo como Agents-A1 no se pueden descargar. Usa qwen3:8b, gemma3:4b, qwen3.5:4b, llama3.1:8b.",
|
|
1202
|
-
credits: "Medido: llama-bench + telemetr\xEDa del SO. Estimado: CanIRun.ai de midudev (https://canirun.ai, https://github.com/midudev/canirun.ai, https://midu.dev). Se etiquetan por separado; nunca se mezclan.",
|
|
1203
|
-
noModels: "No se detectaron modelos locales",
|
|
1204
|
-
notTested: "Sin probar",
|
|
1205
|
-
detected: "detectado",
|
|
1206
|
-
notDetected: "no detectado",
|
|
1207
|
-
unknownMachine: "Equipo desconocido",
|
|
1208
|
-
cpuNa: "CPU N/D",
|
|
1209
|
-
selectBenchmark: "Selecciona un modelo para medir",
|
|
1210
|
-
enterToRun: "Enter para ejecutar llama-bench \xB7 Esc atr\xE1s",
|
|
1211
|
-
noGguf: "No hay modelos GGUF resolubles para medir.",
|
|
1212
|
-
performance: "RENDIMIENTO",
|
|
1213
|
-
system: "SISTEMA",
|
|
1214
|
-
powerThermals: "ENERG\xCDA / T\xC9RMICAS",
|
|
1215
|
-
elapsed: "Transcurrido",
|
|
1216
|
-
measuredAfter: "Los t/s de prompt y generaci\xF3n aparecen al terminar llama-bench (medidos).",
|
|
1217
|
-
permissionsHint: "El % de GPU y la potencia requieren permisos extra; se muestra N/D si no se puede medir.",
|
|
1218
|
-
cancelHint: "Ctrl+C / Q cancela llama-bench y la telemetr\xEDa",
|
|
1219
|
-
saved: "Guardado",
|
|
1220
|
-
hardwareHint: "Potencia, % de GPU y t\xE9rmicas requieren permisos extra del SO; si no hay datos se muestra N/D.",
|
|
1221
|
-
modelsHint: "FIT y EST. SPEED son estimados. MEASURED sale del historial local de llama-bench.",
|
|
1222
|
-
historyEmpty: "No hay historial de benchmarks en ~/.pudu-ai/benchmarks",
|
|
1223
|
-
compareNeedTwo: "Se necesitan al menos dos benchmarks medidos para comparar.",
|
|
1224
|
-
compareTitle: "BENCHMARKS LOCALES (medidos)",
|
|
1225
|
-
winner: "Ganador",
|
|
1226
|
-
qualityNote: "Calidad (no se deriva de la velocidad; ver metadatos del cat\xE1logo)",
|
|
1227
|
-
doctorTitle: "Pudu-AI Doctor",
|
|
1228
|
-
doctorReady: "Listo para medir {count} modelos instalados.",
|
|
1229
|
-
doctorNoBench: "llama-bench no est\xE1 disponible. Instala llama.cpp, p. ej. `brew install llama.cpp`. Pudu-AI no instala dependencias nativas.",
|
|
1230
|
-
loading: "Inspeccionando hardware, runtimes y modelos locales\u2026",
|
|
1231
|
-
addedPath: "Ruta de modelos a\xF1adida {path}",
|
|
1232
|
-
modelNotFound: "Modelo no encontrado: {id}",
|
|
1233
|
-
noHistory: "No hay historial de benchmarks.",
|
|
1234
|
-
jsonNeedModel: "Pasa un id de modelo en modo JSON, p. ej. npx pudu-ai benchmark qwen3:8b --json",
|
|
1235
|
-
useCaseCoding: "C\xF3digo",
|
|
1236
|
-
useCaseGeneral: "General",
|
|
1237
|
-
useCaseReasoning: "Razonamiento",
|
|
1238
|
-
useCaseLightweight: "Ligero",
|
|
1239
|
-
estimated: "Estimado",
|
|
1240
|
-
measured: "Medido",
|
|
1241
|
-
scoreLabel: "Puntuaci\xF3n Pudu-AI",
|
|
1242
|
-
reportTitle: "Benchmark Pudu-AI",
|
|
1243
|
-
benchmarkTitle: "Benchmark Pudu-AI",
|
|
1244
|
-
tasksTitle: "HARNESSES DE TAREAS",
|
|
1245
|
-
tasksHint: "Tareas peque\xF1as estilo OpenCode en tu idioma. Primero modelos instalados. El cat\xE1logo es Estimado (CanIRun.ai de midudev).",
|
|
1246
|
-
tasksKinds: "Tipos de trabajo",
|
|
1247
|
-
tasksScope: "Alcance",
|
|
1248
|
-
tasksPriority: "Prioridad",
|
|
1249
|
-
tasksInstalled: "instalado",
|
|
1250
|
-
tasksNotInstalled: "no instalado",
|
|
1251
|
-
tasksEmpty: "No hay modelos que coincidan con esos tipos de trabajo en este equipo.",
|
|
1252
|
-
tasksQ1: "\xBFQu\xE9 quieres hacer? (espacio para marcar, enter para seguir)",
|
|
1253
|
-
tasksQ2: "\xBFSolo modelos instalados, o tambi\xE9n estimaciones del cat\xE1logo?",
|
|
1254
|
-
tasksQ3: "\xBFPrioridad?",
|
|
1255
|
-
tasksOptCode: "C\xF3digo",
|
|
1256
|
-
tasksOptVideo: "V\xEDdeo",
|
|
1257
|
-
tasksOptImage: "Imagen",
|
|
1258
|
-
tasksOptTranscription: "Transcripci\xF3n",
|
|
1259
|
-
tasksOptChat: "Tareas / chat",
|
|
1260
|
-
tasksOptInstalled: "Solo instalados",
|
|
1261
|
-
tasksOptAll: "Instalados + cat\xE1logo (estimado)",
|
|
1262
|
-
tasksOptSpeed: "Velocidad",
|
|
1263
|
-
tasksOptBalanced: "Equilibrado",
|
|
1264
|
-
tasksOptQuality: "Calidad",
|
|
1265
|
-
taskCodeReviewTitle: "Revisar un diff local",
|
|
1266
|
-
taskCodeReviewPrompt: "En OpenCode: abre un git diff y pide a este modelo bugs, tests faltantes y un resumen de 5 l\xEDneas. No apliques parches salvo que lo pidas.",
|
|
1267
|
-
taskCodeTestsTitle: "Escribir un test que falle",
|
|
1268
|
-
taskCodeTestsPrompt: "Elige una funci\xF3n. Pide un solo test Vitest/Jest que falle con el bug actual. No generes archivos extra.",
|
|
1269
|
-
taskChatPlanTitle: "Convertir un objetivo en tareas",
|
|
1270
|
-
taskChatPlanPrompt: "Dale un objetivo. Exige un plan numerado de 5 tareas tama\xF1o harness. Sin implementar todav\xEDa.",
|
|
1271
|
-
taskChatAgentTitle: "Bucle de agente en un archivo",
|
|
1272
|
-
taskChatAgentPrompt: "Se\xF1ala un archivo y un criterio de aceptaci\xF3n. Solo puede editar ese archivo y luego parar.",
|
|
1273
|
-
taskImageCaptionTitle: "Describir una imagen local",
|
|
1274
|
-
taskImageCaptionPrompt: "Si es un modelo de visi\xF3n/imagen, describe una imagen local en el idioma de la UI. Si es solo texto, indica N/D.",
|
|
1275
|
-
taskImageBriefTitle: "Brief de generaci\xF3n de imagen",
|
|
1276
|
-
taskImageBriefPrompt: "Pide un brief de 6 l\xEDneas (sujeto, lente, luz, negative prompt) para un still. No inventes que el modelo renderiz\xF3 la imagen.",
|
|
1277
|
-
taskVideoBoardTitle: "Storyboard de 8 planos",
|
|
1278
|
-
taskVideoBoardPrompt: "Pide 8 planos: duraci\xF3n, c\xE1mara, acci\xF3n, texto en pantalla. Local. No afirmes que existe un render.",
|
|
1279
|
-
taskVideoShotTitle: "Lista de planos desde un guion",
|
|
1280
|
-
taskVideoShotPrompt: "Pega un guion corto. Pide lista de planos y segundos estimados. Un modelo de texto puede planear; no puede encodear v\xEDdeo.",
|
|
1281
|
-
taskTranscribeCleanTitle: "Limpiar una transcripci\xF3n",
|
|
1282
|
-
taskTranscribeCleanPrompt: "Pega texto de speech-to-text ruidoso. Pide puntuaci\xF3n, quitar muletillas y mantener hablantes. Esto no es ASR; edita texto.",
|
|
1283
|
-
taskTranscribeActionsTitle: "Acciones desde una transcripci\xF3n",
|
|
1284
|
-
taskTranscribeActionsPrompt: "De una reuni\xF3n transcrita, extrae responsables, fechas y preguntas abiertas como checklist en el idioma de la UI.",
|
|
1285
|
-
launchTitle: "INTEGRACIONES OLLAMA",
|
|
1286
|
-
launchHint: "Por defecto solo explica. Pull/instalaci\xF3n/launch solo con --yes y solo si un modelo recomendado cabe en este hardware (nota S\u2013B; velocidad medida si existe). Docs: OpenCode, OpenClaw, Hermes, Claude Code v\xEDa Ollama.",
|
|
1287
|
-
launchNeedOllama: "Ollama no est\xE1 detectado. Inst\xE1lalo primero. Pudu-AI no lo instala.",
|
|
1288
|
-
launchNoModel: "Ning\xFAn modelo de c\xF3digo/chat en este equipo cumple el umbral de hardware para esta integraci\xF3n.",
|
|
1289
|
-
launchGradeFail: "La nota {grade} est\xE1 por debajo del conjunto permitido ({allowed}).",
|
|
1290
|
-
launchSlowFail: "Los {tps} t/s medidos est\xE1n por debajo del m\xEDnimo de agente ({min} t/s).",
|
|
1291
|
-
launchEstimateWeak: "Solo hay una estimaci\xF3n d\xE9bil; no se har\xE1 pull ni launch.",
|
|
1292
|
-
launchOk: "Elegible en este hardware para las tareas recomendadas.",
|
|
1293
|
-
launchModel: "Modelo",
|
|
1294
|
-
launchRunHint: "Para hacer pull (si hace falta) y lanzar: npx pudu-ai launch {tool} --yes",
|
|
1295
|
-
launchBlocked: "Bloqueado. Sin pull, instalaci\xF3n ni launch.",
|
|
1296
|
-
launchNeedTool: "Indica una herramienta: opencode | openclaw | hermes | claude",
|
|
1297
|
-
launchUnknown: "Integraci\xF3n desconocida.",
|
|
1298
|
-
launchPulling: "Descargando {model} con ollama pull\u2026",
|
|
1299
|
-
launchPullFail: "Fall\xF3 ollama pull.",
|
|
1300
|
-
launchExecFail: "Fall\xF3 ollama launch.",
|
|
1301
|
-
help: `Pudu-AI \u2014 laboratorio local de hardware y benchmarks de IA
|
|
1302
|
-
|
|
1303
|
-
Uso:
|
|
1304
|
-
npx pudu-ai
|
|
1305
|
-
npx pudu-ai hardware
|
|
1306
|
-
npx pudu-ai models
|
|
1307
|
-
npx pudu-ai models add-path ~/Models
|
|
1308
|
-
npx pudu-ai setup
|
|
1309
|
-
npx pudu-ai recommend
|
|
1310
|
-
npx pudu-ai recommend --install --yes
|
|
1311
|
-
npx pudu-ai recommend --link opencode --yes
|
|
1312
|
-
npx pudu-ai tasks
|
|
1313
|
-
npx pudu-ai tasks --for code,image --scope all --priority speed
|
|
1314
|
-
npx pudu-ai benchmark [model]
|
|
1315
|
-
npx pudu-ai compare
|
|
1316
|
-
npx pudu-ai history
|
|
1317
|
-
npx pudu-ai doctor
|
|
1318
|
-
npx pudu-ai report --markdown
|
|
1319
|
-
npx pudu-ai launch
|
|
1320
|
-
npx pudu-ai launch opencode
|
|
1321
|
-
npx pudu-ai launch opencode --yes
|
|
1322
|
-
|
|
1323
|
-
Flags:
|
|
1324
|
-
--json JSON legible por m\xE1quinas (sin TUI)
|
|
1325
|
-
--csv Salida CSV
|
|
1326
|
-
--no-network Omite la API de CanIRun; usa cach\xE9/estimaciones locales
|
|
1327
|
-
--no-color Sin color ANSI
|
|
1328
|
-
--verbose Logs de depuraci\xF3n en stderr
|
|
1329
|
-
--preset quick | standard | stress
|
|
1330
|
-
--lang en | es
|
|
1331
|
-
--for code,video,image,transcription,chat
|
|
1332
|
-
--scope installed | all
|
|
1333
|
-
--priority speed | balanced | quality
|
|
1334
|
-
--yes Ejecuta pull/launch solo si el modelo es elegible
|
|
1335
|
-
|
|
1336
|
-
Cr\xE9ditos:
|
|
1337
|
-
Los valores medidos vienen de llama-bench y de la telemetr\xEDa del SO.
|
|
1338
|
-
Los valores estimados vienen de CanIRun.ai de midudev
|
|
1339
|
-
(https://canirun.ai \xB7 https://github.com/midudev/canirun.ai \xB7 https://midu.dev)
|
|
1340
|
-
o de un fallback local, y se etiquetan como tal.
|
|
1341
|
-
`
|
|
1342
|
-
};
|
|
1343
|
-
|
|
1344
|
-
// src/i18n/index.ts
|
|
1345
|
-
var locales = { en, es };
|
|
1346
|
-
var current = "en";
|
|
1347
|
-
function resolveLocale(raw) {
|
|
1348
|
-
const value = (raw ?? process.env.PUDU_AI_LANG ?? process.env.PUDU_LANG ?? process.env.LANG ?? "en").toLowerCase();
|
|
1349
|
-
if (value.startsWith("es")) return "es";
|
|
1350
|
-
return "en";
|
|
1351
|
-
}
|
|
1352
|
-
function setLocale(locale) {
|
|
1353
|
-
current = locale in locales ? locale : "en";
|
|
1354
|
-
}
|
|
1355
|
-
function t(id, vars) {
|
|
1356
|
-
const table = locales[current] ?? locales.en;
|
|
1357
|
-
let text = table[id] ?? locales.en[id];
|
|
1358
|
-
if (vars) {
|
|
1359
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
1360
|
-
text = text.replaceAll(`{${key}}`, String(value));
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
return text;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
// src/integrations/ollama-tags.ts
|
|
1367
|
-
var RULES = [
|
|
1368
|
-
{ test: /qwen\s*3\.5\s*[-:]?\s*8b/i, tag: "qwen3.5:8b" },
|
|
1369
|
-
{ test: /qwen\s*3\.5\s*[-:]?\s*4b/i, tag: "qwen3.5:4b" },
|
|
1370
|
-
{ test: /qwen\s*3\s*[-:]?\s*14b/i, tag: "qwen3:14b" },
|
|
1371
|
-
{ test: /qwen\s*3\s*[-:]?\s*8b/i, tag: "qwen3:8b" },
|
|
1372
|
-
{ test: /qwen\s*3\s*[-:]?\s*4b/i, tag: "qwen3:4b" },
|
|
1373
|
-
{ test: /qwen\s*2\.5\s*[-:]?\s*coder\s*[-:]?\s*7b/i, tag: "qwen2.5-coder:7b" },
|
|
1374
|
-
{ test: /gemma\s*3\s*[-:]?\s*12b/i, tag: "gemma3:12b" },
|
|
1375
|
-
{ test: /gemma\s*3\s*[-:]?\s*4b/i, tag: "gemma3:4b" },
|
|
1376
|
-
{ test: /gemma\s*3\s*[-:]?\s*1b/i, tag: "gemma3:1b" },
|
|
1377
|
-
{ test: /llama\s*3\.2\s*[-:]?\s*3b/i, tag: "llama3.2:3b" },
|
|
1378
|
-
{ test: /llama\s*3\.2\s*[-:]?\s*1b/i, tag: "llama3.2:1b" },
|
|
1379
|
-
{ test: /llama\s*3\.1\s*[-:]?\s*8b/i, tag: "llama3.1:8b" },
|
|
1380
|
-
{ test: /deepseek[- ]r1\s*[-:]?\s*8b|deepseek-r1-distill.*8b/i, tag: "deepseek-r1:8b" },
|
|
1381
|
-
{ test: /deepseek[- ]r1\s*[-:]?\s*7b/i, tag: "deepseek-r1:7b" },
|
|
1382
|
-
{ test: /mistral\s*[-:]?\s*7b|mistral-nemo/i, tag: "mistral:7b" },
|
|
1383
|
-
{ test: /phi\s*4|phi-4/i, tag: "phi4" }
|
|
1384
|
-
];
|
|
1385
|
-
function resolveOllamaTag(...parts) {
|
|
1386
|
-
const haystack = parts.filter(Boolean).join(" ");
|
|
1387
|
-
if (!haystack.trim()) return void 0;
|
|
1388
|
-
if (/^[a-z0-9._-]+:[a-z0-9._-]+$/i.test(haystack.trim())) return haystack.trim();
|
|
1389
|
-
for (const rule of RULES) {
|
|
1390
|
-
if (rule.test.test(haystack)) return rule.tag;
|
|
1391
|
-
}
|
|
1392
|
-
return void 0;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// src/compatibility/local.ts
|
|
1396
|
-
var GB_PER_BILLION_Q4 = 0.65;
|
|
1397
|
-
function estimateModelRamGb(model, quant = "Q4_K_M") {
|
|
1398
|
-
const params = model.paramsBillions ?? 8;
|
|
1399
|
-
const quantFactor = {
|
|
1400
|
-
Q2_K: 0.4,
|
|
1401
|
-
Q3_K_M: 0.5,
|
|
1402
|
-
Q4_K_M: 0.65,
|
|
1403
|
-
Q5_K_M: 0.8,
|
|
1404
|
-
Q6_K: 0.9,
|
|
1405
|
-
Q8_0: 1.1,
|
|
1406
|
-
F16: 2
|
|
1407
|
-
};
|
|
1408
|
-
return params * (quantFactor[quant] ?? GB_PER_BILLION_Q4);
|
|
1409
|
-
}
|
|
1410
|
-
function localCompatibility(hardware, model) {
|
|
1411
|
-
const ramGb = bytesToGiB(hardware.memory.totalBytes);
|
|
1412
|
-
const required = estimateModelRamGb(model);
|
|
1413
|
-
const ratio = required / ramGb;
|
|
1414
|
-
let grade;
|
|
1415
|
-
if (ratio <= 0.35) grade = "S";
|
|
1416
|
-
else if (ratio <= 0.5) grade = "A";
|
|
1417
|
-
else if (ratio <= 0.7) grade = "B";
|
|
1418
|
-
else if (ratio <= 0.9) grade = "C";
|
|
1419
|
-
else if (ratio <= 1.15) grade = "D";
|
|
1420
|
-
else grade = "F";
|
|
1421
|
-
const bandwidthGuess = hardware.cpu.appleSilicon ? 100 : 40;
|
|
1422
|
-
const estimatedTokensPerSecond = grade === "F" ? void 0 : Math.max(4, bandwidthGuess / Math.max(required, 1));
|
|
1423
|
-
return {
|
|
1424
|
-
modelId: model.id,
|
|
1425
|
-
source: "estimated",
|
|
1426
|
-
grade,
|
|
1427
|
-
estimatedRamGb: Number(required.toFixed(2)),
|
|
1428
|
-
estimatedTokensPerSecond: estimatedTokensPerSecond ? Number(estimatedTokensPerSecond.toFixed(1)) : void 0,
|
|
1429
|
-
notes: [`Local estimate from ${required.toFixed(1)} GB Q4 vs ${ramGb.toFixed(1)} GB ${hardware.memory.unified ? "unified memory" : "RAM"}`]
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
581
|
// src/compatibility/service.ts
|
|
1434
582
|
var FALLBACK_CATALOG = [
|
|
1435
583
|
{ id: "gemma3-4b", name: "Gemma 3 4B", provider: "Google", paramsBillions: 4, useCase: ["chat"] },
|
|
@@ -1509,130 +657,13 @@ function labelUseCase(useCase) {
|
|
|
1509
657
|
return t("useCaseGeneral");
|
|
1510
658
|
}
|
|
1511
659
|
|
|
1512
|
-
// src/storage/benchmarks.ts
|
|
1513
|
-
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1514
|
-
import path6 from "node:path";
|
|
1515
|
-
import { z as z3 } from "zod";
|
|
1516
|
-
var benchmarkRecordSchema = z3.object({
|
|
1517
|
-
schemaVersion: z3.literal(1),
|
|
1518
|
-
timestamp: z3.string(),
|
|
1519
|
-
machine: z3.record(z3.unknown()),
|
|
1520
|
-
model: z3.object({
|
|
1521
|
-
id: z3.string(),
|
|
1522
|
-
name: z3.string().optional(),
|
|
1523
|
-
source: z3.string().optional(),
|
|
1524
|
-
path: z3.string().optional()
|
|
1525
|
-
}),
|
|
1526
|
-
runtime: z3.object({
|
|
1527
|
-
name: z3.string(),
|
|
1528
|
-
command: z3.array(z3.string()).optional()
|
|
1529
|
-
}),
|
|
1530
|
-
benchmark: z3.object({
|
|
1531
|
-
promptTokens: z3.number(),
|
|
1532
|
-
generationTokens: z3.number(),
|
|
1533
|
-
repetitions: z3.number(),
|
|
1534
|
-
promptTokensPerSecond: z3.number().optional(),
|
|
1535
|
-
generationTokensPerSecond: z3.number().optional(),
|
|
1536
|
-
elapsedSeconds: z3.number().optional()
|
|
1537
|
-
}),
|
|
1538
|
-
resources: z3.object({
|
|
1539
|
-
peakMemoryGb: z3.number().optional(),
|
|
1540
|
-
peakSwapGb: z3.number().optional(),
|
|
1541
|
-
avgGpuPercent: z3.number().optional(),
|
|
1542
|
-
avgCpuPercent: z3.number().optional(),
|
|
1543
|
-
peakGpuPercent: z3.number().optional(),
|
|
1544
|
-
avgPackagePowerWatts: z3.number().optional(),
|
|
1545
|
-
peakPackagePowerWatts: z3.number().optional(),
|
|
1546
|
-
avgTemperatureC: z3.number().optional(),
|
|
1547
|
-
peakTemperatureC: z3.number().optional(),
|
|
1548
|
-
tokensPerSecondPerWatt: z3.number().optional(),
|
|
1549
|
-
peakProcessRssGb: z3.number().optional()
|
|
1550
|
-
}),
|
|
1551
|
-
score: z3.object({
|
|
1552
|
-
total: z3.number(),
|
|
1553
|
-
speed: z3.string().optional(),
|
|
1554
|
-
memory: z3.string().optional(),
|
|
1555
|
-
energy: z3.string().optional(),
|
|
1556
|
-
thermal: z3.string().optional(),
|
|
1557
|
-
swap: z3.string().optional()
|
|
1558
|
-
}).optional(),
|
|
1559
|
-
origin: z3.enum(["measured"]).default("measured")
|
|
1560
|
-
});
|
|
1561
|
-
async function saveBenchmark(record) {
|
|
1562
|
-
await ensureStorage();
|
|
1563
|
-
const safeName = record.model.id.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
1564
|
-
const file = path6.join(benchmarksDir(), `${record.timestamp.replace(/[:.]/g, "-")}-${safeName}.json`);
|
|
1565
|
-
await writeFile3(file, `${JSON.stringify(record, null, 2)}
|
|
1566
|
-
`, "utf8");
|
|
1567
|
-
return file;
|
|
1568
|
-
}
|
|
1569
|
-
async function listBenchmarks() {
|
|
1570
|
-
await ensureStorage();
|
|
1571
|
-
let files = [];
|
|
1572
|
-
try {
|
|
1573
|
-
files = (await readdir2(benchmarksDir())).filter((f) => f.endsWith(".json"));
|
|
1574
|
-
} catch {
|
|
1575
|
-
return [];
|
|
1576
|
-
}
|
|
1577
|
-
const records = [];
|
|
1578
|
-
for (const file of files.sort()) {
|
|
1579
|
-
try {
|
|
1580
|
-
const raw = await readFile3(path6.join(benchmarksDir(), file), "utf8");
|
|
1581
|
-
records.push(benchmarkRecordSchema.parse(JSON.parse(raw)));
|
|
1582
|
-
} catch {
|
|
1583
|
-
continue;
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
return records;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
// src/session/load.ts
|
|
1590
|
-
init_which();
|
|
1591
|
-
|
|
1592
|
-
// src/integrations/agents.ts
|
|
1593
|
-
init_which();
|
|
1594
|
-
var AGENTS = [
|
|
1595
|
-
{
|
|
1596
|
-
id: "opencode",
|
|
1597
|
-
label: "OpenCode",
|
|
1598
|
-
bin: "opencode",
|
|
1599
|
-
launch: "ollama launch opencode",
|
|
1600
|
-
docs: "https://docs.ollama.com/integrations/opencode"
|
|
1601
|
-
},
|
|
1602
|
-
{
|
|
1603
|
-
id: "hermes",
|
|
1604
|
-
label: "Hermes",
|
|
1605
|
-
bin: "hermes",
|
|
1606
|
-
launch: "ollama launch hermes",
|
|
1607
|
-
docs: "https://docs.ollama.com/integrations/hermes"
|
|
1608
|
-
},
|
|
1609
|
-
{
|
|
1610
|
-
id: "openclaw",
|
|
1611
|
-
label: "OpenClaw",
|
|
1612
|
-
bin: "openclaw",
|
|
1613
|
-
launch: "ollama launch openclaw",
|
|
1614
|
-
docs: "https://docs.ollama.com/integrations/openclaw"
|
|
1615
|
-
}
|
|
1616
|
-
];
|
|
1617
|
-
async function detectAgents() {
|
|
1618
|
-
const out = [];
|
|
1619
|
-
for (const agent of AGENTS) {
|
|
1620
|
-
out.push({
|
|
1621
|
-
...agent,
|
|
1622
|
-
detected: Boolean(await commandExists(agent.bin))
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
return out;
|
|
1626
|
-
}
|
|
1627
|
-
var DOCKER_OLLAMA = "docker run -d --name pudu-ollama -p 11434:11434 -v ollama:/root/.ollama ollama/ollama";
|
|
1628
|
-
|
|
1629
660
|
// src/session/load.ts
|
|
1630
661
|
async function loadSession(options) {
|
|
1631
662
|
const hardware = await detectHardware();
|
|
1632
663
|
const [runtimes2, models, catalogState, history, llamaBench, agents] = await Promise.all([
|
|
1633
664
|
detectRuntimes(),
|
|
1634
665
|
discoverLocalModels(),
|
|
1635
|
-
loadCatalog(options.network),
|
|
666
|
+
options.light ? Promise.resolve({ catalog: [], networkUsed: false }) : loadCatalog(options.network),
|
|
1636
667
|
listBenchmarks(),
|
|
1637
668
|
commandExists("llama-bench").then(Boolean),
|
|
1638
669
|
detectAgents()
|
|
@@ -1640,11 +671,11 @@ async function loadSession(options) {
|
|
|
1640
671
|
const rows = [];
|
|
1641
672
|
for (const local of models) {
|
|
1642
673
|
const catalog = matchCatalog(catalogState.catalog, local);
|
|
1643
|
-
const compatibility = catalog ? await compatibilityFor(hardware, catalog, options.network) : void 0;
|
|
674
|
+
const compatibility = catalog && !options.light ? await compatibilityFor(hardware, catalog, options.network) : void 0;
|
|
1644
675
|
const lastBenchmark = [...history].reverse().find((b) => b.model.id === local.id);
|
|
1645
676
|
rows.push({ local, catalog, compatibility, lastBenchmark });
|
|
1646
677
|
}
|
|
1647
|
-
const recommendations = await recommendForMachine(hardware, catalogState.catalog, options.network);
|
|
678
|
+
const recommendations = options.light ? [] : await recommendForMachine(hardware, catalogState.catalog, options.network);
|
|
1648
679
|
return {
|
|
1649
680
|
hardware,
|
|
1650
681
|
runtimes: runtimes2,
|
|
@@ -1659,1866 +690,11 @@ async function loadSession(options) {
|
|
|
1659
690
|
};
|
|
1660
691
|
}
|
|
1661
692
|
|
|
1662
|
-
// src/
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
init_process();
|
|
1668
|
-
import os9 from "node:os";
|
|
1669
|
-
|
|
1670
|
-
// src/telemetry/fallback.ts
|
|
1671
|
-
import os8 from "node:os";
|
|
1672
|
-
var previous = os8.cpus();
|
|
1673
|
-
function cpuPercent() {
|
|
1674
|
-
const current2 = os8.cpus();
|
|
1675
|
-
let idle = 0;
|
|
1676
|
-
let total = 0;
|
|
1677
|
-
for (let i = 0; i < current2.length; i += 1) {
|
|
1678
|
-
const c = current2[i];
|
|
1679
|
-
const p = previous[i] ?? c;
|
|
1680
|
-
const idleDelta = c.times.idle - p.times.idle;
|
|
1681
|
-
const totalDelta = c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq - (p.times.user + p.times.nice + p.times.sys + p.times.idle + p.times.irq);
|
|
1682
|
-
idle += idleDelta;
|
|
1683
|
-
total += totalDelta;
|
|
1684
|
-
}
|
|
1685
|
-
previous = current2;
|
|
1686
|
-
if (total <= 0) return 0;
|
|
1687
|
-
return Number((100 * (1 - idle / total)).toFixed(1));
|
|
1688
|
-
}
|
|
1689
|
-
var nodeTelemetry = {
|
|
1690
|
-
async start() {
|
|
1691
|
-
previous = os8.cpus();
|
|
1692
|
-
},
|
|
1693
|
-
async sample() {
|
|
1694
|
-
const total = os8.totalmem();
|
|
1695
|
-
const free = os8.freemem();
|
|
1696
|
-
const sample = {
|
|
1697
|
-
timestamp: Date.now(),
|
|
1698
|
-
cpu: { utilizationPercent: cpuPercent() },
|
|
1699
|
-
memory: {
|
|
1700
|
-
usedBytes: total - free,
|
|
1701
|
-
availableBytes: free,
|
|
1702
|
-
swapUsedBytes: 0
|
|
1703
|
-
}
|
|
1704
|
-
};
|
|
1705
|
-
return sample;
|
|
1706
|
-
},
|
|
1707
|
-
async stop() {
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
};
|
|
1711
|
-
|
|
1712
|
-
// src/telemetry/macos.ts
|
|
1713
|
-
async function sampleProcess(pid) {
|
|
1714
|
-
if (!pid) return void 0;
|
|
1715
|
-
const result = await runCommand("ps", ["-o", "pid=,%cpu=,rss=", "-p", String(pid)], { timeout: 3e3 });
|
|
1716
|
-
const parts = result.stdout.trim().split(/\s+/);
|
|
1717
|
-
if (parts.length < 3) return { pid };
|
|
1718
|
-
return {
|
|
1719
|
-
pid,
|
|
1720
|
-
cpuPercent: Number(parts[1]),
|
|
1721
|
-
rssBytes: Number(parts[2]) * 1024
|
|
1722
|
-
};
|
|
1723
|
-
}
|
|
1724
|
-
function createMacosTelemetry(pid) {
|
|
1725
|
-
return {
|
|
1726
|
-
async start() {
|
|
1727
|
-
await nodeTelemetry.start();
|
|
1728
|
-
},
|
|
1729
|
-
async sample() {
|
|
1730
|
-
const base = await nodeTelemetry.sample();
|
|
1731
|
-
const [vm, pressure, pagesize, swap] = await Promise.all([
|
|
1732
|
-
runCommand("vm_stat", [], { timeout: 3e3 }),
|
|
1733
|
-
runCommand("memory_pressure", [], { timeout: 3e3 }),
|
|
1734
|
-
runCommand("sysctl", ["-n", "hw.pagesize"], { timeout: 3e3 }),
|
|
1735
|
-
runCommand("sysctl", ["-n", "vm.swapusage"], { timeout: 3e3 })
|
|
1736
|
-
]);
|
|
1737
|
-
const pageSize = Number(pagesize.stdout.trim()) || 16384;
|
|
1738
|
-
const parsed = parseMacosVmStat(vm.stdout, pageSize);
|
|
1739
|
-
const total = os9.totalmem();
|
|
1740
|
-
const available = parsed.availableBytes ?? os9.freemem();
|
|
1741
|
-
const sample = {
|
|
1742
|
-
...base,
|
|
1743
|
-
memory: {
|
|
1744
|
-
usedBytes: Math.max(0, total - available),
|
|
1745
|
-
availableBytes: available,
|
|
1746
|
-
swapUsedBytes: parseMacosSwapUsage(swap.stdout) ?? 0
|
|
1747
|
-
},
|
|
1748
|
-
thermal: {
|
|
1749
|
-
pressure: parseMacosMemoryPressure(pressure.stdout + pressure.stderr)
|
|
1750
|
-
},
|
|
1751
|
-
process: await sampleProcess(pid),
|
|
1752
|
-
gpu: {
|
|
1753
|
-
utilizationPercent: void 0,
|
|
1754
|
-
powerWatts: void 0
|
|
1755
|
-
}
|
|
1756
|
-
};
|
|
1757
|
-
return sample;
|
|
1758
|
-
},
|
|
1759
|
-
async stop() {
|
|
1760
|
-
await nodeTelemetry.stop();
|
|
1761
|
-
}
|
|
1762
|
-
};
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
// src/telemetry/collector.ts
|
|
1766
|
-
function createTelemetry(pid) {
|
|
1767
|
-
if (detectOs() === "macos") return createMacosTelemetry(pid);
|
|
1768
|
-
return nodeTelemetry;
|
|
1769
|
-
}
|
|
1770
|
-
var TelemetryCollector = class {
|
|
1771
|
-
provider;
|
|
1772
|
-
timer;
|
|
1773
|
-
samples = [];
|
|
1774
|
-
running = false;
|
|
1775
|
-
constructor(pid) {
|
|
1776
|
-
this.provider = createTelemetry(pid);
|
|
1777
|
-
}
|
|
1778
|
-
async start(intervalMs = 750) {
|
|
1779
|
-
await this.provider.start();
|
|
1780
|
-
this.running = true;
|
|
1781
|
-
const tick = async () => {
|
|
1782
|
-
if (!this.running) return;
|
|
1783
|
-
try {
|
|
1784
|
-
const sample = await this.provider.sample();
|
|
1785
|
-
this.samples.push(sample);
|
|
1786
|
-
this.onSample?.(sample);
|
|
1787
|
-
} catch {
|
|
1788
|
-
}
|
|
1789
|
-
};
|
|
1790
|
-
await tick();
|
|
1791
|
-
this.timer = setInterval(() => {
|
|
1792
|
-
void tick();
|
|
1793
|
-
}, intervalMs);
|
|
1794
|
-
}
|
|
1795
|
-
onSample;
|
|
1796
|
-
latest() {
|
|
1797
|
-
return this.samples.at(-1);
|
|
1798
|
-
}
|
|
1799
|
-
history() {
|
|
1800
|
-
return this.samples;
|
|
1801
|
-
}
|
|
1802
|
-
async stop() {
|
|
1803
|
-
this.running = false;
|
|
1804
|
-
if (this.timer) clearInterval(this.timer);
|
|
1805
|
-
await this.provider.stop();
|
|
1806
|
-
return summarize(this.samples);
|
|
1807
|
-
}
|
|
1808
|
-
};
|
|
1809
|
-
function summarize(samples) {
|
|
1810
|
-
if (samples.length === 0) return { samples: 0 };
|
|
1811
|
-
const avg = (values) => {
|
|
1812
|
-
const nums = values.filter((v) => v !== void 0 && !Number.isNaN(v));
|
|
1813
|
-
if (!nums.length) return void 0;
|
|
1814
|
-
return Number((nums.reduce((a, b) => a + b, 0) / nums.length).toFixed(1));
|
|
1815
|
-
};
|
|
1816
|
-
const peak = (values) => {
|
|
1817
|
-
const nums = values.filter((v) => v !== void 0 && !Number.isNaN(v));
|
|
1818
|
-
if (!nums.length) return void 0;
|
|
1819
|
-
return Number(Math.max(...nums).toFixed(2));
|
|
1820
|
-
};
|
|
1821
|
-
const GiB2 = 1024 ** 3;
|
|
1822
|
-
return {
|
|
1823
|
-
samples: samples.length,
|
|
1824
|
-
avgCpuPercent: avg(samples.map((s) => s.cpu?.utilizationPercent)),
|
|
1825
|
-
avgGpuPercent: avg(samples.map((s) => s.gpu?.utilizationPercent)),
|
|
1826
|
-
peakGpuPercent: peak(samples.map((s) => s.gpu?.utilizationPercent)),
|
|
1827
|
-
peakMemoryGb: peak(samples.map((s) => s.memory.usedBytes / GiB2)),
|
|
1828
|
-
peakSwapGb: peak(samples.map((s) => s.memory.swapUsedBytes / GiB2)),
|
|
1829
|
-
avgPackagePowerWatts: avg(samples.map((s) => s.packagePowerWatts ?? s.cpu?.powerWatts)),
|
|
1830
|
-
peakPackagePowerWatts: peak(samples.map((s) => s.packagePowerWatts ?? s.cpu?.powerWatts)),
|
|
1831
|
-
avgTemperatureC: avg(samples.map((s) => s.thermal?.temperatureC)),
|
|
1832
|
-
peakTemperatureC: peak(samples.map((s) => s.thermal?.temperatureC)),
|
|
1833
|
-
peakProcessRssGb: peak(samples.map((s) => (s.process?.rssBytes ?? 0) / GiB2))
|
|
1834
|
-
};
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
// src/benchmark/parse-llama-bench.ts
|
|
1838
|
-
import { z as z4 } from "zod";
|
|
1839
|
-
var jsonRowSchema = z4.object({
|
|
1840
|
-
n_prompt: z4.number().optional(),
|
|
1841
|
-
n_gen: z4.number().optional(),
|
|
1842
|
-
avg_ts: z4.number().optional(),
|
|
1843
|
-
model_type: z4.string().optional(),
|
|
1844
|
-
model_size: z4.number().optional(),
|
|
1845
|
-
backends: z4.string().optional(),
|
|
1846
|
-
test: z4.string().optional()
|
|
1847
|
-
}).passthrough();
|
|
1848
|
-
function parseLlamaBenchJson(text) {
|
|
1849
|
-
const parsed = JSON.parse(text);
|
|
1850
|
-
const rows = Array.isArray(parsed) ? parsed.map((row) => jsonRowSchema.parse(row)) : [jsonRowSchema.parse(parsed)];
|
|
1851
|
-
return metricsFromRows(rows, parsed);
|
|
1852
|
-
}
|
|
1853
|
-
function metricsFromRows(rows, raw) {
|
|
1854
|
-
let promptTokensPerSecond;
|
|
1855
|
-
let generationTokensPerSecond;
|
|
1856
|
-
for (const row of rows) {
|
|
1857
|
-
const test = row.test ?? "";
|
|
1858
|
-
const avg = row.avg_ts;
|
|
1859
|
-
if (avg === void 0) continue;
|
|
1860
|
-
if (/^pp/i.test(test) || (row.n_prompt ?? 0) > 0 && (row.n_gen ?? 0) === 0) {
|
|
1861
|
-
promptTokensPerSecond = avg;
|
|
1862
|
-
} else if (/^tg/i.test(test) || (row.n_gen ?? 0) > 0 && (row.n_prompt ?? 0) === 0) {
|
|
1863
|
-
generationTokensPerSecond = avg;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
const first = rows[0];
|
|
1867
|
-
return {
|
|
1868
|
-
promptTokensPerSecond,
|
|
1869
|
-
generationTokensPerSecond,
|
|
1870
|
-
modelType: first?.model_type,
|
|
1871
|
-
modelSizeBytes: first?.model_size,
|
|
1872
|
-
backend: first?.backends,
|
|
1873
|
-
raw
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
function parseLlamaBenchMarkdown(text) {
|
|
1877
|
-
const rows = [];
|
|
1878
|
-
for (const line of text.split(/\r?\n/)) {
|
|
1879
|
-
if (!line.includes("|")) continue;
|
|
1880
|
-
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
1881
|
-
if (cells.length < 6) continue;
|
|
1882
|
-
if (/^model$/i.test(cells[0] ?? "") || /^-+$/.test(cells[0] ?? "")) continue;
|
|
1883
|
-
const test = cells[cells.length - 2] ?? "";
|
|
1884
|
-
const tsCell = cells[cells.length - 1] ?? "";
|
|
1885
|
-
const tsMatch = tsCell.match(/([\d.]+)/);
|
|
1886
|
-
if (!tsMatch) continue;
|
|
1887
|
-
rows.push({ test, ts: Number(tsMatch[1]), modelType: cells[0] });
|
|
1888
|
-
}
|
|
1889
|
-
let promptTokensPerSecond;
|
|
1890
|
-
let generationTokensPerSecond;
|
|
1891
|
-
for (const row of rows) {
|
|
1892
|
-
if (/^pp/i.test(row.test)) promptTokensPerSecond = row.ts;
|
|
1893
|
-
if (/^tg/i.test(row.test)) generationTokensPerSecond = row.ts;
|
|
1894
|
-
}
|
|
1895
|
-
return {
|
|
1896
|
-
promptTokensPerSecond,
|
|
1897
|
-
generationTokensPerSecond,
|
|
1898
|
-
modelType: rows[0]?.modelType,
|
|
1899
|
-
raw: { format: "markdown", rows }
|
|
1900
|
-
};
|
|
1901
|
-
}
|
|
1902
|
-
function parseLlamaBenchOutput(text) {
|
|
1903
|
-
const trimmed = text.trim();
|
|
1904
|
-
const jsonStart = trimmed.indexOf("[");
|
|
1905
|
-
const jsonObjStart = trimmed.indexOf("{");
|
|
1906
|
-
const start = jsonStart >= 0 && (jsonObjStart < 0 || jsonStart < jsonObjStart) ? jsonStart : jsonObjStart;
|
|
1907
|
-
if (start >= 0) {
|
|
1908
|
-
const slice = trimmed.slice(start);
|
|
1909
|
-
try {
|
|
1910
|
-
return parseLlamaBenchJson(slice);
|
|
1911
|
-
} catch {
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
return parseLlamaBenchMarkdown(text);
|
|
1915
|
-
}
|
|
1916
|
-
function tokensPerSecondPerWatt(tokensPerSecond, watts) {
|
|
1917
|
-
if (!tokensPerSecond || !watts || watts <= 0) return void 0;
|
|
1918
|
-
return Number((tokensPerSecond / watts).toFixed(2));
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
// src/benchmark/presets.ts
|
|
1922
|
-
var PRESETS = {
|
|
1923
|
-
quick: { name: "quick", promptTokens: 512, generationTokens: 128, repetitions: 3 },
|
|
1924
|
-
standard: { name: "standard", promptTokens: 2048, generationTokens: 256, repetitions: 5 },
|
|
1925
|
-
stress: { name: "stress", promptTokens: 4096, generationTokens: 512, repetitions: 10 }
|
|
1926
|
-
};
|
|
1927
|
-
function resolvePreset(name) {
|
|
1928
|
-
if (name === "standard" || name === "stress" || name === "quick") return PRESETS[name];
|
|
1929
|
-
return PRESETS.quick;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
// src/compatibility/grades.ts
|
|
1933
|
-
function gradeFromThresholds(score, thresholds) {
|
|
1934
|
-
if (score >= thresholds.S) return "S";
|
|
1935
|
-
if (score >= thresholds.A) return "A";
|
|
1936
|
-
if (score >= thresholds.B) return "B";
|
|
1937
|
-
if (score >= thresholds.C) return "C";
|
|
1938
|
-
if (score >= thresholds.D) return "D";
|
|
1939
|
-
return "F";
|
|
1940
|
-
}
|
|
1941
|
-
function gradeToScore(grade) {
|
|
1942
|
-
return { S: 100, A: 85, B: 70, C: 55, D: 40, F: 20 }[grade];
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
// src/benchmark/score.ts
|
|
1946
|
-
function computeLocalMeterScore(generationTokensPerSecond, resources, totalMemoryGb) {
|
|
1947
|
-
const dimensions = [
|
|
1948
|
-
{
|
|
1949
|
-
key: "speed",
|
|
1950
|
-
measured: generationTokensPerSecond !== void 0,
|
|
1951
|
-
grade: generationTokensPerSecond === void 0 ? void 0 : gradeFromThresholds(generationTokensPerSecond, { S: 60, A: 40, B: 25, C: 12, D: 5, F: 0 }),
|
|
1952
|
-
weight: 35
|
|
1953
|
-
},
|
|
1954
|
-
{
|
|
1955
|
-
key: "memory",
|
|
1956
|
-
measured: resources.peakMemoryGb !== void 0 && totalMemoryGb > 0,
|
|
1957
|
-
grade: resources.peakMemoryGb === void 0 || totalMemoryGb <= 0 ? void 0 : gradeFromThresholds(100 - resources.peakMemoryGb / totalMemoryGb * 100, {
|
|
1958
|
-
S: 50,
|
|
1959
|
-
A: 30,
|
|
1960
|
-
B: 15,
|
|
1961
|
-
C: 5,
|
|
1962
|
-
D: 0,
|
|
1963
|
-
F: -100
|
|
1964
|
-
}),
|
|
1965
|
-
weight: 25
|
|
1966
|
-
},
|
|
1967
|
-
{
|
|
1968
|
-
key: "energy",
|
|
1969
|
-
measured: resources.avgPackagePowerWatts !== void 0 && generationTokensPerSecond !== void 0,
|
|
1970
|
-
grade: energyGrade(generationTokensPerSecond, resources.avgPackagePowerWatts),
|
|
1971
|
-
weight: 20
|
|
1972
|
-
},
|
|
1973
|
-
{
|
|
1974
|
-
key: "thermal",
|
|
1975
|
-
measured: resources.peakTemperatureC !== void 0,
|
|
1976
|
-
grade: resources.peakTemperatureC === void 0 ? void 0 : gradeFromThresholds(100 - resources.peakTemperatureC, { S: 30, A: 20, B: 10, C: 0, D: -10, F: -100 }),
|
|
1977
|
-
weight: 10
|
|
1978
|
-
},
|
|
1979
|
-
{
|
|
1980
|
-
key: "swap",
|
|
1981
|
-
measured: resources.peakSwapGb !== void 0,
|
|
1982
|
-
grade: resources.peakSwapGb === void 0 ? void 0 : gradeFromThresholds(4 - resources.peakSwapGb, { S: 4, A: 3.75, B: 3, C: 2, D: 0, F: -100 }),
|
|
1983
|
-
weight: 10
|
|
1984
|
-
}
|
|
1985
|
-
];
|
|
1986
|
-
const measured = dimensions.filter((d) => d.measured && d.grade);
|
|
1987
|
-
const weightSum = measured.reduce((sum, d) => sum + d.weight, 0);
|
|
1988
|
-
if (!measured.length || weightSum === 0) return { dimensions };
|
|
1989
|
-
const total = measured.reduce((sum, d) => sum + gradeToScore(d.grade) * (d.weight / weightSum), 0);
|
|
1990
|
-
return { total: Math.round(total), dimensions };
|
|
1991
|
-
}
|
|
1992
|
-
function energyGrade(tps, watts) {
|
|
1993
|
-
if (!tps || !watts || watts <= 0) return void 0;
|
|
1994
|
-
const efficiency = tps / watts;
|
|
1995
|
-
return gradeFromThresholds(efficiency, { S: 3, A: 2, B: 1.2, C: 0.6, D: 0.3, F: 0 });
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
// src/benchmark/assess.ts
|
|
1999
|
-
function assessRun(input) {
|
|
2000
|
-
const lines = [];
|
|
2001
|
-
const { generationTokensPerSecond, resources, catalog } = input;
|
|
2002
|
-
if (generationTokensPerSecond !== void 0 && generationTokensPerSecond >= 25) {
|
|
2003
|
-
lines.push("Runs comfortably on this machine");
|
|
2004
|
-
} else if (generationTokensPerSecond !== void 0 && generationTokensPerSecond >= 10) {
|
|
2005
|
-
lines.push("Runs, but generation is constrained");
|
|
2006
|
-
} else if (generationTokensPerSecond !== void 0) {
|
|
2007
|
-
lines.push("Generation is too slow for interactive use");
|
|
2008
|
-
}
|
|
2009
|
-
if ((resources.peakSwapGb ?? 0) < 0.05) lines.push("No meaningful swap detected");
|
|
2010
|
-
else lines.push("Swap activity detected \u2014 memory is tight");
|
|
2011
|
-
if (resources.avgGpuPercent !== void 0 && resources.avgGpuPercent >= 80) {
|
|
2012
|
-
lines.push("GPU remains highly utilized (system-wide)");
|
|
2013
|
-
}
|
|
2014
|
-
if (generationTokensPerSecond !== void 0 && generationTokensPerSecond >= 35) {
|
|
2015
|
-
lines.push("Good sustained generation performance");
|
|
2016
|
-
}
|
|
2017
|
-
if (resources.avgPackagePowerWatts !== void 0 && generationTokensPerSecond) {
|
|
2018
|
-
const eff = generationTokensPerSecond / resources.avgPackagePowerWatts;
|
|
2019
|
-
if (eff >= 2) lines.push("Excellent energy efficiency");
|
|
2020
|
-
}
|
|
2021
|
-
if (catalog?.useCase?.length) {
|
|
2022
|
-
lines.push(`This model is suitable for: ${catalog.useCase.join(", ")}`);
|
|
2023
|
-
}
|
|
2024
|
-
return lines;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// src/benchmark/engine.ts
|
|
2028
|
-
async function runBenchmark(input) {
|
|
2029
|
-
const bench = await commandExists("llama-bench");
|
|
2030
|
-
if (!bench) {
|
|
2031
|
-
throw new Error("llama-bench not found. Install llama.cpp (e.g. brew install llama.cpp).");
|
|
2032
|
-
}
|
|
2033
|
-
const artifact = input.model.artifactPath;
|
|
2034
|
-
if (!artifact) {
|
|
2035
|
-
throw new Error(`No GGUF path for ${input.model.id}. Cannot run llama-bench.`);
|
|
2036
|
-
}
|
|
2037
|
-
const preset = resolvePreset(input.preset);
|
|
2038
|
-
const started = Date.now();
|
|
2039
|
-
const args2 = [
|
|
2040
|
-
"-m",
|
|
2041
|
-
artifact,
|
|
2042
|
-
"-p",
|
|
2043
|
-
String(preset.promptTokens),
|
|
2044
|
-
"-n",
|
|
2045
|
-
String(preset.generationTokens),
|
|
2046
|
-
"-r",
|
|
2047
|
-
String(preset.repetitions),
|
|
2048
|
-
"-o",
|
|
2049
|
-
"json"
|
|
2050
|
-
];
|
|
2051
|
-
const subprocess = spawnTracked(bench, args2, { timeout: 30 * 6e4 });
|
|
2052
|
-
const collector = new TelemetryCollector(subprocess.pid);
|
|
2053
|
-
collector.onSample = (sample) => {
|
|
2054
|
-
input.onProgress?.({ sample, elapsedSeconds: (Date.now() - started) / 1e3, status: "running" });
|
|
2055
|
-
};
|
|
2056
|
-
await collector.start(750);
|
|
2057
|
-
const abort = () => {
|
|
2058
|
-
subprocess.kill("SIGTERM");
|
|
2059
|
-
};
|
|
2060
|
-
input.signal?.addEventListener("abort", abort, { once: true });
|
|
2061
|
-
const result = await subprocess;
|
|
2062
|
-
input.signal?.removeEventListener("abort", abort);
|
|
2063
|
-
const resources = await collector.stop();
|
|
2064
|
-
if (input.signal?.aborted) {
|
|
2065
|
-
throw new Error("Benchmark cancelled");
|
|
2066
|
-
}
|
|
2067
|
-
const stdout = toText(result.stdout);
|
|
2068
|
-
const stderr = toText(result.stderr);
|
|
2069
|
-
if (result.exitCode !== 0) {
|
|
2070
|
-
throw new Error(stderr || `llama-bench exited with ${result.exitCode}`);
|
|
2071
|
-
}
|
|
2072
|
-
const metrics = parseLlamaBenchOutput(stdout || stderr);
|
|
2073
|
-
const score = computeLocalMeterScore(
|
|
2074
|
-
metrics.generationTokensPerSecond,
|
|
2075
|
-
resources,
|
|
2076
|
-
bytesToGiB(input.hardware.memory.totalBytes)
|
|
2077
|
-
);
|
|
2078
|
-
const efficiency = tokensPerSecondPerWatt(
|
|
2079
|
-
metrics.generationTokensPerSecond,
|
|
2080
|
-
resources.avgPackagePowerWatts
|
|
2081
|
-
);
|
|
2082
|
-
const record = {
|
|
2083
|
-
schemaVersion: 1,
|
|
2084
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2085
|
-
machine: {
|
|
2086
|
-
os: input.hardware.os,
|
|
2087
|
-
arch: input.hardware.arch,
|
|
2088
|
-
model: input.hardware.machineModel,
|
|
2089
|
-
cpu: input.hardware.cpu.name,
|
|
2090
|
-
memoryGb: Number(bytesToGiB(input.hardware.memory.totalBytes).toFixed(1)),
|
|
2091
|
-
unified: input.hardware.memory.unified
|
|
2092
|
-
},
|
|
2093
|
-
model: {
|
|
2094
|
-
id: input.model.id,
|
|
2095
|
-
name: input.model.name,
|
|
2096
|
-
source: input.model.source,
|
|
2097
|
-
path: input.model.artifactPath
|
|
2098
|
-
},
|
|
2099
|
-
runtime: { name: "llama-bench", command: [bench, ...args2] },
|
|
2100
|
-
benchmark: {
|
|
2101
|
-
promptTokens: preset.promptTokens,
|
|
2102
|
-
generationTokens: preset.generationTokens,
|
|
2103
|
-
repetitions: preset.repetitions,
|
|
2104
|
-
promptTokensPerSecond: metrics.promptTokensPerSecond,
|
|
2105
|
-
generationTokensPerSecond: metrics.generationTokensPerSecond,
|
|
2106
|
-
elapsedSeconds: (Date.now() - started) / 1e3
|
|
2107
|
-
},
|
|
2108
|
-
resources: {
|
|
2109
|
-
...resourceFields(resources),
|
|
2110
|
-
tokensPerSecondPerWatt: efficiency
|
|
2111
|
-
},
|
|
2112
|
-
score: {
|
|
2113
|
-
total: score.total ?? 0,
|
|
2114
|
-
speed: score.dimensions.find((d) => d.key === "speed")?.grade,
|
|
2115
|
-
memory: score.dimensions.find((d) => d.key === "memory")?.grade,
|
|
2116
|
-
energy: score.dimensions.find((d) => d.key === "energy")?.grade,
|
|
2117
|
-
thermal: score.dimensions.find((d) => d.key === "thermal")?.grade,
|
|
2118
|
-
swap: score.dimensions.find((d) => d.key === "swap")?.grade
|
|
2119
|
-
},
|
|
2120
|
-
origin: "measured"
|
|
2121
|
-
};
|
|
2122
|
-
const path7 = await saveBenchmark(record);
|
|
2123
|
-
input.onProgress?.({
|
|
2124
|
-
elapsedSeconds: record.benchmark.elapsedSeconds ?? 0,
|
|
2125
|
-
status: "completed"
|
|
2126
|
-
});
|
|
2127
|
-
return {
|
|
2128
|
-
record,
|
|
2129
|
-
assessment: assessRun({
|
|
2130
|
-
generationTokensPerSecond: metrics.generationTokensPerSecond,
|
|
2131
|
-
resources,
|
|
2132
|
-
catalog: input.catalog
|
|
2133
|
-
}),
|
|
2134
|
-
path: path7,
|
|
2135
|
-
stdout
|
|
2136
|
-
};
|
|
2137
|
-
}
|
|
2138
|
-
function resourceFields(resources) {
|
|
2139
|
-
return {
|
|
2140
|
-
peakMemoryGb: resources.peakMemoryGb,
|
|
2141
|
-
peakSwapGb: resources.peakSwapGb,
|
|
2142
|
-
avgGpuPercent: resources.avgGpuPercent,
|
|
2143
|
-
avgCpuPercent: resources.avgCpuPercent,
|
|
2144
|
-
peakGpuPercent: resources.peakGpuPercent,
|
|
2145
|
-
avgPackagePowerWatts: resources.avgPackagePowerWatts,
|
|
2146
|
-
peakPackagePowerWatts: resources.peakPackagePowerWatts,
|
|
2147
|
-
avgTemperatureC: resources.avgTemperatureC,
|
|
2148
|
-
peakTemperatureC: resources.peakTemperatureC,
|
|
2149
|
-
peakProcessRssGb: resources.peakProcessRssGb
|
|
2150
|
-
};
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
// src/cli/run.ts
|
|
2154
|
-
init_process();
|
|
2155
|
-
|
|
2156
|
-
// src/shared/logger.ts
|
|
2157
|
-
var level = "info";
|
|
2158
|
-
function setLogLevel(next) {
|
|
2159
|
-
level = next;
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
// src/tui/render.tsx
|
|
2163
|
-
import { render } from "ink";
|
|
2164
|
-
|
|
2165
|
-
// src/tui/App.tsx
|
|
2166
|
-
import { Box as Box11, useApp as useApp2, useInput as useInput4 } from "ink";
|
|
2167
|
-
import { useMemo as useMemo2, useRef as useRef2, useState as useState5 } from "react";
|
|
2168
|
-
|
|
2169
|
-
// src/tui/views/Dashboard.tsx
|
|
2170
|
-
import { Box, Text as Text2 } from "ink";
|
|
2171
|
-
|
|
2172
|
-
// src/shared/format.ts
|
|
2173
|
-
function formatNumber(value, digits = 1) {
|
|
2174
|
-
if (value === void 0 || Number.isNaN(value)) return "N/A";
|
|
2175
|
-
return value.toFixed(digits);
|
|
2176
|
-
}
|
|
2177
|
-
function formatPercent(value, digits = 0) {
|
|
2178
|
-
if (value === void 0 || Number.isNaN(value)) return "N/A";
|
|
2179
|
-
return `${value.toFixed(digits)} %`;
|
|
2180
|
-
}
|
|
2181
|
-
function formatTokensPerSec(value, digits = 1) {
|
|
2182
|
-
if (value === void 0 || Number.isNaN(value)) return "N/A";
|
|
2183
|
-
return `${value.toFixed(digits)} t/s`;
|
|
2184
|
-
}
|
|
2185
|
-
function na(value) {
|
|
2186
|
-
if (value === void 0 || value === null || value === "") return "N/A";
|
|
2187
|
-
return String(value);
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
// src/hardware/types.ts
|
|
2191
|
-
function memoryLabel(profile) {
|
|
2192
|
-
return profile.memory.unified ? "Unified Memory" : "RAM";
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
// src/tui/theme.ts
|
|
2196
|
-
function gradeColor(grade) {
|
|
2197
|
-
if (grade === "S" || grade === "A") return "green";
|
|
2198
|
-
if (grade === "B") return "yellow";
|
|
2199
|
-
if (grade === "C") return "magenta";
|
|
2200
|
-
if (grade === "D" || grade === "F") return "red";
|
|
2201
|
-
return "gray";
|
|
2202
|
-
}
|
|
2203
|
-
function bar(percent, width = 10) {
|
|
2204
|
-
if (percent === void 0 || Number.isNaN(percent)) return `${"\u2591".repeat(width)} N/A`;
|
|
2205
|
-
const filled = Math.max(0, Math.min(width, Math.round(percent / 100 * width)));
|
|
2206
|
-
return `${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}`;
|
|
2207
|
-
}
|
|
2208
|
-
function sparkline(values, width = 24) {
|
|
2209
|
-
const glyphs = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
2210
|
-
if (!values.length) return glyphs[0].repeat(width);
|
|
2211
|
-
const slice = values.slice(-width);
|
|
2212
|
-
const max = Math.max(...slice, 1);
|
|
2213
|
-
return slice.map((v) => glyphs[Math.min(glyphs.length - 1, Math.floor(v / max * (glyphs.length - 1)))]).join("").padStart(width, glyphs[0]);
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
// src/tui/Typewriter.tsx
|
|
2217
|
-
import { Text } from "ink";
|
|
2218
|
-
import { useEffect, useState } from "react";
|
|
2219
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2220
|
-
var finished = /* @__PURE__ */ new Set();
|
|
2221
|
-
function Typewriter(props) {
|
|
2222
|
-
const start = props.once && finished.has(props.text) ? props.text.length : 0;
|
|
2223
|
-
const [n, setN] = useState(start);
|
|
2224
|
-
useEffect(() => {
|
|
2225
|
-
if (props.once && finished.has(props.text)) {
|
|
2226
|
-
setN(props.text.length);
|
|
2227
|
-
return;
|
|
2228
|
-
}
|
|
2229
|
-
setN(0);
|
|
2230
|
-
}, [props.text, props.once]);
|
|
2231
|
-
useEffect(() => {
|
|
2232
|
-
if (n >= props.text.length) return;
|
|
2233
|
-
const timer = setTimeout(() => {
|
|
2234
|
-
setN((v) => {
|
|
2235
|
-
const next = v + 1;
|
|
2236
|
-
if (props.once && next >= props.text.length) finished.add(props.text);
|
|
2237
|
-
return next;
|
|
2238
|
-
});
|
|
2239
|
-
}, props.ms ?? 16);
|
|
2240
|
-
return () => clearTimeout(timer);
|
|
2241
|
-
}, [n, props.text, props.ms, props.once]);
|
|
2242
|
-
const done = n >= props.text.length;
|
|
2243
|
-
return /* @__PURE__ */ jsxs(Text, { color: props.color, dimColor: props.dimColor, bold: props.bold, children: [
|
|
2244
|
-
props.text.slice(0, n),
|
|
2245
|
-
done ? null : /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u258D" })
|
|
2246
|
-
] });
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
// src/tui/width.ts
|
|
2250
|
-
import { useStdout } from "ink";
|
|
2251
|
-
function useCols() {
|
|
2252
|
-
const { stdout } = useStdout();
|
|
2253
|
-
const cols = stdout?.columns ?? 80;
|
|
2254
|
-
return Math.max(40, Math.min(cols - 2, 100));
|
|
2255
|
-
}
|
|
2256
|
-
function fit(text, width) {
|
|
2257
|
-
if (text.length <= width) return text;
|
|
2258
|
-
if (width <= 1) return "\u2026";
|
|
2259
|
-
return `${text.slice(0, width - 1)}\u2026`;
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
// src/tui/views/Dashboard.tsx
|
|
2263
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2264
|
-
function Dashboard({ session }) {
|
|
2265
|
-
const cols = useCols();
|
|
2266
|
-
const h = session.hardware;
|
|
2267
|
-
const line = fit(
|
|
2268
|
-
`${h.machineModel ?? t("unknownMachine")} \xB7 ${h.cpu.name ?? t("cpuNa")} \xB7 ${formatBytes(h.memory.totalBytes, 0)} ${memoryLabel(h)}`,
|
|
2269
|
-
cols
|
|
2270
|
-
);
|
|
2271
|
-
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", width: cols, children: [
|
|
2272
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: t("machine") }),
|
|
2273
|
-
/* @__PURE__ */ jsx2(Text2, { children: line }),
|
|
2274
|
-
/* @__PURE__ */ jsx2(Text2, { children: session.runtimes.map((r, i) => /* @__PURE__ */ jsxs2(Text2, { color: r.detected ? "green" : "gray", children: [
|
|
2275
|
-
i ? " " : "",
|
|
2276
|
-
r.detected ? "\u2713" : "\u25CB",
|
|
2277
|
-
r.label
|
|
2278
|
-
] }, r.id)) }),
|
|
2279
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: t("installedModels") }),
|
|
2280
|
-
session.rows.length === 0 && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: t("noModels") }),
|
|
2281
|
-
session.rows.slice(0, 6).map((row) => /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2282
|
-
/* @__PURE__ */ jsx2(Text2, { color: "green", children: fit(row.local.name, 18) }),
|
|
2283
|
-
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
2284
|
-
/* @__PURE__ */ jsx2(Text2, { color: gradeColor(row.compatibility?.grade), children: row.compatibility?.grade ?? "\u2014" }),
|
|
2285
|
-
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2286
|
-
" ",
|
|
2287
|
-
row.lastBenchmark?.benchmark.generationTokensPerSecond ? formatTokensPerSec(row.lastBenchmark.benchmark.generationTokensPerSecond) : t("notTested")
|
|
2288
|
-
] })
|
|
2289
|
-
] }, row.local.id)),
|
|
2290
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: t("recommended") }),
|
|
2291
|
-
session.recommendations.slice(0, 4).map((rec) => /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2292
|
-
/* @__PURE__ */ jsx2(Text2, { color: "magenta", children: fit(rec.useCase, 10) }),
|
|
2293
|
-
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2294
|
-
" ",
|
|
2295
|
-
fit(rec.model.name, 22),
|
|
2296
|
-
" "
|
|
2297
|
-
] }),
|
|
2298
|
-
/* @__PURE__ */ jsx2(Text2, { color: gradeColor(rec.grade), children: rec.grade })
|
|
2299
|
-
] }, `${rec.useCase}-${rec.model.id}`)),
|
|
2300
|
-
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2301
|
-
/* @__PURE__ */ jsx2(Text2, { color: session.runtimes.find((r) => r.id === "ollama")?.detected ? "green" : "yellow", children: session.runtimes.find((r) => r.id === "ollama")?.detected ? "\u2713 Ollama" : `\u25CB Ollama ${t("reqOllama")}` }),
|
|
2302
|
-
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
2303
|
-
/* @__PURE__ */ jsx2(Text2, { color: session.runtimes.find((r) => r.id === "lmstudio")?.detected ? "green" : "gray", children: session.runtimes.find((r) => r.id === "lmstudio")?.detected ? "\u2713 LM Studio" : `\u25CB LM Studio ${t("reqLmStudio")}` })
|
|
2304
|
-
] }),
|
|
2305
|
-
/* @__PURE__ */ jsx2(Typewriter, { text: fit(t("setupCta"), cols), ms: 18, color: "yellow" })
|
|
2306
|
-
] });
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
// src/tui/views/Models.tsx
|
|
2310
|
-
import { Box as Box2, Text as Text3 } from "ink";
|
|
2311
|
-
|
|
2312
|
-
// src/compatibility/types.ts
|
|
2313
|
-
var GRADE_MEANING = {
|
|
2314
|
-
S: "Excellent",
|
|
2315
|
-
A: "Recommended",
|
|
2316
|
-
B: "Good",
|
|
2317
|
-
C: "Tight",
|
|
2318
|
-
D: "CPU/offload",
|
|
2319
|
-
F: "Not recommended"
|
|
2320
|
-
};
|
|
2321
|
-
|
|
2322
|
-
// src/cli/color.ts
|
|
2323
|
-
var enabled = () => !process.env.NO_COLOR && process.stdout.isTTY;
|
|
2324
|
-
var wrap = (open, close = 39) => (text) => enabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
2325
|
-
var cyan = wrap(36);
|
|
2326
|
-
var green = wrap(32);
|
|
2327
|
-
var yellow = wrap(33);
|
|
2328
|
-
var red = wrap(31);
|
|
2329
|
-
var magenta = wrap(35);
|
|
2330
|
-
var bold = wrap(1, 22);
|
|
2331
|
-
var dim = wrap(2, 22);
|
|
2332
|
-
function gradeAnsi(grade) {
|
|
2333
|
-
if (grade === "S" || grade === "A") return green(grade);
|
|
2334
|
-
if (grade === "B") return yellow(grade);
|
|
2335
|
-
if (grade === "C") return magenta(grade);
|
|
2336
|
-
if (grade === "D" || grade === "F") return red(grade);
|
|
2337
|
-
return grade;
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
// src/cli/text.ts
|
|
2341
|
-
function hardwareText(session) {
|
|
2342
|
-
const h = session.hardware;
|
|
2343
|
-
const ram = formatBytes(h.memory.totalBytes, 0);
|
|
2344
|
-
const avail = h.memory.availableBytes ? formatBytes(h.memory.availableBytes) : "N/A";
|
|
2345
|
-
return [
|
|
2346
|
-
bold(cyan(t("machine"))),
|
|
2347
|
-
` ${h.machineModel ?? t("unknownMachine")}`,
|
|
2348
|
-
` ${h.cpu.name ?? t("cpuNa")}`,
|
|
2349
|
-
` CPU ${h.cpu.physicalCores ?? "N/A"} cores (${h.cpu.performanceCores ?? "?"}P / ${h.cpu.efficiencyCores ?? "?"}E)`,
|
|
2350
|
-
` GPU ${h.gpu.name ?? "N/A"}`,
|
|
2351
|
-
` ${memoryLabel(h).padEnd(13)} ${ram}`,
|
|
2352
|
-
` Available ${avail}`,
|
|
2353
|
-
` ${h.os} ${h.osVersion ?? h.arch}`
|
|
2354
|
-
].join("\n");
|
|
2355
|
-
}
|
|
2356
|
-
function runtimesText(session) {
|
|
2357
|
-
return [
|
|
2358
|
-
bold(cyan(t("runtimes"))),
|
|
2359
|
-
...session.runtimes.map((r) => {
|
|
2360
|
-
const mark = r.detected ? green("\u2713") : dim("\u25CB");
|
|
2361
|
-
const state = r.detected ? green(r.version ?? t("detected")) : dim(t("notDetected"));
|
|
2362
|
-
return ` ${mark} ${r.label.padEnd(14)} ${state}`;
|
|
2363
|
-
})
|
|
2364
|
-
].join("\n");
|
|
2365
|
-
}
|
|
2366
|
-
function modelsText(session) {
|
|
2367
|
-
const lines = [bold(cyan(t("installedModels"))), dim("MODEL INSTALLED FIT EST. SPEED MEASURED")];
|
|
2368
|
-
for (const row of session.rows) {
|
|
2369
|
-
const fit2 = row.compatibility?.grade ?? "\u2014";
|
|
2370
|
-
const est = row.compatibility?.estimatedTokensPerSecond ? `~${formatNumber(row.compatibility.estimatedTokensPerSecond)} t/s` : "\u2014";
|
|
2371
|
-
const measured = row.lastBenchmark?.benchmark.generationTokensPerSecond ? formatTokensPerSec(row.lastBenchmark.benchmark.generationTokensPerSecond) : t("notTested");
|
|
2372
|
-
lines.push(
|
|
2373
|
-
`${green(row.local.name.padEnd(22))} \u2713 ${gradeAnsi(fit2).padEnd(9)} ${est.padEnd(14)} ${measured}`
|
|
2374
|
-
);
|
|
2375
|
-
}
|
|
2376
|
-
lines.push("", t("compatible"));
|
|
2377
|
-
const extras = session.catalog.filter((model) => !session.rows.some((row) => idsLikelyMatch(row.local.id, model.id))).map((model) => ({ model, fit: localCompatibility(session.hardware, model) })).sort((a, b) => a.fit.grade.localeCompare(b.fit.grade)).slice(0, 12);
|
|
2378
|
-
for (const extra of extras) {
|
|
2379
|
-
const est = extra.fit.estimatedTokensPerSecond ? `~${formatNumber(extra.fit.estimatedTokensPerSecond)} t/s` : "\u2014";
|
|
2380
|
-
lines.push(`${extra.model.name.padEnd(22)} \u2014 ${extra.fit.grade.padEnd(9)} ${est.padEnd(14)} ${t("estimated")}`);
|
|
2381
|
-
}
|
|
2382
|
-
return lines.join("\n");
|
|
2383
|
-
}
|
|
2384
|
-
function recommendText(session) {
|
|
2385
|
-
const lines = [bold(cyan(t("recommended"))), dim(t("recommendedHint")), "", dim(t("credits")), ""];
|
|
2386
|
-
for (const rec of session.recommendations) {
|
|
2387
|
-
lines.push(
|
|
2388
|
-
`${magenta(rec.useCase.padEnd(12))} ${rec.model.name.padEnd(22)} ${gradeAnsi(rec.grade)} ${GRADE_MEANING[rec.grade]} ~${na(rec.estimatedTokensPerSecond)} t/s est.`
|
|
2389
|
-
);
|
|
2390
|
-
}
|
|
2391
|
-
lines.push("", bold(yellow(t("agentsTitle"))));
|
|
2392
|
-
for (const agent of session.agents) {
|
|
2393
|
-
lines.push(
|
|
2394
|
-
` ${agent.detected ? green("\u2713") : dim("\u25CB")} ${agent.label.padEnd(12)} ${agent.detected ? green(t("detected")) : dim(t("agentMissing"))}`
|
|
2395
|
-
);
|
|
2396
|
-
}
|
|
2397
|
-
lines.push("", dim(t("recommendCliHint")));
|
|
2398
|
-
return lines.join("\n");
|
|
2399
|
-
}
|
|
2400
|
-
function historyText(records) {
|
|
2401
|
-
if (!records.length) return t("historyEmpty");
|
|
2402
|
-
return records.map((r) => {
|
|
2403
|
-
const gen = r.benchmark.generationTokensPerSecond;
|
|
2404
|
-
return `${r.timestamp} ${r.model.id.padEnd(18)} ${gen ? formatTokensPerSec(gen) : "N/A"} score ${r.score?.total ?? "N/A"}`;
|
|
2405
|
-
}).join("\n");
|
|
2406
|
-
}
|
|
2407
|
-
function historyCsv(records) {
|
|
2408
|
-
const header = "timestamp,model,prompt_tps,generation_tps,peak_memory_gb,avg_cpu,score";
|
|
2409
|
-
const rows = records.map(
|
|
2410
|
-
(r) => [
|
|
2411
|
-
r.timestamp,
|
|
2412
|
-
r.model.id,
|
|
2413
|
-
r.benchmark.promptTokensPerSecond ?? "",
|
|
2414
|
-
r.benchmark.generationTokensPerSecond ?? "",
|
|
2415
|
-
r.resources.peakMemoryGb ?? "",
|
|
2416
|
-
r.resources.avgCpuPercent ?? "",
|
|
2417
|
-
r.score?.total ?? ""
|
|
2418
|
-
].join(",")
|
|
2419
|
-
);
|
|
2420
|
-
return [header, ...rows].join("\n");
|
|
2421
|
-
}
|
|
2422
|
-
function doctorText(session) {
|
|
2423
|
-
const node = process.version.replace(/^v/, "");
|
|
2424
|
-
const apple = session.hardware.cpu.appleSilicon ? `${session.hardware.cpu.appleSilicon.generation} ${session.hardware.cpu.appleSilicon.variant}` : "no";
|
|
2425
|
-
const lines = [
|
|
2426
|
-
t("doctorTitle"),
|
|
2427
|
-
"",
|
|
2428
|
-
`\u2713 Node.js ${node}`,
|
|
2429
|
-
`${session.hardware.cpu.appleSilicon ? "\u2713" : "\u25CB"} Apple Silicon ${apple}`,
|
|
2430
|
-
`${session.hardware.gpu.metal ? "\u2713" : "\u25CB"} Metal`,
|
|
2431
|
-
...session.runtimes.map((r) => `${r.detected ? "\u2713" : "\u25CB"} ${r.label.padEnd(14)} ${r.version ?? ""}`.trimEnd()),
|
|
2432
|
-
`${session.networkUsed ? "\u2713" : "\u25CB"} CanIRun.ai (midudev)`,
|
|
2433
|
-
...session.agents.map(
|
|
2434
|
-
(a) => `${a.detected ? "\u2713" : "\u25CB"} ${a.label.padEnd(14)} ${a.detected ? t("detected") : a.launch}`
|
|
2435
|
-
),
|
|
2436
|
-
...session.runtimes.find((r) => r.id === "ollama")?.detected ? [] : [`\u25CB Ollama ${t("reqOllama")}`],
|
|
2437
|
-
...session.runtimes.find((r) => r.id === "lmstudio")?.detected ? [] : [`\u25CB LM Studio ${t("reqLmStudio")}`],
|
|
2438
|
-
...session.runtimes.find((r) => r.id === "docker")?.detected ? [`\u2713 Docker ${t("dockerHint")}`] : [`\u25CB Docker ${t("reqDocker")}`],
|
|
2439
|
-
"",
|
|
2440
|
-
session.llamaBench ? t("doctorReady", { count: session.models.filter((m) => m.artifactPath).length }) : t("doctorNoBench")
|
|
2441
|
-
];
|
|
2442
|
-
return lines.join("\n");
|
|
2443
|
-
}
|
|
2444
|
-
function reportMarkdown(record) {
|
|
2445
|
-
const machine = `${record.machine.cpu ?? "unknown"} / ${record.machine.memoryGb ?? "?"} GB`;
|
|
2446
|
-
return `## ${t("reportTitle")}
|
|
2447
|
-
|
|
2448
|
-
**Machine:** ${machine}
|
|
2449
|
-
**Model:** ${record.model.id}
|
|
2450
|
-
|
|
2451
|
-
| Metric | Result |
|
|
2452
|
-
|---|---:|
|
|
2453
|
-
| Prompt processing | ${record.benchmark.promptTokensPerSecond ?? "N/A"} t/s |
|
|
2454
|
-
| Generation | ${record.benchmark.generationTokensPerSecond ?? "N/A"} t/s |
|
|
2455
|
-
| Peak memory | ${record.resources.peakMemoryGb ?? "N/A"} GB |
|
|
2456
|
-
| Average GPU | ${record.resources.avgGpuPercent ?? "N/A"}% |
|
|
2457
|
-
| Average power | ${record.resources.avgPackagePowerWatts ?? "N/A"} W |
|
|
2458
|
-
| Efficiency | ${record.resources.tokensPerSecondPerWatt ?? "N/A"} t/s/W |
|
|
2459
|
-
| ${t("scoreLabel")} | ${record.score?.total ?? "N/A"} / 100 |
|
|
2460
|
-
`;
|
|
2461
|
-
}
|
|
2462
|
-
function compareText(records) {
|
|
2463
|
-
const latestByModel = /* @__PURE__ */ new Map();
|
|
2464
|
-
for (const record of records) latestByModel.set(record.model.id, record);
|
|
2465
|
-
const list = [...latestByModel.values()].slice(0, 4);
|
|
2466
|
-
if (list.length < 2) return t("compareNeedTwo");
|
|
2467
|
-
const names = list.map((r) => r.model.id);
|
|
2468
|
-
const row = (label, pick) => `${label.padEnd(20)}${list.map((r) => pick(r).padStart(14)).join("")}`;
|
|
2469
|
-
const winner = (metric, higher = true) => {
|
|
2470
|
-
let best;
|
|
2471
|
-
for (const r of list) {
|
|
2472
|
-
const v = metric(r);
|
|
2473
|
-
if (v === void 0) continue;
|
|
2474
|
-
if (!best) best = r;
|
|
2475
|
-
else {
|
|
2476
|
-
const b = metric(best);
|
|
2477
|
-
if (b === void 0) best = r;
|
|
2478
|
-
else if (higher ? v > b : v < b) best = r;
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
return best?.model.id ?? "N/A";
|
|
2482
|
-
};
|
|
2483
|
-
return [
|
|
2484
|
-
t("compareTitle"),
|
|
2485
|
-
names.join(" vs "),
|
|
2486
|
-
row("Generation t/s", (r) => formatNumber(r.benchmark.generationTokensPerSecond)),
|
|
2487
|
-
row("Prompt t/s", (r) => formatNumber(r.benchmark.promptTokensPerSecond)),
|
|
2488
|
-
row("Peak RAM", (r) => r.resources.peakMemoryGb ? `${r.resources.peakMemoryGb} GB` : "N/A"),
|
|
2489
|
-
row("Avg CPU", (r) => r.resources.avgCpuPercent ? `${r.resources.avgCpuPercent}%` : "N/A"),
|
|
2490
|
-
row("Power", (r) => r.resources.avgPackagePowerWatts ? `${r.resources.avgPackagePowerWatts} W` : "N/A"),
|
|
2491
|
-
row("t/s/W", (r) => formatNumber(r.resources.tokensPerSecondPerWatt)),
|
|
2492
|
-
"",
|
|
2493
|
-
t("winner"),
|
|
2494
|
-
`Speed ${winner((r) => r.benchmark.generationTokensPerSecond)}`,
|
|
2495
|
-
`Memory ${winner((r) => r.resources.peakMemoryGb, false)}`,
|
|
2496
|
-
`Efficiency ${winner((r) => r.resources.tokensPerSecondPerWatt)}`,
|
|
2497
|
-
t("qualityNote")
|
|
2498
|
-
].join("\n");
|
|
2499
|
-
}
|
|
2500
|
-
function resultText(record, assessment) {
|
|
2501
|
-
return [
|
|
2502
|
-
record.model.id,
|
|
2503
|
-
"",
|
|
2504
|
-
`Prompt processing ${record.benchmark.promptTokensPerSecond ?? "N/A"} t/s`,
|
|
2505
|
-
`Generation ${record.benchmark.generationTokensPerSecond ?? "N/A"} t/s`,
|
|
2506
|
-
`Peak memory ${record.resources.peakMemoryGb ?? "N/A"} GB`,
|
|
2507
|
-
`Average GPU ${record.resources.avgGpuPercent ?? "N/A"} %`,
|
|
2508
|
-
`Average CPU ${record.resources.avgCpuPercent ?? "N/A"} %`,
|
|
2509
|
-
`Average power ${record.resources.avgPackagePowerWatts ?? "N/A"} W`,
|
|
2510
|
-
`Efficiency ${record.resources.tokensPerSecondPerWatt ?? "N/A"} t/s/W`,
|
|
2511
|
-
"",
|
|
2512
|
-
`${t("scoreLabel")} ${record.score?.total ?? "N/A"}/100`,
|
|
2513
|
-
"",
|
|
2514
|
-
...assessment.map((line) => `\u2713 ${line}`)
|
|
2515
|
-
].join("\n");
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
// src/tui/views/Models.tsx
|
|
2519
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2520
|
-
function ModelsView({ session }) {
|
|
2521
|
-
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
|
|
2522
|
-
/* @__PURE__ */ jsx3(Text3, { children: modelsText(session) }),
|
|
2523
|
-
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: t("modelsHint") })
|
|
2524
|
-
] });
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
// src/tui/views/Hardware.tsx
|
|
2528
|
-
import { Box as Box3, Text as Text4 } from "ink";
|
|
2529
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2530
|
-
function HardwareView({ session }) {
|
|
2531
|
-
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
|
|
2532
|
-
/* @__PURE__ */ jsx4(Text4, { children: hardwareText(session) }),
|
|
2533
|
-
/* @__PURE__ */ jsx4(Text4, { children: runtimesText(session) }),
|
|
2534
|
-
/* @__PURE__ */ jsx4(Text4, { color: "yellow", children: t("hardwareHint") })
|
|
2535
|
-
] });
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
// src/tui/views/Recommend.tsx
|
|
2539
|
-
import { Box as Box4, Text as Text5 } from "ink";
|
|
2540
|
-
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2541
|
-
function RecommendView({ session }) {
|
|
2542
|
-
const cols = useCols();
|
|
2543
|
-
return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", width: cols, children: [
|
|
2544
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "magenta", children: t("recommended") }),
|
|
2545
|
-
/* @__PURE__ */ jsx5(Typewriter, { text: fit(t("aboutRecommend"), cols), ms: 12, dimColor: true }),
|
|
2546
|
-
session.recommendations.map((rec) => /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
2547
|
-
/* @__PURE__ */ jsx5(Text5, { color: "magenta", children: fit(rec.useCase, 10) }),
|
|
2548
|
-
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
2549
|
-
" ",
|
|
2550
|
-
fit(rec.model.name, 22),
|
|
2551
|
-
" "
|
|
2552
|
-
] }),
|
|
2553
|
-
/* @__PURE__ */ jsx5(Text5, { color: gradeColor(rec.grade), children: rec.grade }),
|
|
2554
|
-
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
2555
|
-
" ",
|
|
2556
|
-
GRADE_MEANING[rec.grade]
|
|
2557
|
-
] }),
|
|
2558
|
-
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
2559
|
-
" ",
|
|
2560
|
-
resolveOllamaTag(rec.model.id, rec.model.name) ?? "\u2014"
|
|
2561
|
-
] })
|
|
2562
|
-
] }, `${rec.useCase}-${rec.model.id}`)),
|
|
2563
|
-
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: fit(t("setupCta"), cols) })
|
|
2564
|
-
] });
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
// src/tui/views/Setup.tsx
|
|
2568
|
-
import { Box as Box5, Text as Text6, useApp, useInput } from "ink";
|
|
2569
|
-
import { useState as useState2 } from "react";
|
|
2570
|
-
|
|
2571
|
-
// src/integrations/catalog.ts
|
|
2572
|
-
var INTEGRATIONS = {
|
|
2573
|
-
opencode: {
|
|
2574
|
-
id: "opencode",
|
|
2575
|
-
docsUrl: "https://docs.ollama.com/integrations/opencode",
|
|
2576
|
-
ollamaLaunch: "opencode",
|
|
2577
|
-
minGenerationTps: 12,
|
|
2578
|
-
allowedGrades: ["S", "A", "B"],
|
|
2579
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2580
|
-
},
|
|
2581
|
-
openclaw: {
|
|
2582
|
-
id: "openclaw",
|
|
2583
|
-
docsUrl: "https://docs.ollama.com/integrations/openclaw",
|
|
2584
|
-
ollamaLaunch: "openclaw",
|
|
2585
|
-
minGenerationTps: 12,
|
|
2586
|
-
allowedGrades: ["S", "A", "B"],
|
|
2587
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2588
|
-
},
|
|
2589
|
-
hermes: {
|
|
2590
|
-
id: "hermes",
|
|
2591
|
-
docsUrl: "https://docs.ollama.com/integrations/hermes",
|
|
2592
|
-
ollamaLaunch: "hermes",
|
|
2593
|
-
minGenerationTps: 12,
|
|
2594
|
-
allowedGrades: ["S", "A"],
|
|
2595
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2596
|
-
},
|
|
2597
|
-
claude: {
|
|
2598
|
-
id: "claude",
|
|
2599
|
-
docsUrl: "https://docs.ollama.com/integrations/claude-code",
|
|
2600
|
-
ollamaLaunch: "claude",
|
|
2601
|
-
minGenerationTps: 12,
|
|
2602
|
-
allowedGrades: ["S", "A", "B"],
|
|
2603
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2604
|
-
}
|
|
2605
|
-
};
|
|
2606
|
-
|
|
2607
|
-
// src/integrations/types.ts
|
|
2608
|
-
var INTEGRATION_IDS = ["opencode", "openclaw", "hermes", "claude"];
|
|
2609
|
-
|
|
2610
|
-
// src/integrations/decide.ts
|
|
2611
|
-
var GRADE_RANK = { S: 5, A: 4, B: 3, C: 2, D: 1, F: 0 };
|
|
2612
|
-
function useCaseOk(useCases, needed) {
|
|
2613
|
-
if (!useCases?.length) return needed.includes("chat");
|
|
2614
|
-
return useCases.some((u) => needed.some((n) => u.includes(n)));
|
|
2615
|
-
}
|
|
2616
|
-
function decideLaunch(session, id) {
|
|
2617
|
-
const def = INTEGRATIONS[id];
|
|
2618
|
-
const reasons = [];
|
|
2619
|
-
const ollama = session.runtimes.find((r) => r.id === "ollama")?.detected;
|
|
2620
|
-
if (!ollama) reasons.push(t("launchNeedOllama"));
|
|
2621
|
-
const candidates = [];
|
|
2622
|
-
for (const row of session.rows) {
|
|
2623
|
-
const useCases = row.catalog?.useCase ?? ["chat", "code"];
|
|
2624
|
-
if (!useCaseOk(useCases, def.useCases)) continue;
|
|
2625
|
-
const tag = resolveOllamaTag(row.local.id, row.local.name, row.catalog?.id, row.catalog?.name);
|
|
2626
|
-
if (!tag) continue;
|
|
2627
|
-
const tps = row.lastBenchmark?.benchmark.generationTokensPerSecond;
|
|
2628
|
-
candidates.push({
|
|
2629
|
-
modelId: row.local.id,
|
|
2630
|
-
ollamaTag: tag,
|
|
2631
|
-
installed: true,
|
|
2632
|
-
origin: row.lastBenchmark ? "measured" : "estimated",
|
|
2633
|
-
grade: row.compatibility?.grade,
|
|
2634
|
-
tps
|
|
2635
|
-
});
|
|
2636
|
-
}
|
|
2637
|
-
if (!candidates.length) {
|
|
2638
|
-
for (const model of session.catalog) {
|
|
2639
|
-
if (!useCaseOk(model.useCase, def.useCases)) continue;
|
|
2640
|
-
const fit2 = localCompatibility(session.hardware, model);
|
|
2641
|
-
if (!def.allowedGrades.includes(fit2.grade)) continue;
|
|
2642
|
-
const tag = resolveOllamaTag(model.id, model.name);
|
|
2643
|
-
if (!tag) continue;
|
|
2644
|
-
candidates.push({
|
|
2645
|
-
modelId: model.id,
|
|
2646
|
-
ollamaTag: tag,
|
|
2647
|
-
installed: false,
|
|
2648
|
-
origin: "estimated",
|
|
2649
|
-
grade: fit2.grade
|
|
2650
|
-
});
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
candidates.sort((a, b) => {
|
|
2654
|
-
if (a.installed !== b.installed) return a.installed ? -1 : 1;
|
|
2655
|
-
return (GRADE_RANK[b.grade ?? "F"] ?? 0) - (GRADE_RANK[a.grade ?? "F"] ?? 0);
|
|
2656
|
-
});
|
|
2657
|
-
const pick = candidates[0];
|
|
2658
|
-
if (!pick) reasons.push(t("launchNoModel"));
|
|
2659
|
-
if (pick?.grade && !def.allowedGrades.includes(pick.grade)) {
|
|
2660
|
-
reasons.push(t("launchGradeFail", { grade: pick.grade, allowed: def.allowedGrades.join(",") }));
|
|
2661
|
-
}
|
|
2662
|
-
if (pick?.origin === "measured" && pick.tps !== void 0 && pick.tps < def.minGenerationTps) {
|
|
2663
|
-
reasons.push(t("launchSlowFail", { tps: pick.tps.toFixed(1), min: def.minGenerationTps }));
|
|
2664
|
-
}
|
|
2665
|
-
if (pick && pick.origin === "estimated" && (!pick.grade || GRADE_RANK[pick.grade] < GRADE_RANK.B)) {
|
|
2666
|
-
reasons.push(t("launchEstimateWeak"));
|
|
2667
|
-
}
|
|
2668
|
-
const eligible = Boolean(ollama && pick && reasons.length === 0);
|
|
2669
|
-
const command = pick ? `ollama launch ${def.ollamaLaunch} --model ${pick.ollamaTag}` : `ollama launch ${def.ollamaLaunch}`;
|
|
2670
|
-
return {
|
|
2671
|
-
integration: id,
|
|
2672
|
-
docsUrl: def.docsUrl,
|
|
2673
|
-
eligible,
|
|
2674
|
-
reasons: eligible ? [t("launchOk")] : reasons,
|
|
2675
|
-
modelId: pick?.modelId,
|
|
2676
|
-
ollamaTag: pick?.ollamaTag,
|
|
2677
|
-
installed: pick?.installed ?? false,
|
|
2678
|
-
origin: pick?.origin,
|
|
2679
|
-
grade: pick?.grade,
|
|
2680
|
-
command
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
function decideAll(session) {
|
|
2684
|
-
return INTEGRATION_IDS.map((id) => decideLaunch(session, id));
|
|
2685
|
-
}
|
|
2686
|
-
function launchText(decisions) {
|
|
2687
|
-
const lines = [t("launchTitle"), t("launchHint"), ""];
|
|
2688
|
-
for (const d of decisions) {
|
|
2689
|
-
lines.push(`${d.integration} ${d.docsUrl}`);
|
|
2690
|
-
lines.push(` ${d.eligible ? "\u2713" : "\u25CB"} ${d.reasons.join(" ")}`);
|
|
2691
|
-
if (d.modelId) {
|
|
2692
|
-
lines.push(
|
|
2693
|
-
` ${t("launchModel")}: ${d.modelId} ${d.grade ?? "\u2014"} ${d.origin === "measured" ? t("measured") : t("estimated")} ${d.installed ? t("tasksInstalled") : t("tasksNotInstalled")}`
|
|
2694
|
-
);
|
|
2695
|
-
}
|
|
2696
|
-
lines.push(` ${d.command}`);
|
|
2697
|
-
if (d.eligible) lines.push(` ${t("launchRunHint", { tool: d.integration })}`);
|
|
2698
|
-
else lines.push(` ${t("launchBlocked")}`);
|
|
2699
|
-
lines.push("");
|
|
2700
|
-
}
|
|
2701
|
-
return lines.join("\n");
|
|
2702
|
-
}
|
|
2703
|
-
function parseIntegration(raw) {
|
|
2704
|
-
if (!raw) return void 0;
|
|
2705
|
-
const id = raw.toLowerCase();
|
|
2706
|
-
if (id === "claude-code") return "claude";
|
|
2707
|
-
return INTEGRATION_IDS.find((item) => item === id);
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
// src/integrations/execute.ts
|
|
2711
|
-
init_process();
|
|
2712
|
-
async function executeLaunch(decision) {
|
|
2713
|
-
if (!decision.eligible || !decision.ollamaTag) {
|
|
2714
|
-
return { ok: false, log: t("launchBlocked") };
|
|
2715
|
-
}
|
|
2716
|
-
const def = INTEGRATIONS[decision.integration];
|
|
2717
|
-
if (!def) return { ok: false, log: t("launchUnknown") };
|
|
2718
|
-
const lines = [];
|
|
2719
|
-
if (!decision.installed) {
|
|
2720
|
-
lines.push(t("launchPulling", { model: decision.ollamaTag }));
|
|
2721
|
-
const pull = await runCommand("ollama", ["pull", decision.ollamaTag], { timeout: 30 * 6e4 });
|
|
2722
|
-
if (pull.exitCode !== 0) {
|
|
2723
|
-
return { ok: false, log: `${lines.join("\n")}
|
|
2724
|
-
${pull.stderr || t("launchPullFail")}` };
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
const args2 = ["launch", def.ollamaLaunch, "--model", decision.ollamaTag];
|
|
2728
|
-
if (def.id === "openclaw" || def.id === "claude") args2.push("--yes");
|
|
2729
|
-
lines.push(`ollama ${args2.join(" ")}`);
|
|
2730
|
-
const launched = await runCommand("ollama", args2, { timeout: 12e4 });
|
|
2731
|
-
if (launched.exitCode !== 0) {
|
|
2732
|
-
return {
|
|
2733
|
-
ok: false,
|
|
2734
|
-
log: `${lines.join("\n")}
|
|
2735
|
-
${launched.stderr || launched.stdout || t("launchExecFail")}`
|
|
2736
|
-
};
|
|
2737
|
-
}
|
|
2738
|
-
return { ok: true, log: `${lines.join("\n")}
|
|
2739
|
-
${launched.stdout}`.trim() };
|
|
2740
|
-
}
|
|
2741
|
-
async function installWithBrew(kind) {
|
|
2742
|
-
const { brewInstall: brewInstall2 } = await Promise.resolve().then(() => (init_brew(), brew_exports));
|
|
2743
|
-
if (kind === "ollama") {
|
|
2744
|
-
const result = await brewInstall2(["install", "ollama"]);
|
|
2745
|
-
if (result.ok) {
|
|
2746
|
-
void runCommand("ollama", ["serve"], { timeout: 4e3 });
|
|
2747
|
-
}
|
|
2748
|
-
return result;
|
|
2749
|
-
}
|
|
2750
|
-
return brewInstall2(["install", "--cask", "lm-studio"]);
|
|
2751
|
-
}
|
|
2752
|
-
async function executeDockerOllama() {
|
|
2753
|
-
const { brewInstallCask: brewInstallCask2, dockerReady: dockerReady2, startDockerDesktop: startDockerDesktop2, waitForDocker: waitForDocker2 } = await Promise.resolve().then(() => (init_brew(), brew_exports));
|
|
2754
|
-
const notes = [];
|
|
2755
|
-
if (!await dockerReady2()) {
|
|
2756
|
-
notes.push(t("dockerBrewInstall"));
|
|
2757
|
-
const installed2 = await brewInstallCask2("docker");
|
|
2758
|
-
if (!installed2.ok) {
|
|
2759
|
-
return { ok: false, log: `${t("reqDocker")}
|
|
2760
|
-
${installed2.log}` };
|
|
2761
|
-
}
|
|
2762
|
-
notes.push(t("dockerDesktopWait"));
|
|
2763
|
-
await startDockerDesktop2();
|
|
2764
|
-
const up = await waitForDocker2();
|
|
2765
|
-
if (!up) return { ok: false, log: `${notes.join("\n")}
|
|
2766
|
-
${t("dockerRunFail")}` };
|
|
2767
|
-
}
|
|
2768
|
-
const start = await runCommand("docker", ["start", "pudu-ollama"], { timeout: 3e4 });
|
|
2769
|
-
if (start.exitCode === 0) return { ok: true, log: t("dockerRunOk") };
|
|
2770
|
-
const args2 = DOCKER_OLLAMA.split(" ").slice(1);
|
|
2771
|
-
const result = await runCommand("docker", args2, { timeout: 10 * 6e4 });
|
|
2772
|
-
if (result.exitCode !== 0) {
|
|
2773
|
-
return { ok: false, log: result.stderr || t("dockerRunFail") };
|
|
2774
|
-
}
|
|
2775
|
-
return { ok: true, log: t("dockerRunOk") };
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
// src/integrations/pull.ts
|
|
2779
|
-
init_process();
|
|
2780
|
-
var INSTALLABLE = ["S", "A", "B"];
|
|
2781
|
-
function canInstallGrade(grade) {
|
|
2782
|
-
return Boolean(grade && INSTALLABLE.includes(grade));
|
|
2783
|
-
}
|
|
2784
|
-
async function pullOllamaModel(modelId, extraName) {
|
|
2785
|
-
const tag = resolveOllamaTag(modelId, extraName);
|
|
2786
|
-
if (!tag) {
|
|
2787
|
-
return { ok: false, log: t("noOllamaTag", { model: extraName ?? modelId }), tag: modelId };
|
|
2788
|
-
}
|
|
2789
|
-
const result = await runCommand("ollama", ["pull", tag], { timeout: 30 * 6e4 });
|
|
2790
|
-
if (result.exitCode !== 0) {
|
|
2791
|
-
return { ok: false, log: result.stderr || t("launchPullFail"), tag };
|
|
2792
|
-
}
|
|
2793
|
-
return { ok: true, log: t("modelPulled", { model: tag }), tag };
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
// src/tui/views/Setup.tsx
|
|
2797
|
-
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2798
|
-
var TOOLS = [
|
|
2799
|
-
{ key: "1", id: "opencode", label: "OpenCode" },
|
|
2800
|
-
{ key: "2", id: "hermes", label: "Hermes" },
|
|
2801
|
-
{ key: "3", id: "openclaw", label: "OpenClaw" }
|
|
2802
|
-
];
|
|
2803
|
-
function SetupView({ session }) {
|
|
2804
|
-
const cols = useCols();
|
|
2805
|
-
const { exit } = useApp();
|
|
2806
|
-
const [cursor, setCursor] = useState2(0);
|
|
2807
|
-
const [log, setLog] = useState2("");
|
|
2808
|
-
const [busy, setBusy] = useState2(false);
|
|
2809
|
-
const recs = session.recommendations;
|
|
2810
|
-
const rec = recs[cursor];
|
|
2811
|
-
useInput((input, key) => {
|
|
2812
|
-
if (busy) return;
|
|
2813
|
-
if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
|
|
2814
|
-
if (key.downArrow) setCursor((c) => Math.min(Math.max(recs.length - 1, 0), c + 1));
|
|
2815
|
-
if ((input.toLowerCase() === "i" || key.return) && rec) {
|
|
2816
|
-
if (!canInstallGrade(rec.grade)) {
|
|
2817
|
-
setLog(t("installGradeBlock", { grade: rec.grade }));
|
|
2818
|
-
return;
|
|
2819
|
-
}
|
|
2820
|
-
setBusy(true);
|
|
2821
|
-
setLog(t("launchPulling", { model: rec.model.name }));
|
|
2822
|
-
void pullOllamaModel(rec.model.id, rec.model.name).then((result) => {
|
|
2823
|
-
setLog(result.log);
|
|
2824
|
-
setBusy(false);
|
|
2825
|
-
});
|
|
2826
|
-
}
|
|
2827
|
-
if (input === "4") {
|
|
2828
|
-
setBusy(true);
|
|
2829
|
-
setLog(t("dockerStarting"));
|
|
2830
|
-
void executeDockerOllama().then((result) => {
|
|
2831
|
-
setLog(result.log);
|
|
2832
|
-
setBusy(false);
|
|
2833
|
-
});
|
|
2834
|
-
return;
|
|
2835
|
-
}
|
|
2836
|
-
if (input === "5") {
|
|
2837
|
-
setBusy(true);
|
|
2838
|
-
setLog(t("brewInstalling", { pkg: "ollama" }));
|
|
2839
|
-
void installWithBrew("ollama").then((result) => {
|
|
2840
|
-
setLog(result.log);
|
|
2841
|
-
setBusy(false);
|
|
2842
|
-
});
|
|
2843
|
-
return;
|
|
2844
|
-
}
|
|
2845
|
-
if (input === "6") {
|
|
2846
|
-
setBusy(true);
|
|
2847
|
-
setLog(t("brewInstalling", { pkg: "lm-studio" }));
|
|
2848
|
-
void installWithBrew("lmstudio").then((result) => {
|
|
2849
|
-
setLog(result.log);
|
|
2850
|
-
setBusy(false);
|
|
2851
|
-
});
|
|
2852
|
-
return;
|
|
2853
|
-
}
|
|
2854
|
-
const tool = TOOLS.find((item) => item.key === input)?.id;
|
|
2855
|
-
if (tool) {
|
|
2856
|
-
const ollama = session.runtimes.find((r) => r.id === "ollama")?.detected;
|
|
2857
|
-
const agent = session.agents.find((a) => a.id === tool);
|
|
2858
|
-
if (!ollama) {
|
|
2859
|
-
setBusy(true);
|
|
2860
|
-
setLog(t("brewInstalling", { pkg: "ollama" }));
|
|
2861
|
-
void installWithBrew("ollama").then((brewed) => {
|
|
2862
|
-
setLog(brewed.log);
|
|
2863
|
-
setBusy(false);
|
|
2864
|
-
});
|
|
2865
|
-
return;
|
|
2866
|
-
}
|
|
2867
|
-
if (agent && !agent.detected) {
|
|
2868
|
-
setBusy(true);
|
|
2869
|
-
setLog(t("linkingTool", { tool, model: rec?.model.id ?? "" }));
|
|
2870
|
-
const decision = decideLaunch(session, tool);
|
|
2871
|
-
void executeLaunch(
|
|
2872
|
-
decision.eligible ? decision : { ...decision, eligible: true, ollamaTag: decision.ollamaTag ?? "qwen3:8b", installed: false }
|
|
2873
|
-
).then((result) => {
|
|
2874
|
-
setLog(result.log);
|
|
2875
|
-
setBusy(false);
|
|
2876
|
-
});
|
|
2877
|
-
return;
|
|
2878
|
-
}
|
|
2879
|
-
if (rec) {
|
|
2880
|
-
const decision = decideLaunch(session, tool);
|
|
2881
|
-
if (!decision.eligible) {
|
|
2882
|
-
setLog(decision.reasons.join(" "));
|
|
2883
|
-
return;
|
|
2884
|
-
}
|
|
2885
|
-
setBusy(true);
|
|
2886
|
-
setLog(t("linkingTool", { tool, model: rec.model.id }));
|
|
2887
|
-
void executeLaunch(decision).then((result) => {
|
|
2888
|
-
setLog(result.log);
|
|
2889
|
-
setBusy(false);
|
|
2890
|
-
if (result.ok) exit();
|
|
2891
|
-
});
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
});
|
|
2895
|
-
const tag = rec ? resolveOllamaTag(rec.model.id, rec.model.name) : void 0;
|
|
2896
|
-
return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", width: cols, children: [
|
|
2897
|
-
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: fit(t("setupTitle"), cols) }),
|
|
2898
|
-
recs.map((item, i) => /* @__PURE__ */ jsxs6(Text6, { color: i === cursor ? "cyan" : void 0, children: [
|
|
2899
|
-
i === cursor ? "\u276F " : " ",
|
|
2900
|
-
fit(item.model.name, 20),
|
|
2901
|
-
" ",
|
|
2902
|
-
/* @__PURE__ */ jsx6(Text6, { color: gradeColor(item.grade), children: item.grade }),
|
|
2903
|
-
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2904
|
-
" ",
|
|
2905
|
-
resolveOllamaTag(item.model.id, item.model.name) ?? "\u2014"
|
|
2906
|
-
] })
|
|
2907
|
-
] }, `${item.useCase}-${item.model.id}`)),
|
|
2908
|
-
session.agents.map((agent, index) => /* @__PURE__ */ jsxs6(Text6, { color: agent.detected ? "green" : "yellow", children: [
|
|
2909
|
-
"[",
|
|
2910
|
-
index + 1,
|
|
2911
|
-
"] ",
|
|
2912
|
-
agent.detected ? "\u2713" : "\u25CB",
|
|
2913
|
-
" ",
|
|
2914
|
-
agent.label,
|
|
2915
|
-
agent.detected ? "" : ` \u2192 ${agent.launch}`
|
|
2916
|
-
] }, agent.id)),
|
|
2917
|
-
/* @__PURE__ */ jsxs6(Text6, { color: session.runtimes.find((r) => r.id === "docker")?.detected ? "green" : "yellow", children: [
|
|
2918
|
-
"[4] ",
|
|
2919
|
-
session.runtimes.find((r) => r.id === "docker")?.detected ? "\u2713" : "\u25CB",
|
|
2920
|
-
" Docker",
|
|
2921
|
-
session.runtimes.find((r) => r.id === "docker")?.detected ? ` \u2192 ${t("dockerHint")}` : ` \u2192 ${t("reqDocker")}`
|
|
2922
|
-
] }),
|
|
2923
|
-
/* @__PURE__ */ jsxs6(Text6, { color: session.runtimes.find((r) => r.id === "ollama")?.detected ? "green" : "yellow", children: [
|
|
2924
|
-
"[5] ",
|
|
2925
|
-
session.runtimes.find((r) => r.id === "ollama")?.detected ? "\u2713" : "\u25CB",
|
|
2926
|
-
" Ollama",
|
|
2927
|
-
session.runtimes.find((r) => r.id === "ollama")?.detected ? "" : " \u2192 brew install ollama"
|
|
2928
|
-
] }),
|
|
2929
|
-
/* @__PURE__ */ jsxs6(Text6, { color: session.runtimes.find((r) => r.id === "lmstudio")?.detected ? "green" : "yellow", children: [
|
|
2930
|
-
"[6] ",
|
|
2931
|
-
session.runtimes.find((r) => r.id === "lmstudio")?.detected ? "\u2713" : "\u25CB",
|
|
2932
|
-
" LM Studio",
|
|
2933
|
-
session.runtimes.find((r) => r.id === "lmstudio")?.detected ? "" : " \u2192 brew install --cask lm-studio"
|
|
2934
|
-
] }),
|
|
2935
|
-
/* @__PURE__ */ jsx6(Typewriter, { text: fit(`Enter=pull ${tag ?? ""} 1-3 agent 4 docker 5 ollama 6 LM Studio`, cols), ms: 14, dimColor: true }),
|
|
2936
|
-
log ? /* @__PURE__ */ jsx6(Text6, { color: "green", children: fit(log, cols) }) : null
|
|
2937
|
-
] });
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
// src/tui/views/Compare.tsx
|
|
2941
|
-
import { Box as Box6, Text as Text7 } from "ink";
|
|
2942
|
-
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2943
|
-
function CompareView({ session }) {
|
|
2944
|
-
const cols = useCols();
|
|
2945
|
-
const n = new Set(session.history.map((h) => h.model.id)).size;
|
|
2946
|
-
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", width: cols, children: [
|
|
2947
|
-
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "green", children: t("compareTitle") }),
|
|
2948
|
-
/* @__PURE__ */ jsx7(Typewriter, { text: fit(t("compareHow"), cols), ms: 12, dimColor: true }),
|
|
2949
|
-
n < 2 ? /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: fit(t("compareNeedBench"), cols) }) : /* @__PURE__ */ jsx7(Text7, { children: compareText(session.history) })
|
|
2950
|
-
] });
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
// src/tui/views/Benchmark.tsx
|
|
2954
|
-
import { Box as Box7, Text as Text8, useInput as useInput2 } from "ink";
|
|
2955
|
-
import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
|
|
2956
|
-
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2957
|
-
function BenchmarkView(props) {
|
|
2958
|
-
const [phase, setPhase] = useState3(props.models.length ? "select" : "error");
|
|
2959
|
-
const [sample, setSample] = useState3();
|
|
2960
|
-
const [elapsed, setElapsed] = useState3(0);
|
|
2961
|
-
const [result, setResult] = useState3();
|
|
2962
|
-
const [error, setError] = useState3(props.models.length ? "" : t("noGguf"));
|
|
2963
|
-
const cpuHist = useRef([]);
|
|
2964
|
-
const memHist = useRef([]);
|
|
2965
|
-
const abortRef = useRef(void 0);
|
|
2966
|
-
useInput2((input, key) => {
|
|
2967
|
-
if (phase === "select") {
|
|
2968
|
-
if (key.upArrow) props.setSelected(Math.max(0, props.selected - 1));
|
|
2969
|
-
if (key.downArrow) props.setSelected(Math.min(props.models.length - 1, props.selected + 1));
|
|
2970
|
-
if (key.return && props.models[props.selected]) setPhase("running");
|
|
2971
|
-
if (input.toLowerCase() === "q" || key.escape) props.onBack();
|
|
2972
|
-
} else if (phase === "running" && (input.toLowerCase() === "q" || key.escape)) {
|
|
2973
|
-
abortRef.current?.abort();
|
|
2974
|
-
} else if ((phase === "done" || phase === "error") && (input.toLowerCase() === "q" || key.escape || key.return)) {
|
|
2975
|
-
props.onBack();
|
|
2976
|
-
}
|
|
2977
|
-
});
|
|
2978
|
-
useEffect2(() => {
|
|
2979
|
-
if (phase !== "running") return;
|
|
2980
|
-
const model2 = props.models[props.selected];
|
|
2981
|
-
if (!model2) return;
|
|
2982
|
-
const controller = new AbortController();
|
|
2983
|
-
abortRef.current = controller;
|
|
2984
|
-
void (async () => {
|
|
2985
|
-
try {
|
|
2986
|
-
const catalog = props.session.rows.find((r) => r.local.id === model2.id)?.catalog;
|
|
2987
|
-
const out = await runBenchmark({
|
|
2988
|
-
model: model2,
|
|
2989
|
-
hardware: props.session.hardware,
|
|
2990
|
-
preset: props.preset,
|
|
2991
|
-
catalog,
|
|
2992
|
-
signal: controller.signal,
|
|
2993
|
-
onProgress: (p) => {
|
|
2994
|
-
setElapsed(p.elapsedSeconds);
|
|
2995
|
-
if (p.sample) {
|
|
2996
|
-
setSample(p.sample);
|
|
2997
|
-
if (p.sample.cpu?.utilizationPercent !== void 0) {
|
|
2998
|
-
cpuHist.current = [...cpuHist.current, p.sample.cpu.utilizationPercent].slice(-32);
|
|
2999
|
-
}
|
|
3000
|
-
memHist.current = [...memHist.current, p.sample.memory.usedBytes].slice(-32);
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
});
|
|
3004
|
-
setResult(out);
|
|
3005
|
-
setPhase("done");
|
|
3006
|
-
} catch (err) {
|
|
3007
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
3008
|
-
setPhase("error");
|
|
3009
|
-
}
|
|
3010
|
-
})();
|
|
3011
|
-
return () => controller.abort();
|
|
3012
|
-
}, [phase, props.models, props.preset, props.selected, props.session]);
|
|
3013
|
-
const h = props.session.hardware;
|
|
3014
|
-
const model = props.models[props.selected];
|
|
3015
|
-
if (phase === "select") {
|
|
3016
|
-
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
|
|
3017
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, children: t("selectBenchmark") }),
|
|
3018
|
-
props.models.map((m, i) => /* @__PURE__ */ jsxs8(Text8, { color: i === props.selected ? "cyan" : void 0, children: [
|
|
3019
|
-
i === props.selected ? "\u276F " : " ",
|
|
3020
|
-
m.name
|
|
3021
|
-
] }, m.id)),
|
|
3022
|
-
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: t("enterToRun") })
|
|
3023
|
-
] });
|
|
3024
|
-
}
|
|
3025
|
-
if (phase === "done" && result) {
|
|
3026
|
-
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
|
|
3027
|
-
/* @__PURE__ */ jsx8(Text8, { children: resultText(result.record, result.assessment) }),
|
|
3028
|
-
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
3029
|
-
t("saved"),
|
|
3030
|
-
" ",
|
|
3031
|
-
result.path
|
|
3032
|
-
] })
|
|
3033
|
-
] });
|
|
3034
|
-
}
|
|
3035
|
-
if (phase === "error") {
|
|
3036
|
-
return /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: error }) });
|
|
3037
|
-
}
|
|
3038
|
-
const cpu = sample?.cpu?.utilizationPercent;
|
|
3039
|
-
const gpu = sample?.gpu?.utilizationPercent;
|
|
3040
|
-
const memUsed = sample?.memory.usedBytes;
|
|
3041
|
-
const memTotal = h.memory.totalBytes;
|
|
3042
|
-
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
|
|
3043
|
-
/* @__PURE__ */ jsxs8(Box7, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [
|
|
3044
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, children: t("benchmarkTitle") }),
|
|
3045
|
-
/* @__PURE__ */ jsx8(Text8, { children: model?.name }),
|
|
3046
|
-
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
3047
|
-
h.cpu.name,
|
|
3048
|
-
" \xB7 ",
|
|
3049
|
-
formatBytes(h.memory.totalBytes, 0),
|
|
3050
|
-
" ",
|
|
3051
|
-
memoryLabel(h)
|
|
3052
|
-
] })
|
|
3053
|
-
] }),
|
|
3054
|
-
/* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
3055
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("performance") }),
|
|
3056
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3057
|
-
t("elapsed"),
|
|
3058
|
-
" ",
|
|
3059
|
-
elapsed.toFixed(1),
|
|
3060
|
-
" s"
|
|
3061
|
-
] }),
|
|
3062
|
-
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: t("measuredAfter") })
|
|
3063
|
-
] }),
|
|
3064
|
-
/* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
3065
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("system") }),
|
|
3066
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3067
|
-
"CPU ",
|
|
3068
|
-
bar(cpu),
|
|
3069
|
-
" ",
|
|
3070
|
-
formatPercent(cpu)
|
|
3071
|
-
] }),
|
|
3072
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3073
|
-
"GPU ",
|
|
3074
|
-
bar(gpu),
|
|
3075
|
-
" ",
|
|
3076
|
-
formatPercent(gpu)
|
|
3077
|
-
] }),
|
|
3078
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3079
|
-
"Memory ",
|
|
3080
|
-
bar(memUsed && memTotal ? memUsed / memTotal * 100 : void 0),
|
|
3081
|
-
" ",
|
|
3082
|
-
memUsed ? formatBytes(memUsed) : "N/A",
|
|
3083
|
-
" / ",
|
|
3084
|
-
formatBytes(memTotal, 0)
|
|
3085
|
-
] }),
|
|
3086
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3087
|
-
"Swap ",
|
|
3088
|
-
bar(0),
|
|
3089
|
-
" ",
|
|
3090
|
-
sample ? formatBytes(sample.memory.swapUsedBytes) : "N/A"
|
|
3091
|
-
] })
|
|
3092
|
-
] }),
|
|
3093
|
-
/* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
3094
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("powerThermals") }),
|
|
3095
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3096
|
-
"Package ",
|
|
3097
|
-
sample?.packagePowerWatts ?? "N/A",
|
|
3098
|
-
" W"
|
|
3099
|
-
] }),
|
|
3100
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3101
|
-
"Temperature ",
|
|
3102
|
-
sample?.thermal?.temperatureC ?? "N/A",
|
|
3103
|
-
" \xB0C"
|
|
3104
|
-
] }),
|
|
3105
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3106
|
-
"Thermal Pressure ",
|
|
3107
|
-
sample?.thermal?.pressure ?? "N/A"
|
|
3108
|
-
] }),
|
|
3109
|
-
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: t("permissionsHint") })
|
|
3110
|
-
] }),
|
|
3111
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3112
|
-
sparkline(cpuHist.current),
|
|
3113
|
-
" CPU"
|
|
3114
|
-
] }),
|
|
3115
|
-
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3116
|
-
sparkline(memHist.current.map((v) => v / 1024 ** 3)),
|
|
3117
|
-
" Memory"
|
|
3118
|
-
] }),
|
|
3119
|
-
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: t("cancelHint") })
|
|
3120
|
-
] });
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
// src/tui/views/History.tsx
|
|
3124
|
-
import { Box as Box8, Text as Text9 } from "ink";
|
|
3125
|
-
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3126
|
-
function HistoryView({ session }) {
|
|
3127
|
-
return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
|
|
3128
|
-
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: t("measured") }),
|
|
3129
|
-
/* @__PURE__ */ jsx9(Text9, { children: historyText(session.history) })
|
|
3130
|
-
] });
|
|
3131
|
-
}
|
|
3132
|
-
|
|
3133
|
-
// src/tui/views/Tasks.tsx
|
|
3134
|
-
import { Box as Box9, Text as Text10, useInput as useInput3 } from "ink";
|
|
3135
|
-
import { useMemo, useState as useState4 } from "react";
|
|
3136
|
-
|
|
3137
|
-
// src/tasks/catalog.ts
|
|
3138
|
-
var TASK_CATALOG = [
|
|
3139
|
-
{
|
|
3140
|
-
id: "code-review",
|
|
3141
|
-
kind: "code",
|
|
3142
|
-
useCases: ["code"],
|
|
3143
|
-
titleKey: "taskCodeReviewTitle",
|
|
3144
|
-
promptKey: "taskCodeReviewPrompt",
|
|
3145
|
-
harnessId: "H-CODE-REVIEW"
|
|
3146
|
-
},
|
|
3147
|
-
{
|
|
3148
|
-
id: "code-tests",
|
|
3149
|
-
kind: "code",
|
|
3150
|
-
useCases: ["code"],
|
|
3151
|
-
titleKey: "taskCodeTestsTitle",
|
|
3152
|
-
promptKey: "taskCodeTestsPrompt",
|
|
3153
|
-
harnessId: "H-CODE-TESTS"
|
|
3154
|
-
},
|
|
3155
|
-
{
|
|
3156
|
-
id: "chat-plan",
|
|
3157
|
-
kind: "chat",
|
|
3158
|
-
useCases: ["chat"],
|
|
3159
|
-
titleKey: "taskChatPlanTitle",
|
|
3160
|
-
promptKey: "taskChatPlanPrompt",
|
|
3161
|
-
harnessId: "H-CHAT-PLAN"
|
|
3162
|
-
},
|
|
3163
|
-
{
|
|
3164
|
-
id: "chat-agent",
|
|
3165
|
-
kind: "chat",
|
|
3166
|
-
useCases: ["chat", "code"],
|
|
3167
|
-
titleKey: "taskChatAgentTitle",
|
|
3168
|
-
promptKey: "taskChatAgentPrompt",
|
|
3169
|
-
harnessId: "H-CHAT-AGENT"
|
|
3170
|
-
},
|
|
3171
|
-
{
|
|
3172
|
-
id: "image-caption",
|
|
3173
|
-
kind: "image",
|
|
3174
|
-
useCases: ["image", "vision"],
|
|
3175
|
-
titleKey: "taskImageCaptionTitle",
|
|
3176
|
-
promptKey: "taskImageCaptionPrompt",
|
|
3177
|
-
harnessId: "H-IMAGE-CAPTION"
|
|
3178
|
-
},
|
|
3179
|
-
{
|
|
3180
|
-
id: "image-edit-brief",
|
|
3181
|
-
kind: "image",
|
|
3182
|
-
useCases: ["image", "vision"],
|
|
3183
|
-
titleKey: "taskImageBriefTitle",
|
|
3184
|
-
promptKey: "taskImageBriefPrompt",
|
|
3185
|
-
harnessId: "H-IMAGE-BRIEF"
|
|
3186
|
-
},
|
|
3187
|
-
{
|
|
3188
|
-
id: "video-storyboard",
|
|
3189
|
-
kind: "video",
|
|
3190
|
-
useCases: ["video"],
|
|
3191
|
-
titleKey: "taskVideoBoardTitle",
|
|
3192
|
-
promptKey: "taskVideoBoardPrompt",
|
|
3193
|
-
harnessId: "H-VIDEO-BOARD"
|
|
3194
|
-
},
|
|
3195
|
-
{
|
|
3196
|
-
id: "video-shotlist",
|
|
3197
|
-
kind: "video",
|
|
3198
|
-
useCases: ["video"],
|
|
3199
|
-
titleKey: "taskVideoShotTitle",
|
|
3200
|
-
promptKey: "taskVideoShotPrompt",
|
|
3201
|
-
harnessId: "H-VIDEO-SHOTS"
|
|
3202
|
-
},
|
|
3203
|
-
{
|
|
3204
|
-
id: "transcribe-clean",
|
|
3205
|
-
kind: "transcription",
|
|
3206
|
-
useCases: ["chat", "multilingual"],
|
|
3207
|
-
titleKey: "taskTranscribeCleanTitle",
|
|
3208
|
-
promptKey: "taskTranscribeCleanPrompt",
|
|
3209
|
-
harnessId: "H-ASR-CLEAN"
|
|
3210
|
-
},
|
|
3211
|
-
{
|
|
3212
|
-
id: "transcribe-actions",
|
|
3213
|
-
kind: "transcription",
|
|
3214
|
-
useCases: ["chat", "code"],
|
|
3215
|
-
titleKey: "taskTranscribeActionsTitle",
|
|
3216
|
-
promptKey: "taskTranscribeActionsPrompt",
|
|
3217
|
-
harnessId: "H-ASR-ACTIONS"
|
|
3218
|
-
}
|
|
3219
|
-
];
|
|
3220
|
-
|
|
3221
|
-
// src/tasks/types.ts
|
|
3222
|
-
var WORK_KINDS = ["code", "video", "image", "transcription", "chat"];
|
|
3223
|
-
|
|
3224
|
-
// src/tasks/plan.ts
|
|
3225
|
-
function parseKinds(raw) {
|
|
3226
|
-
if (!raw) return [...WORK_KINDS];
|
|
3227
|
-
const parts = raw.split(",").map((p) => p.trim().toLowerCase());
|
|
3228
|
-
const kinds = WORK_KINDS.filter((k) => parts.includes(k));
|
|
3229
|
-
return kinds.length ? kinds : [...WORK_KINDS];
|
|
3230
|
-
}
|
|
3231
|
-
function parseAnswers(input) {
|
|
3232
|
-
return {
|
|
3233
|
-
kinds: parseKinds(input.for),
|
|
3234
|
-
scope: input.scope === "all" ? "all" : "installed",
|
|
3235
|
-
priority: input.priority === "quality" || input.priority === "speed" ? input.priority : "balanced"
|
|
3236
|
-
};
|
|
3237
|
-
}
|
|
3238
|
-
function kindMatches(kind, useCases) {
|
|
3239
|
-
if (kind === "code") return useCases.some((u) => u.includes("code"));
|
|
3240
|
-
if (kind === "image") return useCases.some((u) => u.includes("image") || u.includes("vision"));
|
|
3241
|
-
if (kind === "video") return useCases.some((u) => u.includes("video"));
|
|
3242
|
-
if (kind === "transcription") return useCases.some((u) => u.includes("chat") || u.includes("multilingual"));
|
|
3243
|
-
return useCases.some((u) => u.includes("chat") || u.includes("reasoning") || u.includes("code"));
|
|
3244
|
-
}
|
|
3245
|
-
function planTasks(session, answers) {
|
|
3246
|
-
const wanted = new Set(answers.kinds);
|
|
3247
|
-
const defs = TASK_CATALOG.filter((task) => wanted.has(task.kind));
|
|
3248
|
-
const plans = [];
|
|
3249
|
-
const installed2 = session.rows.map((row) => {
|
|
3250
|
-
const useCases = row.catalog?.useCase ?? ["chat", "code"];
|
|
3251
|
-
const kinds = answers.kinds.filter((kind) => kindMatches(kind, useCases));
|
|
3252
|
-
return {
|
|
3253
|
-
modelId: row.local.id,
|
|
3254
|
-
modelName: row.local.name,
|
|
3255
|
-
installed: true,
|
|
3256
|
-
origin: row.lastBenchmark ? "measured" : "estimated",
|
|
3257
|
-
grade: row.compatibility?.grade,
|
|
3258
|
-
useCases,
|
|
3259
|
-
kinds
|
|
3260
|
-
};
|
|
3261
|
-
});
|
|
3262
|
-
const catalogExtras = answers.scope === "all" ? session.catalog.filter((model) => !session.rows.some((row) => idsLikelyMatch(row.local.id, model.id))).map((model) => {
|
|
3263
|
-
const useCases = model.useCase ?? [];
|
|
3264
|
-
const kinds = answers.kinds.filter((kind) => kindMatches(kind, useCases));
|
|
3265
|
-
const fit2 = localCompatibility(session.hardware, model);
|
|
3266
|
-
return {
|
|
3267
|
-
modelId: model.id,
|
|
3268
|
-
modelName: model.name,
|
|
3269
|
-
installed: false,
|
|
3270
|
-
origin: "estimated",
|
|
3271
|
-
grade: fit2.grade,
|
|
3272
|
-
useCases,
|
|
3273
|
-
kinds
|
|
3274
|
-
};
|
|
3275
|
-
}).filter((row) => row.kinds.length && row.grade !== "F") : [];
|
|
3276
|
-
const ranked = [...installed2, ...catalogExtras].filter((row) => row.kinds.length);
|
|
3277
|
-
ranked.sort((a, b) => {
|
|
3278
|
-
if (a.installed !== b.installed) return a.installed ? -1 : 1;
|
|
3279
|
-
if (answers.priority === "speed") return (a.grade ?? "C").localeCompare(b.grade ?? "C");
|
|
3280
|
-
if (answers.priority === "quality") return (b.grade ?? "C").localeCompare(a.grade ?? "C");
|
|
3281
|
-
return 0;
|
|
3282
|
-
});
|
|
3283
|
-
for (const row of ranked.slice(0, 8)) {
|
|
3284
|
-
const tasks = defs.filter((def) => row.kinds.includes(def.kind) && def.useCases.some((u) => row.useCases.includes(u) || kindMatches(def.kind, row.useCases))).slice(0, 3).map((def) => ({
|
|
3285
|
-
id: def.id,
|
|
3286
|
-
harnessId: def.harnessId,
|
|
3287
|
-
kind: def.kind,
|
|
3288
|
-
title: t(def.titleKey),
|
|
3289
|
-
prompt: t(def.promptKey)
|
|
3290
|
-
}));
|
|
3291
|
-
if (!tasks.length) continue;
|
|
3292
|
-
plans.push({
|
|
3293
|
-
modelId: row.modelId,
|
|
3294
|
-
modelName: row.modelName,
|
|
3295
|
-
installed: row.installed,
|
|
3296
|
-
origin: row.origin,
|
|
3297
|
-
grade: row.grade,
|
|
3298
|
-
kinds: row.kinds,
|
|
3299
|
-
tasks
|
|
3300
|
-
});
|
|
3301
|
-
}
|
|
3302
|
-
return plans;
|
|
3303
|
-
}
|
|
3304
|
-
function tasksText(plans, answers) {
|
|
3305
|
-
const header = [
|
|
3306
|
-
t("tasksTitle"),
|
|
3307
|
-
t("tasksHint"),
|
|
3308
|
-
`${t("tasksKinds")}: ${answers.kinds.join(", ")}`,
|
|
3309
|
-
`${t("tasksScope")}: ${answers.scope}`,
|
|
3310
|
-
`${t("tasksPriority")}: ${answers.priority}`,
|
|
3311
|
-
t("credits"),
|
|
3312
|
-
""
|
|
3313
|
-
];
|
|
3314
|
-
if (!plans.length) return [...header, t("tasksEmpty")].join("\n");
|
|
3315
|
-
const blocks = plans.map((plan) => {
|
|
3316
|
-
const inst = plan.installed ? t("tasksInstalled") : t("tasksNotInstalled");
|
|
3317
|
-
const origin = plan.origin === "measured" ? t("measured") : t("estimated");
|
|
3318
|
-
const lines = [
|
|
3319
|
-
`${plan.modelName} [${inst}] ${plan.grade ?? "\u2014"} ${origin}`,
|
|
3320
|
-
` ${plan.kinds.join(", ")}`,
|
|
3321
|
-
...plan.tasks.map((task) => ` ${task.harnessId} ${task.title}
|
|
3322
|
-
${task.prompt}`)
|
|
3323
|
-
];
|
|
3324
|
-
return lines.join("\n");
|
|
3325
|
-
});
|
|
3326
|
-
return [...header, ...blocks].join("\n\n");
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
// src/tui/views/Tasks.tsx
|
|
3330
|
-
import { Fragment, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3331
|
-
var KIND_LABEL = {
|
|
3332
|
-
code: "tasksOptCode",
|
|
3333
|
-
video: "tasksOptVideo",
|
|
3334
|
-
image: "tasksOptImage",
|
|
3335
|
-
transcription: "tasksOptTranscription",
|
|
3336
|
-
chat: "tasksOptChat"
|
|
3337
|
-
};
|
|
3338
|
-
function TasksView({ session }) {
|
|
3339
|
-
const [step, setStep] = useState4(0);
|
|
3340
|
-
const [cursor, setCursor] = useState4(0);
|
|
3341
|
-
const [kinds, setKinds] = useState4(["code", "chat"]);
|
|
3342
|
-
const [scope, setScope] = useState4("installed");
|
|
3343
|
-
const [priority, setPriority] = useState4("balanced");
|
|
3344
|
-
const plans = useMemo(
|
|
3345
|
-
() => planTasks(session, { kinds, scope, priority }),
|
|
3346
|
-
[session, kinds, scope, priority]
|
|
3347
|
-
);
|
|
3348
|
-
useInput3((input, key) => {
|
|
3349
|
-
if (step === 0) {
|
|
3350
|
-
if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
|
|
3351
|
-
if (key.downArrow) setCursor((c) => Math.min(WORK_KINDS.length - 1, c + 1));
|
|
3352
|
-
if (input === " " || input === "x") {
|
|
3353
|
-
const kind = WORK_KINDS[cursor];
|
|
3354
|
-
if (!kind) return;
|
|
3355
|
-
setKinds(
|
|
3356
|
-
(current2) => current2.includes(kind) ? current2.filter((k) => k !== kind) : [...current2, kind]
|
|
3357
|
-
);
|
|
3358
|
-
}
|
|
3359
|
-
if (key.return && kinds.length) setStep(1);
|
|
3360
|
-
} else if (step === 1) {
|
|
3361
|
-
if (key.upArrow || key.downArrow) setScope((s) => s === "installed" ? "all" : "installed");
|
|
3362
|
-
if (key.return) setStep(2);
|
|
3363
|
-
} else if (step === 2) {
|
|
3364
|
-
const order = ["speed", "balanced", "quality"];
|
|
3365
|
-
if (key.upArrow) setPriority(order[Math.max(0, order.indexOf(priority) - 1)] ?? "speed");
|
|
3366
|
-
if (key.downArrow) setPriority(order[Math.min(2, order.indexOf(priority) + 1)] ?? "quality");
|
|
3367
|
-
if (key.return) setStep(3);
|
|
3368
|
-
}
|
|
3369
|
-
});
|
|
3370
|
-
if (step === 3) {
|
|
3371
|
-
return /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx10(Text10, { children: tasksText(plans, { kinds, scope, priority }) }) });
|
|
3372
|
-
}
|
|
3373
|
-
return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
|
|
3374
|
-
/* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", children: t("tasksTitle") }),
|
|
3375
|
-
step === 0 && /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
3376
|
-
/* @__PURE__ */ jsx10(Text10, { children: t("tasksQ1") }),
|
|
3377
|
-
WORK_KINDS.map((kind, i) => /* @__PURE__ */ jsxs10(Text10, { color: i === cursor ? "cyan" : void 0, children: [
|
|
3378
|
-
i === cursor ? "\u276F " : " ",
|
|
3379
|
-
"[",
|
|
3380
|
-
kinds.includes(kind) ? "x" : " ",
|
|
3381
|
-
"] ",
|
|
3382
|
-
t(KIND_LABEL[kind])
|
|
3383
|
-
] }, kind))
|
|
3384
|
-
] }),
|
|
3385
|
-
step === 1 && /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
3386
|
-
/* @__PURE__ */ jsx10(Text10, { children: t("tasksQ2") }),
|
|
3387
|
-
/* @__PURE__ */ jsxs10(Text10, { color: scope === "installed" ? "cyan" : void 0, children: [
|
|
3388
|
-
scope === "installed" ? "\u276F " : " ",
|
|
3389
|
-
t("tasksOptInstalled")
|
|
3390
|
-
] }),
|
|
3391
|
-
/* @__PURE__ */ jsxs10(Text10, { color: scope === "all" ? "cyan" : void 0, children: [
|
|
3392
|
-
scope === "all" ? "\u276F " : " ",
|
|
3393
|
-
t("tasksOptAll")
|
|
3394
|
-
] })
|
|
3395
|
-
] }),
|
|
3396
|
-
step === 2 && /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
3397
|
-
/* @__PURE__ */ jsx10(Text10, { children: t("tasksQ3") }),
|
|
3398
|
-
["speed", "balanced", "quality"].map((item) => /* @__PURE__ */ jsxs10(Text10, { color: priority === item ? "cyan" : void 0, children: [
|
|
3399
|
-
priority === item ? "\u276F " : " ",
|
|
3400
|
-
item === "speed" ? t("tasksOptSpeed") : item === "quality" ? t("tasksOptQuality") : t("tasksOptBalanced")
|
|
3401
|
-
] }, item))
|
|
3402
|
-
] })
|
|
3403
|
-
] });
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
// src/tui/keys.ts
|
|
3407
|
-
function globalNav(letter) {
|
|
3408
|
-
if (letter === "b") return { type: "screen", screen: "benchmark" };
|
|
3409
|
-
if (letter === "m") return { type: "screen", screen: "models" };
|
|
3410
|
-
if (letter === "r") return { type: "screen", screen: "recommend" };
|
|
3411
|
-
if (letter === "h") return { type: "screen", screen: "hardware" };
|
|
3412
|
-
if (letter === "c") return { type: "screen", screen: "compare" };
|
|
3413
|
-
if (letter === "l") return { type: "screen", screen: "history" };
|
|
3414
|
-
if (letter === "t") return { type: "screen", screen: "tasks" };
|
|
3415
|
-
if (letter === "s") return { type: "screen", screen: "setup" };
|
|
3416
|
-
return void 0;
|
|
3417
|
-
}
|
|
3418
|
-
function handleAppKey(screen, input, key) {
|
|
3419
|
-
const letter = input.toLowerCase();
|
|
3420
|
-
if (screen === "benchmark") return { type: "ignore" };
|
|
3421
|
-
if (letter === "q") return { type: "quit" };
|
|
3422
|
-
if (key.escape) return screen === "home" ? { type: "quit" } : { type: "home" };
|
|
3423
|
-
if (screen === "setup") {
|
|
3424
|
-
if ("i123456".includes(letter)) return { type: "ignore" };
|
|
3425
|
-
}
|
|
3426
|
-
const nav = globalNav(letter);
|
|
3427
|
-
if (nav) return nav;
|
|
3428
|
-
if (letter === "i" && screen === "home") return { type: "screen", screen: "setup" };
|
|
3429
|
-
if (key.return && screen === "home") return { type: "screen", screen: "benchmark" };
|
|
3430
|
-
return { type: "ignore" };
|
|
3431
|
-
}
|
|
3432
|
-
|
|
3433
|
-
// src/tui/Welcome.tsx
|
|
3434
|
-
import { Box as Box10, Text as Text11 } from "ink";
|
|
3435
|
-
import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3436
|
-
function Chrome() {
|
|
3437
|
-
const cols = useCols();
|
|
3438
|
-
const wide = cols >= 72;
|
|
3439
|
-
const brand = `${t("appTitle")} ${t("byline")}`;
|
|
3440
|
-
return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", width: cols, marginBottom: 1, children: [
|
|
3441
|
-
/* @__PURE__ */ jsx11(Typewriter, { text: brand, ms: 28, color: "cyan", bold: true, once: true }),
|
|
3442
|
-
/* @__PURE__ */ jsx11(Text11, { children: wide ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
|
|
3443
|
-
/* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "[S] Setup" }),
|
|
3444
|
-
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3445
|
-
/* @__PURE__ */ jsx11(Text11, { color: "green", children: "[B] Bench" }),
|
|
3446
|
-
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3447
|
-
/* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "[M] Models" }),
|
|
3448
|
-
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3449
|
-
/* @__PURE__ */ jsx11(Text11, { color: "magenta", children: "[R] Rec" }),
|
|
3450
|
-
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3451
|
-
/* @__PURE__ */ jsx11(Text11, { color: "blue", children: "[T] Tasks" }),
|
|
3452
|
-
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3453
|
-
/* @__PURE__ */ jsx11(Text11, { children: "[H] HW" }),
|
|
3454
|
-
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3455
|
-
/* @__PURE__ */ jsx11(Text11, { color: "green", children: "[C] Cmp" }),
|
|
3456
|
-
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3457
|
-
/* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "[L] Log" }),
|
|
3458
|
-
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3459
|
-
/* @__PURE__ */ jsx11(Text11, { color: "red", children: "[Q] Quit" })
|
|
3460
|
-
] }) : /* @__PURE__ */ jsxs11(Fragment2, { children: [
|
|
3461
|
-
/* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "[S]" }),
|
|
3462
|
-
/* @__PURE__ */ jsx11(Text11, { color: "green", children: "[B]" }),
|
|
3463
|
-
/* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "[M]" }),
|
|
3464
|
-
/* @__PURE__ */ jsx11(Text11, { color: "magenta", children: "[R]" }),
|
|
3465
|
-
/* @__PURE__ */ jsx11(Text11, { color: "blue", children: "[T]" }),
|
|
3466
|
-
/* @__PURE__ */ jsx11(Text11, { children: "[H]" }),
|
|
3467
|
-
/* @__PURE__ */ jsx11(Text11, { color: "green", children: "[C]" }),
|
|
3468
|
-
/* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "[L]" }),
|
|
3469
|
-
/* @__PURE__ */ jsx11(Text11, { color: "red", children: "[Q]" })
|
|
3470
|
-
] }) })
|
|
3471
|
-
] });
|
|
3472
|
-
}
|
|
3473
|
-
|
|
3474
|
-
// src/tui/App.tsx
|
|
3475
|
-
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3476
|
-
function App(props) {
|
|
3477
|
-
const { exit } = useApp2();
|
|
3478
|
-
const [screen, setScreen] = useState5(props.start ?? "home");
|
|
3479
|
-
const screenRef = useRef2(screen);
|
|
3480
|
-
screenRef.current = screen;
|
|
3481
|
-
const [selected, setSelected] = useState5(0);
|
|
3482
|
-
const benchable = useMemo2(
|
|
3483
|
-
() => props.session.models.filter((m) => Boolean(m.artifactPath)),
|
|
3484
|
-
[props.session.models]
|
|
3485
|
-
);
|
|
3486
|
-
useInput4((input, key) => {
|
|
3487
|
-
const result = handleAppKey(screenRef.current, input, key);
|
|
3488
|
-
if (result.type === "quit") exit();
|
|
3489
|
-
if (result.type === "home") setScreen("home");
|
|
3490
|
-
if (result.type === "screen") setScreen(result.screen);
|
|
3491
|
-
});
|
|
3492
|
-
return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", paddingX: 1, children: [
|
|
3493
|
-
screen !== "benchmark" && /* @__PURE__ */ jsx12(Chrome, {}),
|
|
3494
|
-
screen === "home" && /* @__PURE__ */ jsx12(Dashboard, { session: props.session }),
|
|
3495
|
-
screen === "models" && /* @__PURE__ */ jsx12(ModelsView, { session: props.session }),
|
|
3496
|
-
screen === "hardware" && /* @__PURE__ */ jsx12(HardwareView, { session: props.session }),
|
|
3497
|
-
screen === "recommend" && /* @__PURE__ */ jsx12(RecommendView, { session: props.session }),
|
|
3498
|
-
screen === "setup" && /* @__PURE__ */ jsx12(SetupView, { session: props.session }),
|
|
3499
|
-
screen === "compare" && /* @__PURE__ */ jsx12(CompareView, { session: props.session }),
|
|
3500
|
-
screen === "history" && /* @__PURE__ */ jsx12(HistoryView, { session: props.session }),
|
|
3501
|
-
screen === "tasks" && /* @__PURE__ */ jsx12(TasksView, { session: props.session }),
|
|
3502
|
-
screen === "benchmark" && /* @__PURE__ */ jsx12(
|
|
3503
|
-
BenchmarkView,
|
|
3504
|
-
{
|
|
3505
|
-
session: props.session,
|
|
3506
|
-
models: benchable,
|
|
3507
|
-
selected,
|
|
3508
|
-
setSelected,
|
|
3509
|
-
preset: props.preset,
|
|
3510
|
-
onBack: () => setScreen("home")
|
|
3511
|
-
}
|
|
3512
|
-
)
|
|
3513
|
-
] });
|
|
3514
|
-
}
|
|
3515
|
-
|
|
3516
|
-
// src/tui/render.tsx
|
|
3517
|
-
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
3518
|
-
async function renderDashboard(session, preset, start) {
|
|
3519
|
-
const instance = render(/* @__PURE__ */ jsx13(App, { session, preset, start }));
|
|
3520
|
-
await instance.waitUntilExit();
|
|
3521
|
-
}
|
|
693
|
+
// src/shared/logger.ts
|
|
694
|
+
var level = "info";
|
|
695
|
+
function setLogLevel(next) {
|
|
696
|
+
level = next;
|
|
697
|
+
}
|
|
3522
698
|
|
|
3523
699
|
// src/cli/help.ts
|
|
3524
700
|
function helpText() {
|
|
@@ -3540,6 +716,19 @@ function explained(aboutKey, body) {
|
|
|
3540
716
|
|
|
3541
717
|
${body}`;
|
|
3542
718
|
}
|
|
719
|
+
async function openLab(session, preset, start) {
|
|
720
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
721
|
+
process.stdout.write(
|
|
722
|
+
`${explained("aboutDashboard", `${hardwareText(session)}
|
|
723
|
+
|
|
724
|
+
${modelsText(session)}`)}
|
|
725
|
+
`
|
|
726
|
+
);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const { renderDashboard } = await import("./render-3UBPZNF4.js");
|
|
730
|
+
await renderDashboard(session, preset, start);
|
|
731
|
+
}
|
|
3543
732
|
async function run(args2) {
|
|
3544
733
|
setLocale(resolveLocale(args2.lang));
|
|
3545
734
|
if (args2.help) {
|
|
@@ -3549,6 +738,10 @@ async function run(args2) {
|
|
|
3549
738
|
}
|
|
3550
739
|
if (args2.verbose) setLogLevel("verbose");
|
|
3551
740
|
if (!args2.color) process.env.NO_COLOR = "1";
|
|
741
|
+
if (args2.command === "repo") {
|
|
742
|
+
const { runRepoCommand } = await import("./repo-cli-4GSG5MD3.js");
|
|
743
|
+
return runRepoCommand(args2);
|
|
744
|
+
}
|
|
3552
745
|
const cleanup = () => {
|
|
3553
746
|
void killAll("SIGTERM");
|
|
3554
747
|
};
|
|
@@ -3565,7 +758,10 @@ async function run(args2) {
|
|
|
3565
758
|
process.stderr.write(`${t("loading")}
|
|
3566
759
|
`);
|
|
3567
760
|
}
|
|
3568
|
-
const session = await loadSession({
|
|
761
|
+
const session = await loadSession({
|
|
762
|
+
network: args2.network,
|
|
763
|
+
light: args2.command === "launch" || args2.command === "doctor" || args2.command === "hardware"
|
|
764
|
+
});
|
|
3569
765
|
switch (args2.command) {
|
|
3570
766
|
case "hardware":
|
|
3571
767
|
print(
|
|
@@ -3583,7 +779,7 @@ ${runtimesText(session)}`),
|
|
|
3583
779
|
print({ recommendations: session.recommendations, agents: session.agents }, true);
|
|
3584
780
|
return 0;
|
|
3585
781
|
}
|
|
3586
|
-
await
|
|
782
|
+
await openLab(session, args2.preset, "setup");
|
|
3587
783
|
return 0;
|
|
3588
784
|
case "recommend": {
|
|
3589
785
|
if (args2.install && args2.yes) {
|
|
@@ -3680,7 +876,7 @@ ${runtimesText(session)}`),
|
|
|
3680
876
|
print({ error: t("jsonNeedModel") }, true);
|
|
3681
877
|
return 1;
|
|
3682
878
|
}
|
|
3683
|
-
await
|
|
879
|
+
await openLab(session, args2.preset);
|
|
3684
880
|
return 0;
|
|
3685
881
|
}
|
|
3686
882
|
const model = session.models.find((m) => m.id === id || m.name === id);
|
|
@@ -3690,6 +886,10 @@ ${runtimesText(session)}`),
|
|
|
3690
886
|
return 1;
|
|
3691
887
|
}
|
|
3692
888
|
const catalog = session.rows.find((r) => r.local.id === model.id)?.catalog;
|
|
889
|
+
if (!model.artifactPath && model.source === "ollama") {
|
|
890
|
+
const artifact = await ollamaAdapter.resolveModel(model.id);
|
|
891
|
+
model.artifactPath = artifact?.path;
|
|
892
|
+
}
|
|
3693
893
|
const result = await runBenchmark({
|
|
3694
894
|
model,
|
|
3695
895
|
hardware: session.hardware,
|
|
@@ -3710,7 +910,7 @@ ${runtimesText(session)}`),
|
|
|
3710
910
|
print(session, true);
|
|
3711
911
|
return 0;
|
|
3712
912
|
}
|
|
3713
|
-
await
|
|
913
|
+
await openLab(session, args2.preset);
|
|
3714
914
|
return 0;
|
|
3715
915
|
}
|
|
3716
916
|
}
|