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 +98 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +372 -0
- package/lib/index.js +217 -0
- package/package.json +73 -0
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
|
package/cordis.patch.yml
ADDED
|
@@ -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
|
+
}
|