pudu-ai 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,7 @@ npx pudu-ai
42
42
  npx pudu-ai hardware
43
43
  npx pudu-ai models
44
44
  npx pudu-ai models add-path ~/Models
45
+ npx pudu-ai setup
45
46
  npx pudu-ai recommend
46
47
  npx pudu-ai recommend --install --yes
47
48
  npx pudu-ai recommend --link opencode --yes
package/dist/cli/index.js CHANGED
@@ -17,7 +17,8 @@ function parseArgs(argv) {
17
17
  "doctor",
18
18
  "report",
19
19
  "tasks",
20
- "launch"
20
+ "launch",
21
+ "setup"
21
22
  ];
22
23
  const isKnown = known.includes(command);
23
24
  let addPath;
@@ -839,7 +840,17 @@ var en = {
839
840
  aboutReport: "Writes the latest measured run as Markdown. Use it to paste results into GitHub or notes.",
840
841
  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.",
841
842
  aboutAddPath: "Adds a GGUF folder to scan (not the whole disk). Use it so llama-bench can find models outside Ollama.",
842
- nav: "[B] Benchmark [M] Models [R] Recommend [T] Tasks [H] Hardware [C] Compare [L] History [Q] Quit",
843
+ nav: "[S] Setup/Install [B] Benchmark [M] Models [R] Recommend [T] Tasks [H] Hardware [C] Compare [L] History [Q] Quit",
844
+ setupTitle: "SETUP \u2014 INSTALL MODELS & AGENTS",
845
+ setupIntro: "Pick a recommended model (grade S\u2013B), install it with Ollama, then link or install OpenCode, Hermes, or OpenClaw.",
846
+ setupStep1: "1. Recommended models (\u2191\u2193 to choose)",
847
+ setupStep2: "2. Install the selected model",
848
+ setupInstallModel: "Press Enter or I \u2192 ollama pull (only if grade is S, A, or B)",
849
+ setupStep3: "3. Coding agent \u2014 link if present, install if missing",
850
+ setupLinkNow: "installed \u2192 press 1/2/3 to link this model",
851
+ setupInstallAgent: "not installed \u2192 press 1/2/3 to install via ollama launch (if eligible)",
852
+ setupKeys: "Enter/I = pull model 1 OpenCode 2 Hermes 3 OpenClaw Esc = back",
853
+ setupCta: "\u2192 Press [S] or [I] to INSTALL a recommended model and link OpenCode / Hermes / OpenClaw",
843
854
  machine: "MACHINE",
844
855
  runtimes: "LOCAL AI RUNTIMES",
845
856
  installedModels: "INSTALLED MODELS",
@@ -960,6 +971,7 @@ Usage:
960
971
  npx pudu-ai hardware
961
972
  npx pudu-ai models
962
973
  npx pudu-ai models add-path ~/Models
974
+ npx pudu-ai setup
963
975
  npx pudu-ai recommend
964
976
  npx pudu-ai recommend --install --yes
965
977
  npx pudu-ai recommend --link opencode --yes
@@ -1011,7 +1023,17 @@ var es = {
1011
1023
  aboutReport: "Exporta la \xFAltima corrida medida en Markdown. Sirve para pegar resultados en GitHub o notas.",
1012
1024
  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.",
1013
1025
  aboutAddPath: "A\xF1ade una carpeta GGUF a escanear (no todo el disco). Sirve para que llama-bench encuentre modelos fuera de Ollama.",
1014
- nav: "[B] Benchmark [M] Modelos [R] Recomendaciones [T] Tareas [H] Hardware [C] Comparar [L] Historial [Q] Salir",
1026
+ nav: "[S] Setup/Instalar [B] Benchmark [M] Modelos [R] Recomendaciones [T] Tareas [H] Hardware [C] Comparar [L] Historial [Q] Salir",
1027
+ setupTitle: "SETUP \u2014 INSTALAR MODELOS Y AGENTES",
1028
+ setupIntro: "Elige un modelo recomendado (nota S\u2013B), inst\xE1lalo con Ollama y luego vincula o instala OpenCode, Hermes u OpenClaw.",
1029
+ setupStep1: "1. Modelos recomendados (\u2191\u2193 para elegir)",
1030
+ setupStep2: "2. Instalar el modelo seleccionado",
1031
+ setupInstallModel: "Enter o I \u2192 ollama pull (solo si la nota es S, A o B)",
1032
+ setupStep3: "3. Agente de c\xF3digo \u2014 vincular si existe, instalar si falta",
1033
+ setupLinkNow: "instalado \u2192 pulsa 1/2/3 para vincular este modelo",
1034
+ setupInstallAgent: "no instalado \u2192 pulsa 1/2/3 para instalar con ollama launch (si es elegible)",
1035
+ setupKeys: "Enter/I = descargar modelo 1 OpenCode 2 Hermes 3 OpenClaw Esc = atr\xE1s",
1036
+ setupCta: "\u2192 Pulsa [S] o [I] para INSTALAR un modelo recomendado y vincular OpenCode / Hermes / OpenClaw",
1015
1037
  machine: "EQUIPO",
1016
1038
  runtimes: "RUNTIMES DE IA LOCAL",
1017
1039
  installedModels: "MODELOS INSTALADOS",
@@ -1132,6 +1154,7 @@ Uso:
1132
1154
  npx pudu-ai hardware
1133
1155
  npx pudu-ai models
1134
1156
  npx pudu-ai models add-path ~/Models
1157
+ npx pudu-ai setup
1135
1158
  npx pudu-ai recommend
1136
1159
  npx pudu-ai recommend --install --yes
1137
1160
  npx pudu-ai recommend --link opencode --yes
@@ -2059,7 +2082,8 @@ function Dashboard({ session }) {
2059
2082
  " ",
2060
2083
  GRADE_MEANING[rec.grade]
2061
2084
  ] }, `${rec.useCase}-${rec.model.id}`)),
