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