Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/brave-pandas-document.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@upstash/context7-deepseek-harness": minor
"ctx7": minor
---

Add the official Context7 plugin for DeepSeek Harness with native library resolution, documentation tools, credential-provider integration, and one-command CLI setup.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ Always use Context7 when I need library/API documentation, code generation, setu
- [`ctx7`](https://www.npmjs.com/package/ctx7) - CLI
- [`@upstash/context7-sdk`](https://www.npmjs.com/package/@upstash/context7-sdk) - TypeScript SDK
- [`@upstash/context7-tools-ai-sdk`](https://www.npmjs.com/package/@upstash/context7-tools-ai-sdk) - Vercel AI SDK tools
- [`@upstash/context7-deepseek-harness`](https://www.npmjs.com/package/@upstash/context7-deepseek-harness) - DeepSeek Harness plugin
- [`@upstash/context7-pi`](https://www.npmjs.com/package/@upstash/context7-pi) - pi.dev extension

## Disclaimer
Expand Down
79 changes: 79 additions & 0 deletions docs/clients/deepseek-harness.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: DeepSeek Harness
description: Using Context7 with DeepSeek Harness
---

Context7 integrates with [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) through the official [`@upstash/context7-deepseek-harness`](https://github.com/upstash/context7/tree/master/packages/deepseek-harness) plugin. It registers Context7's documentation tools natively through the harness tool service and adds system-prompt guidance so library-specific questions invoke them automatically.

## Installation

Install the bundle into a DeepSeek Harness profile and store a Context7 API key in the Harness credential provider:

```bash
npx ctx7@latest setup --deepseek headless
```

Pass another profile name instead of `headless` when needed. Without an existing Context7 login, setup starts the device authorization flow and creates an API key.

To use anonymous Context7 limits without configuring a key, install the bundle directly:

```bash
dsh plugin --profile headless add @upstash/context7-deepseek-harness
```

Verify the composed configuration and start the profile:

```bash
dsh --profile headless --dump-config
dsh --profile headless
```

Remove it with `dsh plugin --profile headless remove @upstash/context7-deepseek-harness`.

## Authentication

The plugin resolves `CONTEXT7_API_KEY` through DeepSeek Harness's credential provider before every request. The setup command stores it in `$DSH_HOME/.credentials.yaml`, which defaults to `~/.dsh/.credentials.yaml`. Credential changes apply without reloading the plugin.

The launching environment remains supported:

```bash
export CONTEXT7_API_KEY=ctx7sk_...
```

For manual credential-file setup, edit the flat YAML mapping and apply owner-only permissions:

```bash
mkdir -p ~/.dsh
chmod 700 ~/.dsh
${EDITOR:-vi} ~/.dsh/.credentials.yaml
chmod 600 ~/.dsh/.credentials.yaml
```

```yaml
CONTEXT7_API_KEY: ctx7sk_...
```

Never put API keys in `cordis.yml` or `cordis.patch.yml`; composed configuration can be printed and shared during debugging.

## What it adds

<CardGroup cols={2}>
<Card title="resolve-library-id" icon="magnifying-glass">
Finds Context7-compatible library IDs and available versions for a package or product.
</Card>
<Card title="query-docs" icon="book">
Fetches current documentation and code examples for a selected library ID.
</Card>
</CardGroup>

## Usage

Ask a documentation question in a DeepSeek Harness session:

```
How do I configure caching in Next.js 16?
Show me the current Prisma syntax for relations.
What are the Supabase authentication methods?
```

The model resolves the relevant Context7 library ID and then queries its documentation.
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"clients/codex",
"clients/copilot-cli",
"clients/cursor",
"clients/deepseek-harness",
"clients/opencode",
"clients/pi",
"clients/vscode",
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"figlet": "^1.9.4",
"open": "^10.1.0",
"ora": "^9.4.0",
"picocolors": "^1.1.1"
"picocolors": "^1.1.1",
"yaml": "^2.9.0"
},
"devDependencies": {
"@types/figlet": "^1.7.0",
Expand Down
104 changes: 104 additions & 0 deletions packages/cli/src/__tests__/deepseek-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";

const writeDeepSeekCredential = vi.fn();
const installDeepSeekPlugin = vi.fn();
const validateDeepSeekProfile = vi.fn((profile: string) => profile);
const trackEvent = vi.fn();

vi.mock("../setup/deepseek.js", () => ({
writeDeepSeekCredential: (...args: unknown[]) => writeDeepSeekCredential(...args),
installDeepSeekPlugin: (...args: unknown[]) => installDeepSeekPlugin(...args),
validateDeepSeekProfile: (...args: [string]) => validateDeepSeekProfile(...args),
}));

vi.mock("../utils/tracking.js", () => ({
trackEvent: (...args: unknown[]) => trackEvent(...args),
}));

const spinner = {
start: vi.fn().mockReturnThis(),
succeed: vi.fn().mockReturnThis(),
fail: vi.fn().mockReturnThis(),
};

vi.mock("ora", () => ({ default: () => spinner }));

import { registerSetupCommand } from "../commands/setup.js";

async function runSetup(...args: string[]): Promise<void> {
const program = new Command();
program.exitOverride();
registerSetupCommand(program);
await program.parseAsync(["node", "test", "setup", ...args]);
}

beforeEach(() => {
vi.clearAllMocks();
writeDeepSeekCredential.mockResolvedValue("/tmp/dsh/.credentials.yaml");
installDeepSeekPlugin.mockResolvedValue(undefined);
validateDeepSeekProfile.mockImplementation((profile: string) => profile);
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
process.exitCode = undefined;
});

afterEach(() => {
vi.restoreAllMocks();
process.exitCode = undefined;
});

describe("DeepSeek Harness setup command", () => {
test("stores the credential and installs the requested profile", async () => {
await runSetup("--deepseek", "team", "--api-key", "ctx7sk-test");

expect(writeDeepSeekCredential).toHaveBeenCalledWith("ctx7sk-test");
expect(installDeepSeekPlugin).toHaveBeenCalledWith("team");
expect(trackEvent).toHaveBeenCalledWith("setup", { mode: "deepseek", profile: "team" });
expect(process.exitCode).toBeUndefined();
});

test("uses the headless profile by default", async () => {
await runSetup("--deepseek", "--api-key", "ctx7sk-test");

expect(installDeepSeekPlugin).toHaveBeenCalledWith("headless");
});

test("rejects incompatible setup modes", async () => {
await runSetup("--deepseek", "--mcp", "--api-key", "ctx7sk-test");

expect(writeDeepSeekCredential).not.toHaveBeenCalled();
expect(installDeepSeekPlugin).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
});

test("validates the profile before storing a credential", async () => {
validateDeepSeekProfile.mockImplementation(() => {
throw new Error("Invalid DeepSeek Harness profile name");
});

await runSetup("--deepseek", "..", "--api-key", "ctx7sk-test");

expect(writeDeepSeekCredential).not.toHaveBeenCalled();
expect(installDeepSeekPlugin).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
});

test("fails when the credential cannot be written", async () => {
writeDeepSeekCredential.mockRejectedValue(new Error("write failed"));

await runSetup("--deepseek", "--api-key", "ctx7sk-test");

expect(installDeepSeekPlugin).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
});

test("fails when the plugin cannot be installed", async () => {
installDeepSeekPlugin.mockRejectedValue(new Error("install failed"));

await runSetup("--deepseek", "--api-key", "ctx7sk-test");

expect(writeDeepSeekCredential).toHaveBeenCalledWith("ctx7sk-test");
expect(process.exitCode).toBe(1);
});
});
150 changes: 150 additions & 0 deletions packages/cli/src/__tests__/deepseek.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { mkdir, mkdtemp, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { parseDocument } from "yaml";
import { afterEach, describe, expect, test } from "vitest";
import {
CONTEXT7_CREDENTIAL_REF,
DEEPSEEK_PLUGIN_PACKAGE,
deepSeekPluginInvocation,
resolveDshHome,
validateDeepSeekProfile,
writeDeepSeekCredential,
} from "../setup/deepseek.js";

const temporaryRoots: string[] = [];

async function temporaryRoot(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), "ctx7-deepseek-setup-"));
temporaryRoots.push(root);
return root;
}

afterEach(async () => {
await Promise.all(
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))
);
});

describe("DeepSeek Harness setup", () => {
test("resolves the Harness home", () => {
const workspace = resolve("workspace");
const userHome = join(workspace, "users", "test");
expect(
resolveDshHome({ DSH_HOME: join(workspace, "custom", "..", "dsh") }, userHome, workspace)
).toBe(join(workspace, "dsh"));
expect(resolveDshHome({}, userHome, workspace)).toBe(join(userHome, ".dsh"));
expect(resolveDshHome({ DSH_HOME: " " }, userHome, workspace)).toBe(join(userHome, ".dsh"));
expect(resolveDshHome({ DSH_HOME: "~/.harness" }, userHome, workspace)).toBe(
join(userHome, ".harness")
);
expect(resolveDshHome({ DSH_HOME: join("state", "dsh") }, userHome, workspace)).toBe(
join(workspace, "state", "dsh")
);
});

test("writes the credential without replacing other entries or comments", async () => {
const home = join(await temporaryRoot(), ".dsh");
const filename = join(home, ".credentials.yaml");
await mkdir(home, { recursive: true, mode: 0o755 });
await writeFile(
filename,
"# existing credential\nDEEPSEEK_API_KEY: sk-deepseek\nCONTEXT7_API_KEY: old\n",
{ mode: 0o644 }
);

await expect(writeDeepSeekCredential("ctx7sk-new", home)).resolves.toBe(filename);

const content = await readFile(filename, "utf8");
const values = parseDocument(content).toJS() as Record<string, string>;
expect(content).toContain("# existing credential");
expect(values).toEqual({
DEEPSEEK_API_KEY: "sk-deepseek",
CONTEXT7_API_KEY: "ctx7sk-new",
});
if (process.platform !== "win32") {
expect((await stat(home)).mode & 0o777).toBe(0o700);
expect((await stat(filename)).mode & 0o777).toBe(0o600);
}
});

test("refuses to overwrite an invalid credential document", async () => {
const home = join(await temporaryRoot(), ".dsh");
const filename = join(home, ".credentials.yaml");
await mkdir(home, { recursive: true });
await writeFile(filename, "DEEPSEEK_API_KEY: 42\n", "utf8");

await expect(writeDeepSeekCredential("ctx7sk-new", home)).rejects.toThrow(
'Credential "DEEPSEEK_API_KEY"'
);
await expect(readFile(filename, "utf8")).resolves.toBe("DEEPSEEK_API_KEY: 42\n");
});

test("folds in a credential update made by another locked writer", async () => {
const home = join(await temporaryRoot(), ".dsh");
const filename = join(home, ".credentials.yaml");
await mkdir(home, { recursive: true, mode: 0o700 });
await writeFile(filename, "DEEPSEEK_API_KEY: first\n", { mode: 0o600 });
await writeFile(`${filename}.lock`, "other-writer\n", { mode: 0o600, flag: "wx" });

const pending = writeDeepSeekCredential("ctx7sk-new", home);
await new Promise((resolve) => setTimeout(resolve, 40));
await writeFile(filename, "DEEPSEEK_API_KEY: second\nOTHER_API_KEY: preserved\n", {
mode: 0o600,
});
await unlink(`${filename}.lock`);
await pending;

expect(parseDocument(await readFile(filename, "utf8")).toJS()).toEqual({
DEEPSEEK_API_KEY: "second",
OTHER_API_KEY: "preserved",
CONTEXT7_API_KEY: "ctx7sk-new",
});
});

test("builds the profile plugin installation command", () => {
expect(deepSeekPluginInvocation("headless")).toEqual({
command: "npx",
args: [
"--yes",
"@deepseek-ai/dsh",
"plugin",
"--profile",
"headless",
"add",
DEEPSEEK_PLUGIN_PACKAGE,
],
});
expect(() => deepSeekPluginInvocation("../profile")).toThrow(
"Invalid DeepSeek Harness profile"
);
for (const profile of [
"",
".",
"..",
"node_modules",
"nested/profile",
"nested\\profile",
"team profile",
"team&calc",
"team|calc",
"team<calc",
"team>calc",
"team^calc",
"team(calc)",
"team%PATH%",
"team!calc",
"team\ncalc",
]) {
expect(() => validateDeepSeekProfile(profile)).toThrow("Invalid DeepSeek Harness profile");
}
expect(validateDeepSeekProfile("team-profile_1.0")).toBe("team-profile_1.0");
});

test("rejects an empty credential", async () => {
await expect(writeDeepSeekCredential("", await temporaryRoot())).rejects.toThrow(
"Context7 API key cannot be empty"
);
expect(CONTEXT7_CREDENTIAL_REF).toBe("CONTEXT7_API_KEY");
});
});
Loading
Loading