@hellcoder/companion 0.116.1-preview.20260814050532.93fba26 → 0.117.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.
Files changed (31) hide show
  1. package/dist/assets/{AgentsPage-DPt9YKyQ.js → AgentsPage-DJIGGU86.js} +1 -1
  2. package/dist/assets/{CronManager-BxRWyTp3.js → CronManager-D7aAMVgs.js} +1 -1
  3. package/dist/assets/{DashboardPage-BSnJNNNL.js → DashboardPage-Bporj7Vr.js} +1 -1
  4. package/dist/assets/{IntegrationsPage-DWrnBrBa.js → IntegrationsPage-BSY0-fsO.js} +1 -1
  5. package/dist/assets/{LinearOAuthSettingsPage-DZJpyUr3.js → LinearOAuthSettingsPage-Cl5dP05g.js} +1 -1
  6. package/dist/assets/{LinearSettingsPage-CUV6wc7-.js → LinearSettingsPage-dwMpnpxL.js} +1 -1
  7. package/dist/assets/{MagicUIDashboard-CbruKd65.js → MagicUIDashboard-Bpa4AupX.js} +1 -1
  8. package/dist/assets/{MagicUIView-NSITfXce.js → MagicUIView-BoHMBELi.js} +1 -1
  9. package/dist/assets/{Playground-BoyZfARc.js → Playground-By2aXWQF.js} +1 -1
  10. package/dist/assets/{PromptsPage-D3aJBO9h.js → PromptsPage-DVv0Qn3d.js} +1 -1
  11. package/dist/assets/{RunsPage-C6YY6SGq.js → RunsPage-bu3TZ6ai.js} +1 -1
  12. package/dist/assets/{SandboxManager-_mggajaE.js → SandboxManager-CDjGbD5N.js} +1 -1
  13. package/dist/assets/{SettingsPage-DBjt4SLp.js → SettingsPage-D0v_BZTU.js} +1 -1
  14. package/dist/assets/{TailscalePage-BaAGOCMu.js → TailscalePage-B8SJQpMF.js} +1 -1
  15. package/dist/assets/index-BqOdTpct.css +1 -0
  16. package/dist/assets/index-DqIZ_Ol5.js +140 -0
  17. package/dist/assets/{sw-register-BH5kpgnP.js → sw-register-CvyRJNug.js} +1 -1
  18. package/dist/index.html +2 -2
  19. package/dist/sw.js +1 -1
  20. package/package.json +1 -1
  21. package/server/linear-agent-bridge.ts +0 -1
  22. package/server/magic-ui-watcher.test.ts +85 -0
  23. package/server/magic-ui-watcher.ts +0 -1
  24. package/server/routes/system-routes.test.ts +401 -0
  25. package/server/routes/system-routes.ts +10 -0
  26. package/server/system-disk.test.ts +48 -0
  27. package/server/system-disk.ts +55 -0
  28. package/server/system-memory.test.ts +32 -0
  29. package/server/system-memory.ts +32 -0
  30. package/dist/assets/index-BaDsUsyb.js +0 -140
  31. package/dist/assets/index-DSCRqqpK.css +0 -1
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getSystemDisk } from "./system-disk.js";
3
+
4
+ // getSystemDisk reads the real filesystem via statfs(2), so exact byte counts
5
+ // are machine-dependent. The invariants below must hold on any host where
6
+ // statfs succeeds. On a host without statfs the function returns null, which
7
+ // every test tolerates — the meter is best-effort by design.
8
+ describe("getSystemDisk", () => {
9
+ it("returns a coherent snapshot with sane invariants", () => {
10
+ const d = getSystemDisk();
11
+ if (!d) return; // statfs unavailable — nothing to assert
12
+
13
+ // A mounted filesystem always has positive capacity.
14
+ expect(d.total_bytes).toBeGreaterThan(0);
15
+
16
+ expect(d.used_bytes).toBeGreaterThanOrEqual(0);
17
+ expect(d.available_bytes).toBeGreaterThanOrEqual(0);
18
+ expect(d.used_bytes).toBeLessThanOrEqual(d.total_bytes);
19
+ expect(d.available_bytes).toBeLessThanOrEqual(d.total_bytes);
20
+
21
+ // used is derived as total - available (never bfree), so the identity
22
+ // must hold exactly. This is the guard against a future refactor
23
+ // switching to bfree and silently under-reporting usage.
24
+ expect(d.used_bytes).toBe(Math.max(0, d.total_bytes - d.available_bytes));
25
+ });
26
+
27
+ it("reports used_percent in 0–100 rounded to one decimal", () => {
28
+ const d = getSystemDisk();
29
+ if (!d) return;
30
+ expect(d.used_percent).toBeGreaterThanOrEqual(0);
31
+ expect(d.used_percent).toBeLessThanOrEqual(100);
32
+ expect(Math.round(d.used_percent * 10)).toBe(d.used_percent * 10);
33
+ });
34
+
35
+ it("reports the Companion data dir as the measured path", () => {
36
+ const d = getSystemDisk();
37
+ if (!d) return;
38
+ // The meter must describe the volume that session/recording files grow
39
+ // into, which is not necessarily the one holding "/".
40
+ expect(d.path).toBeTruthy();
41
+ expect(typeof d.path).toBe("string");
42
+ });
43
+
44
+ it("never throws, even though the underlying syscall can fail", () => {
45
+ // The route calls this on every poll with no try/catch of its own.
46
+ expect(() => getSystemDisk()).not.toThrow();
47
+ });
48
+ });
@@ -0,0 +1,55 @@
1
+ import { statfsSync } from "node:fs";
2
+ import { COMPANION_HOME } from "./paths.js";
3
+
4
+ export interface SystemDiskInfo {
5
+ /** Total size of the filesystem holding COMPANION_HOME, in bytes. */
6
+ total_bytes: number;
7
+ /** Space in use (total - available), in bytes. */
8
+ used_bytes: number;
9
+ /** Space available to this (unprivileged) user, in bytes. */
10
+ available_bytes: number;
11
+ /** Used percentage of total, 0–100, rounded to one decimal. */
12
+ used_percent: number;
13
+ /** Path the figures were measured for. */
14
+ path: string;
15
+ }
16
+
17
+ /**
18
+ * Snapshot of free space on the filesystem that holds the Companion data
19
+ * directory — that's the volume sessions, recordings and logs actually grow
20
+ * into, which is not necessarily the one holding "/".
21
+ *
22
+ * Deliberately uses statfs(2) rather than shelling out to `df`, and never
23
+ * walks the tree the way `du` would: this is a single syscall against the
24
+ * already-in-memory superblock (~0.1ms, no disk I/O), so it is safe to call
25
+ * on a poll interval and needs no caching layer.
26
+ *
27
+ * We report `bavail` (blocks free to unprivileged users) rather than `bfree`,
28
+ * which excludes the root-reserved reserve. That mirrors how getSystemMemory()
29
+ * prefers MemAvailable over MemFree: both answer "how much can I actually
30
+ * still use" rather than "how much is technically unallocated". It is also why
31
+ * this can read a percent or two above `df` for the same filesystem.
32
+ */
33
+ export function getSystemDisk(): SystemDiskInfo | null {
34
+ try {
35
+ const stats = statfsSync(COMPANION_HOME);
36
+ // bsize is the preferred I/O block size that blocks/bavail are counted in.
37
+ const total = stats.blocks * stats.bsize;
38
+ const available = stats.bavail * stats.bsize;
39
+ const used = Math.max(0, total - available);
40
+ const used_percent =
41
+ total > 0 ? Math.round((used / total) * 1000) / 10 : 0;
42
+ return {
43
+ total_bytes: total,
44
+ used_bytes: used,
45
+ available_bytes: available,
46
+ used_percent,
47
+ path: COMPANION_HOME,
48
+ };
49
+ } catch {
50
+ // statfs is unavailable (very old runtime) or the path does not exist yet
51
+ // on a first run. The meter is best-effort — the route returns 204 and the
52
+ // UI renders nothing rather than showing a bogus zeroed bar.
53
+ return null;
54
+ }
55
+ }
@@ -28,4 +28,36 @@ describe("getSystemMemory", () => {
28
28
  // At most one decimal place.
29
29
  expect(Math.round(m.used_percent * 10)).toBe(m.used_percent * 10);
30
30
  });
