peerllm-host-cli 0.2.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.
Files changed (56) hide show
  1. package/dist/cli/banner.d.ts +9 -0
  2. package/dist/cli/banner.d.ts.map +1 -0
  3. package/dist/cli/banner.js +50 -0
  4. package/dist/cli/banner.js.map +1 -0
  5. package/dist/cli/commands/dashboard.d.ts +4 -0
  6. package/dist/cli/commands/dashboard.d.ts.map +1 -0
  7. package/dist/cli/commands/dashboard.js +572 -0
  8. package/dist/cli/commands/dashboard.js.map +1 -0
  9. package/dist/cli/commands/models.js +8 -8
  10. package/dist/cli/commands/models.js.map +1 -1
  11. package/dist/cli/commands/start.d.ts.map +1 -1
  12. package/dist/cli/commands/start.js +5 -4
  13. package/dist/cli/commands/start.js.map +1 -1
  14. package/dist/cli/commands/status.d.ts.map +1 -1
  15. package/dist/cli/commands/status.js +2 -0
  16. package/dist/cli/commands/status.js.map +1 -1
  17. package/dist/cli/index.js +7 -1
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/cli/tui.d.ts +52 -0
  20. package/dist/cli/tui.d.ts.map +1 -0
  21. package/dist/cli/tui.js +177 -0
  22. package/dist/cli/tui.js.map +1 -0
  23. package/dist/core/catalog.d.ts +10 -7
  24. package/dist/core/catalog.d.ts.map +1 -1
  25. package/dist/core/catalog.js +10 -2
  26. package/dist/core/catalog.js.map +1 -1
  27. package/dist/core/host-stats.d.ts +54 -0
  28. package/dist/core/host-stats.d.ts.map +1 -0
  29. package/dist/core/host-stats.js +148 -0
  30. package/dist/core/host-stats.js.map +1 -0
  31. package/dist/core/model-download.d.ts.map +1 -1
  32. package/dist/core/model-download.js +9 -16
  33. package/dist/core/model-download.js.map +1 -1
  34. package/dist/core/npm-version.d.ts +23 -0
  35. package/dist/core/npm-version.d.ts.map +1 -0
  36. package/dist/core/npm-version.js +73 -0
  37. package/dist/core/npm-version.js.map +1 -0
  38. package/dist/core/orchestrator.d.ts +9 -0
  39. package/dist/core/orchestrator.d.ts.map +1 -1
  40. package/dist/core/orchestrator.js +25 -4
  41. package/dist/core/orchestrator.js.map +1 -1
  42. package/dist/core/token-rate.d.ts +14 -0
  43. package/dist/core/token-rate.d.ts.map +1 -0
  44. package/dist/core/token-rate.js +36 -0
  45. package/dist/core/token-rate.js.map +1 -0
  46. package/dist/daemon/methods.d.ts +4 -0
  47. package/dist/daemon/methods.d.ts.map +1 -1
  48. package/dist/daemon/methods.js +58 -6
  49. package/dist/daemon/methods.js.map +1 -1
  50. package/dist/daemon/run.d.ts.map +1 -1
  51. package/dist/daemon/run.js +10 -0
  52. package/dist/daemon/run.js.map +1 -1
  53. package/dist/shared/protocol.d.ts +41 -1
  54. package/dist/shared/protocol.d.ts.map +1 -1
  55. package/dist/shared/protocol.js +1 -1
  56. package/package.json +2 -2
