pudu-ai 0.2.7 → 0.2.8

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 +76 -49
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -866,6 +866,7 @@ var en = {
866
866
  linkingTool: "Linking {tool} with {model}\u2026",
867
867
  modelPulled: "Pulled {model} with Ollama.",
868
868
  installNone: "No recommended model has grade S\u2013B to install.",
869
+ 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.",
869
870
  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.",
870
871
  noModels: "No local models detected",
871
872
  notTested: "Not tested",
@@ -1050,6 +1051,7 @@ var es = {
1050
1051
  linkingTool: "Vinculando {tool} con {model}\u2026",
1051
1052
  modelPulled: "Descargado {model} con Ollama.",
1052
1053
  installNone: "Ning\xFAn modelo recomendado tiene nota S\u2013B para instalar.",
1054
+ 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.",
1053
1055
  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.",
1054
1056
  noModels: "No se detectaron modelos locales",
1055
1057
  notTested: "Sin probar",
@@ -1214,6 +1216,35 @@ function t(id, vars) {
1214
1216
  return text;
1215
1217
  }
1216
1218
 
1219
+ // src/integrations/ollama-tags.ts
1220
+ var RULES = [
1221
+ { test: /qwen\s*3\.5\s*[-:]?\s*8b/i, tag: "qwen3.5:8b" },
1222
+ { test: /qwen\s*3\.5\s*[-:]?\s*4b/i, tag: "qwen3.5:4b" },
1223
+ { test: /qwen\s*3\s*[-:]?\s*14b/i, tag: "qwen3:14b" },
1224
+ { test: /qwen\s*3\s*[-:]?\s*8b/i, tag: "qwen3:8b" },
1225
+ { test: /qwen\s*3\s*[-:]?\s*4b/i, tag: "qwen3:4b" },
1226
+ { test: /qwen\s*2\.5\s*[-:]?\s*coder\s*[-:]?\s*7b/i, tag: "qwen2.5-coder:7b" },
1227
+ { test: /gemma\s*3\s*[-:]?\s*12b/i, tag: "gemma3:12b" },
1228
+ { test: /gemma\s*3\s*[-:]?\s*4b/i, tag: "gemma3:4b" },
1229
+ { test: /gemma\s*3\s*[-:]?\s*1b/i, tag: "gemma3:1b" },
1230
+ { test: /llama\s*3\.2\s*[-:]?\s*3b/i, tag: "llama3.2:3b" },
1231
+ { test: /llama\s*3\.2\s*[-:]?\s*1b/i, tag: "llama3.2:1b" },
1232
+ { test: /llama\s*3\.1\s*[-:]?\s*8b/i, tag: "llama3.1:8b" },
1233
+ { test: /deepseek[- ]r1\s*[-:]?\s*8b|deepseek-r1-distill.*8b/i, tag: "deepseek-r1:8b" },
1234
+ { test: /deepseek[- ]r1\s*[-:]?\s*7b/i, tag: "deepseek-r1:7b" },
1235
+ { test: /mistral\s*[-:]?\s*7b|mistral-nemo/i, tag: "mistral:7b" },
1236
+ { test: /phi\s*4|phi-4/i, tag: "phi4" }
1237
+ ];
1238
+ function resolveOllamaTag(...parts) {
1239
+ const haystack = parts.filter(Boolean).join(" ");
1240
+ if (!haystack.trim()) return void 0;
1241
+ if (/^[a-z0-9._-]+:[a-z0-9._-]+$/i.test(haystack.trim())) return haystack.trim();
1242
+ for (const rule of RULES) {
1243
+ if (rule.test.test(haystack)) return rule.tag;
1244
+ }
1245
+ return void 0;
1246
+ }
1247
+
1217
1248
  // src/compatibility/local.ts
1218
1249
  var GB_PER_BILLION_Q4 = 0.65;
1219
1250
  function estimateModelRamGb(model, quant = "Q4_K_M") {
@@ -1295,8 +1326,9 @@ async function recommendForMachine(hardware, catalog, network) {
1295
1326
  try {
1296
1327
  const rows = [];
1297
1328
  for (const useCase of useCases) {
1298
- const found = await fetchRecommendations(hardware, useCase, 1);
1299
- rows.push(...found.map((item) => ({ ...item, useCase: labelUseCase(useCase) })));
1329
+ const found = await fetchRecommendations(hardware, useCase, 8);
1330
+ const mapped = found.filter((item) => resolveOllamaTag(item.model.id, item.model.name)).slice(0, 1).map((item) => ({ ...item, useCase: labelUseCase(useCase) }));
1331
+ rows.push(...mapped);
1300
1332
  }
1301
1333
  if (rows.length) return rows;
1302
1334
  } catch {
@@ -1308,6 +1340,7 @@ async function recommendForMachine(hardware, catalog, network) {
1308
1340
  for (const useCase of useCases) {
1309
1341
  const hit = ranked.find((row) => {
1310
1342
  if (used.has(row.model.id)) return false;
1343
+ if (!resolveOllamaTag(row.model.id, row.model.name)) return false;
1311
1344
  if (useCase === "chat") return row.model.useCase?.includes("chat") ?? true;
1312
1345
  return row.model.useCase?.some((u) => u.includes(useCase)) ?? false;
1313
1346
  });
@@ -2345,13 +2378,6 @@ var INTEGRATION_IDS = ["opencode", "openclaw", "hermes", "claude"];
2345
2378
 
2346
2379
  // src/integrations/decide.ts
2347
2380
  var GRADE_RANK = { S: 5, A: 4, B: 3, C: 2, D: 1, F: 0 };
2348
- function toOllamaTag(id) {
2349
- const compact = id.toLowerCase().replace(/_/g, "-");
2350
- const match = compact.match(/^([a-z0-9.]+(?:-[a-z0-9.]+)*)-(\d+(?:\.\d+)?b)$/i);
2351
- if (match) return `${match[1]}:${match[2]}`;
2352
- if (compact.includes(":")) return compact;
2353
- return compact;
2354
- }
2355
2381
  function useCaseOk(useCases, needed) {
2356
2382
  if (!useCases?.length) return needed.includes("chat");
2357
2383
  return useCases.some((u) => needed.some((n) => u.includes(n)));
@@ -2365,10 +2391,12 @@ function decideLaunch(session, id) {
2365
2391
  for (const row of session.rows) {
2366
2392
  const useCases = row.catalog?.useCase ?? ["chat", "code"];
2367
2393
  if (!useCaseOk(useCases, def.useCases)) continue;
2394
+ const tag = resolveOllamaTag(row.local.id, row.local.name, row.catalog?.id, row.catalog?.name);
2395
+ if (!tag) continue;
2368
2396
  const tps = row.lastBenchmark?.benchmark.generationTokensPerSecond;
2369
2397
  candidates.push({
2370
2398
  modelId: row.local.id,
2371
- ollamaTag: row.local.id.includes(":") ? row.local.id : toOllamaTag(row.local.id),
2399
+ ollamaTag: tag,
2372
2400
  installed: true,
2373
2401
  origin: row.lastBenchmark ? "measured" : "estimated",
2374
2402
  grade: row.compatibility?.grade,
@@ -2380,9 +2408,11 @@ function decideLaunch(session, id) {
2380
2408
  if (!useCaseOk(model.useCase, def.useCases)) continue;
2381
2409
  const fit = localCompatibility(session.hardware, model);
2382
2410
  if (!def.allowedGrades.includes(fit.grade)) continue;
2411
+ const tag = resolveOllamaTag(model.id, model.name);
2412
+ if (!tag) continue;
2383
2413
  candidates.push({
2384
2414
  modelId: model.id,
2385
- ollamaTag: toOllamaTag(model.id),
2415
+ ollamaTag: tag,
2386
2416
  installed: false,
2387
2417
  origin: "estimated",
2388
2418
  grade: fit.grade
@@ -2482,8 +2512,11 @@ var INSTALLABLE = ["S", "A", "B"];
2482
2512
  function canInstallGrade(grade) {
2483
2513
  return Boolean(grade && INSTALLABLE.includes(grade));
2484
2514
  }
2485
- async function pullOllamaModel(modelId) {
2486
- const tag = toOllamaTag(modelId);
2515
+ async function pullOllamaModel(modelId, extraName) {
2516
+ const tag = resolveOllamaTag(modelId, extraName);
2517
+ if (!tag) {
2518
+ return { ok: false, log: t("noOllamaTag", { model: extraName ?? modelId }), tag: modelId };
2519
+ }
2487
2520
  const result = await runCommand("ollama", ["pull", tag], { timeout: 30 * 6e4 });
2488
2521
  if (result.exitCode !== 0) {
2489
2522
  return { ok: false, log: result.stderr || t("launchPullFail"), tag };
@@ -2515,8 +2548,8 @@ function RecommendView({ session }) {
2515
2548
  return;
2516
2549
  }
2517
2550
  setBusy(true);
2518
- setLog(t("launchPulling", { model: rec.model.id }));
2519
- void pullOllamaModel(rec.model.id).then((result) => {
2551
+ setLog(t("launchPulling", { model: rec.model.name }));
2552
+ void pullOllamaModel(rec.model.id, rec.model.name).then((result) => {
2520
2553
  setLog(result.log);
2521
2554
  setBusy(false);
2522
2555
  });
@@ -2551,7 +2584,9 @@ function RecommendView({ session }) {
2551
2584
  " ",
2552
2585
  /* @__PURE__ */ jsx4(Text4, { color: gradeColor(item.grade), bold: true, children: item.grade }),
2553
2586
  " ",
2554
- GRADE_MEANING[item.grade]
2587
+ GRADE_MEANING[item.grade],
2588
+ " ",
2589
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: resolveOllamaTag(item.model.id, item.model.name) ?? t("noOllamaTag", { model: item.model.name }) })
2555
2590
  ] }, `${item.useCase}-${item.model.id}`)),
2556
2591
  /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
2557
2592
  /* @__PURE__ */ jsx4(Text4, { bold: true, color: "green", children: t("setupStep2") }),
@@ -3062,45 +3097,37 @@ import { Box as Box9, Text as Text9 } from "ink";
3062
3097
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3063
3098
  function Welcome({ compact = false }) {
3064
3099
  if (compact) {
3065
- return /* @__PURE__ */ jsxs8(Box9, { marginBottom: 1, children: [
3066
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u{1F98C} " }),
3100
+ return /* @__PURE__ */ jsxs8(Box9, { marginBottom: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
3067
3101
  /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: t("appTitle") }),
3068
- /* @__PURE__ */ jsxs8(Text9, { color: "magenta", children: [
3069
- " ",
3070
- t("byline")
3071
- ] })
3102
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \xB7 " }),
3103
+ /* @__PURE__ */ jsx9(Text9, { color: "magenta", children: t("byline") })
3072
3104
  ] });
3073
3105
  }
3074
- return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", marginBottom: 1, children: [
3075
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: " \\/ \\/" }),
3076
- /* @__PURE__ */ jsxs8(Text9, { children: [
3077
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: " " }),
3078
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " (\\_ _/)" })
3079
- ] }),
3080
- /* @__PURE__ */ jsxs8(Text9, { children: [
3081
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " / " }),
3082
- /* @__PURE__ */ jsx9(Text9, { color: "white", children: "o o" }),
3083
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \\" }),
3084
- /* @__PURE__ */ jsx9(Text9, { children: " " }),
3085
- /* @__PURE__ */ jsx9(Text9, { bold: true, color: "green", children: t("appTitle") })
3086
- ] }),
3087
- /* @__PURE__ */ jsxs8(Text9, { children: [
3088
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \\ " }),
3089
- /* @__PURE__ */ jsx9(Text9, { color: "red", children: "^" }),
3090
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " /" }),
3091
- /* @__PURE__ */ jsx9(Text9, { children: " " }),
3092
- /* @__PURE__ */ jsx9(Text9, { color: "white", children: t("appSubtitle") })
3093
- ] }),
3094
- /* @__PURE__ */ jsxs8(Text9, { children: [
3095
- /* @__PURE__ */ jsx9(Text9, { color: "green", children: " /| |\\" }),
3096
- /* @__PURE__ */ jsx9(Text9, { children: " " }),
3097
- /* @__PURE__ */ jsx9(Text9, { bold: true, color: "magenta", children: t("byline") })
3106
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", marginBottom: 1, children: [
3107
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", marginRight: 2, children: [
3108
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: " \u2572" }),
3109
+ /* @__PURE__ */ jsxs8(Text9, { children: [
3110
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \u256D\u2500" }),
3111
+ /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF \u25CF" }),
3112
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "\u2500\u256E" })
3113
+ ] }),
3114
+ /* @__PURE__ */ jsxs8(Text9, { children: [
3115
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \u2502 " }),
3116
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25BD" }),
3117
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \u2502" })
3118
+ ] }),
3119
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \u2570\u252C\u2500\u2500\u2500\u252C\u256F" }),
3120
+ /* @__PURE__ */ jsx9(Text9, { color: "green", children: " uu uu" })
3098
3121
  ] }),
3099
- /* @__PURE__ */ jsx9(Text9, { color: "green", children: " U U" })
3122
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", justifyContent: "center", children: [
3123
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: t("appTitle") }),
3124
+ /* @__PURE__ */ jsx9(Text9, { color: "white", children: t("appSubtitle") }),
3125
+ /* @__PURE__ */ jsx9(Text9, { color: "magenta", children: t("byline") })
3126
+ ] })
3100
3127
  ] });
3101
3128
  }
3102
3129
  function ColorNav() {
3103
- return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
3130
+ return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
3104
3131
  /* @__PURE__ */ jsx9(Text9, { color: "yellow", bold: true, children: "[S]" }),
3105
3132
  /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: " Setup " }),
3106
3133
  /* @__PURE__ */ jsx9(Text9, { color: "green", bold: true, children: "[B]" }),
@@ -3241,7 +3268,7 @@ ${runtimesText(session)}`),
3241
3268
  print(t("installNone"), args2.json);
3242
3269
  return 1;
3243
3270
  }
3244
- const pulled = await pullOllamaModel(rec.model.id);
3271
+ const pulled = await pullOllamaModel(rec.model.id, rec.model.name);
3245
3272
  print(args2.json ? pulled : explained("aboutRecommend", pulled.log), args2.json);
3246
3273
  return pulled.ok ? 0 : 1;
3247
3274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pudu-ai",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Pudu-AI — local AI hardware & benchmark lab for the terminal",
5
5
  "keywords": [
6
6
  "ai",