DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-pc-manager

m

dsh-pc-manager

Manifest valid

DSH plugin for system monitor and junk cleanup

UI (client)hasBundlePatch

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

ToolPurposeStatus
pc_statusRead-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_scanEnumerates 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_cleanTrashes 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_listInstalled-app inventory (app bundles + Homebrew; Linux distro packages and Windows registered programs await M3), with size / last-usedStub, M3
pc_app_uninstallUninstalls by exact id; defaults to Trash, residual items are reported rather than silently deletedStub, 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 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.

GroupMetricmacOS sourceLinux sourceParser
CPUUtilization %os.cpus() two samples, 250ms deltasame (sampling window ≥1s, see disk I/O)cpuUsagePercent
CPUModel/cores/load 1/5/15node:osnode:os—
CPUPackage temperature °Cnull (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 → nullpickCpuTempCelsius / isCpuTempSource
GPUUtilization % (best effort)Device Utilization % from ioreg -r -d 1 -c IOAccelerator, max across GPUsnvidia-smi --query-gpu=utilization.gpu (presence probed once and cached; null without NVIDIA hardware, whole card hidden in the UI)parseIoregGpu / parseNvidiaSmiGpu
MemorymacOS: 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
MemorySwap total/usedsysctl -n vm.swapusageSwapTotal/SwapFree from /proc/meminfoparseSwapUsage / parseMeminfo
DiskPer-volume usagedf -kdf -k (parser tolerates both column layouts; filters pseudo-filesystems like tmpfs/udev/overlay/squashfs and /dev /proc /sys /run /snap mount points)parseDf
DiskI/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
BatteryCharge/charging/time remaining/cycle count/healthpmset -g batt + ioreg -rn AppleSmartBattery/sys/class/power_supply/BAT*/uevent (AC from A*/online; card hidden when no battery)parsePmsetBatt / parseIoregBattery / parseBatteryUevent
NetworkPer-interface cumulative rx/tx (rates derived from call-to-call deltas)netstat -ib (excludes lo*, dedupes <Link#> rows)/proc/net/dev (excludes lo)parseNetstatIb / parseProcNetDev
NetworkLocal IPv4node:os networkInterfaces() (non-internal, excluding 169.254.* link-local; identical across all three platforms, works on win32 too)samepickLocalAddresses
NetworkPublic 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)sameparseIpWhoIs / createIpGeoLookup
Process networkDashboard/SSE frames: live rates; the pc_status tool: cumulative values — both scales stand on their ownnettop cumulative + pump deltaas 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 deltaparseNettop / parseSsTinp / diffProcessRates
Process GPUSM 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
SystemOS versionsw_vers -productVersion/etc/os-release PRETTY_NAME (e.g. Debian GNU/Linux 12 (bookworm))parseSwVers / parseOsRelease
ProcessesCPU%/memory%(+rss)/network cumulative/GPU%; disk column reserved, always nullps -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) + pmonparsePs / 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.

GroupMetricWindows sourceParser
CPUUtilization/model/coresnode:os (os.cpus() delta)cpuUsagePercent
CPULoad 1/5/15null (Windows has no load average; os.loadavg() always returns 0, which is "no such number", not "idle" — the dashboard hides the line)—
CPUPackage temperature °Croot\WMI MSAcpi_ThermalZoneTemperature (0.1 K → °C, max across repeated readings; usually needs elevation and desktops often lack the class → null, capability-gated)parseWindowsBundle
GPUUtilization % + adapter nameWin32_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
GPUPer-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 missingparseWindowsGpuEngines / mergeGpuPercent
Memorytotal/usednode builtins os.totalmem()/os.freemem() (the latter is GlobalMemoryStatusEx's "available", including standby — equivalent to MemAvailable)—
Memoryapp/wired/cached/swap breakdownWin32_PerfFormattedData_PerfOS_Memory (AvailableBytes/StandbyCacheNormalPriorityBytes/PoolNonpagedBytes/CommittedBytes) + Win32_PageFileUsage (the page file as swap); no counters exist for compressed/purgeable → nullparseWindowsBundle
DiskPer-volume usageWin32_LogicalDisk (DriveType 2/3/4, Size/FreeSpace/FileSystem; mount is the drive root C:\)parseWindowsVolumes
DiskI/O throughputWin32_PerfFormattedData_PerfDisk_PhysicalDisk's _Total DiskBytesPersec (no waiting — cheaper than Get-Counter, which would cost an extra 1 s sample)parseWindowsBundle
NetworkPer-interface cumulative rx/txWin32_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
BatteryCharge/charging/power source/time remainingWin32_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 → nullparseWindowsBattery
SystemOS versionos.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 availablewindowsOsVersion
ProcessesCPU%/memory%/commandGet-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
ProcessesNetwork columnnull, 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):

ProbeGateSemantics and boundaries
Per-process network attribution ss -tinpprocess.getuid() === 0Only 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_zoneNo privilege needed; read when a sensor is presentCPU-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 pmonBinary 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 countersWin32_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 temperatureroot\WMI MSAcpi_ThermalZoneTemperature returns readingsCommon on laptops and usually needs elevation; desktops/restricted environments lack the class → null, CPU card temperature row hidden
Windows per-process networkNo 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 cachedSpawned 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 kindDirectoriesGranularityminAgesafe
trash~/.local/share/Trashwhole—✅
user-caches~/.cache (incl. thumbnails)children (protected by LINUX_PROTECTED_CHILDREN)—✅
system-temp ×2/tmp, /var/tmpchildren (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/Homebrewwhole / children / children—✅ / ❌ / ✅
pip-cache / uv-cache / yarn-cache / go-build-cache / go-mod-cache~/.cache/{pip,uv,yarn,go-build}, ~/go/pkg/mod/cachewhole—✅
Windows kindDirectoriesGranularityminAgesafe
trash%SystemDrive%\$Recycle.Binchildren (one per account SID; WINDOWS_SYSTEM_SIDS protects LocalSystem/LocalService/NetworkService)—✅
system-temp ×2%TEMP%, %SystemRoot%\Tempchildren (protected by WINDOWS_TEMP_PROTECTED)3 days✅
user-logs ×3%LOCALAPPDATA%\…\WER\{ReportArchive,ReportQueue}, %LOCALAPPDATA%\CrashDumpschildren—✅
user-caches ×5INetCache, D3DSCache, NVIDIA\{DXCache,GLCache}, Terminal Server Client\Cachechildren—✅
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\cachewhole—✅

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="https://github.com/maoqizhen/dsh-pc-manager/blob/HEAD/"> — 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 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

Comments

Loading…