pudu-ai 0.2.7 → 0.2.9

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 +102 -66
  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
  });
@@ -1953,7 +1986,7 @@ import { render } from "ink";
1953
1986
 
1954
1987
  // src/tui/App.tsx
1955
1988
  import { Box as Box10, useApp as useApp2, useInput as useInput4 } from "ink";
1956
- import { useMemo as useMemo2, useState as useState4 } from "react";
1989
+ import { useMemo as useMemo2, useRef as useRef2, useState as useState4 } from "react";
1957
1990
 
1958
1991
  // src/tui/views/Dashboard.tsx
1959
1992
  import { Box, Text } from "ink";
@@ -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") }),
@@ -3036,15 +3071,7 @@ function TasksView({ session }) {
3036
3071
  }
3037
3072
 
3038
3073
  // src/tui/keys.ts
3039
- function handleAppKey(screen, input, key) {
3040
- const letter = input.toLowerCase();
3041
- if (screen === "benchmark") return { type: "ignore" };
3042
- if (screen === "recommend" || screen === "tasks") {
3043
- if (letter === "q") return { type: "quit" };
3044
- if (key.escape) return { type: "home" };
3045
- return { type: "ignore" };
3046
- }
3047
- if (letter === "q" || key.escape) return { type: "quit" };
3074
+ function globalNav(letter) {
3048
3075
  if (letter === "b") return { type: "screen", screen: "benchmark" };
3049
3076
  if (letter === "m") return { type: "screen", screen: "models" };
3050
3077
  if (letter === "r") return { type: "screen", screen: "recommend" };
@@ -3052,7 +3079,20 @@ function handleAppKey(screen, input, key) {
3052
3079
  if (letter === "c") return { type: "screen", screen: "compare" };
3053
3080
  if (letter === "l") return { type: "screen", screen: "history" };
3054
3081
  if (letter === "t") return { type: "screen", screen: "tasks" };
3055
- if (letter === "s" || letter === "i") return { type: "screen", screen: "recommend" };
3082
+ if (letter === "s") return { type: "screen", screen: "recommend" };
3083
+ return void 0;
3084
+ }
3085
+ function handleAppKey(screen, input, key) {
3086
+ const letter = input.toLowerCase();
3087
+ if (screen === "benchmark") return { type: "ignore" };
3088
+ if (letter === "q") return { type: "quit" };
3089
+ if (key.escape) return screen === "home" ? { type: "quit" } : { type: "home" };
3090
+ if (screen === "recommend") {
3091
+ if (letter === "i" || letter === "1" || letter === "2" || letter === "3") return { type: "ignore" };
3092
+ }
3093
+ const nav = globalNav(letter);
3094
+ if (nav) return nav;
3095
+ if (letter === "i" && screen === "home") return { type: "screen", screen: "recommend" };
3056
3096
  if (key.return && screen === "home") return { type: "screen", screen: "benchmark" };
3057
3097
  return { type: "ignore" };
3058
3098
  }
@@ -3062,45 +3102,37 @@ import { Box as Box9, Text as Text9 } from "ink";
3062
3102
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3063
3103
  function Welcome({ compact = false }) {
3064
3104
  if (compact) {
3065
- return /* @__PURE__ */ jsxs8(Box9, { marginBottom: 1, children: [
3066
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u{1F98C} " }),
3105
+ return /* @__PURE__ */ jsxs8(Box9, { marginBottom: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
3067
3106
  /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: t("appTitle") }),
3068
- /* @__PURE__ */ jsxs8(Text9, { color: "magenta", children: [
3069
- " ",
3070
- t("byline")
3071
- ] })
3107
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \xB7 " }),
3108
+ /* @__PURE__ */ jsx9(Text9, { color: "magenta", children: t("byline") })
3072
3109
  ] });
3073
3110
  }
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") })
3111
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", marginBottom: 1, children: [
3112
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", marginRight: 2, children: [
3113
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: " \u2572" }),
3114
+ /* @__PURE__ */ jsxs8(Text9, { children: [
3115
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \u256D\u2500" }),
3116
+ /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF \u25CF" }),
3117
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "\u2500\u256E" })
3118
+ ] }),
3119
+ /* @__PURE__ */ jsxs8(Text9, { children: [
3120
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \u2502 " }),
3121
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25BD" }),
3122
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \u2502" })
3123
+ ] }),
3124
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: " \u2570\u252C\u2500\u2500\u2500\u252C\u256F" }),
3125
+ /* @__PURE__ */ jsx9(Text9, { color: "green", children: " uu uu" })
3098
3126
  ] }),
