DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Gist Settings — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
G

@local/dsh-gist-settings

Gist Settings

使用本地 gh CLI 通过 GitHub Gists 备份和恢复 DeepSeek Harness 配置文件。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:soimy/dsh-gist-settings#c61afc243b1530d600a3c742ac4668a2bf2c1868
README兼容性版本

兼容性与来源证明

Gist Settings 以 @local/dsh-gist-settings 发布,当前版本为 0.2.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
any
发布来源
github
Registry 更新时间
2026/9/28

版本

0.2.0stable
2026/9/28
0.1.0stable
2026/9/27

相关插件

正在加载相关插件…

最新版
0.2.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
any
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/9/30
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 developer-tools 分类下经过校验的插件。

Web App@deepseek-ai/dsh-web-appdsh 浏览器界面捆绑包:位于 dsh-base 之上的 Web 补丁层,加上运行时粘合插件(提供前端 dist、Web 界面提示符、bash 运行时变量和 URL 行)Sdk Minimal@deepseek-ai/dsh-sdk-minimal独立的最小 SDK 配置包:JSON-RPC、一个 DeepSeek 适配器、持久化 Shell 和 JSONL 会话Sdk App@deepseek-ai/dsh-sdk-appdsh SDK 配置包:基于 dsh-base 提供 stdio JSON-RPC 服务和进程生命周期管理Experimental Inspector@deepseek-ai/dsh-experimental-inspector用于 Host 调试和 Client Runtime 检查的实验性跨 realm CDP hub

README

dsh-gist-settings

English · 中文

A DeepSeek Harness Cordis plugin that backs up and restores your Harness profile configuration to GitHub Gists, using the gh CLI you already have installed and authenticated.

One secret gist per profile, holding that profile's tracked config files. Upload, download, and a guarded two-way sync are exposed as agent tools, so you can drive them from a conversation.

Status

PartState
Sync engine (lib/core.js)Done
Host plugin + agent toolsInstalled and live; callable from a session
Real GitHub round-tripVerified against a real account
Test suite180 offline cases across seven suites, plus 12 live cases, all passing
CIOffline suites and repository checks on Linux, Windows and macOS, Node 20.3, 20.x, 22.x and 24.x
Client settings pageNot started — see The settings page
LicenceMIT

This code has been through two rounds of adversarial review (independent security, correctness, integration, mutation-testing and documentation audits, then a re-review of the fixes). Everything they proved is fixed and pinned by a regression test; the safety model below describes the resulting behaviour rather than the original intent.

Contributions are welcome — see CONTRIBUTING.md for the issue, pull-request and changelog conventions, and CHANGELOG.md for what has changed.

Requirements

  • Node.js 20.3+ — the engine uses node:-prefixed built-ins, AsyncLocalStorage to catch a nested lock, and AbortSignal.any to put one deadline on the global fetch that reads truncated gist content. That last one is why the floor is 20.3 rather than 20.0: on 20.0–20.2 the plugin loads and then fails the first time it has to read a file above the API's truncation threshold. .github/workflows/ci.yml runs the suites on 20.3.0 itself, so the floor is tested rather than claimed.

  • gh CLI, installed and authenticated.

    gh auth login
    

    Gists are read and written through gh api, so no extra OAuth scopes are needed beyond the default. The plugin never stores, reads, or asks for a token of its own — gh owns the credential.

    If gh is not on the PATH of the process running the Harness — common on Windows right after installing it — the plugin probes the usual install locations before giving up, and you can always name the binary explicitly with ghPath.

Install

Installing requires a session with full access or an approval prompt; plugin_manager refuses otherwise. An install can also ask for approval of package build scripts, which this bundle does not have.

plugin_manager  action: install_bundle  target: <absolute path to this directory>

That links this directory into the profile, adds the bundle to dsh.profile.bundles, and applies the patch in cordis.patch.yml. Afterwards a restart is needed before a new module generation loads.

Do not add the bundle row or the dependency to the profile by hand — install_bundle owns the selection. The row's config: block is yours to edit (see Configuration).

Uninstall

plugin_manager  action: remove_bundle  target: @local/dsh-gist-settings

