feat/dsh plugin (#3356)

* docs: remove .superpowers

* feat(dsh-plugin): bring the DeepSeek Harness plugin into the monorepo

Moves dsh-plugin-reactive-resume out of its own repository and into
packages/dsh-plugin. It stays a published, public npm package — the only
one here — but now builds, typechecks, tests, and lints under the same
turbo tasks as everything else.

The move pays for itself in the drift guard. Standalone, the plugin kept a
generated snapshot of the tool names scraped from the live server card at
https://rxresu.me, plus a weekly CI job to notice when that snapshot went
stale. Sitting next to packages/mcp, it reads MCP_TOOL_NAME directly, so a
tool rename breaks the prompt guide on the same pull request instead of
days later. The snapshot, the fetch script, and the scheduled job are gone.

packages/mcp gains a ./tool-names export so that import goes through the
public export map rather than another workspace's src.

Also flips autoInstallPeers off. The DeepSeek Harness rc packages declare
peers that are host-supplied and, in one case
(@deepseek-ai/dsh-type-meta), not published at all, so auto-install 404s
the whole workspace. Turning it off drops only optional peers elsewhere;
@neodrag/core was the single hard peer that had been arriving implicitly,
and it is now declared where it is used. Full typecheck and test suites
pass, and pnpm peers check reports nothing new beyond the pre-existing
drizzle-orm range mismatch.

Tests move from test/ to colocated src/*.test.ts and the build output from
lib/ to dist/ to match repository conventions.

* fix(dsh-plugin): ship a bundle manifest and target the current Harness

`dsh plugin add` warned that the package "declares no dsh.bundle — installed
as a plain dependency, not a profile layer", and it was right. Every other
Harness plugin, in-box and third-party, ships a cordis.patch.yml and points
dsh.bundle.patch at it; that declaration is what joins a package to a
profile's bundle stack. Without it the package installed and then sat inert,
and the README's hand-written insert row was a workaround for the gap rather
than the intended way in.

The peer ranges were also a generation behind. They asked for
@deepseek-ai/dsh-mcp-client and dsh-system-prompt at ^0.0.1-rc.1, which
cannot match the 0.1.0-rc.6 a current harness ships, so the plugin could
never have resolved against the thing it targets. Both APIs are unchanged
across the bump — StreamableHttpConfig still takes the same six fields and
PromptSection still takes name/order/text — so this is a range correction,
not a migration.

That bump pays for itself elsewhere. The old generation peer-depended on
@deepseek-ai/dsh-type-meta, which was never published, and working around
that 404 is why merging this package turned autoInstallPeers off for the
whole repository and pulled @neodrag/core in by hand. The new generation
dropped that peer and publishes every other one, so both changes are
reverted and pnpm-workspace.yaml is back to what it was.

Because a bundle patch mounts the plugin the moment it is installed, a
required apiKey would fail config validation and take the profile down
before the user ever had a chance to mint a key. It now defaults to empty
and apply() warns and mounts nothing, matching how dsh-honcho-memory
handles the same problem.

Verified by packing the tarball and installing it into a clean project with
default pnpm settings: it resolves, imports, and reports its exports.
This commit is contained in:
Amruth Pillai
2026-08-18 20:42:42 +02:00
committed by GitHub
parent ebcaa4729f
commit 65618a82a0
20 changed files with 2627 additions and 698 deletions
+30
View File
@@ -0,0 +1,30 @@
import { expect, it } from "vitest";
import { Config } from "./config";
it("applies defaults for every optional field", () => {
const parsed = Config({ apiKey: "test-key" });
expect(parsed).toEqual({
apiKey: "test-key",
url: "https://rxresu.me",
serverName: "resume",
toolCallTimeoutMs: 60_000,
});
});
it("keeps explicit values", () => {
const parsed = Config({
apiKey: "test-key",
url: "http://localhost:3000",
serverName: "rr",
toolCallTimeoutMs: 5_000,
});
expect(parsed.url).toBe("http://localhost:3000");
expect(parsed.serverName).toBe("rr");
expect(parsed.toolCallTimeoutMs).toBe(5_000);
});
it("defaults apiKey to empty rather than throwing, so an unconfigured row still loads", () => {
expect(Config({}).apiKey).toBe("");
});
+34
View File
@@ -0,0 +1,34 @@
import z from "@deepseek-ai/schemastery";
/** `dsh-mcp-client` reserves this shape for a server namespace. */
const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
/** Resolved plugin configuration. Every field is populated after parsing. */
export interface Config {
/** API key minted at `<url>/dashboard/settings/api-keys`. */
apiKey: string;
/** Reactive Resume origin, no trailing slash. */
url: string;
/** Tool namespace: tools reach the model as `mcp__<serverName>__<rawName>`. */
serverName: string;
/** Per-tool-call timeout in milliseconds. */
toolCallTimeoutMs: number;
}
export const Config = z.object({
// Not `.required()`: the bundle patch mounts this plugin as soon as the
// package is installed, so a missing key has to be an inert no-op rather
// than a validation error that takes the whole profile down at boot.
// `apply` warns and mounts nothing instead.
apiKey: z.string().default("").description("API key from <url>/dashboard/settings/api-keys."),
url: z
.string()
.default("https://rxresu.me")
.description("Reactive Resume origin. Set this for a self-hosted instance."),
serverName: z
.string()
.pattern(SERVER_NAME_PATTERN)
.default("resume")
.description("Tool namespace. Must match [A-Za-z0-9_-]{1,32} and be unique across live MCP instances."),
toolCallTimeoutMs: z.natural().default(60_000).description("Per-tool-call timeout in milliseconds."),
});
+58
View File
@@ -0,0 +1,58 @@
import { expect, it, vi } from "vitest";
import { Config } from "./config";
import { apply, inject, name } from "./index";
/** Minimal stand-in for the parts of the Cordis context `apply` touches. */
function fakeContext() {
return {
plugin: vi.fn(async (_plugin: unknown, _config: unknown) => undefined),
systemPrompt: { section: vi.fn(() => () => undefined) },
logger: { warn: vi.fn() },
};
}
it("exports the cordis plugin name", () => {
expect(name).toBe("reactive-resume");
});
it("declares the services it needs", () => {
expect(inject).toEqual(["systemPrompt"]);
});
it("mounts the MCP bridge with streamable-http and the api key header", async () => {
const ctx = fakeContext();
await apply(ctx as never, Config({ apiKey: "test-key" }));
expect(ctx.plugin).toHaveBeenCalledTimes(1);
expect(ctx.plugin.mock.calls[0]?.[1]).toEqual({
transport: "streamable-http",
serverName: "resume",
url: "https://rxresu.me/mcp",
headers: { "x-api-key": "test-key" },
toolCallTimeoutMs: 60_000,
failOnStartupError: true,
});
});
it("strips a trailing slash from the configured url", async () => {
const ctx = fakeContext();
await apply(ctx as never, Config({ apiKey: "test-key", url: "http://localhost:3000/" }));
expect(ctx.plugin.mock.calls[0]?.[1]).toMatchObject({ url: "http://localhost:3000/mcp" });
});
it("rejects a serverName the bridge would refuse at config-parse time, before apply runs", () => {
expect(() => Config({ apiKey: "test-key", serverName: "has spaces" })).toThrow(/serverName/);
});
it("mounts nothing when no apiKey is configured, so an unconfigured install still boots", async () => {
const ctx = fakeContext();
await apply(ctx as never, Config({}));
expect(ctx.plugin).not.toHaveBeenCalled();
expect(ctx.systemPrompt.section).not.toHaveBeenCalled();
expect(ctx.logger.warn).toHaveBeenCalledTimes(1);
});
+66
View File
@@ -0,0 +1,66 @@
/**
* DeepSeek Harness plugin for Reactive Resume.
* @module dsh-plugin-reactive-resume
*/
import type { Context } from "@deepseek-ai/cordis";
// Side-effect import: pulls in the `Context.systemPrompt` module augmentation
// this plugin relies on below. No runtime value is used from this module.
import type {} from "@deepseek-ai/dsh-system-prompt";
import type { Config } from "./config";
import * as mcpClient from "@deepseek-ai/dsh-mcp-client";
import { buildPatchGuide } from "./prompt";
// Re-exports the interface AND the schema — `config.ts` exports both under the
// name `Config`, and Cordis reads the schema export to validate config before
// this plugin starts.
export { Config } from "./config";
/** Cordis plugin name used by loader diagnostics. */
export const name = "reactive-resume";
/**
* Services required by this plugin.
*
* `tools` is deliberately not injected here: `ctx.tools.restrict()` requires
* an agent-scoped context, which a plugin's own `apply(ctx, config)` never
* is, and this plugin never calls it. `@deepseek-ai/dsh-mcp-client` declares
* its own dependency on the tools service, so the bridge still gets what it
* needs without this plugin waiting on it.
*/
export const inject = ["systemPrompt"];
/**
* Connect a Reactive Resume account to the session.
* @param ctx - plugin context carrying prompt assembly.
* @param config - resolved plugin configuration. `Config` already rejects an
* invalid `serverName` at parse time, before `apply` ever runs.
*/
export async function apply(ctx: Context, config: Config): Promise<void> {
// The bundle patch mounts this row on install, before anyone has minted a
// key. Mount nothing rather than failing the profile's boot: `dsh plugin
// add` should never leave the harness unbootable.
if (config.apiKey === "") {
ctx.logger.warn("no apiKey configured — set one at %s/dashboard/settings/api-keys to enable the tools", config.url);
return;
}
const origin = config.url.replace(/\/+$/, "");
await ctx.plugin(mcpClient, {
transport: "streamable-http",
serverName: config.serverName,
url: `${origin}/mcp`,
headers: { "x-api-key": config.apiKey },
toolCallTimeoutMs: config.toolCallTimeoutMs,
failOnStartupError: true,
});
ctx.systemPrompt.section({
// Namespaced by `serverName` so a second instance (e.g. a self-hosted
// account alongside the hosted one) doesn't collide on section name.
name: `reactive-resume:${config.serverName}`,
order: 150,
text: buildPatchGuide(config.serverName),
});
}
+61
View File
@@ -0,0 +1,61 @@
import type { PromptSection } from "@deepseek-ai/dsh-system-prompt";
import { expect, it, vi } from "vitest";
import { Config } from "./config";
import { apply } from "./index";
import { PATCH_GUIDE } from "./prompt";
function fakeContext() {
return {
plugin: vi.fn(async (_plugin: unknown, _config: unknown) => undefined),
systemPrompt: { section: vi.fn((_section: PromptSection) => () => undefined) },
};
}
it("registers one prompt section in the tool-guidance order band", async () => {
const ctx = fakeContext();
await apply(ctx as never, Config({ apiKey: "test-key" }));
expect(ctx.systemPrompt.section).toHaveBeenCalledTimes(1);
const section = ctx.systemPrompt.section.mock.calls[0]?.[0] as PromptSection;
expect(section.name).toBe("reactive-resume:resume");
expect(section.order).toBeGreaterThanOrEqual(100);
expect(section.order).toBeLessThanOrEqual(199);
expect(section.text).toBe(PATCH_GUIDE);
});
it("derives the section name from serverName so a second instance can coexist", async () => {
const first = fakeContext();
const second = fakeContext();
await apply(first as never, Config({ apiKey: "test-key" }));
await apply(second as never, Config({ apiKey: "test-key", serverName: "self-hosted" }));
const firstSection = first.systemPrompt.section.mock.calls[0]?.[0] as PromptSection;
const secondSection = second.systemPrompt.section.mock.calls[0]?.[0] as PromptSection;
expect(firstSection.name).not.toBe(secondSection.name);
});
it("names the tools it references with the configured namespace", async () => {
const ctx = fakeContext();
await apply(ctx as never, Config({ apiKey: "test-key", serverName: "rr" }));
const section = ctx.systemPrompt.section.mock.calls[0]?.[0] as PromptSection;
expect(section.text).toContain("mcp__rr__read_resume");
expect(section.text).not.toContain("mcp__resume__read_resume");
});
it("covers the documented failure modes", () => {
for (const phrase of ["RFC 6902", "unlock_resume", "lock_resume", "list_resumes"]) {
expect(PATCH_GUIDE).toContain(phrase);
}
});
it("does not point at the unreachable schema resource", () => {
expect(PATCH_GUIDE).not.toContain("resume://_meta/schema");
});
it("does not claim update_resume replaces resume content", () => {
expect(PATCH_GUIDE).toContain("only changes metadata");
});
+40
View File
@@ -0,0 +1,40 @@
/**
* Build the Reactive Resume system-prompt section for one tool namespace.
* @param serverName - the MCP namespace bridged tools are published under.
* @returns prompt text naming tools exactly as the model will see them.
*/
export function buildPatchGuide(serverName: string): string {
const t = (raw: string) => `\`mcp__${serverName}__${raw}\``;
return [
"## Reactive Resume",
"",
"These tools operate on the user's real, live resumes and job applications. Changes are immediate and visible in their account.",
"",
"### Reading before writing",
"",
`- Call ${t("list_resumes")} to discover resume IDs. IDs are UUIDs, never titles or slugs.`,
`- Call ${t("read_resume")} before any edit. Never patch a resume you have not read this session.`,
`- If a call fails with "not found", re-run ${t("list_resumes")} rather than guessing an ID.`,
"",
"### Editing",
"",
`- ${t("apply_resume_patch")} takes RFC 6902 JSON Patch operations applied to the resume data document.`,
`- Construct paths from the resume you already read this session — do not guess. ${t("apply_resume_patch")}'s own tool description ships concrete path examples (for example \`/basics/name\`, \`/sections/experience/items/-\`, \`/sections/experience/items/0/company\`, \`/metadata/template\`); match those shapes.`,
"- Section entries are arrays of objects, each with its own UUID `id`. Address an existing entry by locating its index from the document you just read; never treat an `id` as an index.",
"- Prefer one patch with several operations over several single-operation patches. Operations apply in order and the whole patch fails atomically.",
`- ${t("update_resume")} only changes metadata — name, slug, tags, and public visibility. It cannot touch resume content; use ${t("apply_resume_patch")} for that. No tool replaces an existing resume's content wholesale: ${t("import_resume")} creates a brand-new resume from a full data document, it does not overwrite one you already have.`,
"",
"### Locking",
"",
`- A locked resume rejects every write. When a call fails because the resume is locked, call ${t("unlock_resume")}, make the change, then call ${t("lock_resume")} to leave the lock as you found it.`,
"",
"### Scope",
"",
"- Never delete a resume or an application unless the user asked for that specific deletion in this conversation.",
"- When the user describes a change in prose, restate the concrete edit you are about to make before making it.",
].join("\n");
}
/** The prompt section for the default `resume` namespace. */
export const PATCH_GUIDE: string = buildPatchGuide("resume");
@@ -0,0 +1,27 @@
import { expect, it } from "vitest";
import { MCP_TOOL_NAME } from "@reactive-resume/mcp/tool-names";
import { buildPatchGuide } from "./prompt";
/** Every raw tool name the prompt guide instructs the model to call. */
function toolsReferencedByGuide(): string[] {
const guide = buildPatchGuide("resume");
const matches = guide.matchAll(/mcp__resume__([a-z0-9_]+)/g);
return [...new Set([...matches].map((match) => match[1] as string))];
}
it("references at least one tool", () => {
// Guards the regex itself: a guide rewrite that drops the namespaced form
// would otherwise make the next test pass vacuously.
expect(toolsReferencedByGuide().length).toBeGreaterThan(0);
});
it("only references tools the MCP server actually publishes", () => {
// Reads the server's own tool-name table rather than a generated snapshot of
// a live server card, so renaming a tool in `packages/mcp` fails here on the
// same PR instead of drifting until a scheduled network check notices.
const published: readonly string[] = Object.values(MCP_TOOL_NAME);
for (const referenced of toolsReferencedByGuide()) {
expect(published).toContain(referenced);
}
});