zotcli 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {zotcli-0.2.1 → zotcli-0.2.2}/CHANGELOG.md +27 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/PKG-INFO +29 -2
- {zotcli-0.2.1 → zotcli-0.2.2}/README.md +28 -1
- {zotcli-0.2.1 → zotcli-0.2.2}/SKILL.md +31 -1
- {zotcli-0.2.1 → zotcli-0.2.2}/docs/commands.md +25 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/docs/getting_started.md +8 -0
- zotcli-0.2.2/docs/images/fulltext-auth-flow.png +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/docs/index.md +1 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/pyproject.toml +1 -1
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/__init__.py +1 -1
- zotcli-0.2.2/src/zotcli/cli/config_cmd.py +101 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/items.py +74 -4
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/main.py +2 -2
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/config.py +82 -5
- zotcli-0.2.2/src/zotcli/queries/search.py +292 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/conftest.py +7 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/test_cli.py +40 -2
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/test_queries.py +63 -3
- zotcli-0.2.1/src/zotcli/cli/config_cmd.py +0 -55
- zotcli-0.2.1/src/zotcli/queries/search.py +0 -133
- {zotcli-0.2.1 → zotcli-0.2.2}/.github/workflows/docs.yml +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/.github/workflows/publish.yml +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/.gitignore +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/PLAN_WRITE.md +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/docs/api_reference.md +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/docs/architecture-write.md +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/docs/cli_reference.md +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/docs/data_models.md +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/mkdocs.yml +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/__main__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/add.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/attachments.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/collections.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/export.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/render.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/search.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/cli/stats.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/db.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/export/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/export/bibtex.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/export/csv_.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/export/json_.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/export/markdown.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/logging_setup.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/models.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/paths.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/queries/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/queries/attachments.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/queries/collections.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/queries/items.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/queries/tags.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/browser.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/citation_pipeline.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/collection_assign.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/connector_client.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/credentials.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/csl_json.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/dedup.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/identifiers.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/pdf.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/preflight.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/recognize.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/arxiv.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/crossref.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/ieee.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/openalex.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/openlibrary.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/pubmed.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/sciencedirect.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/semantic_scholar.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/resolvers/unpaywall.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/src/zotcli/write/session.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/test_script.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/fixtures/csl/crossref_numpy.json +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/fixtures/sample.bib +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/fixtures/sample.epub +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/fixtures/sample.pdf +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/fixtures/sample.ris +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_add_cite.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_add_file.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_add_import.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_add_pipeline.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_add_url.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_auto_detect.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_batch.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_connector_client.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_verbose.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/integration/test_with_pdf.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/test_db.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/test_export.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_browser_optional_dep.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_citation_pipeline.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_config_write_section.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_credentials.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_csl_json.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_dedup.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_identifiers.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_logging_setup.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_paths.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_pdf.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_recognize.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/__init__.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_arxiv.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_crossref.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_crossref_search.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_ieee.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_openalex.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_openlibrary.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_pubmed.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_sciencedirect.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_semantic_scholar.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/tests/unit/test_resolvers/test_unpaywall.py +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/zot.png +0 -0
- {zotcli-0.2.1 → zotcli-0.2.2}/zotcli.code-workspace +0 -0
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.2] — 2026-05-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`zot items fulltext`** — retrieve item full text with a tiered fallback strategy: direct network access (DOI/URL) → configured library credentials → Playwright interactive auth → local Zotero fulltext index → metadata fallback (title/abstract/notes). Supports `--offline`, `--playwright-auth/--no-playwright-auth`, `--max-chars`.
|
|
10
|
+
- **`zot config library-auth`** — store or display per-library institution/username/password credentials used by `zot items fulltext` for paywalled retrieval. Credentials persist under `[library_auth.<id>]` in `config.toml`.
|
|
11
|
+
- **Fulltext search** — `zot search "query" --fulltext` searches against the local Zotero fulltext index.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`save_config` crashed on `None` values** — when calling `tomli_w.dump`, `None` defaults (e.g. `database.path = None`) raised `TypeError: Object of type 'NoneType' is not TOML serializable`. Now strips `None` entries before serialising.
|
|
16
|
+
|
|
17
|
+
### Merged
|
|
18
|
+
|
|
19
|
+
- Combined `feat/write-capability` (M1–M6 write subsystem) with PR #1 (network-first fulltext retrieval) onto a single linear history on `master`.
|
|
20
|
+
|
|
21
|
+
## [0.2.1] — 2026-05-17
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`zot collections assign`** — assign an existing item to a collection. Writes a single row to the `collectionItems` join table (no sync-critical metadata involved; safe while Zotero is open).
|
|
26
|
+
- **Collection membership check** — `zot collections show` now indicates whether a given item belongs to the collection.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `updateSession` payload — connector requires `target` as a flat string, not a wrapped object; saveItems calls with an empty body no longer fail.
|
|
31
|
+
|
|
5
32
|
## [0.2.0] — 2026-05-10
|
|
6
33
|
|
|
7
34
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zotcli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A CLI for browsing, exporting, and adding items to a Zotero library
|
|
5
5
|
Author-email: MohamedNumair <mo7amednumair@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -33,7 +33,7 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
# zotcli (`zot`)
|
|
34
34
|