@@ -0,0 +1,9 @@
1
+ /** Multi-line PeerLLM block banner. Returns the full multi-row string. */
2
+ export declare function renderBanner(): string;
3
+ /** Inline colored "PeerLLM" wordmark — for one-line headers and prompts. */
4
+ export declare function renderBrand(): string;
5
+ /** Tagline shown under the banner in help / dashboard. */
6
+ export declare function renderTagline(version: string): string;
7
+ /** Banner + tagline, separated by blank lines. Suitable for --help and dashboard. */
8
+ export declare function renderHeader(version: string): string;
9
+ //# sourceMappingURL=banner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/cli/banner.ts"],"names":[],"mappings":"AAyBA,0EAA0E;AAC1E,wBAAgB,YAAY,IAAI,MAAM,CAYrC;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKrD;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,50 @@
1
+ import { styleText } from "node:util";
2
+ function paint(color, text) {
3
+ // util.styleText handles NO_COLOR + TTY detection automatically.
4
+ return styleText(Array.isArray(color) ? color : [color], text);
5
+ }
6
+ const LETTERS = {
7
+ P: ["██████╗ ", "██╔══██╗", "██████╔╝", "██╔═══╝ ", "██║ "],
8
+ E: ["███████╗", "██╔════╝", "█████╗ ", "██╔══╝ ", "███████╗"],
9
+ R: ["██████╗ ", "██╔══██╗", "██████╔╝", "██╔══██╗", "██║ ██║"],
10
+ L: ["██╗ ", "██║ ", "██║ ", "██║ ", "███████╗"],
11
+ M: ["███╗ ███╗", "████╗ ████║", "██╔████╔██║", "██║╚██╔╝██║", "██║ ╚═╝ ██║"],
12
+ };
13
+ // Each banner letter is colored to match the requested branding:
14
+ // Peer = blue, LL = green, M = red.
15
+ const SEGMENTS = [
16
+ { letters: "PEER", color: "blue" },
17
+ { letters: "LL", color: "green" },
18
+ { letters: "M", color: "red" },
19
+ ];
20
+ /** Multi-line PeerLLM block banner. Returns the full multi-row string. */
21
+ export function renderBanner() {
22
+ const rows = ["", "", "", "", ""];
23
+ for (const seg of SEGMENTS) {
24
+ for (const ch of seg.letters) {
25
+ const letter = LETTERS[ch];
26
+ if (!letter)
27
+ continue;
28
+ for (let r = 0; r < 5; r++) {
29
+ rows[r] += paint(seg.color, letter[r] ?? "");
30
+ }
31
+ }
32
+ }
33
+ return rows.join("\n");
34
+ }
35
+ /** Inline colored "PeerLLM" wordmark — for one-line headers and prompts. */
36
+ export function renderBrand() {
37
+ return (paint(["blue", "bold"], "Peer") +
38
+ paint(["green", "bold"], "LL") +
39
+ paint(["red", "bold"], "M"));
40
+ }
41
+ /** Tagline shown under the banner in help / dashboard. */
42
+ export function renderTagline(version) {
43
+ return (paint("dim", " Serve decentralized AI compute from this machine ") +
44
+ paint("gray", `· v${version}`));
45
+ }
46
+ /** Banner + tagline, separated by blank lines. Suitable for --help and dashboard. */
47
+ export function renderHeader(version) {
48
+ return `\n${renderBanner()}\n\n${renderTagline(version)}\n`;
49
+ }
50
+ //# sourceMappingURL=banner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"banner.js","sourceRoot":"","sources":["../../src/cli/banner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAItC,SAAS,KAAK,CAAC,KAAsB,EAAE,IAAY;IACjD,iEAAiE;IACjE,OAAO,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,OAAO,GAA6B;IACxC,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC;IAC/D,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC;IAC/D,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC;IAC/D,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC;IAC/D,CAAC,EAAE,CAAC,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,CAAC;CAC/E,CAAC;AAEF,iEAAiE;AACjE,sCAAsC;AACtC,MAAM,QAAQ,GAA6C;IACzD,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;IAClC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;IACjC,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE;CAC/B,CAAC;AAEF,0EAA0E;AAC1E,MAAM,UAAU,YAAY;IAC1B,MAAM,IAAI,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAClC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;YAC3B,IAAI,CAAC,MAAM;gBAAE,SAAS;YACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC3B,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,WAAW;IACzB,OAAO,CACL,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAC/B,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC;QAC9B,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,CAC5B,CAAC;AACJ,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,OAAO,CACL,KAAK,CAAC,KAAK,EAAE,sDAAsD,CAAC;QACpE,KAAK,CAAC,MAAM,EAAE,MAAM,OAAO,EAAE,CAAC,CAC/B,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,KAAK,YAAY,EAAE,OAAO,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ export declare function runDashboard(ipcEndpoint: string, modelsDir: string, intervalMs?: number): Promise<void>;
3
+ export declare function makeDashboardCommand(): Command;
4
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dashboard.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqepC,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,UAAU,SAAO,GAChB,OAAO,CAAC,IAAI,CAAC,CA2Nf;AAED,wBAAgB,oBAAoB,IAAI,OAAO,CAU9C"}
@@ -0,0 +1,572 @@
1
+ import { cpus, totalmem, freemem } from "node:os";
2
+ import { readdirSync, existsSync } from "node:fs";
3
+ import { Command } from "commander";
4
+ import { graphics, fsSize } from "systeminformation";
5
+ import { resolvePaths } from "../../core/paths.js";
6
+ import { METHOD } from "../../shared/protocol.js";
7
+ import { renderBanner, renderBrand } from "../banner.js";
8
+ import { EXIT } from "../exit-codes.js";
9
+ import { printError } from "../format.js";
10
+ import { DaemonUnreachableError, IpcClient } from "../ipc-client.js";
11
+ import { ANSI, bar, centerInWidth, enterTui, moveTo, padRight, padVisible, paint, sparkline, truncate, truncateVisible, visibleLength, write, } from "../tui.js";
12
+ const MAX_CPU_SAMPLES = 60;
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
16
+ function readCpuSnapshot() {
17
+ let idle = 0;
18
+ let total = 0;
19
+ for (const c of cpus()) {
20
+ const t = c.times;
21
+ idle += t.idle;
22
+ total += t.user + t.nice + t.sys + t.idle + t.irq;
23
+ }
24
+ return { idle, total };
25
+ }
26
+ function statusBadge(s) {
27
+ switch (s) {
28
+ case "connected":
29
+ return paint(["green", "bold"], "● connected");
30
+ case "connecting":
31
+ return paint(["yellow", "bold"], "◐ connecting");
32
+ case "error":
33
+ return paint(["red", "bold"], "✗ error");
34
+ default:
35
+ return paint("dim", "○ disconnected");
36
+ }
37
+ }
38
+ function formatUptime(seconds) {
39
+ if (seconds < 60)
40
+ return `${seconds}s`;
41
+ const m = Math.floor(seconds / 60);
42
+ if (m < 60)
43
+ return `${m}m ${seconds % 60}s`;
44
+ const h = Math.floor(m / 60);
45
+ if (h < 24)
46
+ return `${h}h ${m % 60}m`;
47
+ const d = Math.floor(h / 24);
48
+ return `${d}d ${h % 24}h`;
49
+ }
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`.
55
+ return paint("dim", padRight(label, labelWidth)) + value;
56
+ }
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));
62
+ }
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}`;
77
+ }
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));
99
+ }
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)) +
146
+ paint("dim", " ") +
147
+ row("total up", paint("gray", s.host.totalUptime), 12));
148
+ lines.push(row("conversations", paint("cyan", s.host.conversations.toLocaleString())) +
149
+ paint("dim", " ") +
150
+ row("avg resp", `${s.host.averageResponseMs.toFixed(0)} ms`, 12));
151
+ }
152
+ else {
153
+ lines.push(row("status", na("backend stats loading…")));
154
+ }
155
+ return lines;
156
+ }
157
+ function buildModelPanel(state) {
158
+ if (state.models?.loaded) {
159
+ const m = state.models.loaded;
160
+ return [
161
+ row("loaded", paint(["bold", "green"], m.modelName)),
162
+ row("gpu layers", String(m.gpuLayers)) +
163
+ paint("dim", " ") +
164
+ row("active conv.", paint("cyan", String(m.activeContexts)), 16),
165
+ ];
166
+ }
167
+ return [
168
+ paint("dim", "(no model loaded — ") +
169
+ paint(["bold", "cyan"], "peerllm-host models download") +
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…")));
183
+ }
184
+ else if (state.gpu === null) {
185
+ lines.push(row("GPU", na("no GPU detected")));
186
+ }
187
+ else {
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`));
202
+ }
203
+ }
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") +
328
+ keyHint("r", "refresh now") +
329
+ keyHint("l", "clear logs") +
330
+ keyHint("?", "help");
331
+ write(renderPanel("Keys", undefined, [help.trimEnd()], width) + "\n");
332
+ }
333
+ function padSparkline(values, width) {
334
+ if (values.length >= width)
335
+ return values.slice(-width);
336
+ const pad = new Array(width - values.length).fill(0);
337
+ return pad.concat(values);
338
+ }
339
+ function keyHint(key, label) {
340
+ return paint(["cyan", "bold"], `[${key}]`) + paint("dim", ` ${label} `);
341
+ }
342
+ function formatLog(l, maxWidth) {
343
+ const levelColors = {
344
+ debug: "gray",
345
+ info: "cyan",
346
+ warn: "yellow",
347
+ error: "red",
348
+ };
349
+ const color = levelColors[l.level] ?? "gray";
350
+ const ts = l.timestamp ? new Date(l.timestamp).toLocaleTimeString() : "";
351
+ const prefix = paint("dim", ts + " ") + paint(color, l.level.padEnd(5));
352
+ const visiblePrefixLen = ts.length + 1 + 5;
353
+ return prefix + " " + truncate(l.message, Math.max(0, maxWidth - visiblePrefixLen - 1));
354
+ }
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);
366
+ }
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();
392
+ }
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();
400
+ }
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));
403
+ }
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
+ };
416
+ }
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 () => {
426
+ try {
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;
434
+ }
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));
448
+ }
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;
462
+ try {
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
+ }
479
+ }
480
+ catch {
481
+ // Non-fatal: GPU info is best-effort.
482
+ }
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
+ }
508
+ }
509
+ catch {
510
+ // Non-fatal.
511
+ }
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
+ : [];
519
+ }
520
+ catch {
521
+ // Non-fatal.
522
+ }
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);
570
+ });
571
+ }
572
+ //# sourceMappingURL=dashboard.js.map