@beomsukoh/zotero-cli 0.3.0 → 0.4.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.
package/README.md
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Zotero CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Control Zotero from the command line. `zotero-cli` wraps the Zotero Web API v3 and Local API through shell-friendly verbs, JSON output, and a consistent option grammar.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```bash
|
|
6
|
+
# Search your library
|
|
7
|
+
zt search "machine learning" --limit 5
|
|
8
|
+
|
|
9
|
+
# Import a paper by DOI
|
|
10
|
+
zt import --doi "10.1145/3025453.3025912"
|
|
11
|
+
|
|
12
|
+
# Export as BibTeX
|
|
13
|
+
zt export --format bibtex --collection ABCD1234
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js 20+
|
|
19
|
+
- Zotero 7+ (for local API reads) or a Zotero API key (for writes)
|
|
20
|
+
- [Zotero Translation Server](https://github.com/zotero/translation-server) (optional, for `zt import`)
|
|
6
21
|
|
|
7
22
|
## Install
|
|
8
23
|
|
|
@@ -10,74 +25,87 @@ Same architecture as [devonthink-cli](https://github.com/GoBeromsu/devonthink-cl
|
|
|
10
25
|
npm install -g @beomsukoh/zotero-cli
|
|
11
26
|
```
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
Verify the installation:
|
|
14
29
|
|
|
15
30
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
zt items --limit 1
|
|
32
|
+
```
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
zt search "machine learning"
|
|
34
|
+
This prints a JSON array of items from your local Zotero library. If Zotero desktop is not running, the command exits with an error.
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
zt get ABCD1234
|
|
36
|
+
## Quickstart
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
zt collections
|
|
38
|
+
List items in your library:
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
zt
|
|
40
|
+
```bash
|
|
41
|
+
zt items --limit 10 --sort dateAdded
|
|
42
|
+
```
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
zt template journalArticle
|
|
44
|
+
Search across your library:
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
zt
|
|
46
|
+
```bash
|
|
47
|
+
zt search "attention is all you need"
|
|
48
|
+
```
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
zt attach ./paper.pdf --key ABCD1234
|
|
50
|
+
Get a specific item by key:
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
zt
|
|
52
|
+
```bash
|
|
53
|
+
zt get ABCD1234
|
|
54
|
+
```
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
zt types
|
|
56
|
+
List collections:
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
zt
|
|
58
|
+
```bash
|
|
59
|
+
zt collections
|
|
60
|
+
```
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
Import a paper by DOI (requires Translation Server):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
docker run -d -p 1969:1969 zotero/translation-server
|
|
51
66
|
zt import --doi "10.1145/3025453.3025912"
|
|
67
|
+
```
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
zt import --url "https://arxiv.org/abs/2301.01234"
|
|
69
|
+
Attach a PDF to an existing item:
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
zt
|
|
71
|
+
```bash
|
|
72
|
+
zt attach ./paper.pdf --key ABCD1234
|
|
58
73
|
```
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
Export your library as BibTeX:
|
|
61
76
|
|
|
62
|
-
| Variable | Default | Description |
|
|
63
|
-
|---|---|---|
|
|
64
|
-
| `ZOTERO_BASE_URL` | `http://localhost:23119/api` | API base URL |
|
|
65
|
-
| `ZOTERO_API_KEY` | — | API key (required for writes) |
|
|
66
|
-
| `ZOTERO_USER_ID` | `0` | User ID (`0` = local API) |
|
|
67
|
-
| `ZOTERO_TRANSLATION_SERVER` | `http://localhost:1969` | Translation Server URL (for `import`) |
|
|
68
|
-
|
|
69
|
-
**Local mode** (reads only, Zotero desktop must be running):
|
|
70
77
|
```bash
|
|
71
|
-
zt
|
|
78
|
+
zt export --format bibtex
|
|
72
79
|
```
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
## Dual-Mode Architecture
|
|
82
|
+
|
|
83
|
+
Zotero CLI supports two modes of operation:
|
|
84
|
+
|
|
85
|
+
| Mode | Base URL | Auth | Capabilities |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| **Local** (default) | `http://localhost:23119/api` | None | Read-only; Zotero desktop must be running |
|
|
88
|
+
| **Web** | `https://api.zotero.org` | API key | Full CRUD; works remotely |
|
|
89
|
+
|
|
90
|
+
**Local mode** requires no configuration — just have Zotero desktop running.
|
|
91
|
+
|
|
92
|
+
**Web mode** requires an API key from [zotero.org/settings/keys](https://www.zotero.org/settings/keys/):
|
|
93
|
+
|
|
75
94
|
```bash
|
|
76
95
|
export ZOTERO_API_KEY=your_key_here
|
|
77
96
|
export ZOTERO_USER_ID=12345
|
|
78
97
|
export ZOTERO_BASE_URL=https://api.zotero.org
|
|
79
98
|
```
|
|
80
99
|
|
|
100
|
+
### Configuration
|
|
101
|
+
|
|
102
|
+
| Variable | Default | Description |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `ZOTERO_BASE_URL` | `http://localhost:23119/api` | API base URL |
|
|
105
|
+
| `ZOTERO_API_KEY` | — | API key (required for writes) |
|
|
106
|
+
| `ZOTERO_USER_ID` | `0` | User ID (`0` = local) |
|
|
107
|
+
| `ZOTERO_TRANSLATION_SERVER` | `http://localhost:1969` | Translation Server URL |
|
|
108
|
+
|
|
81
109
|
## Commands
|
|
82
110
|
|
|
83
111
|
### Core
|
|
@@ -85,15 +113,15 @@ export ZOTERO_BASE_URL=https://api.zotero.org
|
|
|
85
113
|
| Command | Description |
|
|
86
114
|
|---|---|
|
|
87
115
|
| `zt items` | List items (`--collection`, `--top`, `--tag`, `--type`, `--limit`, `--sort`) |
|
|
88
|
-
| `zt collections` | List collections (`--parent`) |
|
|
89
|
-
| `zt get <key>` | Get item by key (`--children` for
|
|
116
|
+
| `zt collections` | List collections (`--parent` for subcollections) |
|
|
117
|
+
| `zt get <key>` | Get item by key (`--children` for attachments/notes) |
|
|
90
118
|
| `zt search <query>` | Search items (`--qmode`, `--tag`, `--type`) |
|
|
91
119
|
| `zt tags` | List tags |
|
|
92
120
|
| `zt libraries` | List group libraries |
|
|
93
121
|
| `zt delete --key <key>` | Delete item or collection (`--collection` flag) |
|
|
94
|
-
| `zt attach <file> --key <key>` |
|
|
95
|
-
| `zt export --format <fmt>` | Export items
|
|
96
|
-
| `zt fulltext <key>` | Get full-text content |
|
|
122
|
+
| `zt attach <file> --key <key>` | Upload and attach file to item |
|
|
123
|
+
| `zt export --format <fmt>` | Export items in citation formats |
|
|
124
|
+
| `zt fulltext <key>` | Get full-text content of an item |
|
|
97
125
|
| `zt import` | Import from DOI, ISBN, arXiv, URL, BibTeX, or PDF |
|
|
98
126
|
|
|
99
127
|
### Create
|
|
@@ -107,22 +135,112 @@ export ZOTERO_BASE_URL=https://api.zotero.org
|
|
|
107
135
|
|
|
108
136
|
| Command | Description |
|
|
109
137
|
|---|---|
|
|
110
|
-
| `zt types [itemType]` | List item types, or fields for a
|
|
111
|
-
| `zt template <itemType>` | Get JSON template for
|
|
138
|
+
| `zt types [itemType]` | List item types, or fields/creators for a type |
|
|
139
|
+
| `zt template <itemType>` | Get JSON template for item creation |
|
|
140
|
+
|
|
141
|
+
Run `zt --help` or `zt <command> --help` for full usage.
|
|
142
|
+
|
|
143
|
+
## Examples
|
|
144
|
+
|
|
145
|
+
### Import a paper by DOI and attach its PDF
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Start translation server (one-time)
|
|
149
|
+
docker run -d -p 1969:1969 zotero/translation-server
|
|
150
|
+
|
|
151
|
+
# Import: resolves metadata via Translation Server, creates item, attaches PDF
|
|
152
|
+
zt import ./paper.pdf --doi "10.1145/3025453.3025912" --collection COLL1234
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Import from arXiv
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
zt import --arxiv "2301.01234"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Import from a BibTeX file
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
zt import --bibtex ./references.bib
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Create a journal article manually
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Get the template first
|
|
171
|
+
zt template journalArticle
|
|
172
|
+
|
|
173
|
+
# Create with specific fields
|
|
174
|
+
zt create:item --type journalArticle \
|
|
175
|
+
--title "My Research Paper" \
|
|
176
|
+
--doi "10.1234/example" \
|
|
177
|
+
--date "2026" \
|
|
178
|
+
--collection COLL1234
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Create from raw JSON
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
zt create:item '{"itemType":"book","title":"Clean Code","creators":[{"creatorType":"author","firstName":"Robert","lastName":"Martin"}]}'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Browse item type schema
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# List all 42 item types
|
|
191
|
+
zt types
|
|
192
|
+
|
|
193
|
+
# Show fields for journal articles
|
|
194
|
+
zt types journalArticle
|
|
195
|
+
|
|
196
|
+
# Show creator types for books
|
|
197
|
+
zt types book --creators
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Export a collection as CSL-JSON
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
zt export --format csljson --collection ABCD1234
|
|
204
|
+
```
|
|
112
205
|
|
|
113
206
|
### Export Formats
|
|
114
207
|
|
|
115
208
|
`bibtex`, `biblatex`, `ris`, `csljson`, `csv`, `tei`, `wikipedia`, `mods`, `refer`
|
|
116
209
|
|
|
117
|
-
|
|
210
|
+
### Work with group libraries
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# List groups
|
|
214
|
+
zt libraries
|
|
215
|
+
|
|
216
|
+
# List items in a group
|
|
217
|
+
zt items --group 12345
|
|
218
|
+
|
|
219
|
+
# Search within a group
|
|
220
|
+
zt search "deep learning" --group 12345
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Philosophy
|
|
224
|
+
|
|
225
|
+
`zotero-cli` exposes what the Zotero API provides. It does not add citation management workflows, sync logic, bibliography generation pipelines, or PDF management beyond attachment upload. If the Zotero API does not offer a behavior natively, this package does not invent one.
|
|
118
226
|
|
|
227
|
+
The `import` command integrates with the official [Zotero Translation Server](https://github.com/zotero/translation-server) for DOI/ISBN/URL resolution — the same engine that powers the Zotero Connector browser extension.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pnpm install
|
|
233
|
+
pnpm check # TypeScript + doc validation
|
|
234
|
+
pnpm test # Unit tests
|
|
235
|
+
pnpm build
|
|
236
|
+
pnpm pack:check # Package integrity
|
|
119
237
|
```
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
238
|
+
|
|
239
|
+
## Publishing
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
pnpm release:check
|
|
243
|
+
npm publish --access public
|
|
126
244
|
```
|
|
127
245
|
|
|
128
246
|
## License
|
|
@@ -226,10 +226,69 @@ export class HttpZoteroAdapter {
|
|
|
226
226
|
await this.parseResponse(registerResponse);
|
|
227
227
|
return { success: true, key: attachmentKey };
|
|
228
228
|
}
|
|
229
|
+
async downloadAttachment(library, itemKey) {
|
|
230
|
+
const url = this.buildUrl(`${this.libraryPrefix(library)}/items/${itemKey}/file`);
|
|
231
|
+
const response = await this.http.requestRaw("GET", url, {
|
|
232
|
+
headers: this.headers(),
|
|
233
|
+
});
|
|
234
|
+
if (response.status === 404) {
|
|
235
|
+
throw new NotFoundError(`Attachment not found: ${itemKey}`);
|
|
236
|
+
}
|
|
237
|
+
if (response.status >= 400) {
|
|
238
|
+
throw new ExternalToolError(`Failed to download attachment (${response.status})`);
|
|
239
|
+
}
|
|
240
|
+
return response.data;
|
|
241
|
+
}
|
|
229
242
|
async getFileUrl(library, itemKey) {
|
|
230
243
|
const url = this.buildUrl(`${this.libraryPrefix(library)}/items/${itemKey}/file`);
|
|
231
244
|
return url;
|
|
232
245
|
}
|
|
246
|
+
// ── Reparent ────────────────────────────────────────────────────
|
|
247
|
+
async reparentItem(library, itemKey, newParentKey) {
|
|
248
|
+
const item = (await this.getItem(library, itemKey));
|
|
249
|
+
const version = item["version"] ?? 0;
|
|
250
|
+
return this.updateItem(library, itemKey, { parentItem: newParentKey }, version);
|
|
251
|
+
}
|
|
252
|
+
// ── Better BibTeX ───────────────────────────────────────────────
|
|
253
|
+
get bbtUrl() {
|
|
254
|
+
const base = this.baseUrl.replace(/\/api\/?$/, "");
|
|
255
|
+
return `${base}/better-bibtex/json-rpc`;
|
|
256
|
+
}
|
|
257
|
+
async bbtRpc(method, params) {
|
|
258
|
+
const response = await this.http.request("POST", this.bbtUrl, {
|
|
259
|
+
headers: { "Content-Type": "application/json" },
|
|
260
|
+
body: JSON.stringify({ jsonrpc: "2.0", method, params, id: 1 }),
|
|
261
|
+
});
|
|
262
|
+
if (response.status >= 400) {
|
|
263
|
+
throw new ExternalToolError(`Better BibTeX RPC error (${response.status}): ${response.body}`);
|
|
264
|
+
}
|
|
265
|
+
const parsed = JSON.parse(response.body);
|
|
266
|
+
if (parsed["error"]) {
|
|
267
|
+
throw new ExternalToolError(`BBT error: ${JSON.stringify(parsed["error"])}`);
|
|
268
|
+
}
|
|
269
|
+
return parsed["result"] ?? null;
|
|
270
|
+
}
|
|
271
|
+
async bbtCiteKeys(itemKeys) {
|
|
272
|
+
return this.bbtRpc("item.citationkey", [itemKeys]);
|
|
273
|
+
}
|
|
274
|
+
async bbtExport(itemKeys, translator) {
|
|
275
|
+
const result = await this.bbtRpc("item.export", [itemKeys, translator]);
|
|
276
|
+
return String(result);
|
|
277
|
+
}
|
|
278
|
+
async bbtSearch(query) {
|
|
279
|
+
return this.bbtRpc("item.search", [query]);
|
|
280
|
+
}
|
|
281
|
+
async bbtProbe() {
|
|
282
|
+
try {
|
|
283
|
+
const base = this.baseUrl.replace(/\/api\/?$/, "");
|
|
284
|
+
const url = `${base}/better-bibtex/cayw?probe=true`;
|
|
285
|
+
const response = await this.http.request("GET", url, {});
|
|
286
|
+
return response.status === 200;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
233
292
|
// ── Translation Server methods ────────────────────────────────
|
|
234
293
|
async resolveIdentifier(identifier) {
|
|
235
294
|
const url = `${this.translationServerUrl}/search`;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ValidationError } from "../application/errors.js";
|
|
2
|
+
import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs } from "../utils/args.js";
|
|
3
|
+
import { renderJson } from "./helpers.js";
|
|
4
|
+
const VALID_SUBCOMMANDS = new Set(["status", "cite", "export", "search"]);
|
|
5
|
+
export class BbtCommand {
|
|
6
|
+
name = "bbt";
|
|
7
|
+
category = "Lookup";
|
|
8
|
+
description = "Better BibTeX operations: cite keys, export, search, status.";
|
|
9
|
+
help() {
|
|
10
|
+
return [
|
|
11
|
+
"Usage:",
|
|
12
|
+
" zt bbt status",
|
|
13
|
+
" zt bbt cite <key> [<key>...]",
|
|
14
|
+
" zt bbt export <key> [<key>...] --translator better-bibtex",
|
|
15
|
+
" zt bbt search <query>",
|
|
16
|
+
"",
|
|
17
|
+
"Better BibTeX operations.",
|
|
18
|
+
"",
|
|
19
|
+
"Subcommands:",
|
|
20
|
+
" status Check if BBT is available",
|
|
21
|
+
" cite <key> [<key>...] Get citation keys for items",
|
|
22
|
+
" export <key> [...] Export items via BBT",
|
|
23
|
+
" search <query> Search via BBT",
|
|
24
|
+
"",
|
|
25
|
+
"Options:",
|
|
26
|
+
" --translator <name> Translator for export (default: better-bibtex)",
|
|
27
|
+
"",
|
|
28
|
+
"Examples:",
|
|
29
|
+
" zt bbt status",
|
|
30
|
+
" zt bbt cite ABC12345 DEF67890",
|
|
31
|
+
" zt bbt export ABC12345 --translator better-biblatex",
|
|
32
|
+
" zt bbt search \"attention is all you need\""
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
parse(argv) {
|
|
36
|
+
const parsed = parseArgs(argv);
|
|
37
|
+
assertNoUnknownOptions(parsed, ["translator"]);
|
|
38
|
+
assertNoMissingOptionValues(parsed, ["translator"]);
|
|
39
|
+
const subcommand = parsed.positionals[0];
|
|
40
|
+
if (!subcommand) {
|
|
41
|
+
throw new ValidationError("bbt requires a subcommand: status, cite, export, search");
|
|
42
|
+
}
|
|
43
|
+
if (!VALID_SUBCOMMANDS.has(subcommand)) {
|
|
44
|
+
throw new ValidationError(`Unknown bbt subcommand: ${subcommand}. Valid: status, cite, export, search`);
|
|
45
|
+
}
|
|
46
|
+
const args = parsed.positionals.slice(1);
|
|
47
|
+
const translator = getOption(parsed, "translator") ?? "better-bibtex";
|
|
48
|
+
// Validate args per subcommand
|
|
49
|
+
if (subcommand === "cite" && args.length === 0) {
|
|
50
|
+
throw new ValidationError("bbt cite requires at least one item key.");
|
|
51
|
+
}
|
|
52
|
+
if (subcommand === "export" && args.length === 0) {
|
|
53
|
+
throw new ValidationError("bbt export requires at least one item key.");
|
|
54
|
+
}
|
|
55
|
+
if (subcommand === "search" && args.length === 0) {
|
|
56
|
+
throw new ValidationError("bbt search requires a query.");
|
|
57
|
+
}
|
|
58
|
+
return { subcommand: subcommand, args, translator };
|
|
59
|
+
}
|
|
60
|
+
async execute(input, context) {
|
|
61
|
+
switch (input.subcommand) {
|
|
62
|
+
case "status": {
|
|
63
|
+
const available = await context.zotero.bbtProbe();
|
|
64
|
+
context.output.write(renderJson({ available }));
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "cite": {
|
|
68
|
+
const result = await context.zotero.bbtCiteKeys(input.args);
|
|
69
|
+
context.output.write(renderJson(result));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "export": {
|
|
73
|
+
const result = await context.zotero.bbtExport(input.args, input.translator);
|
|
74
|
+
context.output.write(result);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "search": {
|
|
78
|
+
const query = input.args.join(" ");
|
|
79
|
+
const result = await context.zotero.bbtSearch(query);
|
|
80
|
+
context.output.write(renderJson(result));
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { ValidationError } from "../application/errors.js";
|
|
4
|
+
import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs } from "../utils/args.js";
|
|
5
|
+
import { renderJson, resolveLibrary } from "./helpers.js";
|
|
6
|
+
export class DownloadCommand {
|
|
7
|
+
name = "download";
|
|
8
|
+
category = "Core";
|
|
9
|
+
description = "Download an attachment file to disk.";
|
|
10
|
+
help() {
|
|
11
|
+
return [
|
|
12
|
+
"Usage:",
|
|
13
|
+
" zt download <key>",
|
|
14
|
+
" zt download <key> --output ./paper.pdf",
|
|
15
|
+
"",
|
|
16
|
+
"Download an attachment file to disk.",
|
|
17
|
+
"",
|
|
18
|
+
"Options:",
|
|
19
|
+
" --group <id> Library group ID",
|
|
20
|
+
" --output <path> Output file path (default: current dir + filename from item)",
|
|
21
|
+
"",
|
|
22
|
+
"Examples:",
|
|
23
|
+
" zt download ABC12345",
|
|
24
|
+
" zt download ABC12345 --output ./paper.pdf",
|
|
25
|
+
" zt download ABC12345 --group 123456"
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
parse(argv, context) {
|
|
29
|
+
const parsed = parseArgs(argv);
|
|
30
|
+
assertNoUnknownOptions(parsed, ["group", "output"]);
|
|
31
|
+
assertNoMissingOptionValues(parsed, ["group", "output"]);
|
|
32
|
+
if (parsed.positionals.length > 1) {
|
|
33
|
+
throw new ValidationError("download accepts at most one positional argument (item key).");
|
|
34
|
+
}
|
|
35
|
+
const key = parsed.positionals[0];
|
|
36
|
+
if (!key) {
|
|
37
|
+
throw new ValidationError("download requires an item key. Usage: zt download <key>");
|
|
38
|
+
}
|
|
39
|
+
const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
|
|
40
|
+
const output = getOption(parsed, "output");
|
|
41
|
+
return { library, key, output };
|
|
42
|
+
}
|
|
43
|
+
async execute(input, context) {
|
|
44
|
+
// Get item metadata to determine filename if output path not specified
|
|
45
|
+
const item = await context.zotero.getItem(input.library, input.key);
|
|
46
|
+
const data = item.data;
|
|
47
|
+
const filename = data?.filename ?? `${input.key}`;
|
|
48
|
+
const outputPath = input.output
|
|
49
|
+
? resolve(input.output)
|
|
50
|
+
: resolve(process.cwd(), filename);
|
|
51
|
+
const buffer = await context.zotero.downloadAttachment(input.library, input.key);
|
|
52
|
+
await writeFile(outputPath, buffer);
|
|
53
|
+
context.output.write(renderJson({
|
|
54
|
+
success: true,
|
|
55
|
+
path: outputPath,
|
|
56
|
+
size: buffer.length
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ValidationError } from "../application/errors.js";
|
|
2
|
+
import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs } from "../utils/args.js";
|
|
3
|
+
import { ensureNoPositionals, renderJson, resolveLibrary } from "./helpers.js";
|
|
4
|
+
export class ReparentCommand {
|
|
5
|
+
name = "reparent";
|
|
6
|
+
category = "Core";
|
|
7
|
+
description = "Change the parent of an attachment or note item.";
|
|
8
|
+
help() {
|
|
9
|
+
return [
|
|
10
|
+
"Usage:",
|
|
11
|
+
" zt reparent --key <attachmentKey> --parent <newParentKey>",
|
|
12
|
+
"",
|
|
13
|
+
"Change the parent of an attachment or note item.",
|
|
14
|
+
"",
|
|
15
|
+
"Options:",
|
|
16
|
+
" --key <key> Attachment or note key (required)",
|
|
17
|
+
" --parent <key> New parent item key (required)",
|
|
18
|
+
" --group <id> Library group ID",
|
|
19
|
+
"",
|
|
20
|
+
"Examples:",
|
|
21
|
+
" zt reparent --key ATT12345 --parent ABC12345",
|
|
22
|
+
" zt reparent --key ATT12345 --parent ABC12345 --group 123456"
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
parse(argv, context) {
|
|
26
|
+
const parsed = parseArgs(argv);
|
|
27
|
+
assertNoUnknownOptions(parsed, ["key", "parent", "group"]);
|
|
28
|
+
assertNoMissingOptionValues(parsed, ["key", "parent", "group"]);
|
|
29
|
+
ensureNoPositionals(parsed, "reparent");
|
|
30
|
+
const key = getOption(parsed, "key");
|
|
31
|
+
if (!key) {
|
|
32
|
+
throw new ValidationError("reparent requires --key <attachmentKey>.");
|
|
33
|
+
}
|
|
34
|
+
const parentKey = getOption(parsed, "parent");
|
|
35
|
+
if (!parentKey) {
|
|
36
|
+
throw new ValidationError("reparent requires --parent <newParentKey>.");
|
|
37
|
+
}
|
|
38
|
+
const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
|
|
39
|
+
return { library, key, parentKey };
|
|
40
|
+
}
|
|
41
|
+
async execute(input, context) {
|
|
42
|
+
const result = await context.zotero.reparentItem(input.library, input.key, input.parentKey);
|
|
43
|
+
context.output.write(renderJson(result));
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/compositionRoot.js
CHANGED
|
@@ -17,6 +17,9 @@ import { FulltextCommand } from "./commands/fulltextCommand.js";
|
|
|
17
17
|
import { TemplateCommand } from "./commands/templateCommand.js";
|
|
18
18
|
import { TypesInfoCommand } from "./commands/typesInfoCommand.js";
|
|
19
19
|
import { ImportCommand } from "./commands/importCommand.js";
|
|
20
|
+
import { DownloadCommand } from "./commands/downloadCommand.js";
|
|
21
|
+
import { ReparentCommand } from "./commands/reparentCommand.js";
|
|
22
|
+
import { BbtCommand } from "./commands/bbtCommand.js";
|
|
20
23
|
export function createRuntime() {
|
|
21
24
|
const config = {
|
|
22
25
|
apiKey: process.env["ZOTERO_API_KEY"],
|
|
@@ -43,5 +46,8 @@ export function createRuntime() {
|
|
|
43
46
|
registry.register(new TemplateCommand());
|
|
44
47
|
registry.register(new TypesInfoCommand());
|
|
45
48
|
registry.register(new ImportCommand());
|
|
49
|
+
registry.register(new DownloadCommand());
|
|
50
|
+
registry.register(new ReparentCommand());
|
|
51
|
+
registry.register(new BbtCommand());
|
|
46
52
|
return { output, zotero, registry, config };
|
|
47
53
|
}
|