peerllm-host-cli 0.3.0 → 0.5.0
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/dist/cli/commands/dashboard.d.ts +1 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +465 -209
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/models.js +8 -8
- package/dist/cli/commands/models.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +5 -4
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/tui.d.ts +12 -0
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +49 -0
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/catalog.d.ts +10 -7
- package/dist/core/catalog.d.ts.map +1 -1
- package/dist/core/catalog.js +10 -2
- package/dist/core/catalog.js.map +1 -1
- package/dist/core/host-stats.d.ts +54 -0
- package/dist/core/host-stats.d.ts.map +1 -0
- package/dist/core/host-stats.js +148 -0
- package/dist/core/host-stats.js.map +1 -0
- package/dist/core/model-download.d.ts.map +1 -1
- package/dist/core/model-download.js +9 -16
- package/dist/core/model-download.js.map +1 -1
- package/dist/core/npm-version.d.ts +23 -0
- package/dist/core/npm-version.d.ts.map +1 -0
- package/dist/core/npm-version.js +73 -0
- package/dist/core/npm-version.js.map +1 -0
- package/dist/core/orchestrator.d.ts +9 -0
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +25 -4
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/token-rate.d.ts +14 -0
- package/dist/core/token-rate.d.ts.map +1 -0
- package/dist/core/token-rate.js +36 -0
- package/dist/core/token-rate.js.map +1 -0
- package/dist/daemon/methods.d.ts +4 -0
- package/dist/daemon/methods.d.ts.map +1 -1
- package/dist/daemon/methods.js +58 -6
- package/dist/daemon/methods.js.map +1 -1
- package/dist/daemon/run.d.ts.map +1 -1
- package/dist/daemon/run.js +10 -0
- package/dist/daemon/run.js.map +1 -1
- package/dist/shared/protocol.d.ts +41 -1
- package/dist/shared/protocol.d.ts.map +1 -1
- package/dist/shared/protocol.js +1 -1
- package/package.json +2 -2
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { cpus, totalmem, freemem } from "node:os";
|
|
2
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
2
3
|
import { Command } from "commander";
|
|
4
|
+
import { graphics, fsSize } from "systeminformation";
|
|
3
5
|
import { resolvePaths } from "../../core/paths.js";
|
|
4
6
|
import { METHOD } from "../../shared/protocol.js";
|
|
5
7
|
import { renderBanner, renderBrand } from "../banner.js";
|
|
6
8
|
import { EXIT } from "../exit-codes.js";
|
|
7
9
|
import { printError } from "../format.js";
|
|
8
10
|
import { DaemonUnreachableError, IpcClient } from "../ipc-client.js";
|
|
9
|
-
import { ANSI, bar, enterTui, moveTo, padRight, paint, sparkline, truncate, write, } from "../tui.js";
|
|
11
|
+
import { ANSI, bar, centerInWidth, enterTui, moveTo, padRight, padVisible, paint, sparkline, truncate, truncateVisible, visibleLength, write, } from "../tui.js";
|
|
10
12
|
const MAX_CPU_SAMPLES = 60;
|
|
11
13
|
const MAX_LOG_LINES = 6;
|
|
14
|
+
const GPU_REFRESH_EVERY = 5; // ticks between systeminformation GPU polls
|
|
15
|
+
const DISK_REFRESH_EVERY = 10; // ticks between disk + local-model-list polls
|
|
12
16
|
function readCpuSnapshot() {
|
|
13
17
|
let idle = 0;
|
|
14
18
|
let total = 0;
|
|
@@ -43,121 +47,288 @@ function formatUptime(seconds) {
|
|
|
43
47
|
const d = Math.floor(h / 24);
|
|
44
48
|
return `${d}d ${h % 24}h`;
|
|
45
49
|
}
|
|
46
|
-
function
|
|
50
|
+
function formatMoney(amount) {
|
|
51
|
+
return amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
52
|
+
}
|
|
53
|
+
function row(label, value, labelWidth = 16) {
|
|
54
|
+
// padRight on plain label is safe — no ANSI codes in `label`.
|
|
47
55
|
return paint("dim", padRight(label, labelWidth)) + value;
|
|
48
56
|
}
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
const FRAME_MIN = 60;
|
|
58
|
+
const FRAME_MAX = 100;
|
|
59
|
+
const FRAME_PAD = 4; // "│ " + " │"
|
|
60
|
+
function frameWidth(cols) {
|
|
61
|
+
return Math.max(FRAME_MIN, Math.min(FRAME_MAX, cols - 2));
|
|
51
62
|
}
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
/** Render a fully-bordered panel: `┌─ Title (suffix) ─┐ … │ row │ … └────┘`. */
|
|
64
|
+
function renderPanel(title, suffix, lines, width) {
|
|
65
|
+
const inner = Math.max(2, width - 2); // visible chars between corners
|
|
66
|
+
const header = renderPanelHeader(title, suffix, inner);
|
|
67
|
+
const body = lines
|
|
68
|
+
.map((line) => {
|
|
69
|
+
const trimmed = truncateVisible(line, inner - 2);
|
|
70
|
+
return (paint("cyan", "│ ") +
|
|
71
|
+
padVisible(trimmed, inner - 2) +
|
|
72
|
+
paint("cyan", " │"));
|
|
73
|
+
})
|
|
74
|
+
.join("\n");
|
|
75
|
+
const footer = paint("cyan", "└" + "─".repeat(inner) + "┘");
|
|
76
|
+
return `${header}\n${body}\n${footer}`;
|
|
54
77
|
}
|
|
55
|
-
function
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
function renderPanelHeader(title, suffix, inner) {
|
|
79
|
+
// Target visible width: inner + 2 (matches footer "└" + inner dashes + "┘").
|
|
80
|
+
// Visible parts: "┌─ " (3) + title + " " (in segment) + optional "suffix " + dashes + "┐" (1)
|
|
81
|
+
const titleSegment = `${title} ` + (suffix ? `${suffix} ` : "");
|
|
82
|
+
const dashCount = Math.max(0, inner - 2 - visibleLength(titleSegment));
|
|
83
|
+
const left = paint("cyan", "┌─ ") + paint(["cyan", "bold"], `${title} `);
|
|
84
|
+
const suffixPart = suffix ? paint("yellow", `${suffix} `) : "";
|
|
85
|
+
return left + suffixPart + paint("cyan", "─".repeat(dashCount) + "┐");
|
|
86
|
+
}
|
|
87
|
+
function na(note) {
|
|
88
|
+
return paint("dim", `(${note})`);
|
|
89
|
+
}
|
|
90
|
+
function renderHeader(state, width) {
|
|
91
|
+
const version = state.status?.daemon.version ?? "";
|
|
92
|
+
const lastUpdate = state.lastUpdated
|
|
93
|
+
? paint("gray", `last update ${state.lastUpdated.toLocaleTimeString()}`)
|
|
94
|
+
: paint("dim", "loading…");
|
|
95
|
+
const out = [];
|
|
96
|
+
// Center each row of the ASCII banner independently.
|
|
97
|
+
for (const row of renderBanner().split("\n")) {
|
|
98
|
+
out.push(centerInWidth(row, width));
|
|
67
99
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
out.push("");
|
|
101
|
+
out.push(centerInWidth(renderBrand() + paint("dim", " · decentralized AI host · ") + lastUpdate, width));
|
|
102
|
+
out.push(centerInWidth(paint("dim", "© 2026 Hassan Habib."), width));
|
|
103
|
+
out.push(centerInWidth(paint("dim", "Operated by BestBytes AI, LLC."), width));
|
|
104
|
+
out.push(centerInWidth(paint(["cyan", "bold"], "Terms & Conditions") +
|
|
105
|
+
paint("dim", " · ") +
|
|
106
|
+
paint("bold", version ? `v${version}` : "v—"), width));
|
|
107
|
+
return out.join("\n");
|
|
108
|
+
}
|
|
109
|
+
function buildDaemonPanel(state) {
|
|
110
|
+
if (!state.status)
|
|
111
|
+
return [paint("dim", "(awaiting status…)")];
|
|
112
|
+
const s = state.status;
|
|
113
|
+
const upgradeAvailable = s.daemon.latestCliVersion !== undefined &&
|
|
114
|
+
s.daemon.latestCliVersion !== s.daemon.version;
|
|
115
|
+
const versionCell = upgradeAvailable
|
|
116
|
+
? paint("bold", s.daemon.version) +
|
|
117
|
+
paint("yellow", ` → ${s.daemon.latestCliVersion} on npm`)
|
|
118
|
+
: paint("bold", s.daemon.version);
|
|
119
|
+
const lines = [];
|
|
120
|
+
lines.push(row("pid", String(s.daemon.pid)) +
|
|
121
|
+
paint("dim", " ") +
|
|
122
|
+
row("uptime", paint(["green"], formatUptime(s.daemon.uptimeSeconds)), 10) +
|
|
123
|
+
paint("dim", " ") +
|
|
124
|
+
row("version", versionCell, 10));
|
|
125
|
+
lines.push(row("orchestrator", statusBadge(s.orchestrator.status)));
|
|
126
|
+
const authStr = s.auth.loggedIn
|
|
127
|
+
? paint("green", "logged in") +
|
|
128
|
+
(s.auth.userId ? paint("dim", ` (user ${s.auth.userId.slice(0, 8)}…)`) : "")
|
|
129
|
+
: paint("yellow", "logged out");
|
|
130
|
+
lines.push(row("auth", authStr));
|
|
131
|
+
lines.push(row("agreement", s.auth.agreementAccepted ? paint("green", "accepted") : paint("yellow", "not accepted")));
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
function buildHostPanel(state) {
|
|
135
|
+
if (!state.status)
|
|
136
|
+
return [paint("dim", "(awaiting status…)")];
|
|
137
|
+
const s = state.status;
|
|
138
|
+
const lines = [];
|
|
139
|
+
lines.push(row("name", s.config.hostName ?? paint("dim", "(unset)")));
|
|
140
|
+
lines.push(row("host id", paint("gray", s.config.hostId ? `${s.config.hostId.slice(0, 8)}…` : "(unset)")));
|
|
141
|
+
if (s.host) {
|
|
142
|
+
lines.push(row("status", s.host.isApproved
|
|
143
|
+
? paint("green", "approved")
|
|
144
|
+
: paint("yellow", "pending approval")));
|
|
145
|
+
lines.push(row("uptime", paint("green", s.host.currentUptime)) +
|
|
74
146
|
paint("dim", " ") +
|
|
75
|
-
row("
|
|
147
|
+
row("total up", paint("gray", s.host.totalUptime), 12));
|
|
148
|
+
lines.push(row("conversations", paint("cyan", s.host.conversations.toLocaleString())) +
|
|
76
149
|
paint("dim", " ") +
|
|
77
|
-
row("
|
|
78
|
-
"\n");
|
|
79
|
-
write(" " + row("orchestrator", statusBadge(s.orchestrator.status)) + "\n");
|
|
80
|
-
const authStr = s.auth.loggedIn
|
|
81
|
-
? paint("green", "logged in") +
|
|
82
|
-
(s.auth.userId ? paint("dim", ` (user ${s.auth.userId.slice(0, 8)}…)`) : "")
|
|
83
|
-
: paint("yellow", "logged out");
|
|
84
|
-
write(" " + row("auth", authStr) + "\n");
|
|
85
|
-
const agreementStr = s.auth.agreementAccepted
|
|
86
|
-
? paint("green", "accepted")
|
|
87
|
-
: paint("yellow", "not accepted");
|
|
88
|
-
write(" " + row("agreement", agreementStr) + "\n");
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
write(" " + paint("dim", "(awaiting status…)") + "\n");
|
|
92
|
-
}
|
|
93
|
-
write("\n");
|
|
94
|
-
// Host panel
|
|
95
|
-
write(panelTitle("Host") + "\n");
|
|
96
|
-
if (state.status) {
|
|
97
|
-
const s = state.status;
|
|
98
|
-
const hostName = s.config.hostName ?? paint("dim", "(unset)");
|
|
99
|
-
const hostId = s.config.hostId ? `${s.config.hostId.slice(0, 8)}…` : paint("dim", "(unset)");
|
|
100
|
-
write(" " + row("name", hostName) + "\n");
|
|
101
|
-
write(" " + row("host id", paint("gray", hostId)) + "\n");
|
|
150
|
+
row("avg resp", `${s.host.averageResponseMs.toFixed(0)} ms`, 12));
|
|
102
151
|
}
|
|
103
152
|
else {
|
|
104
|
-
|
|
153
|
+
lines.push(row("status", na("backend stats loading…")));
|
|
105
154
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
function buildModelPanel(state) {
|
|
109
158
|
if (state.models?.loaded) {
|
|
110
159
|
const m = state.models.loaded;
|
|
111
|
-
|
|
112
|
-
|
|
160
|
+
return [
|
|
161
|
+
row("loaded", paint(["bold", "green"], m.modelName)),
|
|
113
162
|
row("gpu layers", String(m.gpuLayers)) +
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
163
|
+
paint("dim", " ") +
|
|
164
|
+
row("active conv.", paint("cyan", String(m.activeContexts)), 16),
|
|
165
|
+
];
|
|
117
166
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
paint("dim", "(no model loaded — ") +
|
|
167
|
+
return [
|
|
168
|
+
paint("dim", "(no model loaded — ") +
|
|
121
169
|
paint(["bold", "cyan"], "peerllm-host models download") +
|
|
122
|
-
paint("dim", " to fetch one)")
|
|
123
|
-
|
|
170
|
+
paint("dim", " to fetch one)"),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
function buildResourcesPanel(state, innerWidth) {
|
|
174
|
+
// Reserve space inside the frame: label (16) + padding (4) + percent/label tail (~14).
|
|
175
|
+
const barWidth = Math.max(20, Math.min(50, innerWidth - 36));
|
|
176
|
+
const lines = [];
|
|
177
|
+
lines.push(row("CPU", bar(state.cpuFraction, barWidth, "cyan")) +
|
|
178
|
+
paint("dim", ` ${(state.cpuFraction * 100).toFixed(0)}%`));
|
|
179
|
+
lines.push(row("RAM", bar(state.ramFraction, barWidth, "magenta")) +
|
|
180
|
+
paint("dim", ` ${(state.ramFraction * 100).toFixed(0)}%`));
|
|
181
|
+
if (state.gpu === undefined) {
|
|
182
|
+
lines.push(row("GPU", paint("dim", "detecting…")));
|
|
124
183
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const barWidth = Math.max(20, Math.min(40, cols - 40));
|
|
128
|
-
write(panelTitle("Resources") + "\n");
|
|
129
|
-
write(" " +
|
|
130
|
-
row("CPU", bar(state.cpuFraction, barWidth, "cyan")) +
|
|
131
|
-
paint("dim", ` ${(state.cpuFraction * 100).toFixed(0)}%`) +
|
|
132
|
-
"\n");
|
|
133
|
-
write(" " +
|
|
134
|
-
row("RAM", bar(state.ramFraction, barWidth, "magenta")) +
|
|
135
|
-
paint("dim", ` ${(state.ramFraction * 100).toFixed(0)}%`) +
|
|
136
|
-
"\n");
|
|
137
|
-
// Sparkline: CPU% over the last MAX_CPU_SAMPLES ticks
|
|
138
|
-
const sparkWidth = Math.min(MAX_CPU_SAMPLES, Math.max(20, cols - 20));
|
|
139
|
-
const padded = padSparkline(state.cpuSamples, sparkWidth);
|
|
140
|
-
write(" " + row("trend (60s)", sparkline(padded, "cyan")) + "\n");
|
|
141
|
-
write("\n");
|
|
142
|
-
// Logs panel (tails recent log lines if any have arrived from the daemon)
|
|
143
|
-
write(panelTitle("Recent activity") + "\n");
|
|
144
|
-
if (state.logs.length === 0) {
|
|
145
|
-
write(" " + paint("dim", "(no recent events — host idle)") + "\n");
|
|
184
|
+
else if (state.gpu === null) {
|
|
185
|
+
lines.push(row("GPU", na("no GPU detected")));
|
|
146
186
|
}
|
|
147
187
|
else {
|
|
148
|
-
|
|
149
|
-
|
|
188
|
+
const g = state.gpu;
|
|
189
|
+
const hasUtil = !Number.isNaN(g.utilizationFraction) && g.utilizationFraction >= 0;
|
|
190
|
+
const gpuLabel = truncate(g.name, 22);
|
|
191
|
+
if (hasUtil) {
|
|
192
|
+
lines.push(row("GPU", bar(g.utilizationFraction, barWidth, "yellow")) +
|
|
193
|
+
paint("dim", ` ${(g.utilizationFraction * 100).toFixed(0)}% ${gpuLabel}`));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
lines.push(row("GPU", paint("dim", gpuLabel) + paint("gray", " (utilization unavailable)")));
|
|
197
|
+
}
|
|
198
|
+
if (g.vramTotalMB > 0) {
|
|
199
|
+
const vramFrac = Math.max(0, Math.min(1, g.vramUsedMB / g.vramTotalMB));
|
|
200
|
+
lines.push(row("VRAM", bar(vramFrac, barWidth, "yellow")) +
|
|
201
|
+
paint("dim", ` ${(g.vramUsedMB / 1024).toFixed(1)} / ${(g.vramTotalMB / 1024).toFixed(1)} GB`));
|
|
150
202
|
}
|
|
151
203
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
204
|
+
if (state.diskFraction !== undefined && state.diskTotalGB && state.diskTotalGB > 0) {
|
|
205
|
+
lines.push(row("Disk", bar(state.diskFraction, barWidth, "green")) +
|
|
206
|
+
paint("dim", ` ${(state.diskUsedGB ?? 0).toFixed(1)} / ${state.diskTotalGB.toFixed(1)} GB`));
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
lines.push(row("Disk", na("disk info unavailable")));
|
|
210
|
+
}
|
|
211
|
+
const sparkWidth = Math.min(MAX_CPU_SAMPLES, Math.max(20, innerWidth - 22));
|
|
212
|
+
lines.push(row("trend (60s)", sparkline(padSparkline(state.cpuSamples, sparkWidth), "cyan")));
|
|
213
|
+
return lines;
|
|
214
|
+
}
|
|
215
|
+
function buildNetworkPanel(state) {
|
|
216
|
+
if (state.status?.network) {
|
|
217
|
+
const n = state.status.network;
|
|
218
|
+
return [
|
|
219
|
+
row("hosts", `${n.activeHosts.toLocaleString()} active`) +
|
|
220
|
+
paint("dim", " ") +
|
|
221
|
+
row("approved", paint("cyan", n.approvedHosts.toLocaleString()), 11) +
|
|
222
|
+
paint("dim", " ") +
|
|
223
|
+
row("total", paint("cyan", n.totalHosts.toLocaleString()), 8),
|
|
224
|
+
row("tokens served", n.totalServedTokens.toLocaleString()),
|
|
225
|
+
row("uptime", `${n.networkUptimePct.toFixed(1)}%`),
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
if (state.status)
|
|
229
|
+
return [na("backend unreachable — retrying")];
|
|
230
|
+
return [na("connecting to daemon…")];
|
|
231
|
+
}
|
|
232
|
+
function buildTokensPanel(state) {
|
|
233
|
+
const t = state.status?.tokens;
|
|
234
|
+
if (!t)
|
|
235
|
+
return [na("awaiting daemon stats…")];
|
|
236
|
+
const fmtFlow = (label, f) => {
|
|
237
|
+
const rate = paint("cyan", `${f.tokensPerSecond.toFixed(1)} tok/s`);
|
|
238
|
+
const session = paint("dim", `session ${f.totalServed.toLocaleString()}`);
|
|
239
|
+
const lifetime = f.lifetime !== undefined
|
|
240
|
+
? paint("dim", ` · lifetime ${f.lifetime.toLocaleString()}`)
|
|
241
|
+
: "";
|
|
242
|
+
return row(label, `${rate} ${session}${lifetime}`);
|
|
243
|
+
};
|
|
244
|
+
return [fmtFlow("in", t.in), fmtFlow("out", t.out)];
|
|
245
|
+
}
|
|
246
|
+
function buildEarningsPanel(state) {
|
|
247
|
+
if (state.status?.earnings) {
|
|
248
|
+
const e = state.status.earnings;
|
|
249
|
+
return [
|
|
250
|
+
row("pending payout", paint(["green", "bold"], `${formatMoney(e.pendingAmount)} ${e.currency}`)),
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
if (state.status?.auth.loggedIn)
|
|
254
|
+
return [na("awaiting payout data…")];
|
|
255
|
+
return [na("login required to fetch earnings")];
|
|
256
|
+
}
|
|
257
|
+
function buildAvailableLlmsPanel(state, innerWidth) {
|
|
258
|
+
const lines = [];
|
|
259
|
+
const nameBudget = Math.max(20, innerWidth - 6); // leave room for " · " indent
|
|
260
|
+
const localModels = state.localModels;
|
|
261
|
+
if (localModels === undefined) {
|
|
262
|
+
lines.push(row("local", paint("dim", "scanning…")));
|
|
263
|
+
}
|
|
264
|
+
else if (localModels.length === 0) {
|
|
265
|
+
lines.push(row("local", paint("dim", "none — run ") + paint(["bold", "cyan"], "peerllm-host models download")));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
lines.push(row("local", paint("cyan", String(localModels.length))));
|
|
269
|
+
for (const name of localModels) {
|
|
270
|
+
lines.push(paint("dim", " · ") + paint("cyan", truncateVisible(name, nameBudget)));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (state.models?.available !== undefined) {
|
|
274
|
+
lines.push(row("daemon list", paint("cyan", String(state.models.available.length))));
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
lines.push(row("daemon list", na("daemon not reporting")));
|
|
278
|
+
}
|
|
279
|
+
const approved = state.models?.approved;
|
|
280
|
+
if (approved && approved.length > 0) {
|
|
281
|
+
lines.push(row("approved", paint(["green", "bold"], String(approved.length))));
|
|
282
|
+
for (const name of approved) {
|
|
283
|
+
lines.push(paint("dim", " · ") + paint(["green", "bold"], truncateVisible(name, nameBudget)));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else if (approved !== undefined) {
|
|
287
|
+
lines.push(row("approved", na("none — orchestrator hasn't registered yet")));
|
|
288
|
+
}
|
|
289
|
+
return lines;
|
|
290
|
+
}
|
|
291
|
+
function buildRecentActivityPanel(state, innerWidth) {
|
|
292
|
+
if (state.logs.length === 0)
|
|
293
|
+
return [paint("dim", "(no recent events — host idle)")];
|
|
294
|
+
return state.logs.slice(-MAX_LOG_LINES).map((l) => formatLog(l, innerWidth - 2));
|
|
295
|
+
}
|
|
296
|
+
function render(state, cols) {
|
|
297
|
+
write(ANSI.home + ANSI.clear);
|
|
298
|
+
const width = frameWidth(cols);
|
|
299
|
+
const inner = width - FRAME_PAD; // visible cells available for row content
|
|
300
|
+
write(renderHeader(state, width) + "\n\n");
|
|
301
|
+
if (state.lastError) {
|
|
302
|
+
write(" " + paint("yellow", `⚠ ${state.lastError}`) + "\n\n");
|
|
303
|
+
}
|
|
304
|
+
const panels = [
|
|
305
|
+
["Daemon", undefined, buildDaemonPanel(state)],
|
|
306
|
+
["Host", state.status?.host?.stale ? "(stale)" : undefined, buildHostPanel(state)],
|
|
307
|
+
["Model", undefined, buildModelPanel(state)],
|
|
308
|
+
["Resources", undefined, buildResourcesPanel(state, inner)],
|
|
309
|
+
[
|
|
310
|
+
"Network",
|
|
311
|
+
state.status?.network?.stale ? "(stale)" : undefined,
|
|
312
|
+
buildNetworkPanel(state),
|
|
313
|
+
],
|
|
314
|
+
["Tokens", undefined, buildTokensPanel(state)],
|
|
315
|
+
[
|
|
316
|
+
"Earnings",
|
|
317
|
+
state.status?.earnings?.stale ? "(stale)" : undefined,
|
|
318
|
+
buildEarningsPanel(state),
|
|
319
|
+
],
|
|
320
|
+
["Available LLMs", undefined, buildAvailableLlmsPanel(state, inner)],
|
|
321
|
+
["Recent activity", undefined, buildRecentActivityPanel(state, inner)],
|
|
322
|
+
];
|
|
323
|
+
for (const [title, suffix, lines] of panels) {
|
|
324
|
+
write(renderPanel(title, suffix, lines, width) + "\n");
|
|
325
|
+
}
|
|
326
|
+
// Help bar — also framed for visual consistency.
|
|
327
|
+
const help = keyHint("q", "quit") +
|
|
157
328
|
keyHint("r", "refresh now") +
|
|
158
329
|
keyHint("l", "clear logs") +
|
|
159
|
-
keyHint("?", "help")
|
|
160
|
-
|
|
330
|
+
keyHint("?", "help");
|
|
331
|
+
write(renderPanel("Keys", undefined, [help.trimEnd()], width) + "\n");
|
|
161
332
|
}
|
|
162
333
|
function padSparkline(values, width) {
|
|
163
334
|
if (values.length >= width)
|
|
@@ -181,136 +352,221 @@ function formatLog(l, maxWidth) {
|
|
|
181
352
|
const visiblePrefixLen = ts.length + 1 + 5;
|
|
182
353
|
return prefix + " " + truncate(l.message, Math.max(0, maxWidth - visiblePrefixLen - 1));
|
|
183
354
|
}
|
|
184
|
-
export function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
355
|
+
export async function runDashboard(ipcEndpoint, modelsDir, intervalMs = 1000) {
|
|
356
|
+
const client = new IpcClient(ipcEndpoint);
|
|
357
|
+
try {
|
|
358
|
+
await client.connect();
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
if (err instanceof DaemonUnreachableError) {
|
|
362
|
+
printError("daemon is not running", {
|
|
363
|
+
hint: "Start it with: peerllm-host start",
|
|
364
|
+
});
|
|
365
|
+
process.exit(EXIT.daemonNotRunning);
|
|
195
366
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
367
|
+
printError(err.message);
|
|
368
|
+
process.exit(EXIT.runtimeError);
|
|
369
|
+
}
|
|
370
|
+
const tui = enterTui();
|
|
371
|
+
const state = {
|
|
372
|
+
cpuSamples: [],
|
|
373
|
+
ramFraction: 0,
|
|
374
|
+
cpuFraction: 0,
|
|
375
|
+
logs: [],
|
|
376
|
+
};
|
|
377
|
+
let prevCpu = readCpuSnapshot();
|
|
378
|
+
// Init counters to (REFRESH_EVERY - 1) so the very first tick fires a refresh.
|
|
379
|
+
let gpuTick = GPU_REFRESH_EVERY - 1;
|
|
380
|
+
let diskTick = DISK_REFRESH_EVERY - 1;
|
|
381
|
+
client.onNotification((method, params) => {
|
|
382
|
+
const name = method.startsWith("event:") ? method.slice("event:".length) : method;
|
|
383
|
+
if (name === "log") {
|
|
384
|
+
const p = params;
|
|
385
|
+
if (p && typeof p.message === "string")
|
|
386
|
+
state.logs.push(p);
|
|
387
|
+
while (state.logs.length > MAX_LOG_LINES * 2)
|
|
388
|
+
state.logs.shift();
|
|
389
|
+
}
|
|
390
|
+
else if (name === "orchestratorStatus") {
|
|
391
|
+
void pollOnce();
|
|
205
392
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
let prevCpu = readCpuSnapshot();
|
|
214
|
-
// Subscribe to daemon events for live updates between polls.
|
|
215
|
-
client.onNotification((method, params) => {
|
|
216
|
-
const name = method.startsWith("event:") ? method.slice("event:".length) : method;
|
|
217
|
-
if (name === "log") {
|
|
218
|
-
const p = params;
|
|
219
|
-
if (p && typeof p.message === "string")
|
|
220
|
-
state.logs.push(p);
|
|
221
|
-
while (state.logs.length > MAX_LOG_LINES * 2)
|
|
222
|
-
state.logs.shift();
|
|
393
|
+
else if (name === "hostStats") {
|
|
394
|
+
const p = params;
|
|
395
|
+
if (typeof p.cpuPercent === "number") {
|
|
396
|
+
state.cpuFraction = Math.max(0, Math.min(1, p.cpuPercent / 100));
|
|
397
|
+
state.cpuSamples.push(state.cpuFraction);
|
|
398
|
+
while (state.cpuSamples.length > MAX_CPU_SAMPLES)
|
|
399
|
+
state.cpuSamples.shift();
|
|
223
400
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
void pollOnce();
|
|
401
|
+
if (typeof p.ramUsedMB === "number" && typeof p.ramTotalMB === "number" && p.ramTotalMB > 0) {
|
|
402
|
+
state.ramFraction = Math.max(0, Math.min(1, p.ramUsedMB / p.ramTotalMB));
|
|
227
403
|
}
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
404
|
+
if (p.gpu) {
|
|
405
|
+
const prev = state.gpu ?? undefined;
|
|
406
|
+
state.gpu = {
|
|
407
|
+
name: p.gpu.name ?? (prev && prev !== null ? prev.name : "GPU"),
|
|
408
|
+
utilizationFraction: typeof p.gpu.utilizationPercent === "number"
|
|
409
|
+
? p.gpu.utilizationPercent / 100
|
|
410
|
+
: prev && prev !== null
|
|
411
|
+
? prev.utilizationFraction
|
|
412
|
+
: Number.NaN,
|
|
413
|
+
vramUsedMB: p.gpu.vramUsedMB ?? (prev && prev !== null ? prev.vramUsedMB : 0),
|
|
414
|
+
vramTotalMB: p.gpu.vramTotalMB ?? (prev && prev !== null ? prev.vramTotalMB : 0),
|
|
415
|
+
};
|
|
239
416
|
}
|
|
240
|
-
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
try {
|
|
420
|
+
await client.call("events.subscribe", { filter: "*" });
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Non-fatal: polling still works without event subscription.
|
|
424
|
+
}
|
|
425
|
+
const pollOnce = async () => {
|
|
241
426
|
try {
|
|
242
|
-
|
|
427
|
+
const [status, models] = await Promise.all([
|
|
428
|
+
client.call(METHOD.getStatus),
|
|
429
|
+
client.call(METHOD.modelsState),
|
|
430
|
+
]);
|
|
431
|
+
state.status = status;
|
|
432
|
+
state.models = models;
|
|
433
|
+
state.lastError = undefined;
|
|
243
434
|
}
|
|
244
|
-
catch {
|
|
245
|
-
|
|
435
|
+
catch (err) {
|
|
436
|
+
state.lastError = `IPC: ${err.message}`;
|
|
437
|
+
}
|
|
438
|
+
state.lastUpdated = new Date();
|
|
439
|
+
};
|
|
440
|
+
const sampleLocal = async () => {
|
|
441
|
+
// CPU (sync, every tick)
|
|
442
|
+
const next = readCpuSnapshot();
|
|
443
|
+
const totalDelta = next.total - prevCpu.total;
|
|
444
|
+
const idleDelta = next.idle - prevCpu.idle;
|
|
445
|
+
prevCpu = next;
|
|
446
|
+
if (totalDelta > 0) {
|
|
447
|
+
state.cpuFraction = Math.max(0, Math.min(1, 1 - idleDelta / totalDelta));
|
|
246
448
|
}
|
|
247
|
-
|
|
449
|
+
state.cpuSamples.push(state.cpuFraction);
|
|
450
|
+
while (state.cpuSamples.length > MAX_CPU_SAMPLES)
|
|
451
|
+
state.cpuSamples.shift();
|
|
452
|
+
// RAM (sync, every tick)
|
|
453
|
+
const total = totalmem();
|
|
454
|
+
const free = freemem();
|
|
455
|
+
if (total > 0)
|
|
456
|
+
state.ramFraction = Math.max(0, Math.min(1, 1 - free / total));
|
|
457
|
+
// GPU — throttled; uses systeminformation for cross-platform support.
|
|
458
|
+
// Daemon hostStats event takes precedence if it starts sending GPU data.
|
|
459
|
+
gpuTick++;
|
|
460
|
+
if (gpuTick >= GPU_REFRESH_EVERY) {
|
|
461
|
+
gpuTick = 0;
|
|
248
462
|
try {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
463
|
+
const { controllers } = await graphics();
|
|
464
|
+
if (controllers.length === 0) {
|
|
465
|
+
state.gpu = null;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
const c = controllers[0];
|
|
469
|
+
const util = typeof c.utilizationGpu === "number" ? c.utilizationGpu / 100 : Number.NaN;
|
|
470
|
+
const vramTotal = (c.memoryTotal ?? c.vram) ?? 0;
|
|
471
|
+
const vramUsed = c.memoryUsed ?? 0;
|
|
472
|
+
state.gpu = {
|
|
473
|
+
name: c.model ?? "GPU",
|
|
474
|
+
utilizationFraction: util,
|
|
475
|
+
vramUsedMB: vramUsed,
|
|
476
|
+
vramTotalMB: vramTotal,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
259
479
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const sampleLocal = () => {
|
|
263
|
-
const next = readCpuSnapshot();
|
|
264
|
-
const totalDelta = next.total - prevCpu.total;
|
|
265
|
-
const idleDelta = next.idle - prevCpu.idle;
|
|
266
|
-
prevCpu = next;
|
|
267
|
-
if (totalDelta > 0) {
|
|
268
|
-
state.cpuFraction = Math.max(0, Math.min(1, 1 - idleDelta / totalDelta));
|
|
480
|
+
catch {
|
|
481
|
+
// Non-fatal: GPU info is best-effort.
|
|
269
482
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
exit(EXIT.ok);
|
|
296
|
-
return;
|
|
483
|
+
}
|
|
484
|
+
// Disk + local model list — throttled together.
|
|
485
|
+
diskTick++;
|
|
486
|
+
if (diskTick >= DISK_REFRESH_EVERY) {
|
|
487
|
+
diskTick = 0;
|
|
488
|
+
// Disk usage via systeminformation (cross-platform).
|
|
489
|
+
try {
|
|
490
|
+
const fsSizes = await fsSize();
|
|
491
|
+
if (fsSizes.length > 0) {
|
|
492
|
+
const mDir = modelsDir.toLowerCase().replace(/\\/g, "/");
|
|
493
|
+
let match = fsSizes[0];
|
|
494
|
+
let bestLen = 0;
|
|
495
|
+
for (const fs of fsSizes) {
|
|
496
|
+
const mount = (fs.mount ?? "").toLowerCase().replace(/\\/g, "/");
|
|
497
|
+
if (mDir.startsWith(mount) && mount.length > bestLen) {
|
|
498
|
+
bestLen = mount.length;
|
|
499
|
+
match = fs;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (match.size > 0) {
|
|
503
|
+
state.diskFraction = Math.max(0, Math.min(1, match.use / 100));
|
|
504
|
+
state.diskTotalGB = match.size / 1024 ** 3;
|
|
505
|
+
state.diskUsedGB = match.used / 1024 ** 3;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
297
508
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return;
|
|
509
|
+
catch {
|
|
510
|
+
// Non-fatal.
|
|
301
511
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
512
|
+
// Local GGUF model listing (sync readdir).
|
|
513
|
+
try {
|
|
514
|
+
state.localModels = existsSync(modelsDir)
|
|
515
|
+
? readdirSync(modelsDir)
|
|
516
|
+
.filter((f) => f.toLowerCase().endsWith(".gguf"))
|
|
517
|
+
.map((f) => f.replace(/\.gguf$/i, ""))
|
|
518
|
+
: [];
|
|
306
519
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
render(state, tui.cols);
|
|
520
|
+
catch {
|
|
521
|
+
// Non-fatal.
|
|
310
522
|
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const tick = async () => {
|
|
526
|
+
await sampleLocal();
|
|
527
|
+
await pollOnce();
|
|
528
|
+
render(state, tui.cols);
|
|
529
|
+
};
|
|
530
|
+
await tick();
|
|
531
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
532
|
+
const exit = (code) => {
|
|
533
|
+
clearInterval(timer);
|
|
534
|
+
client.close();
|
|
535
|
+
tui.teardown();
|
|
536
|
+
write(moveTo(1, 1));
|
|
537
|
+
process.exit(code);
|
|
538
|
+
};
|
|
539
|
+
tui.onKey((key) => {
|
|
540
|
+
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
541
|
+
exit(EXIT.ok);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (key.name === "r") {
|
|
545
|
+
void tick();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (key.name === "l") {
|
|
549
|
+
state.logs = [];
|
|
550
|
+
render(state, tui.cols);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (key.name === "?") {
|
|
554
|
+
state.lastError = "keys: q=quit · r=refresh · l=clear logs · ?=this help";
|
|
555
|
+
render(state, tui.cols);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
// Park forever (timer + key handler drive the rest).
|
|
559
|
+
await new Promise(() => { });
|
|
560
|
+
}
|
|
561
|
+
export function makeDashboardCommand() {
|
|
562
|
+
return new Command("dashboard")
|
|
563
|
+
.description("Interactive TUI: watch live host status, model, and traffic")
|
|
564
|
+
.option("--config-dir <path>", "override config directory")
|
|
565
|
+
.option("--interval <ms>", "refresh interval in milliseconds (default 1000)", "1000")
|
|
566
|
+
.action(async (opts) => {
|
|
567
|
+
const intervalMs = Math.max(250, Number.parseInt(opts.interval ?? "1000", 10) || 1000);
|
|
568
|
+
const paths = resolvePaths({ configDirOverride: opts.configDir });
|
|
569
|
+
await runDashboard(paths.ipcEndpoint, paths.modelsDir, intervalMs);
|
|
314
570
|
});
|
|
315
571
|
}
|
|
316
572
|
//# sourceMappingURL=dashboard.js.map
|