Agent config packer for DeepSeek Harness: pack local agent assets (skills/sessions/profiles/settings/memory) into a zip for migration or sharing, with privacy scanning, diff-based restore, pack management, and a settings UI; memory ships as a read-only Markdown mirror, not the live database.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-packer
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:KLRSL/dsh-packer
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Agent Configuration Packer: pack your local DSH assets into standard zip archives — migrate, share, restore, with a privacy scan guarding every pack.
v0.7.2 · MIT License · DSH 0.2.x (
>=0.2.0-rc.2 <0.3.0-0, tested on the 0.2.1-rc.3 desktop app) · Node ≥ 22.19.0
dsh-packer is the Agent Configuration Packer plugin for DeepSeek Harness (DSH): it packs your local Agent assets, module by module, into standard zip archives, for two purposes:
- Migration — move your entire setup to a new machine, or recover after a reinstall.
- Sharing — hand Skills and other assets to other people, with sensitive content filtered automatically.
Every module is optional (Skills / Sessions / Attachments / Profiles / Settings / Presets / Memory); restore ships with diff reports and conflict strategies, and a privacy scan guards every pack — all capabilities are opt-in.
Features
| Feature | Description |
|---|---|
| Modular packing | Nine modules, any combination: skills / sessions / attachments (the bytes of images/files inside sessions, on by default for migration) / profiles / plugins (installed plugin bodies, on by default for migration) / settings / presets / memory (Markdown mirror) / memoryDb (consistent SQLite snapshot of the memory truth source, on by default) |
| Two built-in presets | Migrate (everything, including installed plugin bodies and session attachments) / Share (Skills only; sessions, attachments, memory data, plugins and personal skill subdirectories are automatically excluded) |
| Privacy & security scan | Detects drive-letter / Unix / UNC paths, user-directory paths, credential assignments and bare key shapes (sk-, ghp_, AKIA, JWT, …), plus personal nicknames, counting every occurrence; share mode force-sanitizes every hit (it never blocks on a hit), migrate mode reports only |
| File-level operation preview | Full file list preview before packing; diff report before restore (added / changed / same / skipped) |
| Three conflict strategies | Overwrite / skip / merge (merge appends, never overwrites existing content) |
| Safe restore writes | Targets are backed up to .restore-backups/<timestamp>/ inside the packs directory first, written to a temp file and swapped in with an atomic rename; any failure aborts and rolls back the files already replaced/added |
| Manifest integrity check | manifest.json records schemaVersion plus a SHA-256 fingerprint per file; source files are verified against it before restore, and a missing or malformed fingerprint is rejected outright (fail-closed) |
| Pack management & notes | Pack list (time / size / modules / note), delete, rename, and a note written at pack time |
| Share packs ship a README | A generated README.md describing the pack contents is attached automatically |
| Dark-mode ready | The settings page follows the DSH theme (--dsw-alias-* tokens plus a brand-amber dual palette, dual-channel detection) |
| Zero native dependencies | Standard zips built by a pure-JS ZIP writer (zip.mjs + node:zlib); openable by any unzip tool |
| Selective restore | Pick the modules to restore (Skills only, without touching sessions) and toggle includeDb separately to restore the memory truth-source database |
| Per-file diff | Importing a pack immediately computes added / changed / same / skipped, file by file, and the result card reports the backup directory |
| One-click rollback | /restore/backups lists every pre-restore backup; /restore/rollback (the UI's "Roll back" button) restores the files that run overwrote |
| Pack management | Browse a pack's file list with inline SHA-256 verification, rename, delete (deleted packs go to the platform's own trash: Recycle Bin on Windows, Trash on macOS, freedesktop Trash on Linux — all recoverable; HarmonyOS has none, so the UI warns that deletion is permanent) |
| Encrypted packs | AES-256-GCM with a scrypt-derived key (PKENC1 container); manifests and restores require the passphrase, and a wrong passphrase fails closed |
| Progress & cancel | Packing and restoring run as background jobs (/jobs/:id) with a done/total percentage in the UI and a cancel button (honoured at file boundaries, no half-written zip) |
Installation
Desktop app: install by package name (recommended)
- Open the DSH desktop app → Plugins in the sidebar → Add plugin;
- Type the package name
dsh-packerand click Install (it is enabled automatically).
CLI equivalent:
# replace desktop with your profile name
dsh plugin --profile desktop add dsh-packer
From GitHub (alternative)
# Requires git
dsh plugin --profile desktop add github:KLRSL/dsh-packer
As a local bundle (development / link)
# Run from the project directory
dsh plugin --profile desktop add link:./dsh-packer
dsh plugin registers the package into dsh.profile.bundles and mounts the patch automatically; restart DSH afterwards.
Verify the install:
- Open Settings → the "Config Packer" tab — you should see the module checkboxes and the pack list.
- Or run
/pack listin the terminal — an empty list (or "no packs yet") means the command registered fine. - Run
/pack create --dry-runonce — it previews the file list and scan results without producing a zip, confirming the module paths are readable.
Quick start
Packing (three steps)
# 1. Create a migrate pack (everything selected by default)
/pack create --note "moving to a new machine"
# 2. Or create a share pack (Skills only, sensitive content excluded)
/pack create --share --note "for a friend"
# 3. Preview first without generating a zip
/pack create --mode migrate --dry-run
When it finishes, the zip is written to ~/.dsh/packs/ (override with DSH_PACKS_DIR); share packs automatically include a README.md.
Restoring (three steps)
# 1. Import the pack
/pack restore ~/.dsh/packs/dsh-packer-2026-09-05-223045-migrate.zip
# 2. Review the diff report (added / changed / same / skipped)
# 3. Pick a conflict strategy: overwrite | skip | merge
/pack restore <zip-path> --strategy merge
manifest.json is validated automatically (schemaVersion + fingerprint validity + every source file's SHA-256) before anything is applied; each target is backed up first and swapped in atomically, and a failure aborts the batch and rolls it back. Restart DSH if needed afterwards.
Packable modules
| Module | Contents | Migrate preset | Share preset |
|---|---|---|---|
skills |
Skills (including the memory-mechanism skill), under ~/.dsh/skills |
✅ | ✅ |
sessions |
Session records (.zstd format), under ~/.dsh/sessions |
✅ | ❌ |
attachments |
Session attachment object store: the bytes of images/files inside sessions (content-addressed, under ~/.dsh/attachments/v1/objects; excludes the rebuildable request-image cache and the tmp staging area) |
✅ | ❌ |
profiles |
Profile configs (excluding node_modules), under ~/.dsh/profiles |
✅ | ❌ |
plugins |
Installed plugin bodies (top-level package directories in profiles/<profile>/node_modules); large |
✅ | ❌ |
settings |
Global settings (settings.yaml) |
✅ | ❌ |
presets |
Agent presets (.agent-presets) |
✅ | ❌ |
memory |
Memory mirror (human-readable Markdown under DSH_MEMORY_ROOT or ~/.dsh/memory, excluding backups/) |
✅ | ❌ |
memoryDb |
Memory truth source (<DSH_BIOMEMORY_DIR || ~/.dsh/biomemory>/biomemory.db): packed as a consistent VACUUM INTO snapshot (read-only connection, never touches a live WAL database); on failure the file is left out and reported under unreadable |
✅ | ❌ |
Session images live in attachments: session logs only store an attachmentId reference (sha256:<64hex>); the bytes live under ~/.dsh/attachments/v1/objects. Packing sessions alone would leave images broken after a restore (the history opens, but the image bytes are missing), so the "Session records" group in the settings page packs sessions + attachments together. Objects are content-addressed, so the target machine needs no reference rewriting.
The two built-in presets:
- Migrate — every module checked by default: ideal for relocating a whole environment.
- Share — only
skillsis checked; sessions, attachments and memory data are excluded automatically, and personal skill subdirectories (_shared) are also excluded, keeping sensitive content out of the pack as much as possible.
Privacy & security
Scan rules (scope: files with known text extensions plus extensionless files that are clearly text, e.g. .env; binaries containing NUL bytes are skipped):
| Rule | Description |
|---|---|
| Local absolute paths (drive letter) | Drive-letter style paths such as D:\..., C:/... |
| Unix home paths | /home/<name>/..., /Users/<name>/..., /root/... — home directories only (the path says whose machine it is). /usr, /etc, /var, /opt, /srv, /mnt, /media and /tmp are shared system paths with no personal information: since v0.7.0 they are neither reported nor sanitized |
| UNC / network paths | \\server\share\... |
| User-directory paths | Paths under the OS user profile directory (C:\Users\<name>, /home/<name>, /Users/<name>); mount prefixes in front of the home directory are consumed too (/System/Volumes/Data/Users/<name>/…, /Volumes/<volume>/Users/<name>/…, `/mnt |
| Mount / derived paths | /Volumes/<volume>, /System/Volumes/Data, /private, /nix/store — these are machine-specific (volume names are user-chosen, /private carries a per-machine hash, /nix/store exposes the local package layout), unlike /usr or /etc, which are identical on every machine. Replaced with <本机路径>, sub-path kept |
| Suspected credentials / tokens | Assignments such as api_key, access_key, secret, password, token, bearer, authorization, credential — quoted or unquoted |
| Bare key shapes | The keys themselves: sk-..., ghp_..., github_pat_..., glpat-..., AKIA..., xox?-..., JWTs (eyJ.....*.*) |
| Personal nicknames | User nickname text (injected by the deployer via config.personalPatterns) |
Counts are tallied per file + rule + line, so multiple hits on one line are all counted (no more "one hit per rule per file").
Scan cache: "Preview" and "Pack" both hit the same /create endpoint; the backend caches scan results by file name + content SHA-256, so the same content is never read and matched twice. Change the content and the fingerprint changes, forcing a re-scan — this is a performance optimization, not a correctness trade-off (calls without a SHA-256 are never cached, and editing the deployer's personal patterns invalidates the whole cache).
Blocking policy:
- Share mode — a hit is never blocked, but sanitizing is forced: the
sanitizeflag sent by the UI is ignored and every pack is sanitized (paths become placeholders, credentials become<已移除的密钥>; only the copy inside the pack changes, source files are untouched). Share packs are meant for others, so content is rewritten rather than passed through. Two things do fail a share pack: selecting a module the share preset forbids (onlyskillsis allowed; anything else →share_scope), and files whose content cannot be read and therefore never went through the scan (fail-closed, the file list is reported). - Migrate mode — hits are reported only; inspect them in advance with
/pack scanor--dry-runand decide yourself, or opt into "sanitize before packing".
Never packed: .credentials.yaml and .anonymous-user-id are skipped in every module's file walk, regardless of mode — local identity/credential files never enter a pack.
Other security measures:
- Every file's SHA-256 fingerprint is recorded in
manifest.jsonfor fail-closed integrity checks on restore; entries without a well-formed fingerprint are rejected, and files whose hash cannot be computed are skipped at pack time and reported (a blank fingerprint is never written). - Restore containment is two-sided: the source must stay inside the extraction directory and the target must stay inside its module root (zip-slip / tampered manifests with
../escapes are rejected). Absolute paths, drive-letter paths, UNC paths and..segments are rejected by a whitelist before restore even starts. - Before unpacking, archive members are enumerated from the ZIP central directory by the plugin's own reader (no external commands) and whitelisted (absolute / drive-letter / UNC /
..names rejected), and symlink / hardlink / device members are refused; the extracted tree is scanned again for symlinks. Temporary extraction directories are always cleaned up intry/finally— on success and on failure. - Targets are backed up first (
<packs>/.restore-backups/<timestamp>/), written to a temp file and swapped in with an atomicrename; any failure aborts and rolls back what this run already replaced or added.
How memory migrates (read this)
Since v0.2.6 the memory truth source is in the pack: the memoryDb module stores a consistent VACUUM INTO snapshot of
<DSH_BIOMEMORY_DIR || ~/.dsh/biomemory>/biomemory.db (entries plus audit records) instead of copying a live, DSH-held WAL database —
a plain copy of a live WAL database would not be safe, hence the snapshot. If the snapshot cannot be taken the file is skipped and
listed under unreadable rather than silently dropped.
| Module | Contents | Enough for migration? |
|---|---|---|
memoryDb |
VACUUM INTO consistent snapshot of biomemory.db (entries + audit) |
✅ the memory itself |
memory |
Read-only Markdown mirror under DSH_MEMORY_ROOT, meant for humans |
⚠️ readable copy only |
Restore still skips *.db* by default (includeDb: true writes them) so a memory database in use on the target machine is never
overwritten — stop DSH before restoring if you want memoryDb written back.
- SQLite is handled on both sides: packing takes a consistent
VACUUM INTOsnapshot (memoryDb, since v0.2.6) instead of copying a live, DSH-held WAL database (failure means the file is skipped and listed underunreadable); restore still skips*.db*by default (includeDb: truewrites them) so a memory database in use on the target machine is never overwritten. - Pack file names carry a unique suffix (
dsh-packer-<timestamp>-<random>-<mode>.zip) so packs created within the same second never overwrite each other. - Packs are built by the plugin own pure-JS ZIP writer (zip.mjs, node:zlib) — standard zips with zero native npm dependencies and no external commands.
- All file and subprocess work is asynchronous (
node:fs/promises+execFile), with bounded concurrency (16 by default) for hashing and copying, so packing large trees never blocks DSH's event loop.
Settings Web API (/packer/api/*): auth & limits
The Settings tab is a peer of the /pack command and talks to the plugin's own prefix route, /packer/api/*. That route is fail-closed by default, and the checks run in the order "rate limit → auth → body size → routing", with all three gates completed before the request body is buffered:
| Item | Default | Behaviour |
|---|---|---|
| Auth | api.authMode: 'auto' |
Official mechanism only: requestRejection(req) on ctx.get('connection') (dsh-client-connection) — the same Host/Origin trust plus signed browser session check the official /api channel uses. 401 → 401, 403 → 403, only undefined lets the request through |
| Auth service unavailable | Hard 403 | If the connection service is missing, has no requestRejection, or throws, the request is rejected with 403 and a reason; the plugin never silently falls back to a weaker path (the built-in token channel requires opting in with authMode: 'token') |
| Rate limit | 60 / 60 s | Outermost: a sliding window keyed by client address (remoteAddress), evaluated before auth and before reading the body, so unauthenticated traffic consumes the same quota. Over the limit: 429 with retry-after; the key table is bounded (no timers, bounded memory) |
| Body size limit | 8 MB | Decided before the body is fully buffered: a declared Content-Length over the limit is rejected with 413 without reading a single byte; without a length the body is counted while streaming and the connection is unbound, paused and answered with 413 the moment it exceeds the limit |
| Error messages | Redacted | Server-side absolute paths (drive letter / UNC / Unix home dirs) are replaced with <路径已隐去> before leaving the process, so directory layout is never echoed to the browser |
api options (apply(ctx, { api: { ... } })):
| Option | Default | Notes |
|---|---|---|
authMode |
'auto' |
'auto': official connection only, 403 when unavailable; 'token': explicit opt-in plugin-owned fallback — Host / Origin / Sec-Fetch-Site same-origin checks plus a one-time token (generated at apply time, injected into the same-origin index.html via webServer.tapIndex; the UI sends it as the x-dsh-packer-token header), for hosts that genuinely have no connection service; 'off': insecure, isolated tests only |
maxBodyBytes |
8388608 |
Body size limit in bytes |
rateLimit / rateWindowMs |
60 / 60000 |
Rate-limit count and window (ms) |
token |
random | Only used with authMode: 'token'; pass one explicitly to pin it for multi-instance or test setups |
authMode: 'off' and an explicitly pinned token hand the security decision to the deployer; they are documented so you know what you opted into, not because they are recommended.
Restore & diff
Restore flow:
- Import the zip — pick the file in the Settings tab, or
/pack restore <zip-path>. - Archive validation — members are listed and whitelisted first (absolute / drive-letter / UNC /
../ link-type members rejected), thenmanifest.jsonis read. - Manifest validation —
manifest.jsonexists, its schemaVersion is compatible, every entry has a well-formed SHA-256 fingerprint, and every source file's hash matches; any mismatch is fail-closed. - Diff report — added / changed / same / skipped counts and file lists.
- Pick a conflict strategy:
overwrite— replace the target file with the pack's content (default);skip— keep the target file and skip conflicting entries;merge— for text files, append the pack's content to the end of the target behind a separator comment; existing content is never overwritten. Non-text files fall back to overwrite.
- Backup → atomic swap → rollback on failure — every target that will be replaced or appended to is backed up to
<packs>/.restore-backups/<timestamp>/first, and the pack's content is written to a temp file and swapped in with an atomicrename; any failure aborts the batch and rolls back everything this run replaced or added (counts return to zero and the rolled-back number is reported separately). - Apply, and restart DSH if needed.
Files already identical to the pack are skipped automatically under every strategy. Merge is not supported for structured configs (JSON/YAML) — appending corrupts them; use overwrite or merge manually. Live SQLite databases (*.db*) are skipped by default to avoid overwriting memory data.
/pack command reference
/pack list # list packs (encrypted ones are marked)
/pack create [--modules skills,memory] [--mode migrate|share] [--note note] [--dry-run] [--encrypt passphrase]
/pack create --share # --share is shorthand for --mode share
/pack restore <zip-path> [--strategy overwrite|skip|merge] [--modules a,b] [--include-db] [--password passphrase]
/pack scan # privacy-scan every packable module
| Command | Arguments | Description |
|---|---|---|
list |
— | Lists generated packs: creation time, size, included modules, note, encryption flag |
create |
--modules a,b pick modules by name; --mode migrate|share (default migrate); --share shorthand; --note note; --dry-run preview only; --encrypt passphrase |
Without --modules, modules follow the mode preset (migrate = all; share = Skills only) |
restore |
<zip-path>; --strategy overwrite|skip|merge; --modules a,b restore only those modules; --include-db also restore the memory database (stop DSH first); --password passphrase |
Import → validate → diff → apply with the chosen strategy; encrypted packs require the passphrase |
scan |
— | Runs the privacy scan over every packable module and reports sensitive traces |
Pack verification no longer has its own command: the settings page's "Browse" recomputes SHA-256 while listing files and flags mismatches in red.
Output directory: packs are written to ~/.dsh/packs/ (override with DSH_PACKS_DIR); names look like dsh-packer-<timestamp>-<random>-<mode>.zip (the unique suffix keeps same-second packs from overwriting each other), and each pack also gets a same-name .json summary file in the same directory (time / modules / note / file count) for the list view and quick identification. Restore backups go to .restore-backups/<timestamp>-<random>/ in that same directory (safe to delete at any time; neither the list nor the plugin reads it). Pack list, delete and rename are also available in the Settings → "Config Packer" tab.
Configuration & environment variables
| Variable | Default | Description |
|---|---|---|
DSH_PACKS_DIR |
~/.dsh/packs |
Pack output directory |
DSH_MEMORY_ROOT |
~/.dsh/memory |
Location of the memory module data |
DSH_HOME |
~/.dsh |
DSH data root (base for all module paths) |
The Settings page is one page in five blocks, following the packer's task chain: existing packs → contents to pack → pack options → restore (with per-item diff) → result details. Pack info, integrity verification, rename and delete live on the pack row (delete sends the pack to the platform's own trash — on HarmonyOS it is permanent and announced first); restore shows the diff first, then module selection and strategy, and the result block reports the backup directory with a one-click rollback. Chinese-only copy, no icon characters, statuses in words.
Compatibility
- Node.js ≥ 22.19.0
- Cross-platform: Windows / macOS / Linux / HarmonyOS share one code base and one pack format. Trash is dispatched per platform (Windows Recycle Bin / macOS Trash / Linux freedesktop Trash); HarmonyOS still has two limits (trash and system file picker) — see docs/platform-support.md.
- DSH version gate:
engines.dsh=>=0.2.0-rc.2 <0.3.0-0(tested on the 0.2.1-rc.3 desktop app). DSH evaluates compatibility withincludePrerelease, so prerelease identifiers participate in range checks; DSH versions outside this range are refused by the plugin manager before install. - Peer dependencies:
@deepseek-ai/cordis^4.0.2 (plugin lifecycle baseline, provided by the host);@deepseek-ai/dsh-tools/@deepseek-ai/dsh-sessionshare the>=0.2.0-rc.2 <0.3.0-0range and are not imported byindex.mjs— the plugin only uses host services such asctx.commands/ctx.webServer/ctx.slots, so both are markedoptional: trueinpeerDependenciesMeta. - No external commands for packing: packs are read and written directly by zip.mjs (node:zlib) — creating, listing and extracting never call tar, bsdtar or unzip, and any standard unzip tool can open the produced packs. (Only the trash feature shells out to OS commands: PowerShell on Windows, osascript on macOS; Linux uses pure Node.)
Version history
| Version | Date | Type | Highlights |
|---|---|---|---|
| v0.7.2 | 2026-10-28 | Fixes the false "pack failed (network · Failed to fetch)" | On a full migration (17469 files) the preview response was ~3.9 MB (3.5 MB of that the manifest) and the terminal poll still carried 2000+ privacy findings. When the host bridge dropped either large response the UI reported network · Failed to fetch even though the server had already written the pack (measured: a 218 MB zip with 17470 valid entries). Three fixes: (1) preview returns the first 1000 file entries plus filesTotal (the panel only renders the first 200 anyway); (2) the poll view replaces result.privacy with privacyCount (the create flow only reads pack / warnings); (3) the client now retries polls on network errors / 5xx with backoff (up to 10 tries, ≤5 s apart) instead of treating one hiccup as a failed pack. |
| v0.7.1 | 2026-10-28 | Tests shipped with the package | files now includes tests, plus a new tests/privacy.test.mjs (zero dependencies, node --test) that pins the behaviour this release promises: shebang lines survive untouched, Unix system paths (/usr, /etc, ...) are neither reported nor rewritten, home directories and Windows user directories are sanitized, URLs are left whole, and SCHEMA_VERSION stays at 1 so older packs still restore. Previously the manifest declared npm test while the tarball carried no tests, so it failed right after install. |
| v0.7.0 | 2026-10-27 | Engineering refactor + privacy-scope correction + accessible UI | (1) Backend layering: index.mjs drops from 3204 to 306 lines (assembly and re-exports only), split into lib/{constants,errors,logger,util,modules,privacy,recycle,archive,pack,restore,jobs,api,cli}.mjs, with privacy rules collapsed into a single rule source shared by scanning and sanitizing (they no longer keep two divergent regex sets). (2) Privacy scope corrected: the Unix path rule now matches home directories only (/home, /Users, /root); /usr, /etc, /var, /opt, /srv, /mnt, /media and /tmp are shared system paths with no personal information, so they are no longer reported or rewritten — previously a #!/usr/bin/env node shebang was turned into #!<Unix路径> node and the script was destroyed. Shebang lines and URLs are now protected ranges. (3) Frontend engineering: lib/client.js stops being "a build artifact maintained as source" and becomes TypeScript source plus a reproducible esbuild build (src/client/: copy / styles / types / format / api / hooks / model / ui / view/*); npm run typecheck is clean. (4) Accessibility: dialogs gain role=dialog + aria-modal + aria-labelledby + Escape-to-close + a Tab focus trap + focus restore on close; the progress bar is a role=progressbar that omits aria-valuenow in the indeterminate state; table headers carry scope=col; async results are announced via aria-live. (5) Fixes: the ZIP classic-format 65535-entry limit now raises a readable error instead of writing a broken archive; SQLite snapshot directories no longer leak after /scan; the missing passwordEmpty copy string is added. Tests 91 → 102 (new artifact smoke tests and real jsdom render tests). (6) Sanitize gaps closed + scan cache: a new "mount / derived path" rule now rewrites /Volumes/<volume>, /System/Volumes/Data, /private and /nix/store — previously these were missed entirely, or only replaced up to the home directory, leaving half-sanitized output such as /System/Volumes/Data<用户目录>/…; the user-directory rule now consumes the mount prefix as well. Privacy scan results are cached by file name + content SHA-256, so "Preview" followed by "Pack" no longer scans the same files twice (any content change invalidates the entry at once). The UI spec is brought in line with the v0.7.0 structure (docs/ui-spec.md, replacing ui-v060-spec.md). Tests 103 → 106. |
| v0.6.5 | 2026-10-25 | Session images/attachments now travel with migrations | (1) New attachments module (root ~/.dsh/attachments, skipDirs: ['tmp'], on by default for migration, never for sharing): session logs only hold an {"type":"image","attachment":{"attachmentId":"sha256:…"}} reference, while the bytes live under attachments/v1/objects/<first2>/<sha256> (content-addressed, deduplicated, append-only) — a pack with sessions alone leaves every session image broken after a restore. (2) SENSITIVE_MODULES gains attachments, so share mode is blocked server-side (not merely greyed out in the UI). (3) The "Session records" group in the settings page becomes ["sessions","attachments"] (without it the backend module exists but the UI cannot select it). (4) MEMORY_ROOT now follows DSH_HOME (it used os.homedir(), diverging from memoryDb). (5) The module table gains attachments (plus the missing plugins row) and documents that session images live in attachments and the "Session records" group packs them; objects are content-addressed, so no reference rewriting is needed on restore. Tests 87 → 91. |
| v0.6.4 | 2026-10-25 | Per-platform trash + manual path fallback | (1) Trash is dispatched per platform (recycleFileNative contract unchanged: true on success, false when the platform has none, throw on failure → deletePack degrades and reports a warning): Windows keeps PowerShell SendToRecycleBin; macOS uses osascript -e 'tell application "Finder" to delete POSIX file "<path>"' (quote/backslash escaping, 30s timeout); Linux implements the freedesktop Trash spec in pure Node — the file moves to $XDG_DATA_HOME/Trash/files/<name> (defaults to ~/.local/share, .1 suffix on collisions) and Trash/info/<name>.trashinfo records Path= (URL-encoded absolute path) plus DeletionDate= (local time YYYY-MM-DDThh:mm:ss); HarmonyOS and everything else still return false (permanent delete + warning). (2) Manual path: the restore page keeps its "or type a pack path" entry; /status gains three platform capability fields (platform / trash / filePicker), so platforms without a system file picker (HarmonyOS) show "当前平台请直接填写包路径" and treat manual input as the primary entry, while the delete confirmation tells the truth (not recoverable). The web layer cannot obtain host paths, so no platform renders a dead picker button. (3) Path examples are platform-specific (/storage/…, /home/…, /Users/…). Tests 80 → 87. |
| v0.6.3 | 2026-10-25 | Docs / pack scope | Cross-platform docs now ship inside the package: files gains docs, so an installed plugin carries docs/platform-support.md (support matrix for Windows / macOS / Linux / HarmonyOS, plus the two limits: Recycle Bin and system file picker). No code or behavior change. |
| v0.6.2 | 2026-10-25 | Intro / copy | Rewrote the intro line; tested desktop version is now 0.2.1-rc.3. No functional change |
| v0.6.1 | 2026-10-25 | Plugin card copy rewritten | Localized title ("Config Packer") and one-line summary via locale/en.json / locale/zh.json meta; description no longer embeds the version or lists features. No functional changes. |
| v0.6.0 | 2026-10-25 | Settings page rebuilt around the packer's job | The UI is no longer a form of module cards and buttons; it is one page following the task chain, in five blocks: existing packs (name / time / contents / size / encryption, with restore · pack info · delete — delete goes to the Windows Recycle Bin) → contents to pack (plain-language list: skills / installed plugins / memory database / sessions / config & presets, with sizes and a total) → pack options (as-is / sanitize before packing / encrypt, with passphrase, scan verdict, progress and cancel) → restore (pick a pack or a path → per-item diff: in pack vs on this machine vs how to handle → module selection + memory-db toggle → strategy) → result details (overwritten / added / skipped / needs confirmation / failed + backup directory + one-click rollback). Naming now follows the plugin itself (no more box/seal/unseal metaphors); Chinese-only copy, no emoji or icon characters, statuses expressed in words. lib/client.js 1048 → 1421 lines. Backend adds two fields: modules and sha256 per pack in GET /packs, and pack.sha256 in the create result. Tests 80/80. |
| v0.5.1 | 2026-10-25 | Privacy actually handled + plugins travel with migrations | (1) Privacy goes from "report only" to "must handle": the pack page offers Sanitize before packing (rewrites only the copy inside the pack — paths become <本机路径> / <用户目录> / <UNC路径> / <Unix路径>, credentials become <已移除的密钥>; source files are never touched; forced in share mode) and Pack as-is (faithful; warns when credentials are included without a passphrase). (2) Five safety gates for sanitizing: single left-to-right pass, structural characters excluded, any hit containing { } [ ] , ; " ' is skipped with a warning, JSON/JSONL is re-parsed with line-shape checks (whole file reverted on failure), non-UTF-8 files are never sanitized, and size/sha256 are recomputed after rewriting (otherwise restore would reject the pack as tampered). (3) New plugins module packs the installed plugin bodies (profiles/*/node_modules top-level packages, scoped @scope/* handled, pnpm links followed, cycles guarded) — on by default for migration, never for sharing. (4) Restore back-fills sanitized packs (<用户目录> → target home, <用户名> → current user) and reports retained placeholders in stats.warnings. (5) /scan findings carry a kind (7 values). Tests 80/80. |
| v0.5.0 | 2026-10-25 | Slimming (small is beautiful) | Deletions only: (1) the built-in trash bin is gone (3 endpoints + a panel) — deleted packs now go to the Windows Recycle Bin; (2) scheduled backups, the retention policy and their config file are gone (no scheduler); (3) the standalone verify endpoint and command are gone — verification is folded into "Browse", which recomputes SHA-256 while listing files and flags mismatches; (4) /status drops trashCount / retention / autoBackup / disk.trashBytes; (5) the UI goes from three tabs to two (Pack / Restore) with pack management moved onto the Restore pack row, the whole English copy table is removed (Chinese-only personal tool), and lib/client.js shrinks from 2334 to ≤1100 lines; (6) the result card gains a "Copy path" action. Endpoints: 15 → 9 groups. Kept: progress & cancel, encrypted packs (one toggle + passphrase), selective restore, includeDb, restore backups with one-click rollback, privacy scan and share guard. |
| v0.4.0 | 2026-10-25 | Feature completion + UI rebuild + three broken security promises fixed | (1) False-negative scan fixed: the settings page's "Scan" used to downgrade the file list to {module,rel,size} and drop absolute paths, so privacyScan could not read any file and always reported "no sensitive traces"; abs is passed again. (2) Share protection now enforced server-side: share mode rejects sessions/profiles/settings/presets/memory/memoryDb with 400 share_scope instead of trusting the UI. (3) Memory database can be restored: includeDb is now reachable from a UI toggle and --include-db (previously API-only). (4) /status no longer runs VACUUM INTO on the memory DB (the old code leaked a ~2.9 MB snapshot per settings-page open). (5) strategy/mode are whitelisted fail-closed (invalid values used to degrade silently to overwrite). (6) New: background jobs with progress/cancel, selective restore, per-file diff, pack-content browsing, /pack verify, trash bin, AES-256-GCM encrypted packs, scheduled backups + retention, one-click rollback of a restore. (7) UI rebuilt from one long page into three tabs (Pack / Library / Restore) with custom input and confirm dialogs (window.prompt/confirm removed — Electron does not support prompt, so Rename never worked in the desktop app). 74/74 tests pass. |
| v0.3.0 | 2026-09-29 | Bug fix | Missing module in the UI fixed: the settings page hard-coded a 6-entry module list, so the memoryDb module added in v0.2.6 (the SQLite truth-source snapshot) could not be selected at all. The list is now driven entirely by the host's /status payload (moduleOrder(status.modules)), and default selection iterates every module the host reports. Two consistency guards were added to the test suite (the client must not hard-code a module list; every host module must be renderable by the client). 50/50 tests pass. |
| v0.2.6 | 2026-09-29 | Feature / adaptation | The memory truth source is now inside the pack (consistent snapshot) + DSH 0.2.0-rc.2 adaptation, superseding the v0.2.5 limitation: (1) new memoryDb module — the runtime source of truth (`<DSH_BIOMEMORY_DIR |
| v0.2.5 | 2026-09-20 | Semantics | The "memory" in a pack is only the mirror: the memory module packs the read-only Markdown copy under ~/.dsh/memory, while the runtime source of truth is SQLite (~/.dsh/biomemory/biomemory.db) — excluded by skipFiles and never scanned (a live WAL database cannot be copied safely). The module label now reads "memory mirror (…; the live SQLite database is not in the pack)" and both READMEs gained a How memory migrates section: stop DSH first, then copy biomemory.db (plus -wal/-shm) by hand — the pack alone gives the target only the mirror. Also: client bundle moved to lib/client.js. 47 tests green |
| v0.2.4 | 2026-09-17 | Async / security | File and subprocess work is async end to end (node:fs/promises + execFile, streaming hashes, bounded concurrency of 16 for hashing and copying, sha256() now throws on failure); the public API returns Promises and no longer blocks the event loop; new /packer/api/* protections: fail-closed auth (official connection.requestRejection only — a missing service, missing method or a throwing call is a hard 403), an explicitly opted-in one-time-token fallback (authMode: 'token', same-origin checks + webServer.tapIndex injection), a rate limit (60/min, outermost gate) and a body size limit (8 MB, decided before the body is buffered), plus path redaction in error messages; apply() now explicitly wires the handler into webServer.register({ kind: 'prefix', path: '/packer/api' }); tests grown to 47 cases (unauthorized 403 / oversized 413 / rate-limited 429 / token path / no-connection default deny / apply wiring) |
| v0.2.3 | 2026-09-16 | Security hardening | Target-path containment plus a rel whitelist on restore (absolute / drive-letter / UNC / .. rejected); integrity made fail-closed (missing or malformed fingerprints rejected, sha256() now throws instead of returning '', streaming hash); archive members whitelisted via tar -tf before unpacking, link-type members refused, temp dirs always cleaned in try/finally; restore now backs up → temp file → atomic rename → abort-and-rollback on failure; memory module excludes *.db* by default; privacy scan gained Unix/UNC paths, unquoted credentials and bare key shapes, .env-style extensionless text, with per-line full counting; unique pack-name suffix; peerDependenciesMeta marks host-provided peers optional; the web UI now surfaces server-side error reasons |
| v0.2.2 | 2026-09-05 | Adaptation / UI | Adapted for DSH 0.1.2-rc.1; management panel redesigned on the "skeleton / flesh / breath" design language — packing-workflow layout (stage progress bar / equipment panel / diff color bands) + orange-amber-teal brand palette (packing & migration) + dark-mode support (follows DSH theme, dual-channel detection) |
| v0.2.1 | 2026-09-05 | UI refactor | Config Packer panel UI rebuilt — neutralSurface background + white cards (max-width 860 centered, radius 16), 4/8px grid spacing, restrained 150ms transitions; colors strictly from dsh-fuse default tokens (--pk-* variables, zero hardcoded values); diff report got four-column count badges + semantic color dots (added=green / changed=orange / same+skipped=muted); privacy risks default to a warning tint |
| v0.2.0 | 2026-09-05 | Security hardening | Privacy scan fixed: merged personal rules now actually participate in the loop; every source file is verified against its manifest SHA-256 before restoring (fail-closed); restore path containment (zip-slip / tampered-manifest escapes rejected); append-merge refused for structured configs (JSON/YAML — it corrupts them) |
| v0.1.2 | — | Metadata / deps | package.json gained keywords / files metadata; peerDeps relaxed to ≥0.1.1-rc.2; README version & dependency notes synced |
| v0.1.0 | — | Initial release | Modular packing (skills / sessions / profiles / settings / presets / memory), privacy scan, restore diff, pack management, Settings panel |
FAQ
The zip won't open / looks corrupted?
Packs are standard zips created by the plugin pure-JS ZIP writer — Windows Explorer and common unzip tools can open them. If a pack fails validation, don't hand-edit its contents (that breaks the SHA-256 fingerprints in manifest.json); regenerate it with /pack create. Check ~/.dsh/packs (or your DSH_PACKS_DIR) with /pack list to see what's there.
Manifest validation fails on restore?
Usually one of: the zip was not created by dsh-packer (no manifest.json inside), manifest.json is missing or its schemaVersion is incompatible with the current version, or the pack was modified after creation. Redistribute the original pack or regenerate it.
My share pack got blocked — what now?
Share mode does not abort on privacy hits — hits are always sanitized before packing. Only two things fail a share pack: selecting a module the share preset forbids (only skills is allowed; anything else returns share_scope), and files whose content cannot be read and therefore never went through the scan (fail-closed — the response lists them). For the first, drop the module that should not be shared; for the second, fix those files (permissions, lock, corruption) and retry. Use migrate mode when you need a faithful backup (report-only, no forced sanitizing).
How does the merge strategy work?
For text files, the pack's content is appended to the target file behind a separator comment — existing content is never overwritten. Non-text files fall back to overwrite; structured configs (JSON/YAML) don't support merging at all (appending corrupts them) — use overwrite or merge manually. Files already identical to the pack are skipped under every strategy.
What does "aborted / rolled back" on restore mean?
Restore is all-or-nothing: if any step (path validation, fingerprint check, writing) fails, it stops immediately and rolls back the files this run already replaced or added, using the backups taken moments earlier. rolled back N reports how many files were restored; failure reasons are listed entry by entry in the Settings tab. Backups stay under <packs>/.restore-backups/ for manual inspection.
Why wasn't my memory database (*.db*) restored?
Live SQLite databases are held open by DSH / the memory plugin, so overwriting them can corrupt data. The plugin neither packs nor restores *.db, *.db-wal, *.db-shm or *.sqlite by default; on restore they are counted as skipped with an explicit reason.
To actually move memory across: quit DSH first, then enable the includeDb toggle on the restore tab (or /pack restore <pack> --include-db). The packed database comes from a consistent VACUUM INTO snapshot (the memoryDb module), so it is complete and usable.
Restore says "entry is missing a valid SHA-256 fingerprint"?
The pack was not generated by dsh-packer, or its manifest.json was hand-edited. Missing or malformed fingerprints are rejected outright (fail-closed) — redistribute the original pack or regenerate it.
Where are packs stored?
~/.dsh/packs by default, overridable via the DSH_PACKS_DIR environment variable; each pack also has a same-name .json summary file in the same directory for identification and the list view.
Development
npm test # node --test "tests/*.test.mjs", 37 tests, all green
node scripts/release-check.mjs # release consistency check (version / README version exposure / files whitelist / git state)
CI: .github/workflows/ci.yml runs "release check → unit tests → npm pack --dry-run" on Node 22.x / 24.x (this package has zero runtime dependencies, so there is no install step) for every push and pull request.
License
MIT — see LICENSE.
Links
More in this category
Tencent/WeKnora#dsh-weknora★ 37401
Four read-only tools over a WeKnora knowledge base: list knowledge bases, hybrid passage search, reassemble one document's chunks in order, and WeKnora's own cited RAG or ReAct-agent answer with a resumable session id.
DayuanJiang/next-ai-draw-io#dsh-plugin★ 36932
Draws draw.io diagrams in DeepSeek Harness through the next-ai-drawio MCP server: the link in the reply opens a live preview in the right sidebar, you can edit the diagram by hand, and each AI change is a version you can restore. Node.js 18+; npx downloads the MCP server on first use.
superdesigndev/treg★ 7929
Tool catalog for agents: search ~2,600 external endpoints (SEO and SERP, backlinks, social, people and company enrichment, ad libraries, scraping) by the task you want done, read each one's parameters and per-call price, then call it with the credential injected server-side. Ships the skill plus an MCP row that stays disabled until TREG_TOKEN is set.
T-Auto/dsh-ops★ 5798
Provides bash, PowerShell 7, and high-performance Rust tools for DSH on Windows to reduce token usage.
sixtysevenlf/dsh-blender-plugin★ 2081
Lets a DSH agent drive a local Blender over a direct TCP channel with 15 tools: viewport frames, custom-angle renders, Python execution, headless and job offload, transaction rollback, and a planning layer of 228 ops across 34 families.
yjl9903/AnimeGarden#dsh★ 1400
Anime Garden for DSH: search anime torrent resources from 動漫花園, 蜜柑计划, 萌番组 and ANi inside DSH — via chat, a sidebar panel with fansub and type filters, a seasonal calendar, and an in-app floating player.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.