From 2b1861e1410798600412211e906c3b8fb84d5519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Fri, 14 Aug 2026 11:32:05 +0300 Subject: [PATCH 1/5] feat: add DeepSeek Harness plugin --- .changeset/brave-pandas-document.md | 5 + README.md | 1 + docs/clients/deepseek-harness.mdx | 62 +++++ docs/docs.json | 1 + packages/deepseek-harness/.gitignore | 5 + packages/deepseek-harness/LICENSE | 21 ++ packages/deepseek-harness/README.md | 47 ++++ packages/deepseek-harness/cordis.patch.yml | 3 + packages/deepseek-harness/eslint.config.js | 37 +++ packages/deepseek-harness/package.json | 77 ++++++ packages/deepseek-harness/src/api.ts | 78 ++++++ packages/deepseek-harness/src/format.ts | 42 +++ packages/deepseek-harness/src/index.ts | 86 ++++++ packages/deepseek-harness/test/plugin.test.ts | 177 ++++++++++++ packages/deepseek-harness/tsconfig.json | 17 ++ packages/deepseek-harness/tsup.config.ts | 11 + packages/deepseek-harness/vitest.config.ts | 8 + pnpm-lock.yaml | 257 ++++++++++++++++++ 18 files changed, 935 insertions(+) create mode 100644 .changeset/brave-pandas-document.md create mode 100644 docs/clients/deepseek-harness.mdx create mode 100644 packages/deepseek-harness/.gitignore create mode 100644 packages/deepseek-harness/LICENSE create mode 100644 packages/deepseek-harness/README.md create mode 100644 packages/deepseek-harness/cordis.patch.yml create mode 100644 packages/deepseek-harness/eslint.config.js create mode 100644 packages/deepseek-harness/package.json create mode 100644 packages/deepseek-harness/src/api.ts create mode 100644 packages/deepseek-harness/src/format.ts create mode 100644 packages/deepseek-harness/src/index.ts create mode 100644 packages/deepseek-harness/test/plugin.test.ts create mode 100644 packages/deepseek-harness/tsconfig.json create mode 100644 packages/deepseek-harness/tsup.config.ts create mode 100644 packages/deepseek-harness/vitest.config.ts diff --git a/.changeset/brave-pandas-document.md b/.changeset/brave-pandas-document.md new file mode 100644 index 000000000..d4a3c846a --- /dev/null +++ b/.changeset/brave-pandas-document.md @@ -0,0 +1,5 @@ +--- +"@upstash/context7-deepseek-harness": minor +--- + +Add the official Context7 plugin for DeepSeek Harness with native library resolution and documentation tools. diff --git a/README.md b/README.md index aa34be9ba..196c01b0e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/clients/deepseek-harness.mdx b/docs/clients/deepseek-harness.mdx new file mode 100644 index 000000000..0bc8861b9 --- /dev/null +++ b/docs/clients/deepseek-harness.mdx @@ -0,0 +1,62 @@ +--- +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. + +## Installation + +Install the bundle into a DeepSeek Harness profile: + +```bash +dsh plugin --profile my-profile add @upstash/context7-deepseek-harness +``` + +The bundle activates Context7 automatically. Verify the composed configuration and start the profile: + +```bash +dsh --profile my-profile --dump-config +dsh --profile my-profile +``` + +Remove it with `dsh plugin --profile my-profile remove @upstash/context7-deepseek-harness`. + +## Authentication + +The plugin works without configuration using Context7's anonymous rate limits. For higher limits and private repositories, create a key in the [Context7 dashboard](https://context7.com/dashboard) and export it before starting the harness: + +```bash +export CONTEXT7_API_KEY=ctx7sk_... +``` + +You can also set the key when loading the plugin directly in `cordis.yml`: + +```yaml +- name: '@upstash/context7-deepseek-harness' + config: + apiKey: 'ctx7sk_...' +``` + +## What it adds + + + +Finds Context7-compatible library IDs and available versions for a package or product. + + +Fetches current documentation and code examples for a selected library ID. + + + +## 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. diff --git a/docs/docs.json b/docs/docs.json index bd4faa07d..1bd612f1e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -156,6 +156,7 @@ "clients/codex", "clients/copilot-cli", "clients/cursor", + "clients/deepseek-harness", "clients/opencode", "clients/pi", "clients/vscode", diff --git a/packages/deepseek-harness/.gitignore b/packages/deepseek-harness/.gitignore new file mode 100644 index 000000000..2a5015ff1 --- /dev/null +++ b/packages/deepseek-harness/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.log +.env +.env.* diff --git a/packages/deepseek-harness/LICENSE b/packages/deepseek-harness/LICENSE new file mode 100644 index 000000000..0a1f72a6b --- /dev/null +++ b/packages/deepseek-harness/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Upstash, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/deepseek-harness/README.md b/packages/deepseek-harness/README.md new file mode 100644 index 000000000..ecae75240 --- /dev/null +++ b/packages/deepseek-harness/README.md @@ -0,0 +1,47 @@ +# Context7 Plugin for DeepSeek Harness + +The official [Context7](https://context7.com) plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness). It adds native `resolve-library-id` and `query-docs` tools so the harness can retrieve current library documentation and code examples. + +## Installation + +```bash +dsh plugin --profile my-profile add @upstash/context7-deepseek-harness +``` + +The bundle adds the Context7 plugin to the selected profile. Verify the composed configuration and start the profile: + +```bash +dsh --profile my-profile --dump-config +dsh --profile my-profile +``` + +To remove it: + +```bash +dsh plugin --profile my-profile remove @upstash/context7-deepseek-harness +``` + +## Authentication + +The plugin works without configuration using Context7's anonymous rate limits. For higher limits, create an API key in the [Context7 dashboard](https://context7.com/dashboard) and set it before launching the harness: + +```bash +export CONTEXT7_API_KEY="your-api-key" +``` + +You can also configure the key when loading the plugin directly in `cordis.yml`: + +```yaml +- name: "@upstash/context7-deepseek-harness" + config: + apiKey: "your-api-key" +``` + +## Tools + +- `resolve-library-id` finds Context7-compatible library IDs and available versions. +- `query-docs` retrieves documentation for a selected library ID and question. + +## License + +MIT diff --git a/packages/deepseek-harness/cordis.patch.yml b/packages/deepseek-harness/cordis.patch.yml new file mode 100644 index 000000000..4729d2044 --- /dev/null +++ b/packages/deepseek-harness/cordis.patch.yml @@ -0,0 +1,3 @@ +- insert: + - id: context7 + name: "@upstash/context7-deepseek-harness" diff --git a/packages/deepseek-harness/eslint.config.js b/packages/deepseek-harness/eslint.config.js new file mode 100644 index 000000000..e1e72c93a --- /dev/null +++ b/packages/deepseek-harness/eslint.config.js @@ -0,0 +1,37 @@ +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; +import eslintPluginPrettier from "eslint-plugin-prettier"; + +export default defineConfig( + { + ignores: ["node_modules/**", "dist/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + parser: tseslint.parser, + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: import.meta.dirname, + }, + globals: { + process: "readonly", + fetch: "readonly", + Response: "readonly", + URL: "readonly", + AbortController: "readonly", + }, + }, + plugins: { + "@typescript-eslint": tseslint.plugin, + prettier: eslintPluginPrettier, + }, + rules: { + ...tseslint.configs.recommended.rules, + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "prettier/prettier": "error", + }, + } +); diff --git a/packages/deepseek-harness/package.json b/packages/deepseek-harness/package.json new file mode 100644 index 000000000..b333f44f6 --- /dev/null +++ b/packages/deepseek-harness/package.json @@ -0,0 +1,77 @@ +{ + "name": "@upstash/context7-deepseek-harness", + "version": "0.1.0", + "description": "Official Context7 plugin for DeepSeek Harness", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "cordis.patch.yml", + "LICENSE", + "README.md" + ], + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + }, + "scripts": { + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist", + "lint": "eslint .", + "lint:check": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/upstash/context7.git", + "directory": "packages/deepseek-harness" + }, + "keywords": [ + "deepseek", + "deepseek-harness", + "context7", + "documentation", + "upstash", + "ai", + "coding-agent" + ], + "author": "Upstash", + "license": "MIT", + "bugs": { + "url": "https://github.com/upstash/context7/issues" + }, + "homepage": "https://github.com/upstash/context7#readme", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": "^22.19.0 || >=24.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": ">=4.0.1 <5", + "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <1" + }, + "dependencies": { + "@deepseek-ai/schemastery": "3.18.1" + }, + "devDependencies": { + "@deepseek-ai/cordis": "4.0.1", + "@deepseek-ai/dsh-tools": "0.1.0-rc.6", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "vitest": "^4.1.9" + } +} diff --git a/packages/deepseek-harness/src/api.ts b/packages/deepseek-harness/src/api.ts new file mode 100644 index 000000000..c8a2a47e7 --- /dev/null +++ b/packages/deepseek-harness/src/api.ts @@ -0,0 +1,78 @@ +const BASE_URL = "https://context7.com/api/v2"; + +export interface SearchResult { + id: string; + title: string; + description: string; + totalSnippets?: number; + trustScore?: number; + benchmarkScore?: number; + versions?: string[]; + source?: string; +} + +export interface SearchResponse { + error?: string; + results: SearchResult[]; + searchFilterApplied?: boolean; +} + +function headers(apiKey?: string): HeadersInit { + return apiKey ? { Authorization: `Bearer ${apiKey}` } : {}; +} + +async function errorMessage(response: Response, apiKey?: string): Promise { + const body = (await response.json().catch(() => undefined)) as { message?: string } | undefined; + if (body?.message) return body.message; + + if (response.status === 429) { + return apiKey + ? "Rate limited or quota exceeded. Upgrade your plan at https://context7.com/plans for higher limits." + : "Rate limited or quota exceeded. Create a free API key at https://context7.com/dashboard for higher limits."; + } + if (response.status === 404) { + return "The requested library does not exist. Resolve the library ID again and choose another result."; + } + if (response.status === 401) { + return "Invalid Context7 API key. API keys should start with the 'ctx7sk' prefix."; + } + return `Context7 request failed with status ${response.status}.`; +} + +async function request( + url: URL, + apiKey: string | undefined, + signal: AbortSignal +): Promise { + const response = await fetch(url, { headers: headers(apiKey), signal }); + if (!response.ok) throw new Error(await errorMessage(response, apiKey)); + return response; +} + +export async function searchLibraries( + query: string, + libraryName: string, + apiKey: string | undefined, + signal: AbortSignal +): Promise { + const url = new URL(`${BASE_URL}/libs/search`); + url.searchParams.set("query", query); + url.searchParams.set("libraryName", libraryName); + const response = await request(url, apiKey, signal); + return (await response.json()) as SearchResponse; +} + +export async function fetchLibraryContext( + query: string, + libraryId: string, + apiKey: string | undefined, + signal: AbortSignal +): Promise { + const url = new URL(`${BASE_URL}/context`); + url.searchParams.set("query", query); + url.searchParams.set("libraryId", libraryId); + const response = await request(url, apiKey, signal); + const text = await response.text(); + if (text) return text; + return "Documentation not found for this library. Resolve the library ID again and choose another result."; +} diff --git a/packages/deepseek-harness/src/format.ts b/packages/deepseek-harness/src/format.ts new file mode 100644 index 000000000..64319485b --- /dev/null +++ b/packages/deepseek-harness/src/format.ts @@ -0,0 +1,42 @@ +import type { SearchResponse, SearchResult } from "./api.js"; + +function reputation(score?: number): "High" | "Medium" | "Low" | "Unknown" { + if (score === undefined || score < 0) return "Unknown"; + if (score >= 7) return "High"; + if (score >= 4) return "Medium"; + return "Low"; +} + +function formatResult(result: SearchResult): string { + const lines = [ + `- Title: ${result.title}`, + `- Context7-compatible library ID: ${result.id}`, + `- Description: ${result.description}`, + ]; + if (result.totalSnippets !== undefined && result.totalSnippets !== -1) { + lines.push(`- Code Snippets: ${result.totalSnippets}`); + } + lines.push(`- Source Reputation: ${reputation(result.trustScore)}`); + if (result.benchmarkScore !== undefined && result.benchmarkScore > 0) { + lines.push(`- Benchmark Score: ${result.benchmarkScore}`); + } + if (result.versions?.length) { + lines.push(`- Versions: ${result.versions.join(", ")}`); + } + if (result.source) { + lines.push(`- Source: ${result.source}`); + } + return lines.join("\n"); +} + +export function formatSearchResults(response: SearchResponse): string { + if (response.results.length === 0) return "No libraries found matching the provided name."; + const parts = []; + if (response.searchFilterApplied) { + parts.push( + "**Note:** Your results only include libraries matching your teamspace's library filters. To adjust quality thresholds or blocked libraries, update your filters at https://context7.com/dashboard?tab=policies" + ); + } + parts.push(response.results.map(formatResult).join("\n----------\n")); + return `Available Libraries:\n\n${parts.join("\n\n")}`; +} diff --git a/packages/deepseek-harness/src/index.ts b/packages/deepseek-harness/src/index.ts new file mode 100644 index 000000000..db9023510 --- /dev/null +++ b/packages/deepseek-harness/src/index.ts @@ -0,0 +1,86 @@ +import type { Context } from "@deepseek-ai/cordis"; +import { defineTool } from "@deepseek-ai/dsh-tools"; +import Schema from "@deepseek-ai/schemastery"; +import { fetchLibraryContext, searchLibraries } from "./api.js"; +import { formatSearchResults } from "./format.js"; + +export const name = "context7"; +export const inject = ["tools"]; + +export interface Config { + apiKey?: string; +} + +export const Config: Schema = Schema.object({ + apiKey: Schema.string(), +}); + +const RESOLVE_DESCRIPTION = `Resolves a package or product name to a Context7-compatible library ID and returns matching libraries. + +Call this tool before query-docs unless the user explicitly provides a library ID in /org/project or /org/project/version format. Select the closest official match using name, source reputation, snippet coverage, benchmark score, and version.`; + +const QUERY_DESCRIPTION = `Retrieves current documentation and code examples from Context7 for a library. + +Call resolve-library-id first unless the user explicitly provides a library ID in /org/project or /org/project/version format. Use a specific query scoped to one concept and do not include secrets, credentials, personal data, or proprietary code.`; + +export function apply(ctx: Context, config: Config = {}): void { + const apiKey = config.apiKey || process.env.CONTEXT7_API_KEY; + + ctx.tools.register( + defineTool({ + name: "resolve-library-id", + description: RESOLVE_DESCRIPTION, + parameters: { + query: { + type: "string", + required: true, + description: + "What to look up in the library documentation. Include the user's goal so Context7 can rank results by relevance.", + }, + libraryName: { + type: "string", + required: true, + description: + "Official library or product name with its normal spelling and punctuation, such as Next.js, Customer.io, or Three.js.", + }, + }, + output: { + schema: { type: "string" }, + render: (_args, value) => [{ type: "text", text: value }], + }, + isConcurrencySafe: () => true, + async execute(args, exec) { + const response = await searchLibraries(args.query, args.libraryName, apiKey, exec.signal); + if (response.results.length === 0 && response.error) throw new Error(response.error); + return formatSearchResults(response); + }, + }) + ); + + ctx.tools.register( + defineTool({ + name: "query-docs", + description: QUERY_DESCRIPTION, + parameters: { + libraryId: { + type: "string", + required: true, + description: + "Exact Context7-compatible library ID returned by resolve-library-id, such as /vercel/next.js or /vercel/next.js/v15.1.8.", + }, + query: { + type: "string", + required: true, + description: + "Specific documentation question scoped to one concept. Include relevant API names and versions.", + }, + }, + output: { + schema: { type: "string" }, + render: (_args, value) => [{ type: "text", text: value }], + }, + isConcurrencySafe: () => true, + execute: (args, exec) => fetchLibraryContext(args.query, args.libraryId, apiKey, exec.signal), + }) + ); +} diff --git a/packages/deepseek-harness/test/plugin.test.ts b/packages/deepseek-harness/test/plugin.test.ts new file mode 100644 index 000000000..4f4b96e73 --- /dev/null +++ b/packages/deepseek-harness/test/plugin.test.ts @@ -0,0 +1,177 @@ +import { readFile } from "node:fs/promises"; +import { Context, Service } from "@deepseek-ai/cordis"; +import ToolRuntime, { type ToolDefinition, type ToolExecutionInput } from "@deepseek-ai/dsh-tools"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import * as context7 from "../src/index.js"; + +const { apply } = context7; + +class SystemPromptStub extends Service { + constructor(ctx: Context) { + super(ctx, "systemPrompt"); + } + + tools(): () => void { + return () => undefined; + } + + section(): () => void { + return () => undefined; + } +} + +function loadTools(config: { apiKey?: string } = {}): Map { + const tools = new Map(); + const ctx = { + tools: { + register(tool: ToolDefinition) { + tools.set(tool.name, tool); + }, + }, + } as unknown as Context; + apply(ctx, config); + return tools; +} + +function execution() { + return { signal: new AbortController().signal } as never; +} + +function toolInput(name: string, args: Record): ToolExecutionInput { + return { + callId: name as never, + name, + arguments: args, + signal: new AbortController().signal, + }; +} + +afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); +}); + +describe("Context7 DeepSeek Harness plugin", () => { + it("registers both Context7 tools", () => { + expect([...loadTools().keys()]).toEqual(["resolve-library-id", "query-docs"]); + }); + + it("activates the bundle in the real Cordis tool runtime", async () => { + vi.stubGlobal( + "fetch", + vi.fn((input: URL) => + Promise.resolve( + input.pathname.endsWith("/libs/search") + ? new Response( + JSON.stringify({ + results: [ + { + id: "/vercel/next.js", + title: "Next.js", + description: "The React framework", + }, + ], + }) + ) + : new Response("Current documentation") + ) + ) + ); + + const manifest = JSON.parse( + await readFile(new URL("../package.json", import.meta.url), "utf8") + ) as { dsh: { bundle: { patch: string } } }; + const patch = await readFile( + new URL(`../${manifest.dsh.bundle.patch}`, import.meta.url), + "utf8" + ); + expect(patch).toBe( + '- insert:\n - id: context7\n name: "@upstash/context7-deepseek-harness"\n' + ); + + const root = new Context(); + await root.plugin(SystemPromptStub); + await root.plugin(ToolRuntime); + await root.plugin(context7); + + expect(root.tools.schemas().map(({ name }) => name)).toEqual([ + "resolve-library-id", + "query-docs", + ]); + const resolveInput = toolInput("resolve-library-id", { + query: "middleware", + libraryName: "Next.js", + }); + const queryInput = toolInput("query-docs", { + libraryId: "/vercel/next.js", + query: "middleware", + }); + expect(root.tools.executionMode(resolveInput)).toEqual({ kind: "parallel" }); + expect(root.tools.executionMode(queryInput)).toEqual({ kind: "parallel" }); + + const results = await Promise.all([ + root.tools.execute(resolveInput), + root.tools.execute(queryInput), + ]); + expect(results.map(({ isError }) => isError)).toEqual([false, false]); + + await root.fiber.dispose(); + }); + + it("resolves libraries with authentication and formats the response", async () => { + vi.stubEnv("CONTEXT7_API_KEY", "ctx7sk-environment"); + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + searchFilterApplied: true, + results: [ + { + id: "/vercel/next.js", + title: "Next.js", + description: "The React framework", + totalSnippets: 100, + trustScore: 10, + benchmarkScore: 92, + versions: ["v15.1.8"], + source: "https://nextjs.org/docs", + }, + ], + }) + ) + ); + vi.stubGlobal("fetch", fetchMock); + const tool = loadTools({ apiKey: "ctx7sk-test" }).get("resolve-library-id")!; + const result = await tool.execute({ query: "middleware", libraryName: "Next.js" }, execution()); + + expect(result).toContain("/vercel/next.js"); + expect(result).toContain("teamspace's library filters"); + expect(result).toContain("Source: https://nextjs.org/docs"); + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + href: expect.stringContaining("libraryName=Next.js"), + }), + expect.objectContaining({ + headers: { Authorization: "Bearer ctx7sk-test" }, + }) + ); + }); + + it("queries documentation without requiring an API key", async () => { + vi.stubEnv("CONTEXT7_API_KEY", ""); + const fetchMock = vi.fn().mockResolvedValue(new Response("Current documentation")); + vi.stubGlobal("fetch", fetchMock); + const tool = loadTools().get("query-docs")!; + const result = await tool.execute( + { libraryId: "/vercel/next.js", query: "middleware" }, + execution() + ); + + expect(result).toBe("Current documentation"); + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + href: expect.stringContaining("libraryId=%2Fvercel%2Fnext.js"), + }), + expect.objectContaining({ headers: {} }) + ); + }); +}); diff --git a/packages/deepseek-harness/tsconfig.json b/packages/deepseek-harness/tsconfig.json new file mode 100644 index 000000000..85319127e --- /dev/null +++ b/packages/deepseek-harness/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "lib": ["ES2023", "DOM"], + "module": "ESNext", + "target": "ES2022", + "moduleResolution": "bundler", + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*", "test/**/*", "tsup.config.ts", "vitest.config.ts", "eslint.config.js"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/deepseek-harness/tsup.config.ts b/packages/deepseek-harness/tsup.config.ts new file mode 100644 index 000000000..c55202499 --- /dev/null +++ b/packages/deepseek-harness/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + clean: true, + sourcemap: true, + target: "node22", + external: ["@deepseek-ai/cordis", "@deepseek-ai/dsh-tools", "@deepseek-ai/schemastery"], +}); diff --git a/packages/deepseek-harness/vitest.config.ts b/packages/deepseek-harness/vitest.config.ts new file mode 100644 index 000000000..ed8bf7739 --- /dev/null +++ b/packages/deepseek-harness/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["test/**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3161a1d5..a90f58a3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,31 @@ importers: specifier: ^4.1.9 version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(vite@7.2.4(@types/node@22.19.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + packages/deepseek-harness: + dependencies: + '@deepseek-ai/schemastery': + specifier: 3.18.1 + version: 3.18.1 + devDependencies: + '@deepseek-ai/cordis': + specifier: 4.0.1 + version: 4.0.1 + '@deepseek-ai/dsh-tools': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(f8724372086ccc1457fc84e7becee2e0) + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@25.9.1)(vite@7.2.4(@types/node@25.9.1)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + packages/mcp: dependencies: '@modelcontextprotocol/node': @@ -419,6 +444,129 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@deepseek-ai/cordis@4.0.1': + resolution: {integrity: sha512-YBdskTU2Po1kru3GgcUWUbkTsPMA9LkSQDAY8rBkFJeajdgcQad3QPJZE26JyK99Xb6HaASvoXg2DSUTeN/0Nw==} + hasBin: true + peerDependencies: + '@deepseek-ai/cordis-plugin-include': ^1.0.6 + '@deepseek-ai/cordis-plugin-loader': ^1.0.2 + peerDependenciesMeta: + '@deepseek-ai/cordis-plugin-include': + optional: true + '@deepseek-ai/cordis-plugin-loader': + optional: true + + '@deepseek-ai/cosmokit@1.8.2': + resolution: {integrity: sha512-muBOKtSrUKU5m/xpq8ZXWL6hQ/jgd4PhU2PqH97bcxIiLEJfNwZOGQEx4t/aS/GgxRAR+ra9pMHPMtTHU4sqqA==} + + '@deepseek-ai/dsh-agent@0.1.0-rc.6': + resolution: {integrity: sha512-vtqq2pWTrzn0dKfj5kREZRpP82AwtGjGx9V1lYnKvF+Uc/a8zyWbSvjDE7V1d3YQAQJzs2cWO31hURWDekDXIA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + '@deepseek-ai/dsh-typert-protocol': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-attachment@0.1.0-rc.6': + resolution: {integrity: sha512-3P6N17NQ8jqSQGzeCs+svCIqArU8oq0YmgEAo+axN9aVuUDferWU4DLRSX59UGpmyldX4LQn81toA+c+DqMcHg==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-brand@0.1.0-rc.6': + resolution: {integrity: sha512-E8j9Nby24qP4rfrdcfc7bpt1CHpGT3tYmycOJJkEOH4ptIdT1m2ro9nmnSd5CWYukTr64A77vjm2WGqHRI92UA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-code-runtime@0.1.0-rc.6': + resolution: {integrity: sha512-aw8D4IOeMo11A3uxQeE4LFoW3bvaQnVkGFqqtS+lsINDARrOCJHXLUgecoKpys+Lc5erZZ4UQDnymJmL4OCcKA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-invariants@0.1.0-rc.6': + resolution: {integrity: sha512-WfEfOi99a4cpOugRAHTBSTnesLieu3ist1q9PXDXFBHX++K1rAl9+sB7YrdnbB8LH0UOY532gS9xJUYU6w0SLw==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + + '@deepseek-ai/dsh-llm@0.1.0-rc.6': + resolution: {integrity: sha512-kuFGC8bHlzGTwlRxQhXjf3CYWl8M4NzH+EYIkrW8rri4iMc9W53xrdvkil5No/DUlMm8g1u7GdeiWYFy0TMvtA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-attachment': ^0.1.0-rc.6 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-timeout': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-scope@0.1.0-rc.6': + resolution: {integrity: sha512-UlDLV4syLoJinNg9imhXrSAHrdaTa5Ff8gg46rzjFJGPUOhAk3DZff0hryT5OhrBi0A5Tj92qVpg2pRVvxnUzQ==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-session@0.1.0-rc.6': + resolution: {integrity: sha512-8tu8I6VWC7050GAUXWhcEWQw4pakALQc8TlhKr52m7Y4+kIKeNt3FBgP86PaGPBtpK0p5zUPRQNkFpzZbBdxyw==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + '@deepseek-ai/dsh-typert-protocol': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-system-prompt@0.1.0-rc.6': + resolution: {integrity: sha512-E7g+XChh4q4/wX++v56z1pV4SA1Rtz42xkznLPPi9FlXrrzJxwHMOUzBZ9Rz3Y1kLhQ++HJG3ZatmNx3rjFilg==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-timeout@0.1.0-rc.6': + resolution: {integrity: sha512-CUean0fAnfsJVszFEip7PsU/S26W+JfDFfsza2dCtlw8n6xlkbHA9Gjxdk2aTwqDGCgXEPkRW7mYkdJ0n6FR7w==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-tools@0.1.0-rc.6': + resolution: {integrity: sha512-Tu08EPK3JyK0iNjH4FGzu/1uADynNSS6SmwOLdfytUN0YNqwNuKFSt2OJUg19famNlTgy992DcHfDu0T+gLXFg==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-agent': ^0.1.0-rc.6 + '@deepseek-ai/dsh-code-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + '@deepseek-ai/dsh-user-approval': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-typert-protocol@0.1.0-rc.6': + resolution: {integrity: sha512-weWzN8r01YCkoDCAM7BsKw2YhRrD4zL8N2SAZu9hovYtXSq8xHXsP4Zh8RLYIlYcuotjyff/6hic+0TJPd14YA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-user-approval@0.1.0-rc.6': + resolution: {integrity: sha512-9rnkSDGOpu2XUeGwbPeTzVUTFWTND1PMPM5L/ZQPptV5yyZlQiNxM2rCC6OdL+ZVerwxEqrRhZIQn/KVtQfKag==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-agent': ^0.1.0-rc.6 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + + '@deepseek-ai/schemastery@3.18.1': + resolution: {integrity: sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==} + '@earendil-works/pi-agent-core@0.78.0': resolution: {integrity: sha512-xhWd59Qzd8yO88gYQw2S4dEQstJJEiUtxRP01//YzVJ61jCtUASMfcyAmYhgGYR4Onp7GmwEAbBBGOiV6Iwk9g==} engines: {node: '>=22.19.0'} @@ -3557,6 +3705,115 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@deepseek-ai/cordis@4.0.1': + dependencies: + '@deepseek-ai/cosmokit': 1.8.2 + '@standard-schema/spec': 1.1.0 + + '@deepseek-ai/cosmokit@1.8.2': {} + + '@deepseek-ai/dsh-agent@0.1.0-rc.6(b0514d7a320728b6d8f5a31eb3960e10)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-session': 0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6) + '@deepseek-ai/dsh-system-prompt': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-typert-protocol': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + + '@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-code-runtime@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-attachment': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-timeout': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-session@0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-typert-protocol': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + + '@deepseek-ai/dsh-system-prompt@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-tools@0.1.0-rc.6(f8724372086ccc1457fc84e7becee2e0)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-agent': 0.1.0-rc.6(b0514d7a320728b6d8f5a31eb3960e10) + '@deepseek-ai/dsh-code-runtime': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-session': 0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6) + '@deepseek-ai/dsh-system-prompt': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-user-approval': 0.1.0-rc.6(dbdaba06c174c5e30e3a58edf3cc27a9) + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/dsh-typert-protocol@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-user-approval@0.1.0-rc.6(dbdaba06c174c5e30e3a58edf3cc27a9)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-agent': 0.1.0-rc.6(b0514d7a320728b6d8f5a31eb3960e10) + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-session': 0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6) + '@deepseek-ai/dsh-system-prompt': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/schemastery@3.18.1': + dependencies: + '@deepseek-ai/cosmokit': 1.8.2 + '@standard-schema/spec': 1.1.0 + '@earendil-works/pi-agent-core@0.78.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': dependencies: '@earendil-works/pi-ai': 0.78.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) From 7a71258308240e630c23b4e03ecf99e7bb81385b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Fri, 14 Aug 2026 11:48:55 +0300 Subject: [PATCH 2/5] fix: mark Harness host peers optional --- packages/deepseek-harness/package.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/deepseek-harness/package.json b/packages/deepseek-harness/package.json index b333f44f6..0e190397c 100644 --- a/packages/deepseek-harness/package.json +++ b/packages/deepseek-harness/package.json @@ -63,6 +63,14 @@ "@deepseek-ai/cordis": ">=4.0.1 <5", "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <1" }, + "peerDependenciesMeta": { + "@deepseek-ai/cordis": { + "optional": true + }, + "@deepseek-ai/dsh-tools": { + "optional": true + } + }, "dependencies": { "@deepseek-ai/schemastery": "3.18.1" }, From 7c52bec895380f2eae12bbf97f94540a92cbea56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Fri, 14 Aug 2026 12:34:11 +0300 Subject: [PATCH 3/5] feat: add Context7 invocation guidance --- docs/clients/deepseek-harness.mdx | 2 +- packages/deepseek-harness/README.md | 2 +- packages/deepseek-harness/package.json | 5 ++ packages/deepseek-harness/src/index.ts | 21 ++++++++- packages/deepseek-harness/test/plugin.test.ts | 47 +++++++++++-------- pnpm-lock.yaml | 3 ++ 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/docs/clients/deepseek-harness.mdx b/docs/clients/deepseek-harness.mdx index 0bc8861b9..8f11515d5 100644 --- a/docs/clients/deepseek-harness.mdx +++ b/docs/clients/deepseek-harness.mdx @@ -3,7 +3,7 @@ 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. +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 diff --git a/packages/deepseek-harness/README.md b/packages/deepseek-harness/README.md index ecae75240..5d0301baf 100644 --- a/packages/deepseek-harness/README.md +++ b/packages/deepseek-harness/README.md @@ -1,6 +1,6 @@ # Context7 Plugin for DeepSeek Harness -The official [Context7](https://context7.com) plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness). It adds native `resolve-library-id` and `query-docs` tools so the harness can retrieve current library documentation and code examples. +The official [Context7](https://context7.com) plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness). It adds native `resolve-library-id` and `query-docs` tools so the harness can retrieve current library documentation and code examples. It also adds invocation guidance to the system prompt so library-specific questions use Context7 automatically. ## Installation diff --git a/packages/deepseek-harness/package.json b/packages/deepseek-harness/package.json index 0e190397c..1f3b13d72 100644 --- a/packages/deepseek-harness/package.json +++ b/packages/deepseek-harness/package.json @@ -61,12 +61,16 @@ }, "peerDependencies": { "@deepseek-ai/cordis": ">=4.0.1 <5", + "@deepseek-ai/dsh-system-prompt": ">=0.1.0-rc.6 <1", "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <1" }, "peerDependenciesMeta": { "@deepseek-ai/cordis": { "optional": true }, + "@deepseek-ai/dsh-system-prompt": { + "optional": true + }, "@deepseek-ai/dsh-tools": { "optional": true } @@ -76,6 +80,7 @@ }, "devDependencies": { "@deepseek-ai/cordis": "4.0.1", + "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6", "@deepseek-ai/dsh-tools": "0.1.0-rc.6", "@types/node": "^25.9.1", "tsup": "^8.5.1", diff --git a/packages/deepseek-harness/src/index.ts b/packages/deepseek-harness/src/index.ts index db9023510..9dbf5edef 100644 --- a/packages/deepseek-harness/src/index.ts +++ b/packages/deepseek-harness/src/index.ts @@ -1,11 +1,12 @@ import type { Context } from "@deepseek-ai/cordis"; +import type { PromptSection } from "@deepseek-ai/dsh-system-prompt"; import { defineTool } from "@deepseek-ai/dsh-tools"; import Schema from "@deepseek-ai/schemastery"; import { fetchLibraryContext, searchLibraries } from "./api.js"; import { formatSearchResults } from "./format.js"; export const name = "context7"; -export const inject = ["tools"]; +export const inject = ["tools", "systemPrompt"]; export interface Config { apiKey?: string; @@ -23,9 +24,27 @@ const QUERY_DESCRIPTION = `Retrieves current documentation and code examples fro Call resolve-library-id first unless the user explicitly provides a library ID in /org/project or /org/project/version format. Use a specific query scoped to one concept and do not include secrets, credentials, personal data, or proprietary code.`; +const CONTEXT7_PROMPT: PromptSection = { + name: "context7:tool-guidance", + order: 120, + text: `Use Context7 to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service, even well-known ones. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use it even when you think you know the answer because training data may not reflect recent changes. Prefer Context7 over web search for library documentation. + +Do not use Context7 for refactoring, writing scripts from scratch, debugging business logic, code review, repository-local code, or general programming concepts. + +Workflow: +1. Call resolve-library-id with the official library name and the user's specific goal unless the user provides a Context7 library ID in /org/project or /org/project/version format. +2. Select the best match using exact name match, description relevance, snippet coverage, source reputation, benchmark score, and requested version. Prefer official sources. +3. Call query-docs with the selected library ID and a specific query scoped to one concept. Split distinct concepts into separate calls with the same library ID unless the question is about how they interact. +4. Answer using the fetched documentation. + +Do not call either tool more than three times per question. Never include secrets, credentials, personal data, or proprietary code in a query.`, +}; + export function apply(ctx: Context, config: Config = {}): void { const apiKey = config.apiKey || process.env.CONTEXT7_API_KEY; + ctx.systemPrompt.section(CONTEXT7_PROMPT); + ctx.tools.register( defineTool({ name: "resolve-library-id", diff --git a/packages/deepseek-harness/test/plugin.test.ts b/packages/deepseek-harness/test/plugin.test.ts index 4f4b96e73..0714f557e 100644 --- a/packages/deepseek-harness/test/plugin.test.ts +++ b/packages/deepseek-harness/test/plugin.test.ts @@ -1,25 +1,12 @@ import { readFile } from "node:fs/promises"; -import { Context, Service } from "@deepseek-ai/cordis"; +import { Context } from "@deepseek-ai/cordis"; +import SystemPrompt, { renderPrompt, type PromptSection } from "@deepseek-ai/dsh-system-prompt"; import ToolRuntime, { type ToolDefinition, type ToolExecutionInput } from "@deepseek-ai/dsh-tools"; import { afterEach, describe, expect, it, vi } from "vitest"; import * as context7 from "../src/index.js"; const { apply } = context7; -class SystemPromptStub extends Service { - constructor(ctx: Context) { - super(ctx, "systemPrompt"); - } - - tools(): () => void { - return () => undefined; - } - - section(): () => void { - return () => undefined; - } -} - function loadTools(config: { apiKey?: string } = {}): Map { const tools = new Map(); const ctx = { @@ -28,6 +15,11 @@ function loadTools(config: { apiKey?: string } = {}): Map undefined; + }, + }, } as unknown as Context; apply(ctx, config); return tools; @@ -90,14 +82,29 @@ describe("Context7 DeepSeek Harness plugin", () => { ); const root = new Context(); - await root.plugin(SystemPromptStub); + await root.plugin(SystemPrompt, { + includeHarnessIdentity: false, + includeRuntimeContext: false, + persona: "", + }); await root.plugin(ToolRuntime); await root.plugin(context7); - expect(root.tools.schemas().map(({ name }) => name)).toEqual([ - "resolve-library-id", - "query-docs", - ]); + const toolNames = ["resolve-library-id", "query-docs"]; + expect(root.tools.schemas().map(({ name }) => name)).toEqual(toolNames); + const assembly = await root.systemPrompt.assemble(); + expect(assembly.tools.map(({ name }) => name).sort()).toEqual([...toolNames].sort()); + const prompt = renderPrompt(assembly); + expect(prompt).toContain( + "Use Context7 to fetch current documentation whenever the user asks about a library" + ); + expect(prompt).toContain( + "Do not use Context7 for refactoring, writing scripts from scratch, debugging business logic" + ); + expect(prompt).toContain( + "Call resolve-library-id with the official library name and the user's specific goal" + ); + expect(context7.inject).toEqual(["tools", "systemPrompt"]); const resolveInput = toolInput("resolve-library-id", { query: "middleware", libraryName: "Next.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a90f58a3f..e117c6c8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,9 @@ importers: '@deepseek-ai/cordis': specifier: 4.0.1 version: 4.0.1 + '@deepseek-ai/dsh-system-prompt': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) '@deepseek-ai/dsh-tools': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(f8724372086ccc1457fc84e7becee2e0) From 22264da24b87a4f987785f6b6977fe72b5eaab31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Fri, 14 Aug 2026 12:43:08 +0300 Subject: [PATCH 4/5] fix: bound Context7 tool requests --- packages/deepseek-harness/src/index.ts | 4 ++ packages/deepseek-harness/test/plugin.test.ts | 72 ++++++++++++++++--- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/packages/deepseek-harness/src/index.ts b/packages/deepseek-harness/src/index.ts index 9dbf5edef..a6dba1355 100644 --- a/packages/deepseek-harness/src/index.ts +++ b/packages/deepseek-harness/src/index.ts @@ -24,6 +24,8 @@ const QUERY_DESCRIPTION = `Retrieves current documentation and code examples fro Call resolve-library-id first unless the user explicitly provides a library ID in /org/project or /org/project/version format. Use a specific query scoped to one concept and do not include secrets, credentials, personal data, or proprietary code.`; +const API_TIMEOUT_MS = 60_000; + const CONTEXT7_PROMPT: PromptSection = { name: "context7:tool-guidance", order: 120, @@ -67,6 +69,7 @@ export function apply(ctx: Context, config: Config = {}): void { schema: { type: "string" }, render: (_args, value) => [{ type: "text", text: value }], }, + timeoutMs: API_TIMEOUT_MS, isConcurrencySafe: () => true, async execute(args, exec) { const response = await searchLibraries(args.query, args.libraryName, apiKey, exec.signal); @@ -98,6 +101,7 @@ export function apply(ctx: Context, config: Config = {}): void { schema: { type: "string" }, render: (_args, value) => [{ type: "text", text: value }], }, + timeoutMs: API_TIMEOUT_MS, isConcurrencySafe: () => true, execute: (args, exec) => fetchLibraryContext(args.query, args.libraryId, apiKey, exec.signal), }) diff --git a/packages/deepseek-harness/test/plugin.test.ts b/packages/deepseek-harness/test/plugin.test.ts index 0714f557e..c6ade10c8 100644 --- a/packages/deepseek-harness/test/plugin.test.ts +++ b/packages/deepseek-harness/test/plugin.test.ts @@ -25,16 +25,32 @@ function loadTools(config: { apiKey?: string } = {}): Map { + const root = new Context(); + await root.plugin(SystemPrompt, { + includeHarnessIdentity: false, + includeRuntimeContext: false, + persona: "", + }); + await root.plugin(ToolRuntime); + await root.plugin(context7, config); + return root; +} + function execution() { return { signal: new AbortController().signal } as never; } -function toolInput(name: string, args: Record): ToolExecutionInput { +function toolInput( + name: string, + args: Record, + signal = new AbortController().signal +): ToolExecutionInput { return { callId: name as never, name, arguments: args, - signal: new AbortController().signal, + signal, }; } @@ -45,7 +61,9 @@ afterEach(() => { describe("Context7 DeepSeek Harness plugin", () => { it("registers both Context7 tools", () => { - expect([...loadTools().keys()]).toEqual(["resolve-library-id", "query-docs"]); + const tools = loadTools(); + expect([...tools.keys()]).toEqual(["resolve-library-id", "query-docs"]); + expect([...tools.values()].map(({ timeoutMs }) => timeoutMs)).toEqual([60_000, 60_000]); }); it("activates the bundle in the real Cordis tool runtime", async () => { @@ -81,14 +99,7 @@ describe("Context7 DeepSeek Harness plugin", () => { '- insert:\n - id: context7\n name: "@upstash/context7-deepseek-harness"\n' ); - const root = new Context(); - await root.plugin(SystemPrompt, { - includeHarnessIdentity: false, - includeRuntimeContext: false, - persona: "", - }); - await root.plugin(ToolRuntime); - await root.plugin(context7); + const root = await loadRuntime(); const toolNames = ["resolve-library-id", "query-docs"]; expect(root.tools.schemas().map(({ name }) => name)).toEqual(toolNames); @@ -125,6 +136,45 @@ describe("Context7 DeepSeek Harness plugin", () => { await root.fiber.dispose(); }); + it("propagates cancellation to Context7 requests", async () => { + let notifyFetchStarted: () => void = () => undefined; + let observedSignal: AbortSignal | undefined; + const fetchStarted = new Promise((resolve) => { + notifyFetchStarted = resolve; + }); + vi.stubGlobal( + "fetch", + vi.fn((_input: URL, init?: RequestInit) => { + const signal = init?.signal; + if (!signal) throw new Error("Missing request signal"); + observedSignal = signal; + notifyFetchStarted(); + return new Promise((_resolve, reject) => { + const abort = () => reject(signal.reason); + if (signal.aborted) abort(); + else signal.addEventListener("abort", abort, { once: true }); + }); + }) + ); + + const root = await loadRuntime(); + const controller = new AbortController(); + const pending = root.tools.execute( + toolInput( + "query-docs", + { libraryId: "/vercel/next.js", query: "middleware" }, + controller.signal + ) + ); + await fetchStarted; + controller.abort(new Error("cancelled")); + + await expect(pending).resolves.toMatchObject({ isError: true }); + expect(observedSignal?.aborted).toBe(true); + + await root.fiber.dispose(); + }); + it("resolves libraries with authentication and formats the response", async () => { vi.stubEnv("CONTEXT7_API_KEY", "ctx7sk-environment"); const fetchMock = vi.fn().mockResolvedValue( From c7e1b3265b4edda9d57c6370c9d823d18b86a17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Fri, 14 Aug 2026 19:44:15 +0300 Subject: [PATCH 5/5] feat: integrate DeepSeek Harness credentials --- .changeset/brave-pandas-document.md | 3 +- docs/clients/deepseek-harness.mdx | 39 ++-- packages/cli/package.json | 3 +- .../src/__tests__/deepseek-command.test.ts | 104 +++++++++++ packages/cli/src/__tests__/deepseek.test.ts | 150 +++++++++++++++ packages/cli/src/commands/setup.ts | 65 +++++++ packages/cli/src/index.ts | 1 + packages/cli/src/setup/deepseek.ts | 175 ++++++++++++++++++ packages/deepseek-harness/README.md | 37 +++- packages/deepseek-harness/package.json | 8 +- packages/deepseek-harness/src/index.ts | 23 +-- packages/deepseek-harness/test/plugin.test.ts | 78 +++++++- packages/deepseek-harness/tsup.config.ts | 7 +- pnpm-lock.yaml | 23 ++- 14 files changed, 663 insertions(+), 53 deletions(-) create mode 100644 packages/cli/src/__tests__/deepseek-command.test.ts create mode 100644 packages/cli/src/__tests__/deepseek.test.ts create mode 100644 packages/cli/src/setup/deepseek.ts diff --git a/.changeset/brave-pandas-document.md b/.changeset/brave-pandas-document.md index d4a3c846a..18fcfdd70 100644 --- a/.changeset/brave-pandas-document.md +++ b/.changeset/brave-pandas-document.md @@ -1,5 +1,6 @@ --- "@upstash/context7-deepseek-harness": minor +"ctx7": minor --- -Add the official Context7 plugin for DeepSeek Harness with native library resolution and documentation tools. +Add the official Context7 plugin for DeepSeek Harness with native library resolution, documentation tools, credential-provider integration, and one-command CLI setup. diff --git a/docs/clients/deepseek-harness.mdx b/docs/clients/deepseek-harness.mdx index 8f11515d5..b841554ca 100644 --- a/docs/clients/deepseek-harness.mdx +++ b/docs/clients/deepseek-harness.mdx @@ -7,37 +7,54 @@ Context7 integrates with [DeepSeek Harness](https://github.com/deepseek-ai/deeps ## Installation -Install the bundle into a DeepSeek Harness profile: +Install the bundle into a DeepSeek Harness profile and store a Context7 API key in the Harness credential provider: ```bash -dsh plugin --profile my-profile add @upstash/context7-deepseek-harness +npx ctx7@latest setup --deepseek headless ``` -The bundle activates Context7 automatically. Verify the composed configuration and start the profile: +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 my-profile --dump-config -dsh --profile my-profile +dsh --profile headless --dump-config +dsh --profile headless ``` -Remove it with `dsh plugin --profile my-profile remove @upstash/context7-deepseek-harness`. +Remove it with `dsh plugin --profile headless remove @upstash/context7-deepseek-harness`. ## Authentication -The plugin works without configuration using Context7's anonymous rate limits. For higher limits and private repositories, create a key in the [Context7 dashboard](https://context7.com/dashboard) and export it before starting the harness: +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_... ``` -You can also set the key when loading the plugin directly in `cordis.yml`: +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 -- name: '@upstash/context7-deepseek-harness' - config: - apiKey: 'ctx7sk_...' +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 diff --git a/packages/cli/package.json b/packages/cli/package.json index d6c7aa652..ab44a4257 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/__tests__/deepseek-command.test.ts b/packages/cli/src/__tests__/deepseek-command.test.ts new file mode 100644 index 000000000..3abe6768d --- /dev/null +++ b/packages/cli/src/__tests__/deepseek-command.test.ts @@ -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 { + 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); + }); +}); diff --git a/packages/cli/src/__tests__/deepseek.test.ts b/packages/cli/src/__tests__/deepseek.test.ts new file mode 100644 index 000000000..a76f16aaf --- /dev/null +++ b/packages/cli/src/__tests__/deepseek.test.ts @@ -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 { + 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; + 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", + "teamcalc", + "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"); + }); +}); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index c5b8a3f75..d51641d4d 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -35,6 +35,11 @@ import { patchStdioApiKey, getJsonServerEntry, } from "../setup/mcp-writer.js"; +import { + installDeepSeekPlugin, + validateDeepSeekProfile, + writeDeepSeekCredential, +} from "../setup/deepseek.js"; type Scope = "global" | "project"; type SetupMode = "mcp" | "cli"; @@ -53,6 +58,7 @@ interface SetupOptions { cli?: boolean; mcp?: boolean; stdio?: boolean; + deepseek?: string | boolean; } function resolveTransport(options: SetupOptions): Transport { @@ -87,6 +93,7 @@ export function registerSetupCommand(program: Command): void { .option("--opencode", "Set up for OpenCode") .option("--codex", "Set up for Codex") .option("--gemini", "Set up for Gemini CLI") + .option("--deepseek [profile]", "Set up DeepSeek Harness (default profile: headless)") .option("--mcp", "Set up MCP server mode") .option("--cli", "Set up CLI + Skills mode (no MCP server)") .option("-p, --project", "Configure for current project instead of globally") @@ -142,6 +149,60 @@ async function resolveAuth(options: SetupOptions): Promise { return { mode: "api-key", apiKey }; } +async function setupDeepSeek(options: SetupOptions): Promise { + if (options.project || options.oauth || options.mcp || options.cli || options.stdio) { + log.error("--deepseek cannot be combined with --project, --oauth, --mcp, --cli, or --stdio."); + process.exitCode = 1; + return; + } + if (getSelectedAgents(options).length > 0) { + log.error("--deepseek cannot be combined with another agent option."); + process.exitCode = 1; + return; + } + + const requestedProfile = typeof options.deepseek === "string" ? options.deepseek : "headless"; + let profile: string; + try { + profile = validateDeepSeekProfile(requestedProfile); + } catch (error) { + log.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + return; + } + + const auth = await resolveAuth(options); + if (!auth?.apiKey) { + log.warn("Setup cancelled"); + return; + } + const spinner = ora("Saving Context7 credential...").start(); + let credentialPath: string; + try { + credentialPath = await writeDeepSeekCredential(auth.apiKey); + spinner.succeed("Saved Context7 credential"); + } catch (error) { + spinner.fail("Failed to save Context7 credential"); + log.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + return; + } + + try { + await installDeepSeekPlugin(profile); + } catch (error) { + log.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + return; + } + + log.blank(); + log.success(`Context7 configured for DeepSeek Harness profile ${pc.bold(profile)}`); + log.plain(` Credential ${pc.dim(credentialPath)}`); + log.blank(); + trackEvent("setup", { mode: "deepseek", profile }); +} + async function resolveMode(options: SetupOptions): Promise { if (options.cli) return "cli"; if (options.mcp || options.yes || options.oauth || options.stdio) return "mcp"; @@ -534,6 +595,10 @@ async function setupCommand(options: SetupOptions): Promise { trackEvent("command", { name: "setup" }); try { + if (options.deepseek !== undefined) { + await setupDeepSeek(options); + return; + } const mode = await resolveMode(options); if (mode === "mcp") { const scope: Scope = options.project ? "project" : "global"; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 2544b9ffb..e45db0c23 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -43,6 +43,7 @@ Examples: ${brand.primary("npx ctx7 setup")} ${brand.primary("npx ctx7 setup --mcp")} ${brand.primary("npx ctx7 setup --cli")} + ${brand.primary("npx ctx7 setup --deepseek headless")} ${brand.dim("# Remove Context7 setup")} ${brand.primary("npx ctx7 remove --cursor")} diff --git a/packages/cli/src/setup/deepseek.ts b/packages/cli/src/setup/deepseek.ts new file mode 100644 index 000000000..d5b9ca5a8 --- /dev/null +++ b/packages/cli/src/setup/deepseek.ts @@ -0,0 +1,175 @@ +import { spawn } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { chmod, mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join, resolve } from "node:path"; +import { Document, parseDocument } from "yaml"; + +export const DEEPSEEK_PLUGIN_PACKAGE = "@upstash/context7-deepseek-harness"; +export const CONTEXT7_CREDENTIAL_REF = "CONTEXT7_API_KEY"; + +const CREDENTIAL_REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; +const PROFILE_PATTERN = /^[A-Za-z0-9._-]+$/; +const LOCK_RETRY_INITIAL_MS = 20; +const LOCK_RETRY_MAX_MS = 200; +const LOCK_TIMEOUT_MS = 2_000; + +function isEnoent(error: unknown): boolean { + return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT"; +} + +function isEexist(error: unknown): boolean { + return (error as NodeJS.ErrnoException | undefined)?.code === "EEXIST"; +} + +export function resolveDshHome( + env: NodeJS.ProcessEnv = process.env, + userHome = homedir(), + currentDirectory = process.cwd() +): string { + const configured = env.DSH_HOME; + const selected = + configured !== undefined && configured.trim().length > 0 ? configured : join(userHome, ".dsh"); + const expanded = + selected === "~" + ? userHome + : selected.startsWith("~/") || selected.startsWith("~\\") + ? join(userHome, selected.slice(2)) + : selected; + return resolve(currentDirectory, expanded); +} + +function credentialsDocument(text: string | undefined, filename: string): Document { + const document = + text === undefined ? new Document({}) : parseDocument(text, { uniqueKeys: true }); + if (document.errors.length > 0) { + throw new Error(`Invalid DeepSeek Harness credentials document at ${filename}`); + } + const root: unknown = document.toJS() ?? {}; + if (typeof root !== "object" || root === null || Array.isArray(root)) { + throw new Error(`DeepSeek Harness credentials document at ${filename} must be a mapping`); + } + for (const [ref, value] of Object.entries(root as Record)) { + if (!CREDENTIAL_REF_PATTERN.test(ref)) { + throw new Error(`Invalid credential reference "${ref}" in ${filename}`); + } + if (typeof value !== "string" || value.length === 0) { + throw new Error(`Credential "${ref}" in ${filename} must have a non-empty string value`); + } + } + return document; +} + +async function withFileLock(filename: string, operation: () => Promise): Promise { + const lockPath = `${filename}.lock`; + const deadline = Date.now() + LOCK_TIMEOUT_MS; + let delay = LOCK_RETRY_INITIAL_MS; + for (;;) { + try { + await writeFile(lockPath, `${process.pid}\n`, { mode: 0o600, flag: "wx" }); + break; + } catch (error) { + if (!isEexist(error)) throw error; + } + if (Date.now() >= deadline) { + throw new Error(`Timed out waiting for the credential writer lock at ${lockPath}`); + } + await new Promise((resolveDelay) => setTimeout(resolveDelay, delay)); + delay = Math.min(delay * 2, LOCK_RETRY_MAX_MS); + } + try { + return await operation(); + } finally { + await rm(lockPath, { force: true }); + } +} + +export async function writeDeepSeekCredential( + apiKey: string, + dshHome = resolveDshHome() +): Promise { + if (!apiKey) throw new Error("Context7 API key cannot be empty"); + const filename = join(dshHome, ".credentials.yaml"); + await mkdir(dshHome, { recursive: true, mode: 0o700 }); + await chmod(dshHome, 0o700); + await withFileLock(filename, async () => { + let existing: string | undefined; + try { + existing = await readFile(filename, "utf8"); + } catch (error) { + if (!isEnoent(error)) throw error; + } + const document = credentialsDocument(existing, filename); + document.setIn([CONTEXT7_CREDENTIAL_REF], apiKey); + const temporary = join( + dshHome, + `.credentials.yaml.${process.pid}.${randomBytes(6).toString("hex")}.tmp` + ); + try { + await writeFile(temporary, document.toString(), { + encoding: "utf8", + flag: "wx", + mode: 0o600, + }); + await chmod(temporary, 0o600); + await rename(temporary, filename); + await chmod(filename, 0o600); + } finally { + await unlink(temporary).catch((error: unknown) => { + if (!isEnoent(error)) throw error; + }); + } + }); + return filename; +} + +export function validateDeepSeekProfile(profile: string): string { + if ( + !PROFILE_PATTERN.test(profile) || + profile === "." || + profile === ".." || + profile === "node_modules" + ) { + throw new Error(`Invalid DeepSeek Harness profile name "${profile}"`); + } + return profile; +} + +export function deepSeekPluginInvocation(profile: string): { + command: string; + args: string[]; +} { + validateDeepSeekProfile(profile); + return { + command: "npx", + args: [ + "--yes", + "@deepseek-ai/dsh", + "plugin", + "--profile", + profile, + "add", + DEEPSEEK_PLUGIN_PACKAGE, + ], + }; +} + +export async function installDeepSeekPlugin(profile: string): Promise { + const invocation = deepSeekPluginInvocation(profile); + const code = await new Promise((resolve, reject) => { + const child = spawn(invocation.command, invocation.args, { + stdio: "inherit", + shell: process.platform === "win32", + }); + child.once("error", reject); + child.once("close", resolve); + }).catch((error: unknown) => { + if (isEnoent(error)) { + throw new Error("The `npx` package runner was not found on PATH"); + } + throw error; + }); + if (code !== 0) { + throw new Error(`DeepSeek Harness plugin installation failed with exit code ${String(code)}`); + } +} diff --git a/packages/deepseek-harness/README.md b/packages/deepseek-harness/README.md index 5d0301baf..4c96c07dc 100644 --- a/packages/deepseek-harness/README.md +++ b/packages/deepseek-harness/README.md @@ -4,39 +4,56 @@ The official [Context7](https://context7.com) plugin for [DeepSeek Harness](http ## Installation +Install the plugin and store a Context7 API key in DeepSeek Harness's credential provider: + +```bash +npx ctx7@latest setup --deepseek headless +``` + +Pass another profile name instead of `headless` when needed. Without an existing Context7 login, setup opens the device authorization flow and creates an API key. To install the bundle without configuring authentication: + ```bash -dsh plugin --profile my-profile add @upstash/context7-deepseek-harness +dsh plugin --profile headless add @upstash/context7-deepseek-harness ``` -The bundle adds the Context7 plugin to the selected profile. Verify the composed configuration and start the profile: +Verify the composed configuration and start the profile: ```bash -dsh --profile my-profile --dump-config -dsh --profile my-profile +dsh --profile headless --dump-config +dsh --profile headless ``` To remove it: ```bash -dsh plugin --profile my-profile remove @upstash/context7-deepseek-harness +dsh plugin --profile headless remove @upstash/context7-deepseek-harness ``` ## Authentication -The plugin works without configuration using Context7's anonymous rate limits. For higher limits, create an API key in the [Context7 dashboard](https://context7.com/dashboard) and set it before launching the harness: +The plugin works without authentication using Context7's anonymous rate limits. For higher limits, it resolves `CONTEXT7_API_KEY` through DeepSeek Harness's credential provider before every request, so credential updates apply without reloading the plugin. + +The setup command stores the key in `$DSH_HOME/.credentials.yaml`, which defaults to `~/.dsh/.credentials.yaml`. You can also provide it through the environment: ```bash export CONTEXT7_API_KEY="your-api-key" ``` -You can also configure the key when loading the plugin directly in `cordis.yml`: +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 -- name: "@upstash/context7-deepseek-harness" - config: - apiKey: "your-api-key" +CONTEXT7_API_KEY: your-api-key ``` +Do not put API keys in `cordis.yml` or `cordis.patch.yml`; composed configuration can be printed and shared during debugging. + ## Tools - `resolve-library-id` finds Context7-compatible library IDs and available versions. diff --git a/packages/deepseek-harness/package.json b/packages/deepseek-harness/package.json index 1f3b13d72..460000ca0 100644 --- a/packages/deepseek-harness/package.json +++ b/packages/deepseek-harness/package.json @@ -61,6 +61,7 @@ }, "peerDependencies": { "@deepseek-ai/cordis": ">=4.0.1 <5", + "@deepseek-ai/dsh-credentials": ">=0.1.0-rc.6 <1", "@deepseek-ai/dsh-system-prompt": ">=0.1.0-rc.6 <1", "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <1" }, @@ -68,6 +69,9 @@ "@deepseek-ai/cordis": { "optional": true }, + "@deepseek-ai/dsh-credentials": { + "optional": true + }, "@deepseek-ai/dsh-system-prompt": { "optional": true }, @@ -75,11 +79,9 @@ "optional": true } }, - "dependencies": { - "@deepseek-ai/schemastery": "3.18.1" - }, "devDependencies": { "@deepseek-ai/cordis": "4.0.1", + "@deepseek-ai/dsh-credentials": "0.1.0-rc.6", "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6", "@deepseek-ai/dsh-tools": "0.1.0-rc.6", "@types/node": "^25.9.1", diff --git a/packages/deepseek-harness/src/index.ts b/packages/deepseek-harness/src/index.ts index a6dba1355..812b679b7 100644 --- a/packages/deepseek-harness/src/index.ts +++ b/packages/deepseek-harness/src/index.ts @@ -1,20 +1,12 @@ import type { Context } from "@deepseek-ai/cordis"; +import { credentialRef } from "@deepseek-ai/dsh-credentials"; import type { PromptSection } from "@deepseek-ai/dsh-system-prompt"; import { defineTool } from "@deepseek-ai/dsh-tools"; -import Schema from "@deepseek-ai/schemastery"; import { fetchLibraryContext, searchLibraries } from "./api.js"; import { formatSearchResults } from "./format.js"; export const name = "context7"; -export const inject = ["tools", "systemPrompt"]; - -export interface Config { - apiKey?: string; -} - -export const Config: Schema = Schema.object({ - apiKey: Schema.string(), -}); +export const inject = ["credentials", "tools", "systemPrompt"]; const RESOLVE_DESCRIPTION = `Resolves a package or product name to a Context7-compatible library ID and returns matching libraries. @@ -25,6 +17,7 @@ const QUERY_DESCRIPTION = `Retrieves current documentation and code examples fro Call resolve-library-id first unless the user explicitly provides a library ID in /org/project or /org/project/version format. Use a specific query scoped to one concept and do not include secrets, credentials, personal data, or proprietary code.`; const API_TIMEOUT_MS = 60_000; +const API_KEY_REF = credentialRef("CONTEXT7_API_KEY"); const CONTEXT7_PROMPT: PromptSection = { name: "context7:tool-guidance", @@ -42,9 +35,7 @@ Workflow: Do not call either tool more than three times per question. Never include secrets, credentials, personal data, or proprietary code in a query.`, }; -export function apply(ctx: Context, config: Config = {}): void { - const apiKey = config.apiKey || process.env.CONTEXT7_API_KEY; - +export function apply(ctx: Context): void { ctx.systemPrompt.section(CONTEXT7_PROMPT); ctx.tools.register( @@ -72,6 +63,7 @@ export function apply(ctx: Context, config: Config = {}): void { timeoutMs: API_TIMEOUT_MS, isConcurrencySafe: () => true, async execute(args, exec) { + const apiKey = (await ctx.credentials.resolve(API_KEY_REF))?.value; const response = await searchLibraries(args.query, args.libraryName, apiKey, exec.signal); if (response.results.length === 0 && response.error) throw new Error(response.error); return formatSearchResults(response); @@ -103,7 +95,10 @@ export function apply(ctx: Context, config: Config = {}): void { }, timeoutMs: API_TIMEOUT_MS, isConcurrencySafe: () => true, - execute: (args, exec) => fetchLibraryContext(args.query, args.libraryId, apiKey, exec.signal), + async execute(args, exec) { + const apiKey = (await ctx.credentials.resolve(API_KEY_REF))?.value; + return fetchLibraryContext(args.query, args.libraryId, apiKey, exec.signal); + }, }) ); } diff --git a/packages/deepseek-harness/test/plugin.test.ts b/packages/deepseek-harness/test/plugin.test.ts index c6ade10c8..feece805c 100644 --- a/packages/deepseek-harness/test/plugin.test.ts +++ b/packages/deepseek-harness/test/plugin.test.ts @@ -1,15 +1,57 @@ import { readFile } from "node:fs/promises"; import { Context } from "@deepseek-ai/cordis"; +import CredentialProvider, { + credentialRef, + type CredentialInfo, + type CredentialRef, + type ResolvedCredential, +} from "@deepseek-ai/dsh-credentials"; import SystemPrompt, { renderPrompt, type PromptSection } from "@deepseek-ai/dsh-system-prompt"; import ToolRuntime, { type ToolDefinition, type ToolExecutionInput } from "@deepseek-ai/dsh-tools"; import { afterEach, describe, expect, it, vi } from "vitest"; import * as context7 from "../src/index.js"; const { apply } = context7; +const API_KEY_REF = credentialRef("CONTEXT7_API_KEY"); -function loadTools(config: { apiKey?: string } = {}): Map { +class MemoryCredentials extends CredentialProvider { + private apiKey?: string; + + constructor(ctx: Context, config: { apiKey?: string } = {}) { + super(ctx); + this.apiKey = config.apiKey; + } + + resolve(ref: CredentialRef): Promise { + return Promise.resolve( + ref === API_KEY_REF && this.apiKey ? { value: this.apiKey, source: "memory" } : undefined + ); + } + + describe(ref: CredentialRef): Promise { + return Promise.resolve({ + configured: ref === API_KEY_REF && Boolean(this.apiKey), + writable: true, + }); + } + + set(ref: CredentialRef, value: string): Promise { + if (ref === API_KEY_REF) this.apiKey = value; + return Promise.resolve(); + } + + unset(ref: CredentialRef): Promise { + if (ref === API_KEY_REF) this.apiKey = undefined; + return Promise.resolve(); + } +} + +function loadTools(apiKey?: string): Map { const tools = new Map(); const ctx = { + credentials: { + resolve: () => Promise.resolve(apiKey ? { value: apiKey, source: "memory" } : undefined), + }, tools: { register(tool: ToolDefinition) { tools.set(tool.name, tool); @@ -21,19 +63,20 @@ function loadTools(config: { apiKey?: string } = {}): Map { +async function loadRuntime(apiKey?: string): Promise { const root = new Context(); + await root.plugin(MemoryCredentials, { apiKey }); await root.plugin(SystemPrompt, { includeHarnessIdentity: false, includeRuntimeContext: false, persona: "", }); await root.plugin(ToolRuntime); - await root.plugin(context7, config); + await root.plugin(context7); return root; } @@ -115,7 +158,7 @@ describe("Context7 DeepSeek Harness plugin", () => { expect(prompt).toContain( "Call resolve-library-id with the official library name and the user's specific goal" ); - expect(context7.inject).toEqual(["tools", "systemPrompt"]); + expect(context7.inject).toEqual(["credentials", "tools", "systemPrompt"]); const resolveInput = toolInput("resolve-library-id", { query: "middleware", libraryName: "Next.js", @@ -176,7 +219,6 @@ describe("Context7 DeepSeek Harness plugin", () => { }); it("resolves libraries with authentication and formats the response", async () => { - vi.stubEnv("CONTEXT7_API_KEY", "ctx7sk-environment"); const fetchMock = vi.fn().mockResolvedValue( new Response( JSON.stringify({ @@ -197,7 +239,7 @@ describe("Context7 DeepSeek Harness plugin", () => { ) ); vi.stubGlobal("fetch", fetchMock); - const tool = loadTools({ apiKey: "ctx7sk-test" }).get("resolve-library-id")!; + const tool = loadTools("ctx7sk-test").get("resolve-library-id")!; const result = await tool.execute({ query: "middleware", libraryName: "Next.js" }, execution()); expect(result).toContain("/vercel/next.js"); @@ -213,8 +255,28 @@ describe("Context7 DeepSeek Harness plugin", () => { ); }); + it("resolves the Context7 credential for every request", async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response("Current documentation")); + vi.stubGlobal("fetch", fetchMock); + const root = await loadRuntime("ctx7sk-first"); + const input = toolInput("query-docs", { + libraryId: "/vercel/next.js", + query: "middleware", + }); + + await root.tools.execute(input); + await root.credentials.set(API_KEY_REF, "ctx7sk-second"); + await root.tools.execute(input); + + expect(fetchMock.mock.calls.map(([, init]) => init.headers.Authorization)).toEqual([ + "Bearer ctx7sk-first", + "Bearer ctx7sk-second", + ]); + + await root.fiber.dispose(); + }); + it("queries documentation without requiring an API key", async () => { - vi.stubEnv("CONTEXT7_API_KEY", ""); const fetchMock = vi.fn().mockResolvedValue(new Response("Current documentation")); vi.stubGlobal("fetch", fetchMock); const tool = loadTools().get("query-docs")!; diff --git a/packages/deepseek-harness/tsup.config.ts b/packages/deepseek-harness/tsup.config.ts index c55202499..82a5a34fc 100644 --- a/packages/deepseek-harness/tsup.config.ts +++ b/packages/deepseek-harness/tsup.config.ts @@ -7,5 +7,10 @@ export default defineConfig({ clean: true, sourcemap: true, target: "node22", - external: ["@deepseek-ai/cordis", "@deepseek-ai/dsh-tools", "@deepseek-ai/schemastery"], + external: [ + "@deepseek-ai/cordis", + "@deepseek-ai/dsh-credentials", + "@deepseek-ai/dsh-system-prompt", + "@deepseek-ai/dsh-tools", + ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e117c6c8a..91ce31c09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: picocolors: specifier: ^1.1.1 version: 1.1.1 + yaml: + specifier: ^2.9.0 + version: 2.9.0 devDependencies: '@types/figlet': specifier: ^1.7.0 @@ -108,14 +111,13 @@ importers: version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(vite@7.2.4(@types/node@22.19.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages/deepseek-harness: - dependencies: - '@deepseek-ai/schemastery': - specifier: 3.18.1 - version: 3.18.1 devDependencies: '@deepseek-ai/cordis': specifier: 4.0.1 version: 4.0.1 + '@deepseek-ai/dsh-credentials': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) '@deepseek-ai/dsh-system-prompt': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) @@ -492,6 +494,13 @@ packages: '@deepseek-ai/cordis': ^4.0.1 '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-credentials@0.1.0-rc.6': + resolution: {integrity: sha512-zyYRs3A9gxfZjZfONzJdMhM0Gzbslpha+FGYkLDRAozgPj0N7mZdE+Qflx2V4WNrCnsSjuvOW0HFBxWtSRUTUw==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants@0.1.0-rc.6': resolution: {integrity: sha512-WfEfOi99a4cpOugRAHTBSTnesLieu3ist1q9PXDXFBHX++K1rAl9+sB7YrdnbB8LH0UOY532gS9xJUYU6w0SLw==} peerDependencies: @@ -3741,6 +3750,12 @@ snapshots: '@deepseek-ai/cordis': 4.0.1 '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-credentials@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)': dependencies: '@deepseek-ai/cordis': 4.0.1