dsh-vitals 0.1.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/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # dsh-vitals
2
+
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.
4
+
5
+ ## What it shows
6
+
7
+ - **CPU** — overall utilisation, a live 60s sparkline, highest temperature reading, and per-core load (every physical core, any count) — with per-thermal-zone detail when there's more than one sensor
8
+ - **Memory** — used / available / total with utilisation %
9
+ - **GPUs** — one card each: name, temperature, utilisation, power draw, and VRAM (used / total)
10
+
11
+ Data is sampled host-side on every poll (~0.5 s) and rendered live.
12
+
13
+ ## How it works
14
+
15
+ - **Host** — a `GET`-only route registered on the dsh web server
16
+ (`/plugins/dsh-vitals/data`) that reads the OS's own sources
17
+ with **no privileged access** and returns JSON.
18
+ - **Client** — a single self-contained bundle that polls the route and draws
19
+ the panels with inline styles (no CSS pipeline, no assets).
20
+
21
+ ### Where it shows up
22
+
23
+ Two access points, both live the moment the plugin is bundled — no setup:
24
+
25
+ - **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.)
32
+
33
+ ### Platform support
34
+
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 |
41
+
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.
45
+
46
+ ## Install
47
+
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):
51
+
52
+ ```bash
53
+ dsh plugin --profile web add github:JoblessJoe/dsh-vitals
54
+ ```
55
+
56
+ This installs the package into the profile and adds it to
57
+ `dsh.profile.bundles` for you — no manual `package.json` editing. (No local
58
+ `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.)
62
+
63
+ The repo ships its built `lib/` output committed, so no build step runs on
64
+ install — nothing else to do before restarting.
65
+
66
+ Restart your dsh web service and open the web GUI — see
67
+ [Where it shows up](#where-it-shows-up) above for how to find it.
68
+
69
+ > New bundles are registered from the profile's `bundles` array, so a one-time
70
+ > service restart is required the first time you add it.
71
+
72
+ ## Building from source
73
+
74
+ Requires Node 20+. Dependencies are dev-only (esbuild); the published package
75
+ has **zero runtime npm dependencies**.
76
+
77
+ ```bash
78
+ git clone https://github.com/JoblessJoe/dsh-vitals.git
79
+ cd dsh-vitals
80
+ pnpm install
81
+ node scripts/build.mjs # emits lib/index.js + lib/client.js
82
+ ```
83
+
84
+ `package.json` declares a `build` script, so `pnpm build` works too.
85
+
86
+ ## Configuration
87
+
88
+ None. The plugin reads whatever the host exposes; there is no config surface.
89
+
90
+ ## Security
91
+
92
+ The data route sits outside the dsh web server's `/api` trust fence, so it
93
+ enforces that fence itself: `Sec-Fetch-Site: cross-site` requests are rejected
94
+ (403) and only `GET`/`HEAD` are served (405 otherwise). All sources are read-only.
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,7 @@
1
+ # dsh-vitals bundle mount.
2
+ # A NEW plugin contributes a brand-new profile entry, so the row is an `insert:`
3
+ # (a bare `- id:` would patch an existing entry by id and fail silently).
4
+ # Adding a new `insert:` requires a web-server restart to take effect.
5
+ - insert:
6
+ - id: dsh-vitals
7
+ name: 'dsh-vitals'
package/lib/client.js ADDED
@@ -0,0 +1,372 @@
1
+ // dsh-vitals client bundle (esbuild CJS + __ModuleLoader__ banner). Do not edit.
2
+ window.__ModuleLoader__.load({id:"dsh-vitals",factory:Pt=>{
3
+ var module={exports:{}},exports=module.exports,require=Pt;
4
+ "use strict";
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+
23
+ // src/client/index.tsx
24
+ var index_exports = {};
25
+ __export(index_exports, {
26
+ HARDWARE_ID: () => HARDWARE_ID,
27
+ HARDWARE_KIND: () => HARDWARE_KIND,
28
+ apply: () => apply,
29
+ inject: () => inject
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/client/locales.ts
34
+ var zh = {
35
+ "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
+ "section.cpu": "CPU",
39
+ "section.mem": "\u5185\u5B58",
40
+ "section.temp": "CPU \u6E29\u5EA6",
41
+ "section.gpu": "GPU",
42
+ "cpu.overall": "\u603B\u4F53",
43
+ "cpu.cores": "\u6BCF\u6838",
44
+ "mem.used": "\u5DF2\u7528",
45
+ "temp.max": "\u6700\u9AD8",
46
+ "gpu.none": "\u672A\u68C0\u6D4B\u5230 NVIDIA GPU",
47
+ "gpu.util": "\u8D1F\u8F7D",
48
+ "gpu.power": "\u529F\u8017",
49
+ "gpu.mem": "\u663E\u5B58",
50
+ "gpu.temp": "\u6E29\u5EA6",
51
+ "loading": "\u8BFB\u53D6\u4E2D\u2026",
52
+ "error": "\u8BFB\u53D6\u5931\u8D25",
53
+ "retry": "\u91CD\u8BD5"
54
+ };
55
+ var en = {
56
+ "tab.title": "Hardware",
57
+ "guide.title": "Hardware",
58
+ "guide.description": "Live CPU / memory / temps / GPU load",
59
+ "section.cpu": "CPU",
60
+ "section.mem": "Memory",
61
+ "section.temp": "CPU Temp",
62
+ "section.gpu": "GPU",
63
+ "cpu.overall": "Overall",
64
+ "cpu.cores": "Per-core",
65
+ "mem.used": "Used",
66
+ "temp.max": "Max",
67
+ "gpu.none": "No NVIDIA GPU detected",
68
+ "gpu.util": "Load",
69
+ "gpu.power": "Power",
70
+ "gpu.mem": "VRAM",
71
+ "gpu.temp": "Temp",
72
+ "loading": "Loading\u2026",
73
+ "error": "Read failed",
74
+ "retry": "Retry"
75
+ };
76
+
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
+ // src/client/body.tsx
97
+ var import_react = require("react");
98
+ var import_jsx_runtime = require("react/jsx-runtime");
99
+ var DATA_URL = "/plugins/dsh-vitals/data";
100
+ var HISTORY = 120;
101
+ var POLL_MS = 500;
102
+ var C = {
103
+ bg: "#0d1117",
104
+ panel: "#161b22",
105
+ border: "#30363d",
106
+ text: "#c9d1d9",
107
+ dim: "#8b949e",
108
+ track: "#21262d",
109
+ ok: "#3fb950",
110
+ warn: "#d29922",
111
+ bad: "#f85149",
112
+ accent: "#58a6ff"
113
+ };
114
+ var S = {
115
+ root: {
116
+ display: "flex",
117
+ flexDirection: "column",
118
+ gap: 10,
119
+ height: "100%",
120
+ padding: 12,
121
+ boxSizing: "border-box",
122
+ overflowY: "auto",
123
+ background: C.bg,
124
+ color: C.text,
125
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
126
+ fontSize: 13,
127
+ lineHeight: 1.4
128
+ },
129
+ box: {
130
+ background: C.panel,
131
+ border: `1px solid ${C.border}`,
132
+ borderRadius: 8,
133
+ padding: "10px 12px",
134
+ display: "flex",
135
+ flexDirection: "column",
136
+ gap: 8
137
+ },
138
+ head: { display: "flex", alignItems: "baseline", justifyContent: "space-between" },
139
+ title: { fontSize: 11, fontWeight: 700, letterSpacing: 1.2, textTransform: "uppercase", color: C.dim },
140
+ big: { fontSize: 26, fontWeight: 700, lineHeight: 1 },
141
+ row: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 },
142
+ dim: { color: C.dim, fontSize: 11 },
143
+ track: { flex: 1, height: 8, background: C.track, borderRadius: 4, overflow: "hidden" },
144
+ fill: { height: "100%", borderRadius: 4, transition: "width 0.3s ease" },
145
+ grid: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "4px 12px" },
146
+ chip: { display: "inline-block", minWidth: 18, textAlign: "right", color: C.dim, fontSize: 11 }
147
+ };
148
+ function barColor(pct) {
149
+ if (pct >= 85) return C.bad;
150
+ if (pct >= 60) return C.warn;
151
+ return C.ok;
152
+ }
153
+ var TEMP_LO = 30;
154
+ var TEMP_HI = 95;
155
+ var tempPct = (c) => Math.max(0, Math.min(100, (c - TEMP_LO) / (TEMP_HI - TEMP_LO) * 100));
156
+ var gb = (kb) => (kb / 1024 / 1024).toFixed(1);
157
+ var pct1 = (n) => n == null ? "\u2014" : `${Math.round(n)}%`;
158
+ var n1 = (n) => n == null ? "\u2014" : n.toFixed(1);
159
+ var mb = (mbv) => mbv == null ? "\u2014" : `${Math.round(mbv)} MB`;
160
+ function Bar({ pct, color }) {
161
+ const c = color != null ? color : barColor(pct);
162
+ 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
+ }
164
+ function Sparkline({ samples }) {
165
+ const w = 100, h = 40;
166
+ if (samples.length < 2) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { height: h } });
167
+ const step = w / (HISTORY - 1);
168
+ const n = samples.length;
169
+ const pts = samples.map((v, i) => {
170
+ const x = w - (n - 1 - i) * step;
171
+ const y = h - Math.max(0, Math.min(100, v)) / 100 * (h - 2) - 1;
172
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
173
+ });
174
+ const line = pts.join(" ");
175
+ const firstX = pts[0].split(",")[0];
176
+ const lastX = pts[pts.length - 1].split(",")[0];
177
+ const area = `${firstX},${h} ${line} ${lastX},${h}`;
178
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "100%", height: h, viewBox: `0 0 ${w} ${h}`, preserveAspectRatio: "none", style: { display: "block" }, children: [
179
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: area, fill: C.accent, fillOpacity: 0.15, stroke: "none" }),
180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: line, fill: "none", stroke: C.accent, strokeWidth: 1.5, vectorEffect: "non-scaling-stroke" })
181
+ ] });
182
+ }
183
+ function CoreBar({ idx, v }) {
184
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.row, children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.chip, children: idx }),
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Bar, { pct: v }),
187
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...S.chip, minWidth: 34 }, children: [
188
+ Math.round(v),
189
+ "%"
190
+ ] })
191
+ ] });
192
+ }
193
+ function GpuBox({ g, t }) {
194
+ var _a;
195
+ const memPct = g.memTotalMb && g.memUsedMb != null ? g.memUsedMb / g.memTotalMb * 100 : 0;
196
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.box, children: [
197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.head, children: [
198
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.title, children: `GPU ${g.index}` }),
199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.dim, children: g.name })
200
+ ] }),
201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.row, children: [
202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.dim, children: t("gpu.temp") }),
203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: g.temp == null ? "\u2014" : `${Math.round(g.temp)} \xB0C` })
204
+ ] }),
205
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.row, children: [
206
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.dim, minWidth: 40 }, children: t("gpu.util") }),
207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Bar, { pct: (_a = g.util) != null ? _a : 0 }),
208
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.chip, minWidth: 30 }, children: pct1(g.util) })
209
+ ] }),
210
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.row, children: [
211
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.dim, minWidth: 40 }, children: t("gpu.power") }),
212
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: g.powerW == null ? "\u2014" : `${n1(g.powerW)} W` })
213
+ ] }),
214
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.row, children: [
215
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.dim, minWidth: 40 }, children: t("gpu.mem") }),
216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Bar, { pct: memPct, color: C.accent }),
217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.chip, minWidth: 70 }, children: `${mb(g.memUsedMb)} / ${mb(g.memTotalMb)}` })
218
+ ] })
219
+ ] });
220
+ }
221
+ var AVG_SAMPLES = Math.round(1e4 / POLL_MS);
222
+ var avg = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
223
+ function useHardwareData() {
224
+ var _a;
225
+ const [data, setData] = (0, import_react.useState)(null);
226
+ const [err, setErr] = (0, import_react.useState)(null);
227
+ const [tick, setTick] = (0, import_react.useState)(0);
228
+ const totalHistRef = (0, import_react.useRef)([]);
229
+ const smoothedHistRef = (0, import_react.useRef)([]);
230
+ const coreHistRef = (0, import_react.useRef)([]);
231
+ const [corePcts, setCorePcts] = (0, import_react.useState)([]);
232
+ (0, import_react.useEffect)(() => {
233
+ let alive = true;
234
+ const load = async () => {
235
+ try {
236
+ const res = await fetch(DATA_URL, { headers: { accept: "application/json" } });
237
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
238
+ const json = await res.json();
239
+ if (!alive) return;
240
+ totalHistRef.current = [...totalHistRef.current.slice(-(HISTORY - 1)), json.cpu.total];
241
+ const smoothedTotal = avg(totalHistRef.current.slice(-AVG_SAMPLES));
242
+ smoothedHistRef.current = [...smoothedHistRef.current.slice(-(HISTORY - 1)), smoothedTotal];
243
+ coreHistRef.current = [...coreHistRef.current.slice(-(AVG_SAMPLES - 1)), json.cpu.cores];
244
+ setCorePcts(json.cpu.cores.map((_, i) => avg(coreHistRef.current.map((snap) => {
245
+ var _a2;
246
+ return (_a2 = snap[i]) != null ? _a2 : 0;
247
+ }))));
248
+ setData(json);
249
+ setErr(null);
250
+ } catch (e) {
251
+ if (alive) setErr(e.message);
252
+ }
253
+ };
254
+ load();
255
+ const id = setInterval(load, POLL_MS);
256
+ return () => {
257
+ alive = false;
258
+ clearInterval(id);
259
+ };
260
+ }, [tick]);
261
+ return {
262
+ data,
263
+ err,
264
+ retry: () => setTick((x) => x + 1),
265
+ history: smoothedHistRef.current,
266
+ cpuPct: (_a = smoothedHistRef.current.at(-1)) != null ? _a : 0,
267
+ corePcts
268
+ };
269
+ }
270
+ function HardwareBody({ t }) {
271
+ var _a;
272
+ const { data, err, retry, history, cpuPct, corePcts } = useHardwareData();
273
+ if (err && !data) {
274
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.root, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.box, children: [
275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: S.dim, children: [
276
+ t("error"),
277
+ "\uFF1A",
278
+ err
279
+ ] }),
280
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
281
+ "button",
282
+ {
283
+ style: { alignSelf: "flex-start", background: C.track, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, padding: "4px 10px", cursor: "pointer" },
284
+ onClick: retry,
285
+ children: t("retry")
286
+ }
287
+ )
288
+ ] }) });
289
+ }
290
+ if (!data) {
291
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.root, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.dim, children: t("loading") }) });
292
+ }
293
+ const { cpu, mem, temp, gpus } = data;
294
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.root, children: [
295
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.box, children: [
296
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.head, children: [
297
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.title, children: t("section.cpu") }),
298
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.dim, children: (_a = cpu.model) != null ? _a : "" })
299
+ ] }),
300
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 16, alignItems: "flex-end" }, children: [
301
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
302
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...S.big, color: barColor(cpuPct) }, children: [
303
+ Math.round(cpuPct),
304
+ "%"
305
+ ] }),
306
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.dim, children: t("cpu.overall") })
307
+ ] }),
308
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
309
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.big, fontSize: 20, color: temp.overall == null ? C.dim : barColor(tempPct(temp.overall)) }, children: temp.overall == null ? "\u2014" : `${Math.round(temp.overall)}\xB0` }),
310
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.dim, children: t("temp.max") })
311
+ ] }),
312
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { flex: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Sparkline, { samples: history }) })
313
+ ] }),
314
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...S.row, marginTop: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: S.dim, children: [
315
+ t("cpu.cores"),
316
+ " \xB7 ",
317
+ cpu.cores.length
318
+ ] }) }),
319
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.grid, children: corePcts.map((v, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CoreBar, { idx: i, v }, i)) }),
320
+ temp.zones.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
321
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...S.row, marginTop: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: S.dim, children: [
322
+ t("section.temp"),
323
+ " \xB7 ",
324
+ temp.zones.length
325
+ ] }) }),
326
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.grid, children: temp.zones.map((z, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.row, children: [
327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.dim, minWidth: 80, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: z.name }),
328
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Bar, { pct: tempPct(z.temp) }),
329
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.chip, minWidth: 40 }, children: `${Math.round(z.temp)}\xB0` })
330
+ ] }, i)) })
331
+ ] })
332
+ ] }),
333
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.box, children: [
334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.head, children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.title, children: t("section.mem") }),
336
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...S.big, fontSize: 18, color: barColor(mem.usedPct) }, children: [
337
+ Math.round(mem.usedPct),
338
+ "%"
339
+ ] })
340
+ ] }),
341
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.row, children: [
342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.dim, children: t("mem.used") }),
343
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Bar, { pct: mem.usedPct, color: C.accent }),
344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.chip, minWidth: 90 }, children: `${gb(mem.usedKb)} / ${gb(mem.totalKb)} GB` })
345
+ ] })
346
+ ] }),
347
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.head, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.title, children: t("section.gpu") }) }),
348
+ gpus.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.box, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: S.dim, children: t("gpu.none") }) }) : gpus.map((g) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GpuBox, { g, t }, g.index))
349
+ ] });
350
+ }
351
+
352
+ // src/client/index.tsx
353
+ var inject = ["slots", "locale", "sidebarRightTabs"];
354
+ function apply(ctx) {
355
+ const t = ctx.locale.bind("hardwareMonitor");
356
+ 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
+ ctx.effect(
366
+ () => ctx.slots.inject("conversation.view", () => ctx.slots.register({ name: "conversation.view", id: HARDWARE_ID, order: 20, locale: "hardwareMonitor", label: () => t("tab.title") }, HardwareBody)),
367
+ "dsh-vitals: conversation tab"
368
+ );
369
+ }
370
+
371
+ return module.exports;
372
+ }});
package/lib/index.js ADDED
@@ -0,0 +1,217 @@
1
+ // src/index.ts
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { execFile } from "node:child_process";
4
+ import { cpus, freemem, totalmem } from "node:os";
5
+ var inject = ["webServer"];
6
+ function parseStat(raw) {
7
+ const cores = [];
8
+ for (const line of raw.split("\n")) {
9
+ if (!line.startsWith("cpu")) break;
10
+ const f = line.trim().split(/\s+/).slice(1).map(Number);
11
+ if (f.some(Number.isNaN)) continue;
12
+ cores.push(f);
13
+ }
14
+ return { cores };
15
+ }
16
+ function pct(prev, now) {
17
+ const dTotal = now.reduce((a, b, i) => a + (b - (prev[i] ?? b)), 0);
18
+ const idle = (now[3] ?? 0) - (prev[3] ?? now[3] ?? 0);
19
+ if (dTotal <= 0) return 0;
20
+ return Math.max(0, Math.min(100, (1 - idle / dTotal) * 100));
21
+ }
22
+ var prevStat = null;
23
+ var prevCpuTimes = null;
24
+ var cpuModel = null;
25
+ function cpuModelOnce() {
26
+ if (cpuModel !== null) return cpuModel;
27
+ try {
28
+ const m = readFileSync("/proc/cpuinfo", "utf8").match(/model name\s*:\s*(.+)/);
29
+ if (m) {
30
+ cpuModel = m[1].trim();
31
+ return cpuModel;
32
+ }
33
+ } catch {
34
+ }
35
+ try {
36
+ cpuModel = cpus()[0]?.model ?? null;
37
+ } catch {
38
+ cpuModel = null;
39
+ }
40
+ return cpuModel;
41
+ }
42
+ function corePct(prev, now) {
43
+ if (!prev) return 0;
44
+ const dTotal = now.reduce((a, b, i) => a + (b - (prev[i] ?? b)), 0);
45
+ const dIdle = (now[3] ?? 0) - (prev[3] ?? 0);
46
+ if (dTotal <= 0) return 0;
47
+ return Math.max(0, Math.min(100, (1 - dIdle / dTotal) * 100));
48
+ }
49
+ function sampleCpu() {
50
+ try {
51
+ const now = parseStat(readFileSync("/proc/stat", "utf8"));
52
+ const total = pct(prevStat?.cores[0] ?? now.cores[0], now.cores[0]);
53
+ const cores = now.cores.slice(1).map((c, i) => pct(prevStat?.cores[i + 1] ?? c, c));
54
+ prevStat = now;
55
+ return { total, cores, model: cpuModelOnce() };
56
+ } catch {
57
+ const now = cpus().map((c) => {
58
+ const t = [c.user, c.nice, c.sys, c.idle, c.irq];
59
+ return { t, idle: t[3] };
60
+ });
61
+ const prev = prevCpuTimes;
62
+ const cores = now.map((c, i) => corePct(prev?.total[i], c.t));
63
+ const total = cores.length ? cores.reduce((a, b) => a + b, 0) / cores.length : 0;
64
+ prevCpuTimes = { total: now.map((c) => c.t), idle: now.map((c) => c.idle) };
65
+ return { total, cores, model: cpuModelOnce() };
66
+ }
67
+ }
68
+ function sampleMem() {
69
+ let totalKb = null, availKb = null;
70
+ try {
71
+ const g = {};
72
+ for (const line of readFileSync("/proc/meminfo", "utf8").split("\n")) {
73
+ const m = line.match(/^(\w+):\s+(\d+)\s*kB/);
74
+ if (m) g[m[1]] = Number(m[2]);
75
+ }
76
+ totalKb = g.MemTotal ?? null;
77
+ availKb = g.MemAvailable ?? (g.MemFree ?? 0) + (g.Buffers ?? 0) + (g.Cached ?? 0);
78
+ } catch {
79
+ const t = totalmem(), f = freemem();
80
+ totalKb = Math.round(t / 1024);
81
+ availKb = Math.round(f / 1024);
82
+ }
83
+ if (totalKb == null || totalKb <= 0 || availKb == null) {
84
+ return { totalKb: 0, availKb: 0, usedKb: 0, usedPct: 0 };
85
+ }
86
+ const usedKb = Math.max(0, totalKb - availKb);
87
+ const usedPct = Math.min(100, usedKb / totalKb * 100);
88
+ return { totalKb, availKb, usedKb, usedPct };
89
+ }
90
+ async function sampleTemp() {
91
+ const zones = [];
92
+ try {
93
+ for (const z of readdirSync("/sys/class/thermal")) {
94
+ if (!z.startsWith("thermal_zone")) continue;
95
+ try {
96
+ const type = readFileSync(`/sys/class/thermal/${z}/type`, "utf8").trim();
97
+ const raw = Number(readFileSync(`/sys/class/thermal/${z}/temp`, "utf8").trim());
98
+ if (!Number.isFinite(raw)) continue;
99
+ const temp = raw / 1e3;
100
+ if (temp < 0 || temp > 200) continue;
101
+ zones.push({ name: type || z, temp: Math.round(temp * 10) / 10 });
102
+ } catch {
103
+ }
104
+ }
105
+ } catch {
106
+ }
107
+ const overall = zones.length ? Math.max(...zones.map((z) => z.temp)) : null;
108
+ return { overall, zones };
109
+ }
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
+ });
124
+ }
125
+ async function sampleGpus() {
126
+ const out = await nvidiaSmi();
127
+ if (out === null || out.length === 0) return { gpus: [], gpuError: null };
128
+ const gpus = [];
129
+ for (const line of out.split("\n")) {
130
+ const [index, name, temp, util, power, memUsed, memTotal] = line.split(",").map((s) => s.trim());
131
+ const num = (s) => {
132
+ const n = Number(s);
133
+ return Number.isFinite(n) ? n : null;
134
+ };
135
+ gpus.push({
136
+ index: num(index) ?? 0,
137
+ name,
138
+ temp: num(temp),
139
+ util: num(util),
140
+ powerW: num(power),
141
+ memUsedMb: num(memUsed),
142
+ memTotalMb: num(memTotal)
143
+ });
144
+ }
145
+ return { gpus, gpuError: null };
146
+ }
147
+ async function sample() {
148
+ const errors = [];
149
+ let cpu, mem;
150
+ let temp;
151
+ let gpus = [], gpuError = null;
152
+ try {
153
+ cpu = sampleCpu();
154
+ } catch (e) {
155
+ errors.push(`cpu: ${e.message}`);
156
+ }
157
+ try {
158
+ mem = sampleMem();
159
+ } catch (e) {
160
+ errors.push(`mem: ${e.message}`);
161
+ }
162
+ try {
163
+ temp = await sampleTemp();
164
+ } catch (e) {
165
+ errors.push(`temp: ${e.message}`);
166
+ }
167
+ try {
168
+ ({ gpus, gpuError } = await sampleGpus());
169
+ } catch (e) {
170
+ gpuError = e.message;
171
+ }
172
+ return {
173
+ ok: true,
174
+ ts: Date.now(),
175
+ cpu: cpu ?? { total: 0, cores: [], model: null },
176
+ mem: mem ?? { totalKb: 0, availKb: 0, usedKb: 0, usedPct: 0 },
177
+ temp: temp ?? { overall: null, zones: [] },
178
+ gpus,
179
+ gpuError,
180
+ errors
181
+ };
182
+ }
183
+ var DATA_ENDPOINT = "/plugins/dsh-vitals/data";
184
+ function send(res, code, body) {
185
+ res.writeHead(code, {
186
+ "content-type": "application/json",
187
+ "cache-control": "no-store"
188
+ });
189
+ res.end(body);
190
+ }
191
+ function apply(ctx, _config) {
192
+ ctx.effect(() => {
193
+ const dispose = ctx.webServer.register({
194
+ kind: "exact",
195
+ path: DATA_ENDPOINT,
196
+ handler: async (req, res) => {
197
+ if (req.headers["sec-fetch-site"] === "cross-site") {
198
+ send(res, 403, JSON.stringify({ ok: false, error: "forbidden" }));
199
+ return;
200
+ }
201
+ if (req.method !== "GET" && req.method !== "HEAD") {
202
+ send(res, 405, JSON.stringify({ ok: false, error: "method not allowed" }));
203
+ return;
204
+ }
205
+ const data = await sample();
206
+ send(res, 200, JSON.stringify(data));
207
+ }
208
+ });
209
+ return () => {
210
+ dispose();
211
+ };
212
+ }, "vitals: /plugins/dsh-vitals/data");
213
+ }
214
+ export {
215
+ apply,
216
+ inject
217
+ };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "dsh-vitals",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./lib/index.js"
10
+ },
11
+ "./client": {
12
+ "default": "./lib/client.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dsh": {
17
+ "bundle": {
18
+ "patch": "./cordis.patch.yml"
19
+ },
20
+ "client": {
21
+ "platform": "web"
22
+ },
23
+ "compatibility": {
24
+ "dsh": ">=0.1.0-rc.5"
25
+ }
26
+ },
27
+ "dshhub": {
28
+ "schemaVersion": 1,
29
+ "displayName": "Vitals",
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" },
35
+ "permissions": {}
36
+ },
37
+ "files": [
38
+ "lib",
39
+ "cordis.patch.yml"
40
+ ],
41
+ "peerDependencies": {
42
+ "react": "^18.0.0 || ^19.0.0"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/JoblessJoe/dsh-vitals.git"
47
+ },
48
+ "keywords": [
49
+ "deepseek",
50
+ "deepseek-harness",
51
+ "dsh",
52
+ "dsh-plugin",
53
+ "client-plugin",
54
+ "hardware",
55
+ "btop",
56
+ "monitor",
57
+ "cpu",
58
+ "gpu",
59
+ "nvidia",
60
+ "observability"
61
+ ],
62
+ "license": "MIT",
63
+ "engines": {
64
+ "node": ">=20"
65
+ },
66
+ "os": ["darwin", "linux"],
67
+ "scripts": {
68
+ "build": "node scripts/build.mjs"
69
+ },
70
+ "devDependencies": {
71
+ "esbuild": "^0.28.2"
72
+ }
73
+ }