dshpluginsdev
DeepSeek Harness (DSH) web plugins, by DIV7NE.
Each plugin is a self-contained package in this repository. Every one of them declares its own bundle patch, so installing a single directory is enough — the DSH CLI appends it to the profile's bundle stack and no profile file is edited by hand.
| Plugin | What it does |
|---|---|
dsh-suggest-next-prompt/ |
Puts a model-generated next prompt in the chat composer's placeholder. Tab pastes it, ↑/↓ cycle the shortlist, Esc dismisses. |
dsh-chrome-agent/ |
Drives your real, already logged-in Chrome through a pinned companion extension, instead of a separate browser profile. |
| dsh-run-in-terminal | A Run button on every chat code block, and an integrated terminal in the right sidebar to run it in. |
Installing a plugin
git clone https://github.com/DIV7NE/dshpluginsdev
cd dshpluginsdev/<plugin>
npm install
npm run install:profile # builds, packs, and installs into the web profile
Then restart the DSH server for that profile — a profile's bundle stack and the browser's client-module graph are both composed at boot, so a new plugin cannot appear in a running server. Once a plugin is in the graph, later changes need only a page reload.
The Chrome extension
dsh-chrome-agent ships two artifacts from one directory, and they are released
separately:
- the DSH plugin (this repository's package, installed like its siblings);
- the Chrome extension, a plain folder under
dsh-chrome-agent/extension/packed byscripts/pack-webstore.mjsinto a root-level zip, which is the artifact the Chrome Web Store uploads. It is built and attached to Releases, not published through the DSH plugin channel.
npm run pack:webstore in that directory regenerates the zip. CI runs it and
asserts the zip still contains manifest.json at its root, because the plugin's
own tests would not notice a broken packer.
Repository layout
<plugin>/ one self-contained DSH plugin package
src/ source
lib/ committed build output (see below)
cordis.patch.yml the bundle patch that installs it
test/ hermetic tests, plus a non-hermetic live probe
README.md what it does, how to configure it, its permissions and risks
docs/superpowers/ design, plan, and implementation notes
Why lib/ is committed. A DSH plugin may not run a prepare or postinstall
script, so nothing may build it at install time. The build output therefore ships
in the repository, unminified. CI fails if it drifts from src/.
Development
cd <plugin>
npm run build # esbuild
npm run typecheck # tsc --noEmit
npm test # node:test — hermetic
npm run install:profile
npm run build && npm run typecheck && npm test is the same gate CI runs.
Release status
Both plugins pass the DSH-Store fixed-source precheck. Submission issues:
| Plugin | Issue | Precheck |
|---|---|---|
dsh-suggest-next-prompt |
#778 | passed, 1 warning |
dsh-run-in-terminal |
#779 | passed, 2 warnings, partial scan |
The precheck is a bounded static read of the pinned commit. It is not a security audit, not a runtime verification, and not an automatic listing — the Catalog gate that follows re-pins the source and applies stricter licence, dependency, lifecycle, bundle and runtime-source checks before anything appears in the store.
Two known constraints, stated rather than hidden:
scripts/install-profile.mjsin each plugin invokesnpm,npm packand thedshCLI throughchild_processso that a rebuild-and-reinstall is one command. Both plugins' prechecks flag it. It is a local development helper: it is not in thefileslist, so it is never part of a published package, and no runtime code path reaches it. Arguments are fixed strings, not user input.dsh-run-in-terminal's committed browser bundle exceeds the scanner's 256 KiB per-file limit, because it inlines xterm. The store reports the scan surface as incomplete for that reason. The bundle cannot simply be shrunk — inlining is deliberate, since the shell does not seed xterm in its module table, and a plugin may not build at install time.
License
MIT. See LICENSE.
dsh-run-in-terminal
A DeepSeek Harness web plugin that puts a Run button on every code block in the chat and gives you a real terminal to run it in.
A fenced block in a reply grows a Run button next to the Copy button DSH
already draws. Clicking it opens an integrated terminal in the right sidebar,
pastes the snippet into it, and presses Enter. Right-click inside the terminal
for Attach selection as context (plus Copy / Paste / Select all): the
selected output is appended to the conversation draft as a fenced block, ready
to send.
Install
cd <this directory>
npm install
npm run install:profile # builds, packs, and installs into the web profile
Then restart the DSH server for that profile (dsh web) and reload the
page. A restart is required, not optional: the profile's bundle stack and the
browser's client-module graph are both composed at boot, so a new plugin cannot
appear in a running server.
npm run install:profile is the whole update loop — run it again after any
source change and restart. It takes a profile name as its first argument
(node scripts/install-profile.mjs myprofile, default web).
Why a tarball and not a local link
dsh plugin --profile web add <directory> is the usual way to install an
out-of-tree plugin, and on this machine it does not work for a plugin that
lives on a different drive than the profile: pnpm creates the node_modules
junction with the wrong target (<profile>\\D:\\projects\\dshpluginsdev,
which resolves to nothing). The launcher then cannot read the package manifest,
never sees dsh.bundle.patch, and reports
declares no dsh.bundle — installed as a plain dependency, not a profile layer.
scripts/install-profile.mjs works around it by installing from an npm
tarball, which pnpm materialises as a real directory; the bundle then
reconciles correctly (verified: dsh-run-in-terminal is appended to
dsh.profile.bundles).
What each half does
| File | Role |
|---|---|
src/index.ts |
Host half: one persistent pty per session, served over GET /runterm/pty (WebSocket), behind a browser-trust fence. |
src/client/index.tsx |
Registers the runterminal right-Sidebar page type and the code-block button pass. |
src/client/code-run.ts |
Watches the transcript for .md-code-block nodes and attaches the Run button. |
src/client/terminal-view.tsx |
The xterm pane, its socket, and the right-click menu. |
src/client/draft.ts |
Appends a terminal selection to the composer draft through the conversation service. |
scripts/build.mjs |
esbuild: the node bundle, and the browser bundle wrapped in the window.__ModuleLoader__.load boilerplate. |
How the Run button reaches the shell
openTab('runterminal', { params: { run } }) acts on the session whose
conversation is on screen and deduplicates by kind, so the click either creates
the terminal or re-navigates the one already open. The tab body watches
navigation.revision, writes the snippet to the pty as {t:'run', code}, and
the host turns a snippet into shell input:
- line endings become CR (a terminal sends Enter as CR) with one final CR to submit, so a backslash-continued command — the shape long commands take in chat code blocks — runs exactly as written;
- trailing blank lines are dropped, so a snippet that already ends in a newline does not submit an extra empty command.
Framing lives on the host deliberately: the bytes the shell reads have one
authority, and that is the part covered by npm test.
The code text itself is read back as textContent of the block's content
node. DSH draws line numbers with a CSS counter, never as DOM text, so this is
the exact snippet and not a numbered copy of it.
Security
The route spawns processes, so every request passes a browser-trust fence
before it can reach one — the same fence the shipped /api gateway applies to
its own routes:
- the
Hostheader must name a loopback authority (a name that merely resolves here from elsewhere is a DNS-rebinding attempt); - a request a browser marks
Sec-Fetch-Site: cross-siteis refused; - an
Origin, when present, must name the same hostname.
This is a DNS-rebinding / cross-site defense, not authentication. It does not distinguish one loopback page from another: a page served from the same hostname on a different port shares the hostname and passes. If you bind DSH to a non-loopback address, this route is exposed with everything else.
The pty starts in the session's workspace root when the client knows it, and in the server's own working directory otherwise; a requested directory that is not an existing directory is ignored.
Configuration
Add config.shell to the plugin row to choose the shell:
- id: run-in-terminal
name: 'dsh-run-in-terminal'
config:
shell: 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
The default is pwsh.exe on Windows when it is on PATH (powershell.exe
otherwise), and $SHELL — else /bin/bash — on POSIX, started as a login
shell.
Known limitations
- One terminal per conversation session, keyed by session id. Closing the tab ends the process; hiding or switching away from it does not.
- A multi-line snippet is submitted line by line, so a shell control block
(
for … do … done,if … { }) run from a chat code block needs its continuation characters — the same as pasting into a terminal that lacks bracketed-paste support. - The pane lives in DSH's native right Sidebar under its own kind
(
runterminal). It does not reuse and does not collide with theterminalkind that other sidebar plugins register. - Strings are English-only; the plugin registers no locale namespace.
Development
npm run build # esbuild: lib/index.js + the wrapped lib/client.js
npm run typecheck # tsc --noEmit
npm test # node --test test/host.test.mjs (hermetic)
npm run install:profile
The hermetic suite covers the byte framing, the transcript bound, shell resolution, and the trust fence.
test/live-probe.mjs is the non-hermetic companion: point it at a running
server that has this plugin loaded and it asserts a same-origin handshake is
accepted, a Run frame round-trips through a real shell, and a foreign Origin
is refused.
node test/live-probe.mjs http://127.0.0.1:3080 probe
Browser bundle details: react and @deepseek-ai/* stay external (the shell
seeds them in its frozen module table), while xterm and its fit addon are
inlined, so the bundle needs no dsh.client.external entry.
No comments yet. Be the first to write one.