3099
- /* @__PURE__ */ jsx9(Text9, { color: "green", children: " U U" })
3127
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", justifyContent: "center", children: [
3128
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: t("appTitle") }),
3129
+ /* @__PURE__ */ jsx9(Text9, { color: "white", children: t("appSubtitle") }),
3130
+ /* @__PURE__ */ jsx9(Text9, { color: "magenta", children: t("byline") })
3131
+ ] })
3100
3132
  ] });
3101
3133
  }
3102
3134
  function ColorNav() {
3103
- return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
3135
+ return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
3104
3136
  /* @__PURE__ */ jsx9(Text9, { color: "yellow", bold: true, children: "[S]" }),
3105
3137
  /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: " Setup " }),
3106
3138
  /* @__PURE__ */ jsx9(Text9, { color: "green", bold: true, children: "[B]" }),
@@ -3123,23 +3155,28 @@ function ColorNav() {
3123
3155
  }
3124
3156
 
3125
3157
  // src/tui/App.tsx
3126
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3158
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3127
3159
  function App(props) {
3128
3160
  const { exit } = useApp2();
3129
3161
  const [screen, setScreen] = useState4(props.start ?? "home");
3162
+ const screenRef = useRef2(screen);
3163
+ screenRef.current = screen;
3130
3164
  const [selected, setSelected] = useState4(0);
3131
3165
  const benchable = useMemo2(
3132
3166
  () => props.session.models.filter((m) => Boolean(m.artifactPath)),
3133
3167
  [props.session.models]
3134
3168
  );
3135
3169
  useInput4((input, key) => {
3136
- const result = handleAppKey(screen, input, key);
3170
+ const result = handleAppKey(screenRef.current, input, key);
3137
3171
  if (result.type === "quit") exit();
3138
3172
  if (result.type === "home") setScreen("home");
3139
3173
  if (result.type === "screen") setScreen(result.screen);
3140
3174
  });
3141
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", padding: 1, children: [
3142
- screen !== "benchmark" && /* @__PURE__ */ jsx10(Welcome, { compact: screen !== "home" }),
3175
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, paddingTop: 0, paddingBottom: 1, children: [
3176
+ screen !== "benchmark" && /* @__PURE__ */ jsxs9(Fragment2, { children: [
3177
+ /* @__PURE__ */ jsx10(ColorNav, {}),
3178
+ /* @__PURE__ */ jsx10(Welcome, { compact: true })
3179
+ ] }),
3143
3180
  screen === "home" && /* @__PURE__ */ jsx10(Dashboard, { session: props.session }),
3144
3181
  screen === "models" && /* @__PURE__ */ jsx10(ModelsView, { session: props.session }),
3145
3182
  screen === "hardware" && /* @__PURE__ */ jsx10(HardwareView, { session: props.session }),
@@ -3157,8 +3194,7 @@ function App(props) {
3157
3194
  preset: props.preset,
3158
3195
  onBack: () => setScreen("home")
3159
3196
  }
3160
- ),
3161
- screen !== "benchmark" && /* @__PURE__ */ jsx10(ColorNav, {})
3197
+ )
3162
3198
  ] });
3163
3199
  }
3164
3200
 
@@ -3241,7 +3277,7 @@ ${runtimesText(session)}`),
3241
3277
  print(t("installNone"), args2.json);
3242
3278
  return 1;
3243
3279
  }
3244
- const pulled = await pullOllamaModel(rec.model.id);
3280
+ const pulled = await pullOllamaModel(rec.model.id, rec.model.name);
3245
3281
  print(args2.json ? pulled : explained("aboutRecommend", pulled.log), args2.json);
3246
3282
  return pulled.ok ? 0 : 1;
3247
3283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pudu-ai",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Pudu-AI — local AI hardware & benchmark lab for the terminal",
5
5
  "keywords": [
6
6
  "ai",