agent-dag 3.1.0 → 3.2.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 +16 -1
- package/dist/web/assets/{index-BYupRwZc.js → index-7UsJp5Ht.js} +22 -18
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/server/hwmonitor.mjs +56 -0
- package/src/server/lhm-parse.mjs +91 -0
- package/src/server/system-metrics.mjs +15 -3
package/dist/web/index.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
|
|
41
41
|
})();
|
|
42
42
|
</script>
|
|
43
|
-
<script type="module" crossorigin src="/assets/index-
|
|
43
|
+
<script type="module" crossorigin src="/assets/index-7UsJp5Ht.js"></script>
|
|
44
44
|
<link rel="stylesheet" crossorigin href="/assets/index-Bnn7d7u8.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch tool calls, token spend and every Claude Code subagent on one calm canvas. Run it with npx ccdeck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/release-notes.json
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
"about a defect in the type, and the suite refuses both it and no space at",
|
|
32
32
|
"all."
|
|
33
33
|
],
|
|
34
|
+
"3.2.0": [
|
|
35
|
+
{
|
|
36
|
+
"title": "🌡️ Windows shows a temperature where it can find one",
|
|
37
|
+
"body": "On most Windows laptops there is nothing to show, and that is Windows rather than this deck: modern machines declare no ACPI thermal zone, and the sensors that do exist sit behind an interface only an administrator may read. Measured on real hardware, elevated and not.\n\nOne thing does work, and it costs you nothing to have. If LibreHardwareMonitor happens to be running with its web server on, the deck now reads its numbers over localhost — which needs no privileges at all.\n\nIt is only ever a read. Nothing is installed, nothing is asked of you, and a machine without it shows no section, exactly as before."
|
|
38
|
+
}
|
|
39
|
+
],
|
|
34
40
|
"3.1.0": [
|
|
35
41
|
{
|
|
36
42
|
"title": "🚀 The first run stops looking like it hung",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Windows' temperature, when something else on the machine already has it.
|
|
2
|
+
//
|
|
3
|
+
// #747. On a modern Intel laptop Windows publishes no CPU temperature that an
|
|
4
|
+
// ordinary process may read. Measured on a physical Windows 11 machine, both
|
|
5
|
+
// elevated and not: the firmware declares zero ACPI thermal zones, MSAcpi
|
|
6
|
+
// answers "Not supported" even to an administrator, Win32_TemperatureProbe is a
|
|
7
|
+
// stub whose every field reads 32768, and the one source that DOES have the
|
|
8
|
+
// numbers — Intel Dynamic Tuning's `EsifDeviceInformation` — is Access denied
|
|
9
|
+
// without admin. That is the state of Windows, not a gap in this deck: there is
|
|
10
|
+
// no standard user-mode API for it, which is why every tool that shows one
|
|
11
|
+
// installs a kernel driver.
|
|
12
|
+
//
|
|
13
|
+
// LibreHardwareMonitor is such a tool. It installs that driver, reads the
|
|
14
|
+
// registers directly, and — if its web server is switched on — publishes
|
|
15
|
+
// everything as plain HTTP on localhost. Reading THAT needs no privileges at
|
|
16
|
+
// all.
|
|
17
|
+
//
|
|
18
|
+
// SO THIS IS A READ, NEVER A REQUEST. The deck does not install
|
|
19
|
+
// LibreHardwareMonitor, does not ask anybody to, and does not mention it: a
|
|
20
|
+
// user must do nothing but install ccdeck. If the tool happens to be running —
|
|
21
|
+
// and on the machines where this matters it often is, because the person who
|
|
22
|
+
// wants a temperature has already gone and got one — the deck uses it. If not,
|
|
23
|
+
// the section is not drawn, exactly as before.
|
|
24
|
+
import { readTemps } from "./lhm-parse.mjs";
|
|
25
|
+
|
|
26
|
+
/** LibreHardwareMonitor's default, and Open Hardware Monitor's before it. Not
|
|
27
|
+
* probed across a range: a scan of somebody's loopback ports is not a thing to
|
|
28
|
+
* do unasked, and a user who moved the port can say so. */
|
|
29
|
+
export const LHM_URL = "http://127.0.0.1:8085/data.json";
|
|
30
|
+
|
|
31
|
+
/** Short. A refused connection returns at once; this bounds the case where
|
|
32
|
+
* something else holds the port open and never answers. */
|
|
33
|
+
const TIMEOUT_MS = 1_500;
|
|
34
|
+
|
|
35
|
+
export function lhmUrl(env = process.env) {
|
|
36
|
+
const port = Number(env.AGENTS_DECK_LHM_PORT);
|
|
37
|
+
return Number.isInteger(port) && port > 0 && port < 65536
|
|
38
|
+
? `http://127.0.0.1:${port}/data.json`
|
|
39
|
+
: LHM_URL;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* CPU and GPU degrees from a running LibreHardwareMonitor, or nothing.
|
|
44
|
+
*
|
|
45
|
+
* Never throws: nothing listening is the ordinary answer, and it arrives as a
|
|
46
|
+
* rejected fetch.
|
|
47
|
+
*/
|
|
48
|
+
export async function readHwMonitorTemps({ env = process.env, fetchFn = fetch } = {}) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetchFn(lhmUrl(env), { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
51
|
+
if (!res.ok) return {};
|
|
52
|
+
return readTemps(await res.json());
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Reading LibreHardwareMonitor's sensor tree, out of the shape its own source
|
|
2
|
+
// produces rather than out of an example.
|
|
3
|
+
//
|
|
4
|
+
// `GenerateJsonForNode` in HttpServer.cs builds every node as
|
|
5
|
+
//
|
|
6
|
+
// { id, Text, Min, Value, Max }
|
|
7
|
+
//
|
|
8
|
+
// and a SENSOR node adds
|
|
9
|
+
//
|
|
10
|
+
// SensorId : "/intelcpu/0/temperature/0" the stable identifier
|
|
11
|
+
// Type : "Temperature" the SensorType enum, as text
|
|
12
|
+
// Value : "52.0 °C" formatted for a human
|
|
13
|
+
// RawValue : 52.0 the number, unformatted
|
|
14
|
+
//
|
|
15
|
+
// Two of those decide this file.
|
|
16
|
+
//
|
|
17
|
+
// RAWVALUE, NEVER VALUE. `Value` is formatted with the machine's culture, so on
|
|
18
|
+
// a German or Russian Windows it reads "52,0 °C" — a comma — and every naive
|
|
19
|
+
// parse of it either throws away the decimal or produces 520. `RawValue` is the
|
|
20
|
+
// number itself. This is the same trap the deck already hit once with `ps`
|
|
21
|
+
// output and fixed with LC_NUMERIC.
|
|
22
|
+
//
|
|
23
|
+
// AND RAWVALUE CAN BE THE STRING "NaN". The server serialises with
|
|
24
|
+
// JsonNumberHandling.AllowNamedFloatingPointLiterals, which writes a NaN as a
|
|
25
|
+
// quoted "NaN" rather than failing — so a sensor that has not read yet arrives
|
|
26
|
+
// as text where a number is expected. `Number("NaN")` is NaN and is rejected
|
|
27
|
+
// below, which is the right answer, but it is rejected on purpose rather than
|
|
28
|
+
// by luck.
|
|
29
|
+
//
|
|
30
|
+
// TYPE, NEVER THE NAME. `Type` comes from an enum and is the same word on every
|
|
31
|
+
// machine in every language; the `Text` beside it is a display name that
|
|
32
|
+
// differs between vendors and driver versions.
|
|
33
|
+
|
|
34
|
+
/** A plausible temperature. The same floor the rest of the thermal code uses:
|
|
35
|
+
* 0 is a sensor that has not read, and nothing above this is a temperature. */
|
|
36
|
+
const plausible = (c) => Number.isFinite(c) && c > 0 && c < 150;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every temperature sensor in the tree, flattened.
|
|
40
|
+
*
|
|
41
|
+
* Exported for its own test, and because "what did that machine actually
|
|
42
|
+
* publish" is the question anybody debugging this will have first.
|
|
43
|
+
*/
|
|
44
|
+
export function flattenSensors(root) {
|
|
45
|
+
const out = [];
|
|
46
|
+
const walk = (n, depth) => {
|
|
47
|
+
// The tree is four or five deep in practice — root, computer, hardware,
|
|
48
|
+
// type, sensor. The bound is against a cycle, which JSON cannot contain but
|
|
49
|
+
// a hand-written fixture can.
|
|
50
|
+
if (!n || typeof n !== "object" || depth > 12) return;
|
|
51
|
+
if (n.Type === "Temperature") {
|
|
52
|
+
out.push({
|
|
53
|
+
id: String(n.SensorId ?? ""),
|
|
54
|
+
name: String(n.Text ?? ""),
|
|
55
|
+
celsius: Number(n.RawValue),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(n.Children)) for (const c of n.Children) walk(c, depth + 1);
|
|
59
|
+
};
|
|
60
|
+
walk(root, 0);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Which sensor is the CPU, and which is the GPU.
|
|
66
|
+
*
|
|
67
|
+
* Chosen by SensorId rather than by name. The identifier is built from the
|
|
68
|
+
* hardware type — `/intelcpu/0/…`, `/amdcpu/0/…`, `/gpu-nvidia/0/…` — and is
|
|
69
|
+
* the same on every machine, while `Text` is a display name that reads "CPU
|
|
70
|
+
* Package" on one driver and "Core (Tctl/Tdie)" on another.
|
|
71
|
+
*
|
|
72
|
+
* The HOTTEST of a hardware's sensors is taken, not the first. A CPU publishes
|
|
73
|
+
* a package reading and one per core; the package is usually the highest and is
|
|
74
|
+
* what a person means by "the CPU temperature", and where a vendor publishes no
|
|
75
|
+
* package the hottest core is the honest stand-in. Taking the first would
|
|
76
|
+
* report core #1 while core #6 is thermal-throttling.
|
|
77
|
+
*/
|
|
78
|
+
export function readTemps(root) {
|
|
79
|
+
const sensors = flattenSensors(root).filter(s => plausible(s.celsius));
|
|
80
|
+
const hottest = (re) => {
|
|
81
|
+
const mine = sensors.filter(s => re.test(s.id));
|
|
82
|
+
if (!mine.length) return null;
|
|
83
|
+
return Math.round(Math.max(...mine.map(s => s.celsius)));
|
|
84
|
+
};
|
|
85
|
+
const out = {};
|
|
86
|
+
const cpu = hottest(/^\/(intel|amd)cpu\//i);
|
|
87
|
+
const gpu = hottest(/^\/gpu-/i);
|
|
88
|
+
if (cpu != null) out.cpu = cpu;
|
|
89
|
+
if (gpu != null) out.gpu = gpu;
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
@@ -1096,9 +1096,21 @@ export async function readThermal(platform = process.platform) {
|
|
|
1096
1096
|
const out = await run("powershell.exe", [
|
|
1097
1097
|
"-NoProfile", "-NonInteractive", "-Command", WIN_THERMAL_PS,
|
|
1098
1098
|
], 6_000);
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1099
|
+
const answer = out ? parseWinThermal(out.trim()) : [];
|
|
1100
|
+
if (answer.length) return { celsius: answer, throttle: null };
|
|
1101
|
+
|
|
1102
|
+
// Windows itself had nothing, which on a modern Intel laptop is every time:
|
|
1103
|
+
// the firmware declares no ACPI thermal zone and the sensors sit behind
|
|
1104
|
+
// Intel DTT, which an ordinary process may not read. If something on this
|
|
1105
|
+
// machine has already gone and got them — LibreHardwareMonitor, with its
|
|
1106
|
+
// web server on — they are a plain HTTP read away. Never installed, never
|
|
1107
|
+
// asked for. See hwmonitor.mjs.
|
|
1108
|
+
const { readHwMonitorTemps } = await import("./hwmonitor.mjs");
|
|
1109
|
+
const t = await readHwMonitorTemps();
|
|
1110
|
+
const rows = [];
|
|
1111
|
+
if (t.cpu != null) rows.push({ label: "CPU", celsius: t.cpu, warnAt: WARN_C, critAt: CRIT_C });
|
|
1112
|
+
if (t.gpu != null) rows.push({ label: "GPU", celsius: t.gpu, warnAt: WARN_C, critAt: CRIT_C });
|
|
1113
|
+
return rows.length ? { celsius: rows, throttle: null } : null;
|
|
1102
1114
|
}
|
|
1103
1115
|
|
|
1104
1116
|
return null;
|