dsh-zotero 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +180 -0
  3. package/README.md +180 -0
  4. package/cordis.patch.yml +6 -0
  5. package/lib/attachments.d.ts +50 -0
  6. package/lib/attachments.d.ts.map +1 -0
  7. package/lib/attachments.js +95 -0
  8. package/lib/attachments.js.map +1 -0
  9. package/lib/client.d.ts +49 -0
  10. package/lib/client.d.ts.map +1 -0
  11. package/lib/client.js +159 -0
  12. package/lib/client.js.map +1 -0
  13. package/lib/command.d.ts +18 -0
  14. package/lib/command.d.ts.map +1 -0
  15. package/lib/command.js +44 -0
  16. package/lib/command.js.map +1 -0
  17. package/lib/config.d.ts +67 -0
  18. package/lib/config.d.ts.map +1 -0
  19. package/lib/config.js +78 -0
  20. package/lib/config.js.map +1 -0
  21. package/lib/constants.d.ts +13 -0
  22. package/lib/constants.d.ts.map +1 -0
  23. package/lib/constants.js +18 -0
  24. package/lib/constants.js.map +1 -0
  25. package/lib/errors.d.ts +67 -0
  26. package/lib/errors.d.ts.map +1 -0
  27. package/lib/errors.js +107 -0
  28. package/lib/errors.js.map +1 -0
  29. package/lib/evidence.d.ts +35 -0
  30. package/lib/evidence.d.ts.map +1 -0
  31. package/lib/evidence.js +81 -0
  32. package/lib/evidence.js.map +1 -0
  33. package/lib/index.d.ts +13 -0
  34. package/lib/index.d.ts.map +1 -0
  35. package/lib/index.js +12 -0
  36. package/lib/index.js.map +1 -0
  37. package/lib/normalize.d.ts +114 -0
  38. package/lib/normalize.d.ts.map +1 -0
  39. package/lib/normalize.js +327 -0
  40. package/lib/normalize.js.map +1 -0
  41. package/lib/prompt.d.ts +15 -0
  42. package/lib/prompt.d.ts.map +1 -0
  43. package/lib/prompt.js +28 -0
  44. package/lib/prompt.js.map +1 -0
  45. package/lib/provider-local.d.ts +111 -0
  46. package/lib/provider-local.d.ts.map +1 -0
  47. package/lib/provider-local.js +514 -0
  48. package/lib/provider-local.js.map +1 -0
  49. package/lib/refs.d.ts +39 -0
  50. package/lib/refs.d.ts.map +1 -0
  51. package/lib/refs.js +80 -0
  52. package/lib/refs.js.map +1 -0
  53. package/lib/service.d.ts +76 -0
  54. package/lib/service.d.ts.map +1 -0
  55. package/lib/service.js +154 -0
  56. package/lib/service.js.map +1 -0
  57. package/lib/tools/attachment.d.ts +78 -0
  58. package/lib/tools/attachment.d.ts.map +1 -0
  59. package/lib/tools/attachment.js +73 -0
  60. package/lib/tools/attachment.js.map +1 -0
  61. package/lib/tools/export.d.ts +101 -0
  62. package/lib/tools/export.d.ts.map +1 -0
  63. package/lib/tools/export.js +133 -0
  64. package/lib/tools/export.js.map +1 -0
  65. package/lib/tools/get.d.ts +261 -0
  66. package/lib/tools/get.d.ts.map +1 -0
  67. package/lib/tools/get.js +191 -0
  68. package/lib/tools/get.js.map +1 -0
  69. package/lib/tools/retrieve.d.ts +113 -0
  70. package/lib/tools/retrieve.d.ts.map +1 -0
  71. package/lib/tools/retrieve.js +142 -0
  72. package/lib/tools/retrieve.js.map +1 -0
  73. package/lib/tools/search.d.ts +222 -0
  74. package/lib/tools/search.d.ts.map +1 -0
  75. package/lib/tools/search.js +243 -0
  76. package/lib/tools/search.js.map +1 -0
  77. package/lib/types.d.ts +294 -0
  78. package/lib/types.d.ts.map +1 -0
  79. package/lib/types.js +13 -0
  80. package/lib/types.js.map +1 -0
  81. package/package.json +88 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wenjie Xu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,180 @@
