zotkit 0.1.0__tar.gz → 0.2.0__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.
- {zotkit-0.1.0/zotkit.egg-info → zotkit-0.2.0}/PKG-INFO +38 -12
- {zotkit-0.1.0 → zotkit-0.2.0}/README.md +37 -11
- {zotkit-0.1.0 → zotkit-0.2.0}/pyproject.toml +1 -1
- {zotkit-0.1.0 → zotkit-0.2.0}/zotkit/__init__.py +1 -1
- {zotkit-0.1.0 → zotkit-0.2.0}/zotkit/cli.py +3 -3
- {zotkit-0.1.0 → zotkit-0.2.0}/zotkit/core.py +31 -12
- {zotkit-0.1.0 → zotkit-0.2.0/zotkit.egg-info}/PKG-INFO +38 -12
- {zotkit-0.1.0 → zotkit-0.2.0}/LICENSE +0 -0
- {zotkit-0.1.0 → zotkit-0.2.0}/setup.cfg +0 -0
- {zotkit-0.1.0 → zotkit-0.2.0}/zotkit.egg-info/SOURCES.txt +0 -0
- {zotkit-0.1.0 → zotkit-0.2.0}/zotkit.egg-info/dependency_links.txt +0 -0
- {zotkit-0.1.0 → zotkit-0.2.0}/zotkit.egg-info/entry_points.txt +0 -0
- {zotkit-0.1.0 → zotkit-0.2.0}/zotkit.egg-info/requires.txt +0 -0
- {zotkit-0.1.0 → zotkit-0.2.0}/zotkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zotkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Headless Zotero library management: Web API CRUD plus direct WebDAV attachment upload/download — no desktop app required
|
|
5
5
|
Author: Shawn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,12 +22,14 @@ Dynamic: license-file
|
|
|
22
22
|
|
|
23
23
|
**Headless Zotero library management — no desktop app required.**
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
"Headless" simply means zotkit never needs the Zotero app (or any window) open: it is a
|
|
26
|
+
Python library + CLI that talks straight to the
|
|
27
|
+
[Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start), so you can
|
|
28
|
+
search, create, tag, and organize items from any terminal — macOS, Windows, or Linux,
|
|
29
|
+
your laptop or a remote server. Uniquely, if your attachments sync to a **personal
|
|
30
|
+
WebDAV server**, zotkit can **upload and download the files themselves** by speaking
|
|
31
|
+
Zotero's WebDAV storage format directly — the one capability the Web API does not
|
|
32
|
+
provide, and (as far as we know) not offered by any other headless tool.
|
|
31
33
|
|
|
32
34
|
Built for servers, scripts, and **LLM agents**: every write is dry-run by default,
|
|
33
35
|
batched, and version-checked, and you can define a tag taxonomy that is *enforced in
|
|
@@ -39,11 +41,15 @@ code* so an agent (or a tired human) can't pollute your library with inconsisten
|
|
|
39
41
|
|---|---|---|---|
|
|
40
42
|
| Works headless (server, SSH, CI) | ❌ | ✅ read-mostly | ✅ |
|
|
41
43
|
| Write items/tags/collections | ✅ | ⚠️ usually needs the desktop app running | ✅ |
|
|
44
|
+
| Attachment files (Zotero Storage) | ✅ | ⚠️ some | ✅ upload + download |
|
|
42
45
|
| Attachment files on **WebDAV** | ✅ | ⚠️ download at best | ✅ **upload + download** |
|
|
43
46
|
| Tag conventions enforced in code | ❌ | ❌ | ✅ optional `conventions.toml` |
|
|
44
47
|
|
|
45
48
|
## Install
|
|
46
49
|
|
|
50
|
+
Pure Python (3.11+), no platform-specific bits — the same package works on macOS,
|
|
51
|
+
Windows, and Linux:
|
|
52
|
+
|
|
47
53
|
```bash
|
|
48
54
|
pipx install zotkit # or: uv tool install zotkit / pip install zotkit
|
|
49
55
|
uvx zotkit --help # …or try it without installing anything
|
|
@@ -60,6 +66,9 @@ Copy [`.env.example`](.env.example) to `./.env`, `~/.config/zotkit/env`, or any
|
|
|
60
66
|
- **WebDAV** (only for `attach`/`fetch`): copy the exact values from the Zotero desktop
|
|
61
67
|
app on any of your machines — **Settings → Sync → File Syncing** — and append
|
|
62
68
|
`/zotero/` to the URL (the desktop does this implicitly).
|
|
69
|
+
- **Using Zotero Storage instead of WebDAV?** Just leave the `WEBDAV_*` lines out —
|
|
70
|
+
`attach`/`fetch` automatically use Zotero Storage through the Web API's upload/download
|
|
71
|
+
endpoints instead. The storage mode is detected from your `.env`, nothing to configure.
|
|
63
72
|
|
|
64
73
|
Optionally, copy [`conventions.example.toml`](conventions.example.toml) to
|
|
65
74
|
`conventions.toml` next to your `.env` to define a namespaced tag taxonomy
|
|
@@ -114,6 +123,22 @@ z.backup()
|
|
|
114
123
|
`z.z` is the underlying [pyzotero](https://github.com/urschrei/pyzotero) client for
|
|
115
124
|
anything not wrapped.
|
|
116
125
|
|
|
126
|
+
## Using zotkit with AI agents
|
|
127
|
+
|
|
128
|
+
zotkit is designed to be driven by coding agents (Claude Code and similar): dry-run
|
|
129
|
+
defaults, code-enforced tag conventions, and a ready-made **Claude Code skill** in
|
|
130
|
+
[`skills/zotkit/`](skills/zotkit/SKILL.md) — copy it to `~/.claude/skills/zotkit/` and
|
|
131
|
+
any Claude session can search, file, and attach papers for you while respecting your
|
|
132
|
+
taxonomy. (An MCP server is planned.)
|
|
133
|
+
|
|
134
|
+
Want to clean up a messy library, not just maintain one? The battle-tested method —
|
|
135
|
+
taxonomy design, parallel read-only analysis, serial reviewed writes — is written up in
|
|
136
|
+
[`docs/organizing-with-agents.md`](docs/organizing-with-agents.md).
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
mkdir -p ~/.claude/skills && cp -r skills/zotkit ~/.claude/skills/
|
|
140
|
+
```
|
|
141
|
+
|
|
117
142
|
## Safety model
|
|
118
143
|
|
|
119
144
|
- `create` is **dry-run by default**; `--apply` to execute.
|
|
@@ -125,11 +150,12 @@ anything not wrapped.
|
|
|
125
150
|
|
|
126
151
|
## How WebDAV attachments work
|
|
127
152
|
|
|
128
|
-
Zotero
|
|
129
|
-
|
|
130
|
-
attachment item
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
(With Zotero Storage, zotkit simply uses the Web API's official file endpoints — this
|
|
154
|
+
section is about the WebDAV mode.) Zotero's WebDAV storage format is undocumented but
|
|
155
|
+
simple: each attachment item `K` is stored as `K.zip` (the file, zipped) plus `K.prop`
|
|
156
|
+
(its md5 + mtime). zotkit creates the attachment item via the Web API and PUTs both
|
|
157
|
+
objects directly — after which every desktop client syncs the file down normally.
|
|
158
|
+
Details in [`docs/webdav-format.md`](docs/webdav-format.md).
|
|
133
159
|
|
|
134
160
|
The format was determined by interoperability inspection of the author's own library.
|
|
135
161
|
This project is not affiliated with or endorsed by Zotero.
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
**Headless Zotero library management — no desktop app required.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
"Headless" simply means zotkit never needs the Zotero app (or any window) open: it is a
|
|
6
|
+
Python library + CLI that talks straight to the
|
|
7
|
+
[Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start), so you can
|
|
8
|
+
search, create, tag, and organize items from any terminal — macOS, Windows, or Linux,
|
|
9
|
+
your laptop or a remote server. Uniquely, if your attachments sync to a **personal
|
|
10
|
+
WebDAV server**, zotkit can **upload and download the files themselves** by speaking
|
|
11
|
+
Zotero's WebDAV storage format directly — the one capability the Web API does not
|
|
12
|
+
provide, and (as far as we know) not offered by any other headless tool.
|
|
11
13
|
|
|
12
14
|
Built for servers, scripts, and **LLM agents**: every write is dry-run by default,
|
|
13
15
|
batched, and version-checked, and you can define a tag taxonomy that is *enforced in
|
|
@@ -19,11 +21,15 @@ code* so an agent (or a tired human) can't pollute your library with inconsisten
|
|
|
19
21
|
|---|---|---|---|
|
|
20
22
|
| Works headless (server, SSH, CI) | ❌ | ✅ read-mostly | ✅ |
|
|
21
23
|
| Write items/tags/collections | ✅ | ⚠️ usually needs the desktop app running | ✅ |
|
|
24
|
+
| Attachment files (Zotero Storage) | ✅ | ⚠️ some | ✅ upload + download |
|
|
22
25
|
| Attachment files on **WebDAV** | ✅ | ⚠️ download at best | ✅ **upload + download** |
|
|
23
26
|
| Tag conventions enforced in code | ❌ | ❌ | ✅ optional `conventions.toml` |
|
|
24
27
|
|
|
25
28
|
## Install
|
|
26
29
|
|
|
30
|
+
Pure Python (3.11+), no platform-specific bits — the same package works on macOS,
|
|
31
|
+
Windows, and Linux:
|
|
32
|
+
|
|
27
33
|
```bash
|
|
28
34
|
pipx install zotkit # or: uv tool install zotkit / pip install zotkit
|
|
29
35
|
uvx zotkit --help # …or try it without installing anything
|
|
@@ -40,6 +46,9 @@ Copy [`.env.example`](.env.example) to `./.env`, `~/.config/zotkit/env`, or any
|
|
|
40
46
|
- **WebDAV** (only for `attach`/`fetch`): copy the exact values from the Zotero desktop
|
|
41
47
|
app on any of your machines — **Settings → Sync → File Syncing** — and append
|
|
42
48
|
`/zotero/` to the URL (the desktop does this implicitly).
|
|
49
|
+
- **Using Zotero Storage instead of WebDAV?** Just leave the `WEBDAV_*` lines out —
|
|
50
|
+
`attach`/`fetch` automatically use Zotero Storage through the Web API's upload/download
|
|
51
|
+
endpoints instead. The storage mode is detected from your `.env`, nothing to configure.
|
|
43
52
|
|
|
44
53
|
Optionally, copy [`conventions.example.toml`](conventions.example.toml) to
|
|
45
54
|
`conventions.toml` next to your `.env` to define a namespaced tag taxonomy
|
|
@@ -94,6 +103,22 @@ z.backup()
|
|
|
94
103
|
`z.z` is the underlying [pyzotero](https://github.com/urschrei/pyzotero) client for
|
|
95
104
|
anything not wrapped.
|
|
96
105
|
|
|
106
|
+
## Using zotkit with AI agents
|
|
107
|
+
|
|
108
|
+
zotkit is designed to be driven by coding agents (Claude Code and similar): dry-run
|
|
109
|
+
defaults, code-enforced tag conventions, and a ready-made **Claude Code skill** in
|
|
110
|
+
[`skills/zotkit/`](skills/zotkit/SKILL.md) — copy it to `~/.claude/skills/zotkit/` and
|
|
111
|
+
any Claude session can search, file, and attach papers for you while respecting your
|
|
112
|
+
taxonomy. (An MCP server is planned.)
|
|
113
|
+
|
|
114
|
+
Want to clean up a messy library, not just maintain one? The battle-tested method —
|
|
115
|
+
taxonomy design, parallel read-only analysis, serial reviewed writes — is written up in
|
|
116
|
+
[`docs/organizing-with-agents.md`](docs/organizing-with-agents.md).
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
mkdir -p ~/.claude/skills && cp -r skills/zotkit ~/.claude/skills/
|
|
120
|
+
```
|
|
121
|
+
|
|
97
122
|
## Safety model
|
|
98
123
|
|
|
99
124
|
- `create` is **dry-run by default**; `--apply` to execute.
|
|
@@ -105,11 +130,12 @@ anything not wrapped.
|
|
|
105
130
|
|
|
106
131
|
## How WebDAV attachments work
|
|
107
132
|
|
|
108
|
-
Zotero
|
|
109
|
-
|
|
110
|
-
attachment item
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
(With Zotero Storage, zotkit simply uses the Web API's official file endpoints — this
|
|
134
|
+
section is about the WebDAV mode.) Zotero's WebDAV storage format is undocumented but
|
|
135
|
+
simple: each attachment item `K` is stored as `K.zip` (the file, zipped) plus `K.prop`
|
|
136
|
+
(its md5 + mtime). zotkit creates the attachment item via the Web API and PUTs both
|
|
137
|
+
objects directly — after which every desktop client syncs the file down normally.
|
|
138
|
+
Details in [`docs/webdav-format.md`](docs/webdav-format.md).
|
|
113
139
|
|
|
114
140
|
The format was determined by interoperability inspection of the author's own library.
|
|
115
141
|
This project is not affiliated with or endorsed by Zotero.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "zotkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Headless Zotero library management: Web API CRUD plus direct WebDAV attachment upload/download — no desktop app required"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -76,7 +76,7 @@ def main(argv=None):
|
|
|
76
76
|
_print_items(zot.find(a.title, a.tag, a.collection))
|
|
77
77
|
|
|
78
78
|
elif a.cmd == "create":
|
|
79
|
-
data = json.load(open(a.file))
|
|
79
|
+
data = json.load(open(a.file, encoding="utf-8"))
|
|
80
80
|
items = data["items"] if isinstance(data, dict) else data
|
|
81
81
|
if not a.apply:
|
|
82
82
|
for d in items:
|
|
@@ -89,7 +89,7 @@ def main(argv=None):
|
|
|
89
89
|
created = zot.create_items(items, dedup=not a.no_dedup, strict_tags=not a.loose_tags)
|
|
90
90
|
new = [c for c in created if c.get("key")]
|
|
91
91
|
out = Path(a.file).with_suffix(".created.json")
|
|
92
|
-
json.dump(new, open(out, "w"), ensure_ascii=False, indent=2)
|
|
92
|
+
json.dump(new, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
|
93
93
|
print(f"created {len(new)} (skipped {len(created)-len(new)} dup) -> {out}")
|
|
94
94
|
if any(c.get("file_path") for c in new):
|
|
95
95
|
print(f"attach PDFs: zotkit attach --from {out} --all")
|
|
@@ -98,7 +98,7 @@ def main(argv=None):
|
|
|
98
98
|
if a.key and a.pdf:
|
|
99
99
|
print(zot.attach(a.key, a.pdf))
|
|
100
100
|
elif a.src:
|
|
101
|
-
rows = json.load(open(a.src))
|
|
101
|
+
rows = json.load(open(a.src, encoding="utf-8"))
|
|
102
102
|
done = 0
|
|
103
103
|
for m in rows:
|
|
104
104
|
if not m.get("file_path") or not Path(m["file_path"]).exists():
|
|
@@ -50,7 +50,7 @@ def load_env(path: str | os.PathLike | None = None) -> dict[str, str]:
|
|
|
50
50
|
if not p.is_file():
|
|
51
51
|
continue
|
|
52
52
|
env: dict[str, str] = {}
|
|
53
|
-
for line in p.read_text().splitlines():
|
|
53
|
+
for line in p.read_text(encoding="utf-8").splitlines():
|
|
54
54
|
line = line.strip()
|
|
55
55
|
if line and not line.startswith("#") and "=" in line:
|
|
56
56
|
k, v = line.split("=", 1)
|
|
@@ -97,7 +97,7 @@ def load_conventions(env_dir: str | os.PathLike | None = None) -> Conventions |
|
|
|
97
97
|
cands.append(Path.home() / ".config" / "zotkit" / "conventions.toml")
|
|
98
98
|
for p in cands:
|
|
99
99
|
if p.is_file():
|
|
100
|
-
return Conventions(tomllib.loads(p.read_text()))
|
|
100
|
+
return Conventions(tomllib.loads(p.read_text(encoding="utf-8")))
|
|
101
101
|
return None
|
|
102
102
|
|
|
103
103
|
|
|
@@ -305,8 +305,19 @@ class Zot:
|
|
|
305
305
|
"need WEBDAV_URL/WEBDAV_USER/WEBDAV_PASS") from None
|
|
306
306
|
|
|
307
307
|
def attach(self, item_key: str, file_path: str | os.PathLike) -> dict:
|
|
308
|
-
"""Attach a local file to an item
|
|
308
|
+
"""Attach a local file to an item. Bytes go to WebDAV (<key>.zip + .prop)
|
|
309
|
+
when WEBDAV_* is configured, otherwise to Zotero Storage via the Web API."""
|
|
309
310
|
f = Path(file_path)
|
|
311
|
+
if "WEBDAV_URL" not in self.env:
|
|
312
|
+
resp = self.z.attachment_simple([str(f)], item_key)
|
|
313
|
+
done = (resp.get("success") or []) + (resp.get("unchanged") or [])
|
|
314
|
+
if not done:
|
|
315
|
+
raise RuntimeError(f"Zotero Storage upload failed: {resp}")
|
|
316
|
+
entry = done[0]
|
|
317
|
+
key = ""
|
|
318
|
+
if isinstance(entry, dict):
|
|
319
|
+
key = entry.get("key") or entry.get("data", {}).get("key", "")
|
|
320
|
+
return {"attachment_key": key, "filename": f.name, "storage": "zotero"}
|
|
310
321
|
filename, md5 = f.name, _md5_file(f)
|
|
311
322
|
mtime = int(os.path.getmtime(f) * 1000)
|
|
312
323
|
ctype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
|
@@ -329,28 +340,36 @@ class Zot:
|
|
|
329
340
|
ok = r1.status_code in (200, 201, 204) and r2.status_code in (200, 201, 204)
|
|
330
341
|
if not ok:
|
|
331
342
|
raise RuntimeError(f"WebDAV PUT failed: zip={r1.status_code} prop={r2.status_code}")
|
|
332
|
-
return {"attachment_key": key, "filename": filename, "
|
|
343
|
+
return {"attachment_key": key, "filename": filename, "storage": "webdav"}
|
|
333
344
|
|
|
334
345
|
def has_attachment(self, item_key: str) -> bool:
|
|
335
346
|
return any(k["data"].get("itemType") == "attachment"
|
|
336
347
|
for k in self.z.children(item_key))
|
|
337
348
|
|
|
338
349
|
def fetch(self, item_key: str, out_dir: str | os.PathLike = "downloads") -> list[Path]:
|
|
339
|
-
"""Download an item's attachment files from WebDAV
|
|
350
|
+
"""Download an item's attachment files (from WebDAV when configured,
|
|
351
|
+
otherwise from Zotero Storage); returns saved paths."""
|
|
340
352
|
out = Path(out_dir)
|
|
341
353
|
out.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
saved: list[Path] = []
|
|
355
|
+
atts = [ch for ch in self.z.children(item_key)
|
|
356
|
+
if ch["data"].get("itemType") == "attachment" and ch["data"].get("filename")]
|
|
357
|
+
if "WEBDAV_URL" not in self.env:
|
|
358
|
+
for ch in atts:
|
|
359
|
+
try:
|
|
360
|
+
self.z.dump(ch["key"], ch["data"]["filename"], str(out))
|
|
361
|
+
saved.append(out / ch["data"]["filename"])
|
|
362
|
+
except Exception:
|
|
363
|
+
continue
|
|
364
|
+
return saved
|
|
342
365
|
base, auth = self._webdav()
|
|
343
|
-
saved = []
|
|
344
366
|
with httpx.Client(auth=auth, timeout=120) as c:
|
|
345
|
-
for ch in
|
|
346
|
-
d = ch["data"]
|
|
347
|
-
if d.get("itemType") != "attachment" or not d.get("filename"):
|
|
348
|
-
continue
|
|
367
|
+
for ch in atts:
|
|
349
368
|
r = c.get(f"{base}/{ch['key']}.zip")
|
|
350
369
|
if r.status_code != 200:
|
|
351
370
|
continue
|
|
352
371
|
zipfile.ZipFile(io.BytesIO(r.content)).extractall(out)
|
|
353
|
-
saved.append(out /
|
|
372
|
+
saved.append(out / ch["data"]["filename"])
|
|
354
373
|
return saved
|
|
355
374
|
|
|
356
375
|
# ---------- backup ----------
|
|
@@ -375,5 +394,5 @@ class Zot:
|
|
|
375
394
|
out = Path(out_dir or (self.home / "backups"))
|
|
376
395
|
out.mkdir(parents=True, exist_ok=True)
|
|
377
396
|
p = out / f"zotero_backup_v{version}.json"
|
|
378
|
-
p.write_text(json.dumps(backup, ensure_ascii=False, indent=2))
|
|
397
|
+
p.write_text(json.dumps(backup, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
379
398
|
return p
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zotkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Headless Zotero library management: Web API CRUD plus direct WebDAV attachment upload/download — no desktop app required
|
|
5
5
|
Author: Shawn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,12 +22,14 @@ Dynamic: license-file
|
|
|
22
22
|
|
|
23
23
|
**Headless Zotero library management — no desktop app required.**
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
"Headless" simply means zotkit never needs the Zotero app (or any window) open: it is a
|
|
26
|
+
Python library + CLI that talks straight to the
|
|
27
|
+
[Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start), so you can
|
|
28
|
+
search, create, tag, and organize items from any terminal — macOS, Windows, or Linux,
|
|
29
|
+
your laptop or a remote server. Uniquely, if your attachments sync to a **personal
|
|
30
|
+
WebDAV server**, zotkit can **upload and download the files themselves** by speaking
|
|
31
|
+
Zotero's WebDAV storage format directly — the one capability the Web API does not
|
|
32
|
+
provide, and (as far as we know) not offered by any other headless tool.
|
|
31
33
|
|
|
32
34
|
Built for servers, scripts, and **LLM agents**: every write is dry-run by default,
|
|
33
35
|
batched, and version-checked, and you can define a tag taxonomy that is *enforced in
|
|
@@ -39,11 +41,15 @@ code* so an agent (or a tired human) can't pollute your library with inconsisten
|
|
|
39
41
|
|---|---|---|---|
|
|
40
42
|
| Works headless (server, SSH, CI) | ❌ | ✅ read-mostly | ✅ |
|
|
41
43
|
| Write items/tags/collections | ✅ | ⚠️ usually needs the desktop app running | ✅ |
|
|
44
|
+
| Attachment files (Zotero Storage) | ✅ | ⚠️ some | ✅ upload + download |
|
|
42
45
|
| Attachment files on **WebDAV** | ✅ | ⚠️ download at best | ✅ **upload + download** |
|
|
43
46
|
| Tag conventions enforced in code | ❌ | ❌ | ✅ optional `conventions.toml` |
|
|
44
47
|
|
|
45
48
|
## Install
|
|
46
49
|
|
|
50
|
+
Pure Python (3.11+), no platform-specific bits — the same package works on macOS,
|
|
51
|
+
Windows, and Linux:
|
|
52
|
+
|
|
47
53
|
```bash
|
|
48
54
|
pipx install zotkit # or: uv tool install zotkit / pip install zotkit
|
|
49
55
|
uvx zotkit --help # …or try it without installing anything
|
|
@@ -60,6 +66,9 @@ Copy [`.env.example`](.env.example) to `./.env`, `~/.config/zotkit/env`, or any
|
|
|
60
66
|
- **WebDAV** (only for `attach`/`fetch`): copy the exact values from the Zotero desktop
|
|
61
67
|
app on any of your machines — **Settings → Sync → File Syncing** — and append
|
|
62
68
|
`/zotero/` to the URL (the desktop does this implicitly).
|
|
69
|
+
- **Using Zotero Storage instead of WebDAV?** Just leave the `WEBDAV_*` lines out —
|
|
70
|
+
`attach`/`fetch` automatically use Zotero Storage through the Web API's upload/download
|
|
71
|
+
endpoints instead. The storage mode is detected from your `.env`, nothing to configure.
|
|
63
72
|
|
|
64
73
|
Optionally, copy [`conventions.example.toml`](conventions.example.toml) to
|
|
65
74
|
`conventions.toml` next to your `.env` to define a namespaced tag taxonomy
|
|
@@ -114,6 +123,22 @@ z.backup()
|
|
|
114
123
|
`z.z` is the underlying [pyzotero](https://github.com/urschrei/pyzotero) client for
|
|
115
124
|
anything not wrapped.
|
|
116
125
|
|
|
126
|
+
## Using zotkit with AI agents
|
|
127
|
+
|
|
128
|
+
zotkit is designed to be driven by coding agents (Claude Code and similar): dry-run
|
|
129
|
+
defaults, code-enforced tag conventions, and a ready-made **Claude Code skill** in
|
|
130
|
+
[`skills/zotkit/`](skills/zotkit/SKILL.md) — copy it to `~/.claude/skills/zotkit/` and
|
|
131
|
+
any Claude session can search, file, and attach papers for you while respecting your
|
|
132
|
+
taxonomy. (An MCP server is planned.)
|
|
133
|
+
|
|
134
|
+
Want to clean up a messy library, not just maintain one? The battle-tested method —
|
|
135
|
+
taxonomy design, parallel read-only analysis, serial reviewed writes — is written up in
|
|
136
|
+
[`docs/organizing-with-agents.md`](docs/organizing-with-agents.md).
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
mkdir -p ~/.claude/skills && cp -r skills/zotkit ~/.claude/skills/
|
|
140
|
+
```
|
|
141
|
+
|
|
117
142
|
## Safety model
|
|
118
143
|
|
|
119
144
|
- `create` is **dry-run by default**; `--apply` to execute.
|
|
@@ -125,11 +150,12 @@ anything not wrapped.
|
|
|
125
150
|
|
|
126
151
|
## How WebDAV attachments work
|
|
127
152
|
|
|
128
|
-
Zotero
|
|
129
|
-
|
|
130
|
-
attachment item
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
(With Zotero Storage, zotkit simply uses the Web API's official file endpoints — this
|
|
154
|
+
section is about the WebDAV mode.) Zotero's WebDAV storage format is undocumented but
|
|
155
|
+
simple: each attachment item `K` is stored as `K.zip` (the file, zipped) plus `K.prop`
|
|
156
|
+
(its md5 + mtime). zotkit creates the attachment item via the Web API and PUTs both
|
|
157
|
+
objects directly — after which every desktop client syncs the file down normally.
|
|
158
|
+
Details in [`docs/webdav-format.md`](docs/webdav-format.md).
|
|
133
159
|
|
|
134
160
|
The format was determined by interoperability inspection of the author's own library.
|
|
135
161
|
This project is not affiliated with or endorsed by Zotero.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|