2062
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: t("credits") })
2085
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: t("credits") }),
2086
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: t("setupCta") })
2063
2087
  ] })
2064
2088
  ] });
2065
2089
  }
@@ -2471,19 +2495,23 @@ async function pullOllamaModel(modelId) {
2471
2495
 
2472
2496
  // src/tui/views/Recommend.tsx
2473
2497
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2498
+ var TOOLS = [
2499
+ { key: "1", id: "opencode", label: "OpenCode" },
2500
+ { key: "2", id: "hermes", label: "Hermes" },
2501
+ { key: "3", id: "openclaw", label: "OpenClaw" }
2502
+ ];
2474
2503
  function RecommendView({ session }) {
2475
2504
  const { exit } = useApp();
2476
2505
  const [cursor, setCursor] = useState(0);
2477
2506
  const [log, setLog] = useState("");
2478
2507
  const [busy, setBusy] = useState(false);
2479
2508
  const recs = session.recommendations;
2509
+ const rec = recs[cursor];
2480
2510
  useInput((input, key) => {
2481
2511
  if (busy) return;
2482
- if (key.escape) return;
2483
2512
  if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
2484
2513
  if (key.downArrow) setCursor((c) => Math.min(Math.max(recs.length - 1, 0), c + 1));
2485
- const rec = recs[cursor];
2486
- if (input.toLowerCase() === "i" && rec) {
2514
+ if ((input.toLowerCase() === "i" || key.return) && rec) {
2487
2515
  if (!canInstallGrade(rec.grade)) {
2488
2516
  setLog(t("installGradeBlock", { grade: rec.grade }));
2489
2517
  return;
@@ -2495,8 +2523,7 @@ function RecommendView({ session }) {
2495
2523
  setBusy(false);
2496
2524
  });
2497
2525
  }
2498
- const map = { o: "opencode", e: "hermes", w: "openclaw" };
2499
- const tool = map[input.toLowerCase()];
2526
+ const tool = TOOLS.find((item) => item.key === input)?.id;
2500
2527
  if (tool && rec) {
2501
2528
  const decision = decideLaunch(session, tool);
2502
2529
  if (!decision.eligible) {
@@ -2513,29 +2540,40 @@ function RecommendView({ session }) {
2513
2540
  }
2514
2541
  });
2515
2542
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
2516
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: t("recommended") }),
2517
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("aboutRecommend") }),
2518
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("recommendKeys") }),
2519
- recs.map((rec, i) => /* @__PURE__ */ jsxs4(Text4, { color: i === cursor ? "cyan" : void 0, children: [
2543
+ /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
2544
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: t("setupTitle") }),
2545
+ /* @__PURE__ */ jsx4(Text4, { children: t("setupIntro") })
2546
+ ] }),
2547
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: t("setupStep1") }),
2548
+ recs.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("installNone") }),
2549
+ recs.map((item, i) => /* @__PURE__ */ jsxs4(Text4, { color: i === cursor ? "cyan" : void 0, children: [
2520
2550
  i === cursor ? "\u276F " : " ",
2521
- /* @__PURE__ */ jsx4(Text4, { color: "magenta", children: rec.useCase.padEnd(12) }),
2522
- rec.model.name.padEnd(22),
2551
+ /* @__PURE__ */ jsx4(Text4, { color: "magenta", children: item.useCase.padEnd(12) }),
2552
+ item.model.name.padEnd(22),
2523
2553
  " ",
2524
- /* @__PURE__ */ jsx4(Text4, { color: gradeColor(rec.grade), bold: true, children: rec.grade }),
2554
+ /* @__PURE__ */ jsx4(Text4, { color: gradeColor(item.grade), bold: true, children: item.grade }),
2525
2555
  " ",
2526
- GRADE_MEANING[rec.grade]
2527
- ] }, `${rec.useCase}-${rec.model.id}`)),
2556
+ GRADE_MEANING[item.grade]
2557
+ ] }, `${item.useCase}-${item.model.id}`)),
2558
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
2559
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "green", children: t("setupStep2") }),
2560
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: t("setupInstallModel") })
2561
+ ] }),
2528
2562
  /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