The gists the plugin created are not touched. To remove those too, delete them on GitHub (the URLs are in state.json), then delete the state directory:

<stateDir>            # default: <dshHome>/gist-settings

That directory holds state.json (the profile → gist mapping), state.json.bak, every backup the plugin has taken, and a state.lock file for as long as an operation is writing. Deleting it loses the mapping and the local backups, but not the gists.

A download also creates a short-lived .dsh-gist-settings-staging-<pid>-<time> directory inside the profile it is writing to, so that the new content can be staged on the same volume before anything tracked is touched. A killed process can leave one behind; it is inert — never tracked, never uploaded, skipped when profiles are listed, and removed by the download's own finally on every normal path.

Tools

ToolDirectionWhat it does
gist_status—Reports whether gh is installed and authenticated, then lists every profile with its gist URL and sync state. Never touches profile files or the gist; it may repair a stale sync baseline in its own state file.
gist_uploadlocal → gistCreates a secret gist on a profile's first upload, then updates that same gist. Removes gist files that are no longer tracked — see the warning below. Never modifies local files. A tracked profile whose directory is gone is named in the result rather than silently skipped.
gist_downloadgist → localRestores local files from the gist. Backs up the existing local files first, and writes all-or-nothing.
gist_syncbothFast-forwards whichever side changed, restores tracked files that went missing locally, and does nothing when both sides match. Refuses to guess on a true divergence, and never propagates a deletion.

All four accept an optional profile; omitting it operates on every profile. A failure on one profile never aborts the others. Every argument is type-checked — a wrong type or an unknown key is an error, never a silent coercion, so force: "false" cannot become a forced overwrite.

The one exception to "every profile": gist_upload without a profile covers the profiles that still have a directory on this machine, because a deleted directory holds nothing to send. Such a profile is named in the result and pointed at gist_download rather than dropped from the report — silence there would read as "everything is backed up" when it is not.

Profile names are matched exactly. ALPHA is refused when the directory is alpha, because on Windows and macOS those name one directory and accepting both would back it up twice, into two gists that then diverge silently. The error names the real profile. An unknown name is refused with the list of the ones that exist, rather than failing later with a confusing "has none of the tracked files".

Status vocabulary

gist_status prints exactly these:

PrintedMeaning
not trackedNo gist exists for this profile yet.
in syncLocal and gist agree.
local changes to uploadOnly the local side changed since the last sync.
gist changes to downloadOnly the gist changed since the last sync.
DIVERGED - needs a decisionBoth sides changed. Nothing is guessed; choose a direction.
tracked files missing locally - run gist_download to restoreA tracked file is gone from disk but still in the gist.
gist deleted - the next upload recreates itGitHub returned a genuine 404 for the gist.
gist UNREACHABLE (not deleted) - retry when connectedThe gist could not be read for any other reason — offline, 401, 5xx, rate limit.
unknown - gh is unavailable, remote state not checkedgh is missing or logged out, so only the local tracking record is known.

gist_upload deletes gist files that are no longer tracked, and keeps no backup of them. "No longer tracked" means absent from the configured profileFiles — not merely missing from disk. A file added to the gist by hand (through the GitHub web UI, say) will be removed by the next upload. A tracked file that is only missing locally is a different case: the upload fails and tells you to download instead, because the gist may hold the only remaining copy. force: true overrides that and deletes it from the gist too — and the result says which files it deleted, so the two cases are never confused.

Configuration