|
|
35
35
|
|
|
36
|
-
A command-line interface for your local [Zotero](https://www.zotero.org/) library. **Default: strictly read-only.** Write capabilities are opt-in and route through Zotero's own connector HTTP server — `zotero.sqlite` is **never** modified directly.
|
|
36
|
+
A "crazy" good command-line interface for your local [Zotero](https://www.zotero.org/) library. Queries `zotero.sqlite` directly — no Zotero app running and no API key required for reads. **Default: strictly read-only.** Write capabilities are opt-in and route through Zotero's own connector HTTP server — `zotero.sqlite` is **never** modified directly.
|
|
37
37
|
|
|
38
38
|
**Library stats on this machine:** 3,771 items · 200 collections · 3,201 tags · 6.2 GB storage
|
|
39
39
|
|
|
@@ -395,6 +395,22 @@ zot attachments open 5UFZMSLU
|
|
|
395
395
|
|
|
396
396
|
---
|
|
397
397
|
|
|
398
|
+
### `zot items fulltext` — retrieve full text
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
zot items fulltext 5UFZMSLU
|
|
402
|
+
zot items fulltext 5UFZMSLU --offline
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Retrieval order:
|
|
406
|
+
1. direct network access from DOI/URL (institution/network-location access),
|
|
407
|
+
2. configured credentials (`zot config library-auth`),
|
|
408
|
+
3. Playwright interactive login fallback,
|
|
409
|
+
4. local Zotero fulltext index,
|
|
410
|
+
5. metadata fallback (title/abstract/notes).
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
398
414
|
### `zot export` — export to files
|
|
399
415
|
|
|
400
416
|
**BibTeX:**
|
|
@@ -542,6 +558,11 @@ Self-contained config at `<zotcli-home>/config.toml`. Run `zot config path` to f
|
|
|
542
558
|
[database]
|
|
543
559
|
path = "" # empty = auto-detect zotero.sqlite
|
|
544
560
|
|
|
561
|
+
[output]
|
|
562
|
+
default_format = "table"
|
|
563
|
+
color = true
|
|
564
|
+
page_size = 50
|
|
565
|
+
|
|
545
566
|
[write]
|
|
546
567
|
enabled = false # opt-in; set true once with: zot config set write.enabled true
|
|
547
568
|
connector_url = "http://127.0.0.1:23119"
|
|
@@ -553,6 +574,11 @@ email = ""
|
|
|
553
574
|
|
|
554
575
|
[browser]
|
|
555
576
|
headless = false # SSO/captcha needs headed browser
|
|
577
|
+
|
|
578
|
+
[library_auth.1]
|
|
579
|
+
institution = "KU Leuven"
|
|
580
|
+
username = "alice"
|
|
581
|
+
password = "token"
|
|
556
582
|
```
|
|
557
583
|
|
|
558
584
|
Override `<zotcli-home>` with the `ZOTCLI_HOME` environment variable.
|
|
@@ -563,6 +589,7 @@ Override `<zotcli-home>` with the `ZOTCLI_HOME` environment variable.
|
|
|
563
589
|
|
|
564
590
|
- Database opened with `sqlite3://…?mode=ro` — the OS-level read-only URI flag makes direct writes impossible
|
|
565
591
|
- WAL journal detection warns if Zotero is currently open (pending writes may not be visible yet)
|
|
592
|
+
- Network retrieval is optional and only used by `zot items fulltext` unless `--offline` is passed
|
|
566
593
|
- Write operations route through Zotero's own connector HTTP server — never via direct SQLite mutation
|
|
567
594
|
- Default is read-only; writes require explicit opt-in (`zot config set write.enabled true`)
|
|
568
595
|
- No Zotero API key required
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# zotcli (`zot`)
|
|
2
2
|

|
|
3
3
|
|
|
4
|
-
A command-line interface for your local [Zotero](https://www.zotero.org/) library. **Default: strictly read-only.** Write capabilities are opt-in and route through Zotero's own connector HTTP server — `zotero.sqlite` is **never** modified directly.
|
|
4
|
+
A "crazy" good command-line interface for your local [Zotero](https://www.zotero.org/) library. Queries `zotero.sqlite` directly — no Zotero app running and no API key required for reads. **Default: strictly read-only.** Write capabilities are opt-in and route through Zotero's own connector HTTP server — `zotero.sqlite` is **never** modified directly.
|
|
5
5
|
|
|
6
6
|
**Library stats on this machine:** 3,771 items · 200 collections · 3,201 tags · 6.2 GB storage
|
|
7
7
|
|
|
@@ -363,6 +363,22 @@ zot attachments open 5UFZMSLU
|
|
|
363
363
|
|
|
364
364
|
---
|
|
365
365
|
|
|
366
|
+
### `zot items fulltext` — retrieve full text
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
zot items fulltext 5UFZMSLU
|
|
370
|
+
zot items fulltext 5UFZMSLU --offline
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Retrieval order:
|
|
374
|
+
1. direct network access from DOI/URL (institution/network-location access),
|
|
375
|
+
2. configured credentials (`zot config library-auth`),
|
|
376
|
+
3. Playwright interactive login fallback,
|
|
377
|
+
4. local Zotero fulltext index,
|
|
378
|
+
5. metadata fallback (title/abstract/notes).
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
366
382
|
### `zot export` — export to files
|
|
367
383
|
|
|
368
384
|
**BibTeX:**
|
|
@@ -510,6 +526,11 @@ Self-contained config at `<zotcli-home>/config.toml`. Run `zot config path` to f
|
|
|
510
526
|
[database]
|
|
511
527
|
path = "" # empty = auto-detect zotero.sqlite
|
|
512
528
|
|
|
529
|
+
[output]
|
|
530
|
+
default_format = "table"
|
|
531
|
+
color = true
|
|
532
|
+
page_size = 50
|
|
533
|
+
|
|
513
534
|
[write]
|
|
514
535
|
enabled = false # opt-in; set true once with: zot config set write.enabled true
|
|
515
536
|
connector_url = "http://127.0.0.1:23119"
|
|
@@ -521,6 +542,11 @@ email = ""
|
|
|
521
542
|
|
|
522
543
|
[browser]
|
|
523
544
|
headless = false # SSO/captcha needs headed browser
|
|
545
|
+
|
|
546
|
+
[library_auth.1]
|
|
547
|
+
institution = "KU Leuven"
|
|
548
|
+
username = "alice"
|
|
549
|
+
password = "token"
|
|
524
550
|
```
|
|
525
551
|
|
|
526
552
|
Override `<zotcli-home>` with the `ZOTCLI_HOME` environment variable.
|
|
@@ -531,6 +557,7 @@ Override `<zotcli-home>` with the `ZOTCLI_HOME` environment variable.
|
|
|
531
557
|
|
|
532
558
|
- Database opened with `sqlite3://…?mode=ro` — the OS-level read-only URI flag makes direct writes impossible
|
|
533
559
|
- WAL journal detection warns if Zotero is currently open (pending writes may not be visible yet)
|
|
560
|
+
- Network retrieval is optional and only used by `zot items fulltext` unless `--offline` is passed
|
|
534
561
|
- Write operations route through Zotero's own connector HTTP server — never via direct SQLite mutation
|
|
535
562
|
- Default is read-only; writes require explicit opt-in (`zot config set write.enabled true`)
|
|
536
563
|
- No Zotero API key required
|
|
@@ -241,17 +241,30 @@ zot items list [--type TYPE] [--collection NAME] [--limit N]
|
|
|
241
241
|
zot items show <id|key>
|
|
242
242
|
zot items attachments <id|key>
|
|
243
243
|
zot items notes <id|key>
|
|
244
|
-
zot
|
|
244
|
+
zot items fulltext <id|key> [--offline] [--playwright-auth/--no-playwright-auth]
|
|
245
|
+
|
|
246
|
+
# Search
|
|
247
|
+
zot search "query" [--field title] [--field abstract] [--type TYPE]
|
|
248
|
+
zot search --author "Name"
|
|
249
|
+
zot search --doi 10.xxxx/yyyy
|
|
250
|
+
zot search --tag "tag-name"
|
|
251
|
+
zot search --year 2020-2024
|
|
252
|
+
zot search "query" --fulltext
|
|
253
|
+
|
|
254
|
+
# Attachments
|
|
245
255
|
zot attachments list [--missing] [--type pdf]
|
|
246
256
|
zot attachments path <id|key>
|
|
247
257
|
zot attachments open <id|key>
|
|
248
258
|
zot export json|csv|bib|markdown --collection "Name" [--output file]
|
|
249
259
|
zot export json|csv|bib|markdown --all [--output file]
|
|
260
|
+
zot export markdown --all --notes --output report.md
|
|
250
261
|
|
|
251
262
|
# Config (read-only-safe)
|
|
252
263
|
zot config path
|
|
253
264
|
zot config get <key>
|
|
254
265
|
zot config set <key> <value>
|
|
266
|
+
zot config library-auth --library 1 --institution "KU Leuven" --username "alice" --password "token"
|
|
267
|
+
zot config library-auth --library 1 --show
|
|
255
268
|
|
|
256
269
|
# Write — requires write.enabled=true and Zotero running
|
|
257
270
|
zot add status
|
|
@@ -299,7 +312,24 @@ If it shows unreachable, trigger the protocol above **before** attempting the ad
|
|
|
299
312
|
|
|
300
313
|
- Database opened in **strict read-only URI mode** (`sqlite3://…?mode=ro`) — impossible to corrupt your Zotero library via direct SQL
|
|
301
314
|
- WAL journal detection: warns if Zotero is currently running (changes may not be visible yet)
|
|
315
|
+
- Network calls are optional and used only by `zot items fulltext` unless `--offline` is passed
|
|
302
316
|
- Writes only via Zotero's own connector HTTP server — never via direct SQLite mutation for item data. Default is read-only; writes require explicit opt-in (`zot config set write.enabled true`)
|
|
303
317
|
- **One exception:** `zot collection assign` (and `zot add … --collection` on a duplicate) writes a single row into `collectionItems`, the join table that records which items belong to which collections. This table carries no sync-critical metadata (no version counters, no timestamps) so the write is safe even while Zotero is open. Zotero will pick up the change on its next UI refresh.
|
|
304
318
|
- No Zotero API key required
|
|
305
319
|
- All credentials stored at rest in `<zotcli-home>/credentials.json` (mode 0600 on POSIX)
|
|
320
|
+
|
|
321
|
+
### Full-text retrieval strategy
|
|
322
|
+
|
|
323
|
+
`zot items fulltext` uses this order:
|
|
324
|
+
1. direct network access (institution/network location),
|
|
325
|
+
2. config credentials (`zot config library-auth`),
|
|
326
|
+
3. Playwright interactive authentication fallback,
|
|
327
|
+
4. local Zotero fulltext index,
|
|
328
|
+
5. metadata fallback (title/abstract/notes).
|
|
329
|
+
|
|
330
|
+
## Running tests
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
python3 -m pytest tests/ -q # unit + integration tests (in-memory DB fixture)
|
|
334
|
+
python3 -m pytest tests/e2e -m e2e # End-to-end against real zotero.sqlite (slow)
|
|
335
|
+
```
|
|
@@ -88,6 +88,8 @@ zot items show AABB0001
|
|
|
88
88
|
|
|
89
89
|
zot items attachments <id|key> # Attachments with path + existence check
|
|
90
90
|
zot items notes <id|key> # Attached notes (HTML stripped)
|
|
91
|
+
zot items fulltext <id|key> # Network-first full-text retrieval
|
|
92
|
+
zot items fulltext <id|key> --offline # Only local Zotero fulltext/metadata
|
|
91
93
|
```
|
|
92
94
|
|
|
93
95
|
`<id|key>` accepts a numeric item ID or the 8-character Zotero key (e.g. `AABB0001`).
|
|
@@ -192,6 +194,29 @@ zot export bib --collection "NLP"
|
|
|
192
194
|
|
|
193
195
|
---
|
|
194
196
|
|
|
197
|
+
## `zot config`
|
|
198
|
+
|
|
199
|
+
Manage local CLI configuration values.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
zot config library-auth --library 1 --institution "KU Leuven" --username "alice" --password "token"
|
|
203
|
+
zot config library-auth --library 1 --show
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`library-auth` stores optional per-library institutional/login details used when direct network-location access to full text fails.
|
|
207
|
+
|
|
208
|
+
### Full-text retrieval strategy (`zot items fulltext`)
|
|
209
|
+
|
|
210
|
+
The command follows this order:
|
|
211
|
+
|
|
212
|
+
1. **Direct network access** from DOI/URL (works immediately on institutional networks like campus/VPN).
|
|
213
|
+
2. **Config credentials** from `zot config library-auth` (username/password flow).
|
|
214
|
+
3. **Playwright-assisted login** (interactive browser fallback).
|
|
215
|
+
4. **Local Zotero fulltext index**.
|
|
216
|
+
5. **Metadata fallback** (title/abstract/notes).
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
195
220
|
## Item types reference
|
|
196
221
|
|
|
197
222
|
Common values for `--type`:
|
|
@@ -27,6 +27,10 @@ zot search "bayesian" --field title
|
|
|
27
27
|
|
|
28
28
|
# Get attachment paths for a specific item
|
|
29
29
|
zot attachments path <ITEM_ID_OR_KEY>
|
|
30
|
+
|
|
31
|
+
# Retrieve full text (network-first, then auth/local fallbacks)
|
|
32
|
+
zot items fulltext <ITEM_ID_OR_KEY>
|
|
33
|
+
zot items fulltext <ITEM_ID_OR_KEY> --offline
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
## Programmatic Usage (SDK)
|
|
@@ -62,3 +66,7 @@ def extract_pdfs():
|
|
|
62
66
|
print("Extracting PDFs...")
|
|
63
67
|
extract_pdfs()
|
|
64
68
|
```
|
|
69
|
+
|
|
70
|
+
### CLI preview (fulltext + auth config)
|
|
71
|
+
|
|
72
|
+

|
|
Binary file
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- **Read-Only**: Safe to use. Never writes to the database.
|
|
9
9
|
- **Python SDK**: Rich programmatic access to your Zotero library via Pydantic models.
|
|
10
10
|
- **CLI Tool**: Powerful `zot` command-line interface for browsing, searching, and exporting.
|
|
11
|
+
- **Full-text Retrieval Flow**: Network-first retrieval with config-auth and Playwright fallback.
|
|
11
12
|
|
|
12
13
|
## Project Layout
|
|
13
14
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""CLI group: zot config — read/write zotcli configuration and per-library auth."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from zotcli.cli.main import Context, pass_ctx
|
|
10
|
+
from zotcli.cli.render import make_console
|
|
11
|
+
from zotcli.config import get_library_auth, set_library_auth
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group("config")
|
|
15
|
+
def config_cmd():
|
|
16
|
+
"""Manage zotcli settings."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@config_cmd.command("path")
|
|
20
|
+
def config_path_cmd():
|
|
21
|
+
"""Print the zotcli home directory path."""
|
|
22
|
+
from zotcli.paths import zotcli_home
|
|
23
|
+
click.echo(str(zotcli_home()))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@config_cmd.command("get")
|
|
27
|
+
@click.argument("key")
|
|
28
|
+
def config_get(key: str):
|
|
29
|
+
"""Print the value of KEY (section.key form, e.g. write.enabled).
|
|
30
|
+
|
|
31
|
+
Exits with code 1 if the key is not set.
|
|
32
|
+
"""
|
|
33
|
+
from zotcli.config import get_config_value
|
|
34
|
+
val = get_config_value(key)
|
|
35
|
+
if val is None:
|
|
36
|
+
click.echo(f"Key '{key}' is not set.", err=True)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
click.echo(val)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@config_cmd.command("set")
|
|
42
|
+
@click.argument("key")
|
|
43
|
+
@click.argument("value")
|
|
44
|
+
def config_set(key: str, value: str):
|
|
45
|
+
"""Set KEY to VALUE and persist to <zotcli-home>/config.toml.
|
|
46
|
+
|
|
47
|
+
Supported keys include: write.enabled, write.connector_url,
|
|
48
|
+
unpaywall.email, and any other dotted section.key value.
|
|
49
|
+
"""
|
|
50
|
+
from zotcli.config import set_config_value
|
|
51
|
+
set_config_value(key, value)
|
|
52
|
+
click.echo(f"Set {key} = {value}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@config_cmd.command("library-auth")
|
|
56
|
+
@click.option("--library", "library_id", type=int, default=None, help="Library ID (default: current)")
|
|
57
|
+
@click.option("--institution", default=None, help="Institution/library provider name")
|
|
58
|
+
@click.option("--username", default=None, help="Library login username")
|
|
59
|
+
@click.option("--password", default=None, help="Library login password/token")
|
|
60
|
+
@click.option("--show", is_flag=True, help="Show current authentication details")
|
|
61
|
+
@pass_ctx
|
|
62
|
+
def library_auth(
|
|
63
|
+
ctx: Context,
|
|
64
|
+
library_id: int | None,
|
|
65
|
+
institution: str | None,
|
|
66
|
+
username: str | None,
|
|
67
|
+
password: str | None,
|
|
68
|
+
show: bool,
|
|
69
|
+
):
|
|
70
|
+
"""Store or display per-library authentication details for full-text retrieval."""
|
|
71
|
+
console = make_console(ctx.color)
|
|
72
|
+
lib_id = library_id if library_id is not None else ctx.library_id
|
|
73
|
+
|
|
74
|
+
if show:
|
|
75
|
+
auth = get_library_auth(lib_id)
|
|
76
|
+
if not any(auth.values()):
|
|
77
|
+
console.print(f"[dim]No auth details configured for library {lib_id}.[/dim]")
|
|
78
|
+
return
|
|
79
|
+
masked_password = "******" if auth["password"] else ""
|
|
80
|
+
console.print(f"Library: {lib_id}")
|
|
81
|
+
console.print(f"Institution: {auth['institution']}")
|
|
82
|
+
console.print(f"Username: {auth['username']}")
|
|
83
|
+
console.print(f"Password: {masked_password}")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
institution_value = institution or click.prompt("Institution", default="", show_default=False)
|
|
87
|
+
username_value = username or click.prompt("Username", default="", show_default=False)
|
|
88
|
+
password_value = password or click.prompt(
|
|
89
|
+
"Password / token",
|
|
90
|
+
default="",
|
|
91
|
+
show_default=False,
|
|
92
|
+
hide_input=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
set_library_auth(
|
|
96
|
+
lib_id,
|
|
97
|
+
institution=institution_value.strip(),
|
|
98
|
+
username=username_value.strip(),
|
|
99
|
+
password=password_value,
|
|
100
|
+
)
|
|
101
|
+
console.print(f"[green]Saved auth details for library {lib_id}.[/green]")
|
|
@@ -6,10 +6,12 @@ import click
|
|
|
6
6
|
from rich.panel import Panel
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
|
|
9
|
-
from zotcli.cli.main import
|
|
10
|
-
from zotcli.cli.render import
|
|
11
|
-
from zotcli.
|
|
9
|
+
from zotcli.cli.main import Context, pass_ctx
|
|
10
|
+
from zotcli.cli.render import item_panel, items_table, make_console
|
|
11
|
+
from zotcli.config import get_library_auth
|
|
12
12
|
from zotcli.queries.collections import get_collection_by_name, get_items_in_collection
|
|
13
|
+
from zotcli.queries.items import get_item, get_items
|
|
14
|
+
from zotcli.queries.search import get_item_fulltext_with_strategy
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@click.group()
|
|
@@ -60,7 +62,7 @@ def show_item(ctx: Context, id_or_key: str):
|
|
|
60
62
|
item = get_item(ctx.db, item_id)
|
|
61
63
|
if item is None:
|
|
62
64
|
raise click.ClickException(f"Item not found: {id_or_key!r}")
|
|
63
|
-
|
|
65
|
+
|
|
64
66
|
from zotcli.queries.collections import get_collection_by_id
|
|
65
67
|
cols = []
|
|
66
68
|
if item.collections:
|
|
@@ -118,3 +120,71 @@ def item_notes(ctx: Context, id_or_key: str):
|
|
|
118
120
|
|
|
119
121
|
for note in item.notes:
|
|
120
122
|
console.print(Panel(note.plain_text[:2000], title=note.title or "Note"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@items.command("fulltext")
|
|
126
|
+
@click.argument("id_or_key")
|
|
127
|
+
@click.option("--max-chars", default=10000, show_default=True, help="Maximum number of characters to print")
|
|
128
|
+
@click.option("--offline", is_flag=True, help="Skip network/auth retrieval and only use local Zotero data")
|
|
129
|
+
@click.option(
|
|
130
|
+
"--playwright-auth/--no-playwright-auth",
|
|
131
|
+
default=True,
|
|
132
|
+
show_default=True,
|
|
133
|
+
help="Use Playwright login fallback when direct/config retrieval fails",
|
|
134
|
+
)
|
|
135
|
+
@pass_ctx
|
|
136
|
+
def item_fulltext(
|
|
137
|
+
ctx: Context,
|
|
138
|
+
id_or_key: str,
|
|
139
|
+
max_chars: int,
|
|
140
|
+
offline: bool,
|
|
141
|
+
playwright_auth: bool,
|
|
142
|
+
):
|
|
143
|
+
"""Retrieve full text for an item with network/auth/local fallback."""
|
|
144
|
+
console = make_console(ctx.color)
|
|
145
|
+
item_id_val = int(id_or_key) if id_or_key.isdigit() else id_or_key
|
|
146
|
+
auth = get_library_auth(ctx.library_id)
|
|
147
|
+
|
|
148
|
+
def _playwright_fetch(url: str) -> str | None:
|
|
149
|
+
if not playwright_auth:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
from playwright.sync_api import Error as PlaywrightError
|
|
154
|
+
from playwright.sync_api import sync_playwright
|
|
155
|
+
except ImportError:
|
|
156
|
+
console.print(
|
|
157
|
+
"[yellow]Playwright fallback unavailable (install `playwright` + browser binaries).[/yellow]"
|
|
158
|
+
)
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
console.print(
|
|
162
|
+
"[yellow]Open browser for manual institutional login.[/yellow]\n"
|
|
163
|
+
"Complete authentication in the opened window, then press Enter here."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
with sync_playwright() as p:
|
|
168
|
+
browser = p.chromium.launch(headless=False)
|
|
169
|
+
page = browser.new_page()
|
|
170
|
+
page.goto(url, wait_until="domcontentloaded", timeout=45000)
|
|
171
|
+
click.pause()
|
|
172
|
+
text = page.locator("body").inner_text(timeout=5000).strip()
|
|
173
|
+
browser.close()
|
|
174
|
+
return text or None
|
|
175
|
+
except (PlaywrightError, TimeoutError):
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
text, source = get_item_fulltext_with_strategy(
|
|
179
|
+
ctx.db,
|
|
180
|
+
item_id_val,
|
|
181
|
+
prefer_network=not offline,
|
|
182
|
+
auth=auth,
|
|
183
|
+
playwright_fetcher=_playwright_fetch if playwright_auth and not offline else None,
|
|
184
|
+
)
|
|
185
|
+
if text is None:
|
|
186
|
+
raise click.ClickException(f"Item not found: {id_or_key!r}")
|
|
187
|
+
|
|
188
|
+
title = f"Full text for {id_or_key}"
|
|
189
|
+
console.print(f"[dim]Source: {source}[/dim]")
|
|
190
|
+
console.print(Panel(text[:max_chars], title=title))
|
|
@@ -126,6 +126,7 @@ from zotcli.cli import attachments as _att_mod # noqa: E402
|
|
|
126
126
|
from zotcli.cli import search as _search_mod # noqa: E402
|
|
127
127
|
from zotcli.cli import stats as _stats_mod # noqa: E402
|
|
128
128
|
from zotcli.cli import export as _export_mod # noqa: E402
|
|
129
|
+
from zotcli.cli import config_cmd as _config_mod # noqa: E402
|
|
129
130
|
|
|
130
131
|
cli.add_command(_col_mod.collections)
|
|
131
132
|
cli.add_command(_items_mod.items)
|
|
@@ -133,10 +134,9 @@ cli.add_command(_att_mod.attachments)
|
|
|
133
134
|
cli.add_command(_search_mod.search)
|
|
134
135
|
cli.add_command(_stats_mod.stats)
|
|
135
136
|
cli.add_command(_export_mod.export)
|
|
137
|
+
cli.add_command(_config_mod.config_cmd)
|
|
136
138
|
|
|
137
139
|
# Import and register write-capability groups (M1+)
|
|
138
|
-
from zotcli.cli.config_cmd import config as _config_cmd # noqa: E402
|
|
139
140
|
from zotcli.cli.add import add as _add_cmd # noqa: E402
|
|
140
141
|
|
|
141
|
-
cli.add_command(_config_cmd)
|
|
142
142
|
cli.add_command(_add_cmd)
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import sys
|
|
5
|
+
import json
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
try:
|
|
@@ -34,6 +33,7 @@ _DEFAULTS: dict = {
|
|
|
34
33
|
"color": True,
|
|
35
34
|
"page_size": 50,
|
|
36
35
|
},
|
|
36
|
+
"library_auth": {},
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
# Defaults for the [write] section (stored in zotcli-home/config.toml)
|
|
@@ -68,11 +68,56 @@ def load_config() -> dict:
|
|
|
68
68
|
|
|
69
69
|
def save_config(cfg: dict) -> None:
|
|
70
70
|
"""Persist config to disk (TOML format)."""
|
|
71
|
-
import tomli_w # type: ignore[import]
|
|
72
71
|
p = _config_path()
|
|
73
72
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
-
|
|
75
|
-
tomli_w
|
|
73
|
+
try:
|
|
74
|
+
import tomli_w # type: ignore[import]
|
|
75
|
+
|
|
76
|
+
with open(p, "wb") as f:
|
|
77
|
+
tomli_w.dump(_strip_none(cfg), f)
|
|
78
|
+
return
|
|
79
|
+
except ImportError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
lines: list[str] = []
|
|
83
|
+
for section, values in cfg.items():
|
|
84
|
+
if not isinstance(values, dict):
|
|
85
|
+
continue
|
|
86
|
+
lines.append(f"[{section}]")
|
|
87
|
+
for key, value in values.items():
|
|
88
|
+
if isinstance(value, dict):
|
|
89
|
+
lines.append(f"[{section}.{key}]")
|
|
90
|
+
for nested_key, nested_value in value.items():
|
|
91
|
+
if isinstance(nested_value, dict):
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"Fallback TOML writer supports only one level of nested tables; "
|
|
94
|
+
"install tomli_w for deeper nesting."
|
|
95
|
+
)
|
|
96
|
+
if nested_value is None:
|
|
97
|
+
continue
|
|
98
|
+
lines.append(f"{nested_key} = {_toml_literal(nested_value)}")
|
|
99
|
+
else:
|
|
100
|
+
if value is None:
|
|
101
|
+
continue
|
|
102
|
+
lines.append(f"{key} = {_toml_literal(value)}")
|
|
103
|
+
lines.append("")
|
|
104
|
+
p.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _strip_none(obj):
|
|
108
|
+
if isinstance(obj, dict):
|
|
109
|
+
return {k: _strip_none(v) for k, v in obj.items() if v is not None}
|
|
110
|
+
return obj
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _toml_literal(value: object) -> str:
|
|
114
|
+
if isinstance(value, bool):
|
|
115
|
+
return "true" if value else "false"
|
|
116
|
+
if isinstance(value, int):
|
|
117
|
+
return str(value)
|
|
118
|
+
if isinstance(value, float):
|
|
119
|
+
return str(value)
|
|
120
|
+
return json.dumps(str(value))
|
|
76
121
|
|
|
77
122
|
|
|
78
123
|
def get_db_path(override: str | None = None) -> Path | None:
|
|
@@ -93,6 +138,38 @@ def get_library_id(override: int | None = None) -> int:
|
|
|
93
138
|
return cfg.get("database", {}).get("library_id", 1)
|
|
94
139
|
|
|
95
140
|
|
|
141
|
+
def get_library_auth(library_id: int) -> dict[str, str]:
|
|
142
|
+
cfg = load_config()
|
|
143
|
+
auth = cfg.get("library_auth", {}).get(str(library_id), {})
|
|
144
|
+
if not isinstance(auth, dict):
|
|
145
|
+
return {}
|
|
146
|
+
return {
|
|
147
|
+
"institution": str(auth.get("institution", "") or ""),
|
|
148
|
+
"username": str(auth.get("username", "") or ""),
|
|
149
|
+
"password": str(auth.get("password", "") or ""),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def set_library_auth(
|
|
154
|
+
library_id: int,
|
|
155
|
+
*,
|
|
156
|
+
institution: str,
|
|
157
|
+
username: str,
|
|
158
|
+
password: str,
|
|
159
|
+
) -> None:
|
|
160
|
+
cfg = load_config()
|
|
161
|
+
auth_section = cfg.setdefault("library_auth", {})
|
|
162
|
+
if not isinstance(auth_section, dict):
|
|
163
|
+
auth_section = {}
|
|
164
|
+
cfg["library_auth"] = auth_section
|
|
165
|
+
auth_section[str(library_id)] = {
|
|
166
|
+
"institution": institution,
|
|
167
|
+
"username": username,
|
|
168
|
+
"password": password,
|
|
169
|
+
}
|
|
170
|
+
save_config(cfg)
|
|
171
|
+
|
|
172
|
+
|
|
96
173
|
# ---------------------------------------------------------------------------
|
|
97
174
|
# Write-section helpers — config stored in <zotcli-home>/config.toml
|
|
98
175
|
# ---------------------------------------------------------------------------
|