pudu-ai 0.2.2 → 0.2.4
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 +2 -0
- package/dist/cli/index.js +460 -271
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ npx pudu-ai hardware
|
|
|
43
43
|
npx pudu-ai models
|
|
44
44
|
npx pudu-ai models add-path ~/Models
|
|
45
45
|
npx pudu-ai recommend
|
|
46
|
+
npx pudu-ai recommend --install --yes
|
|
47
|
+
npx pudu-ai recommend --link opencode --yes
|
|
46
48
|
npx pudu-ai tasks
|
|
47
49
|
npx pudu-ai tasks --for code,image,transcription --lang es
|
|
48
50
|
npx pudu-ai benchmark
|
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/cli/parse-args.ts
|
|
5
4
|
function parseArgs(argv) {
|
|
@@ -30,6 +29,7 @@ function parseArgs(argv) {
|
|
|
30
29
|
const forIndex = args2.findIndex((a) => a === "--for");
|
|
31
30
|
const scopeIndex = args2.findIndex((a) => a === "--scope");
|
|
32
31
|
const priorityIndex = args2.findIndex((a) => a === "--priority");
|
|
32
|
+
const linkIndex = args2.findIndex((a) => a === "--link");
|
|
33
33
|
return {
|
|
34
34
|
command: isKnown ? command : "dashboard",
|
|
35
35
|
positional: isKnown ? rest.slice(1) : rest,
|
|
@@ -46,7 +46,9 @@ function parseArgs(argv) {
|
|
|
46
46
|
forKinds: forIndex >= 0 ? args2[forIndex + 1] : void 0,
|
|
47
47
|
scope: scopeIndex >= 0 ? args2[scopeIndex + 1] : void 0,
|
|
48
48
|
priority: priorityIndex >= 0 ? args2[priorityIndex + 1] : void 0,
|
|
49
|
-
yes: flags.has("--yes")
|
|
49
|
+
yes: flags.has("--yes"),
|
|
50
|
+
install: flags.has("--install"),
|
|
51
|
+
link: linkIndex >= 0 ? args2[linkIndex + 1] : void 0
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -844,6 +846,14 @@ var en = {
|
|
|
844
846
|
compatible: "COMPATIBLE (catalog, estimated)",
|
|
845
847
|
recommended: "RECOMMENDED FOR THIS MACHINE",
|
|
846
848
|
recommendedHint: "Estimated values come from CanIRun.ai by midudev (canirun.ai \xB7 midu.dev) or a local fallback \u2014 not measured",
|
|
849
|
+
recommendKeys: "[\u2191\u2193] model [I] ollama pull if grade S\u2013B [O] OpenCode [E] Hermes [W] OpenClaw [Esc] back",
|
|
850
|
+
recommendCliHint: "Install: npx pudu-ai recommend --install --yes Link: npx pudu-ai recommend --link opencode --yes",
|
|
851
|
+
agentsTitle: "CODING AGENTS",
|
|
852
|
+
agentMissing: "not installed \u2014 select to install via ollama launch if the model is eligible",
|
|
853
|
+
installGradeBlock: "Grade {grade} is too low to pull. Need S, A, or B.",
|
|
854
|
+
linkingTool: "Linking {tool} with {model}\u2026",
|
|
855
|
+
modelPulled: "Pulled {model} with Ollama.",
|
|
856
|
+
installNone: "No recommended model has grade S\u2013B to install.",
|
|
847
857
|
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.",
|
|
848
858
|
noModels: "No local models detected",
|
|
849
859
|
notTested: "Not tested",
|
|
@@ -951,6 +961,8 @@ Usage:
|
|
|
951
961
|
npx pudu-ai models
|
|
952
962
|
npx pudu-ai models add-path ~/Models
|
|
953
963
|
npx pudu-ai recommend
|
|
964
|
+
npx pudu-ai recommend --install --yes
|
|
965
|
+
npx pudu-ai recommend --link opencode --yes
|
|
954
966
|
npx pudu-ai tasks
|
|
955
967
|
npx pudu-ai tasks --for code,image --scope all --priority speed
|
|
956
968
|
npx pudu-ai benchmark [model]
|
|
@@ -1006,6 +1018,14 @@ var es = {
|
|
|
1006
1018
|
compatible: "COMPATIBLES (cat\xE1logo, estimado)",
|
|
1007
1019
|
recommended: "RECOMENDADOS PARA ESTE EQUIPO",
|
|
1008
1020
|
recommendedHint: "Los valores estimados vienen de CanIRun.ai de midudev (canirun.ai \xB7 midu.dev) o de un fallback local \u2014 no son mediciones",
|
|
1021
|
+
recommendKeys: "[\u2191\u2193] modelo [I] ollama pull si nota S\u2013B [O] OpenCode [E] Hermes [W] OpenClaw [Esc] atr\xE1s",
|
|
1022
|
+
recommendCliHint: "Instalar: npx pudu-ai recommend --install --yes Vincular: npx pudu-ai recommend --link opencode --yes",
|
|
1023
|
+
agentsTitle: "AGENTES DE C\xD3DIGO",
|
|
1024
|
+
agentMissing: "no instalado \u2014 selecci\xF3nalo para instalar con ollama launch si el modelo es elegible",
|
|
1025
|
+
installGradeBlock: "La nota {grade} es baja para hacer pull. Hace falta S, A o B.",
|
|
1026
|
+
linkingTool: "Vinculando {tool} con {model}\u2026",
|
|
1027
|
+
modelPulled: "Descargado {model} con Ollama.",
|
|
1028
|
+
installNone: "Ning\xFAn modelo recomendado tiene nota S\u2013B para instalar.",
|
|
1009
1029
|
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.",
|
|
1010
1030
|
noModels: "No se detectaron modelos locales",
|
|
1011
1031
|
notTested: "Sin probar",
|
|
@@ -1113,6 +1133,8 @@ Uso:
|
|
|
1113
1133
|
npx pudu-ai models
|
|
1114
1134
|
npx pudu-ai models add-path ~/Models
|
|
1115
1135
|
npx pudu-ai recommend
|
|
1136
|
+
npx pudu-ai recommend --install --yes
|
|
1137
|
+
npx pudu-ai recommend --link opencode --yes
|
|
1116
1138
|
npx pudu-ai tasks
|
|
1117
1139
|
npx pudu-ai tasks --for code,image --scope all --priority speed
|
|
1118
1140
|
npx pudu-ai benchmark [model]
|
|
@@ -1359,15 +1381,33 @@ async function listBenchmarks() {
|
|
|
1359
1381
|
return records;
|
|
1360
1382
|
}
|
|
1361
1383
|
|
|
1384
|
+
// src/integrations/agents.ts
|
|
1385
|
+
var AGENTS = [
|
|
1386
|
+
{ id: "opencode", label: "OpenCode", bin: "opencode" },
|
|
1387
|
+
{ id: "hermes", label: "Hermes", bin: "hermes" },
|
|
1388
|
+
{ id: "openclaw", label: "OpenClaw", bin: "openclaw" }
|
|
1389
|
+
];
|
|
1390
|
+
async function detectAgents() {
|
|
1391
|
+
const out = [];
|
|
1392
|
+
for (const agent of AGENTS) {
|
|
1393
|
+
out.push({
|
|
1394
|
+
...agent,
|
|
1395
|
+
detected: Boolean(await commandExists(agent.bin))
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
return out;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1362
1401
|
// src/session/load.ts
|
|
1363
1402
|
async function loadSession(options) {
|
|
1364
1403
|
const hardware = await detectHardware();
|
|
1365
|
-
const [runtimes2, models, catalogState, history, llamaBench] = await Promise.all([
|
|
1404
|
+
const [runtimes2, models, catalogState, history, llamaBench, agents] = await Promise.all([
|
|
1366
1405
|
detectRuntimes(),
|
|
1367
1406
|
discoverLocalModels(),
|
|
1368
1407
|
loadCatalog(options.network),
|
|
1369
1408
|
listBenchmarks(),
|
|
1370
|
-
commandExists("llama-bench").then(Boolean)
|
|
1409
|
+
commandExists("llama-bench").then(Boolean),
|
|
1410
|
+
detectAgents()
|
|
1371
1411
|
]);
|
|
1372
1412
|
const rows = [];
|
|
1373
1413
|
for (const local of models) {
|
|
@@ -1386,7 +1426,8 @@ async function loadSession(options) {
|
|
|
1386
1426
|
recommendations,
|
|
1387
1427
|
history,
|
|
1388
1428
|
llamaBench,
|
|
1389
|
-
networkUsed: catalogState.networkUsed
|
|
1429
|
+
networkUsed: catalogState.networkUsed,
|
|
1430
|
+
agents
|
|
1390
1431
|
};
|
|
1391
1432
|
}
|
|
1392
1433
|
|
|
@@ -1886,8 +1927,8 @@ function setLogLevel(next) {
|
|
|
1886
1927
|
import { render } from "ink";
|
|
1887
1928
|
|
|
1888
1929
|
// src/tui/App.tsx
|
|
1889
|
-
import { Box as Box9, Text as Text9, useApp, useInput as
|
|
1890
|
-
import { useMemo as useMemo2, useState as
|
|
1930
|
+
import { Box as Box9, Text as Text9, useApp as useApp2, useInput as useInput4 } from "ink";
|
|
1931
|
+
import { useMemo as useMemo2, useState as useState4 } from "react";
|
|
1891
1932
|
|
|
1892
1933
|
// src/tui/views/Dashboard.tsx
|
|
1893
1934
|
import { Box, Text } from "ink";
|
|
@@ -1925,6 +1966,27 @@ var GRADE_MEANING = {
|
|
|
1925
1966
|
F: "Not recommended"
|
|
1926
1967
|
};
|
|
1927
1968
|
|
|
1969
|
+
// src/tui/theme.ts
|
|
1970
|
+
function gradeColor(grade) {
|
|
1971
|
+
if (grade === "S" || grade === "A") return "green";
|
|
1972
|
+
if (grade === "B") return "yellow";
|
|
1973
|
+
if (grade === "C") return "magenta";
|
|
1974
|
+
if (grade === "D" || grade === "F") return "red";
|
|
1975
|
+
return "gray";
|
|
1976
|
+
}
|
|
1977
|
+
function bar(percent, width = 10) {
|
|
1978
|
+
if (percent === void 0 || Number.isNaN(percent)) return `${"\u2591".repeat(width)} N/A`;
|
|
1979
|
+
const filled = Math.max(0, Math.min(width, Math.round(percent / 100 * width)));
|
|
1980
|
+
return `${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}`;
|
|
1981
|
+
}
|
|
1982
|
+
function sparkline(values, width = 24) {
|
|
1983
|
+
const glyphs = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
1984
|
+
if (!values.length) return glyphs[0].repeat(width);
|
|
1985
|
+
const slice = values.slice(-width);
|
|
1986
|
+
const max = Math.max(...slice, 1);
|
|
1987
|
+
return slice.map((v) => glyphs[Math.min(glyphs.length - 1, Math.floor(v / max * (glyphs.length - 1)))]).join("").padStart(width, glyphs[0]);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1928
1990
|
// src/tui/views/Dashboard.tsx
|
|
1929
1991
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1930
1992
|
function Dashboard({ session }) {
|
|
@@ -1964,7 +2026,7 @@ function Dashboard({ session }) {
|
|
|
1964
2026
|
] }),
|
|
1965
2027
|
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1966
2028
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: t("runtimes") }),
|
|
1967
|
-
session.runtimes.map((r) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
2029
|
+
session.runtimes.map((r) => /* @__PURE__ */ jsxs(Text, { color: r.detected ? "green" : "gray", children: [
|
|
1968
2030
|
r.detected ? "\u2713" : "\u25CB",
|
|
1969
2031
|
" ",
|
|
1970
2032
|
r.label.padEnd(14),
|
|
@@ -1976,12 +2038,12 @@ function Dashboard({ session }) {
|
|
|
1976
2038
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: t("installedModels") }),
|
|
1977
2039
|
session.rows.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: t("noModels") }),
|
|
1978
2040
|
session.rows.map((row) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
1979
|
-
row.local.name.padEnd(20),
|
|
2041
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: row.local.name.padEnd(20) }),
|
|
1980
2042
|
" ",
|
|
1981
2043
|
row.local.sizeBytes ? formatBytes(row.local.sizeBytes) : "N/A",
|
|
1982
|
-
"
|
|
1983
|
-
row.compatibility?.grade ?? "\u2014",
|
|
1984
|
-
"
|
|
2044
|
+
" ",
|
|
2045
|
+
/* @__PURE__ */ jsx(Text, { color: gradeColor(row.compatibility?.grade), bold: true, children: row.compatibility?.grade ?? "\u2014" }),
|
|
2046
|
+
" ",
|
|
1985
2047
|
row.lastBenchmark?.benchmark.generationTokensPerSecond ? formatTokensPerSec(row.lastBenchmark.benchmark.generationTokensPerSecond) : t("notTested")
|
|
1986
2048
|
] }, row.local.id))
|
|
1987
2049
|
] }),
|
|
@@ -1989,11 +2051,11 @@ function Dashboard({ session }) {
|
|
|
1989
2051
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: t("recommended") }),
|
|
1990
2052
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: t("recommendedHint") }),
|
|
1991
2053
|
session.recommendations.map((rec) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
1992
|
-
rec.useCase.padEnd(12),
|
|
2054
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", children: rec.useCase.padEnd(12) }),
|
|
1993
2055
|
" ",
|
|
1994
2056
|
rec.model.name.padEnd(22),
|
|
1995
2057
|
" ",
|
|
1996
|
-
rec.grade,
|
|
2058
|
+
/* @__PURE__ */ jsx(Text, { color: gradeColor(rec.grade), bold: true, children: rec.grade }),
|
|
1997
2059
|
" ",
|
|
1998
2060
|
GRADE_MEANING[rec.grade]
|
|
1999
2061
|
] }, `${rec.useCase}-${rec.model.id}`)),
|
|
@@ -2005,13 +2067,31 @@ function Dashboard({ session }) {
|
|
|
2005
2067
|
// src/tui/views/Models.tsx
|
|
2006
2068
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
2007
2069
|
|
|
2070
|
+
// src/cli/color.ts
|
|
2071
|
+
var enabled = () => !process.env.NO_COLOR && process.stdout.isTTY;
|
|
2072
|
+
var wrap = (open, close = 39) => (text) => enabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
2073
|
+
var cyan = wrap(36);
|
|
2074
|
+
var green = wrap(32);
|
|
2075
|
+
var yellow = wrap(33);
|
|
2076
|
+
var red = wrap(31);
|
|
2077
|
+
var magenta = wrap(35);
|
|
2078
|
+
var bold = wrap(1, 22);
|
|
2079
|
+
var dim = wrap(2, 22);
|
|
2080
|
+
function gradeAnsi(grade) {
|
|
2081
|
+
if (grade === "S" || grade === "A") return green(grade);
|
|
2082
|
+
if (grade === "B") return yellow(grade);
|
|
2083
|
+
if (grade === "C") return magenta(grade);
|
|
2084
|
+
if (grade === "D" || grade === "F") return red(grade);
|
|
2085
|
+
return grade;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2008
2088
|
// src/cli/text.ts
|
|
2009
2089
|
function hardwareText(session) {
|
|
2010
2090
|
const h = session.hardware;
|
|
2011
2091
|
const ram = formatBytes(h.memory.totalBytes, 0);
|
|
2012
2092
|
const avail = h.memory.availableBytes ? formatBytes(h.memory.availableBytes) : "N/A";
|
|
2013
2093
|
return [
|
|
2014
|
-
t("machine"),
|
|
2094
|
+
bold(cyan(t("machine"))),
|
|
2015
2095
|
` ${h.machineModel ?? t("unknownMachine")}`,
|
|
2016
2096
|
` ${h.cpu.name ?? t("cpuNa")}`,
|
|
2017
2097
|
` CPU ${h.cpu.physicalCores ?? "N/A"} cores (${h.cpu.performanceCores ?? "?"}P / ${h.cpu.efficiencyCores ?? "?"}E)`,
|
|
@@ -2023,18 +2103,22 @@ function hardwareText(session) {
|
|
|
2023
2103
|
}
|
|
2024
2104
|
function runtimesText(session) {
|
|
2025
2105
|
return [
|
|
2026
|
-
t("runtimes"),
|
|
2027
|
-
...session.runtimes.map((r) =>
|
|
2106
|
+
bold(cyan(t("runtimes"))),
|
|
2107
|
+
...session.runtimes.map((r) => {
|
|
2108
|
+
const mark = r.detected ? green("\u2713") : dim("\u25CB");
|
|
2109
|
+
const state = r.detected ? green(r.version ?? t("detected")) : dim(t("notDetected"));
|
|
2110
|
+
return ` ${mark} ${r.label.padEnd(14)} ${state}`;
|
|
2111
|
+
})
|
|
2028
2112
|
].join("\n");
|
|
2029
2113
|
}
|
|
2030
2114
|
function modelsText(session) {
|
|
2031
|
-
const lines = [t("installedModels"), "MODEL INSTALLED FIT EST. SPEED MEASURED"];
|
|
2115
|
+
const lines = [bold(cyan(t("installedModels"))), dim("MODEL INSTALLED FIT EST. SPEED MEASURED")];
|
|
2032
2116
|
for (const row of session.rows) {
|
|
2033
2117
|
const fit = row.compatibility?.grade ?? "\u2014";
|
|
2034
2118
|
const est = row.compatibility?.estimatedTokensPerSecond ? `~${formatNumber(row.compatibility.estimatedTokensPerSecond)} t/s` : "\u2014";
|
|
2035
2119
|
const measured = row.lastBenchmark?.benchmark.generationTokensPerSecond ? formatTokensPerSec(row.lastBenchmark.benchmark.generationTokensPerSecond) : t("notTested");
|
|
2036
2120
|
lines.push(
|
|
2037
|
-
`${row.local.name.padEnd(22)} \u2713 ${fit.padEnd(9)} ${est.padEnd(14)} ${measured}`
|
|
2121
|
+
`${green(row.local.name.padEnd(22))} \u2713 ${gradeAnsi(fit).padEnd(9)} ${est.padEnd(14)} ${measured}`
|
|
2038
2122
|
);
|
|
2039
2123
|
}
|
|
2040
2124
|
lines.push("", t("compatible"));
|
|
@@ -2046,12 +2130,19 @@ function modelsText(session) {
|
|
|
2046
2130
|
return lines.join("\n");
|
|
2047
2131
|
}
|
|
2048
2132
|
function recommendText(session) {
|
|
2049
|
-
const lines = [t("recommended"), t("recommendedHint"), "", t("credits"), ""];
|
|
2133
|
+
const lines = [bold(cyan(t("recommended"))), dim(t("recommendedHint")), "", dim(t("credits")), ""];
|
|
2050
2134
|
for (const rec of session.recommendations) {
|
|
2051
2135
|
lines.push(
|
|
2052
|
-
`${rec.useCase.padEnd(12)} ${rec.model.name.padEnd(22)} ${rec.grade} ${GRADE_MEANING[rec.grade]} ~${na(rec.estimatedTokensPerSecond)} t/s est.`
|
|
2136
|
+
`${magenta(rec.useCase.padEnd(12))} ${rec.model.name.padEnd(22)} ${gradeAnsi(rec.grade)} ${GRADE_MEANING[rec.grade]} ~${na(rec.estimatedTokensPerSecond)} t/s est.`
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
lines.push("", bold(yellow(t("agentsTitle"))));
|
|
2140
|
+
for (const agent of session.agents) {
|
|
2141
|
+
lines.push(
|
|
2142
|
+
` ${agent.detected ? green("\u2713") : dim("\u25CB")} ${agent.label.padEnd(12)} ${agent.detected ? green(t("detected")) : dim(t("agentMissing"))}`
|
|
2053
2143
|
);
|
|
2054
2144
|
}
|
|
2145
|
+
lines.push("", dim(t("recommendCliHint")));
|
|
2055
2146
|
return lines.join("\n");
|
|
2056
2147
|
}
|
|
2057
2148
|
function historyText(records) {
|
|
@@ -2087,6 +2178,7 @@ function doctorText(session) {
|
|
|
2087
2178
|
`${session.hardware.gpu.metal ? "\u2713" : "\u25CB"} Metal`,
|
|
2088
2179
|
...session.runtimes.map((r) => `${r.detected ? "\u2713" : "\u25CB"} ${r.label.padEnd(14)} ${r.version ?? ""}`.trimEnd()),
|
|
2089
2180
|
`${session.networkUsed ? "\u2713" : "\u25CB"} CanIRun.ai (midudev)`,
|
|
2181
|
+
...session.agents.map((a) => `${a.detected ? "\u2713" : "\u25CB"} ${a.label.padEnd(14)} ${a.detected ? t("detected") : t("agentMissing")}`),
|
|
2090
2182
|
"",
|
|
2091
2183
|
session.llamaBench ? t("doctorReady", { count: session.models.filter((m) => m.artifactPath).length }) : t("doctorNoBench")
|
|
2092
2184
|
];
|
|
@@ -2187,10 +2279,264 @@ function HardwareView({ session }) {
|
|
|
2187
2279
|
}
|
|
2188
2280
|
|
|
2189
2281
|
// src/tui/views/Recommend.tsx
|
|
2190
|
-
import { Box as Box4, Text as Text4 } from "ink";
|
|
2191
|
-
import {
|
|
2282
|
+
import { Box as Box4, Text as Text4, useApp, useInput } from "ink";
|
|
2283
|
+
import { useState } from "react";
|
|
2284
|
+
|
|
2285
|
+
// src/integrations/catalog.ts
|
|
2286
|
+
var INTEGRATIONS = {
|
|
2287
|
+
opencode: {
|
|
2288
|
+
id: "opencode",
|
|
2289
|
+
docsUrl: "https://docs.ollama.com/integrations/opencode",
|
|
2290
|
+
ollamaLaunch: "opencode",
|
|
2291
|
+
minGenerationTps: 12,
|
|
2292
|
+
allowedGrades: ["S", "A", "B"],
|
|
2293
|
+
useCases: ["code", "chat", "reasoning"]
|
|
2294
|
+
},
|
|
2295
|
+
openclaw: {
|
|
2296
|
+
id: "openclaw",
|
|
2297
|
+
docsUrl: "https://docs.ollama.com/integrations/openclaw",
|
|
2298
|
+
ollamaLaunch: "openclaw",
|
|
2299
|
+
minGenerationTps: 12,
|
|
2300
|
+
allowedGrades: ["S", "A", "B"],
|
|
2301
|
+
useCases: ["code", "chat", "reasoning"]
|
|
2302
|
+
},
|
|
2303
|
+
hermes: {
|
|
2304
|
+
id: "hermes",
|
|
2305
|
+
docsUrl: "https://docs.ollama.com/integrations/hermes",
|
|
2306
|
+
ollamaLaunch: "hermes",
|
|
2307
|
+
minGenerationTps: 12,
|
|
2308
|
+
allowedGrades: ["S", "A"],
|
|
2309
|
+
useCases: ["code", "chat", "reasoning"]
|
|
2310
|
+
},
|
|
2311
|
+
claude: {
|
|
2312
|
+
id: "claude",
|
|
2313
|
+
docsUrl: "https://docs.ollama.com/integrations/claude-code",
|
|
2314
|
+
ollamaLaunch: "claude",
|
|
2315
|
+
minGenerationTps: 12,
|
|
2316
|
+
allowedGrades: ["S", "A", "B"],
|
|
2317
|
+
useCases: ["code", "chat", "reasoning"]
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
// src/integrations/types.ts
|
|
2322
|
+
var INTEGRATION_IDS = ["opencode", "openclaw", "hermes", "claude"];
|
|
2323
|
+
|
|
2324
|
+
// src/integrations/decide.ts
|
|
2325
|
+
var GRADE_RANK = { S: 5, A: 4, B: 3, C: 2, D: 1, F: 0 };
|
|
2326
|
+
function toOllamaTag(id) {
|
|
2327
|
+
const compact = id.toLowerCase().replace(/_/g, "-");
|
|
2328
|
+
const match = compact.match(/^([a-z0-9.]+(?:-[a-z0-9.]+)*)-(\d+(?:\.\d+)?b)$/i);
|
|
2329
|
+
if (match) return `${match[1]}:${match[2]}`;
|
|
2330
|
+
if (compact.includes(":")) return compact;
|
|
2331
|
+
return compact;
|
|
2332
|
+
}
|
|
2333
|
+
function useCaseOk(useCases, needed) {
|
|
2334
|
+
if (!useCases?.length) return needed.includes("chat");
|
|
2335
|
+
return useCases.some((u) => needed.some((n) => u.includes(n)));
|
|
2336
|
+
}
|
|
2337
|
+
function decideLaunch(session, id) {
|
|
2338
|
+
const def = INTEGRATIONS[id];
|
|
2339
|
+
const reasons = [];
|
|
2340
|
+
const ollama = session.runtimes.find((r) => r.id === "ollama")?.detected;
|
|
2341
|
+
if (!ollama) reasons.push(t("launchNeedOllama"));
|
|
2342
|
+
const candidates = [];
|
|
2343
|
+
for (const row of session.rows) {
|
|
2344
|
+
const useCases = row.catalog?.useCase ?? ["chat", "code"];
|
|
2345
|
+
if (!useCaseOk(useCases, def.useCases)) continue;
|
|
2346
|
+
const tps = row.lastBenchmark?.benchmark.generationTokensPerSecond;
|
|
2347
|
+
candidates.push({
|
|
2348
|
+
modelId: row.local.id,
|
|
2349
|
+
ollamaTag: row.local.id.includes(":") ? row.local.id : toOllamaTag(row.local.id),
|
|
2350
|
+
installed: true,
|
|
2351
|
+
origin: row.lastBenchmark ? "measured" : "estimated",
|
|
2352
|
+
grade: row.compatibility?.grade,
|
|
2353
|
+
tps
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
if (!candidates.length) {
|
|
2357
|
+
for (const model of session.catalog) {
|
|
2358
|
+
if (!useCaseOk(model.useCase, def.useCases)) continue;
|
|
2359
|
+
const fit = localCompatibility(session.hardware, model);
|
|
2360
|
+
if (!def.allowedGrades.includes(fit.grade)) continue;
|
|
2361
|
+
candidates.push({
|
|
2362
|
+
modelId: model.id,
|
|
2363
|
+
ollamaTag: toOllamaTag(model.id),
|
|
2364
|
+
installed: false,
|
|
2365
|
+
origin: "estimated",
|
|
2366
|
+
grade: fit.grade
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
candidates.sort((a, b) => {
|
|
2371
|
+
if (a.installed !== b.installed) return a.installed ? -1 : 1;
|
|
2372
|
+
return (GRADE_RANK[b.grade ?? "F"] ?? 0) - (GRADE_RANK[a.grade ?? "F"] ?? 0);
|
|
2373
|
+
});
|
|
2374
|
+
const pick = candidates[0];
|
|
2375
|
+
if (!pick) reasons.push(t("launchNoModel"));
|
|
2376
|
+
if (pick?.grade && !def.allowedGrades.includes(pick.grade)) {
|
|
2377
|
+
reasons.push(t("launchGradeFail", { grade: pick.grade, allowed: def.allowedGrades.join(",") }));
|
|
2378
|
+
}
|
|
2379
|
+
if (pick?.origin === "measured" && pick.tps !== void 0 && pick.tps < def.minGenerationTps) {
|
|
2380
|
+
reasons.push(t("launchSlowFail", { tps: pick.tps.toFixed(1), min: def.minGenerationTps }));
|
|
2381
|
+
}
|
|
2382
|
+
if (pick && pick.origin === "estimated" && (!pick.grade || GRADE_RANK[pick.grade] < GRADE_RANK.B)) {
|
|
2383
|
+
reasons.push(t("launchEstimateWeak"));
|
|
2384
|
+
}
|
|
2385
|
+
const eligible = Boolean(ollama && pick && reasons.length === 0);
|
|
2386
|
+
const command = pick ? `ollama launch ${def.ollamaLaunch} --model ${pick.ollamaTag}` : `ollama launch ${def.ollamaLaunch}`;
|
|
2387
|
+
return {
|
|
2388
|
+
integration: id,
|
|
2389
|
+
docsUrl: def.docsUrl,
|
|
2390
|
+
eligible,
|
|
2391
|
+
reasons: eligible ? [t("launchOk")] : reasons,
|
|
2392
|
+
modelId: pick?.modelId,
|
|
2393
|
+
ollamaTag: pick?.ollamaTag,
|
|
2394
|
+
installed: pick?.installed ?? false,
|
|
2395
|
+
origin: pick?.origin,
|
|
2396
|
+
grade: pick?.grade,
|
|
2397
|
+
command
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function decideAll(session) {
|
|
2401
|
+
return INTEGRATION_IDS.map((id) => decideLaunch(session, id));
|
|
2402
|
+
}
|
|
2403
|
+
function launchText(decisions) {
|
|
2404
|
+
const lines = [t("launchTitle"), t("launchHint"), ""];
|
|
2405
|
+
for (const d of decisions) {
|
|
2406
|
+
lines.push(`${d.integration} ${d.docsUrl}`);
|
|
2407
|
+
lines.push(` ${d.eligible ? "\u2713" : "\u25CB"} ${d.reasons.join(" ")}`);
|
|
2408
|
+
if (d.modelId) {
|
|
2409
|
+
lines.push(
|
|
2410
|
+
` ${t("launchModel")}: ${d.modelId} ${d.grade ?? "\u2014"} ${d.origin === "measured" ? t("measured") : t("estimated")} ${d.installed ? t("tasksInstalled") : t("tasksNotInstalled")}`
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
lines.push(` ${d.command}`);
|
|
2414
|
+
if (d.eligible) lines.push(` ${t("launchRunHint", { tool: d.integration })}`);
|
|
2415
|
+
else lines.push(` ${t("launchBlocked")}`);
|
|
2416
|
+
lines.push("");
|
|
2417
|
+
}
|
|
2418
|
+
return lines.join("\n");
|
|
2419
|
+
}
|
|
2420
|
+
function parseIntegration(raw) {
|
|
2421
|
+
if (!raw) return void 0;
|
|
2422
|
+
const id = raw.toLowerCase();
|
|
2423
|
+
if (id === "claude-code") return "claude";
|
|
2424
|
+
return INTEGRATION_IDS.find((item) => item === id);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/integrations/execute.ts
|
|
2428
|
+
async function executeLaunch(decision) {
|
|
2429
|
+
if (!decision.eligible || !decision.ollamaTag) {
|
|
2430
|
+
return { ok: false, log: t("launchBlocked") };
|
|
2431
|
+
}
|
|
2432
|
+
const def = INTEGRATIONS[decision.integration];
|
|
2433
|
+
if (!def) return { ok: false, log: t("launchUnknown") };
|
|
2434
|
+
const lines = [];
|
|
2435
|
+
if (!decision.installed) {
|
|
2436
|
+
lines.push(t("launchPulling", { model: decision.ollamaTag }));
|
|
2437
|
+
const pull = await runCommand("ollama", ["pull", decision.ollamaTag], { timeout: 30 * 6e4 });
|
|
2438
|
+
if (pull.exitCode !== 0) {
|
|
2439
|
+
return { ok: false, log: `${lines.join("\n")}
|
|
2440
|
+
${pull.stderr || t("launchPullFail")}` };
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
const args2 = ["launch", def.ollamaLaunch, "--model", decision.ollamaTag];
|
|
2444
|
+
if (def.id === "openclaw" || def.id === "claude") args2.push("--yes");
|
|
2445
|
+
lines.push(`ollama ${args2.join(" ")}`);
|
|
2446
|
+
const launched = await runCommand("ollama", args2, { timeout: 12e4 });
|
|
2447
|
+
if (launched.exitCode !== 0) {
|
|
2448
|
+
return {
|
|
2449
|
+
ok: false,
|
|
2450
|
+
log: `${lines.join("\n")}
|
|
2451
|
+
${launched.stderr || launched.stdout || t("launchExecFail")}`
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
return { ok: true, log: `${lines.join("\n")}
|
|
2455
|
+
${launched.stdout}`.trim() };
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// src/integrations/pull.ts
|
|
2459
|
+
var INSTALLABLE = ["S", "A", "B"];
|
|
2460
|
+
function canInstallGrade(grade) {
|
|
2461
|
+
return Boolean(grade && INSTALLABLE.includes(grade));
|
|
2462
|
+
}
|
|
2463
|
+
async function pullOllamaModel(modelId) {
|
|
2464
|
+
const tag = toOllamaTag(modelId);
|
|
2465
|
+
const result = await runCommand("ollama", ["pull", tag], { timeout: 30 * 6e4 });
|
|
2466
|
+
if (result.exitCode !== 0) {
|
|
2467
|
+
return { ok: false, log: result.stderr || t("launchPullFail"), tag };
|
|
2468
|
+
}
|
|
2469
|
+
return { ok: true, log: t("modelPulled", { model: tag }), tag };
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/tui/views/Recommend.tsx
|
|
2473
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2192
2474
|
function RecommendView({ session }) {
|
|
2193
|
-
|
|
2475
|
+
const { exit } = useApp();
|
|
2476
|
+
const [cursor, setCursor] = useState(0);
|
|
2477
|
+
const [log, setLog] = useState("");
|
|
2478
|
+
const [busy, setBusy] = useState(false);
|
|
2479
|
+
const recs = session.recommendations;
|
|
2480
|
+
useInput((input, key) => {
|
|
2481
|
+
if (busy) return;
|
|
2482
|
+
if (key.escape) return;
|
|
2483
|
+
if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
|
|
2484
|
+
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) {
|
|
2487
|
+
if (!canInstallGrade(rec.grade)) {
|
|
2488
|
+
setLog(t("installGradeBlock", { grade: rec.grade }));
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
setBusy(true);
|
|
2492
|
+
setLog(t("launchPulling", { model: rec.model.id }));
|
|
2493
|
+
void pullOllamaModel(rec.model.id).then((result) => {
|
|
2494
|
+
setLog(result.log);
|
|
2495
|
+
setBusy(false);
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
const map = { o: "opencode", e: "hermes", w: "openclaw" };
|
|
2499
|
+
const tool = map[input.toLowerCase()];
|
|
2500
|
+
if (tool && rec) {
|
|
2501
|
+
const decision = decideLaunch(session, tool);
|
|
2502
|
+
if (!decision.eligible) {
|
|
2503
|
+
setLog(decision.reasons.join(" "));
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
setBusy(true);
|
|
2507
|
+
setLog(t("linkingTool", { tool, model: rec.model.id }));
|
|
2508
|
+
void executeLaunch({ ...decision, ollamaTag: decision.ollamaTag, modelId: rec.model.id }).then((result) => {
|
|
2509
|
+
setLog(result.log);
|
|
2510
|
+
setBusy(false);
|
|
2511
|
+
if (result.ok) exit();
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
});
|
|
2515
|
+
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: [
|
|
2520
|
+
i === cursor ? "\u276F " : " ",
|
|
2521
|
+
/* @__PURE__ */ jsx4(Text4, { color: "magenta", children: rec.useCase.padEnd(12) }),
|
|
2522
|
+
rec.model.name.padEnd(22),
|
|
2523
|
+
" ",
|
|
2524
|
+
/* @__PURE__ */ jsx4(Text4, { color: gradeColor(rec.grade), bold: true, children: rec.grade }),
|
|
2525
|
+
" ",
|
|
2526
|
+
GRADE_MEANING[rec.grade]
|
|
2527
|
+
] }, `${rec.useCase}-${rec.model.id}`)),
|
|
2528
|
+
/* @__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: [
|
|
2531
|
+
agent.detected ? "\u2713" : "\u25CB",
|
|
2532
|
+
" ",
|
|
2533
|
+
agent.label.padEnd(12),
|
|
2534
|
+
" ",
|
|
2535
|
+
agent.detected ? t("detected") : t("agentMissing")
|
|
2536
|
+
] }, agent.id))
|
|
2537
|
+
] }),
|
|
2538
|
+
log ? /* @__PURE__ */ jsx4(Text4, { color: "green", children: log }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("credits") })
|
|
2539
|
+
] });
|
|
2194
2540
|
}
|
|
2195
2541
|
|
|
2196
2542
|
// src/tui/views/Compare.tsx
|
|
@@ -2201,35 +2547,19 @@ function CompareView({ session }) {
|
|
|
2201
2547
|
}
|
|
2202
2548
|
|
|
2203
2549
|
// src/tui/views/Benchmark.tsx
|
|
2204
|
-
import { Box as Box6, Text as Text6, useInput } from "ink";
|
|
2205
|
-
import { useEffect, useRef, useState } from "react";
|
|
2206
|
-
|
|
2207
|
-
// src/tui/theme.ts
|
|
2208
|
-
function bar(percent, width = 10) {
|
|
2209
|
-
if (percent === void 0 || Number.isNaN(percent)) return `${"\u2591".repeat(width)} N/A`;
|
|
2210
|
-
const filled = Math.max(0, Math.min(width, Math.round(percent / 100 * width)));
|
|
2211
|
-
return `${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}`;
|
|
2212
|
-
}
|
|
2213
|
-
function sparkline(values, width = 24) {
|
|
2214
|
-
const glyphs = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
2215
|
-
if (!values.length) return glyphs[0].repeat(width);
|
|
2216
|
-
const slice = values.slice(-width);
|
|
2217
|
-
const max = Math.max(...slice, 1);
|
|
2218
|
-
return slice.map((v) => glyphs[Math.min(glyphs.length - 1, Math.floor(v / max * (glyphs.length - 1)))]).join("").padStart(width, glyphs[0]);
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
// src/tui/views/Benchmark.tsx
|
|
2222
|
-
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2550
|
+
import { Box as Box6, Text as Text6, useInput as useInput2 } from "ink";
|
|
2551
|
+
import { useEffect, useRef, useState as useState2 } from "react";
|
|
2552
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2223
2553
|
function BenchmarkView(props) {
|
|
2224
|
-
const [phase, setPhase] =
|
|
2225
|
-
const [sample, setSample] =
|
|
2226
|
-
const [elapsed, setElapsed] =
|
|
2227
|
-
const [result, setResult] =
|
|
2228
|
-
const [error, setError] =
|
|
2554
|
+
const [phase, setPhase] = useState2(props.models.length ? "select" : "error");
|
|
2555
|
+
const [sample, setSample] = useState2();
|
|
2556
|
+
const [elapsed, setElapsed] = useState2(0);
|
|
2557
|
+
const [result, setResult] = useState2();
|
|
2558
|
+
const [error, setError] = useState2(props.models.length ? "" : t("noGguf"));
|
|
2229
2559
|
const cpuHist = useRef([]);
|
|
2230
2560
|
const memHist = useRef([]);
|
|
2231
2561
|
const abortRef = useRef(void 0);
|
|
2232
|
-
|
|
2562
|
+
useInput2((input, key) => {
|
|
2233
2563
|
if (phase === "select") {
|
|
2234
2564
|
if (key.upArrow) props.setSelected(Math.max(0, props.selected - 1));
|
|
2235
2565
|
if (key.downArrow) props.setSelected(Math.min(props.models.length - 1, props.selected + 1));
|
|
@@ -2279,9 +2609,9 @@ function BenchmarkView(props) {
|
|
|
2279
2609
|
const h = props.session.hardware;
|
|
2280
2610
|
const model = props.models[props.selected];
|
|
2281
2611
|
if (phase === "select") {
|
|
2282
|
-
return /* @__PURE__ */
|
|
2612
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
2283
2613
|
/* @__PURE__ */ jsx6(Text6, { bold: true, children: t("selectBenchmark") }),
|
|
2284
|
-
props.models.map((m, i) => /* @__PURE__ */
|
|
2614
|
+
props.models.map((m, i) => /* @__PURE__ */ jsxs5(Text6, { color: i === props.selected ? "cyan" : void 0, children: [
|
|
2285
2615
|
i === props.selected ? "\u276F " : " ",
|
|
2286
2616
|
m.name
|
|
2287
2617
|
] }, m.id)),
|
|
@@ -2289,9 +2619,9 @@ function BenchmarkView(props) {
|
|
|
2289
2619
|
] });
|
|
2290
2620
|
}
|
|
2291
2621
|
if (phase === "done" && result) {
|
|
2292
|
-
return /* @__PURE__ */
|
|
2622
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
2293
2623
|
/* @__PURE__ */ jsx6(Text6, { children: resultText(result.record, result.assessment) }),
|
|
2294
|
-
/* @__PURE__ */
|
|
2624
|
+
/* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
2295
2625
|
t("saved"),
|
|
2296
2626
|
" ",
|
|
2297
2627
|
result.path
|
|
@@ -2305,11 +2635,11 @@ function BenchmarkView(props) {
|
|
|
2305
2635
|
const gpu = sample?.gpu?.utilizationPercent;
|
|
2306
2636
|
const memUsed = sample?.memory.usedBytes;
|
|
2307
2637
|
const memTotal = h.memory.totalBytes;
|
|
2308
|
-
return /* @__PURE__ */
|
|
2309
|
-
/* @__PURE__ */
|
|
2638
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
2639
|
+
/* @__PURE__ */ jsxs5(Box6, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [
|
|
2310
2640
|
/* @__PURE__ */ jsx6(Text6, { bold: true, children: t("benchmarkTitle") }),
|
|
2311
2641
|
/* @__PURE__ */ jsx6(Text6, { children: model?.name }),
|
|
2312
|
-
/* @__PURE__ */
|
|
2642
|
+
/* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
2313
2643
|
h.cpu.name,
|
|
2314
2644
|
" \xB7 ",
|
|
2315
2645
|
formatBytes(h.memory.totalBytes, 0),
|
|
@@ -2317,9 +2647,9 @@ function BenchmarkView(props) {
|
|
|
2317
2647
|
memoryLabel(h)
|
|
2318
2648
|
] })
|
|
2319
2649
|
] }),
|
|
2320
|
-
/* @__PURE__ */
|
|
2650
|
+
/* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
2321
2651
|
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: t("performance") }),
|
|
2322
|
-
/* @__PURE__ */
|
|
2652
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2323
2653
|
t("elapsed"),
|
|
2324
2654
|
" ",
|
|
2325
2655
|
elapsed.toFixed(1),
|
|
@@ -2327,21 +2657,21 @@ function BenchmarkView(props) {
|
|
|
2327
2657
|
] }),
|
|
2328
2658
|
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: t("measuredAfter") })
|
|
2329
2659
|
] }),
|
|
2330
|
-
/* @__PURE__ */
|
|
2660
|
+
/* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
2331
2661
|
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: t("system") }),
|
|
2332
|
-
/* @__PURE__ */
|
|
2662
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2333
2663
|
"CPU ",
|
|
2334
2664
|
bar(cpu),
|
|
2335
2665
|
" ",
|
|
2336
2666
|
formatPercent(cpu)
|
|
2337
2667
|
] }),
|
|
2338
|
-
/* @__PURE__ */
|
|
2668
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2339
2669
|
"GPU ",
|
|
2340
2670
|
bar(gpu),
|
|
2341
2671
|
" ",
|
|
2342
2672
|
formatPercent(gpu)
|
|
2343
2673
|
] }),
|
|
2344
|
-
/* @__PURE__ */
|
|
2674
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2345
2675
|
"Memory ",
|
|
2346
2676
|
bar(memUsed && memTotal ? memUsed / memTotal * 100 : void 0),
|
|
2347
2677
|
" ",
|
|
@@ -2349,36 +2679,36 @@ function BenchmarkView(props) {
|
|
|
2349
2679
|
" / ",
|
|
2350
2680
|
formatBytes(memTotal, 0)
|
|
2351
2681
|
] }),
|
|
2352
|
-
/* @__PURE__ */
|
|
2682
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2353
2683
|
"Swap ",
|
|
2354
2684
|
bar(0),
|
|
2355
2685
|
" ",
|
|
2356
2686
|
sample ? formatBytes(sample.memory.swapUsedBytes) : "N/A"
|
|
2357
2687
|
] })
|
|
2358
2688
|
] }),
|
|
2359
|
-
/* @__PURE__ */
|
|
2689
|
+
/* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
2360
2690
|
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: t("powerThermals") }),
|
|
2361
|
-
/* @__PURE__ */
|
|
2691
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2362
2692
|
"Package ",
|
|
2363
2693
|
sample?.packagePowerWatts ?? "N/A",
|
|
2364
2694
|
" W"
|
|
2365
2695
|
] }),
|
|
2366
|
-
/* @__PURE__ */
|
|
2696
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2367
2697
|
"Temperature ",
|
|
2368
2698
|
sample?.thermal?.temperatureC ?? "N/A",
|
|
2369
2699
|
" \xB0C"
|
|
2370
2700
|
] }),
|
|
2371
|
-
/* @__PURE__ */
|
|
2701
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2372
2702
|
"Thermal Pressure ",
|
|
2373
2703
|
sample?.thermal?.pressure ?? "N/A"
|
|
2374
2704
|
] }),
|
|
2375
2705
|
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: t("permissionsHint") })
|
|
2376
2706
|
] }),
|
|
2377
|
-
/* @__PURE__ */
|
|
2707
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2378
2708
|
sparkline(cpuHist.current),
|
|
2379
2709
|
" CPU"
|
|
2380
2710
|
] }),
|
|
2381
|
-
/* @__PURE__ */
|
|
2711
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2382
2712
|
sparkline(memHist.current.map((v) => v / 1024 ** 3)),
|
|
2383
2713
|
" Memory"
|
|
2384
2714
|
] }),
|
|
@@ -2388,17 +2718,17 @@ function BenchmarkView(props) {
|
|
|
2388
2718
|
|
|
2389
2719
|
// src/tui/views/History.tsx
|
|
2390
2720
|
import { Box as Box7, Text as Text7 } from "ink";
|
|
2391
|
-
import { jsx as jsx7, jsxs as
|
|
2721
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2392
2722
|
function HistoryView({ session }) {
|
|
2393
|
-
return /* @__PURE__ */
|
|
2723
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
|
|
2394
2724
|
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: t("measured") }),
|
|
2395
2725
|
/* @__PURE__ */ jsx7(Text7, { children: historyText(session.history) })
|
|
2396
2726
|
] });
|
|
2397
2727
|
}
|
|
2398
2728
|
|
|
2399
2729
|
// src/tui/views/Tasks.tsx
|
|
2400
|
-
import { Box as Box8, Text as Text8, useInput as
|
|
2401
|
-
import { useMemo, useState as
|
|
2730
|
+
import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
|
|
2731
|
+
import { useMemo, useState as useState3 } from "react";
|
|
2402
2732
|
|
|
2403
2733
|
// src/tasks/catalog.ts
|
|
2404
2734
|
var TASK_CATALOG = [
|
|
@@ -2593,7 +2923,7 @@ function tasksText(plans, answers) {
|
|
|
2593
2923
|
}
|
|
2594
2924
|
|
|
2595
2925
|
// src/tui/views/Tasks.tsx
|
|
2596
|
-
import { Fragment, jsx as jsx8, jsxs as
|
|
2926
|
+
import { Fragment, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2597
2927
|
var KIND_LABEL = {
|
|
2598
2928
|
code: "tasksOptCode",
|
|
2599
2929
|
video: "tasksOptVideo",
|
|
@@ -2602,16 +2932,16 @@ var KIND_LABEL = {
|
|
|
2602
2932
|
chat: "tasksOptChat"
|
|
2603
2933
|
};
|
|
2604
2934
|
function TasksView({ session }) {
|
|
2605
|
-
const [step, setStep] =
|
|
2606
|
-
const [cursor, setCursor] =
|
|
2607
|
-
const [kinds, setKinds] =
|
|
2608
|
-
const [scope, setScope] =
|
|
2609
|
-
const [priority, setPriority] =
|
|
2935
|
+
const [step, setStep] = useState3(0);
|
|
2936
|
+
const [cursor, setCursor] = useState3(0);
|
|
2937
|
+
const [kinds, setKinds] = useState3(["code", "chat"]);
|
|
2938
|
+
const [scope, setScope] = useState3("installed");
|
|
2939
|
+
const [priority, setPriority] = useState3("balanced");
|
|
2610
2940
|
const plans = useMemo(
|
|
2611
2941
|
() => planTasks(session, { kinds, scope, priority }),
|
|
2612
2942
|
[session, kinds, scope, priority]
|
|
2613
2943
|
);
|
|
2614
|
-
|
|
2944
|
+
useInput3((input, key) => {
|
|
2615
2945
|
if (step === 0) {
|
|
2616
2946
|
if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
|
|
2617
2947
|
if (key.downArrow) setCursor((c) => Math.min(WORK_KINDS.length - 1, c + 1));
|
|
@@ -2636,11 +2966,11 @@ function TasksView({ session }) {
|
|
|
2636
2966
|
if (step === 3) {
|
|
2637
2967
|
return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsx8(Text8, { children: tasksText(plans, { kinds, scope, priority }) }) });
|
|
2638
2968
|
}
|
|
2639
|
-
return /* @__PURE__ */
|
|
2969
|
+
return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
|
|
2640
2970
|
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("tasksTitle") }),
|
|
2641
|
-
step === 0 && /* @__PURE__ */
|
|
2971
|
+
step === 0 && /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
2642
2972
|
/* @__PURE__ */ jsx8(Text8, { children: t("tasksQ1") }),
|
|
2643
|
-
WORK_KINDS.map((kind, i) => /* @__PURE__ */
|
|
2973
|
+
WORK_KINDS.map((kind, i) => /* @__PURE__ */ jsxs7(Text8, { color: i === cursor ? "cyan" : void 0, children: [
|
|
2644
2974
|
i === cursor ? "\u276F " : " ",
|
|
2645
2975
|
"[",
|
|
2646
2976
|
kinds.includes(kind) ? "x" : " ",
|
|
@@ -2648,20 +2978,20 @@ function TasksView({ session }) {
|
|
|
2648
2978
|
t(KIND_LABEL[kind])
|
|
2649
2979
|
] }, kind))
|
|
2650
2980
|
] }),
|
|
2651
|
-
step === 1 && /* @__PURE__ */
|
|
2981
|
+
step === 1 && /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
2652
2982
|
/* @__PURE__ */ jsx8(Text8, { children: t("tasksQ2") }),
|
|
2653
|
-
/* @__PURE__ */
|
|
2983
|
+
/* @__PURE__ */ jsxs7(Text8, { color: scope === "installed" ? "cyan" : void 0, children: [
|
|
2654
2984
|
scope === "installed" ? "\u276F " : " ",
|
|
2655
2985
|
t("tasksOptInstalled")
|
|
2656
2986
|
] }),
|
|
2657
|
-
/* @__PURE__ */
|
|
2987
|
+
/* @__PURE__ */ jsxs7(Text8, { color: scope === "all" ? "cyan" : void 0, children: [
|
|
2658
2988
|
scope === "all" ? "\u276F " : " ",
|
|
2659
2989
|
t("tasksOptAll")
|
|
2660
2990
|
] })
|
|
2661
2991
|
] }),
|
|
2662
|
-
step === 2 && /* @__PURE__ */
|
|
2992
|
+
step === 2 && /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
2663
2993
|
/* @__PURE__ */ jsx8(Text8, { children: t("tasksQ3") }),
|
|
2664
|
-
["speed", "balanced", "quality"].map((item) => /* @__PURE__ */
|
|
2994
|
+
["speed", "balanced", "quality"].map((item) => /* @__PURE__ */ jsxs7(Text8, { color: priority === item ? "cyan" : void 0, children: [
|
|
2665
2995
|
priority === item ? "\u276F " : " ",
|
|
2666
2996
|
item === "speed" ? t("tasksOptSpeed") : item === "quality" ? t("tasksOptQuality") : t("tasksOptBalanced")
|
|
2667
2997
|
] }, item))
|
|
@@ -2670,17 +3000,22 @@ function TasksView({ session }) {
|
|
|
2670
3000
|
}
|
|
2671
3001
|
|
|
2672
3002
|
// src/tui/App.tsx
|
|
2673
|
-
import { jsx as jsx9, jsxs as
|
|
3003
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2674
3004
|
function App(props) {
|
|
2675
|
-
const { exit } =
|
|
2676
|
-
const [screen, setScreen] =
|
|
2677
|
-
const [selected, setSelected] =
|
|
3005
|
+
const { exit } = useApp2();
|
|
3006
|
+
const [screen, setScreen] = useState4("home");
|
|
3007
|
+
const [selected, setSelected] = useState4(0);
|
|
2678
3008
|
const benchable = useMemo2(
|
|
2679
3009
|
() => props.session.models.filter((m) => Boolean(m.artifactPath)),
|
|
2680
3010
|
[props.session.models]
|
|
2681
3011
|
);
|
|
2682
|
-
|
|
3012
|
+
useInput4((input, key) => {
|
|
2683
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
|
+
}
|
|
2684
3019
|
const letter = input.toLowerCase();
|
|
2685
3020
|
if (letter === "q" || key.escape) exit();
|
|
2686
3021
|
if (letter === "b") setScreen("benchmark");
|
|
@@ -2692,7 +3027,7 @@ function App(props) {
|
|
|
2692
3027
|
if (letter === "t") setScreen("tasks");
|
|
2693
3028
|
if (key.return && screen === "home") setScreen("benchmark");
|
|
2694
3029
|
});
|
|
2695
|
-
return /* @__PURE__ */
|
|
3030
|
+
return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", padding: 1, children: [
|
|
2696
3031
|
screen === "home" && /* @__PURE__ */ jsx9(Dashboard, { session: props.session }),
|
|
2697
3032
|
screen === "models" && /* @__PURE__ */ jsx9(ModelsView, { session: props.session }),
|
|
2698
3033
|
screen === "hardware" && /* @__PURE__ */ jsx9(HardwareView, { session: props.session }),
|
|
@@ -2722,179 +3057,6 @@ async function renderDashboard(session, preset) {
|
|
|
2722
3057
|
await instance.waitUntilExit();
|
|
2723
3058
|
}
|
|
2724
3059
|
|
|
2725
|
-
// src/integrations/catalog.ts
|
|
2726
|
-
var INTEGRATIONS = {
|
|
2727
|
-
opencode: {
|
|
2728
|
-
id: "opencode",
|
|
2729
|
-
docsUrl: "https://docs.ollama.com/integrations/opencode",
|
|
2730
|
-
ollamaLaunch: "opencode",
|
|
2731
|
-
minGenerationTps: 12,
|
|
2732
|
-
allowedGrades: ["S", "A", "B"],
|
|
2733
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2734
|
-
},
|
|
2735
|
-
openclaw: {
|
|
2736
|
-
id: "openclaw",
|
|
2737
|
-
docsUrl: "https://docs.ollama.com/integrations/openclaw",
|
|
2738
|
-
ollamaLaunch: "openclaw",
|
|
2739
|
-
minGenerationTps: 12,
|
|
2740
|
-
allowedGrades: ["S", "A", "B"],
|
|
2741
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2742
|
-
},
|
|
2743
|
-
hermes: {
|
|
2744
|
-
id: "hermes",
|
|
2745
|
-
docsUrl: "https://docs.ollama.com/integrations/hermes",
|
|
2746
|
-
ollamaLaunch: "hermes",
|
|
2747
|
-
minGenerationTps: 12,
|
|
2748
|
-
allowedGrades: ["S", "A"],
|
|
2749
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2750
|
-
},
|
|
2751
|
-
claude: {
|
|
2752
|
-
id: "claude",
|
|
2753
|
-
docsUrl: "https://docs.ollama.com/integrations/claude-code",
|
|
2754
|
-
ollamaLaunch: "claude",
|
|
2755
|
-
minGenerationTps: 12,
|
|
2756
|
-
allowedGrades: ["S", "A", "B"],
|
|
2757
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2758
|
-
}
|
|
2759
|
-
};
|
|
2760
|
-
|
|
2761
|
-
// src/integrations/types.ts
|
|
2762
|
-
var INTEGRATION_IDS = ["opencode", "openclaw", "hermes", "claude"];
|
|
2763
|
-
|
|
2764
|
-
// src/integrations/decide.ts
|
|
2765
|
-
var GRADE_RANK = { S: 5, A: 4, B: 3, C: 2, D: 1, F: 0 };
|
|
2766
|
-
function toOllamaTag(id) {
|
|
2767
|
-
const compact = id.toLowerCase().replace(/_/g, "-");
|
|
2768
|
-
const match = compact.match(/^([a-z0-9.]+(?:-[a-z0-9.]+)*)-(\d+(?:\.\d+)?b)$/i);
|
|
2769
|
-
if (match) return `${match[1]}:${match[2]}`;
|
|
2770
|
-
if (compact.includes(":")) return compact;
|
|
2771
|
-
return compact;
|
|
2772
|
-
}
|
|
2773
|
-
function useCaseOk(useCases, needed) {
|
|
2774
|
-
if (!useCases?.length) return needed.includes("chat");
|
|
2775
|
-
return useCases.some((u) => needed.some((n) => u.includes(n)));
|
|
2776
|
-
}
|
|
2777
|
-
function decideLaunch(session, id) {
|
|
2778
|
-
const def = INTEGRATIONS[id];
|
|
2779
|
-
const reasons = [];
|
|
2780
|
-
const ollama = session.runtimes.find((r) => r.id === "ollama")?.detected;
|
|
2781
|
-
if (!ollama) reasons.push(t("launchNeedOllama"));
|
|
2782
|
-
const candidates = [];
|
|
2783
|
-
for (const row of session.rows) {
|
|
2784
|
-
const useCases = row.catalog?.useCase ?? ["chat", "code"];
|
|
2785
|
-
if (!useCaseOk(useCases, def.useCases)) continue;
|
|
2786
|
-
const tps = row.lastBenchmark?.benchmark.generationTokensPerSecond;
|
|
2787
|
-
candidates.push({
|
|
2788
|
-
modelId: row.local.id,
|
|
2789
|
-
ollamaTag: row.local.id.includes(":") ? row.local.id : toOllamaTag(row.local.id),
|
|
2790
|
-
installed: true,
|
|
2791
|
-
origin: row.lastBenchmark ? "measured" : "estimated",
|
|
2792
|
-
grade: row.compatibility?.grade,
|
|
2793
|
-
tps
|
|
2794
|
-
});
|
|
2795
|
-
}
|
|
2796
|
-
if (!candidates.length) {
|
|
2797
|
-
for (const model of session.catalog) {
|
|
2798
|
-
if (!useCaseOk(model.useCase, def.useCases)) continue;
|
|
2799
|
-
const fit = localCompatibility(session.hardware, model);
|
|
2800
|
-
if (!def.allowedGrades.includes(fit.grade)) continue;
|
|
2801
|
-
candidates.push({
|
|
2802
|
-
modelId: model.id,
|
|
2803
|
-
ollamaTag: toOllamaTag(model.id),
|
|
2804
|
-
installed: false,
|
|
2805
|
-
origin: "estimated",
|
|
2806
|
-
grade: fit.grade
|
|
2807
|
-
});
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
candidates.sort((a, b) => {
|
|
2811
|
-
if (a.installed !== b.installed) return a.installed ? -1 : 1;
|
|
2812
|
-
return (GRADE_RANK[b.grade ?? "F"] ?? 0) - (GRADE_RANK[a.grade ?? "F"] ?? 0);
|
|
2813
|
-
});
|
|
2814
|
-
const pick = candidates[0];
|
|
2815
|
-
if (!pick) reasons.push(t("launchNoModel"));
|
|
2816
|
-
if (pick?.grade && !def.allowedGrades.includes(pick.grade)) {
|
|
2817
|
-
reasons.push(t("launchGradeFail", { grade: pick.grade, allowed: def.allowedGrades.join(",") }));
|
|
2818
|
-
}
|
|
2819
|
-
if (pick?.origin === "measured" && pick.tps !== void 0 && pick.tps < def.minGenerationTps) {
|
|
2820
|
-
reasons.push(t("launchSlowFail", { tps: pick.tps.toFixed(1), min: def.minGenerationTps }));
|
|
2821
|
-
}
|
|
2822
|
-
if (pick && pick.origin === "estimated" && (!pick.grade || GRADE_RANK[pick.grade] < GRADE_RANK.B)) {
|
|
2823
|
-
reasons.push(t("launchEstimateWeak"));
|
|
2824
|
-
}
|
|
2825
|
-
const eligible = Boolean(ollama && pick && reasons.length === 0);
|
|
2826
|
-
const command = pick ? `ollama launch ${def.ollamaLaunch} --model ${pick.ollamaTag}` : `ollama launch ${def.ollamaLaunch}`;
|
|
2827
|
-
return {
|
|
2828
|
-
integration: id,
|
|
2829
|
-
docsUrl: def.docsUrl,
|
|
2830
|
-
eligible,
|
|
2831
|
-
reasons: eligible ? [t("launchOk")] : reasons,
|
|
2832
|
-
modelId: pick?.modelId,
|
|
2833
|
-
ollamaTag: pick?.ollamaTag,
|
|
2834
|
-
installed: pick?.installed ?? false,
|
|
2835
|
-
origin: pick?.origin,
|
|
2836
|
-
grade: pick?.grade,
|
|
2837
|
-
command
|
|
2838
|
-
};
|
|
2839
|
-
}
|
|
2840
|
-
function decideAll(session) {
|
|
2841
|
-
return INTEGRATION_IDS.map((id) => decideLaunch(session, id));
|
|
2842
|
-
}
|
|
2843
|
-
function launchText(decisions) {
|
|
2844
|
-
const lines = [t("launchTitle"), t("launchHint"), ""];
|
|
2845
|
-
for (const d of decisions) {
|
|
2846
|
-
lines.push(`${d.integration} ${d.docsUrl}`);
|
|
2847
|
-
lines.push(` ${d.eligible ? "\u2713" : "\u25CB"} ${d.reasons.join(" ")}`);
|
|
2848
|
-
if (d.modelId) {
|
|
2849
|
-
lines.push(
|
|
2850
|
-
` ${t("launchModel")}: ${d.modelId} ${d.grade ?? "\u2014"} ${d.origin === "measured" ? t("measured") : t("estimated")} ${d.installed ? t("tasksInstalled") : t("tasksNotInstalled")}`
|
|
2851
|
-
);
|
|
2852
|
-
}
|
|
2853
|
-
lines.push(` ${d.command}`);
|
|
2854
|
-
if (d.eligible) lines.push(` ${t("launchRunHint", { tool: d.integration })}`);
|
|
2855
|
-
else lines.push(` ${t("launchBlocked")}`);
|
|
2856
|
-
lines.push("");
|
|
2857
|
-
}
|
|
2858
|
-
return lines.join("\n");
|
|
2859
|
-
}
|
|
2860
|
-
function parseIntegration(raw) {
|
|
2861
|
-
if (!raw) return void 0;
|
|
2862
|
-
const id = raw.toLowerCase();
|
|
2863
|
-
if (id === "claude-code") return "claude";
|
|
2864
|
-
return INTEGRATION_IDS.find((item) => item === id);
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
// src/integrations/execute.ts
|
|
2868
|
-
async function executeLaunch(decision) {
|
|
2869
|
-
if (!decision.eligible || !decision.ollamaTag) {
|
|
2870
|
-
return { ok: false, log: t("launchBlocked") };
|
|
2871
|
-
}
|
|
2872
|
-
const def = INTEGRATIONS[decision.integration];
|
|
2873
|
-
if (!def) return { ok: false, log: t("launchUnknown") };
|
|
2874
|
-
const lines = [];
|
|
2875
|
-
if (!decision.installed) {
|
|
2876
|
-
lines.push(t("launchPulling", { model: decision.ollamaTag }));
|
|
2877
|
-
const pull = await runCommand("ollama", ["pull", decision.ollamaTag], { timeout: 30 * 6e4 });
|
|
2878
|
-
if (pull.exitCode !== 0) {
|
|
2879
|
-
return { ok: false, log: `${lines.join("\n")}
|
|
2880
|
-
${pull.stderr || t("launchPullFail")}` };
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2883
|
-
const args2 = ["launch", def.ollamaLaunch, "--model", decision.ollamaTag];
|
|
2884
|
-
if (def.id === "openclaw" || def.id === "claude") args2.push("--yes");
|
|
2885
|
-
lines.push(`ollama ${args2.join(" ")}`);
|
|
2886
|
-
const launched = await runCommand("ollama", args2, { timeout: 12e4 });
|
|
2887
|
-
if (launched.exitCode !== 0) {
|
|
2888
|
-
return {
|
|
2889
|
-
ok: false,
|
|
2890
|
-
log: `${lines.join("\n")}
|
|
2891
|
-
${launched.stderr || launched.stdout || t("launchExecFail")}`
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
|
-
return { ok: true, log: `${lines.join("\n")}
|
|
2895
|
-
${launched.stdout}`.trim() };
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
3060
|
// src/cli/help.ts
|
|
2899
3061
|
function helpText() {
|
|
2900
3062
|
return t("help");
|
|
@@ -2953,9 +3115,36 @@ ${runtimesText(session)}`),
|
|
|
2953
3115
|
case "models":
|
|
2954
3116
|
print(args2.json ? session.rows : explained("aboutModels", modelsText(session)), args2.json);
|
|
2955
3117
|
return 0;
|
|
2956
|
-
case "recommend":
|
|
2957
|
-
|
|
3118
|
+
case "recommend": {
|
|
3119
|
+
if (args2.install && args2.yes) {
|
|
3120
|
+
const rec = session.recommendations.find((r) => canInstallGrade(r.grade));
|
|
3121
|
+
if (!rec) {
|
|
3122
|
+
print(t("installNone"), args2.json);
|
|
3123
|
+
return 1;
|
|
3124
|
+
}
|
|
3125
|
+
const pulled = await pullOllamaModel(rec.model.id);
|
|
3126
|
+
print(args2.json ? pulled : explained("aboutRecommend", pulled.log), args2.json);
|
|
3127
|
+
return pulled.ok ? 0 : 1;
|
|
3128
|
+
}
|
|
3129
|
+
if (args2.link && args2.yes) {
|
|
3130
|
+
const tool = parseIntegration(args2.link);
|
|
3131
|
+
if (!tool || tool === "claude") {
|
|
3132
|
+
process.stderr.write(`${t("launchNeedTool")}
|
|
3133
|
+
`);
|
|
3134
|
+
return 1;
|
|
3135
|
+
}
|
|
3136
|
+
const decision = decideLaunch(session, tool);
|
|
3137
|
+
if (!decision.eligible) {
|
|
3138
|
+
print(explained("aboutLaunch", launchText([decision])), args2.json);
|
|
3139
|
+
return 1;
|
|
3140
|
+
}
|
|
3141
|
+
const result = await executeLaunch(decision);
|
|
3142
|
+
print(args2.json ? result : explained("aboutLaunch", result.log), args2.json);
|
|
3143
|
+
return result.ok ? 0 : 1;
|
|
3144
|
+
}
|
|
3145
|
+
print(args2.json ? { recommendations: session.recommendations, agents: session.agents } : explained("aboutRecommend", recommendText(session)), args2.json);
|
|
2958
3146
|
return 0;
|
|
3147
|
+
}
|
|
2959
3148
|
case "launch": {
|
|
2960
3149
|
const tool = parseIntegration(args2.positional[0]);
|
|
2961
3150
|
const decisions = tool ? [decideLaunch(session, tool)] : decideAll(session);
|
|
@@ -3059,10 +3248,10 @@ ${runtimesText(session)}`),
|
|
|
3059
3248
|
// src/cli/index.ts
|
|
3060
3249
|
var args = parseArgs(process.argv);
|
|
3061
3250
|
run(args).then((code) => {
|
|
3062
|
-
process.
|
|
3251
|
+
process.exit(code);
|
|
3063
3252
|
}).catch((error) => {
|
|
3064
3253
|
const message = error instanceof Error ? error.message : String(error);
|
|
3065
3254
|
process.stderr.write(`${message}
|
|
3066
3255
|
`);
|
|
3067
|
-
process.
|
|
3256
|
+
process.exit(1);
|
|
3068
3257
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pudu-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Pudu-AI — local AI hardware & benchmark lab for the terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"author": "devjaime",
|
|
25
25
|
"type": "module",
|
|
26
26
|
"bin": {
|
|
27
|
-
"pudu-ai": "
|
|
27
|
+
"pudu-ai": "dist/cli/index.js"
|
|
28
28
|
},
|
|
29
29
|
"exports": {
|
|
30
30
|
".": "./dist/cli/index.js"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=20"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"build": "
|
|
42
|
+
"build": "npx --yes esbuild src/cli/index.ts --bundle --platform=node --format=esm --outfile=dist/cli/index.js --packages=external --jsx=automatic && node scripts/add-shebang.mjs",
|
|
43
43
|
"dev": "tsx src/cli/index.ts",
|
|
44
44
|
"pudu-ai": "tsx src/cli/index.ts",
|
|
45
45
|
"start": "node dist/cli/index.js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"test:watch": "vitest",
|
|
48
48
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
49
49
|
"lint": "tsc --noEmit -p tsconfig.json",
|
|
50
|
-
"prepublishOnly": "npm run
|
|
50
|
+
"prepublishOnly": "npm run build"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"execa": "^9.6.0",
|