DeepSeek Harness plugins are ordinary TypeScript: a Host side that registers tools and APIs, and a Client side that renders slots in the UI. The difficult part is not writing one. It is keeping both sides consistent while the manifest, the build and the runtime each enforce their own rules.
DSHX is a build-time toolchain for out-of-tree DeepSeek Harness plugins. It deliberately does not replace the runtime: the official DSH/Cordis services still own execution, registries, scopes, transport, persistence, assembly, HMR and disposal. What DSHX adds is typed authoring, a bounded Vite build, offline diagnostics, Profile-aware development and reproducible templates.
Create a plugin
pnpm create dshx@preview my-plugin
cd my-plugin
pnpm check
pnpm dev
The default template is a minimal starter with CSS Modules: one Host Tool and one visible Client Slot. If you want the full Candidate API surface — Settings, typed API, locale, slots, Tailwind — generate the showcase instead:
pnpm create dshx@preview my-plugin --template showcase --style tailwind
Templates are starter and showcase; styles are css-modules, tailwind or none.
One contract, two sides
A typed API is declared once and consumed from both processes. The contract is plain TypeScript, so a handler that returns the wrong shape fails at check time instead of at runtime:
// src/api/status.ts
import { defineApi, method } from "@becomeopc/dshx/api";
export const statusApi = defineApi({
id: "status",
version: 1,
methods: {
get: method<void, { readonly ready: boolean }>(),
},
});
The Host implements it:
// src/host.ts
import { defineHost } from "@becomeopc/dshx/host";
import { statusApi } from "./api/status.js";
export default defineHost({
apis: [statusApi.host({ get: () => ({ ready: true }) })],
});
The Client queries it through a hook that already knows about connection state:
// src/client.tsx
import {
defineClient,
defineLocale,
defineSlot,
useApiQuery,
type PropsLocaleOf,
} from "@becomeopc/dshx/client";
import { statusApi } from "./api/status.js";
const copy = defineLocale("my-plugin.status", {
zh: { ready: "就绪", unavailable: "不可用" },
en: { ready: "Ready", unavailable: "Unavailable" },
});
function Status({ t }: PropsLocaleOf<typeof copy>) {
const query = useApiQuery(statusApi, "get", { enabled: true });
if (query.status === "pending") return <p>{query.fetchStatus}</p>;
if (query.status === "error") return <button onClick={query.refetch}>Retry</button>;
return <p>{t(query.data.ready ? "ready" : "unavailable")}</p>;
}
export default defineClient({
locales: [copy],
slots: [
defineSlot("sidebar.footer.action", {
id: "my-plugin.status",
order: 0,
locale: copy,
component: Status,
}),
],
});
Three details are easy to miss. defineLocale infers one exact key set, registers its dictionaries before slots and gives the component a typed t(). The package still declares @deepseek-ai/dsh-client-locale in dsh.client.inject, and create-dshx writes that provider edge for you. Finally, useApiQuery surviving final tree-shaking is what drives the connection capability automatically — no manual wiring.
The build is bounded on purpose
Plugin authors get Vite, but not an unbounded Vite. host.vite.plugins and client.vite.plugins accept native Vite PluginOption[], while DSHX preserves the entry, target, external, chunk, format and asset policy. Client CSS and inline assets still go through the standard Vite pipeline.
Three commands cover the loop:
dshx check # offline: config, manifest, migration, compatibility, TypeScript
dshx check --runtime # additionally require Profile, Composition, bridge and runtime readiness
dshx build # typecheck, then emit Host and Client artifacts plus declarations
dshx dev # build-watch plus an official DSH development session
check is read-only unless --fix is passed, and build never rewrites source or manifest metadata. That makes check safe to run in CI and build safe to run in a release job.
Compatibility, explicitly
The active protocol-1 adapter publishes this peer range:
>=0.1.0-rc.8 <0.2.0-0 || 0.1.1-rc.2
Verified real-runtime boundaries are 0.1.0-rc.8 and 0.1.1-rc.2. The second arm exists because npm excludes prerelease versions across patch ranges. Plugin public support belongs in peerDependencies; one concrete development version belongs in devDependencies.
Where it fits
If you are writing a plugin that should stay out of the harness tree, DSHX is the layer that keeps the contract, the build and the diagnostics honest. Start with pnpm create dshx@preview, run check in CI, and pin the runtime version you actually test against.
- Project and docs: https://dshx.io/
- Repository: https://github.com/liyown/dshx
- Reference plugin:
@becomeopc/dshx-plugin-marketplace
DSHX is in preview: the Candidate API is the intended 0.1.x authoring shape, not a 1.0 stability promise.
Top comments (0)