dsh-ENHANCED
Everyday upgrades for DeepSeek Harness in one plugin: multi-engine free web search, skills & MCP management, auto-compact tuning, instance controls:RESTART/SHUTDOWN, themes
- Stars
- 2
- Language
- JavaScript
- Created
- Aug 24, 2026
- Updated
- Aug 25, 2026
Introduction
dsh-enhanced
One plugin that bundles the everyday upgrades DeepSeek Harness (DSH) lacks out of the box:
- Free web search — a vendored multi-engine search provider (DuckDuckGo ×2, Bing, AnySearch, SearXNG, Mojeek-ready fallback chain + optional paid engines), configured from a Settings tab, with self-hosted-SearXNG support and live health checks.
- Skills manager — install/edit/remove agent
SKILL.mdskills persistently. Installs accept a single skill or a folder OF skills (disk path or browser folder-picker alike), keep bundledscripts/executable (browsers drop permission bits; shebang files are restored to 0755), and report per-skill results so one bad bundle never blocks the rest. - MCP server manager — manage
@deepseek-ai/dsh-mcp-clientrows across profiles. - Auto-compact tuner — clamp the context-compaction trigger below the harness default.
- Instance controls — clean shutdown/restart of the GUI process via icon buttons beside Settings in the sidebar foot.
- Themes — original ENHANCED theme (phosphor-green terminal look, digital-rain boot intro) and a Cyberpunk 2077 theme ported from the community theme.
Single host composition plugin (cordis.patch.yml row), single model tool surface (manage_skills_mcps), single browser card (Settings → Plugins). No telemetry, no external services beyond the search engines themselves.
Install
dsh plugin add https://github.com/HIT-HTML/dsh-ENHANCED # or clone into a profile and add a composition row
The repo ships prebuilt dist/ and client.js, so no build step is needed to install. To develop, see Development.
Plugin row (usually written for you by dsh plugin add):
- id: dsh-enhanced
name: dsh-enhanced
config:
mcpProfiles: ["default", "web"] # whose cordis.patch.yml receives managed rows
allowRestart: false # opt-in for supervisor-assisted restarts
Architecture
Two halves, standard DSH plugin shape:
┌─ HOST (Node, src/*.ts → dist/) ─────────────────────────────┐
│ index.ts composition root: registers the model tool, │
│ browser RPC channel, settings anchor, boots the │
│ vendored search engine │
│ shared.ts paths, managed-block surgery, Env/Handler types │
│ skills.ts mcp.ts compact.ts instance.ts search.ts │
│ feature modules — each owns its actions end-to-end│
│ free-search-vendor.ts vendored engine (see below) │
└──────────────┬──────────────────────────────────────────────┘
│ package-private JSON RPC (browser → host)
┌─ CLIENT (browser, client/** → client.js bundle) ────────────┐
│ core.js draft/save pipeline, sections registry │
│ main.js boot, saved-theme activation │
│ sections/* one card per feature (skills, mcp, compact, │
│ search, theme) │
│ themes/* matrix, cyberpunk2077 (+ boot intro) │
└─────────────────────────────────────────────────────────────┘
Persistence is deliberately boring: every host feature writes marker-delimited
"managed blocks" into <profile>/cordis.patch.yml (and ~/.dsh/skills/ for skills).
No database, no state file — the user's config tree is the state, readable by eye:
# >>> dsh-enhanced:mcp >>> …rows… # <<< dsh-enhanced:mcp <<<
# >>> dsh-enhanced:compact >>> …row… # <<< dsh-enhanced:compact <<<
# >>> dsh-enhanced:search >>> …row… # <<< dsh-enhanced:search <<<
shared.ts owns the split/merge (splitBlock, splitInner); feature modules never regex the file themselves.
The action-core pattern (how everything stays in sync)
The GUI and the model tool call the same handlers, so they can't drift:
- A feature module exports
X_ACTIONS(string list) +handleX: Handler. index.tsconcatenates all action lists into themanage_skills_mcpstool schema and lines all handlers up inHANDLERS.- Dispatch = first handler to return non-null wins; unknown action ⇒ error.
- Browser RPC reuses the same
performActioncore over a package-private channel.
To add a feature: new src/<feature>.ts exporting ACTIONS + Handler, two lines in index.ts, one section file under client/sections/. That's the whole integration story.
Feature reference
| Module | Actions | Writes to |
|---|---|---|
| skills | list_skills, read_skill, add_skill, edit_skill, remove_skill, install_skill, install_skill_files | ~/.dsh/skills/<name>/SKILL.md |
| mcp | list_mcps, mcp_status, add_mcp, remove_mcp, enable_mcp, disable_mcp | managed :mcp: block |
| compact | compact_status, set_compact | managed :compact: block |
| instance | shutdown_instance, restart_instance | process control only |
| search | list_search, set_search | managed :search: block |
Client-side, each feature is a section plugged into three registries in core.js:
DRAFT_SHAPES (form state), DIRTY_CHECKS (unsaved chip), SAVE_STEPS (replay on Save).
Sections self-fetch on expand and stage edits locally; Save replays steps in order and
a mid-batch failure keeps exactly the unapplied part staged.
Secrets (API keys) are never echoed back: list_search reports hasKey.<field> booleans,
key inputs start blank meaning "unchanged", and only non-blank values are written.
Search subsystem
Provenance
The engine is vendored from dsh-free-search
v0.4.12 (MIT, © DDDMUC) into src/free-search-vendor.ts. We vendor rather than depend because
upstream's peerDependencies aren't published to npm — fresh installs of the standalone plugin
can fail outright. Upstream fixes do not propagate automatically; re-port when adopting them.
Local adaptations vs upstream (all collision-safety or de-branding):
| What | Upstream | Here |
|---|---|---|
| settings namespace | free-search | enhanced-free-search |
| bridge prefix | /api/dsh-free-search-settings | /api/dsh-enhanced-free-search |
| search provider id | ddg | enhanced-free |
| settings UI section | installed its own card | removed — our Search tab owns config UX |
| self-update machinery | check-update + pnpm add upgrade routes | removed — a vendored copy must not reinstall upstream over itself |
| agent-visible strings | "Settings > Plugins > Free Search" | point at this plugin's Search section |
Boot flow
profile cordis.patch.yml dsh-enhanced host boot
:search: managed block ─┐
├─► index.ts: readSearchConfig(profiles[0])
Settings→Search tab ────┘ │
▼
ctx.inject(["web"], scope => freeSearch.apply(scope, cfg))
│
registers provider id "enhanced-free",
agent tools, system-prompt section, bridge routes
│
▼
harness web_search / advanced_search route through it
Config is read once at boot — after saving in the tab, restart the profile to apply.
If the standalone dsh-free-search is also installed somewhere, nothing clashes:
distinct namespace/prefix/provider-id, and each registration is guarded.
Engines and the fallback chain
Preferred engine = your Provider dropdown. On failure/empty results the chain walks on:
paid (only if keyed): exa → tavily → keenable → perplexity → deepseek-official
free, always: bing → anysearch → ddg → ddg-lite → searxng
Time filtering (advanced_search) is honored by engines that support it and skips the rest.
Results are cached per query (LRU ~50, TTL ≤5 min, configurable).
SearXNG instances (self-hosted path)
Public SearXNG instances rate-limit the JSON API to death; run your own:
mkdir -p ~/.searxng && cat > ~/.searxng/settings.yml <<'EOF'
use_default_settings: true
server:
secret_key: "pick-any-random-string"
limiter: false
search:
formats:
- html
- json # required — without this every API query gets 403
EOF
docker run -d --name searxng -p 8888:8080 -v ~/.searxng:/etc/searxng searxng/searxng
Paste http://127.0.0.1:8888 into the Search tab (SearXNG provider). Saved URLs are normalized
at the parser in src/search.ts (set_search): missing scheme ⇒ http://, trailing slashes
stripped, comma/space/newline separators, multiple URLs tried in order. A bad URL can never break
searching — dead instances are skipped and the chain falls through.
Health checks: whenever the Search tab loads (and right after Save), list_search probes each
saved URL (2.5 s cap, parallel) and renders one line per instance:
✓ ok— real SearXNG JSON answered⚠ misconfigured— reachable but JSON API off (the missing-formats: jsonmistake)⚠ down— nothing listening
Probe lives in probeInstances() (src/search.ts); statuses ride the list_search response as
instanceStatus[].
Surfaces
| Surface | Where | Notes |
|---|---|---|
| Config tab | Settings → Plugins → Search | provider/region/market/keys/instances |
dsh_search tool | ours, thin | delegates to active provider, fails clearly if none |
web_search, advanced_search, platform_search, free_search_test | vendored | standard + time-filtered + per-platform + engine-doctor tools |
| Test bridge | POST /api/dsh-enhanced-free-search/raw-search | loopback-only; body {query, maxResults?, engine?, timeRange?} — handy for curl smoke tests |
Development
npm install
npm run build # tsc → dist/, then client/** → client.js (+ node --check)
npm run selfcheck # offline end-to-end test, no frameworks
selfcheck (test/selfcheck.mjs) builds a temp $DSH_HOME, runs the real handlers against a
stubbed plugin context (captured registrations instead of live services), and asserts disk output:
managed-block round-trips, secret preservation, YAML shapes, name validation, provider registration.
It is the regression gate — extend it when you add behavior.
Lab bench (second live GUI without touching your main one):
echo $'webserver:\n port: 3090' > /tmp/lab-port.yml
dsh --profile lab --patch /tmp/lab-port.yml
Serves the workspace client live (cache-control: no-cache), so client edits show on refresh;
host edits need a lab restart. Never point this at your main profile.
Adding an engine (search)
src/free-search-vendor.ts:const X_URL,async function searchX(query, maxResults, options, signal)returning{ sources: [{url,title?,snippet?}], truncated:false }— reusefetchHtmlWithRetry,stripTags,uniqueSources.- Add the id to
FREE_ENGINES/ALL_ENGINES, thefreeEngineschain array insideprovider.search, and acaseinrunEngineTest. client/sections/search.js: onePROVIDER_METAentry (free: trueunless it needs a key).- Smoke-test through the bridge:
curl -X POST .../raw-search -d '{"engine":"x","query":"hi"}'.
Adding a feature module
Copy the shape of src/compact.ts (smallest full example): actions const, Handler switch,
managed block via splitBlock/writeFile, guard-rails server-side. Then one handleX line +
X_ACTIONS spread in index.ts, and a section file registering into the client registries.
Release checklist
-
npm run build && npm run selfcheck - Lab bench smoke: bridge query with preferred engine + one fallback
-
dist/andclient.jsare committed artifacts here — make sure they're fresh - No machine-specific paths or secrets in tracked files (
git grep -E "/Users/|sk-[A-Za-z0-9]") - Bump
package.jsonversion
Privacy & security notes
- No telemetry. The only outbound traffic is the searches you (or your agent) run.
- API keys are stored in the profile's
cordis.patch.ymland never sent back to the browser (blank field = unchanged;hasKeybooleans only). - The test bridge accepts loopback requests only (
isLoopbackRequestguard) and redacts secrets. - Strings like
EXA123in tests are deliberate fixtures, not credentials.
License
MIT — see LICENSE. Contains code vendored from dsh-free-search (MIT, © DDDMUC) and theme work originating from the Cyberpunk 2077 community theme; both attributed in-file.