dsh-vitals 0.1.0 → 0.1.8
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/LICENSE +21 -0
- package/README.md +51 -26
- package/lib/client.js +88 -37
- package/lib/index.js +101 -21
- package/package.json +26 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johannes Tebbert
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/JoblessJoe/dsh-vitals/main/.github/assets/banner.svg" alt="dsh-vitals: live CPU, memory, temperature and GPU load inside the DeepSeek Harness web GUI" width="100%">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/dsh-vitals"><img src="https://img.shields.io/npm/v/dsh-vitals?color=34d399&label=npm" alt="npm version"></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/dsh-vitals"><img src="https://img.shields.io/npm/dm/dsh-vitals?color=34d399" alt="npm downloads"></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/dependencies-0-34d399" alt="zero runtime dependencies">
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/JoblessJoe/dsh-vitals?color=34d399" alt="MIT license"></a>
|
|
10
|
+
<a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://img.shields.io/badge/awesome-dsh--plugin-fc60a8" alt="listed on awesome-dsh-plugin"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) web GUI plugin that shows **live hardware load** right where you work: a self-contained, dependency-free companion to `btop`. Watch your GPU while your local model thinks, without leaving the chat.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
dsh plugin --profile web add dsh-vitals
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Restart dsh: CPU and GPU load appear in the session header, and the full **Hardware** tab sits next to Chat / Trajectory. Full details under [Install](#install).
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/JoblessJoe/dsh-vitals/main/.github/assets/hardware-tab.png" alt="The Hardware tab: per-core CPU load, memory and two GPUs" width="100%">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/JoblessJoe/dsh-vitals/main/.github/assets/header-widget.png" alt="Mini CPU and GPU load widget in the session header" width="520">
|
|
27
|
+
<br><sub>The mini widget in the session header: always visible while you chat. Click it to jump to the full Hardware tab.</sub>
|
|
28
|
+
</p>
|
|
4
29
|
|
|
5
30
|
## What it shows
|
|
6
31
|
|
|
@@ -22,46 +47,46 @@ Data is sampled host-side on every poll (~0.5 s) and rendered live.
|
|
|
22
47
|
|
|
23
48
|
Two access points, both live the moment the plugin is bundled — no setup:
|
|
24
49
|
|
|
50
|
+
- **A mini widget in the session header**, next to the "⋯" menu: CPU and
|
|
51
|
+
per-GPU load as tiny bars, always visible while you chat. Hover for temps
|
|
52
|
+
and VRAM; click to jump to the full tab.
|
|
25
53
|
- **A full `Hardware` tab** next to **Chat** / **Trajectory** at the top of
|
|
26
|
-
the conversation view
|
|
27
|
-
actively chatting.
|
|
28
|
-
- **Docked in the right sidebar**, alongside a session: open the right
|
|
29
|
-
sidebar, click **+** (add tab), pick **Hardware** from the Guide list. Lets
|
|
30
|
-
you chat and watch load at the same time. (Every dsh "page type" plugin
|
|
31
|
-
opens this way — nothing dsh-vitals-specific about the click path.)
|
|
54
|
+
the conversation view: per-core CPU, memory, temperatures and every GPU.
|
|
32
55
|
|
|
33
56
|
### Platform support
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
58
|
+
Linux, macOS and Windows. Each signal uses the best unprivileged source the OS
|
|
59
|
+
offers and hides itself when there is none:
|
|
60
|
+
|
|
61
|
+
| Signal | Linux | macOS | Windows |
|
|
62
|
+
| ------ | ----- | ----- | ------- |
|
|
63
|
+
| CPU load (overall + per core) | `/proc/stat` | `os.cpus()` | `os.cpus()` |
|
|
64
|
+
| Memory | `/proc/meminfo` | `vm_stat` (counts cache as available, like Activity Monitor) | `os.freemem()` |
|
|
65
|
+
| CPU temperature | `/sys/class/thermal` | hidden (needs root) | WMI thermal zones, when the board exposes them |
|
|
66
|
+
| NVIDIA GPUs | `nvidia-smi` | `nvidia-smi` | `nvidia-smi.exe` (PATH or `NVSMI` folder) |
|
|
67
|
+
| Apple / other GPUs | — | `ioreg` utilisation + memory in use | — |
|
|
41
68
|
|
|
42
|
-
Everything degrades gracefully: a missing source
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
Everything degrades gracefully: a missing source hides that panel or shows a
|
|
70
|
+
placeholder instead of breaking the tab. Any number of GPUs and any CPU core
|
|
71
|
+
count is supported.
|
|
45
72
|
|
|
46
73
|
## Install
|
|
47
74
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
your web GUI):
|
|
75
|
+
From your dsh **web profile** (`web` below is the profile name; use whichever
|
|
76
|
+
profile backs your web GUI):
|
|
51
77
|
|
|
52
78
|
```bash
|
|
53
|
-
dsh plugin --profile web add
|
|
79
|
+
dsh plugin --profile web add dsh-vitals
|
|
54
80
|
```
|
|
55
81
|
|
|
56
82
|
This installs the package into the profile and adds it to
|
|
57
83
|
`dsh.profile.bundles` for you — no manual `package.json` editing. (No local
|
|
58
84
|
`dsh` binary? Run the equivalent by hand from the profile directory, e.g.
|
|
59
|
-
`~/.dsh/profiles/web/`: `pnpm add
|
|
60
|
-
|
|
61
|
-
yourself.)
|
|
85
|
+
`~/.dsh/profiles/web/`: `pnpm add dsh-vitals`, then add `"dsh-vitals"` to
|
|
86
|
+
that `package.json`'s `dsh.profile.bundles` array yourself.)
|
|
62
87
|
|
|
63
|
-
The
|
|
64
|
-
install — nothing else to do before restarting.
|
|
88
|
+
The published package ships its built `lib/` output, so no build step runs
|
|
89
|
+
on install — nothing else to do before restarting.
|
|
65
90
|
|
|
66
91
|
Restart your dsh web service and open the web GUI — see
|
|
67
92
|
[Where it shows up](#where-it-shows-up) above for how to find it.
|
package/lib/client.js
CHANGED
|
@@ -24,7 +24,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
24
24
|
var index_exports = {};
|
|
25
25
|
__export(index_exports, {
|
|
26
26
|
HARDWARE_ID: () => HARDWARE_ID,
|
|
27
|
-
HARDWARE_KIND: () => HARDWARE_KIND,
|
|
28
27
|
apply: () => apply,
|
|
29
28
|
inject: () => inject
|
|
30
29
|
});
|
|
@@ -33,8 +32,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
33
32
|
// src/client/locales.ts
|
|
34
33
|
var zh = {
|
|
35
34
|
"tab.title": "\u786C\u4EF6",
|
|
36
|
-
"guide.title": "\u786C\u4EF6\u76D1\u63A7",
|
|
37
|
-
"guide.description": "\u5B9E\u65F6 CPU / \u5185\u5B58 / \u6E29\u5EA6 / GPU \u8D1F\u8F7D",
|
|
38
35
|
"section.cpu": "CPU",
|
|
39
36
|
"section.mem": "\u5185\u5B58",
|
|
40
37
|
"section.temp": "CPU \u6E29\u5EA6",
|
|
@@ -50,12 +47,11 @@ var zh = {
|
|
|
50
47
|
"gpu.temp": "\u6E29\u5EA6",
|
|
51
48
|
"loading": "\u8BFB\u53D6\u4E2D\u2026",
|
|
52
49
|
"error": "\u8BFB\u53D6\u5931\u8D25",
|
|
53
|
-
"retry": "\u91CD\u8BD5"
|
|
50
|
+
"retry": "\u91CD\u8BD5",
|
|
51
|
+
"mini.open": "\u6253\u5F00\u786C\u4EF6\u76D1\u63A7"
|
|
54
52
|
};
|
|
55
53
|
var en = {
|
|
56
54
|
"tab.title": "Hardware",
|
|
57
|
-
"guide.title": "Hardware",
|
|
58
|
-
"guide.description": "Live CPU / memory / temps / GPU load",
|
|
59
55
|
"section.cpu": "CPU",
|
|
60
56
|
"section.mem": "Memory",
|
|
61
57
|
"section.temp": "CPU Temp",
|
|
@@ -71,28 +67,10 @@ var en = {
|
|
|
71
67
|
"gpu.temp": "Temp",
|
|
72
68
|
"loading": "Loading\u2026",
|
|
73
69
|
"error": "Read failed",
|
|
74
|
-
"retry": "Retry"
|
|
70
|
+
"retry": "Retry",
|
|
71
|
+
"mini.open": "Open hardware monitor"
|
|
75
72
|
};
|
|
76
73
|
|
|
77
|
-
// src/client/definition.ts
|
|
78
|
-
var HARDWARE_KIND = "hardware";
|
|
79
|
-
var HARDWARE_ID = "dsh-vitals";
|
|
80
|
-
function hardwareDefinition(t) {
|
|
81
|
-
return {
|
|
82
|
-
id: HARDWARE_ID,
|
|
83
|
-
kind: HARDWARE_KIND,
|
|
84
|
-
priority: "extension",
|
|
85
|
-
title: () => t("tab.title"),
|
|
86
|
-
guide: [
|
|
87
|
-
{
|
|
88
|
-
order: 1e3,
|
|
89
|
-
title: () => t("guide.title"),
|
|
90
|
-
description: () => t("guide.description")
|
|
91
|
-
}
|
|
92
|
-
]
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
74
|
// src/client/body.tsx
|
|
97
75
|
var import_react = require("react");
|
|
98
76
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -157,8 +135,8 @@ var gb = (kb) => (kb / 1024 / 1024).toFixed(1);
|
|
|
157
135
|
var pct1 = (n) => n == null ? "\u2014" : `${Math.round(n)}%`;
|
|
158
136
|
var n1 = (n) => n == null ? "\u2014" : n.toFixed(1);
|
|
159
137
|
var mb = (mbv) => mbv == null ? "\u2014" : `${Math.round(mbv)} MB`;
|
|
160
|
-
function Bar({ pct, color }) {
|
|
161
|
-
const c =
|
|
138
|
+
function Bar({ pct, color: color2 }) {
|
|
139
|
+
const c = color2 != null ? color2 : barColor(pct);
|
|
162
140
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.track, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...S.fill, width: `${Math.max(0, Math.min(100, pct))}%`, background: c } }) });
|
|
163
141
|
}
|
|
164
142
|
function Sparkline({ samples }) {
|
|
@@ -349,23 +327,96 @@ function HardwareBody({ t }) {
|
|
|
349
327
|
] });
|
|
350
328
|
}
|
|
351
329
|
|
|
330
|
+
// src/client/mini.tsx
|
|
331
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
332
|
+
var ok = "#3fb950";
|
|
333
|
+
var warn = "#d29922";
|
|
334
|
+
var bad = "#f85149";
|
|
335
|
+
var color = (pct) => pct >= 85 ? bad : pct >= 60 ? warn : ok;
|
|
336
|
+
var S2 = {
|
|
337
|
+
button: {
|
|
338
|
+
display: "inline-flex",
|
|
339
|
+
alignItems: "center",
|
|
340
|
+
gap: 10,
|
|
341
|
+
height: 26,
|
|
342
|
+
padding: "0 8px",
|
|
343
|
+
background: "transparent",
|
|
344
|
+
border: "1px solid transparent",
|
|
345
|
+
borderRadius: 6,
|
|
346
|
+
color: "inherit",
|
|
347
|
+
cursor: "pointer",
|
|
348
|
+
opacity: 0.85,
|
|
349
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
350
|
+
fontSize: 11
|
|
351
|
+
},
|
|
352
|
+
cell: { display: "inline-flex", alignItems: "center", gap: 5 },
|
|
353
|
+
label: { opacity: 0.6 },
|
|
354
|
+
track: { width: 28, height: 5, borderRadius: 3, background: "rgba(127,127,127,0.25)", overflow: "hidden" },
|
|
355
|
+
value: { minWidth: 28, textAlign: "right" }
|
|
356
|
+
};
|
|
357
|
+
function Cell({ label, pct }) {
|
|
358
|
+
const p = Math.max(0, Math.min(100, pct));
|
|
359
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: S2.cell, children: [
|
|
360
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: S2.label, children: label }),
|
|
361
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: S2.track, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { display: "block", height: "100%", width: `${p}%`, background: color(p), transition: "width 0.3s ease" } }) }),
|
|
362
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: S2.value, children: [
|
|
363
|
+
Math.round(p),
|
|
364
|
+
"%"
|
|
365
|
+
] })
|
|
366
|
+
] });
|
|
367
|
+
}
|
|
368
|
+
function openHardwareTab(e, label) {
|
|
369
|
+
var _a, _b;
|
|
370
|
+
const header = e.currentTarget.closest("header");
|
|
371
|
+
const tabs = (_a = header == null ? void 0 : header.querySelectorAll('[role="tab"]')) != null ? _a : [];
|
|
372
|
+
for (const tab of tabs) if (((_b = tab.textContent) == null ? void 0 : _b.trim()) === label) {
|
|
373
|
+
tab.click();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function HeaderVitals({ t }) {
|
|
378
|
+
const { data, cpuPct } = useHardwareData();
|
|
379
|
+
if (!data) return null;
|
|
380
|
+
const multi = data.gpus.length > 1;
|
|
381
|
+
const tip = [
|
|
382
|
+
`CPU ${Math.round(cpuPct)}%${data.temp.overall == null ? "" : ` \xB7 ${Math.round(data.temp.overall)}\xB0C`}`,
|
|
383
|
+
`${t("section.mem")} ${Math.round(data.mem.usedPct)}%`,
|
|
384
|
+
...data.gpus.map((g) => {
|
|
385
|
+
var _a;
|
|
386
|
+
return `GPU ${g.index} ${g.name}: ${(_a = g.util) != null ? _a : "\u2014"}%${g.temp == null ? "" : ` \xB7 ${Math.round(g.temp)}\xB0C`}` + (g.memUsedMb == null || g.memTotalMb == null ? "" : ` \xB7 ${Math.round(g.memUsedMb)} / ${Math.round(g.memTotalMb)} MB`);
|
|
387
|
+
}),
|
|
388
|
+
t("mini.open")
|
|
389
|
+
].join("\n");
|
|
390
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { type: "button", style: S2.button, title: tip, "aria-label": t("mini.open"), onClick: (e) => {
|
|
391
|
+
openHardwareTab(e, t("tab.title"));
|
|
392
|
+
}, children: [
|
|
393
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Cell, { label: "CPU", pct: cpuPct }),
|
|
394
|
+
data.gpus.map((g) => {
|
|
395
|
+
var _a;
|
|
396
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Cell, { label: multi ? `GPU${g.index}` : "GPU", pct: (_a = g.util) != null ? _a : 0 }, g.index);
|
|
397
|
+
})
|
|
398
|
+
] });
|
|
399
|
+
}
|
|
400
|
+
|
|
352
401
|
// src/client/index.tsx
|
|
353
|
-
var
|
|
402
|
+
var HARDWARE_ID = "dsh-vitals";
|
|
403
|
+
var inject = ["slots", "locale"];
|
|
354
404
|
function apply(ctx) {
|
|
355
405
|
const t = ctx.locale.bind("hardwareMonitor");
|
|
356
406
|
ctx.effect(() => ctx.locale.register("hardwareMonitor", { zh, en }), "dsh-vitals: dictionaries");
|
|
357
|
-
ctx.effect(
|
|
358
|
-
() => ctx.sidebarRightTabs.register(hardwareDefinition(t)),
|
|
359
|
-
"dsh-vitals: hardware type"
|
|
360
|
-
);
|
|
361
|
-
ctx.effect(
|
|
362
|
-
() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({ name: "sidebar.right.pane.tab", key: HARDWARE_ID, locale: "hardwareMonitor" }, HardwareBody)),
|
|
363
|
-
"dsh-vitals: body seat"
|
|
364
|
-
);
|
|
365
407
|
ctx.effect(
|
|
366
408
|
() => ctx.slots.inject("conversation.view", () => ctx.slots.register({ name: "conversation.view", id: HARDWARE_ID, order: 20, locale: "hardwareMonitor", label: () => t("tab.title") }, HardwareBody)),
|
|
367
409
|
"dsh-vitals: conversation tab"
|
|
368
410
|
);
|
|
411
|
+
ctx.effect(
|
|
412
|
+
() => ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
413
|
+
name: "conversation.session.header.utilities",
|
|
414
|
+
id: HARDWARE_ID,
|
|
415
|
+
order: -20,
|
|
416
|
+
locale: "hardwareMonitor"
|
|
417
|
+
}, HeaderVitals)),
|
|
418
|
+
"dsh-vitals: header mini view"
|
|
419
|
+
);
|
|
369
420
|
}
|
|
370
421
|
|
|
371
422
|
return module.exports;
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,67 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
3
|
import { cpus, freemem, totalmem } from "node:os";
|
|
4
|
+
|
|
5
|
+
// src/platform.ts
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
function run(cmd, args, timeout = 1500) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
execFile(cmd, args, { timeout, windowsHide: true }, (err, stdout) => {
|
|
10
|
+
resolve(err ? null : String(stdout));
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function parseVmStat(out) {
|
|
15
|
+
const page = Number(out.match(/page size of (\d+) bytes/)?.[1]);
|
|
16
|
+
if (!Number.isFinite(page) || page <= 0) return null;
|
|
17
|
+
const pages = (key) => Number(out.match(new RegExp(`^${key}:\\s+(\\d+)`, "m"))?.[1] ?? 0);
|
|
18
|
+
const avail = pages("Pages free") + pages("Pages inactive") + pages("Pages speculative") + pages("Pages purgeable");
|
|
19
|
+
return avail > 0 ? avail * page : null;
|
|
20
|
+
}
|
|
21
|
+
function parseIoreg(out) {
|
|
22
|
+
const gpus = [];
|
|
23
|
+
for (const block of out.split(/^\+-o /m).slice(1)) {
|
|
24
|
+
const util = block.match(/"Device Utilization %"=(\d+)/);
|
|
25
|
+
if (!util) continue;
|
|
26
|
+
const model = block.match(/"model"\s*=\s*"([^"]+)"/)?.[1] ?? block.match(/^(\S+)/)?.[1] ?? "GPU";
|
|
27
|
+
const used = block.match(/"In use system memory"=(\d+)/);
|
|
28
|
+
gpus.push({
|
|
29
|
+
name: model,
|
|
30
|
+
util: Number(util[1]),
|
|
31
|
+
memUsedMb: used ? Math.round(Number(used[1]) / 1024 / 1024) : null
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return gpus;
|
|
35
|
+
}
|
|
36
|
+
function parseWmiTemps(out) {
|
|
37
|
+
return out.split(/\r?\n/).map((s) => Number(s.trim())).filter((n) => Number.isFinite(n) && n > 0).map((dk) => Math.round((dk / 10 - 273.15) * 10) / 10).filter((c) => c > 0 && c < 150);
|
|
38
|
+
}
|
|
39
|
+
var WMI_TEMP = [
|
|
40
|
+
"-NoProfile",
|
|
41
|
+
"-NonInteractive",
|
|
42
|
+
"-Command",
|
|
43
|
+
"Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature | ForEach-Object { $_.CurrentTemperature }"
|
|
44
|
+
];
|
|
45
|
+
var winTemps = [];
|
|
46
|
+
var winTempAt = 0;
|
|
47
|
+
var winTempOff = false;
|
|
48
|
+
var winTempBusy = false;
|
|
49
|
+
function windowsTemps() {
|
|
50
|
+
if (!winTempOff && !winTempBusy && Date.now() - winTempAt > 1e4) {
|
|
51
|
+
winTempBusy = true;
|
|
52
|
+
void run("powershell.exe", WMI_TEMP, 5e3).then((out) => {
|
|
53
|
+
const temps = out ? parseWmiTemps(out) : [];
|
|
54
|
+
if (temps.length === 0) winTempOff = true;
|
|
55
|
+
winTemps = temps;
|
|
56
|
+
winTempAt = Date.now();
|
|
57
|
+
winTempBusy = false;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return winTemps;
|
|
61
|
+
}
|
|
62
|
+
var NVIDIA_SMI = process.platform === "win32" ? ["nvidia-smi", "C:\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe"] : ["nvidia-smi"];
|
|
63
|
+
|
|
64
|
+
// src/index.ts
|
|
5
65
|
var inject = ["webServer"];
|
|
6
66
|
function parseStat(raw) {
|
|
7
67
|
const cores = [];
|
|
@@ -65,7 +125,7 @@ function sampleCpu() {
|
|
|
65
125
|
return { total, cores, model: cpuModelOnce() };
|
|
66
126
|
}
|
|
67
127
|
}
|
|
68
|
-
function sampleMem() {
|
|
128
|
+
async function sampleMem() {
|
|
69
129
|
let totalKb = null, availKb = null;
|
|
70
130
|
try {
|
|
71
131
|
const g = {};
|
|
@@ -76,9 +136,9 @@ function sampleMem() {
|
|
|
76
136
|
totalKb = g.MemTotal ?? null;
|
|
77
137
|
availKb = g.MemAvailable ?? (g.MemFree ?? 0) + (g.Buffers ?? 0) + (g.Cached ?? 0);
|
|
78
138
|
} catch {
|
|
79
|
-
const
|
|
80
|
-
totalKb = Math.round(
|
|
81
|
-
availKb = Math.round(
|
|
139
|
+
const mac = process.platform === "darwin" ? parseVmStat(await run("vm_stat", []) ?? "") : null;
|
|
140
|
+
totalKb = Math.round(totalmem() / 1024);
|
|
141
|
+
availKb = Math.round((mac ?? freemem()) / 1024);
|
|
82
142
|
}
|
|
83
143
|
if (totalKb == null || totalKb <= 0 || availKb == null) {
|
|
84
144
|
return { totalKb: 0, availKb: 0, usedKb: 0, usedPct: 0 };
|
|
@@ -104,26 +164,40 @@ async function sampleTemp() {
|
|
|
104
164
|
}
|
|
105
165
|
} catch {
|
|
106
166
|
}
|
|
167
|
+
if (process.platform === "win32") windowsTemps().forEach((temp, i) => zones.push({ name: `zone ${i}`, temp }));
|
|
107
168
|
const overall = zones.length ? Math.max(...zones.map((z) => z.temp)) : null;
|
|
108
169
|
return { overall, zones };
|
|
109
170
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
171
|
+
var SMI_ARGS = [
|
|
172
|
+
"--query-gpu=index,name,temperature.gpu,utilization.gpu,power.draw,memory.used,memory.total",
|
|
173
|
+
"--format=csv,noheader,nounits"
|
|
174
|
+
];
|
|
175
|
+
var smiPath = null;
|
|
176
|
+
async function nvidiaSmi() {
|
|
177
|
+
for (const cmd of smiPath ? [smiPath] : NVIDIA_SMI) {
|
|
178
|
+
const out = await run(cmd, SMI_ARGS, 800);
|
|
179
|
+
if (out !== null) {
|
|
180
|
+
smiPath = cmd;
|
|
181
|
+
return out.trim();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
async function macGpus() {
|
|
187
|
+
const out = await run("ioreg", ["-r", "-d", "1", "-c", "IOAccelerator"]);
|
|
188
|
+
return (out ? parseIoreg(out) : []).map((g, index) => ({
|
|
189
|
+
index,
|
|
190
|
+
name: g.name,
|
|
191
|
+
temp: null,
|
|
192
|
+
util: g.util,
|
|
193
|
+
powerW: null,
|
|
194
|
+
memUsedMb: g.memUsedMb,
|
|
195
|
+
memTotalMb: null
|
|
196
|
+
}));
|
|
124
197
|
}
|
|
125
198
|
async function sampleGpus() {
|
|
126
199
|
const out = await nvidiaSmi();
|
|
200
|
+
if ((out === null || out.length === 0) && process.platform === "darwin") return { gpus: await macGpus(), gpuError: null };
|
|
127
201
|
if (out === null || out.length === 0) return { gpus: [], gpuError: null };
|
|
128
202
|
const gpus = [];
|
|
129
203
|
for (const line of out.split("\n")) {
|
|
@@ -155,7 +229,7 @@ async function sample() {
|
|
|
155
229
|
errors.push(`cpu: ${e.message}`);
|
|
156
230
|
}
|
|
157
231
|
try {
|
|
158
|
-
mem = sampleMem();
|
|
232
|
+
mem = await sampleMem();
|
|
159
233
|
} catch (e) {
|
|
160
234
|
errors.push(`mem: ${e.message}`);
|
|
161
235
|
}
|
|
@@ -180,6 +254,12 @@ async function sample() {
|
|
|
180
254
|
errors
|
|
181
255
|
};
|
|
182
256
|
}
|
|
257
|
+
var SAMPLE_TTL_MS = 400;
|
|
258
|
+
var last = null;
|
|
259
|
+
function sharedSample() {
|
|
260
|
+
if (!last || Date.now() - last.at > SAMPLE_TTL_MS) last = { at: Date.now(), data: sample() };
|
|
261
|
+
return last.data;
|
|
262
|
+
}
|
|
183
263
|
var DATA_ENDPOINT = "/plugins/dsh-vitals/data";
|
|
184
264
|
function send(res, code, body) {
|
|
185
265
|
res.writeHead(code, {
|
|
@@ -202,7 +282,7 @@ function apply(ctx, _config) {
|
|
|
202
282
|
send(res, 405, JSON.stringify({ ok: false, error: "method not allowed" }));
|
|
203
283
|
return;
|
|
204
284
|
}
|
|
205
|
-
const data = await
|
|
285
|
+
const data = await sharedSample();
|
|
206
286
|
send(res, 200, JSON.stringify(data));
|
|
207
287
|
}
|
|
208
288
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vitals",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Live hardware load panel for the DeepSeek Harness web GUI: a btop-style tab showing CPU load per core and overall, DRAM usage, CPU temperatures, and per-GPU temperature/load/power/VRAM. Reads /proc and nvidia-smi on the host and polls ~1s in the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -28,10 +28,24 @@
|
|
|
28
28
|
"schemaVersion": 1,
|
|
29
29
|
"displayName": "Vitals",
|
|
30
30
|
"summary": "Live btop-style hardware monitor tab: CPU per-core + overall load, DRAM, CPU temps, and per-GPU temp/load/power/VRAM.",
|
|
31
|
-
"categories": [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
"categories": [
|
|
32
|
+
"System",
|
|
33
|
+
"Web UI",
|
|
34
|
+
"Observability"
|
|
35
|
+
],
|
|
36
|
+
"surfaces": [
|
|
37
|
+
"host",
|
|
38
|
+
"web"
|
|
39
|
+
],
|
|
40
|
+
"capabilities": {
|
|
41
|
+
"provides": [
|
|
42
|
+
"service:vitals"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"compatibility": {
|
|
46
|
+
"dsh": ">=0.1.0-rc.5",
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
35
49
|
"permissions": {}
|
|
36
50
|
},
|
|
37
51
|
"files": [
|
|
@@ -63,9 +77,14 @@
|
|
|
63
77
|
"engines": {
|
|
64
78
|
"node": ">=20"
|
|
65
79
|
},
|
|
66
|
-
"os": [
|
|
80
|
+
"os": [
|
|
81
|
+
"darwin",
|
|
82
|
+
"linux"
|
|
83
|
+
],
|
|
67
84
|
"scripts": {
|
|
68
|
-
"build": "node scripts/build.mjs"
|
|
85
|
+
"build": "node scripts/build.mjs",
|
|
86
|
+
"release": "git pull origin main && npm publish",
|
|
87
|
+
"test": "node test/platform.test.ts"
|
|
69
88
|
},
|
|
70
89
|
"devDependencies": {
|
|
71
90
|
"esbuild": "^0.28.2"
|