Tunable values live in the profile patch layer, so they survive plugin upgrades. Edit the dsh-gist-settings row's config: block in your profile's cordis.patch.yml (the commented block in this repo's cordis.patch.yml is the template):

KeyDefaultMeaning
ghPathprobe PATH, then well-known install dirsPath to the gh binary. May also be an array of [command, ...prefixArgs] to reach gh through a wrapper, e.g. ['wsl', 'gh'].
profileFiles['cordis.patch.yml', 'package.json']Which files in each profile directory are tracked.
dshHome$DSH_HOME, else ~/.dshHarness home directory. A leading ~ is expanded and relative values are resolved against the process working directory.
profilesDir<dshHome>/profilesWhere profiles live.
stateDir<dshHome>/gist-settingsWhere the gist index and backups are written.

Each profileFiles entry is also the file's name in the gist and its path inside a backup, so it has to be a plain single name: no separator, no .., no absolute or drive-relative prefix, no colon, and no two entries that fold to the same name on this platform. A separator is refused because a gist is a flat collection of files rather than a tree — GitHub answers a filename containing a slash with HTTP 422 Validation Failed, and that filename is exactly what has to be sent. A name that could never be handled is rejected while the plugin loads, rather than on the first tool call.

The plugin declares no Config schema, so it validates the block itself at load time: an unknown key is rejected with the list of known keys, and a wrong type is rejected rather than silently falling back to a default. A typo therefore fails loudly instead of quietly changing which files are backed up.

The safety model

Gist layout. One secret gist per profile, described as DeepSeek Harness profile config: <name>, containing that profile's tracked files under their own names. The profile-to-gist mapping lives in <stateDir>/state.json.

A transient failure is never mistaken for a deletion. gh reports a deleted gist and an expired token the same way — exit 1 with a message on stderr. Only a genuine HTTP 404 is read as "the gist is gone"; a 401, a 5xx, a rate limit or a DNS failure yields unreachable, and both upload and sync refuse rather than mint a replacement gist that would abandon the original.

Divergence is never guessed at. The state file records the content hash of the last agreed revision as a baseline. Comparing the current local and gist hashes against it distinguishes "only local changed" (safe to upload) from "only the gist changed" (safe to download) from "both changed" (diverged, and the plugin stops). Only the tracked subset of the gist is compared, so an extra file in the gist cannot make a profile look permanently out of date.

A tracked file missing locally is restored, not deleted. It is reported as its own state, sync downloads it, and an upload refuses without force. This is the one asymmetry the tools never resolve on their own, because the gist may hold the only copy.

A deletion is never propagated, in either direction. Deleting a tracked file from the gist by hand does not delete it locally: the download keeps it, and gist_sync puts it back into the gist in the same call, so one sync reaches a stable state instead of leaving the profile reporting local changes to upload after a sync that claimed to have finished. Deleting it locally does not delete it from the gist either: the upload refuses. There is no flag that applies a remote deletion to the local copy — delete the local file yourself, then sync, if that is what you want. The one deliberate override is gist_upload with force: true, which drops a locally-missing file from the gist.

Backups before destructive writes, and an all-or-nothing write. gist_download copies the profile's tracked files to <stateDir>/backups/<profile>/<timestamp>/ before overwriting anything, including when forced. New content is then staged inside the profile directory and measured, and only once every byte has landed is it renamed into place: one atomic rename per file, which is either the old content or the new one and never a truncated mixture. If a rename still fails, the files already replaced are put back from the same bytes the backup holds, and the error says so and names the backup directory. A failed download therefore leaves the profile as it was, rather than at a revision that exists nowhere — with one deliberate exception, because the rollback undoes only what this transaction truly wrote. A file that changed since, or one the rollback cannot read back to compare, is left exactly as it is and named in the error: an edit made after the download is in no backup, so overwriting it on a guess is the one outcome that could destroy data nothing can recover. Two further honest qualifications: the rollback is best effort, so if it cannot restore a file it says which one and the backup is the recovery route; and if the state write after a successful commit fails, the files are in place and the error says that instead of claiming a lost download — the next gist_status repairs the stale baseline.

Containment — the profile, and then every tracked file inside it. Profile names are validated against the framework's own rules, and a resolved profile directory is re-checked against realpath(profilesDir), so a junction placed at profiles/<name> is refused rather than followed. Each tracked file is then held to the profile's boundary: a profileFiles entry must be a relative path with no separator, no .., no drive letter and no reserved character, and its resolved path is re-checked. A tracked file that is itself a link out of the tree, or that is a link into a sibling profile — which would let one profile's gist publish and then overwrite another profile's config — is refused. The check is repeated immediately before each write rather than once per operation, which narrows the window in which the profile directory could be swapped for a link mid-download to the rename call itself; closing it completely would need handle-relative operations that Node's fs does not expose. Note what that does and does not cover: a link that is live when it is checked is caught, and one that is dangling cannot be written through at all, but the check is still a check-then-use — the gap is narrowed, not closed.

One writer at a time, across processes. Read-modify-write cycles over state.json are serialised by an in-process queue and by a lock file (<stateDir>/state.lock) naming the pid that owns it. Two Harness hosts sharing one state directory therefore queue rather than lose each other's updates — the failure mode without it being a dropped profile record, which untracks a profile and orphans its gist, or two gists minted for one profile. A lock whose owner has exited is reclaimed at once; one written by another machine cannot be judged by this machine's pid rules, so it is judged by age instead and kept fresh by a heartbeat while it is held. Releasing a lock removes it only if it is still the one this process took. A nested call is refused with a clear error rather than deadlocking.

No interactive editor, ever. Gist reads and writes go through gh api. The obvious alternative, gh gist edit, opens $EDITOR and would hang a non-interactive plugin host.

A large file comes back whole. GitHub truncates a gist file's content in the API response above 1 MB and names the remainder at raw_url. That URL is validated to be a GitHub raw host over https, checked again after any redirect, and fetched with fetch — not gh api, which is documented as taking an API endpoint and would carry the caller's token to a host it was never configured for — under a 60 second timeout and a 64 MiB cap on the body. No credentials are sent, which is why it works: a secret gist is unlisted, not access-controlled. Node's fetch does not honour HTTP(S)_PROXY or the platform certificate store the way gh does, so a transport failure falls back to the request gh would have made; an HTTP error status does not, because a 404 has to stay a 404. Truncation is resolved only for files this profile tracks, so a large attachment someone added through the web UI neither costs a request on every status call nor can break one. test/regression.test.mjs pins the offline half of this, and test/live.test.mjs round-trips a 1.5 MB file against real GitHub to prove the rest.

No imports from the Harness installation. Tool definitions are written as plain objects matching the shape defineTool produces. This keeps the bundle immune to module-resolution changes; test/schema.test.mjs replays the Harness's own validators so the definitions cannot silently drift out of contract. The trade-off is that the Harness's version gate cannot see the plugin either, so peerDependencies pins the DSH version it was written against.

A secret gist URL is a bearer read capability. Anyone holding the URL can read a secret gist, and the tools return that URL in their output, which becomes part of the conversation transcript. Treat the URLs accordingly.

Repository layout

index.js               Host plugin: registers the four agent tools
lib/core.js            Sync engine — no Cordis dependency, directly testable
cordis.patch.yml       Bundle patch (inserts the plugin row; documents config)
client.js              Client settings page (not yet written)
locale/{en,zh}.json    Plugin display metadata for Plugin Manager cards
icon.svg               Bundle icon
test/                  180 offline cases across seven suites, plus 12 live ones
scripts/               check-changelog.mjs — validates CHANGELOG.md
.github/               Issue forms and the pull-request template

Documentation

DocumentWhat it covers
README.zh-CN.mdThis README in Chinese. The two are kept in sync; a behaviour change that updates only one is incomplete.
CONTRIBUTING.mdReporting, development setup, what each test suite proves, and the changelog, commit and release conventions.
CHANGELOG.mdEvery notable change, newest first, in Keep a Changelog form.
LICENSEMIT.

Development

npm test                # the seven offline suites (180 cases) plus the changelog and docs checks
npm run test:sync       # engine lifecycle against a fake gh
npm run test:tools      # tool layer, argument validation, failure isolation
npm run test:schema     # definitions vs. the installed Harness validators
npm run test:regression # the defects the adversarial reviews found
npm run test:safety     # containment, atomic writes, forced deletion, recovery, locking
npm run changelog:check # CHANGELOG.md structure and version consistency
npm run test:live       # opt-in: real GitHub

test/fake-gh.mjs is an in-memory stand-in for gh implementing --version, auth status, and the /gists API, with optional injection of truncated files, an untrusted raw_url host, HTTP 500s and a logged-out CLI. The sync, tools, regression and safety suites point ghPath at it, so the whole lifecycle — create, upload, download, divergence, backup, pruning, gist recreation, idempotency, recovery — runs offline with no GitHub account.

test/schema.test.mjs locates the installed @deepseek-ai/dsh-tools from process.execPath (searched first when you set DSH_TOOLS_DIR) and replays the runtime's own checks: the registration contract, the supported JSON Schema subset, argument validation, and that each tool's returned value satisfies its declared output schema. Finding no installation fails the suite rather than skipping, because a silent skip would leave npm test green with none of those checks having run; set DSH_ALLOW_SCHEMA_SKIP=1 to accept the skip deliberately.

test/live.test.mjs is opt-in because it creates a real secret gist. It works inside a throwaway DSH_HOME under the OS temp directory, so no real profile is read or written, and it deletes every gist it created even when an assertion fails. Besides the gh api round trip it covers the one path a fake cannot vouch for: a 1.5 MB file, which real GitHub truncates in the JSON response, coming back whole from the raw_url the API named.

$env:DSH_GIST_LIVE_TEST='1'; npm run test:live     # PowerShell
DSH_GIST_LIVE_TEST=1 npm run test:live             # bash

Because install_bundle links this directory into the profile, the working copy is the live plugin: edits to index.js and lib/ take effect on reload.

The settings page

A Client settings page is planned but not written. Two findings shape it:

  1. The active profile has no Web UI. The dsh-tui profile composes the terminal interface; the dsh-web-app bundle — which provides the browser surface and the settings slots — is disabled there. A settings page therefore has nowhere to render until the plugin is also installed into the web profile, or the web bundle is enabled for dsh-tui.

  2. Client-to-Host calls need a supported channel. The shipped, typed route is the settings domain (ctx.remote.settings.*, or the ctx.configForms wrapper), which requires the Host half to declare a settings namespace — and that means importing the Harness's schemastery from a workspace-installed bundle, which is unverified for a non-shipped bundle. The alternative, the raw ctx.connection.rpc channel, is fully typed but has no shipped consumer outside the API gateway. This is the open question to settle before writing client.js.

The page is intended to register into the settings.section slot (one nav entry in the Settings dialog), showing the same table as gist_status with buttons wired to the four operations.

Licence

MIT © 2026 Shen Yiming.

package.json sets "private": true on purpose: it means the package is never published to npm and exists only to be installed as a local bundle. That is orthogonal to the licence.

Review history

An adversarial review of this repository found, among other things: a gh failure that could kill the whole Harness process; every gist read error being read as "deleted", silently forking backups; and an upload that deleted the gist's only copy of a file that was merely missing from disk. All of those are fixed and covered by test/regression.test.mjs, whose cases are written to fail if the fix is reverted. The review also confirmed several things were already sound: the registration contract, the effect and disposal lifecycle, the manifest, and the divergence classifier when both sides hash the same file set.

A second review (issue #1) re-examined those fixes and found the guards stopped one level too early: the profile directory was contained but the tracked files inside it were not, a download could still half-apply, force: true was accepted and then ignored, a profile whose directory had been deleted was invisible to every bulk operation, one sync did not converge after a remote deletion, the state lock was process-local, and truncated content was fetched through gh api on an undocumented assumption. All seven are fixed.

Reviewing that round found more, including one defect it had introduced: resolving a tracked path whose parent directory was missing returned the shortened path, so downloading config/app.yml into a profile without config/ wrote the content into a file named config and reported success. Also found and fixed: containment held at the profiles directory rather than the profile, so a link to a sibling profile let one profile's gist publish and then overwrite another's config; the path was resolved once per operation, so a directory swapped for a link mid-download could redirect the write out of the tree; a symlinked profiles directory made a deleted profile look like an escape and disabled its recovery; two tracked names that fold to one file were accepted and then made every download fail; a failed state write reported a lost download and left a temp file behind; the fetch used for truncated content had no timeout, no size cap, no redirect check and no proxy fallback; a large untracked file could make the whole profile unreachable; and the prose claimed a remote deletion could be applied with gist_download force, which no code path did. test/safety.test.mjs, test/regression.test.mjs and test/live.test.mjs pin each one.