2529
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: t("agentsTitle") }),
2530
- session.agents.map((agent) => /* @__PURE__ */ jsxs4(Text4, { color: agent.detected ? "green" : "gray", children: [
2563
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: t("setupStep3") }),
2564
+ session.agents.map((agent, index) => /* @__PURE__ */ jsxs4(Text4, { color: agent.detected ? "green" : "yellow", children: [
2565
+ "[",
2566
+ index + 1,
2567
+ "] ",
2531
2568
  agent.detected ? "\u2713" : "\u25CB",
2532
2569
  " ",
2533
2570
  agent.label.padEnd(12),
2534
2571
  " ",
2535
- agent.detected ? t("detected") : t("agentMissing")
2572
+ agent.detected ? t("setupLinkNow") : t("setupInstallAgent")
2536
2573
  ] }, agent.id))
2537
2574
  ] }),
2538
- log ? /* @__PURE__ */ jsx4(Text4, { color: "green", children: log }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("credits") })
2575
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("setupKeys") }) }),
2576
+ log ? /* @__PURE__ */ jsx4(Text4, { color: "green", children: log }) : null
2539
2577
  ] });
2540
2578
  }
2541
2579
 
@@ -2999,33 +3037,43 @@ function TasksView({ session }) {
2999
3037
  ] });
3000
3038
  }
3001
3039
 