1
+ # dsh-zotero
2
+
3
+ <p align="center">
4
+ <b>English</b> · <a href="README.md"><b>中文</b></a>
5
+ </p>
6
+
7
+ Let agents search, read, and cite your local [Zotero](https://www.zotero.org) library: find papers, browse notes and annotations, pull evidence by question, open the source document, generate citations.
8
+
9
+ Describe what you need in a session and the Agent calls the tools below as needed. The only manual command is `/zotero status`.
10
+
11
+ ## Tools
12
+
13
+ | Tool | Purpose |
14
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `zotero_search` | Discover: search the library by title/creator/year, or indexed full text with `everything`; scope to a collection or saved search. |
16
+ | `zotero_get` | Inspect: read one item's structured core metadata, optionally with manifests and previews of its notes, annotations, and attachments. |
17
+ | `zotero_retrieve` | Evidence: return the most relevant bounded evidence passages (annotations, notes, abstract, full-text chunks) for a query. |
18
+ | `zotero_attachment` | Source: resolve an item or attachment ref to the original attachment's verified on-disk path or linked URL. An item ref yields the best attachment Zotero itself picks; an attachment ref pinpoints one. |
19
+ | `zotero_export` | Cite: let Zotero's own citation/export machinery produce citations, a CSL bibliography, or `bibtex` / `biblatex` / `ris` / `csljson`. |
20
+
21
+ Every tool returns reusable refs of the form `zotero://user/0/<item|attachment|annotation|collection|search>/<KEY>`, optionally qualified with `?server=<id>`. Later calls chain through these refs. The Zotero 10+ `server` qualifier binds a ref to the database that produced it, so a database switch blocks stale refs instead of misreading them.
22
+
23
+ ## Usage example
24
+
25
+ The Agent moves down the ladder as a request deepens. A typical conversation:
26
+
27
+ > User: "Find papers about FlashAttention."
28
+ > Agent → `zotero_search`, returning candidates with refs.
29
+ >
30
+ > User: "What is the first one? Have I read it before?"
31
+ > Agent → `zotero_get`: metadata, 17 annotations, 2 notes, limited previews.
32
+ >
33
+ > User: "What did I think about its evaluation?"
34
+ > Agent → `zotero_retrieve(query:"evaluation", sources:["annotations","notes"])`, returning matching note and annotation evidence.
35
+ >
36
+ > User: "How does the paper itself explain memory efficiency?"
37
+ > Agent → `zotero_retrieve(query:"memory efficiency", sources:["fulltext","abstract"])`, returning abstract and full-text passages.
38
+ >
39
+ > User: "Show me the original PDF."
40
+ > Agent → `zotero_attachment(item ref)`, returning the verified file path; if the composition has a PDF/file reader, the Agent hands it off for further analysis.
41
+ >
42
+ > User: "Generate an APA bibliography for these three."
43
+ > Agent → `zotero_export(format:"bibliography", style:"apa")`.
44
+
45
+ ## Command
46
+
47
+ `/zotero status` reports connectivity, API/schema versions, and the database identity (Server ID, Zotero 10+). This is the only health check. Ordinary calls fail with typed domain errors.
48
+
49
+ ## Limits
50
+
51
+ - Read-only library: V1 has no path that modifies items, notes, tags, or collections.
52
+ - Full-text evidence depends on Zotero's index: `everything` search and `retrieve` full-text passages both require indexing.
53
+ - Attachment depth depends on the harness composition: `zotero_attachment` returns the file location; reading that PDF further needs a matching file/PDF capability.
54
+ - Evidence ranking is term-based relevance, not embedding or semantic search.
55
+
56
+ ## Requirements
57
+
58
+ - Zotero desktop with the local API enabled: **Settings → Advanced → "Allow other applications on this computer to communicate with Zotero"**.
59
+ - Read access is unauthenticated on `http://127.0.0.1:23119/api`. V1 has no path that modifies library data (items, notes, tags, collections).
60
+ - Zotero ≥ 7 speaking local API version 3. Upgrade if the status command reports a version mismatch.
61
+
62
+ ## Install
63
+
64
+ ### By package name
65
+
66
+ ```sh
67
+ dsh plugin --profile <name> add dsh-zotero
68
+ ```
69
+
70
+ The tarball ships the built `lib/`; no local build is needed.
71
+
72
+ ### From a local tarball
73
+
74
+ ```sh
75
+ cd dsh-zotero
76
+ npm pack
77
+ dsh plugin --profile <name> add ./dsh-zotero-0.1.0.tgz
78
+ ```
79
+
80
+ `npm pack` runs `prepare` first, so the tarball carries a fresh `lib/`. Use this for unpublished or local trial installs.
81
+
82
+ ### From the GitHub source
83
+
84
+ ```sh
85
+ dsh plugin --profile <name> add github:Vncntvx/dsh-zotero
86
+ ```
87
+
88
+ A git install fetches sources instead of built artifacts, so pnpm installs the dependencies and then runs this package's `prepare` to build in place (TypeScript and `@types/node` live in `dependencies`). pnpm ≥ 10 refuses to run a git dependency's `prepare` by default, so the first `add` fails and points at the fix: add the package key to the profile's `pnpm-workspace.yaml` and re-run:
89
+
90
+ ```yaml
91
+ allowBuilds:
92
+ dsh-zotero: true
93
+ ```
94
+
95
+ `allowBuilds` grants permission to run the package's code on your machine at install time. Only allow sources you trust, and prefer pinning a commit (`github:Vncntvx/dsh-zotero#<sha>`).
96
+
97
+ The plugin mounts as id `zotero` and takes effect on the next dsh start. After installing or enabling the plugin, start a new session if the current one was created before the plugin loaded, so the Agent picks up the Zotero tools.
98
+
99
+ ## Configuration
100
+
101
+ All values are `Config` fields changeable from the bundle's `config` block (e.g. via `dsh plugin config`). Defaults are shown.
102
+
103
+ | Field | Default | Meaning |
104
+ | ---------------------- | ---------------------------- | ------------------------------------------------------------ |
105
+ | `baseUrl` | `http://127.0.0.1:23119/api` | Local API base URL. Plain loopback HTTP only. |
106
+ | `provider` | `local` | Provider id to select. |
107
+ | `timeoutMs` | `5000` | Per-request provider deadline. |
108
+ | `maxSearchResults` | `20` | Upper bound for `zotero_search` `limit`. |
109
+ | `maxEvidenceChars` | `6000` | Total character budget for retrieved evidence. |
110
+ | `maxEvidencePassages` | `4` | Upper bound for evidence passage counts. |
111
+ | `maxDetailChars` | `3000` | Character budget for `zotero_get` abstract previews. |
112
+ | `maxNoteChars` | `2000` | Character budget per note preview in `zotero_get`. |
113
+ | `maxNoteRecords` | `50` | Upper bound for note records returned by `zotero_get`. |
114
+ | `maxAnnotationRecords` | `100` | Upper bound for annotation records returned by `zotero_get`. |
115
+ | `fulltextChunkWords` | `200` | Word count per full-text passage entering evidence ranking. |
116
+ | `maxFulltextChars` | `250000` | Full text accepted into evidence ranking. |
117
+ | `maxResponseBytes` | `16777216` | Streaming byte bound for every API response. |
118
+ | `maxExportChars` | `1000000` | Export output hard limit. Never mid-truncated. |
119
+ | `defaultStyle` | `apa` | CSL style for citation/bibliography formats. |
120
+ | `defaultLocale` | `en-US` | CSL locale for citation/bibliography formats. |
121
+
122
+ ## Development
123
+
124
+ ### Commands
125
+
126
+ ```sh
127
+ npm install # uses a local npm cache
128
+ npm test # unit tests (mock Zotero server)
129
+ npm run test:coverage # 100% coverage gate on src/
130
+ npm run typecheck # tsc --noEmit, app + test projects
131
+ npm run build # emits lib/
132
+ npm run format # prettier --write across the repo
133
+ npm run format:check # verify formatting (run before committing)
134
+ ```
135
+
136
+ Integration tests run against a live Zotero and stay skipped unless enabled:
137
+
138
+ ```sh
139
+ npm run test:integration
140
+ # or: ZOTERO_INTEGRATION=1 npx vitest run tests/integration/zotero.integration.spec.ts
141
+ ```
142
+
143
+ ### Running locally
144
+
145
+ #### From a dsh source checkout
146
+
147
+ Build the checkout once (`pnpm install && pnpm run build`), then load the plugin source through the dev overlay:
148
+
149
+ ```sh
150
+ pnpm dsh web --patch ./dsh-zotero/dev.cordis.yml
151
+ ```
152
+
153
+ `dev.cordis.yml` points the plugin entry at the absolute `src/index.ts`. The dsh source launch loads that TypeScript entry through tsx, so the plugin requires no prebuild. Update the absolute path when the checkout location differs.
154
+
155
+ #### With the npm-installed dsh
156
+
157
+ **Resident instance**: pack a tarball and install it into a profile. The plugin runs from the tarball copy; code updates require re-packing and re-installing. Verify with the production-stack smoke:
158
+
159
+ ```sh
160
+ npm pack
161
+ dsh plugin --profile <name> add ./dsh-zotero-0.1.0.tgz
162
+ cd ~/.dsh/profiles/<name>
163
+ node --input-type=module < /path/to/dsh-zotero/scripts/smoke.mjs
164
+ ```
165
+
166
+ Run the smoke inside the profile directory, so bare imports resolve from the profile's flat `node_modules`. It verifies `status`, `search`, `get`, `retrieve`, `export`, the policy prompt section, and tool registration; `SMOKE PASS` indicates the packed plugin passes the installed-path checks.
167
+
168
+ **Dev instance (hot swap)**: the `dev-lib.cordis.yml` overlay disables the profile's tarball copy (id `zotero`), inserts `zotero-dev` at this checkout's `lib/index.js`, and re-enables HMR. The production web profile disables loader HMR, and the HMR watch base sits in the profile directory, so the overlay sets `base` explicitly. When the build output changes, HMR disposes the old instance and re-constructs the plugin in the same process; dsh keeps running:
169
+
170
+ ```sh
171
+ cd /Volumes/Work/deepseek-harness/dsh-zotero
172
+ npm run dev & # tsc --watch: rebuild lib on src changes
173
+ dsh web --patch ./dev-lib.cordis.yml --port 3307
174
+ ```
175
+
176
+ Hot swap affects only the instance started with `--patch`; the resident instance keeps running the tarball version.
177
+
178
+ ## License
179
+
180
+ MIT. See [LICENSE](./LICENSE).
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # dsh-zotero
2
+
3
+ <p align="center">
4
+ <a href="README.en.md"><b>English</b></a> · <b>中文</b>
5
+ </p>
6
+
7
+ 让 Agents 搜索、阅读并引用你的本地 [Zotero](https://www.zotero.org) 文献库:找文献、查看笔记与批注、按问题取证、打开原文、生成引用。
8
+
9
+ 在会话里用自然语言描述需求,Agent 自动按需调用下面的工具;唯一的手动命令是 `/zotero status`。
10
+
11
+ ## 工具
12
+
13
+ | 工具 | 用途 |
14
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
15
+ | `zotero_search` | 发现:按标题/作者/年份搜索库里的资料,`everything` 模式连全文索引一起搜;可限定某个分类或已保存搜索。 |
16
+ | `zotero_get` | 检查:读取一条资料的结构化核心元数据,可选检查笔记、注释、附件的清单与预览。 |
17
+ | `zotero_retrieve` | 取证:按问题返回最相关的有界证据片段(注释、笔记、摘要、全文分块)。 |
18
+ | `zotero_attachment` | 原文:解析条目或附件 ref,返回原始附件已验证的磁盘路径或链接 URL。条目 ref 取 Zotero 自选的最佳附件,附件 ref 指定单个附件。 |
19
+ | `zotero_export` | 引用:让 Zotero 按自己的 citation/export 能力生成结果(引用、CSL 参考文献表、`bibtex` / `biblatex` / `ris` / `csljson`)。 |
20
+
21
+ 每个工具都返回形如 `zotero://user/0/<item|attachment|annotation|collection|search>/<KEY>` 的可复用 ref,后续操作都通过它串联。Zotero 10+ 的 `?server=<id>` 限定符把 ref 绑定到产生它的数据库身份,数据库切换时阻止误读旧 ref。
22
+
23
+ ## 使用示例
24
+
25
+ Agent 按需求逐层深入,一段典型对话:
26
+
27
+ > 用户:「帮我找 FlashAttention 相关论文」
28
+ > Agent → `zotero_search`,返回候选条目与 ref。
29
+ >
30
+ > 用户:「第一篇是什么?我以前读过吗?」
31
+ > Agent → `zotero_get`:元数据、17 条批注、2 条笔记与有限预览。
32
+ >
33
+ > 用户:「我当时对 evaluation 有什么意见?」
34
+ > Agent → `zotero_retrieve(query:"evaluation", sources:["annotations","notes"])`,返回相关笔记与批注证据。
35
+ >
36
+ > 用户:「论文自己怎么解释 memory efficiency?」
37
+ > Agent → `zotero_retrieve(query:"memory efficiency", sources:["fulltext","abstract"])`,返回摘要与全文片段。
38
+ >
39
+ > 用户:「我要看原 PDF」
40
+ > Agent → `zotero_attachment(条目 ref)`,返回已验证的文件路径;若当前 Harness 配置了 PDF/file 读取能力,再交给该能力继续分析。
41
+ >
42
+ > 用户:「把这三篇生成 APA 参考文献表」
43
+ > Agent → `zotero_export(format:"bibliography", style:"apa")`。
44
+
45
+ ## 命令
46
+
47
+ `/zotero status` 报告连通性、API/schema 版本和数据库身份标识(Server ID,Zotero 10+)。这是唯一的健康检查。普通调用失败时返回带类型的领域错误。
48
+
49
+ ## 限制
50
+
51
+ - 对文献库只读:V1 没有任何修改条目、笔记、标签、分类等文献库数据的路径。
52
+ - 全文证据依赖 Zotero 的全文索引:`everything` 搜索和 `retrieve` 的全文片段都以索引为前提。
53
+ - 附件深度分析取决于当前 Harness 配置:`zotero_attachment` 返回文件位置,能否继续读取该 PDF 由 composition 里是否有相应文件/PDF 能力决定。
54
+ - 证据排序是词项相关度检索,不是 embedding 或语义搜索。
55
+
56
+ ## 环境要求
57
+
58
+ - 已安装 Zotero 桌面版,并启用本地 API:**设置 → 高级 → “Allow other applications on this computer to communicate with Zotero”**。
59
+ - 本地 API 为无认证读取,地址为 `http://127.0.0.1:23119/api`。V1 没有任何修改文献库数据(条目、笔记、标签、分类等)的路径。
60
+ - Zotero ≥ 7,本地 API 版本为 3。如果 status 命令报告版本不匹配,请升级。
61
+
62
+ ## 安装
63
+
64
+ ### 按包名安装
65
+
66
+ ```sh
67
+ dsh plugin --profile <name> add dsh-zotero
68
+ ```
69
+
70
+ tarball 内含已构建的 `lib/`,无需本地构建。
71
+
72
+ ### 本地 tarball
73
+
74
+ ```sh
75
+ cd dsh-zotero
76
+ npm pack
77
+ dsh plugin --profile <name> add ./dsh-zotero-0.1.0.tgz
78
+ ```
79
+
80
+ `npm pack` 先运行 `prepare` 构建 `lib/`,适合未发布或本地试装。
81
+
82
+ ### 从 GitHub 源码安装
83
+
84
+ ```sh
85
+ dsh plugin --profile <name> add github:Vncntvx/dsh-zotero
86
+ ```
87
+
88
+ git 安装拉取源码而非构建产物,pnpm 安装依赖后运行本包的 `prepare` 现场构建(TypeScript 与 `@types/node` 在 `dependencies` 中)。pnpm ≥ 10 默认拒绝运行 git 依赖的 `prepare`,首次 `add` 会失败并提示:把包名加进 profile 的 `pnpm-workspace.yaml` 后重新执行:
89
+
90
+ ```yaml
91
+ allowBuilds:
92
+ dsh-zotero: true
93
+ ```
94
+
95
+ `allowBuilds` 授权该包在安装时执行代码,只允许你信任的来源,建议固定到具体提交(`github:Vncntvx/dsh-zotero#<sha>`)。
96
+
97
+ 插件以 id `zotero` 挂载,下次启动 dsh 时生效。安装或启用插件后,如果当前会话创建于插件加载之前,请新建会话,确保 Agent 获得 Zotero 工具。
98
+
99
+ ## 配置
100
+
101
+ 所有值都是 `Config` 字段,可在 bundle 的 `config` 块中修改(例如通过 `dsh plugin config`)。以下为默认值。
102
+
103
+ | 字段 | 默认值 | 含义 |
104
+ | ---------------------- | ---------------------------- | -------------------------------------- |
105
+ | `baseUrl` | `http://127.0.0.1:23119/api` | 本地 API 基础 URL。仅支持纯回环 HTTP。 |
106
+ | `provider` | `local` | 要选择的 provider id。 |
107
+ | `timeoutMs` | `5000` | 每个请求的 provider 超时时间。 |
108
+ | `maxSearchResults` | `20` | `zotero_search` `limit` 的上限。 |
109
+ | `maxEvidenceChars` | `6000` | 检索证据的总字符预算。 |
110
+ | `maxEvidencePassages` | `4` | 证据片段数量的上限。 |
111
+ | `maxDetailChars` | `3000` | `zotero_get` 摘要预览的字符预算。 |
112
+ | `maxNoteChars` | `2000` | `zotero_get` 单条笔记预览的字符预算。 |
113
+ | `maxNoteRecords` | `50` | `zotero_get` 返回笔记数量的上限。 |
114
+ | `maxAnnotationRecords` | `100` | `zotero_get` 返回批注数量的上限。 |
115
+ | `fulltextChunkWords` | `200` | 进入证据排序的全文片段词数。 |
116
+ | `maxFulltextChars` | `250000` | 进入证据排序的全文大小上限。 |
117
+ | `maxResponseBytes` | `16777216` | 每个 API 响应的流式字节上限。 |
118
+ | `maxExportChars` | `1000000` | 导出输出的硬上限。不会中途截断。 |
119
+ | `defaultStyle` | `apa` | 引用/参考文献使用的 CSL 样式。 |
120
+ | `defaultLocale` | `en-US` | 引用/参考文献使用的 CSL locale。 |
121
+
122
+ ## 开发
123
+
124
+ ### 命令
125
+
126
+ ```sh
127
+ npm install # 使用本地 npm 缓存
128
+ npm test # 单元测试(mock Zotero server)
129
+ npm run test:coverage # 对 src/ 的 100% 覆盖率门禁
130
+ npm run typecheck # tsc --noEmit,app + test 项目
131
+ npm run build # 生成 lib/
132
+ npm run format # prettier --write 全仓格式化
133
+ npm run format:check # 校验格式化(提交前执行)
134
+ ```
135
+
136
+ 集成测试面向真实 Zotero,默认跳过,需显式开启:
137
+
138
+ ```sh
139
+ npm run test:integration
140
+ # 或:ZOTERO_INTEGRATION=1 npx vitest run tests/integration/zotero.integration.spec.ts
141
+ ```
142
+
143
+ ### 本地启动
144
+
145
+ #### 从 dsh 源码启动
146
+
147
+ 在 deepseek-harness 源码 checkout 中构建一次(`pnpm install && pnpm run build`),然后通过 dev overlay 加载插件源码:
148
+
149
+ ```sh
150
+ pnpm dsh web --patch ./dsh-zotero/dev.cordis.yml
151
+ ```
152
+
153
+ `dev.cordis.yml` 将插件入口指向绝对的 `src/index.ts`。dsh 的源码启动经 tsx 加载该 TypeScript 入口,插件因此无需预构建;若 checkout 路径不同,需同步修改文件中的绝对路径。
154
+
155
+ #### 使用 npm 安装的 dsh
156
+
157
+ **常驻实例**:打包为 tarball 并安装到 profile,插件以 tarball 中的副本运行;代码更新需重新打包安装。安装后运行生产栈 smoke 验证:
158
+
159
+ ```sh
160
+ npm pack
161
+ dsh plugin --profile <name> add ./dsh-zotero-0.1.0.tgz
162
+ cd ~/.dsh/profiles/<name>
163
+ node --input-type=module < /path/to/dsh-zotero/scripts/smoke.mjs
164
+ ```
165
+
166
+ smoke 需在 profile 目录内运行,裸导入由此从 profile 的扁平 `node_modules` 解析。脚本依次验证 `status`、`search`、`get`、`retrieve`、`export`、策略提示词分区与五个工具注册;输出 `SMOKE PASS` 表示打包后的插件通过安装路径验证。
167
+
168
+ **开发实例(热替换)**:`dev-lib.cordis.yml` 覆盖层禁用 profile 中的 tarball 副本(id `zotero`),插入 `zotero-dev` 指向本仓库的 `lib/index.js`,并重新启用 HMR。生产 web profile 默认禁用 loader HMR,且 HMR 的监视根位于 profile 目录,因此覆盖层显式设置了 `base`。构建输出变化后,HMR 在同一进程内销毁旧实例并重新构造插件,无需重启 dsh:
169
+
170
+ ```sh
171
+ cd /Volumes/Work/deepseek-harness/dsh-zotero
172
+ npm run dev & # tsc --watch:修改 src 后自动重建 lib
173
+ dsh web --patch ./dev-lib.cordis.yml --port 3307
174
+ ```
175
+
176
+ 热替换仅作用于通过 `--patch` 启动的实例;常驻实例继续运行 tarball 版本。
177
+
178
+ ## 许可证
179
+
180
+ 本插件以 [MIT](./LICENSE) 许可证发布。
@@ -0,0 +1,6 @@
1
+ # This patch is used when the plugin is installed as a dsh bundle.
2
+ # The `name` refers to the package name, so Node module resolution can find it.
3
+ - insert:
4
+ - id: zotero
5
+ name: dsh-zotero
6
+ config: {}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Attachment records and deterministic attachment selection.
3
+ *
4
+ * Zotero's own best-attachment choice (`links.attachment` on item
5
+ * responses) is preferred wherever it exists; `selectAttachment` is the
6
+ * documented fallback that ranks child rows the same way Zotero's
7
+ * `getBestAttachment` does — PDF over other kinds, imported files first,
8
+ * then earliest addition date, then key order.
9
+ * @module dsh-zotero/attachments
10
+ */
11
+ /**
12
+ * A normalized attachment child row, before ref provenance is attached.
13
+ * Ref-free on purpose: callers own the `?server=` qualifier.
14
+ */
15
+ export interface ZoteroAttachmentCandidate {
16
+ readonly key: string;
17
+ readonly title: string;
18
+ readonly contentType: string;
19
+ readonly linkMode?: string;
20
+ /** `data.url`; meaningful for `linked_url` attachments. */
21
+ readonly url?: string;
22
+ }
23
+ /** Extract a Zotero object key from an API `links.attachment.href`. */
24
+ export declare function extractAttachmentKey(href: string | undefined): string | undefined;
25
+ /**
26
+ * Read Zotero's own best-attachment link from an item response.
27
+ * @returns the attachment key and content type, or undefined when the item
28
+ * has no attachment link (or its href carries no valid Zotero key).
29
+ */
30
+ export declare function bestAttachmentFromLinks(json: unknown): {
31
+ key: string;
32
+ contentType: string;
33
+ } | undefined;
34
+ /**
35
+ * Normalize one attachment item JSON object.
36
+ * @throws {ZoteroError} `ZOTERO_UNEXPECTED` when the object has no valid Zotero key.
37
+ */
38
+ export declare function normalizeAttachmentRecord(json: unknown): ZoteroAttachmentCandidate;
39
+ /**
40
+ * Select one attachment of the requested kind from raw child rows, ranked
41
+ * deterministically: imported files first, then earliest `dateAdded`, then
42
+ * key order. Rows without an attachment content type are skipped; a
43
+ * malformed attachment row fails loud like every other broken invariant.
44
+ * @param rows - raw child item JSON objects (notes and annotations are skipped).
45
+ * @param kind - `pdf` selects `application/pdf`; anything else is matched as a literal content type.
46
+ * @returns the winning record, or undefined when no row matches the kind.
47
+ * @throws {ZoteroError} `ZOTERO_UNEXPECTED` on an attachment row without a valid key.
48
+ */
49
+ export declare function selectAttachment(rows: readonly unknown[], kind: string): ZoteroAttachmentCandidate | undefined;
50
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgBH;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,OAAO,GACZ;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAKlD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,OAAO,GAAG,yBAAyB,CAmBlF;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,IAAI,EAAE,MAAM,GACX,yBAAyB,GAAG,SAAS,CAoBvC"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Attachment records and deterministic attachment selection.
3
+ *
4
+ * Zotero's own best-attachment choice (`links.attachment` on item
5
+ * responses) is preferred wherever it exists; `selectAttachment` is the
6
+ * documented fallback that ranks child rows the same way Zotero's
7
+ * `getBestAttachment` does — PDF over other kinds, imported files first,
8
+ * then earliest addition date, then key order.
9
+ * @module dsh-zotero/attachments
10
+ */
11
+ import { ZOTERO_UNEXPECTED, ZoteroError } from './errors.js';
12
+ const OBJECT_KEY_PATTERN = /^[A-Z0-9]{8}$/;
13
+ function asRecord(value) {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
15
+ ? value
16
+ : undefined;
17
+ }
18
+ function asString(value) {
19
+ return typeof value === 'string' ? value : undefined;
20
+ }
21
+ /** Extract a Zotero object key from an API `links.attachment.href`. */
22
+ export function extractAttachmentKey(href) {
23
+ if (href === undefined)
24
+ return undefined;
25
+ return /\/items\/([A-Z0-9]{8})(?:[/?#]|$)/.exec(href)?.[1];
26
+ }
27
+ /**
28
+ * Read Zotero's own best-attachment link from an item response.
29
+ * @returns the attachment key and content type, or undefined when the item
30
+ * has no attachment link (or its href carries no valid Zotero key).
31
+ */
32
+ export function bestAttachmentFromLinks(json) {
33
+ const attachment = asRecord(asRecord(asRecord(json)?.links)?.attachment);
34
+ const key = extractAttachmentKey(asString(attachment?.href));
35
+ if (key === undefined)
36
+ return undefined;
37
+ return { key, contentType: asString(attachment?.attachmentType) ?? '' };
38
+ }
39
+ /**
40
+ * Normalize one attachment item JSON object.
41
+ * @throws {ZoteroError} `ZOTERO_UNEXPECTED` when the object has no valid Zotero key.
42
+ */
43
+ export function normalizeAttachmentRecord(json) {
44
+ const record = asRecord(json);
45
+ const key = asString(record?.key);
46
+ if (key === undefined || !OBJECT_KEY_PATTERN.test(key)) {
47
+ throw new ZoteroError('Zotero returned an attachment without a valid object key.', ZOTERO_UNEXPECTED);
48
+ }
49
+ const data = asRecord(record?.data);
50
+ const linkMode = asString(data?.linkMode);
51
+ const url = asString(data?.url);
52
+ return {
53
+ key,
54
+ title: asString(data?.title) ?? '',
55
+ contentType: asString(data?.contentType) ?? '',
56
+ ...(linkMode !== undefined ? { linkMode } : {}),
57
+ ...(url !== undefined ? { url } : {}),
58
+ };
59
+ }
60
+ /**
61
+ * Select one attachment of the requested kind from raw child rows, ranked
62
+ * deterministically: imported files first, then earliest `dateAdded`, then
63
+ * key order. Rows without an attachment content type are skipped; a
64
+ * malformed attachment row fails loud like every other broken invariant.
65
+ * @param rows - raw child item JSON objects (notes and annotations are skipped).
66
+ * @param kind - `pdf` selects `application/pdf`; anything else is matched as a literal content type.
67
+ * @returns the winning record, or undefined when no row matches the kind.
68
+ * @throws {ZoteroError} `ZOTERO_UNEXPECTED` on an attachment row without a valid key.
69
+ */
70
+ export function selectAttachment(rows, kind) {
71
+ const wantedContentType = kind === 'pdf' ? 'application/pdf' : kind;
72
+ const scored = [];
73
+ for (const row of rows) {
74
+ const record = asRecord(row);
75
+ const data = asRecord(record?.data);
76
+ if (asString(data?.contentType) === undefined)
77
+ continue;
78
+ const candidate = normalizeAttachmentRecord(row);
79
+ if (candidate.contentType !== wantedContentType)
80
+ continue;
81
+ scored.push({ candidate, dateAdded: asString(data?.dateAdded) ?? '' });
82
+ }
83
+ scored.sort((a, b) => {
84
+ const rankA = a.candidate.linkMode === 'imported_file' ? 0 : 1;
85
+ const rankB = b.candidate.linkMode === 'imported_file' ? 0 : 1;
86
+ if (rankA !== rankB)
87
+ return rankA - rankB;
88
+ const byDate = a.dateAdded.localeCompare(b.dateAdded);
89
+ if (byDate !== 0)
90
+ return byDate;
91
+ return a.candidate.key.localeCompare(b.candidate.key);
92
+ });
93
+ return scored[0]?.candidate;
94
+ }
95
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The Local API HTTP boundary: plain-loopback `fetch` with a pinned API
3
+ * version, instance-identity protection, a streaming response byte bound,
4
+ * and strict transport-error translation. Every request is request-driven —
5
+ * there is no keep-alive state, no background work, and no redirect
6
+ * following, so a loopback endpoint can never be taken elsewhere.
7
+ * @module dsh-zotero/client
8
+ */
9
+ export interface ZoteroHttpClientOptions {
10
+ readonly baseUrl: string;
11
+ readonly timeoutMs: number;
12
+ readonly maxResponseBytes: number;
13
+ }
14
+ export interface ZoteroHttpGetOptions {
15
+ /** Caller cancellation; an abort preserves harness cancellation semantics, never a timeout. */
16
+ readonly signal?: AbortSignal;
17
+ /** Override the remembered instance id for this request. */
18
+ readonly serverId?: string;
19
+ /** Send the remembered instance id (default true); false suppresses it for identity refreshes. */
20
+ readonly sendServerId?: boolean;
21
+ }
22
+ export interface ZoteroHttpResponse {
23
+ readonly body: string;
24
+ readonly headers: Headers;
25
+ }
26
+ export declare class ZoteroHttpClient {
27
+ private readonly options;
28
+ private currentServerId;
29
+ private readonly baseUrlWithSlash;
30
+ constructor(options: ZoteroHttpClientOptions);
31
+ /** The instance id remembered from the latest response carrying one (Zotero 10+). */
32
+ get serverId(): string | undefined;
33
+ /**
34
+ * GET a path relative to the API base (no leading slash; `''` is `/api/`).
35
+ * @param path - relative path, e.g. `users/0/items/ABCD1234`.
36
+ * @param search - query parameters, serialized verbatim.
37
+ */
38
+ get(path: string, search?: URLSearchParams, opts?: ZoteroHttpGetOptions): Promise<ZoteroHttpResponse>;
39
+ /** GET and parse a JSON response. */
40
+ getJson<T>(path: string, search?: URLSearchParams, opts?: ZoteroHttpGetOptions): Promise<{
41
+ json: T;
42
+ body: string;
43
+ headers: Headers;
44
+ }>;
45
+ private rememberServerId;
46
+ /** Re-read `/api/` without the remembered id so a stale id cannot 412 the refresh. */
47
+ private refreshIdentity;
48
+ }
49
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuBH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;IAC7B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,kGAAkG;IAClG,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAkFD,qBAAa,gBAAgB;IAIf,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,eAAe,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;gBAEZ,OAAO,EAAE,uBAAuB;IAI7D,qFAAqF;IACrF,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED;;;;OAIG;IACG,GAAG,CACP,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,eAAe,EACxB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,kBAAkB,CAAC;IAwC9B,qCAAqC;IAC/B,OAAO,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,eAAe,EACxB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAavD,OAAO,CAAC,gBAAgB;IAKxB,sFAAsF;YACxE,eAAe;CAG9B"}