31
+
32
+ // Swap is read from the same /proc/meminfo snapshot as RAM. A host may have
33
+ // no swap at all, in which case every swap field must be 0 — the UI keys off
34
+ // swap_total_bytes === 0 to hide the meter entirely.
35
+ it("returns coherent swap figures, or zeroes when swap is absent", () => {
36
+ const m = getSystemMemory();
37
+
38
+ expect(m.swap_total_bytes).toBeGreaterThanOrEqual(0);
39
+ expect(m.swap_used_bytes).toBeGreaterThanOrEqual(0);
40
+
41
+ if (m.swap_total_bytes === 0) {
42
+ // No swap configured (or a non-Linux host): must not report phantom use.
43
+ expect(m.swap_used_bytes).toBe(0);
44
+ expect(m.swap_used_percent).toBe(0);
45
+ return;
46
+ }
47
+
48
+ // Used swap can never exceed the configured total, even though SwapFree
49
+ // can briefly exceed SwapTotal during swapoff — that's what the clamp is for.
50
+ expect(m.swap_used_bytes).toBeLessThanOrEqual(m.swap_total_bytes);
51
+ expect(m.swap_used_percent).toBeGreaterThanOrEqual(0);
52
+ expect(m.swap_used_percent).toBeLessThanOrEqual(100);
53
+ expect(Math.round(m.swap_used_percent * 10)).toBe(m.swap_used_percent * 10);
54
+ });
55
+
56
+ it("keeps swap independent of the RAM figures", () => {
57
+ // MemAvailable deliberately excludes swap, so a swapping box shows healthy
58
+ // RAM headroom while thrashing. Guards against a refactor that folds swap
59
+ // into used_bytes and hides exactly the condition we want to surface.
60
+ const m = getSystemMemory();
61
+ expect(m.used_bytes).toBe(Math.max(0, m.total_bytes - m.available_bytes));
62
+ });
31
63
  });
