dsh-pc-manager — PC Manager
English · 简体中文
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), 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
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:
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):
- 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):
# 1. Let the profile resolve the package by name (the client half is discovered
# via require.resolve('<pkg>/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) | ✅ 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. 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\<SID> — 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 kinds /
exact root for kinds, dual-sided realpath against symlink redirection, blocked list,
safeToClean; any failure ).
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.
| 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) |
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_<pid>_…_engtype_<type> (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 |
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 |
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 <base href="./"> — 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:
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 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):
npm run dev-setup # defaults to --profile web; or node scripts/dev-setup.mjs --profile <name>
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):
cd ../deepseek-harness
pnpm dsh --patch ../dsh-pc-manager/cordis.patch.yml --profile web
Tests and type checking:
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/<name>.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 + with 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).
License
MIT