dsh-endnote
Verifieddsh-endnote · v1.0.0 · MIT
EndNote library integration for the DeepSeek Harness: add papers with their open-access PDFs, manage attachments and groups, de-duplicate, and refresh the search index. Registers 2 skills and 7 native tools over one shared Python implementation.
Install
dsh plugin add dsh-endnote Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-endnote
EndNote 文献库集成插件:把论文加进 EndNote(自动附带开放获取 PDF)、给已有文献补/换附件、 管理 group、查重清理、刷新检索索引,以及单独下载开放获取 PDF。
一个装好即用的 DSH 插件:2 个 skill + 7 个原生工具,全部共用同一套 Python 实现。
English | 中文说明见下
English
What it does
An DSH plugin that turns a local EndNote 21 library into something an agent can actually operate:
- Add a paper from a DOI, PMID, title or URL — and attach its open-access PDF.
- Attach a PDF to a reference that is already in the library.
- Manage groups — list them with their members, create a custom group, add or remove records.
- Find and remove duplicates.
- Refresh the search index so new records are findable.
- Download an open-access PDF on its own, falling back to the full text when no PDF is obtainable.
Everything works while EndNote is open. See Design notes for why that is not obvious.
Requirements
- Windows (the EndNote automation is Windows-specific)
- EndNote 21 (20 may work; the discovery logic is version-agnostic)
- Python 3.10+ on
PATH, or pointpythonExeat one - DSH with the
webprofile - Optional:
endnote-mcpif you also want read-only search tools inside conversations. This plugin works without it; the two complement each other.
Install
dsh plugin --profile web add link:<absolute path to this directory>
Then restart DSH — bundle lists are not hot-reloaded.
Uninstall:
dsh plugin --profile web remove dsh-endnote
Configuration
Every field is optional. With none set, the plugin discovers your library
(see scripts/endnote_paths.py):
DSH_ENDNOTE_LIBRARYenvironment variable$DSH_HOME/endnote.json—{"library": "...", "stagingDir": "..."}- The existing
endnote-mcpconfig (%APPDATA%\endnote-mcp\config.yaml), whose XML/PDF paths identify the live library — so a machine already using EndNote MCP needs no configuration at all - A bounded scan of
Documents,Desktop,Downloads,OneDrive/Documents - A conventional fallback, reported honestly rather than guessed at
To set values explicitly, edit the plugin's cordis.patch.yml:
- insert:
- id: dsh-endnote
name: dsh-endnote
config:
library: 'D:\Research\My Library.enl' # empty -> auto-detect
stagingDir: 'D:\Research\pdf-inbox' # empty -> <DSH_HOME>/endnote-staging
endnoteExe: 'C:\Program Files (x86)\EndNote 21\EndNote.EXE'
pythonExe: 'C:\Python312\python.exe'
toolTimeoutMs: 300000
registerTools: true
| Field | Default | Meaning |
|---|---|---|
library |
auto-detect | Path to the EndNote .enl |
stagingDir |
<DSH_HOME>/endnote-staging |
Where downloaded PDFs and generated .enw files go |
endnoteExe |
auto-detect | EndNote.EXE, used to import .enw files |
pythonExe |
the running interpreter | Interpreter for the bundled scripts |
toolTimeoutMs |
300000 |
Per-tool-call timeout (PDF downloads are slow) |
registerTools |
true |
false registers only the skills |
These are passed to Python as DSH_ENDNOTE_* variables, so no library path is
hardcoded — discovery is centralised in scripts/endnote_paths.py. The one
literal that remains is EndNote's own conventional install location
(C:\Program Files (x86)\EndNote 21\EndNote.EXE), used only as a last-resort
fallback and reported as default (not found) when it is used.
Environment variables you may want to set yourself:
| Variable | Default | Meaning |
|---|---|---|
DSH_ENDNOTE_LIBRARY |
(unset) | Overrides library; checked first during discovery |
DSH_ENDNOTE_PROXY |
(unset) | HTTP(S) proxy for metadata and PDF requests. Unset means direct. Setting it relaxes TLS verification for the proxied connection only — an intercepting proxy presents its own CA, but ordinary direct requests stay fully verified |
DSH_ENDNOTE_CONTACT |
[email protected] |
Contact address sent to Unpaywall, which asks callers to identify themselves. Set this to your own address if you want to be a good API citizen; the placeholder is RFC 2606-reserved and some services reject implausible addresses |
There is no pip install step: the bundled scripts use only the Python
standard library (sqlite3, ssl, urllib, ctypes). endnote-mcp is optional
and only needed to make records searchable.
Tools
| Tool | Purpose |
|---|---|
endnote_add |
Add a paper from a DOI/PMID/title/URL, attaching its OA PDF. Can also file it into a group and reindex in the same call |
endnote_attach |
Attach, list or remove a PDF on an existing reference |
endnote_groups |
List groups and members; create a group, add/remove records |
endnote_dedupe |
Find duplicates and trash them (reversible) |
endnote_status |
Health check: install, library sources, index sync, APIs |
paper_pdf |
Download an OA PDF, or its full text |
endnote_refresh |
Re-export the library and rebuild the search index |
Adding a paper files it into a group automatically. By default endnote_add
chooses the best-matching existing custom group and files the record there:
python scripts/endnote_add.py 10.1038/s41586-020-2649-2 --refresh
The choice is made from the paper's own title/abstract/keywords, compared against the titles and keywords of each custom group's existing members — those members are evidence of what a group is for, whereas a group name alone is not ("Chapter drafts" vs a paper on phage encapsulation is genuinely ambiguous). Only custom groups are considered, because a derived group's membership is computed by EndNote and cannot be written.
It declines when the evidence is thin. If no group matches convincingly, or two groups score within 10% of each other, the paper is still added but not filed, and the reason is printed. A paper silently filed into the wrong group is worse than one not filed: you will not notice until you go looking for it.
--group "Exact name" # force a group (a typo refuses rather than creating one)
--group none # or --no-group: do not file at all
--group-create # with an explicit name, create the group if missing
This follows the same reasoning as the combined call: tool round trips cost far
more than the work inside them — measured at ~26 s of wall clock per call against
~2 s of real work per step — so the follow-up steps run inside endnote_add.
That single call imports the paper, waits for EndNote to apply the (asynchronous)
import, files the record, and rebuilds the search index. --wait SECONDS bounds
the wait for the import (default 30), after which the tool reports what did not
happen instead of hanging.
Skills: endnote-add (library workflows and their traps) and paper-pdf
(publisher-by-publisher PDF retrieval strategy).
Design notes
Things that were established by experiment, not assumption — each shaped the code:
EndNote holds an exclusive OS lock on .enl while it runs. Not mode=ro,
not even immutable=1; polling showed it never releases. So all reads go through
<lib>.Data/sdb/sdb.eni, an unlocked working copy with the same tables. That is
what makes index refresh possible without closing EndNote.
%> (the .enw "Link to PDF" field) only attaches a file when it holds a
local path. A URL there creates a dead link and no attachment. So the importer
downloads first and points %> at the file — and omits the field entirely when
no PDF was obtained, rather than leaving a broken link.
A minimized EndNote silently ignores the import. The .enl stayed
byte-identical until the window was foregrounded. The plugin restores and focuses
the window itself.
Writing refs needs two SQL shims. EndNote defines an AFTER UPDATE
trigger calling EN_MAKE_SORT_KEY() and writing refs_ord (custom collation).
The dedupe tool registers a placeholder collation and replays the existing
sort key rather than reimplementing EndNote's algorithm; a lookup miss raises
instead of writing a corrupt key.
Group membership is little-endian. members is a 4-byte prefix plus
little-endian uint32 record ids. The big-endian reading yields plausible-looking
but impossible ids; little-endian yields real ones, stable across every backup
tested. endnote_groups.py --probe prints both so the assumption stays checkable.
Only custom groups are editable. spec XML carries a <rules> element:
TYPE;3 is a manual group; TYPE;6 is an online-search group; others are smart
groups. Derived membership is refused rather than overwritten.
Index refresh must be --full. The incremental index command upserts but
never prunes, so deleted records stay searchable forever.
PDF retrieval: publishers differ
Measured on the author's machine:
| Works | Blocked (403 to non-browser clients) |
|---|---|
| arxiv.org, link.springer.com, nature.com, frontiersin.org, bmcmicrobiol.biomedcentral.com, institutional repositories | mdpi.com, europepmc.org, pubs.acs.org, pubs.rsc.org |
Repository mirrors rescue blocked papers (an ACS article 403s at the publisher
but downloads from an institutional repository). When every PDF route fails,
Europe PMC's fullTextXML still works, so the file is replaced by full text.
Verification
Run from the repository root:
python tools/audit_publication.py # no personal paths in shipped files
python tools/audit_bibliography.py # no library DOI / group name / surname in examples
python tools/verify_package.py # asks npm what the tarball would contain
python tools/test_publication.py # the plugin works when extracted, as a consumer gets it
python tools/test_tls_posture.py # direct requests stay TLS-verified with a proxy set
python tools/test_discovery.py # library auto-discovery
node tools/test_group_selection.mjs # automatic group filing, all forms
node tools/validate_patch.mjs # patch YAML + config plumbing
node tools/smoke_test.mjs # plugin logic against a fake host context
node tools/e2e_test.mjs # really executes the tools (read-only)
python tools/check_skills.py skills # bundled skills are well formed
Some checks need a readable EndNote library, and say so — they report SKIP with a
reason rather than a failure, because an unreadable library is not a code
regression. test_group_selection.mjs exits 2 when it skipped anything, so that
cannot be mistaken for a clean pass; pass --allow-skip to accept it.
The full list, with what each one is for, is in CONTRIBUTING.md.
License
MIT — see LICENSE.
AI usage disclosure
Substantial portions of this plugin were written by an AI coding agent, working under human direction and review. Read AI-USAGE.md before relying on it for anything destructive: it states exactly which parts are machine-written, what was verified experimentally versus assumed, and where the author's confidence is lower.
中文说明
这是什么
把本地 EndNote 21 文献库变成 agent 可操作的 DSH 插件:
- 加入文献 —— 给 DOI / PMID / 标题 / URL,自动附上开放获取 PDF
- 补附件 —— 给库里已有的文献贴 PDF
- 管理 group —— 列出 group 及成员、新建自定义 group、加入 / 移出
- 查重清理 —— 找重复记录并移入回收站(可恢复)
- 刷新索引 —— 让新记录能被检索到
- 单独下载 OA PDF —— 取不到 PDF 时回退全文
全程无需关闭 EndNote。
安装
dsh plugin --profile web add link:<本目录绝对路径>
# 然后重启 DSH(bundle 列表不热更新)
配置
全部字段可留空。留空时插件会自动发现文献库(依次尝试环境变量、
$DSH_HOME/endnote.json、已有的 endnote-mcp 配置、常见目录扫描)。
已用过 endnote-mcp 的机器无需任何配置。
具体字段见上方英文表格;值通过 DSH_ENDNOTE_* 环境变量传给脚本,
脚本内零硬编码路径。
两点必须知道的限制
- EndNote 运行期间独占锁定
.enl,所以读取走未加锁的工作副本<lib>.Data\sdb\sdb.eni。这是"刷新索引不必关闭 EndNote"的原因。 - 只有自定义 group 可编辑。在线检索组与智能组的成员由 EndNote 计算, 插件会拒绝写入而不是写一个 EndNote 会忽略的成员表。
AI 使用声明
本插件相当大一部分代码由 AI 编程 agent 在人类指导下撰写与审阅。 用于破坏性操作前请先读 AI-USAGE.md:其中说明了哪些部分是 机器生成、哪些结论经过实测、哪些属于假设,以及作者信心较低的环节。
许可
MIT,见 LICENSE。