<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[liyown dev notes]]></title><description><![CDATA[liyown dev notes]]></description><link>https://liyown.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 22 Oct 2026 22:40:13 GMT</lastBuildDate><atom:link href="https://liyown.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Typed Plugins for DeepSeek Harness: A Tour of DSHX]]></title><description><![CDATA[DSHX is a build-time toolchain for typed DeepSeek Harness plugins: one contract for Host and Client, a bounded Vite build, and offline checks.]]></description><link>https://liyown.hashnode.dev/typed-plugins-for-deepseek-harness-a-tour-of-dshx</link><guid isPermaLink="true">https://liyown.hashnode.dev/typed-plugins-for-deepseek-harness-a-tour-of-dshx</guid><category><![CDATA[Deepseek]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[devtools]]></category><dc:creator><![CDATA[Evan]]></dc:creator><pubDate>Tue, 20 Oct 2026 09:49:16 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<p><a href="https://dshx.io/">DSHX</a> 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.</p>
<h2>Create a plugin</h2>
<pre><code class="language-bash">pnpm create dshx@preview my-plugin
cd my-plugin
pnpm check
pnpm dev
</code></pre>
<p>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:</p>
<pre><code class="language-bash">pnpm create dshx@preview my-plugin --template showcase --style tailwind
</code></pre>
<p>Templates are <code>starter</code> and <code>showcase</code>; styles are <code>css-modules</code>, <code>tailwind</code> or <code>none</code>.</p>
<h2>One contract, two sides</h2>
<p>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:</p>
<pre><code class="language-ts">// src/api/status.ts
import { defineApi, method } from "@becomeopc/dshx/api";

export const statusApi = defineApi({
  id: "status",
  version: 1,
  methods: {
    get: method&lt;void, { readonly ready: boolean }&gt;(),
  },
});
</code></pre>
<p>The Host implements it:</p>
<pre><code class="language-ts">// src/host.ts
import { defineHost } from "@becomeopc/dshx/host";
import { statusApi } from "./api/status.js";

export default defineHost({
  apis: [statusApi.host({ get: () =&gt; ({ ready: true }) })],
});
</code></pre>
<p>The Client queries it through a hook that already knows about connection state:</p>
<pre><code class="language-tsx">// 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&lt;typeof copy&gt;) {
  const query = useApiQuery(statusApi, "get", { enabled: true });
  if (query.status === "pending") return &lt;p&gt;{query.fetchStatus}&lt;/p&gt;;
  if (query.status === "error") return &lt;button onClick={query.refetch}&gt;Retry&lt;/button&gt;;
  return &lt;p&gt;{t(query.data.ready ? "ready" : "unavailable")}&lt;/p&gt;;
}

export default defineClient({
  locales: [copy],
  slots: [
    defineSlot("sidebar.footer.action", {
      id: "my-plugin.status",
      order: 0,
      locale: copy,
      component: Status,
    }),
  ],
});
</code></pre>
<p>Three details are easy to miss. <code>defineLocale</code> infers one exact key set, registers its dictionaries before slots and gives the component a typed <code>t()</code>. The package still declares <code>@deepseek-ai/dsh-client-locale</code> in <code>dsh.client.inject</code>, and <code>create-dshx</code> writes that provider edge for you. Finally, <code>useApiQuery</code> surviving final tree-shaking is what drives the connection capability automatically — no manual wiring.</p>
<h2>The build is bounded on purpose</h2>
<p>Plugin authors get Vite, but not an unbounded Vite. <code>host.vite.plugins</code> and <code>client.vite.plugins</code> accept native Vite <code>PluginOption[]</code>, while DSHX preserves the entry, target, external, chunk, format and asset policy. Client CSS and inline assets still go through the standard Vite pipeline.</p>
<p>Three commands cover the loop:</p>
<pre><code class="language-bash">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
</code></pre>
<p><code>check</code> is read-only unless <code>--fix</code> is passed, and <code>build</code> never rewrites source or manifest metadata. That makes <code>check</code> safe to run in CI and <code>build</code> safe to run in a release job.</p>
<h2>Compatibility, explicitly</h2>
<p>The active protocol-1 adapter publishes this peer range:</p>
<pre><code>&gt;=0.1.0-rc.8 &lt;0.2.0-0 || 0.1.1-rc.2
</code></pre>
<p>Verified real-runtime boundaries are <code>0.1.0-rc.8</code> and <code>0.1.1-rc.2</code>. The second arm exists because npm excludes prerelease versions across patch ranges. Plugin public support belongs in <code>peerDependencies</code>; one concrete development version belongs in <code>devDependencies</code>.</p>
<h2>Where it fits</h2>
<p>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 <code>pnpm create dshx@preview</code>, run <code>check</code> in CI, and pin the runtime version you actually test against.</p>
<ul>
<li>Project and docs: <a href="https://dshx.io/">https://dshx.io/</a></li>
<li>Repository: <a href="https://github.com/liyown/dshx">github.com/liyown/dshx</a></li>
<li>Reference plugin: <code>@becomeopc/dshx-plugin-marketplace</code></li>
</ul>
<p>DSHX is in preview: the Candidate API is the intended 0.1.x authoring shape, not a 1.0 stability promise.</p>
<p><em>Originally published on <a href="https://dev.to/liyown/typed-plugins-for-deepseek-harness-a-tour-of-dshx-nnf">dev.to</a>.</em></p>
]]></content:encoded></item></channel></rss>