maoqizhen/dsh-pc-manager

dsh-pc-manager

DSH plugin for system monitor and junk cleanup

Uncategorizeddeepseek-harness

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