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 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
- # dsh-vitals
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
- A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) Web-GUI plugin that shows **live hardware load** right in the web GUI — a self-contained, dependency-free companion to `btop`. Opens as a full tab next to Chat/Trajectory, or docked beside a session in the right sidebar.
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. The dedicated way to watch the box while you're not
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
- | Signal | Primary source | Fallback |
36
- | ------ | -------------- | -------- |
37
- | CPU load | `/proc/stat` (Linux) | `os.cpus()` deltas (macOS / other) |
38
- | Memory | `/proc/meminfo` (Linux) | `os.totalmem()` / `os.freemem()` |
39
- | CPU temp | `/sys/class/thermal` | hidden when absent |
40
- | GPUs | `nvidia-smi` (any count) | hidden when absent |
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 simply hides that panel or
43
- shows a placeholder rather than breaking the tab. Any number of GPUs is
44
- supported; any CPU (core count and vendor) is handled.
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
- Not on the npm registry (yet) — install straight from GitHub, into your dsh
49
- **web profile** (`web` below is the profile name; use whichever profile backs
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 github:JoblessJoe/dsh-vitals
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 github:JoblessJoe/dsh-vitals`, then add
60
- `"dsh-vitals"` to that `package.json`'s `dsh.profile.bundles` array
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 repo ships its built `lib/` output committed, so no build step runs on
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 = color != null ? color : barColor(pct);
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 inject = ["slots", "locale", "sidebarRightTabs"];
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 t = totalmem(), f = freemem();
80
- totalKb = Math.round(t / 1024);
81
- availKb = Math.round(f / 1024);
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
- function nvidiaSmi() {
111
- return new Promise((resolve) => {
112
- const args = [
113
- "--query-gpu=index,name,temperature.gpu,utilization.gpu,power.draw,memory.used,memory.total",
114
- "--format=csv,noheader,nounits"
115
- ];
116
- execFile("nvidia-smi", args, { timeout: 800 }, (err, stdout) => {
117
- if (err) {
118
- resolve(null);
119
- return;
120
- }
121
- resolve(stdout.trim());
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 sample();
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.0",
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": ["System", "Web UI", "Observability"],
32
- "surfaces": ["host", "web"],
33
- "capabilities": { "provides": ["service:vitals"] },
34
- "compatibility": { "dsh": ">=0.1.0-rc.5", "node": ">=20" },
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": ["darwin", "linux"],
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"