pudu-ai 0.2.3 → 0.2.5
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 +480 -281
- package/package.json +1 -1
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
|
@@ -29,6 +29,7 @@ function parseArgs(argv) {
|
|
|
29
29
|
const forIndex = args2.findIndex((a) => a === "--for");
|
|
30
30
|
const scopeIndex = args2.findIndex((a) => a === "--scope");
|
|
31
31
|
const priorityIndex = args2.findIndex((a) => a === "--priority");
|
|
32
|
+
const linkIndex = args2.findIndex((a) => a === "--link");
|
|
32
33
|
return {
|
|
33
34
|
command: isKnown ? command : "dashboard",
|
|
34
35
|
positional: isKnown ? rest.slice(1) : rest,
|
|
@@ -45,7 +46,9 @@ function parseArgs(argv) {
|
|
|
45
46
|
forKinds: forIndex >= 0 ? args2[forIndex + 1] : void 0,
|
|
46
47
|
scope: scopeIndex >= 0 ? args2[scopeIndex + 1] : void 0,
|
|
47
48
|
priority: priorityIndex >= 0 ? args2[priorityIndex + 1] : void 0,
|
|
48
|
-
yes: flags.has("--yes")
|
|
49
|
+
yes: flags.has("--yes"),
|
|
50
|
+
install: flags.has("--install"),
|
|
51
|
+
link: linkIndex >= 0 ? args2[linkIndex + 1] : void 0
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -843,6 +846,14 @@ var en = {
|
|
|
843
846
|
compatible: "COMPATIBLE (catalog, estimated)",
|
|
844
847
|
recommended: "RECOMMENDED FOR THIS MACHINE",
|
|
845
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.",
|
|
846
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.",
|
|
847
858
|
noModels: "No local models detected",
|
|
848
859
|
notTested: "Not tested",
|
|
@@ -950,6 +961,8 @@ Usage:
|
|
|
950
961
|
npx pudu-ai models
|
|
951
962
|
npx pudu-ai models add-path ~/Models
|
|
952
963
|
npx pudu-ai recommend
|
|
964
|
+
npx pudu-ai recommend --install --yes
|
|
965
|
+
npx pudu-ai recommend --link opencode --yes
|
|
953
966
|
npx pudu-ai tasks
|
|
954
967
|
npx pudu-ai tasks --for code,image --scope all --priority speed
|
|
955
968
|
npx pudu-ai benchmark [model]
|
|
@@ -1005,6 +1018,14 @@ var es = {
|
|
|
1005
1018
|
compatible: "COMPATIBLES (cat\xE1logo, estimado)",
|
|
1006
1019
|
recommended: "RECOMENDADOS PARA ESTE EQUIPO",
|
|
1007
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.",
|
|
1008
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.",
|
|
1009
1030
|
noModels: "No se detectaron modelos locales",
|
|
1010
1031
|
notTested: "Sin probar",
|
|
@@ -1112,6 +1133,8 @@ Uso:
|
|
|
1112
1133
|
npx pudu-ai models
|
|
1113
1134
|
npx pudu-ai models add-path ~/Models
|
|
1114
1135
|
npx pudu-ai recommend
|
|
1136
|
+
npx pudu-ai recommend --install --yes
|
|
1137
|
+
npx pudu-ai recommend --link opencode --yes
|
|
1115
1138
|
npx pudu-ai tasks
|
|
1116
1139
|
npx pudu-ai tasks --for code,image --scope all --priority speed
|
|
1117
1140
|
npx pudu-ai benchmark [model]
|
|
@@ -1358,15 +1381,33 @@ async function listBenchmarks() {
|
|
|
1358
1381
|
return records;
|
|
1359
1382
|
}
|
|
1360
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
|
+
|
|
1361
1401
|
// src/session/load.ts
|
|
1362
1402
|
async function loadSession(options) {
|
|
1363
1403
|
const hardware = await detectHardware();
|
|
1364
|
-
const [runtimes2, models, catalogState, history, llamaBench] = await Promise.all([
|
|
1404
|
+
const [runtimes2, models, catalogState, history, llamaBench, agents] = await Promise.all([
|
|
1365
1405
|
detectRuntimes(),
|
|
1366
1406
|
discoverLocalModels(),
|
|
1367
1407
|
loadCatalog(options.network),
|
|
1368
1408
|
listBenchmarks(),
|
|
1369
|
-
commandExists("llama-bench").then(Boolean)
|
|
1409
|
+
commandExists("llama-bench").then(Boolean),
|
|
1410
|
+
detectAgents()
|
|
1370
1411
|
]);
|
|
1371
1412
|
const rows = [];
|
|
1372
1413
|
for (const local of models) {
|
|
@@ -1385,7 +1426,8 @@ async function loadSession(options) {
|
|
|
1385
1426
|
recommendations,
|
|
1386
1427
|
history,
|
|
1387
1428
|
llamaBench,
|
|
1388
|
-
networkUsed: catalogState.networkUsed
|
|
1429
|
+
networkUsed: catalogState.networkUsed,
|
|
1430
|
+
agents
|
|
1389
1431
|
};
|
|
1390
1432
|
}
|
|
1391
1433
|
|
|
@@ -1885,8 +1927,8 @@ function setLogLevel(next) {
|
|
|
1885
1927
|
import { render } from "ink";
|
|
1886
1928
|
|
|
1887
1929
|
// src/tui/App.tsx
|
|
1888
|
-
import { Box as Box9, Text as Text9, useApp, useInput as
|
|
1889
|
-
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";
|
|
1890
1932
|
|
|
1891
1933
|
// src/tui/views/Dashboard.tsx
|
|
1892
1934
|
import { Box, Text } from "ink";
|
|
@@ -1924,6 +1966,27 @@ var GRADE_MEANING = {
|
|
|
1924
1966
|
F: "Not recommended"
|
|
1925
1967
|
};
|
|
1926
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
|
+
|
|
1927
1990
|
// src/tui/views/Dashboard.tsx
|
|
1928
1991
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1929
1992
|
function Dashboard({ session }) {
|
|
@@ -1963,7 +2026,7 @@ function Dashboard({ session }) {
|
|
|
1963
2026
|
] }),
|
|
1964
2027
|
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1965
2028
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: t("runtimes") }),
|
|
1966
|
-
session.runtimes.map((r) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
2029
|
+
session.runtimes.map((r) => /* @__PURE__ */ jsxs(Text, { color: r.detected ? "green" : "gray", children: [
|
|
1967
2030
|
r.detected ? "\u2713" : "\u25CB",
|
|
1968
2031
|
" ",
|
|
1969
2032
|
r.label.padEnd(14),
|
|
@@ -1975,12 +2038,12 @@ function Dashboard({ session }) {
|
|
|
1975
2038
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: t("installedModels") }),
|
|
1976
2039
|
session.rows.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: t("noModels") }),
|
|
1977
2040
|
session.rows.map((row) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
1978
|
-
row.local.name.padEnd(20),
|
|
2041
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: row.local.name.padEnd(20) }),
|
|
1979
2042
|
" ",
|
|
1980
2043
|
row.local.sizeBytes ? formatBytes(row.local.sizeBytes) : "N/A",
|
|
1981
|
-
"
|
|
1982
|
-
row.compatibility?.grade ?? "\u2014",
|
|
1983
|
-
"
|
|
2044
|
+
" ",
|
|
2045
|
+
/* @__PURE__ */ jsx(Text, { color: gradeColor(row.compatibility?.grade), bold: true, children: row.compatibility?.grade ?? "\u2014" }),
|
|
2046
|
+
" ",
|
|
1984
2047
|
row.lastBenchmark?.benchmark.generationTokensPerSecond ? formatTokensPerSec(row.lastBenchmark.benchmark.generationTokensPerSecond) : t("notTested")
|
|
1985
2048
|
] }, row.local.id))
|
|
1986
2049
|
] }),
|
|
@@ -1988,11 +2051,11 @@ function Dashboard({ session }) {
|
|
|
1988
2051
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: t("recommended") }),
|
|
1989
2052
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: t("recommendedHint") }),
|
|
1990
2053
|
session.recommendations.map((rec) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
1991
|
-
rec.useCase.padEnd(12),
|
|
2054
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", children: rec.useCase.padEnd(12) }),
|
|
1992
2055
|
" ",
|
|
1993
2056
|
rec.model.name.padEnd(22),
|
|
1994
2057
|
" ",
|
|
1995
|
-
rec.grade,
|
|
2058
|
+
/* @__PURE__ */ jsx(Text, { color: gradeColor(rec.grade), bold: true, children: rec.grade }),
|
|
1996
2059
|
" ",
|
|
1997
2060
|
GRADE_MEANING[rec.grade]
|
|
1998
2061
|
] }, `${rec.useCase}-${rec.model.id}`)),
|
|
@@ -2004,13 +2067,31 @@ function Dashboard({ session }) {
|
|
|
2004
2067
|
// src/tui/views/Models.tsx
|
|
2005
2068
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
2006
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
|
+
|
|
2007
2088
|
// src/cli/text.ts
|
|
2008
2089
|
function hardwareText(session) {
|
|
2009
2090
|
const h = session.hardware;
|
|
2010
2091
|
const ram = formatBytes(h.memory.totalBytes, 0);
|
|
2011
2092
|
const avail = h.memory.availableBytes ? formatBytes(h.memory.availableBytes) : "N/A";
|
|
2012
2093
|
return [
|
|
2013
|
-
t("machine"),
|
|
2094
|
+
bold(cyan(t("machine"))),
|
|
2014
2095
|
` ${h.machineModel ?? t("unknownMachine")}`,
|
|
2015
2096
|
` ${h.cpu.name ?? t("cpuNa")}`,
|
|
2016
2097
|
` CPU ${h.cpu.physicalCores ?? "N/A"} cores (${h.cpu.performanceCores ?? "?"}P / ${h.cpu.efficiencyCores ?? "?"}E)`,
|
|
@@ -2022,18 +2103,22 @@ function hardwareText(session) {
|
|
|
2022
2103
|
}
|
|
2023
2104
|
function runtimesText(session) {
|
|
2024
2105
|
return [
|
|
2025
|
-
t("runtimes"),
|
|
2026
|
-
...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
|
+
})
|
|
2027
2112
|
].join("\n");
|
|
2028
2113
|
}
|
|
2029
2114
|
function modelsText(session) {
|
|
2030
|
-
const lines = [t("installedModels"), "MODEL INSTALLED FIT EST. SPEED MEASURED"];
|
|
2115
|
+
const lines = [bold(cyan(t("installedModels"))), dim("MODEL INSTALLED FIT EST. SPEED MEASURED")];
|
|
2031
2116
|
for (const row of session.rows) {
|
|
2032
2117
|
const fit = row.compatibility?.grade ?? "\u2014";
|
|
2033
2118
|
const est = row.compatibility?.estimatedTokensPerSecond ? `~${formatNumber(row.compatibility.estimatedTokensPerSecond)} t/s` : "\u2014";
|
|
2034
2119
|
const measured = row.lastBenchmark?.benchmark.generationTokensPerSecond ? formatTokensPerSec(row.lastBenchmark.benchmark.generationTokensPerSecond) : t("notTested");
|
|
2035
2120
|
lines.push(
|
|
2036
|
-
`${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}`
|
|
2037
2122
|
);
|
|
2038
2123
|
}
|
|
2039
2124
|
lines.push("", t("compatible"));
|
|
@@ -2045,12 +2130,19 @@ function modelsText(session) {
|
|
|
2045
2130
|
return lines.join("\n");
|
|
2046
2131
|
}
|
|
2047
2132
|
function recommendText(session) {
|
|
2048
|
-
const lines = [t("recommended"), t("recommendedHint"), "", t("credits"), ""];
|
|
2133
|
+
const lines = [bold(cyan(t("recommended"))), dim(t("recommendedHint")), "", dim(t("credits")), ""];
|
|
2049
2134
|
for (const rec of session.recommendations) {
|
|
2050
2135
|
lines.push(
|
|
2051
|
-
`${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"))}`
|
|
2052
2143
|
);
|
|
2053
2144
|
}
|
|
2145
|
+
lines.push("", dim(t("recommendCliHint")));
|
|
2054
2146
|
return lines.join("\n");
|
|
2055
2147
|
}
|
|
2056
2148
|
function historyText(records) {
|
|
@@ -2086,6 +2178,7 @@ function doctorText(session) {
|
|
|
2086
2178
|
`${session.hardware.gpu.metal ? "\u2713" : "\u25CB"} Metal`,
|
|
2087
2179
|
...session.runtimes.map((r) => `${r.detected ? "\u2713" : "\u25CB"} ${r.label.padEnd(14)} ${r.version ?? ""}`.trimEnd()),
|
|
2088
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")}`),
|
|
2089
2182
|
"",
|
|
2090
2183
|
session.llamaBench ? t("doctorReady", { count: session.models.filter((m) => m.artifactPath).length }) : t("doctorNoBench")
|
|
2091
2184
|
];
|
|
@@ -2186,10 +2279,264 @@ function HardwareView({ session }) {
|
|
|
2186
2279
|
}
|
|
2187
2280
|
|
|
2188
2281
|
// src/tui/views/Recommend.tsx
|
|
2189
|
-
import { Box as Box4, Text as Text4 } from "ink";
|
|
2190
|
-
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";
|
|
2191
2474
|
function RecommendView({ session }) {
|
|
2192
|
-
|
|
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
|
+
] });
|
|
2193
2540
|
}
|
|
2194
2541
|
|
|
2195
2542
|
// src/tui/views/Compare.tsx
|
|
@@ -2200,35 +2547,19 @@ function CompareView({ session }) {
|
|
|
2200
2547
|
}
|
|
2201
2548
|
|
|
2202
2549
|
// src/tui/views/Benchmark.tsx
|
|
2203
|
-
import { Box as Box6, Text as Text6, useInput } from "ink";
|
|
2204
|
-
import { useEffect, useRef, useState } from "react";
|
|
2205
|
-
|
|
2206
|
-
// src/tui/theme.ts
|
|
2207
|
-
function bar(percent, width = 10) {
|
|
2208
|
-
if (percent === void 0 || Number.isNaN(percent)) return `${"\u2591".repeat(width)} N/A`;
|
|
2209
|
-
const filled = Math.max(0, Math.min(width, Math.round(percent / 100 * width)));
|
|
2210
|
-
return `${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}`;
|
|
2211
|
-
}
|
|
2212
|
-
function sparkline(values, width = 24) {
|
|
2213
|
-
const glyphs = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
2214
|
-
if (!values.length) return glyphs[0].repeat(width);
|
|
2215
|
-
const slice = values.slice(-width);
|
|
2216
|
-
const max = Math.max(...slice, 1);
|
|
2217
|
-
return slice.map((v) => glyphs[Math.min(glyphs.length - 1, Math.floor(v / max * (glyphs.length - 1)))]).join("").padStart(width, glyphs[0]);
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
// src/tui/views/Benchmark.tsx
|
|
2221
|
-
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";
|
|
2222
2553
|
function BenchmarkView(props) {
|
|
2223
|
-
const [phase, setPhase] =
|
|
2224
|
-
const [sample, setSample] =
|
|
2225
|
-
const [elapsed, setElapsed] =
|
|
2226
|
-
const [result, setResult] =
|
|
2227
|
-
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"));
|
|
2228
2559
|
const cpuHist = useRef([]);
|
|
2229
2560
|
const memHist = useRef([]);
|
|
2230
2561
|
const abortRef = useRef(void 0);
|
|
2231
|
-
|
|
2562
|
+
useInput2((input, key) => {
|
|
2232
2563
|
if (phase === "select") {
|
|
2233
2564
|
if (key.upArrow) props.setSelected(Math.max(0, props.selected - 1));
|
|
2234
2565
|
if (key.downArrow) props.setSelected(Math.min(props.models.length - 1, props.selected + 1));
|
|
@@ -2278,9 +2609,9 @@ function BenchmarkView(props) {
|
|
|
2278
2609
|
const h = props.session.hardware;
|
|
2279
2610
|
const model = props.models[props.selected];
|
|
2280
2611
|
if (phase === "select") {
|
|
2281
|
-
return /* @__PURE__ */
|
|
2612
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
2282
2613
|
/* @__PURE__ */ jsx6(Text6, { bold: true, children: t("selectBenchmark") }),
|
|
2283
|
-
props.models.map((m, i) => /* @__PURE__ */
|
|
2614
|
+
props.models.map((m, i) => /* @__PURE__ */ jsxs5(Text6, { color: i === props.selected ? "cyan" : void 0, children: [
|
|
2284
2615
|
i === props.selected ? "\u276F " : " ",
|
|
2285
2616
|
m.name
|
|
2286
2617
|
] }, m.id)),
|
|
@@ -2288,9 +2619,9 @@ function BenchmarkView(props) {
|
|
|
2288
2619
|
] });
|
|
2289
2620
|
}
|
|
2290
2621
|
if (phase === "done" && result) {
|
|
2291
|
-
return /* @__PURE__ */
|
|
2622
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
2292
2623
|
/* @__PURE__ */ jsx6(Text6, { children: resultText(result.record, result.assessment) }),
|
|
2293
|
-
/* @__PURE__ */
|
|
2624
|
+
/* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
2294
2625
|
t("saved"),
|
|
2295
2626
|
" ",
|
|
2296
2627
|
result.path
|
|
@@ -2304,11 +2635,11 @@ function BenchmarkView(props) {
|
|
|
2304
2635
|
const gpu = sample?.gpu?.utilizationPercent;
|
|
2305
2636
|
const memUsed = sample?.memory.usedBytes;
|
|
2306
2637
|
const memTotal = h.memory.totalBytes;
|
|
2307
|
-
return /* @__PURE__ */
|
|
2308
|
-
/* @__PURE__ */
|
|
2638
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
2639
|
+
/* @__PURE__ */ jsxs5(Box6, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [
|
|
2309
2640
|
/* @__PURE__ */ jsx6(Text6, { bold: true, children: t("benchmarkTitle") }),
|
|
2310
2641
|
/* @__PURE__ */ jsx6(Text6, { children: model?.name }),
|
|
2311
|
-
/* @__PURE__ */
|
|
2642
|
+
/* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
2312
2643
|
h.cpu.name,
|
|
2313
2644
|
" \xB7 ",
|
|
2314
2645
|
formatBytes(h.memory.totalBytes, 0),
|
|
@@ -2316,9 +2647,9 @@ function BenchmarkView(props) {
|
|
|
2316
2647
|
memoryLabel(h)
|
|
2317
2648
|
] })
|
|
2318
2649
|
] }),
|
|
2319
|
-
/* @__PURE__ */
|
|
2650
|
+
/* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
2320
2651
|
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: t("performance") }),
|
|
2321
|
-
/* @__PURE__ */
|
|
2652
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2322
2653
|
t("elapsed"),
|
|
2323
2654
|
" ",
|
|
2324
2655
|
elapsed.toFixed(1),
|
|
@@ -2326,21 +2657,21 @@ function BenchmarkView(props) {
|
|
|
2326
2657
|
] }),
|
|
2327
2658
|
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: t("measuredAfter") })
|
|
2328
2659
|
] }),
|
|
2329
|
-
/* @__PURE__ */
|
|
2660
|
+
/* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
2330
2661
|
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: t("system") }),
|
|
2331
|
-
/* @__PURE__ */
|
|
2662
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2332
2663
|
"CPU ",
|
|
2333
2664
|
bar(cpu),
|
|
2334
2665
|
" ",
|
|
2335
2666
|
formatPercent(cpu)
|
|
2336
2667
|
] }),
|
|
2337
|
-
/* @__PURE__ */
|
|
2668
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2338
2669
|
"GPU ",
|
|
2339
2670
|
bar(gpu),
|
|
2340
2671
|
" ",
|
|
2341
2672
|
formatPercent(gpu)
|
|
2342
2673
|
] }),
|
|
2343
|
-
/* @__PURE__ */
|
|
2674
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2344
2675
|
"Memory ",
|
|
2345
2676
|
bar(memUsed && memTotal ? memUsed / memTotal * 100 : void 0),
|
|
2346
2677
|
" ",
|
|
@@ -2348,36 +2679,36 @@ function BenchmarkView(props) {
|
|
|
2348
2679
|
" / ",
|
|
2349
2680
|
formatBytes(memTotal, 0)
|
|
2350
2681
|
] }),
|
|
2351
|
-
/* @__PURE__ */
|
|
2682
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2352
2683
|
"Swap ",
|
|
2353
2684
|
bar(0),
|
|
2354
2685
|
" ",
|
|
2355
2686
|
sample ? formatBytes(sample.memory.swapUsedBytes) : "N/A"
|
|
2356
2687
|
] })
|
|
2357
2688
|
] }),
|
|
2358
|
-
/* @__PURE__ */
|
|
2689
|
+
/* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
2359
2690
|
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: t("powerThermals") }),
|
|
2360
|
-
/* @__PURE__ */
|
|
2691
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2361
2692
|
"Package ",
|
|
2362
2693
|
sample?.packagePowerWatts ?? "N/A",
|
|
2363
2694
|
" W"
|
|
2364
2695
|
] }),
|
|
2365
|
-
/* @__PURE__ */
|
|
2696
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2366
2697
|
"Temperature ",
|
|
2367
2698
|
sample?.thermal?.temperatureC ?? "N/A",
|
|
2368
2699
|
" \xB0C"
|
|
2369
2700
|
] }),
|
|
2370
|
-
/* @__PURE__ */
|
|
2701
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2371
2702
|
"Thermal Pressure ",
|
|
2372
2703
|
sample?.thermal?.pressure ?? "N/A"
|
|
2373
2704
|
] }),
|
|
2374
2705
|
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: t("permissionsHint") })
|
|
2375
2706
|
] }),
|
|
2376
|
-
/* @__PURE__ */
|
|
2707
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2377
2708
|
sparkline(cpuHist.current),
|
|
2378
2709
|
" CPU"
|
|
2379
2710
|
] }),
|
|
2380
|
-
/* @__PURE__ */
|
|
2711
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2381
2712
|
sparkline(memHist.current.map((v) => v / 1024 ** 3)),
|
|
2382
2713
|
" Memory"
|
|
2383
2714
|
] }),
|
|
@@ -2387,17 +2718,17 @@ function BenchmarkView(props) {
|
|
|
2387
2718
|
|
|
2388
2719
|
// src/tui/views/History.tsx
|
|
2389
2720
|
import { Box as Box7, Text as Text7 } from "ink";
|
|
2390
|
-
import { jsx as jsx7, jsxs as
|
|
2721
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2391
2722
|
function HistoryView({ session }) {
|
|
2392
|
-
return /* @__PURE__ */
|
|
2723
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
|
|
2393
2724
|
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: t("measured") }),
|
|
2394
2725
|
/* @__PURE__ */ jsx7(Text7, { children: historyText(session.history) })
|
|
2395
2726
|
] });
|
|
2396
2727
|
}
|
|
2397
2728
|
|
|
2398
2729
|
// src/tui/views/Tasks.tsx
|
|
2399
|
-
import { Box as Box8, Text as Text8, useInput as
|
|
2400
|
-
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";
|
|
2401
2732
|
|
|
2402
2733
|
// src/tasks/catalog.ts
|
|
2403
2734
|
var TASK_CATALOG = [
|
|
@@ -2592,7 +2923,7 @@ function tasksText(plans, answers) {
|
|
|
2592
2923
|
}
|
|
2593
2924
|
|
|
2594
2925
|
// src/tui/views/Tasks.tsx
|
|
2595
|
-
import { Fragment, jsx as jsx8, jsxs as
|
|
2926
|
+
import { Fragment, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2596
2927
|
var KIND_LABEL = {
|
|
2597
2928
|
code: "tasksOptCode",
|
|
2598
2929
|
video: "tasksOptVideo",
|
|
@@ -2601,16 +2932,16 @@ var KIND_LABEL = {
|
|
|
2601
2932
|
chat: "tasksOptChat"
|
|
2602
2933
|
};
|
|
2603
2934
|
function TasksView({ session }) {
|
|
2604
|
-
const [step, setStep] =
|
|
2605
|
-
const [cursor, setCursor] =
|
|
2606
|
-
const [kinds, setKinds] =
|
|
2607
|
-
const [scope, setScope] =
|
|
2608
|
-
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");
|
|
2609
2940
|
const plans = useMemo(
|
|
2610
2941
|
() => planTasks(session, { kinds, scope, priority }),
|
|
2611
2942
|
[session, kinds, scope, priority]
|
|
2612
2943
|
);
|
|
2613
|
-
|
|
2944
|
+
useInput3((input, key) => {
|
|
2614
2945
|
if (step === 0) {
|
|
2615
2946
|
if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
|
|
2616
2947
|
if (key.downArrow) setCursor((c) => Math.min(WORK_KINDS.length - 1, c + 1));
|
|
@@ -2635,11 +2966,11 @@ function TasksView({ session }) {
|
|
|
2635
2966
|
if (step === 3) {
|
|
2636
2967
|
return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsx8(Text8, { children: tasksText(plans, { kinds, scope, priority }) }) });
|
|
2637
2968
|
}
|
|
2638
|
-
return /* @__PURE__ */
|
|
2969
|
+
return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
|
|
2639
2970
|
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("tasksTitle") }),
|
|
2640
|
-
step === 0 && /* @__PURE__ */
|
|
2971
|
+
step === 0 && /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
2641
2972
|
/* @__PURE__ */ jsx8(Text8, { children: t("tasksQ1") }),
|
|
2642
|
-
WORK_KINDS.map((kind, i) => /* @__PURE__ */
|
|
2973
|
+
WORK_KINDS.map((kind, i) => /* @__PURE__ */ jsxs7(Text8, { color: i === cursor ? "cyan" : void 0, children: [
|
|
2643
2974
|
i === cursor ? "\u276F " : " ",
|
|
2644
2975
|
"[",
|
|
2645
2976
|
kinds.includes(kind) ? "x" : " ",
|
|
@@ -2647,20 +2978,20 @@ function TasksView({ session }) {
|
|
|
2647
2978
|
t(KIND_LABEL[kind])
|
|
2648
2979
|
] }, kind))
|
|
2649
2980
|
] }),
|
|
2650
|
-
step === 1 && /* @__PURE__ */
|
|
2981
|
+
step === 1 && /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
2651
2982
|
/* @__PURE__ */ jsx8(Text8, { children: t("tasksQ2") }),
|
|
2652
|
-
/* @__PURE__ */
|
|
2983
|
+
/* @__PURE__ */ jsxs7(Text8, { color: scope === "installed" ? "cyan" : void 0, children: [
|
|
2653
2984
|
scope === "installed" ? "\u276F " : " ",
|
|
2654
2985
|
t("tasksOptInstalled")
|
|
2655
2986
|
] }),
|
|
2656
|
-
/* @__PURE__ */
|
|
2987
|
+
/* @__PURE__ */ jsxs7(Text8, { color: scope === "all" ? "cyan" : void 0, children: [
|
|
2657
2988
|
scope === "all" ? "\u276F " : " ",
|
|
2658
2989
|
t("tasksOptAll")
|
|
2659
2990
|
] })
|
|
2660
2991
|
] }),
|
|
2661
|
-
step === 2 && /* @__PURE__ */
|
|
2992
|
+
step === 2 && /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
2662
2993
|
/* @__PURE__ */ jsx8(Text8, { children: t("tasksQ3") }),
|
|
2663
|
-
["speed", "balanced", "quality"].map((item) => /* @__PURE__ */
|
|
2994
|
+
["speed", "balanced", "quality"].map((item) => /* @__PURE__ */ jsxs7(Text8, { color: priority === item ? "cyan" : void 0, children: [
|
|
2664
2995
|
priority === item ? "\u276F " : " ",
|
|
2665
2996
|
item === "speed" ? t("tasksOptSpeed") : item === "quality" ? t("tasksOptQuality") : t("tasksOptBalanced")
|
|
2666
2997
|
] }, item))
|
|
@@ -2668,30 +2999,44 @@ function TasksView({ session }) {
|
|
|
2668
2999
|
] });
|
|
2669
3000
|
}
|
|
2670
3001
|
|
|
3002
|
+
// src/tui/keys.ts
|
|
3003
|
+
function handleAppKey(screen, input, key) {
|
|
3004
|
+
const letter = input.toLowerCase();
|
|
3005
|
+
if (screen === "benchmark") return { type: "ignore" };
|
|
3006
|
+
if (screen === "recommend" || screen === "tasks") {
|
|
3007
|
+
if (letter === "q") return { type: "quit" };
|
|
3008
|
+
if (key.escape) return { type: "home" };
|
|
3009
|
+
return { type: "ignore" };
|
|
3010
|
+
}
|
|
3011
|
+
if (letter === "q" || key.escape) return { type: "quit" };
|
|
3012
|
+
if (letter === "b") return { type: "screen", screen: "benchmark" };
|
|
3013
|
+
if (letter === "m") return { type: "screen", screen: "models" };
|
|
3014
|
+
if (letter === "r") return { type: "screen", screen: "recommend" };
|
|
3015
|
+
if (letter === "h") return { type: "screen", screen: "hardware" };
|
|
3016
|
+
if (letter === "c") return { type: "screen", screen: "compare" };
|
|
3017
|
+
if (letter === "l") return { type: "screen", screen: "history" };
|
|
3018
|
+
if (letter === "t") return { type: "screen", screen: "tasks" };
|
|
3019
|
+
if (key.return && screen === "home") return { type: "screen", screen: "benchmark" };
|
|
3020
|
+
return { type: "ignore" };
|
|
3021
|
+
}
|
|
3022
|
+
|
|
2671
3023
|
// src/tui/App.tsx
|
|
2672
|
-
import { jsx as jsx9, jsxs as
|
|
3024
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2673
3025
|
function App(props) {
|
|
2674
|
-
const { exit } =
|
|
2675
|
-
const [screen, setScreen] =
|
|
2676
|
-
const [selected, setSelected] =
|
|
3026
|
+
const { exit } = useApp2();
|
|
3027
|
+
const [screen, setScreen] = useState4("home");
|
|
3028
|
+
const [selected, setSelected] = useState4(0);
|
|
2677
3029
|
const benchable = useMemo2(
|
|
2678
3030
|
() => props.session.models.filter((m) => Boolean(m.artifactPath)),
|
|
2679
3031
|
[props.session.models]
|
|
2680
3032
|
);
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
if (
|
|
2685
|
-
if (
|
|
2686
|
-
if (letter === "m") setScreen("models");
|
|
2687
|
-
if (letter === "r") setScreen("recommend");
|
|
2688
|
-
if (letter === "h") setScreen("hardware");
|
|
2689
|
-
if (letter === "c") setScreen("compare");
|
|
2690
|
-
if (letter === "l") setScreen("history");
|
|
2691
|
-
if (letter === "t") setScreen("tasks");
|
|
2692
|
-
if (key.return && screen === "home") setScreen("benchmark");
|
|
3033
|
+
useInput4((input, key) => {
|
|
3034
|
+
const result = handleAppKey(screen, input, key);
|
|
3035
|
+
if (result.type === "quit") exit();
|
|
3036
|
+
if (result.type === "home") setScreen("home");
|
|
3037
|
+
if (result.type === "screen") setScreen(result.screen);
|
|
2693
3038
|
});
|
|
2694
|
-
return /* @__PURE__ */
|
|
3039
|
+
return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", padding: 1, children: [
|
|
2695
3040
|
screen === "home" && /* @__PURE__ */ jsx9(Dashboard, { session: props.session }),
|
|
2696
3041
|
screen === "models" && /* @__PURE__ */ jsx9(ModelsView, { session: props.session }),
|
|
2697
3042
|
screen === "hardware" && /* @__PURE__ */ jsx9(HardwareView, { session: props.session }),
|
|
@@ -2721,179 +3066,6 @@ async function renderDashboard(session, preset) {
|
|
|
2721
3066
|
await instance.waitUntilExit();
|
|
2722
3067
|
}
|
|
2723
3068
|
|
|
2724
|
-
// src/integrations/catalog.ts
|
|
2725
|
-
var INTEGRATIONS = {
|
|
2726
|
-
opencode: {
|
|
2727
|
-
id: "opencode",
|
|
2728
|
-
docsUrl: "https://docs.ollama.com/integrations/opencode",
|
|
2729
|
-
ollamaLaunch: "opencode",
|
|
2730
|
-
minGenerationTps: 12,
|
|
2731
|
-
allowedGrades: ["S", "A", "B"],
|
|
2732
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2733
|
-
},
|
|
2734
|
-
openclaw: {
|
|
2735
|
-
id: "openclaw",
|
|
2736
|
-
docsUrl: "https://docs.ollama.com/integrations/openclaw",
|
|
2737
|
-
ollamaLaunch: "openclaw",
|
|
2738
|
-
minGenerationTps: 12,
|
|
2739
|
-
allowedGrades: ["S", "A", "B"],
|
|
2740
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2741
|
-
},
|
|
2742
|
-
hermes: {
|
|
2743
|
-
id: "hermes",
|
|
2744
|
-
docsUrl: "https://docs.ollama.com/integrations/hermes",
|
|
2745
|
-
ollamaLaunch: "hermes",
|
|
2746
|
-
minGenerationTps: 12,
|
|
2747
|
-
allowedGrades: ["S", "A"],
|
|
2748
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2749
|
-
},
|
|
2750
|
-
claude: {
|
|
2751
|
-
id: "claude",
|
|
2752
|
-
docsUrl: "https://docs.ollama.com/integrations/claude-code",
|
|
2753
|
-
ollamaLaunch: "claude",
|
|
2754
|
-
minGenerationTps: 12,
|
|
2755
|
-
allowedGrades: ["S", "A", "B"],
|
|
2756
|
-
useCases: ["code", "chat", "reasoning"]
|
|
2757
|
-
}
|
|
2758
|
-
};
|
|
2759
|
-
|
|
2760
|
-
// src/integrations/types.ts
|
|
2761
|
-
var INTEGRATION_IDS = ["opencode", "openclaw", "hermes", "claude"];
|
|
2762
|
-
|
|
2763
|
-
// src/integrations/decide.ts
|
|
2764
|
-
var GRADE_RANK = { S: 5, A: 4, B: 3, C: 2, D: 1, F: 0 };
|
|
2765
|
-
function toOllamaTag(id) {
|
|
2766
|
-
const compact = id.toLowerCase().replace(/_/g, "-");
|
|
2767
|
-
const match = compact.match(/^([a-z0-9.]+(?:-[a-z0-9.]+)*)-(\d+(?:\.\d+)?b)$/i);
|
|
2768
|
-
if (match) return `${match[1]}:${match[2]}`;
|
|
2769
|
-
if (compact.includes(":")) return compact;
|
|
2770
|
-
return compact;
|
|
2771
|
-
}
|
|
2772
|
-
function useCaseOk(useCases, needed) {
|
|
2773
|
-
if (!useCases?.length) return needed.includes("chat");
|
|
2774
|
-
return useCases.some((u) => needed.some((n) => u.includes(n)));
|
|
2775
|
-
}
|
|
2776
|
-
function decideLaunch(session, id) {
|
|
2777
|
-
const def = INTEGRATIONS[id];
|
|
2778
|
-
const reasons = [];
|
|
2779
|
-
const ollama = session.runtimes.find((r) => r.id === "ollama")?.detected;
|
|
2780
|
-
if (!ollama) reasons.push(t("launchNeedOllama"));
|
|
2781
|
-
const candidates = [];
|
|
2782
|
-
for (const row of session.rows) {
|
|
2783
|
-
const useCases = row.catalog?.useCase ?? ["chat", "code"];
|
|
2784
|
-
if (!useCaseOk(useCases, def.useCases)) continue;
|
|
2785
|
-
const tps = row.lastBenchmark?.benchmark.generationTokensPerSecond;
|
|
2786
|
-
candidates.push({
|
|
2787
|
-
modelId: row.local.id,
|
|
2788
|
-
ollamaTag: row.local.id.includes(":") ? row.local.id : toOllamaTag(row.local.id),
|
|
2789
|
-
installed: true,
|
|
2790
|
-
origin: row.lastBenchmark ? "measured" : "estimated",
|
|
2791
|
-
grade: row.compatibility?.grade,
|
|
2792
|
-
tps
|
|
2793
|
-
});
|
|
2794
|
-
}
|
|
2795
|
-
if (!candidates.length) {
|
|
2796
|
-
for (const model of session.catalog) {
|
|
2797
|
-
if (!useCaseOk(model.useCase, def.useCases)) continue;
|
|
2798
|
-
const fit = localCompatibility(session.hardware, model);
|
|
2799
|
-
if (!def.allowedGrades.includes(fit.grade)) continue;
|
|
2800
|
-
candidates.push({
|
|
2801
|
-
modelId: model.id,
|
|
2802
|
-
ollamaTag: toOllamaTag(model.id),
|
|
2803
|
-
installed: false,
|
|
2804
|
-
origin: "estimated",
|
|
2805
|
-
grade: fit.grade
|
|
2806
|
-
});
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
candidates.sort((a, b) => {
|
|
2810
|
-
if (a.installed !== b.installed) return a.installed ? -1 : 1;
|
|
2811
|
-
return (GRADE_RANK[b.grade ?? "F"] ?? 0) - (GRADE_RANK[a.grade ?? "F"] ?? 0);
|
|
2812
|
-
});
|
|
2813
|
-
const pick = candidates[0];
|
|
2814
|
-
if (!pick) reasons.push(t("launchNoModel"));
|
|
2815
|
-
if (pick?.grade && !def.allowedGrades.includes(pick.grade)) {
|
|
2816
|
-
reasons.push(t("launchGradeFail", { grade: pick.grade, allowed: def.allowedGrades.join(",") }));
|
|
2817
|
-
}
|
|
2818
|
-
if (pick?.origin === "measured" && pick.tps !== void 0 && pick.tps < def.minGenerationTps) {
|
|
2819
|
-
reasons.push(t("launchSlowFail", { tps: pick.tps.toFixed(1), min: def.minGenerationTps }));
|
|
2820
|
-
}
|
|
2821
|
-
if (pick && pick.origin === "estimated" && (!pick.grade || GRADE_RANK[pick.grade] < GRADE_RANK.B)) {
|
|
2822
|
-
reasons.push(t("launchEstimateWeak"));
|
|
2823
|
-
}
|
|
2824
|
-
const eligible = Boolean(ollama && pick && reasons.length === 0);
|
|
2825
|
-
const command = pick ? `ollama launch ${def.ollamaLaunch} --model ${pick.ollamaTag}` : `ollama launch ${def.ollamaLaunch}`;
|
|
2826
|
-
return {
|
|
2827
|
-
integration: id,
|
|
2828
|
-
docsUrl: def.docsUrl,
|
|
2829
|
-
eligible,
|
|
2830
|
-
reasons: eligible ? [t("launchOk")] : reasons,
|
|
2831
|
-
modelId: pick?.modelId,
|
|
2832
|
-
ollamaTag: pick?.ollamaTag,
|
|
2833
|
-
installed: pick?.installed ?? false,
|
|
2834
|
-
origin: pick?.origin,
|
|
2835
|
-
grade: pick?.grade,
|
|
2836
|
-
command
|
|
2837
|
-
};
|
|
2838
|
-
}
|
|
2839
|
-
function decideAll(session) {
|
|
2840
|
-
return INTEGRATION_IDS.map((id) => decideLaunch(session, id));
|
|
2841
|
-
}
|
|
2842
|
-
function launchText(decisions) {
|
|
2843
|
-
const lines = [t("launchTitle"), t("launchHint"), ""];
|
|
2844
|
-
for (const d of decisions) {
|
|
2845
|
-
lines.push(`${d.integration} ${d.docsUrl}`);
|
|
2846
|
-
lines.push(` ${d.eligible ? "\u2713" : "\u25CB"} ${d.reasons.join(" ")}`);
|
|
2847
|
-
if (d.modelId) {
|
|
2848
|
-
lines.push(
|
|
2849
|
-
` ${t("launchModel")}: ${d.modelId} ${d.grade ?? "\u2014"} ${d.origin === "measured" ? t("measured") : t("estimated")} ${d.installed ? t("tasksInstalled") : t("tasksNotInstalled")}`
|
|
2850
|
-
);
|
|
2851
|
-
}
|
|
2852
|
-
lines.push(` ${d.command}`);
|
|
2853
|
-
if (d.eligible) lines.push(` ${t("launchRunHint", { tool: d.integration })}`);
|
|
2854
|
-
else lines.push(` ${t("launchBlocked")}`);
|
|
2855
|
-
lines.push("");
|
|
2856
|
-
}
|
|
2857
|
-
return lines.join("\n");
|
|
2858
|
-
}
|
|
2859
|
-
function parseIntegration(raw) {
|
|
2860
|
-
if (!raw) return void 0;
|
|
2861
|
-
const id = raw.toLowerCase();
|
|
2862
|
-
if (id === "claude-code") return "claude";
|
|
2863
|
-
return INTEGRATION_IDS.find((item) => item === id);
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
// src/integrations/execute.ts
|
|
2867
|
-
async function executeLaunch(decision) {
|
|
2868
|
-
if (!decision.eligible || !decision.ollamaTag) {
|
|
2869
|
-
return { ok: false, log: t("launchBlocked") };
|
|
2870
|
-
}
|
|
2871
|
-
const def = INTEGRATIONS[decision.integration];
|
|
2872
|
-
if (!def) return { ok: false, log: t("launchUnknown") };
|
|
2873
|
-
const lines = [];
|
|
2874
|
-
if (!decision.installed) {
|
|
2875
|
-
lines.push(t("launchPulling", { model: decision.ollamaTag }));
|
|
2876
|
-
const pull = await runCommand("ollama", ["pull", decision.ollamaTag], { timeout: 30 * 6e4 });
|
|
2877
|
-
if (pull.exitCode !== 0) {
|
|
2878
|
-
return { ok: false, log: `${lines.join("\n")}
|
|
2879
|
-
${pull.stderr || t("launchPullFail")}` };
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
const args2 = ["launch", def.ollamaLaunch, "--model", decision.ollamaTag];
|
|
2883
|
-
if (def.id === "openclaw" || def.id === "claude") args2.push("--yes");
|
|
2884
|
-
lines.push(`ollama ${args2.join(" ")}`);
|
|
2885
|
-
const launched = await runCommand("ollama", args2, { timeout: 12e4 });
|
|
2886
|
-
if (launched.exitCode !== 0) {
|
|
2887
|
-
return {
|
|
2888
|
-
ok: false,
|
|
2889
|
-
log: `${lines.join("\n")}
|
|
2890
|
-
${launched.stderr || launched.stdout || t("launchExecFail")}`
|
|
2891
|
-
};
|
|
2892
|
-
}
|
|
2893
|
-
return { ok: true, log: `${lines.join("\n")}
|
|
2894
|
-
${launched.stdout}`.trim() };
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
3069
|
// src/cli/help.ts
|
|
2898
3070
|
function helpText() {
|
|
2899
3071
|
return t("help");
|
|
@@ -2952,9 +3124,36 @@ ${runtimesText(session)}`),
|
|
|
2952
3124
|
case "models":
|
|
2953
3125
|
print(args2.json ? session.rows : explained("aboutModels", modelsText(session)), args2.json);
|
|
2954
3126
|
return 0;
|
|
2955
|
-
case "recommend":
|
|
2956
|
-
|
|
3127
|
+
case "recommend": {
|
|
3128
|
+
if (args2.install && args2.yes) {
|
|
3129
|
+
const rec = session.recommendations.find((r) => canInstallGrade(r.grade));
|
|
3130
|
+
if (!rec) {
|
|
3131
|
+
print(t("installNone"), args2.json);
|
|
3132
|
+
return 1;
|
|
3133
|
+
}
|
|
3134
|
+
const pulled = await pullOllamaModel(rec.model.id);
|
|
3135
|
+
print(args2.json ? pulled : explained("aboutRecommend", pulled.log), args2.json);
|
|
3136
|
+
return pulled.ok ? 0 : 1;
|
|
3137
|
+
}
|
|
3138
|
+
if (args2.link && args2.yes) {
|
|
3139
|
+
const tool = parseIntegration(args2.link);
|
|
3140
|
+
if (!tool || tool === "claude") {
|
|
3141
|
+
process.stderr.write(`${t("launchNeedTool")}
|
|
3142
|
+
`);
|
|
3143
|
+
return 1;
|
|
3144
|
+
}
|
|
3145
|
+
const decision = decideLaunch(session, tool);
|
|
3146
|
+
if (!decision.eligible) {
|
|
3147
|
+
print(explained("aboutLaunch", launchText([decision])), args2.json);
|
|
3148
|
+
return 1;
|
|
3149
|
+
}
|
|
3150
|
+
const result = await executeLaunch(decision);
|
|
3151
|
+
print(args2.json ? result : explained("aboutLaunch", result.log), args2.json);
|
|
3152
|
+
return result.ok ? 0 : 1;
|
|
3153
|
+
}
|
|
3154
|
+
print(args2.json ? { recommendations: session.recommendations, agents: session.agents } : explained("aboutRecommend", recommendText(session)), args2.json);
|
|
2957
3155
|
return 0;
|
|
3156
|
+
}
|
|
2958
3157
|
case "launch": {
|
|
2959
3158
|
const tool = parseIntegration(args2.positional[0]);
|
|
2960
3159
|
const decisions = tool ? [decideLaunch(session, tool)] : decideAll(session);
|
|
@@ -3058,10 +3257,10 @@ ${runtimesText(session)}`),
|
|
|
3058
3257
|
// src/cli/index.ts
|
|
3059
3258
|
var args = parseArgs(process.argv);
|
|
3060
3259
|
run(args).then((code) => {
|
|
3061
|
-
process.
|
|
3260
|
+
process.exit(code);
|
|
3062
3261
|
}).catch((error) => {
|
|
3063
3262
|
const message = error instanceof Error ? error.message : String(error);
|
|
3064
3263
|
process.stderr.write(`${message}
|
|
3065
3264
|
`);
|
|
3066
|
-
process.
|
|
3265
|
+
process.exit(1);
|
|
3067
3266
|
});
|