@chronicle.app/zotero 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Louis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @chronicle.app/zotero
2
+
3
+ Chronicle plugin for a local [Zotero](https://www.zotero.org/) library: works
4
+ (references), the highlights and notes made on them, and standalone
5
+ attachments filed without a parent work.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @chronicle.app/zotero
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Basic extraction
16
+
17
+ ```bash
18
+ chronicle extract zotero
19
+ ```
20
+
21
+ By default the plugin reads `~/Zotero`, Zotero's default data directory. Point
22
+ it elsewhere with `--input`, either the data directory itself or a path
23
+ directly to `zotero.sqlite`:
24
+
25
+ ```bash
26
+ chronicle extract zotero --input /path/to/Zotero
27
+ chronicle extract zotero --input /path/to/Zotero/zotero.sqlite
28
+ ```
29
+
30
+ ### With date filtering
31
+
32
+ ```bash
33
+ chronicle extract zotero \
34
+ --input /path/to/Zotero \
35
+ --since 2024-01-01 \
36
+ --until 2024-12-31 \
37
+ --limit 1000
38
+ ```
39
+
40
+ `--since`/`--until` filter on each item's own `dateModified`, so a filtered run
41
+ re-reports whatever changed (or was added) in that window, including edits to
42
+ older items.
43
+
44
+ ## Configuration
45
+
46
+ The extractor supports the following options:
47
+
48
+ - `--input`: Path to the Zotero data directory, or directly to `zotero.sqlite`.
49
+ Defaults to `~/Zotero`.
50
+ - `--since`: Extract records modified from this date onwards
51
+ - `--until`: Extract records modified up to this date
52
+ - `--limit`: Maximum number of records to extract, across all record types
53
+
54
+ ## Source
55
+
56
+ The plugin opens `zotero.sqlite` read-only — it never writes to the library,
57
+ and never needs Zotero to be closed. It reads the **user library only**; group
58
+ libraries are out of scope. Trashed items (and anything whose parent is
59
+ trashed) are excluded throughout.
60
+
61
+ If Zotero is running, its exclusive lock on the database can make that
62
+ readonly open fail with `SQLITE_BUSY`; the extractor falls back to reading a
63
+ disposable filesystem-level clone of the (closed-journal, so consistent)
64
+ database file instead.
65
+
66
+ Every record carries the library's `localUserKey` (`settings.account`), stable
67
+ even for a library that has never synced to zotero.org.
68
+
69
+ ## What it extracts
70
+
71
+ - **Works** (`recordType: work`) — top-level reference items (journal
72
+ articles, books, book sections, preprints, …), each with its bibliographic
73
+ fields (title, date, publisher, DOI, ISBN, …), creators, tag list, the
74
+ collections it's filed under (with parent chains resolved), and its child
75
+ attachments (with storage paths resolved to a file on disk where possible).
76
+ - **Annotations** (`recordType: annotation`) — highlights, underlines, notes,
77
+ and other markup made in the Zotero reader (PDF or EPUB), with the
78
+ annotated attachment and its parent work carried as context.
79
+ - **Attachments** (`recordType: attachment`) — standalone files saved
80
+ directly to the library with no parent work, including their own tags and
81
+ collection memberships.
82
+ - **Notes** (`recordType: note`) — Zotero note items, written on a work or
83
+ standing alone, with the parent work carried as context and the HTML body
84
+ passed through verbatim.
85
+
86
+ ## Schema
87
+
88
+ The transformer maps these records onto Chronicle's schema: the catalog item
89
+ is the abstract work (`Book`, `Article`, `Post`, `VideoObject`, … by Zotero
90
+ item type, keyed by its item key so a re-typing refines in place), saved via a
91
+ `BookmarkAction` and carrying protocol `sameAs` edges (DOI with its resolver
92
+ url, ISBN, arXiv, PMID/PMCID — a bookSection's ISBN lands on its containing
93
+ book), creators, collections, and tags. Item keys are only per-library
94
+ unique, so every item-keyed identity is scoped by the library as an `inRealm`
95
+ Realm (the zotero.org userID when synced, else the local user key). A child
96
+ attachment is not a separate entity: it merges into its work via `sameAs` as
97
+ a sparse facet (mimeType, contentPath, its filesystem and content
98
+ identities — no name), entering the log through reading — each last-read
99
+ timestamp is a one-shot `ViewAction` (Zotero has no completion signal, so no
100
+ reading run is opened) whose object is the file facet, resolving to the
101
+ work. Annotations anchor to the facet too, so which copy a selector is valid
102
+ in survives beneath the merge. A carrier shared by two different works (one
103
+ anthology PDF under several items) withholds its merge edges so distinct
104
+ works never weld through a shared file; standalone attachments stay their
105
+ own entities. The file also
106
+ carries a filesystem-keyed `sameAs` (path scoped by machine, the timing/shell
107
+ convention), so app-tracked sessions on the same file fold onto it, and
108
+ `snapshotOf` the url hub for webpage snapshots. A highlight is a
109
+ `QuoteAction` → `Quotation` carrying a `PageSelector` (PDF) or
110
+ `EpubCfiSelector` (EPUB), and a note is an `AnnotateAction` → `Comment`
111
+ about the quotation.
112
+
113
+ ## Tests
114
+
115
+ The tests build a synthetic Zotero data directory in a temporary folder: a
116
+ `zotero.sqlite` with only the tables the extractor reads, and a few files
117
+ under `storage/`. They stub the machine name, so they never read the host's
118
+ Zotero library, hostname, or accounts.
@@ -0,0 +1,73 @@
1
+ import { Record } from '@chronicle.app/etl';
2
+ import { SqliteExtractor } from '@chronicle.app/etl-sqlite';
3
+ import ZoteroTransformer from './ZoteroTransformer.js';
4
+ /**
5
+ * A local Zotero library — works (references), the highlights and notes made
6
+ * on their attachments, standalone attachments filed without a parent work,
7
+ * and note items written on a work or standing alone. Reads `zotero.sqlite`
8
+ * directly; the user library only (group libraries are out of scope),
9
+ * excluding anything in the trash.
10
+ */
11
+ export declare class ZoteroExtractor extends SqliteExtractor<typeof ZoteroExtractor> {
12
+ static source: string;
13
+ static description: string;
14
+ static delivery: "local";
15
+ static strategy: string;
16
+ static recordTypes: string[];
17
+ static default: boolean;
18
+ static defaultTransformer: typeof ZoteroTransformer;
19
+ static temporality: "snapshot";
20
+ static schema: any;
21
+ private dataDir;
22
+ private localUserKey;
23
+ private library;
24
+ private machine;
25
+ private dbMtime;
26
+ private extractedCount;
27
+ /** Set when Zotero's lock forced a filesystem-level clone; cleaned up in teardown(). */
28
+ private tempDbPath?;
29
+ /** Item keys of child attachments whose carrier (path or content hash) is shared with a different parent work; rebuilt per extract() run. */
30
+ private flaggedAttachmentKeys;
31
+ /**
32
+ * The item's 8-char Zotero key. Every record type (work, annotation,
33
+ * attachment, note) is an `items` row, and the key is unique across all of
34
+ * them within a library — the same field each transformer keyset uses.
35
+ */
36
+ keyOf(record: Record): string | null;
37
+ setup(): Promise<void>;
38
+ private readLocalUserKey;
39
+ private readUserID;
40
+ teardown(): Promise<void>;
41
+ protected asOfTime(): Date;
42
+ extract(): AsyncGenerator<Record>;
43
+ /** The since/until window over a `dateModified` column, as ` AND …` terms appended to each stream's WHERE. */
44
+ private sinceUntilFilter;
45
+ private iterateWorks;
46
+ private iterateAnnotations;
47
+ private iterateOrphanAttachments;
48
+ private iterateNotes;
49
+ private buildAttachment;
50
+ /**
51
+ * Item keys of child attachments whose carrier — resolved path, or file
52
+ * content hash — also appears under a DIFFERENT parent work. Built from
53
+ * every non-deleted attachment in the user library (children and orphans),
54
+ * but only children contribute to (and can be flagged by) the map: an
55
+ * orphan has no parent to weld two works through.
56
+ */
57
+ private buildFlaggedAttachmentKeys;
58
+ /** The resolved path, plus a content-hash key when the file is readable — the two ways two attachments can carry the same bytes. */
59
+ private carrierKeys;
60
+ private hashFile;
61
+ /** Storage folders are named by the attachment's own key, not its filename. */
62
+ private resolveAttachmentPath;
63
+ private loadWorkRef;
64
+ private loadWorkAttachments;
65
+ private loadFields;
66
+ private loadCreators;
67
+ private loadTags;
68
+ private loadCollections;
69
+ private buildCollectionChain;
70
+ private getItemCollections;
71
+ determineCount(): Promise<number | null>;
72
+ }
73
+ //# sourceMappingURL=ZoteroExtractor.d.ts.map