3040
+ // src/tui/keys.ts
3041
+ function handleAppKey(screen, input, key) {
3042
+ const letter = input.toLowerCase();
3043
+ if (screen === "benchmark") return { type: "ignore" };
3044
+ if (screen === "recommend" || screen === "tasks") {
3045
+ if (letter === "q") return { type: "quit" };
3046
+ if (key.escape) return { type: "home" };
3047
+ return { type: "ignore" };
3048
+ }
3049
+ if (letter === "q" || key.escape) return { type: "quit" };
3050
+ if (letter === "b") return { type: "screen", screen: "benchmark" };
3051
+ if (letter === "m") return { type: "screen", screen: "models" };
3052
+ if (letter === "r") return { type: "screen", screen: "recommend" };
3053
+ if (letter === "h") return { type: "screen", screen: "hardware" };
3054
+ if (letter === "c") return { type: "screen", screen: "compare" };
3055
+ if (letter === "l") return { type: "screen", screen: "history" };
3056
+ if (letter === "t") return { type: "screen", screen: "tasks" };
3057
+ if (letter === "s" || letter === "i") return { type: "screen", screen: "recommend" };
3058
+ if (key.return && screen === "home") return { type: "screen", screen: "benchmark" };
3059
+ return { type: "ignore" };
3060
+ }
3061
+
3002
3062
  // src/tui/App.tsx
3003
3063
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3004
3064
  function App(props) {
3005
3065
  const { exit } = useApp2();
3006
- const [screen, setScreen] = useState4("home");
3066
+ const [screen, setScreen] = useState4(props.start ?? "home");
3007
3067
  const [selected, setSelected] = useState4(0);
3008
3068
  const benchable = useMemo2(
3009
3069
  () => props.session.models.filter((m) => Boolean(m.artifactPath)),
3010
3070
  [props.session.models]
3011
3071
  );
3012
3072
  useInput4((input, key) => {
3013
- if (screen === "benchmark") return;
3014
- if (screen === "recommend" || screen === "tasks") {
3015
- if (letter === "q") exit();
3016
- if (key.escape) setScreen("home");
3017
- return;
3018
- }
3019
- const letter = input.toLowerCase();
3020
- if (letter === "q" || key.escape) exit();
3021
- if (letter === "b") setScreen("benchmark");
3022
- if (letter === "m") setScreen("models");
3023
- if (letter === "r") setScreen("recommend");
3024
- if (letter === "h") setScreen("hardware");
3025
- if (letter === "c") setScreen("compare");
3026
- if (letter === "l") setScreen("history");
3027
- if (letter === "t") setScreen("tasks");
3028
- if (key.return && screen === "home") setScreen("benchmark");
3073
+ const result = handleAppKey(screen, input, key);
3074
+ if (result.type === "quit") exit();
3075
+ if (result.type === "home") setScreen("home");
3076
+ if (result.type === "screen") setScreen(result.screen);
3029
3077
  });
3030
3078
  return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", padding: 1, children: [
3031
3079
  screen === "home" && /* @__PURE__ */ jsx9(Dashboard, { session: props.session }),
@@ -3052,8 +3100,8 @@ function App(props) {
3052
3100
 
3053
3101
  // src/tui/render.tsx
3054
3102
  import { jsx as jsx10 } from "react/jsx-runtime";
3055
- async function renderDashboard(session, preset) {
3056
- const instance = render(/* @__PURE__ */ jsx10(App, { session, preset }));
3103
+ async function renderDashboard(session, preset, start) {
3104
+ const instance = render(/* @__PURE__ */ jsx10(App, { session, preset, start }));
3057
3105
  await instance.waitUntilExit();
3058
3106
  }
3059
3107
 
@@ -3115,6 +3163,13 @@ ${runtimesText(session)}`),
3115
3163
  case "models":
3116
3164
  print(args2.json ? session.rows : explained("aboutModels", modelsText(session)), args2.json);
3117
3165
  return 0;
3166
+ case "setup":
3167
+ if (args2.json) {
3168
+ print({ recommendations: session.recommendations, agents: session.agents }, true);
3169
+ return 0;
3170
+ }
3171
+ await renderDashboard(session, args2.preset, "recommend");
3172
+ return 0;
3118
3173
  case "recommend": {
3119
3174
  if (args2.install && args2.yes) {
3120
3175
  const rec = session.recommendations.find((r) => canInstallGrade(r.grade));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pudu-ai",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Pudu-AI — local AI hardware & benchmark lab for the terminal",
5
5
  "keywords": [
6
6
  "ai",