@@ -10,6 +10,15 @@ export interface SystemMemoryInfo {
10
10
  available_bytes: number;
11
11
  /** Used percentage of total, 0–100, rounded to one decimal. */
12
12
  used_percent: number;
13
+ /**
14
+ * Total swap, in bytes. 0 when no swap is configured, or on platforms where
15
+ * we cannot read it — callers should treat 0 as "no swap meter to show".
16
+ */
17
+ swap_total_bytes: number;
18
+ /** Swap in use (total - free), in bytes. */
19
+ swap_used_bytes: number;
20
+ /** Used percentage of swap, 0–100, rounded to one decimal. 0 when no swap. */
21
+ swap_used_percent: number;
13
22
  }
14
23
 
15
24
  /**
@@ -25,10 +34,19 @@ function readMeminfoKb(meminfo: string, key: string): number | null {
25
34
  * MemAvailable, which accounts for reclaimable page cache and therefore
26
35
  * reflects true OOM headroom far better than os.freemem(). Everywhere else
27
36
  * (or if /proc is unreadable) we fall back to the os module.
37
+ *
38
+ * Swap comes from the same single /proc/meminfo read, so reporting it costs
39
+ * nothing extra. It is worth surfacing separately rather than folding into the
40
+ * RAM figure: a box that has begun swapping heavily is already thrashing, and
41
+ * because MemAvailable excludes swap the RAM meter alone cannot show it. Swap
42
+ * exhaustion on top of high RAM use is the state immediately preceding the OOM
43
+ * killer, which is exactly what these meters exist to warn about.
28
44
  */
29
45
  export function getSystemMemory(): SystemMemoryInfo {
30
46
  let total = os.totalmem();
31
47
  let available = os.freemem();
48
+ let swapTotal = 0;
49
+ let swapUsed = 0;
32
50
 
33
51
  try {
34
52
  const meminfo = readFileSync("/proc/meminfo", "utf8");
@@ -38,16 +56,30 @@ export function getSystemMemory(): SystemMemoryInfo {
38
56
  total = totalKb * 1024;
39
57
  available = availableKb * 1024;
40
58
  }
59
+ const swapTotalKb = readMeminfoKb(meminfo, "SwapTotal");
60
+ const swapFreeKb = readMeminfoKb(meminfo, "SwapFree");
61
+ if (swapTotalKb !== null && swapFreeKb !== null) {
62
+ swapTotal = swapTotalKb * 1024;
63
+ // Clamp: SwapFree can momentarily exceed SwapTotal mid-swapoff.
64
+ swapUsed = Math.max(0, swapTotal - swapFreeKb * 1024);
65
+ }
41
66
  } catch {
42
67
  // Not Linux, or /proc/meminfo unavailable — keep the os module values.
68
+ // The os module exposes no swap figures, so swap stays 0 (= "unknown",
69
+ // rendered as no meter) rather than being reported as 0-bytes-used.
43
70
  }
44
71
 
45
72
  const used = Math.max(0, total - available);
46
73
  const used_percent = total > 0 ? Math.round((used / total) * 1000) / 10 : 0;
74
+ const swap_used_percent =
75
+ swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 1000) / 10 : 0;
47
76
  return {
48
77
  total_bytes: total,
49
78
  used_bytes: used,
50
79
  available_bytes: available,
51
80
  used_percent,
81
+ swap_total_bytes: swapTotal,
82
+ swap_used_bytes: swapUsed,
83
+ swap_used_percent,
52
84
  };
53
85
  }