@beomsukoh/zotero-cli 0.2.0 → 0.3.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
|
@@ -7,7 +7,7 @@ Same architecture as [devonthink-cli](https://github.com/GoBeromsu/devonthink-cl
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install -g @
|
|
10
|
+
npm install -g @beomsukoh/zotero-cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
@@ -45,6 +45,16 @@ zt types
|
|
|
45
45
|
|
|
46
46
|
# Show fields for a specific type
|
|
47
47
|
zt types journalArticle
|
|
48
|
+
|
|
49
|
+
# Import paper by DOI (requires Translation Server)
|
|
50
|
+
docker run -p 1969:1969 zotero/translation-server
|
|
51
|
+
zt import --doi "10.1145/3025453.3025912"
|
|
52
|
+
|
|
53
|
+
# Import from URL
|
|
54
|
+
zt import --url "https://arxiv.org/abs/2301.01234"
|
|
55
|
+
|
|
56
|
+
# Import PDF: resolve DOI from filename, create item, attach PDF
|
|
57
|
+
zt import ./10.1234_example.pdf
|
|
48
58
|
```
|
|
49
59
|
|
|
50
60
|
## Configuration
|
|
@@ -54,6 +64,7 @@ zt types journalArticle
|
|
|
54
64
|
| `ZOTERO_BASE_URL` | `http://localhost:23119/api` | API base URL |
|
|
55
65
|
| `ZOTERO_API_KEY` | — | API key (required for writes) |
|
|
56
66
|
| `ZOTERO_USER_ID` | `0` | User ID (`0` = local API) |
|
|
67
|
+
| `ZOTERO_TRANSLATION_SERVER` | `http://localhost:1969` | Translation Server URL (for `import`) |
|
|
57
68
|
|
|
58
69
|
**Local mode** (reads only, Zotero desktop must be running):
|
|
59
70
|
```bash
|
|
@@ -83,6 +94,7 @@ export ZOTERO_BASE_URL=https://api.zotero.org
|
|
|
83
94
|
| `zt attach <file> --key <key>` | Attach file to item (`--content-type`) |
|
|
84
95
|
| `zt export --format <fmt>` | Export items (bibtex, ris, csljson, csv, tei, etc.) |
|
|
85
96
|
| `zt fulltext <key>` | Get full-text content |
|
|
97
|
+
| `zt import` | Import from DOI, ISBN, arXiv, URL, BibTeX, or PDF |
|
|
86
98
|
|
|
87
99
|
### Create
|
|
88
100
|
|
|
@@ -110,6 +122,7 @@ zt <command> [options]
|
|
|
110
122
|
-> ZoteroPort (application boundary)
|
|
111
123
|
-> HttpZoteroAdapter
|
|
112
124
|
-> Zotero Local API (localhost:23119) or Web API (api.zotero.org)
|
|
125
|
+
-> Translation Server (localhost:1969) for import
|
|
113
126
|
```
|
|
114
127
|
|
|
115
128
|
## License
|
|
@@ -7,11 +7,13 @@ export class HttpZoteroAdapter {
|
|
|
7
7
|
baseUrl;
|
|
8
8
|
apiKey;
|
|
9
9
|
userId;
|
|
10
|
-
|
|
10
|
+
translationServerUrl;
|
|
11
|
+
constructor(http, baseUrl, apiKey, userId, translationServerUrl) {
|
|
11
12
|
this.http = http;
|
|
12
13
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
13
14
|
this.apiKey = apiKey;
|
|
14
15
|
this.userId = userId;
|
|
16
|
+
this.translationServerUrl = translationServerUrl ?? "http://localhost:1969";
|
|
15
17
|
}
|
|
16
18
|
// ── Query methods ──────────────────────────────────────────────
|
|
17
19
|
async listItems(library, collectionKey, options) {
|
|
@@ -228,6 +230,37 @@ export class HttpZoteroAdapter {
|
|
|
228
230
|
const url = this.buildUrl(`${this.libraryPrefix(library)}/items/${itemKey}/file`);
|
|
229
231
|
return url;
|
|
230
232
|
}
|
|
233
|
+
// ── Translation Server methods ────────────────────────────────
|
|
234
|
+
async resolveIdentifier(identifier) {
|
|
235
|
+
const url = `${this.translationServerUrl}/search`;
|
|
236
|
+
const response = await this.http.request("POST", url, {
|
|
237
|
+
headers: { "Content-Type": "text/plain" },
|
|
238
|
+
body: identifier,
|
|
239
|
+
});
|
|
240
|
+
if (response.status === 501) {
|
|
241
|
+
throw new ExternalToolError("No translator available for this identifier.");
|
|
242
|
+
}
|
|
243
|
+
return this.parseResponse(response);
|
|
244
|
+
}
|
|
245
|
+
async scrapeUrl(url) {
|
|
246
|
+
const tsUrl = `${this.translationServerUrl}/web`;
|
|
247
|
+
const response = await this.http.request("POST", tsUrl, {
|
|
248
|
+
headers: { "Content-Type": "text/plain" },
|
|
249
|
+
body: url,
|
|
250
|
+
});
|
|
251
|
+
if (response.status === 501) {
|
|
252
|
+
throw new ExternalToolError("No translator available for this URL.");
|
|
253
|
+
}
|
|
254
|
+
return this.parseResponse(response);
|
|
255
|
+
}
|
|
256
|
+
async importBibliography(text) {
|
|
257
|
+
const url = `${this.translationServerUrl}/import`;
|
|
258
|
+
const response = await this.http.request("POST", url, {
|
|
259
|
+
headers: { "Content-Type": "text/plain" },
|
|
260
|
+
body: text,
|
|
261
|
+
});
|
|
262
|
+
return this.parseResponse(response);
|
|
263
|
+
}
|
|
231
264
|
// ── Helpers ────────────────────────────────────────────────────
|
|
232
265
|
libraryPrefix(library) {
|
|
233
266
|
return `/${library.type === "group" ? "groups" : "users"}/${library.id}`;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { ValidationError, NotFoundError } from "../application/errors.js";
|
|
4
|
+
import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs, } from "../utils/args.js";
|
|
5
|
+
import { renderJson, resolveLibrary } from "./helpers.js";
|
|
6
|
+
export class ImportCommand {
|
|
7
|
+
name = "import";
|
|
8
|
+
category = "Core";
|
|
9
|
+
description = "Import items from DOI, ISBN, arXiv ID, URL, or BibTeX.";
|
|
10
|
+
help() {
|
|
11
|
+
return [
|
|
12
|
+
"Usage:",
|
|
13
|
+
' zt import --doi "10.1234/example"',
|
|
14
|
+
' zt import --isbn "978-0-123456-78-9"',
|
|
15
|
+
' zt import --arxiv "2301.01234"',
|
|
16
|
+
' zt import --url "https://arxiv.org/abs/2301.01234"',
|
|
17
|
+
" zt import --bibtex ./references.bib",
|
|
18
|
+
" zt import ./paper.pdf",
|
|
19
|
+
' zt import ./paper.pdf --doi "10.1234/example"',
|
|
20
|
+
"",
|
|
21
|
+
"Import bibliographic metadata via the Zotero Translation Server",
|
|
22
|
+
"and create items in Zotero. Optionally attach a PDF.",
|
|
23
|
+
"",
|
|
24
|
+
"Options:",
|
|
25
|
+
" --doi <doi> DOI to resolve",
|
|
26
|
+
" --isbn <isbn> ISBN to resolve",
|
|
27
|
+
" --arxiv <id> arXiv ID to resolve",
|
|
28
|
+
" --url <url> URL to scrape",
|
|
29
|
+
" --bibtex <file> BibTeX file to import",
|
|
30
|
+
" --collection <key> Add to collection after import",
|
|
31
|
+
" --group <id> Library group ID",
|
|
32
|
+
"",
|
|
33
|
+
"Positional:",
|
|
34
|
+
" <file> PDF file to attach after creating item",
|
|
35
|
+
"",
|
|
36
|
+
"Examples:",
|
|
37
|
+
' zt import --doi "10.1038/s41586-021-03819-2"',
|
|
38
|
+
' zt import --arxiv "2301.01234" --collection ABC12345',
|
|
39
|
+
" zt import ./paper.pdf --doi 10.1234/example",
|
|
40
|
+
" zt import --bibtex refs.bib --group 123456",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
parse(argv, context) {
|
|
44
|
+
const parsed = parseArgs(argv);
|
|
45
|
+
assertNoUnknownOptions(parsed, [
|
|
46
|
+
"doi", "isbn", "arxiv", "url", "bibtex", "collection", "group",
|
|
47
|
+
]);
|
|
48
|
+
assertNoMissingOptionValues(parsed, [
|
|
49
|
+
"doi", "isbn", "arxiv", "url", "bibtex", "collection", "group",
|
|
50
|
+
]);
|
|
51
|
+
const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
|
|
52
|
+
const doi = getOption(parsed, "doi");
|
|
53
|
+
const isbn = getOption(parsed, "isbn");
|
|
54
|
+
const arxiv = getOption(parsed, "arxiv");
|
|
55
|
+
const url = getOption(parsed, "url");
|
|
56
|
+
const bibtexFile = getOption(parsed, "bibtex");
|
|
57
|
+
const collection = getOption(parsed, "collection");
|
|
58
|
+
const filePath = parsed.positionals[0];
|
|
59
|
+
if (parsed.positionals.length > 1) {
|
|
60
|
+
throw new ValidationError("import accepts at most one positional argument (file path).");
|
|
61
|
+
}
|
|
62
|
+
return { library, doi, isbn, arxiv, url, bibtexFile, collection, filePath };
|
|
63
|
+
}
|
|
64
|
+
async execute(input, context) {
|
|
65
|
+
const library = input.library;
|
|
66
|
+
let metadata;
|
|
67
|
+
// Step 1: Resolve metadata
|
|
68
|
+
if (input.doi) {
|
|
69
|
+
metadata = await context.zotero.resolveIdentifier(input.doi);
|
|
70
|
+
}
|
|
71
|
+
else if (input.isbn) {
|
|
72
|
+
metadata = await context.zotero.resolveIdentifier(input.isbn);
|
|
73
|
+
}
|
|
74
|
+
else if (input.arxiv) {
|
|
75
|
+
metadata = await context.zotero.resolveIdentifier(`arXiv:${input.arxiv}`);
|
|
76
|
+
}
|
|
77
|
+
else if (input.url) {
|
|
78
|
+
metadata = await context.zotero.scrapeUrl(input.url);
|
|
79
|
+
}
|
|
80
|
+
else if (input.bibtexFile) {
|
|
81
|
+
const content = await readFile(input.bibtexFile, "utf8");
|
|
82
|
+
metadata = await context.zotero.importBibliography(content);
|
|
83
|
+
}
|
|
84
|
+
else if (input.filePath) {
|
|
85
|
+
// Try to extract DOI from filename (pattern: 10.xxxx/yyyy)
|
|
86
|
+
const doiMatch = basename(input.filePath).match(/10\.\d{4,}[^\s]*/);
|
|
87
|
+
if (doiMatch) {
|
|
88
|
+
metadata = await context.zotero.resolveIdentifier(doiMatch[0]);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
throw new ValidationError("Cannot determine metadata source. Provide --doi, --url, --isbn, or --arxiv.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
throw new ValidationError("import requires --doi, --url, --isbn, --arxiv, --bibtex, or a file path.");
|
|
96
|
+
}
|
|
97
|
+
// Step 2: Create the item
|
|
98
|
+
// Translation server returns array of items or single item
|
|
99
|
+
const items = Array.isArray(metadata) ? metadata : [metadata];
|
|
100
|
+
if (items.length === 0) {
|
|
101
|
+
throw new NotFoundError("No items found for the given identifier.");
|
|
102
|
+
}
|
|
103
|
+
// Add to collection if specified
|
|
104
|
+
if (input.collection) {
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
if (typeof item === "object" && item !== null) {
|
|
107
|
+
item["collections"] = [input.collection];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const created = await context.zotero.createItem(library, items);
|
|
112
|
+
// Step 3: Attach PDF if file path provided
|
|
113
|
+
if (input.filePath && input.filePath.endsWith(".pdf")) {
|
|
114
|
+
const createdData = created;
|
|
115
|
+
const successful = (createdData?.["successful"] ?? createdData?.["success"]);
|
|
116
|
+
if (successful) {
|
|
117
|
+
const firstKey = Object.values(successful)[0];
|
|
118
|
+
const itemKey = firstKey?.["key"] ??
|
|
119
|
+
firstKey?.["data"]?.["key"];
|
|
120
|
+
if (itemKey) {
|
|
121
|
+
await context.zotero.uploadAttachment(library, itemKey, input.filePath, "application/pdf");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
context.output.write(renderJson(created));
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/compositionRoot.js
CHANGED
|
@@ -16,14 +16,16 @@ import { ExportCommand } from "./commands/exportCommand.js";
|
|
|
16
16
|
import { FulltextCommand } from "./commands/fulltextCommand.js";
|
|
17
17
|
import { TemplateCommand } from "./commands/templateCommand.js";
|
|
18
18
|
import { TypesInfoCommand } from "./commands/typesInfoCommand.js";
|
|
19
|
+
import { ImportCommand } from "./commands/importCommand.js";
|
|
19
20
|
export function createRuntime() {
|
|
20
21
|
const config = {
|
|
21
22
|
apiKey: process.env["ZOTERO_API_KEY"],
|
|
22
23
|
userId: process.env["ZOTERO_USER_ID"] ?? "0",
|
|
23
|
-
baseUrl: process.env["ZOTERO_BASE_URL"] ?? "http://localhost:23119/api"
|
|
24
|
+
baseUrl: process.env["ZOTERO_BASE_URL"] ?? "http://localhost:23119/api",
|
|
25
|
+
translationServerUrl: process.env["ZOTERO_TRANSLATION_SERVER"] ?? "http://localhost:1969",
|
|
24
26
|
};
|
|
25
27
|
const http = new NodeHttpClient();
|
|
26
|
-
const zotero = new HttpZoteroAdapter(http, config.baseUrl, config.apiKey, config.userId);
|
|
28
|
+
const zotero = new HttpZoteroAdapter(http, config.baseUrl, config.apiKey, config.userId, config.translationServerUrl);
|
|
27
29
|
const output = new ConsoleOutputAdapter();
|
|
28
30
|
const registry = new CommandRegistry();
|
|
29
31
|
registry.register(new LibrariesCommand());
|
|
@@ -40,5 +42,6 @@ export function createRuntime() {
|
|
|
40
42
|
registry.register(new FulltextCommand());
|
|
41
43
|
registry.register(new TemplateCommand());
|
|
42
44
|
registry.register(new TypesInfoCommand());
|
|
45
|
+
registry.register(new ImportCommand());
|
|
43
46
|
return { output, zotero, registry, config };
|
|
44
47
|
}
|