pudu-ai 0.2.17 → 0.2.19

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +133 -132
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -29,7 +29,7 @@ function spawnTracked(file, args2 = [], options = {}) {
29
29
  installSignalHandlers();
30
30
  const subprocess = execa(file, args2, {
31
31
  reject: false,
32
- timeout: options.timeout ?? 3e4,
32
+ timeout: options.timeout === void 0 ? 3e4 : options.timeout === 0 ? void 0 : options.timeout,
33
33
  ...options
34
34
  });
35
35
  children.add(subprocess);
@@ -71,7 +71,7 @@ var init_process = __esm({
71
71
  // src/shared/which.ts
72
72
  import { access } from "node:fs/promises";
73
73
  import { constants } from "node:fs";
74
- import path2 from "node:path";
74
+ import path from "node:path";
75
75
  async function commandExists(name) {
76
76
  if (name.includes("/") || name.includes("\\")) {
77
77
  try {
@@ -82,9 +82,9 @@ async function commandExists(name) {
82
82
  }
83
83
  }
84
84
  const pathEnv = process.env.PATH ?? "";
85
- const parts = pathEnv.split(path2.delimiter);
85
+ const parts = pathEnv.split(path.delimiter);
86
86
  for (const dir of parts) {
87
- const candidate = path2.join(dir, name);
87
+ const candidate = path.join(dir, name);
88
88
  try {
89
89
  await access(candidate, constants.X_OK);
90
90
  return candidate;
@@ -238,31 +238,138 @@ function parseArgs(argv) {
238
238
  };
239
239
  }
240
240
 
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
+ // src/adapters/ollama/parse.ts
281
+ function parseOllamaList(output) {
282
+ const lines = output.split(/\r?\n/).map((l) => l.trimEnd()).filter(Boolean);
283
+ if (lines.length === 0) return [];
284
+ const header = lines[0] ?? "";
285
+ if (!/^NAME\s+/i.test(header)) {
286
+ return lines.flatMap((line) => parseDataLine(line));
287
+ }
288
+ return lines.slice(1).flatMap((line) => parseDataLine(line));
289
+ }
290
+ function parseDataLine(line) {
291
+ const parts = line.trim().split(/\s{2,}/);
292
+ if (parts.length < 2) return [];
293
+ const name = parts[0];
294
+ if (!name || /^NAME$/i.test(name)) return [];
295
+ const digest = parts[1];
296
+ const sizeRaw = parts[2];
297
+ const modified = parts.slice(3).join(" ") || void 0;
298
+ return [
299
+ {
300
+ id: name,
301
+ name,
302
+ source: "ollama",
303
+ digest,
304
+ sizeBytes: sizeRaw ? parseSizeToBytes(sizeRaw.replace(/([A-Z]+)$/i, " $1")) : void 0,
305
+ modifiedAt: modified
306
+ }
307
+ ];
308
+ }
309
+ function parseOllamaModelfile(modelfile) {
310
+ const match = modelfile.match(/^FROM\s+(.+)$/m);
311
+ return { from: match?.[1]?.trim() };
312
+ }
313
+
314
+ // src/adapters/ollama/index.ts
315
+ var ollamaAdapter = {
316
+ id: "ollama",
317
+ label: "Ollama",
318
+ async detect() {
319
+ return Boolean(await commandExists("ollama"));
320
+ },
321
+ async version() {
322
+ const result = await runCommand("ollama", ["--version"], { timeout: 8e3 });
323
+ const text = `${result.stdout} ${result.stderr}`;
324
+ const match = text.match(/(\d+\.\d+\.\d+)/);
325
+ return match?.[1];
326
+ },
327
+ async listModels() {
328
+ if (!await this.detect()) return [];
329
+ const result = await runCommand("ollama", ["list"], { timeout: 15e3 });
330
+ return parseOllamaList(result.stdout);
331
+ },
332
+ async resolveModel(id) {
333
+ return resolveOllamaBlob(id);
334
+ },
335
+ async benchmarkCapabilities() {
336
+ return ["llama-bench"];
337
+ }
338
+ };
339
+ async function resolveOllamaBlob(id) {
340
+ const result = await runCommand("ollama", ["show", "--modelfile", id], { timeout: 15e3 });
341
+ if (result.exitCode !== 0) return void 0;
342
+ const { from } = parseOllamaModelfile(result.stdout);
343
+ if (!from) return void 0;
344
+ const format = from.endsWith(".gguf") || from.includes("sha256-") ? "gguf" : "unknown";
345
+ return { id, path: from, format };
346
+ }
347
+
241
348
  // src/storage/config.ts
242
349
  import { readFile, writeFile } from "node:fs/promises";
243
350
  import { z } from "zod";
244
351
 
245
352
  // src/storage/paths.ts
246
353
  import os from "node:os";
247
- import path from "node:path";
354
+ import path2 from "node:path";
248
355
  import { mkdir } from "node:fs/promises";
249
356
  function homeDir() {
250
- return path.join(os.homedir(), ".pudu-ai");
357
+ return path2.join(os.homedir(), ".pudu-ai");
251
358
  }
252
359
  function configPath() {
253
- return path.join(homeDir(), "config.json");
360
+ return path2.join(homeDir(), "config.json");
254
361
  }
255
362
  function benchmarksDir() {
256
- return path.join(homeDir(), "benchmarks");
363
+ return path2.join(homeDir(), "benchmarks");
257
364
  }
258
365
  function telemetryDir() {
259
- return path.join(homeDir(), "telemetry");
366
+ return path2.join(homeDir(), "telemetry");
260
367
  }
261
368
  function cacheDir() {
262
- return path.join(homeDir(), "cache");
369
+ return path2.join(homeDir(), "cache");
263
370
  }
264
371
  function canirunCachePath() {
265
- return path.join(cacheDir(), "canirun-models.json");
372
+ return path2.join(cacheDir(), "canirun-models.json");
266
373
  }
267
374
  async function ensureStorage() {
268
375
  await mkdir(benchmarksDir(), { recursive: true });
@@ -305,43 +412,6 @@ import os5 from "node:os";
305
412
  // src/platform/macos/hardware.ts
306
413
  init_process();
307
414
  import os2 from "node:os";
308
-
309
- // src/shared/bytes.ts
310
- var GiB = 1024 ** 3;
311
- var MiB = 1024 ** 2;
312
- function bytesToGiB(bytes) {
313
- return bytes / GiB;
314
- }
315
- function parseSizeToBytes(input) {
316
- const match = input.trim().match(/^([\d.]+)\s*(B|KB|MB|GB|TB|KiB|MiB|GiB|TiB)$/i);
317
- if (!match) return void 0;
318
- const value = Number(match[1]);
319
- const unit = match[2].toLowerCase();
320
- const map = {
321
- b: 1,
322
- kb: 1e3,
323
- mb: 1e3 ** 2,
324
- gb: 1e3 ** 3,
325
- tb: 1e3 ** 4,
326
- kib: 1024,
327
- mib: 1024 ** 2,
328
- gib: 1024 ** 3,
329
- tib: 1024 ** 4
330
- };
331
- const factor = map[unit];
332
- if (!factor || Number.isNaN(value)) return void 0;
333
- return value * factor;
334
- }
335
- function formatBytes(bytes, digits = 1) {
336
- if (!Number.isFinite(bytes)) return "N/A";
337
- const abs = Math.abs(bytes);
338
- if (abs >= GiB) return `${(bytes / GiB).toFixed(digits)} GB`;
339
- if (abs >= MiB) return `${(bytes / MiB).toFixed(digits)} MB`;
340
- if (abs >= 1024) return `${(bytes / 1024).toFixed(digits)} KB`;
341
- return `${Math.round(bytes)} B`;
342
- }
343
-
344
- // src/platform/macos/hardware.ts
345
415
  function parseAppleSilicon(chip) {
346
416
  const match = chip.match(/Apple\s+(M\d+)(?:\s+(Pro|Max|Ultra))?/i);
347
417
  if (!match) return void 0;
@@ -515,85 +585,6 @@ async function detectHardware() {
515
585
  import os7 from "node:os";
516
586
  import path5 from "node:path";
517
587
 
518
- // src/adapters/ollama/index.ts
519
- init_process();
520
- init_which();
521
-
522
- // src/adapters/ollama/parse.ts
523
- function parseOllamaList(output) {
524
- const lines = output.split(/\r?\n/).map((l) => l.trimEnd()).filter(Boolean);
525
- if (lines.length === 0) return [];
526
- const header = lines[0] ?? "";
527
- if (!/^NAME\s+/i.test(header)) {
528
- return lines.flatMap((line) => parseDataLine(line));
529
- }
530
- return lines.slice(1).flatMap((line) => parseDataLine(line));
531
- }
532
- function parseDataLine(line) {
533
- const parts = line.trim().split(/\s{2,}/);
534
- if (parts.length < 2) return [];
535
- const name = parts[0];
536
- if (!name || /^NAME$/i.test(name)) return [];
537
- const digest = parts[1];
538
- const sizeRaw = parts[2];
539
- const modified = parts.slice(3).join(" ") || void 0;
540
- return [
541
- {
542
- id: name,
543
- name,
544
- source: "ollama",
545
- digest,
546
- sizeBytes: sizeRaw ? parseSizeToBytes(sizeRaw.replace(/([A-Z]+)$/i, " $1")) : void 0,
547
- modifiedAt: modified
548
- }
549
- ];
550
- }
551
- function parseOllamaModelfile(modelfile) {
552
- const match = modelfile.match(/^FROM\s+(.+)$/m);
553
- return { from: match?.[1]?.trim() };
554
- }
555
-
556
- // src/adapters/ollama/index.ts
557
- var ollamaAdapter = {
558
- id: "ollama",
559
- label: "Ollama",
560
- async detect() {
561
- return Boolean(await commandExists("ollama"));
562
- },
563
- async version() {
564
- const result = await runCommand("ollama", ["--version"], { timeout: 8e3 });
565
- const text = `${result.stdout} ${result.stderr}`;
566
- const match = text.match(/(\d+\.\d+\.\d+)/);
567
- return match?.[1];
568
- },
569
- async listModels() {
570
- if (!await this.detect()) return [];
571
- const result = await runCommand("ollama", ["list"], { timeout: 15e3 });
572
- const models = parseOllamaList(result.stdout);
573
- const resolved = await Promise.all(
574
- models.map(async (model) => {
575
- const artifact = await resolveOllamaBlob(model.id);
576
- return { ...model, artifactPath: artifact?.path };
577
- })
578
- );
579
- return resolved;
580
- },
581
- async resolveModel(id) {
582
- return resolveOllamaBlob(id);
583
- },
584
- async benchmarkCapabilities() {
585
- return ["llama-bench"];
586
- }
587
- };
588
- async function resolveOllamaBlob(id) {
589
- const result = await runCommand("ollama", ["show", "--modelfile", id], { timeout: 15e3 });
590
- if (result.exitCode !== 0) return void 0;
591
- const { from } = parseOllamaModelfile(result.stdout);
592
- if (!from) return void 0;
593
- const format = from.endsWith(".gguf") || from.includes("sha256-") ? "gguf" : "unknown";
594
- return { id, path: from, format };
595
- }
596
-
597
588
  // src/adapters/lmstudio/index.ts
598
589
  import os6 from "node:os";
599
590
  import path4 from "node:path";
@@ -1632,7 +1623,7 @@ async function loadSession(options) {
1632
1623
  const [runtimes2, models, catalogState, history, llamaBench, agents] = await Promise.all([
1633
1624
  detectRuntimes(),
1634
1625
  discoverLocalModels(),
1635
- loadCatalog(options.network),
1626
+ options.light ? Promise.resolve({ catalog: [], networkUsed: false }) : loadCatalog(options.network),
1636
1627
  listBenchmarks(),
1637
1628
  commandExists("llama-bench").then(Boolean),
1638
1629
  detectAgents()
@@ -1640,11 +1631,11 @@ async function loadSession(options) {
1640
1631
  const rows = [];
1641
1632
  for (const local of models) {
1642
1633
  const catalog = matchCatalog(catalogState.catalog, local);
1643
- const compatibility = catalog ? await compatibilityFor(hardware, catalog, options.network) : void 0;
1634
+ const compatibility = catalog && !options.light ? await compatibilityFor(hardware, catalog, options.network) : void 0;
1644
1635
  const lastBenchmark = [...history].reverse().find((b) => b.model.id === local.id);
1645
1636
  rows.push({ local, catalog, compatibility, lastBenchmark });
1646
1637
  }
1647
- const recommendations = await recommendForMachine(hardware, catalogState.catalog, options.network);
1638
+ const recommendations = options.light ? [] : await recommendForMachine(hardware, catalogState.catalog, options.network);
1648
1639
  return {
1649
1640
  hardware,
1650
1641
  runtimes: runtimes2,
@@ -2727,7 +2718,10 @@ ${pull.stderr || t("launchPullFail")}` };
2727
2718
  const args2 = ["launch", def.ollamaLaunch, "--model", decision.ollamaTag];
2728
2719
  if (def.id === "openclaw" || def.id === "claude") args2.push("--yes");
2729
2720
  lines.push(`ollama ${args2.join(" ")}`);
2730
- const launched = await runCommand("ollama", args2, { timeout: 12e4 });
2721
+ const launched = await runCommand("ollama", args2, {
2722
+ timeout: 0,
2723
+ stdio: "inherit"
2724
+ });
2731
2725
  if (launched.exitCode !== 0) {
2732
2726
  return {
2733
2727
  ok: false,
@@ -3565,7 +3559,10 @@ async function run(args2) {
3565
3559
  process.stderr.write(`${t("loading")}
3566
3560
  `);
3567
3561
  }
3568
- const session = await loadSession({ network: args2.network });
3562
+ const session = await loadSession({
3563
+ network: args2.network,
3564
+ light: args2.command === "launch" || args2.command === "doctor" || args2.command === "hardware"
3565
+ });
3569
3566
  switch (args2.command) {
3570
3567
  case "hardware":
3571
3568
  print(
@@ -3690,6 +3687,10 @@ ${runtimesText(session)}`),
3690
3687
  return 1;
3691
3688
  }
3692
3689
  const catalog = session.rows.find((r) => r.local.id === model.id)?.catalog;
3690
+ if (!model.artifactPath && model.source === "ollama") {
3691
+ const artifact = await ollamaAdapter.resolveModel(model.id);
3692
+ model.artifactPath = artifact?.path;
3693
+ }
3693
3694
  const result = await runBenchmark({
3694
3695
  model,
3695
3696
  hardware: session.hardware,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pudu-ai",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "description": "Pudu-AI — local AI hardware & benchmark lab for the terminal",
5
5
  "keywords": [
6
6
  "ai",