Back to home@jwilson411

dsh-arxiv

DeepSeek Harness plugin: tiny read-only arXiv search + abstract fetch (Atom API, no PDF ingest)

Stars
0
Language
JavaScript
Created
Aug 29, 2026
Updated
Aug 29, 2026
GitHub repo

Introduction

dsh-arxiv

A small DeepSeek Harness function plugin for looking papers up on arXiv. It registers exactly two model-facing tools — arxiv_search and arxiv_get — over arXiv's public Atom API, and owns nothing else.

It is deliberately not a science platform. There is no PDF ingest, no full-text extraction, no embedding store, no citation graph, no reference manager. It answers two questions — what has been written about this? and what is this paper? — and hands back metadata and the abstract. If you want the PDF, the tools tell you where it lives; fetching it is somebody else's job.

Read-only, no API key, no credentials, no state.

Install

dsh plugin --profile web add github:jwilson411/dsh-arxiv

dsh plugin forwards to pnpm inside $DSH_HOME/profiles/web, then reconciles the profile against the installed state: because this package's manifest declares dsh.bundle.patch, it is appended to the profile manifest's ordered dsh.profile.bundles list and its patch becomes a layer.

Remove it the same way, with remove in place of add.

Pinned DSH release candidate

This package is written and tested against the pinned release candidate 0.1.1-rc.2 — the current @deepseek-ai/dsh release and the matching @deepseek-ai/dsh-tools@0.1.1-rc.2, which is pinned exactly in devDependencies so tests run against one known API. The peer range is ^0.1.1-rc.2, matching how the harness's own tool packages declare it.

Note that @deepseek-ai/dsh-tools's npm latest tag still points at the older 0.0.1-rc.1; the 0.1.1-rc.2 line is published under next. Pin explicitly rather than relying on the tag.

What it registers

Cordis plugin idarxiv (the row id in cordis.patch.yml)
Injectstools — a hard dependency; the plugin waits rather than degrading
ToolArgumentsReturns
arxiv_searchquery (string, required), max_results (integer, optional, 1–25, default 5){ query, search_query, max_results, returned, papers[], plugin }
arxiv_getid (string, required){ requested_id, paper, plugin }

Every paper is the same object in both tools:

{
  "id": "1706.03762v7",
  "title": "Attention Is All You Need",
  "authors": ["Ashish Vaswani", "Noam Shazeer", "..."],
  "published": "2017-06-12T17:57:34Z",
  "abstract": "The dominant sequence transduction models are based on …",
  "pdf_url": "https://arxiv.org/pdf/1706.03762v7",
  "abs_url": "https://arxiv.org/abs/1706.03762v7"
}

pdf_url is reported so a caller can open it. This plugin never fetches it.

arxiv_search sends a plain phrase across all fields. A query that opens with one of arXiv's field prefixes (ti:, au:, abs:, cat:, all:, …) is passed through as written, so au:Hinton AND cat:cs.LG works. max_results is clamped into 1–25 rather than rejected — this is a lookup tool, not a harvester.

arxiv_get accepts 1706.03762, 1706.03762v7, arxiv:1706.03762, an /abs/ or /pdf/ URL, and the pre-2007 hep-th/9901001 form; all normalize to the bare identifier. A version suffix is kept, since dropping it would quietly answer about a different revision than the one asked for. arXiv answers an unknown id with HTTP 200 and an error entry, so arxiv_get checks and throws ARXIV_NOT_FOUND rather than reporting a silent nothing.

Talking to arXiv

One endpoint, over HTTPS, with no key: https://export.arxiv.org/api/query. Nothing scrapes an HTML page.

Every request is bounded twice and fails closed on either bound — an AbortSignal deadline (15s) and a response byte cap (2 MiB) enforced while the body streams, so an oversized response is abandoned rather than buffered. Both are configurable from the plugin's patch row:

- insert:
    - id: arxiv
      name: dsh-arxiv
      config:
        timeoutMs: 15000
        maxBytes: 2097152

Failures carry a stable codeARXIV_TIMEOUT, ARXIV_HTTP_ERROR, ARXIV_RESPONSE_TOO_LARGE, ARXIV_NOT_FOUND, ARXIV_BAD_ID, ARXIV_BAD_QUERY, ARXIV_PARSE_ERROR, ARXIV_UNREACHABLE — so a caller can tell a timeout from a missing paper without matching on prose.

Requests identify themselves with a User-Agent naming the plugin and its repository, as arXiv's terms of use ask.

Headless use

The tool factories take the fetch to use, so you can drive either tool from a plain Node script without booting a profile:

// titles.mjs — node titles.mjs
import { createArxivSearchTool } from 'dsh-arxiv'

// Omit `fetch` to use the global one and hit the real API. Doing so sends live
// requests to export.arxiv.org: be polite, and read the terms linked below.
const search = createArxivSearchTool({ fetch: globalThis.fetch })

const exec = { signal: AbortSignal.timeout(20_000) }
const result = await search.execute({ query: 'au:Hinton AND cat:cs.LG', max_results: 5 }, exec)

console.log(`${result.returned} paper(s) for ${result.search_query}`)
for (const paper of result.papers) {
  console.log(`  ${paper.id}  ${paper.title}`)
}

Swap in a stub fetch and the same script runs entirely offline — this is exactly how the test suite drives it:

const canned = async () =>
  new Response(await readFile('feed.xml', 'utf8'), {
    status: 200,
    headers: { 'content-type': 'application/atom+xml' },
  })

const search = createArxivSearchTool({ fetch: canned })

execute validates its arguments before the body runs, so a bad call rejects with ToolArgsError and never reaches the network.

arXiv's terms, and whose text this is

This plugin is an API client. It is not affiliated with or endorsed by arXiv.

Use of the API is governed by the arXiv API Terms of Use. In short: identify your client, do not hammer the service, and respect the metadata's licensing.

Abstracts and metadata returned by these tools are arXiv's and their authors', not this plugin's. They are passed through unaltered except for collapsing the newlines arXiv wraps prose with. Attribute them to arXiv and to the paper's authors, and cite the paper — the abs_url on every result is the place to point. Thank you to arXiv for making the API and its data openly available.

Layout

package.json        manifest + `dsh.bundle.patch` — what makes this a bundle
cordis.patch.yml    the bundle's patch layer: one insert, one plugin row
src/index.js        the plugin: `name`, `inject`, `apply(ctx)`, both tools
src/arxiv.js        the HTTP client: endpoint, bounds, id and query handling
src/atom.js         a small dedicated parser for arXiv's Atom feed
src/errors.js       `ArxivError` and its codes
test/               offline tests, with checked-in feed fixtures

The Atom parser is hand-written against arXiv's one documented response shape rather than pulling in a general XML library, which is why this package has no runtime dependencies at all.

Tests

npm install
npm test

Offline by construction. Every test injects a fetch double answering from a checked-in fixture, and each test file replaces the global fetch with a guard that throws — so a code path that forgot its injected fetch fails as a test failure instead of a live request to export.arxiv.org. npm test opens no socket and reads no credential.

The suite covers registration, the Atom parser against captured real feeds, clamping, id normalization, both render projections, output-schema validation, loud failure on timeout, oversize, non-2xx, unknown id and bad arguments, and the manifest/patch wiring.

CI (.github/workflows/ci.yml) runs the same two commands on Node 22 and 24 with contents: read and no secrets.

License

MIT — see LICENSE. Covers this plugin's code only, not the arXiv content it retrieves.