# dsh-pc-manager — PC Manager [English](README.md) · [简体中文](README.zh-CN.md) > DeepSeek Harness plugin for system monitoring & junk cleanup · 系统监控、垃圾清理与应用卸载的 DeepSeek Harness 插件 A PC Manager plugin for DeepSeek Harness (dsh), shipped in the official bundle format. It provides: system status monitoring (model tools + a right-sidebar dashboard), system junk cleanup (a zero-LLM direct-execution window), and app uninstall (M3) — 5 model-visible tools in total. **Platform support**: monitoring (`pc_status` + dashboard) and junk cleanup (`pc_junk_scan`/`pc_junk_clean` + the cleanup window) work on **macOS, Linux and Windows**; other platforms return `unsupported_platform`. On Windows, metrics come from node builtins plus a single batched PowerShell probe (see [Windows data sources](#windows-data-sources)), the junk registry is `JUNK_TARGETS_WIN32`, and recycling goes to the real Recycle Bin. It is positioned as a **security-first** agent system tool: scans are always dry-run; the two destructive tools default to `disabled_by_config` and only act after the host explicitly opts in; trash/uninstall defaults to the Trash (recoverable); if any link of the id validation chain fails, the whole call is rejected with **zero deletion**. ## Table of contents - [Quick start](#quick-start) - [Install (one line)](#install-one-line) - [Enable junk cleanup (optional)](#enable-junk-cleanup-optional) - [Manual mounting (fallback, mutually exclusive with bundle install)](#manual-mounting-fallback-mutually-exclusive-with-bundle-install) - [Features](#features) - [Model tools](#model-tools) - [Dashboard (right sidebar)](#dashboard-right-sidebar) - [Junk cleanup (zero-LLM direct-execution window)](#junk-cleanup-zero-llm-direct-execution-window) - [Security design](#security-design) - [Appendix](#appendix) - [Monitoring metrics and data sources](#monitoring-metrics-and-data-sources) - [Windows data sources](#windows-data-sources) - [Capability-gated probes](#capability-gated-probes) - [Junk target registry (per-platform dispatch)](#junk-target-registry-per-platform-dispatch) - [Reverse proxy / prefix-mounted deployment](#reverse-proxy--prefix-mounted-deployment) - [Repository layout](#repository-layout) - [Local development](#local-development) - [Milestones](#milestones) - [License](#license) ## Quick start ### Install (one line) This repository is an official **bundle plugin** (`dsh.bundle` + `dsh.client` in the root `package.json`); the build artifacts in `lib/` are committed, so a git install requires no build step: ```sh dsh plugin --profile web add "github:maoqizhen/dsh-pc-manager#main" ``` After installing, **restart `dsh web`** (the bundle layer is synthesized at startup). To update: `dsh plugin --profile web update dsh-pc-manager`; to uninstall: `dsh plugin --profile web remove dsh-pc-manager` — both require a restart to take effect. > **pnpm required**: `dsh plugin` is a pnpm forwarder and fails outright if pnpm is not on the PATH > (install with `npm i -g pnpm`; the pnpm major version must match the profile's existing store). > If dsh runs under a non-login shell on a server, make sure `node`/`pnpm` are visible on that > process's PATH (e.g. in `/usr/local/bin`). ### Enable junk cleanup (optional) Out of the box the install is safe-by-default: read-only tools work immediately, while the two destructive tools (`pc_junk_clean` / `pc_app_uninstall`) refuse to execute (`disabled_by_config`). To enable junk cleanup, write to a **back-layer patch** (the profile's `cordis.patch.yml` or `~/.dsh/cordis.patch.yml`; the back layer replaces the whole config line, so all keys must be restated): ```yaml - insert: - id: pc-manager name: dsh-pc-manager config: enableJunkClean: true # the per-call host approval gate still applies moveToTrash: true # default to Trash (recoverable) ``` ### Manual mounting (fallback, mutually exclusive with bundle install) No `dsh plugin add` needed, but a **dual entry** is mandatory (the package-name entry lets the client half be discovered by the `clientModules` scan; the file-path entry lets the host half's `apply` run; the official bundle install has no such issue): ```bash # 1. Let the profile resolve the package by name (the client half is discovered # via require.resolve('/package.json')) ln -s "$PWD" ~/.dsh/profiles/web/node_modules/dsh-pc-manager # 2. Append the dual entry to ~/.dsh/cordis.patch.yml (host: absolute path; client: package name) # - insert: # - id: pc-manager # name: /abs/path/to/dsh-pc-manager/lib/index.js # - id: pc-manager-client # name: dsh-pc-manager # 3. Restart dsh web ``` ## Features ### Model tools | Tool | Purpose | Status | | --- | --- | --- | | `pc_status` | Read-only system snapshot: CPU/GPU utilization, load, temperature (capability-gated), memory breakdown (incl. swap), disk I/O and per-volume usage, battery/power, network counters, multi-key sortable process table (macOS + Linux + Windows; Windows has no load average, and the per-process network and disk columns are unobtainable — see [Windows data sources](#windows-data-sources)) | ✅ Shipped | | `pc_junk_scan` | Enumerates reclaimable junk (macOS: Trash, user caches/logs, system temp, Xcode artifacts, simulator leftovers, package-manager caches, iOS backups; Linux: XDG Trash, ~/.cache, /tmp and /var/tmp, package-manager caches; Windows: the per-account Recycle Bin, %TEMP% and %SystemRoot%\Temp, WER reports and crash dumps, WinINet/D3D/NVIDIA shader/RDP caches, package-manager caches) with per-item size / safety flag / protected-exclusion records; params `kinds` / `minItemBytes`; always dry-run | ✅ Shipped | | `pc_junk_clean` | Trashes items by the exact ids returned by scan; if any link of the structural validation chain (format/root containment/realpath/blocked/safeToClean) fails, the whole call is rejected with zero deletion; the `trash` kind is emptied in place, everything else defaults to the three-tier trash (macOS: `/usr/bin/trash` → rename into `~/.Trash` → cross-volume cp+rm; Linux: `trash-put`/`gio trash` → freedesktop `~/.local/share/Trash/{files,info}` with `.trashinfo` restore records → cross-volume cp+rm; Windows: the `Microsoft.VisualBasic` recycle API into the **real Recycle Bin** → fallback `%LOCALAPPDATA%\pc-manager\trash` → cross-volume cp+rm); refuses to delete when sizes can't be verified; tier-1 results are re-verified (success reported but source still present counts as failure and degrades); requires explicit config opt-in | ✅ Shipped | | `pc_apps_list` | Installed-app inventory (app bundles + Homebrew; Linux distro packages and Windows registered programs await M3), with size / last-used | Stub, M3 | | `pc_app_uninstall` | Uninstalls by exact id; defaults to Trash, residual items are reported rather than silently deleted | Stub, M3 | ### Dashboard (right sidebar) The "System Monitor" entry (order 30) in the web profile's right sidebar opens a narrow-column card dashboard: header (host/OS + a muted second line "local … · public IP · city" (geo shows city only; the segment collapses when lookup is off or misses) / uptime) → CPU (bar + load + temperature row* + sparkline) → GPU (whole card hidden when unavailable) → memory (swap + breakdown) → disk (usage bars for valid volumes + I/O rate) → battery (hidden when absent) → network (main-interface rate + sparkline) → process table (switchable by CPU/memory/network, top 10 by default; hovering the CPU column header explains its scale). Hand-written SVG, no chart library. - **Capability-gated rows/columns**: the CPU card's temperature row (Linux with a sensor / Windows with an ACPI thermal zone), the load line (Windows has no load average — the whole line hides instead of showing a fabricated 0), the GPU card and the process table's "GPU" column (Windows uses the vendor-agnostic engine counters, so AMD/Intel/NVIDIA all show; Linux needs NVIDIA hardware + `nvidia-smi`), the process table's "network" column (always present on macOS via nettop; Linux only with root-attributed `ss -tinp`; Windows needs ETW and stays hidden) — missing capability means hidden, no wall of "—", see [Capability-gated probes](#capability-gated-probes). On Windows the disk card lists every volume by drive letter (`C:\`, `Z:\`, …); on POSIX it is still `/` plus data volumes under `/Volumes`, `/media`, `/mnt`. Process CPU% is **per-core** (multi-core processes can exceed 100%) and a **process-lifetime average** (macOS/Linux from ps; Windows computed from `Get-Process`'s cumulative CPU seconds over process uptime); the column header tooltip explains this. - Data flows through the host half's **single collection pump + SSE push**: `GET /pc-manager/stream` (text/event-stream) is the shared foundation — one host-side timer (interval `dashboardPollMs`, adjustable, min 500) collects on a single path and diffs server-side (network rates available from the first frame); the dashboard, floating window and every other consumer share one connection and one frame stream; **with no consumers, the connection and collection stop automatically** (reference-counted; hidden tab / closed floating window releases it). `GET /pc-manager/status` (`?processSort=&processLimit=`) remains as a cache fallback: requests with the pump's sort order (default CPU) read the fresh cache directly, other sort orders collect on demand. The LLM tool `pc_status` does not go through the pump; its contract is unchanged. - Headless profiles (no webServer) skip route registration automatically; tools are unaffected. On platforms with a source, the GPU card is titled with the **adapter name** (Windows reads it from the display adapter, e.g. `AMD Radeon(TM) Vega 8 Graphics`; the narrow column truncates with an ellipsis, full name in the tooltip); platforms without a name source keep the generic `GPU`. ### Junk cleanup (zero-LLM direct-execution window) A dedicated **"Junk Cleanup" window** in the right sidebar (guide entry peer-level with System Monitor, order 31), **zero LLM end to end**: click "Scan" → host `GET /pc-manager/junk/scan` (read-only; the server filters out small items below a 1 MiB threshold) → presented **by category** (the checkbox unit is the category, not single items; an inline size preview with the top 2–3 item names answers "which apps does this affect"; each category's "details (N)" folds out into a scrollable list for surgical add/remove; recommended categories are pre-checked — non-regenerable Xcode archives/iOS backups and `safeToClean:false` pnpm store/simulator leftovers are unchecked by default with a stated reason) → "Clean selected" → an **in-place two-step confirmation bar** (N items · size → Trash (recoverable) / permanent delete) → `POST /pc-manager/junk/clean` executes directly → an **in-place result briefing**. The host side shares the same config gate and id validation chain as the tool path (all-or-nothing rejection, zero deletion) and emits an audit log line; the confirmation bar cannot be skipped and stands in for the tool path's approval panel. **The floating window's "Scan" button invokes this window and starts scanning automatically** (`openTab` + navigation revision; re-invoking re-scans); the five tools and all security defenses are unchanged. **Note**: the conversational approval panel only appears when the session Access mode's approval policy is ask; under the danger-full-access deployment default (`approval: never`), in-session ask is silently auto-rejected — switch Access mode in the session or adjust the deployment preset. The UI direct-execution window is not subject to this. ## Security design - **Read-only tools work out of the box**; the two destructive tools default to `disabled_by_config` and only act after the host flips the switches in a back-layer patch. - Trash/uninstall defaults to the **Trash** (recoverable — on Windows that is the system "Recycle Bin"; the two terms are synonyms in this document); permanent deletion requires `moveToTrash: false`. The trash landing has three tiers: on macOS `/usr/bin/trash` (invoked by absolute path, guarding against PATH hijacking) → ownership-checked rename into `~/.Trash` (name conflicts get ` 2`/` 3` suffixes) → cross-volume EXDEV cp+rm; on Linux `trash-put`/`gio trash` (probed absolute paths) → freedesktop `~/.local/share/Trash/{files,info}` rename + **`.trashinfo` restore records** (name conflicts suffix both sides in sync) → cross-volume cp+rm; on Windows the `Microsoft.VisualBasic.FileIO.FileSystem` `SendToRecycleBin` (the same shell recycle as Explorer, landing in the **real Recycle Bin** with the original location recorded, restorable from Explorer; paths are **embedded into the script as PowerShell literals** rather than appended to argv — `-Command` re-joins arguments with spaces, so a path containing one silently splits, and a multi-statement script never sees `$args` at all) → fallback to `%LOCALAPPDATA%\pc-manager\trash` (a private holding area inside the profile, still recoverable, just not in the Recycle Bin UI) → cross-volume cp+rm. After tier-1 reports success, the source is **checked to have actually disappeared**, otherwise the tier counts as failed and the next one runs — a silent no-op (the shell API blocked by policy, arguments never bound) must never be reported as "reclaimed N bytes" (the Windows e2e caught exactly one such fake success). The `trash` kind itself is emptied in place (macOS empties `~/.Trash`, Linux empties the contents of `files/`+`info/`, Windows empties this account's `%SystemDrive%\$Recycle.Bin\` — exactly what "Empty Recycle Bin" means for that volume). Moving the Trash back into the Trash would be a pointless matryoshka. - **Three-layer confirmation for junk cleanup**: model conversation confirmation (the tool description instructs preferring `ask_user_question`) + the framework's `tools/pre-execute` approval gate (`askBeforeJunkClean` defaults to true, asked every time, fail-closed when no answerer) + the id structural validation chain (strict child paths for `children` kinds / exact root for `whole` kinds, dual-sided realpath against symlink redirection, blocked list, safeToClean; any failure **rejects the whole call with zero deletion**). - The **safe target registry** is the core asset: macOS 18 kinds / 19 rows, Linux 11 kinds / 12 rows (XDG), Windows 11 kinds / 18 rows (`%VAR%` roots), including a sensitive-cache protection list (the "caches" of password managers/IDEs/input methods/VPNs/sync drives/AI apps are actually non-regenerable state; hits are recorded as `skipped` and produce no item) and EDR/active-service prefix protection (deleting enterprise security agent caches triggers tamper alarms; Linux additionally protects `systemd-private-*` and `snap-private-tmp`; Windows protects AV/EDR directories and installer scaffolding); `safeToClean:false` entries are reported but never cleaned, with suggested commands in the rationale (`xcrun simctl delete unavailable` / `pnpm store prune`). - **Path validation uses each literal's own path flavor**: Windows paths are case-insensitive and rooted at drive letters, POSIX paths are case-sensitive; the validation chain (root containment / equality / blocked) normalizes and compares according to each path's own style, so the same code validates both `C:\Users\t\AppData\Local\Temp\x` and `/home/t/.cache/x`. - Domain modules (`src/monitor.ts`, `junk.ts`, `apps.ts`, `win32.ts`) do not depend on cordis; pure logic, independently unit-testable. ## Appendix ### Monitoring metrics and data sources All probes dispatch per platform and degrade to null/empty + `console.warn` on failure — a probe failure never takes down the snapshot. Linux base metrics are all read from `/proc` and `/sys`, zero dependencies and zero privilege escalation; **capability-detection enhancements** (per-process network attribution, temperature, per-process GPU) follow "enable only when the capability is detected; missing capability means silent null, no warn spam" — see the table and [Capability-gated probes](#capability-gated-probes). | Group | Metric | macOS source | Linux source | Parser | | --- | --- | --- | --- | --- | | CPU | Utilization % | `os.cpus()` two samples, 250ms delta | same (sampling window ≥1s, see disk I/O) | `cpuUsagePercent` | | CPU | Model/cores/load 1/5/15 | `node:os` | `node:os` | — | | CPU | Package temperature °C | null (`powermetrics` needs sudo, not run on the user's behalf) | `/sys/class/hwmon` (name + `temp*_input` millidegrees) falling back to `/sys/class/thermal` (type + temp); max across CPU-family chips; cloud hosts often lack sensors → null | `pickCpuTempCelsius` / `isCpuTempSource` | | GPU | Utilization % (best effort) | `Device Utilization %` from `ioreg -r -d 1 -c IOAccelerator`, max across GPUs | `nvidia-smi --query-gpu=utilization.gpu` (presence probed once and cached; null without NVIDIA hardware, whole card hidden in the UI) | `parseIoregGpu` / `parseNvidiaSmiGpu` | | Memory | macOS: used = active+wired+compressed (fallback total−free); Linux: used = MemTotal−MemAvailable (fallback total−free−buffers−cached) | `vm_stat` (page size parsed from the header) | `/proc/meminfo` (app≈AnonPages, wired≈SUnreclaim, cached=Buffers+Cached+SReclaimable) | `parseVmStat` / `parseMeminfo` | | Memory | Swap total/used | `sysctl -n vm.swapusage` | SwapTotal/SwapFree from `/proc/meminfo` | `parseSwapUsage` / `parseMeminfo` | | Disk | Per-volume usage | `df -k` | `df -k` (parser tolerates both column layouts; filters pseudo-filesystems like tmpfs/udev/overlay/squashfs and /dev /proc /sys /run /snap mount points) | `parseDf` | | Disk | I/O throughput (read+write combined) | `iostat -d -c 2`, last sample summed | `/proc/diskstats` two-sample delta (whole physical disks sd/nvme/vd/hd/mmcblk, excluding partitions and loop/dm; sampling window stretched to 1s) | `parseIostat` / `parseDiskstats` + `diskstatRate` | | Battery | Charge/charging/time remaining/cycle count/health | `pmset -g batt` + `ioreg -rn AppleSmartBattery` | `/sys/class/power_supply/BAT*/uevent` (AC from `A*/online`; card hidden when no battery) | `parsePmsetBatt` / `parseIoregBattery` / `parseBatteryUevent` | | Network | Per-interface cumulative rx/tx (rates derived from call-to-call deltas) | `netstat -ib` (excludes lo*, dedupes `` rows) | `/proc/net/dev` (excludes lo) | `parseNetstatIb` / `parseProcNetDev` | | Network | Local IPv4 | `node:os` `networkInterfaces()` (non-internal, excluding `169.254.*` link-local; **identical across all three platforms, works on win32 too**) | same | `pickLocalAddresses` | | Network | Public IP and geo (optional) | ipwho.is HTTPS lookup (`enableIpGeoLookup` on by default; successes cached for `ipGeoRefreshMinutes` (default 30min, min 5), failures negatively cached 60s; the endpoint can be swapped for a compatible mirror via `ipGeoEndpoint`) | same | `parseIpWhoIs` / `createIpGeoLookup` | | Process network | **Dashboard/SSE frames: live rates**; **the `pc_status` tool: cumulative values** — both scales stand on their own | `nettop` cumulative + pump delta | **as root** `ss -tinp` socket attribution (TCP only: sum of bytes_received/bytes_sent over currently open sockets; without root the whole column is null and hidden) + pump delta | `parseNettop` / `parseSsTinp` / `diffProcessRates` | | Process GPU | SM utilization % | null (no source) | `nvidia-smi pmon -c 1` attributed by pid (max across GPUs; column hidden without the binary or without occupying processes) | `parseNvidiaSmiPmon` / `mergeGpuPercent` | | System | OS version | `sw_vers -productVersion` | `/etc/os-release` PRETTY_NAME (e.g. `Debian GNU/Linux 12 (bookworm)`) | `parseSwVers` / `parseOsRelease` | | Processes | CPU%/memory%(+rss)/network cumulative/GPU%; disk column reserved, always null | `ps -Ao pid,pcpu,pmem,rss,comm` + `nettop` (CSV/JSON both supported) | `ps -Ao pid,pcpu,pmem,rss,args` (Linux comm truncates at 15 chars, hence args) + `ss -tinp` (root) + `pmon` | `parsePs` / `parseNettop` / `parseSsTinp` / `mergeProcesses` / `mergeGpuPercent` / `sortProcesses` | ### Windows data sources Windows has neither `/proc` nor `df`/`ps`, so the strategy is **node builtins carry the hot fields + one batched PowerShell probe for everything else**: a single `powershell.exe -NoProfile -NonInteractive -NoLogo -Command` process fetches everything in one round (volumes, memory split, per-NIC counters, disk throughput, GPU, battery, ACPI thermal zones, the process table). One interpreter per round, because **startup dominates** (~2.3 s cold start on a host with endpoint security; each extra WMI query adds only 30–700 ms) — and a second concurrent interpreter would show up as a row in the very process table it is collecting: the script reports its own `$PID` and the parser drops that row. | Group | Metric | Windows source | Parser | | --- | --- | --- | --- | | CPU | Utilization/model/cores | `node:os` (`os.cpus()` delta) | `cpuUsagePercent` | | CPU | Load 1/5/15 | **null** (Windows has no load average; `os.loadavg()` always returns 0, which is "no such number", not "idle" — the dashboard hides the line) | — | | CPU | Package temperature °C | `root\WMI` `MSAcpi_ThermalZoneTemperature` (0.1 K → °C, max across repeated readings; usually needs elevation and desktops often lack the class → null, capability-gated) | `parseWindowsBundle` | | GPU | Utilization % + adapter name | `Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine` — **vendor-agnostic** engine counters (AMD/Intel/NVIDIA alike, unlike `nvidia-smi` which serves one vendor); the whole-card scale takes the **busiest engine** (its per-process sum, clamped to 100, the Task Manager scale). The adapter name comes separately from `Win32_VideoController.Name` (e.g. `AMD Radeon(TM) Vega 8 Graphics`, the dashboard card title). When the counter class is missing, falls back to `nvidia-smi --query-gpu` (presence probed once and cached) | `parseWindowsGpuEngines` | | GPU | Per-process utilization % | The same engine counters attributed by `pid__…_engtype_` (that process's engine instances summed, clamped to 100 = the Task Manager "Processes" column); falls back to `nvidia-smi pmon` when the class is missing | `parseWindowsGpuEngines` / `mergeGpuPercent` | | Memory | total/used | node builtins `os.totalmem()`/`os.freemem()` (the latter is `GlobalMemoryStatusEx`'s "available", including standby — equivalent to MemAvailable) | — | | Memory | app/wired/cached/swap breakdown | `Win32_PerfFormattedData_PerfOS_Memory` (`AvailableBytes`/`StandbyCacheNormalPriorityBytes`/`PoolNonpagedBytes`/`CommittedBytes`) + `Win32_PageFileUsage` (the page file as swap); no counters exist for compressed/purgeable → null | `parseWindowsBundle` | | Disk | Per-volume usage | `Win32_LogicalDisk` (DriveType 2/3/4, `Size`/`FreeSpace`/`FileSystem`; `mount` is the drive root `C:\`) | `parseWindowsVolumes` | | Disk | I/O throughput | `Win32_PerfFormattedData_PerfDisk_PhysicalDisk`'s `_Total` `DiskBytesPersec` (no waiting — cheaper than `Get-Counter`, which would cost an extra 1 s sample) | `parseWindowsBundle` | | Network | Per-interface cumulative rx/tx | `Win32_PerfRawData_Tcpip_NetworkInterface` (the `…Persec` suffix is a perf-counter naming convention; the values are cumulative since boot; excludes Loopback/isatap/Teredo/Pseudo; duplicate instances keep their `_2` suffix) | `parseWindowsNetwork` | | Battery | Charge/charging/power source/time remaining | `Win32_Battery` (`BatteryStatus` 2/6/7/8/9/11 = on AC, 6–9/11 = charging; `powerSource` reuses the macOS `AC Power`/`Battery Power` literals; the `EstimatedRunTime` sentinel 71582788 → null); cycle count/health need vendor WMI or `powercfg /batteryreport` → null | `parseWindowsBattery` | | System | OS version | `os.version()` + `os.release()` (e.g. `Windows 11 Pro for Workstations 10.0.26300`; obtainable even without PowerShell), overridden by `Win32_OperatingSystem`'s Caption when the probe is available | `windowsOsVersion` | | Processes | CPU%/memory%/command | `Get-Process`: `cpuPercent = cumulative CPU seconds / (now − StartTime) × 100` (**per-core scale, process-lifetime average**, matching ps semantics so rankings compare across platforms; protected processes whose `StartTime`/`CPU` are unreadable → 0), `memPercent = WorkingSet64 / totalmem`, command from `Path` (falls back to the process name when unreadable) | `parseWindowsProcesses` | | Processes | Network column | **null, the whole column stays hidden** — the only metric Windows cannot supply: per-process byte attribution needs an ETW kernel network session (Task Manager shows that column only because it runs with an admin token by default); there is no zero-privilege source. Global rates are still given per interface by the "Network" card | — | **Round cost and dashboard cadence**: one Windows collection = 1 PowerShell process (~2.5–4.5 s depending on the machine's security software) + node builtins (~10 ms) + optional `nvidia-smi`; the pump skips overlapping rounds, so even with `dashboardPollMs` set to 500, the actual refresh cadence on Windows is one round's duration (a platform fact — the host does not silently rewrite the config). When the probe is absent (no PowerShell) the snapshot still works: CPU, memory totals, uptime and OS version all come from node builtins; only sizes, the process table, battery and similar details are empty. ### Capability-gated probes Enhancements that "enable only when the capability is detected" (no retry probing on failure, no warn spam; missing capability means the whole column/row is null and the UI hides it accordingly): | Probe | Gate | Semantics and boundaries | | --- | --- | --- | | Per-process network attribution `ss -tinp` | `process.getuid() === 0` | Only under root does `ss -p` attribute **all** sockets; without root only one's own processes are visible, so the whole column is withheld instead. TCP only, summed over current sockets (a closed socket's counters zero out; the delta window drops that pid's rate automatically, so no negative or fake rates) | | Temperature hwmon/thermal_zone | No privilege needed; read when a sensor is present | CPU-family chip names (coretemp/k10temp/zenpower/cpu_*/acpitz/x86_pkg_temp/soc_*) take the max reading; nvme/amdgpu etc. are not counted; cloud hosts without sensors → null (CPU card temperature row hidden) | | Per-process GPU `nvidia-smi pmon` | Binary present (probed once in `/usr/bin`, `/usr/local/bin`, cached) | SM utilization attributed by pid; `-` placeholder rows skipped; max across GPUs. Hosts without NVIDIA hardware never spawn it again after the first probe | | Windows full probe (PowerShell bundle) | `powershell.exe` present (`%SystemRoot%\System32\WindowsPowerShell\v1.0`, probed once and cached; `pwsh` 7 as fallback) | Absent → degrade to the node-builtin "minimal snapshot" (CPU, memory totals, uptime, OS version remain), sizes/process table/battery/network empty; each field inside the bundle degrades on its own (missing WMI class or privilege required → null) | | Windows GPU engine counters | `Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine` class present (probed once with `-ErrorAction Stop`; present on Windows 10 1709+ with a display adapter) | Vendor-agnostic whole-card + **per-process** GPU utilization (two scales from one dataset), plus the `Win32_VideoController` adapter name for the card title. Missing class → GPU card and GPU column hidden, falling back to `nvidia-smi` (if present). **0% means "measured idle", null means "no such capability"** — an idle iGPU still gets a card | | Windows package temperature | `root\WMI` `MSAcpi_ThermalZoneTemperature` returns readings | Common on laptops and usually needs elevation; desktops/restricted environments lack the class → null, CPU card temperature row hidden | | Windows per-process network | No source (needs an ETW kernel session / privileged helper) | The network column is hidden entirely rather than faked with 0; global rates still live in the "Network" card | | Windows `nvidia-smi` (fallback) | Present in `System32`, `NVIDIA Corporation\NVSMI` or PATH, probed once and cached | Spawned only when the engine counters are unavailable; the same parsers as Linux (whole-card `--query-gpu` + per-process `pmon`) | ### Junk target registry (per-platform dispatch) The registry structure is shared across platforms (`JunkTarget`); entries dispatch per platform (`JUNK_TARGETS_BY_PLATFORM`): `JUNK_TARGETS_DARWIN` (18 kinds / 19 rows), `JUNK_TARGETS_LINUX` (11 kinds / 12 rows, XDG layout) and `JUNK_TARGETS_WIN32` (11 kinds / 18 rows, `%VAR%` roots); the `JUNK_KINDS` closed vocabulary is identical across all three platforms (schema stays stable); platform-only kinds have no entries on the other sides (user-logs on Linux; the Xcode/simulator family and ios-backups on Linux/Windows; homebrew-cache on Windows). **The path-flavor layer**: the registry carries both POSIX and Windows literals, while the test suite pins all three tables on any host — so every path transformation picks `path.posix`/`path.win32` **by the literal's own flavor** (`pathFlavor`/`normalizePath`/`joinPath`/ `isAbsolutePath`/`basenamePath`), never by the host default — otherwise running the tests on Windows would rewrite POSIX fixtures into `\Users\t\.Trash`. Normalization strips trailing separators (except on roots): `user-caches:/x/.cache/` is the root itself and must not pass as "a child of the root" (otherwise a trailing slash could wipe a whole registry root). | Linux kind | Directories | Granularity | minAge | safe | | --- | --- | --- | --- | --- | | `trash` | `~/.local/share/Trash` | whole | — | ✅ | | `user-caches` | `~/.cache` (incl. thumbnails) | children (protected by `LINUX_PROTECTED_CHILDREN`) | — | ✅ | | `system-temp` ×2 | `/tmp`, `/var/tmp` | children (`systemd-private-*`, `snap-private-tmp`, Linux EDR prefixes protected) | 3 days | ✅ | | `npm-cache` / `pnpm-store` / `homebrew-cache` | `~/.npm/_cacache` / `~/.local/share/pnpm/store` (❌ prefer `pnpm store prune`) / `~/.cache/Homebrew` | whole / children / children | — | ✅ / ❌ / ✅ | | `pip-cache` / `uv-cache` / `yarn-cache` / `go-build-cache` / `go-mod-cache` | `~/.cache/{pip,uv,yarn,go-build}`, `~/go/pkg/mod/cache` | whole | — | ✅ | | Windows kind | Directories | Granularity | minAge | safe | | --- | --- | --- | --- | --- | | `trash` | `%SystemDrive%\$Recycle.Bin` | children (one per account SID; `WINDOWS_SYSTEM_SIDS` protects LocalSystem/LocalService/NetworkService) | — | ✅ | | `system-temp` ×2 | `%TEMP%`, `%SystemRoot%\Temp` | children (protected by `WINDOWS_TEMP_PROTECTED`) | 3 days | ✅ | | `user-logs` ×3 | `%LOCALAPPDATA%\…\WER\{ReportArchive,ReportQueue}`, `%LOCALAPPDATA%\CrashDumps` | children | — | ✅ | | `user-caches` ×5 | `INetCache`, `D3DSCache`, `NVIDIA\{DXCache,GLCache}`, `Terminal Server Client\Cache` | children | — | ✅ | | `npm-cache` / `pnpm-store` | `%LOCALAPPDATA%\npm-cache` / `%LOCALAPPDATA%\pnpm\store` (❌ prefer `pnpm store prune`) | whole / children | — | ✅ / ❌ | | `pip-cache` / `uv-cache` / `yarn-cache` / `go-build-cache` / `go-mod-cache` | `%LOCALAPPDATA%\{pip\Cache,uv\cache,Yarn\Cache,go-build}`, `%USERPROFILE%\go\pkg\mod\cache` | whole | — | ✅ | **Windows has no `user-caches` umbrella row** (deliberate): `%LOCALAPPDATA%` is not a `~/.cache`-style cache directory — it mixes real application state (`Packages\*\LocalState`, `Microsoft\Credentials`, browser profiles) that no protection list could reliably cover, so only individually known-safe cache roots are registered. Likewise `$Recycle.Bin` uses children granularity with one row per account SID: other accounts' SID directories are unreadable (degraded to `skipped`), and the system-account SIDs are held back by the protection list. **Imperative reclamation is not in the registry** (following design doc §14): `apt-get autoremove --purge` (simulated with `-s` first), `journalctl --vacuum-size=`, old snap revisions (disabled rows of `snap list --all` → `snap remove --revision`), `flatpak uninstall --unused`, `docker system prune`, plus on Windows `Dism /Online /Cleanup-Image /StartComponentCleanup` (WinSxS component cleanup), `SoftwareDistribution\Download` (the Windows Update cache, requires stopping `wuauserv`) and Storage Sense — reported as suggested commands only, never execution paths. **The blocked red-line list** (union of all three platforms, guarding against future registry misconfiguration): macOS keeps `/System`, `/usr`, `/private/var/db`, `~/Library/Containers` etc. as-is; Linux: `/etc`, `/boot`, `/var/log`, `/var/lib` (dpkg/snapd/flatpak state included), `/var/cache`, `/lib*` (running kernel modules included), `/srv`; Windows: the list is **derived from the environment** (`SystemRoot`/`ProgramFiles`/`ProgramData`/`SystemDrive`, with conventional-path fallbacks), covering `%SystemRoot%`, `Program Files`(+ x86), `ProgramData`, `Recovery`, `PerfLogs`, `System Volume Information`, `Users\{Default,Public,All Users}`, and additionally rejects **drive roots** (`C:\`) and other-account trees under the `$HOME` account root (case-insensitive: Windows filesystems ignore case); `$Recycle.Bin` itself is not on the red line — it is a registry root. In the multi-user home dual layout (`/Users` and `/home`) other users' directories are always rejected, and `/root` is rejected likewise when it is not $HOME. ### Reverse proxy / prefix-mounted deployment All browser-side requests (SSE and JSON) use **document-relative paths** (`pc-manager/…`, no leading `/`), resolved by the web shell's `` — a pre-existing harness convention (architecture note *web-document-relative-app-routes*, same as the `/plugins/events` channel). The plugin therefore **naturally supports prefix-stripping reverse-proxy mounts** (e.g. `https://host/dsh/` → forward and strip the prefix) and root-path deployments alike, with no rebuild. When the SSE stream traverses a reverse proxy, mirror the config dsh itself uses for `/plugins/events`; otherwise default buffering delays frames and the default 60s read timeout kills the long connection: ```nginx location = /dsh/pc-manager/stream { proxy_pass http://127.0.0.1:3080/pc-manager/stream; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; proxy_read_timeout 24h; proxy_send_timeout 24h; } ``` ## Repository layout A dual-face package: the host half (tools + routes) and the browser half both consume the **committed build artifacts in `lib/`** (host `index.js` ESM, external dependencies resolved by dsh at runtime; client `client.js` is a `window.__ModuleLoader__.load` artifact): ``` ├── src/ │ ├── index.ts Plugin entry: Config (TS interface + Schemastery schema) + apply; host half holds the webServer routes │ ├── monitor.ts System probes and pure parsers (macOS subcommands + Linux /proc /sys + the Windows batched PowerShell bundle; no cordis dependency, unit-testable standalone) │ ├── junk.ts Junk domain: three-platform safe registry (darwin/linux/win32), the path-flavor layer, protection/blocking lists, walk, id validation chain, three-tier trash │ ├── win32.ts Windows platform facilities: PowerShell discovery (cached promise, avoiding concurrent first-call races) and the fault-tolerant JSON reader │ ├── apps.ts App domain (M3 stub) │ ├── types.ts Domain contracts, PcManagerError, the closed error vocabulary PcErrorCode │ ├── tools.ts The only layer touching defineTool: guarded() translates exceptions into the closed error union │ └── client/ Browser half: dashboard / junk cleanup window / floating window / locale (value imports limited to react-family) ├── lib/ Build artifacts (committed: host index.js + client client.js; *.map ignored) ├── tests/ vitest: all parsers + the full junk domain suite (registry rows pinned, validation chain layer by layer) │ + monitor.win32 / junk.win32 (Windows parsers and registry, host-independent) │ + e2e.win32 (real-host end to end: snapshot / scan / real Recycle Bin round-trip / HTTP and tool faces / build artifacts) ├── scripts/ build / test / dev-setup (one command: link into profile + build; cross-platform .bin/.cmd shim resolution) ├── bundle.patch.yml Bundle layer (applied by dsh.plugin add; safe defaults, destructive tools off) ├── cordis.patch.yml Dev overlay (mounted via --patch, with local dogfooding switches) ├── tsdown.config.ts Dual-entry build (host ESM + client CJS artifact contract) ├── tsconfig.json Type-check only (dual-candidate paths mapping to sibling/nested harness layouts) └── package.json dsh-pc-manager (dsh.bundle + dsh.client + real semver peer ranges) ``` ## Local development Prerequisite: this repo and [deepseek-harness](../deepseek-harness) are checked out **side by side** (or nested one level up in the workspace directory, or pointed at via the `DSH_HARNESS_ROOT` env var). The toolchain (tsdown / vitest / tsc) is entirely resolved from the harness side; **do not `pnpm install`** in this repo — there is no `node_modules` at all. > **Verifiable without a harness checkout**: `scripts/test.mjs` only requires `DSH_HARNESS_ROOT` > to point at a directory with a `package.json` and `vitest`/`tsc` installed, and > `tsconfig.json` resolves `@deepseek-ai/*` by the same convention in its paths. So you can build > a "toolchain stand-in" yourself (`npm i vitest typescript @types/node @types/react react > react-dom tsdown` + junctions following the nested paths in `tsconfig.json`) — that is exactly > how this repo's Windows support was verified (tsdown 0.23 / vitest 3.2 / TypeScript 6.0, with > the `@deepseek-ai/*` 0.2.0-rc.2 series). One-command dev install (links into the profile's node_modules + builds artifacts, idempotent): ```sh npm run dev-setup # defaults to --profile web; or node scripts/dev-setup.mjs --profile ``` Run (from the harness repo root; the host half now also consumes `lib/index.js`, so host code changes require a fresh `npm run build`): ```sh cd ../deepseek-harness pnpm dsh --patch ../dsh-pc-manager/cordis.patch.yml --profile web ``` Tests and type checking: ```sh npm test # vitest (182 specs: 176 pass + 6 platform skips) + tsc --noEmit, both via the harness binaries ``` **Cross-platform toolchain notes**: on Windows, `scripts/{build,test}.mjs` automatically target `.bin/.cmd` and start it with `shell: true` (the extensionless shims in `node_modules/.bin` are not directly executable on Windows), and `scripts/dev-setup.mjs` uses a **junction** instead of a directory symlink (symlinks on Windows need Developer Mode or elevation). `tsc` requires TypeScript 6+: `tsconfig.json`'s `ignoreDeprecations: "6.0"` errors out with `TS5103` on 5.x. Coverage spans every pure parser (df both platforms incl. Linux pseudo-filesystem filtering / ps / vm_stat / meminfo / swapusage / os-release / pmset / ioreg battery & GPU / nvidia-smi whole-card + pmon per-process / iostat / diskstats rate delta / netstat / net-dev / nettop CSV+JSON / ss -tinp attribution / power_supply uevent / temperature chip selection / process & GPU merge+sort / CPU delta) and the full junk domain (per-row pins for all three platform registries and protection lists, glob expansion, walk statistics, scan semantics, the validation chain layer by layer incl. symlink redirection and the trailing-slash root escape, all-or-nothing rejection, three-tier trash on all three platforms incl. `.trashinfo` restore records and conflict-suffix sync, the blocked red-line union, size-mismatch refusal, approval summary en/zh snapshots), plus the Windows-specific parsers (`Win32_LogicalDisk` volume rows, `PerfRawData_Tcpip_NetworkInterface` NIC rows, `Win32_Battery` status codes and the `EstimatedRunTime` sentinel, `Get-Process` process rows with self-exclusion of the sampling process, per-field bundle degradation) and Windows path-flavor/red-line/validation cases. **End to end (`tests/e2e.win32.spec.ts`)** — nothing injected: on a real host it runs `collectStatus` and asserts snapshot self-consistency (volume arithmetic closes, memory within total, this process present in the process table, `loadavg` null); a real read-only registry scan; a **real Recycle Bin round-trip** (hands a fixture file to the shell recycle, finds it again in the `$Recycle.Bin` `$I`/`$R` records to verify the original location, then deletes only that one entry without touching the user's existing Recycle Bin contents); real delete mode; a real `apply()` against a stubbed cordis context hitting the four `/pc-manager/{status,junk/scan,junk/clean}` faces (incl. 405/400/403 rejection paths) and the five tools; and finally loads the **committed `lib/` artifacts** (both host and client) to check their contracts — this step fails when source changed but `npm run build` was forgotten. Permission-degradation (EACCES) cases need POSIX permission semantics and skip automatically under root or Windows (root's DAC override reads through 0000 permissions; Windows `chmod` only flips the read-only attribute and cannot block reads). **Release convention**: after any source change you must `npm run build` and commit `lib/*.js` together — the git install loads the committed artifacts directly and runs no build scripts (this also sidesteps pnpm ≥10 build authorization). Toolchain note: `scripts/build.mjs` loads the TS config with `tsdown --config-loader native` and automatically adds `--experimental-strip-types` (tsdown's default unrun config loader is not a harness dependency, and Node <23 needs the flag too) — no manual intervention required. ## Milestones - **M0 (done)**: project skeleton — type contracts, 5 tool schemas and registration, plugin entry, patch manifests; basic `pc_status` (node built-ins + `df`/`ps`). - **M1 (done, 2026-10-05)**: full `pc_status` metrics — precise `vm_stat` memory, swap, battery/power, GPU, disk I/O, network, macOS version, five-metric process table (GPU/disk columns reserved); plus the right-sidebar dashboard (client half + webServer routes + polling). - **M2 (done, 2026-10-06)**: junk scan & cleanup shipped — 18-kind safe registry + protection/ blocking lists, dry-run walk (concurrency 4, symlinks not followed, EACCES degrades to `skipped`), id validation chain (all-or-nothing rejection, zero deletion), three-tier trash, pre-execute approval gate (`askBeforeJunkClean`). - **M2.5 (done, 2026-10-06)**: trigger UI — dashboard action card + floating-window action row + `startSession` seeded draft, sharing the `cleanup.ts` trigger. - **M2.6 (done, 2026-10-07)**: plan-first — prebuilt recommended plan (`RECOMMENDED_PLAN` into domain data + test pin), host `GET /pc-manager/junk/scan` read-only route, dashboard plan card, execution seed carrying ids; LLM turns 4→1. - **M2.7 (done, 2026-10-07)**: plan card rework + UI direct execution — categories as the selection unit (kind rows on one screen by default, details folded), server-side 1 MiB threshold filter, `POST /pc-manager/junk/clean` direct execution (same gate and validation chain + in-place two-step confirmation + audit log line + in-place briefing), dashboard path **zero LLM**. - **M2.8 (done, 2026-10-07)**: standalone window — junk cleanup upgraded to its own right-sidebar tab (guide entry peer-level with System Monitor, order 30/31); the floating window's "Scan" invokes it via `sidebarRight.openTab` + the autoScan param and scans automatically (re-invoke re-scans); the conversational seed path retired. Real cleanup e2e verified three times (pip/npm/Homebrew caches → Trash, byte-exact). - **M2.9 (done, 2026-10-07)**: Linux platform support — monitoring probes dispatch per platform (/proc/meminfo, /proc/net/dev, /proc/diskstats two-sample delta, /sys/class/power_supply, /etc/os-release, best-effort nvidia-smi, df pseudo-filesystem filtering, ps args column); the junk registry splits per platform (`JUNK_TARGETS_LINUX` 12-row XDG layout + `LINUX_PROTECTED_CHILDREN`/`LINUX_TEMP_PROTECTED` protection lists); blocked red-line union (/etc, /var/lib, /lib*, /boot etc.); Linux landing of the three-tier trash (trash-put/gio trash → freedesktop `files/`+`info/` with `.trashinfo` restore records → cross-volume cp+rm); platform-neutral tool copy and dashboard labels; the test suite runs green on both Linux and macOS (platform assertions injected explicitly, EACCES cases skipped under root). - **M2.10 (done, 2026-10-07)**: capability-gated probes — Linux root `ss -tinp` socket attribution completes per-process network (TCP only, rates via pump delta; whole column hidden without root); `nvidia-smi pmon -c 1` attributes SM utilization by pid (binary presence probed once and cached; absent → never spawned, no warn spam); CPU package temperature reads `/sys/class/hwmon` falling back to `/sys/class/thermal` (max across CPU-family chips, `cpu.temperatureCelsius` enters the schema and the CPU card); `pc_status` description synced. - **M2.11 (done, 2026-10-07)**: Windows platform support — monitoring on node builtins for the hot fields (CPU delta, memory totals/available, uptime, `os.version()`+`os.release()` OS name) plus a **single batched PowerShell probe** (`Win32_LogicalDisk` volumes, `PerfOS_Memory` memory split, page file as swap, `PerfRawData_Tcpip_NetworkInterface` NICs, `PerfDisk_PhysicalDisk` throughput, `GPUPerformanceCounters_GPUEngine` vendor-agnostic GPU engines (whole-card "busiest engine" scale + per-process GPU attributed by pid, falling back to `nvidia-smi` when the class is missing), `Win32_Battery`, `MSAcpi_ThermalZoneTemperature`, `Get-Process` process table, with the script self-reporting its `$PID` so the sampling process vanishes from its own table); `loadavg` became nullable (Windows has no load average; the dashboard hides the line instead of showing a fake 0); on the junk side `JUNK_TARGETS_WIN32` (11 kinds / 18 rows with `%VAR%` roots, the Recycle Bin split per account SID) and Windows red lines (environment-derived system directories + drive roots + other-account trees, case-insensitive); the **path-flavor layer** (posix/win32 chosen by each literal's own style, which also fixed a POSIX-side vulnerability where a trailing slash could treat a registry root as "a child of the root" and wipe it); Windows landing of the three-tier trash (real Recycle Bin → profile-private holding area → cross-volume cp+rm) with tier-1 result re-verification; cross-platform `scripts/*` (`.cmd` shims + junctions); 60 new Windows cases and 9 real-host e2e cases (incl. the Recycle Bin round-trip, GPU capability consistency assertions, and the committed-`lib/` contract). **The only metric that cannot be supplied** is per-process network attribution (needs an ETW kernel session, no zero-privilege source); that column is capability-gated hidden on Windows. - **M3 (planned)**: app inventory & uninstall — macOS `/Applications` bundle walk (du + kMDItemLastUseDate), merged with `brew list`, trash-based uninstall, residual reporting (plist/`App Support`/caches); Linux distro-package inventory and `rc` residual cleanup, plus Windows registry Uninstall keys + a `%ProgramFiles%` walk, as later candidates. - **Unscheduled**: macOS temperature (`powermetrics` needs sudo, not run on the user's behalf), real per-process disk numbers (needs a privileged helper), Windows per-process network attribution (needs ETW), refcount-aware pnpm store cleanup, user-persistent exclusion lists. ## License [MIT](LICENSE)